mirror of
https://github.com/documenso/documenso.git
synced 2026-08-25 07:42:28 +10:00
feat: add bulk document selection and move functionality (#2387)
This PR introduces bulk actions for documents, allowing users to select multiple envelopes and perform actions such as moving or deleting 1 or more documents simultaneously.
This commit is contained in:
@@ -16,9 +16,12 @@ import { trpc } from '@documenso/trpc/react';
|
||||
import type { TFindDocumentsInternalResponse } from '@documenso/trpc/server/document-router/find-documents-internal.types';
|
||||
import { ZFindDocumentsInternalRequestSchema } from '@documenso/trpc/server/document-router/find-documents-internal.types';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import type { RowSelectionState } from '@documenso/ui/primitives/data-table';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
import { DocumentMoveToFolderDialog } from '~/components/dialogs/document-move-to-folder-dialog';
|
||||
import { EnvelopesBulkDeleteDialog } from '~/components/dialogs/envelopes-bulk-delete-dialog';
|
||||
import { EnvelopesBulkMoveDialog } from '~/components/dialogs/envelopes-bulk-move-dialog';
|
||||
import { DocumentSearch } from '~/components/general/document/document-search';
|
||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||
import { EnvelopeDropZoneWrapper } from '~/components/general/envelope/envelope-drop-zone-wrapper';
|
||||
@@ -27,6 +30,7 @@ import { PeriodSelector } from '~/components/general/period-selector';
|
||||
import { DocumentsTable } from '~/components/tables/documents-table';
|
||||
import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
|
||||
import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter';
|
||||
import { EnvelopesTableBulkActionBar } from '~/components/tables/envelopes-table-bulk-action-bar';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
|
||||
@@ -54,6 +58,14 @@ export default function DocumentsPage() {
|
||||
const [isMovingDocument, setIsMovingDocument] = useState(false);
|
||||
const [documentToMove, setDocumentToMove] = useState<number | null>(null);
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [isBulkMoveDialogOpen, setIsBulkMoveDialogOpen] = useState(false);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
|
||||
const selectedEnvelopeIds = useMemo(() => {
|
||||
return Object.keys(rowSelection).filter((id) => rowSelection[id]);
|
||||
}, [rowSelection]);
|
||||
|
||||
const [stats, setStats] = useState<TFindDocumentsInternalResponse['stats']>({
|
||||
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||
[ExtendedDocumentStatus.PENDING]: 0,
|
||||
@@ -109,6 +121,11 @@ export default function DocumentsPage() {
|
||||
}
|
||||
}, [data?.stats]);
|
||||
|
||||
// Clear selection when navigation or filters change
|
||||
useEffect(() => {
|
||||
setRowSelection({});
|
||||
}, [folderId, findDocumentSearchParams]);
|
||||
|
||||
return (
|
||||
<EnvelopeDropZoneWrapper type={EnvelopeType.DOCUMENT}>
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
@@ -116,9 +133,9 @@ export default function DocumentsPage() {
|
||||
|
||||
<div className="mt-8 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
|
||||
<div className="flex flex-row items-center">
|
||||
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
|
||||
<Avatar className="mr-3 h-12 w-12 border-2 border-solid border-white dark:border-border">
|
||||
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
|
||||
<AvatarFallback className="text-muted-foreground text-xs">
|
||||
<AvatarFallback className="text-xs text-muted-foreground">
|
||||
{team.name.slice(0, 1)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -148,7 +165,7 @@ export default function DocumentsPage() {
|
||||
.map((value) => (
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
className="hover:text-foreground min-w-[60px]"
|
||||
className="min-w-[60px] hover:text-foreground"
|
||||
value={value}
|
||||
asChild
|
||||
>
|
||||
@@ -190,6 +207,9 @@ export default function DocumentsPage() {
|
||||
setDocumentToMove(documentId);
|
||||
setIsMovingDocument(true);
|
||||
}}
|
||||
enableSelection
|
||||
rowSelection={rowSelection}
|
||||
onRowSelectionChange={setRowSelection}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -209,6 +229,30 @@ export default function DocumentsPage() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EnvelopesTableBulkActionBar
|
||||
selectedCount={selectedEnvelopeIds.length}
|
||||
onMoveClick={() => setIsBulkMoveDialogOpen(true)}
|
||||
onDeleteClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||
onClearSelection={() => setRowSelection({})}
|
||||
/>
|
||||
|
||||
<EnvelopesBulkMoveDialog
|
||||
envelopeIds={selectedEnvelopeIds}
|
||||
envelopeType={EnvelopeType.DOCUMENT}
|
||||
open={isBulkMoveDialogOpen}
|
||||
currentFolderId={folderId}
|
||||
onOpenChange={setIsBulkMoveDialogOpen}
|
||||
onSuccess={() => setRowSelection({})}
|
||||
/>
|
||||
|
||||
<EnvelopesBulkDeleteDialog
|
||||
envelopeIds={selectedEnvelopeIds}
|
||||
envelopeType={EnvelopeType.DOCUMENT}
|
||||
open={isBulkDeleteDialogOpen}
|
||||
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||
onSuccess={() => setRowSelection({})}
|
||||
/>
|
||||
</div>
|
||||
</EnvelopeDropZoneWrapper>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { Bird } from 'lucide-react';
|
||||
@@ -8,9 +10,13 @@ import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import type { RowSelectionState } from '@documenso/ui/primitives/data-table';
|
||||
|
||||
import { EnvelopesBulkDeleteDialog } from '~/components/dialogs/envelopes-bulk-delete-dialog';
|
||||
import { EnvelopesBulkMoveDialog } from '~/components/dialogs/envelopes-bulk-move-dialog';
|
||||
import { EnvelopeDropZoneWrapper } from '~/components/general/envelope/envelope-drop-zone-wrapper';
|
||||
import { FolderGrid } from '~/components/general/folder/folder-grid';
|
||||
import { EnvelopesTableBulkActionBar } from '~/components/tables/envelopes-table-bulk-action-bar';
|
||||
import { TemplatesTable } from '~/components/tables/templates-table';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
@@ -28,6 +34,14 @@ export default function TemplatesPage() {
|
||||
const page = Number(searchParams.get('page')) || 1;
|
||||
const perPage = Number(searchParams.get('perPage')) || 10;
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [isBulkMoveDialogOpen, setIsBulkMoveDialogOpen] = useState(false);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
|
||||
const selectedEnvelopeIds = useMemo(() => {
|
||||
return Object.keys(rowSelection).filter((id) => rowSelection[id]);
|
||||
}, [rowSelection]);
|
||||
|
||||
const documentRootPath = formatDocumentsPath(team.url);
|
||||
const templateRootPath = formatTemplatesPath(team.url);
|
||||
|
||||
@@ -37,6 +51,11 @@ export default function TemplatesPage() {
|
||||
folderId,
|
||||
});
|
||||
|
||||
// Clear selection when navigation or filters change
|
||||
useEffect(() => {
|
||||
setRowSelection({});
|
||||
}, [folderId, page, perPage]);
|
||||
|
||||
return (
|
||||
<EnvelopeDropZoneWrapper type={EnvelopeType.TEMPLATE}>
|
||||
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
||||
@@ -44,9 +63,9 @@ export default function TemplatesPage() {
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-row items-center">
|
||||
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
|
||||
<Avatar className="mr-3 h-12 w-12 border-2 border-solid border-white dark:border-border">
|
||||
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
|
||||
<AvatarFallback className="text-muted-foreground text-xs">
|
||||
<AvatarFallback className="text-xs text-muted-foreground">
|
||||
{team.name.slice(0, 1)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -58,7 +77,7 @@ export default function TemplatesPage() {
|
||||
|
||||
<div className="mt-8">
|
||||
{data && data.count === 0 ? (
|
||||
<div className="text-muted-foreground/60 flex h-96 flex-col items-center justify-center gap-y-4">
|
||||
<div className="flex h-96 flex-col items-center justify-center gap-y-4 text-muted-foreground/60">
|
||||
<Bird className="h-12 w-12" strokeWidth={1.5} />
|
||||
|
||||
<div className="text-center">
|
||||
@@ -81,10 +100,37 @@ export default function TemplatesPage() {
|
||||
isLoadingError={isLoadingError}
|
||||
documentRootPath={documentRootPath}
|
||||
templateRootPath={templateRootPath}
|
||||
enableSelection
|
||||
rowSelection={rowSelection}
|
||||
onRowSelectionChange={setRowSelection}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EnvelopesTableBulkActionBar
|
||||
selectedCount={selectedEnvelopeIds.length}
|
||||
onMoveClick={() => setIsBulkMoveDialogOpen(true)}
|
||||
onDeleteClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||
onClearSelection={() => setRowSelection({})}
|
||||
/>
|
||||
|
||||
<EnvelopesBulkMoveDialog
|
||||
envelopeIds={selectedEnvelopeIds}
|
||||
envelopeType={EnvelopeType.TEMPLATE}
|
||||
open={isBulkMoveDialogOpen}
|
||||
currentFolderId={folderId}
|
||||
onOpenChange={setIsBulkMoveDialogOpen}
|
||||
onSuccess={() => setRowSelection({})}
|
||||
/>
|
||||
|
||||
<EnvelopesBulkDeleteDialog
|
||||
envelopeIds={selectedEnvelopeIds}
|
||||
envelopeType={EnvelopeType.TEMPLATE}
|
||||
open={isBulkDeleteDialogOpen}
|
||||
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||
onSuccess={() => setRowSelection({})}
|
||||
/>
|
||||
</div>
|
||||
</EnvelopeDropZoneWrapper>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user