From 9739a0ca96b037d64aa7b3c44fc14a8e94049637 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Thu, 5 Jun 2025 10:53:53 +0000 Subject: [PATCH] feat: use data-table on template pages --- .../template-page-view-documents-table.tsx | 258 ++++++++++++------ .../template-documents-table-empty-state.tsx | 70 +++++ .../_authenticated+/documents._index.tsx | 20 +- .../templates.f.$folderId._index.tsx | 241 +++++----------- .../server-only/document/find-documents.ts | 15 +- .../trpc/server/document-router/router.ts | 2 +- .../data-table/data-table-toolbar.tsx | 26 +- .../ui/primitives/data-table/data-table.tsx | 9 + .../ui/primitives/data-table/data/data.tsx | 23 +- .../data-table/utils/time-filters.ts | 27 +- 10 files changed, 390 insertions(+), 301 deletions(-) create mode 100644 apps/remix/app/components/tables/template-documents-table-empty-state.tsx diff --git a/apps/remix/app/components/general/template/template-page-view-documents-table.tsx b/apps/remix/app/components/general/template/template-page-view-documents-table.tsx index b96d83ef2..dd7a5cd93 100644 --- a/apps/remix/app/components/general/template/template-page-view-documents-table.tsx +++ b/apps/remix/app/components/general/template/template-page-view-documents-table.tsx @@ -1,80 +1,148 @@ -import { useMemo } from 'react'; +import { useMemo, useTransition } from 'react'; +import { i18n } from '@lingui/core'; import type { MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { DocumentSource, DocumentStatus as DocumentStatusEnum } from '@prisma/client'; -import { InfoIcon } from 'lucide-react'; +import { DocumentSource } from '@prisma/client'; +import { InfoIcon, Loader } from 'lucide-react'; import { DateTime } from 'luxon'; import { useSearchParams } from 'react-router'; -import { z } from 'zod'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { trpc } from '@documenso/trpc/react'; +import type { TFindDocumentsInternalResponse } from '@documenso/trpc/server/document-router/schema'; import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; -import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; -import { SelectItem } from '@documenso/ui/primitives/select'; +import { DataTable } from '@documenso/ui/primitives/data-table/data-table'; +import { + type TimePeriod, + isDateInPeriod, + timePeriods, +} from '@documenso/ui/primitives/data-table/utils/time-filters'; import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { TableCell } from '@documenso/ui/primitives/table'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; -import { SearchParamSelector } from '~/components/forms/search-param-selector'; -import { DocumentSearch } from '~/components/general/document/document-search'; -import { DocumentStatus } from '~/components/general/document/document-status'; +import { DocumentStatus as DocumentStatusComponent } from '~/components/general/document/document-status'; import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with-tooltip'; import { DocumentsTableActionButton } from '~/components/tables/documents-table-action-button'; import { DocumentsTableActionDropdown } from '~/components/tables/documents-table-action-dropdown'; import { DataTableTitle } from '~/components/tables/documents-table-title'; +import { TemplateDocumentsTableEmptyState } from '~/components/tables/template-documents-table-empty-state'; import { useOptionalCurrentTeam } from '~/providers/team'; -import { PeriodSelector } from '../period-selector'; - const DOCUMENT_SOURCE_LABELS: { [key in DocumentSource]: MessageDescriptor } = { DOCUMENT: msg`Document`, TEMPLATE: msg`Template`, TEMPLATE_DIRECT_LINK: msg`Direct link`, }; -const ZDocumentSearchParamsSchema = ZUrlSearchParamsSchema.extend({ - source: z - .nativeEnum(DocumentSource) - .optional() - .catch(() => undefined), - status: z - .nativeEnum(DocumentStatusEnum) - .optional() - .catch(() => undefined), -}); - type TemplatePageViewDocumentsTableProps = { templateId: number; }; +type DocumentsTableRow = TFindDocumentsInternalResponse['data'][number]; + export const TemplatePageViewDocumentsTable = ({ templateId, }: TemplatePageViewDocumentsTableProps) => { - const { _, i18n } = useLingui(); - + const { _ } = useLingui(); const [searchParams] = useSearchParams(); + const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); const team = useOptionalCurrentTeam(); - const parsedSearchParams = ZDocumentSearchParamsSchema.parse( - Object.fromEntries(searchParams ?? []), - ); + const handleStatusFilterChange = (values: string[]) => { + startTransition(() => { + if (values.length === 0) { + updateSearchParams({ status: undefined, page: undefined }); + } else { + updateSearchParams({ status: values.join(','), page: undefined }); + } + }); + }; - const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery( + const currentStatus = searchParams.get('status'); + const selectedStatusValues = currentStatus ? currentStatus.split(',').filter(Boolean) : []; + + const handleResetFilters = () => { + startTransition(() => { + updateSearchParams({ + status: undefined, + source: undefined, + period: undefined, + page: undefined, + }); + }); + }; + + const isStatusFiltered = selectedStatusValues.length > 0; + + const handleTimePeriodFilterChange = (values: string[]) => { + startTransition(() => { + if (values.length === 0) { + updateSearchParams({ period: undefined, page: undefined }); + } else { + updateSearchParams({ period: values[0], page: undefined }); + } + }); + }; + + const currentPeriod = searchParams.get('period'); + const selectedTimePeriodValues = currentPeriod ? [currentPeriod] : []; + const isTimePeriodFiltered = selectedTimePeriodValues.length > 0; + + const handleSourceFilterChange = (values: string[]) => { + startTransition(() => { + if (values.length === 0) { + updateSearchParams({ source: undefined, page: undefined }); + } else { + updateSearchParams({ source: values.join(','), page: undefined }); + } + }); + }; + + const currentSource = searchParams.get('source'); + const selectedSourceValues = currentSource ? currentSource.split(',').filter(Boolean) : []; + const isSourceFiltered = selectedSourceValues.length > 0; + + const sourceParam = searchParams.get('source'); + const statusParam = searchParams.get('status'); + const periodParam = searchParams.get('period'); + + // Parse status parameter to handle multiple values + const parsedStatus = statusParam + ? statusParam + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + .filter((status) => + Object.values(ExtendedDocumentStatus).includes(status as ExtendedDocumentStatus), + ) + .map((status) => status as ExtendedDocumentStatus) + : undefined; + + const parsedPeriod = + periodParam && timePeriods.includes(periodParam as TimePeriod) + ? (periodParam as TimePeriod) + : undefined; + + const { data, isLoading, isLoadingError } = trpc.document.findDocumentsInternal.useQuery( { templateId, - page: parsedSearchParams.page, - perPage: parsedSearchParams.perPage, - query: parsedSearchParams.query, - source: parsedSearchParams.source, - status: parsedSearchParams.status, + page: Number(searchParams.get('page')) || 1, + perPage: Number(searchParams.get('perPage')) || 10, + query: searchParams.get('query') || undefined, + source: + sourceParam && Object.values(DocumentSource).includes(sourceParam as DocumentSource) + ? (sourceParam as DocumentSource) + : undefined, + status: parsedStatus, + period: parsedPeriod, }, { placeholderData: (previousData) => previousData, @@ -82,9 +150,11 @@ export const TemplatePageViewDocumentsTable = ({ ); const onPaginationChange = (page: number, perPage: number) => { - updateSearchParams({ - page, - perPage, + startTransition(() => { + updateSearchParams({ + page, + perPage, + }); }); }; @@ -93,6 +163,21 @@ export const TemplatePageViewDocumentsTable = ({ perPage: 10, currentPage: 1, totalPages: 1, + stats: { + [ExtendedDocumentStatus.DRAFT]: 0, + [ExtendedDocumentStatus.PENDING]: 0, + [ExtendedDocumentStatus.COMPLETED]: 0, + [ExtendedDocumentStatus.REJECTED]: 0, + [ExtendedDocumentStatus.INBOX]: 0, + [ExtendedDocumentStatus.ALL]: 0, + }, + }; + + const getEmptyStateStatus = (): ExtendedDocumentStatus => { + if (selectedStatusValues.length > 0) { + return selectedStatusValues[0] as ExtendedDocumentStatus; + } + return ExtendedDocumentStatus.ALL; }; const columns = useMemo(() => { @@ -102,12 +187,21 @@ export const TemplatePageViewDocumentsTable = ({ accessorKey: 'createdAt', cell: ({ row }) => i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }), + filterFn: (row, id, value) => { + const createdAt = row.getValue(id) as Date; + if (!value || !Array.isArray(value) || value.length === 0) { + return true; + } + + const period = value[0] as TimePeriod; + return isDateInPeriod(createdAt, period); + }, }, { header: _(msg`Title`), + accessorKey: 'title', cell: ({ row }) => , }, - { header: _(msg`Recipient`), accessorKey: 'recipient', @@ -121,8 +215,11 @@ export const TemplatePageViewDocumentsTable = ({ { header: _(msg`Status`), accessorKey: 'status', - cell: ({ row }) => , + cell: ({ row }) => , size: 140, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + }, }, { header: () => ( @@ -161,79 +258,48 @@ export const TemplatePageViewDocumentsTable = ({ ), - accessorKey: 'type', + accessorKey: 'source', cell: ({ row }) => (
- {_(DOCUMENT_SOURCE_LABELS[row.original.source])} + {_(DOCUMENT_SOURCE_LABELS[row.original.source as DocumentSource])}
), + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + }, }, { - id: 'actions', header: _(msg`Actions`), cell: ({ row }) => ( -
+
-
), }, - ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[]; - }, []); + ] satisfies DataTableColumnDef[]; + }, [_, team?.url]); return ( -
-
- - - - [...DocumentStatusEnum.COMPLETED].includes(value as unknown as string) - } - > - - Any Status - - - Completed - - - Pending - - - Draft - - - - - [...DocumentSource.TEMPLATE].includes(value as unknown as string) - } - > - - Any Source - - - Template - - - Direct Link - - - - -
- +
), }} + emptyState={{ + enable: !isLoading && !isLoadingError, + component: , + }} > {(table) => } + + {isPending && ( +
+ +
+ )}
); }; diff --git a/apps/remix/app/components/tables/template-documents-table-empty-state.tsx b/apps/remix/app/components/tables/template-documents-table-empty-state.tsx new file mode 100644 index 000000000..a4dc13f60 --- /dev/null +++ b/apps/remix/app/components/tables/template-documents-table-empty-state.tsx @@ -0,0 +1,70 @@ +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Bird, CheckCircle2 } from 'lucide-react'; +import { match } from 'ts-pattern'; + +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; + +export type TemplateDocumentsTableEmptyStateProps = { status: ExtendedDocumentStatus }; + +export const TemplateDocumentsTableEmptyState = ({ + status, +}: TemplateDocumentsTableEmptyStateProps) => { + const { _ } = useLingui(); + + const { + title, + message, + icon: Icon, + } = match(status) + .with(ExtendedDocumentStatus.COMPLETED, () => ({ + title: msg`No completed documents`, + message: msg`No documents created from this template have been completed yet. Completed documents will appear here once all recipients have signed.`, + icon: CheckCircle2, + })) + .with(ExtendedDocumentStatus.DRAFT, () => ({ + title: msg`No draft documents`, + message: msg`There are no draft documents created from this template. Use this template to create a new document.`, + icon: CheckCircle2, + })) + .with(ExtendedDocumentStatus.PENDING, () => ({ + title: msg`No pending documents`, + message: msg`There are no pending documents created from this template. Documents awaiting signatures will appear here.`, + icon: CheckCircle2, + })) + .with(ExtendedDocumentStatus.REJECTED, () => ({ + title: msg`No rejected documents`, + message: msg`No documents created from this template have been rejected. Documents that have been declined will appear here.`, + icon: CheckCircle2, + })) + .with(ExtendedDocumentStatus.INBOX, () => ({ + title: msg`No documents in inbox`, + message: msg`There are no documents from this template waiting for your action. Documents requiring your signature will appear here.`, + icon: CheckCircle2, + })) + .with(ExtendedDocumentStatus.ALL, () => ({ + title: msg`No documents yet`, + message: msg`No documents have been created from this template yet. Use this template to create your first document.`, + icon: Bird, + })) + .otherwise(() => ({ + title: msg`No documents found`, + message: msg`No documents created from this template match the current filters. Try adjusting your search criteria.`, + icon: CheckCircle2, + })); + + return ( +
+ + +
+

{_(title)}

+ +

{_(message)}

+
+
+ ); +}; diff --git a/apps/remix/app/routes/_authenticated+/documents._index.tsx b/apps/remix/app/routes/_authenticated+/documents._index.tsx index 701cdb35a..4beeda361 100644 --- a/apps/remix/app/routes/_authenticated+/documents._index.tsx +++ b/apps/remix/app/routes/_authenticated+/documents._index.tsx @@ -246,17 +246,15 @@ export default function DocumentsPage() {
-
- { - setDocumentToMove(documentId); - setIsMovingDocument(true); - }} - /> -
+ { + setDocumentToMove(documentId); + setIsMovingDocument(true); + }} + />
{documentToMove && ( diff --git a/apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx b/apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx index f341e7a83..aaf443bd4 100644 --- a/apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx +++ b/apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Trans } from '@lingui/react/macro'; -import { Bird, FolderIcon, HomeIcon, Loader2, PinIcon } from 'lucide-react'; +import { Bird, FolderIcon, HomeIcon, Loader2 } from 'lucide-react'; import { useNavigate, useParams, useSearchParams } from 'react-router'; import { FolderType } from '@documenso/lib/types/folder-type'; @@ -11,18 +11,13 @@ import { trpc } from '@documenso/trpc/react'; import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@documenso/ui/primitives/dropdown-menu'; -import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog'; -import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog'; -import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog'; import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog'; import { TemplateFolderCreateDialog } from '~/components/dialogs/template-folder-create-dialog'; +import { TemplateFolderDeleteDialog } from '~/components/dialogs/template-folder-delete-dialog'; +import { TemplateFolderMoveDialog } from '~/components/dialogs/template-folder-move-dialog'; +import { TemplateFolderSettingsDialog } from '~/components/dialogs/template-folder-settings-dialog'; +import { FolderCard } from '~/components/general/folder/folder-card'; import { TemplatesTable } from '~/components/tables/templates-table'; import { useOptionalCurrentTeam } from '~/providers/team'; import { appMetaTags } from '~/utils/meta'; @@ -36,8 +31,18 @@ export default function TemplatesPage() { const { folderId } = useParams(); const navigate = useNavigate(); + const [isMovingFolder, setIsMovingFolder] = useState(false); + const [folderToMove, setFolderToMove] = useState(null); + const [isDeletingFolder, setIsDeletingFolder] = useState(false); + const [folderToDelete, setFolderToDelete] = useState(null); + const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false); + const [folderToSettings, setFolderToSettings] = useState(null); + const team = useOptionalCurrentTeam(); + const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation(); + const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation(); + const page = Number(searchParams.get('page')) || 1; const perPage = Number(searchParams.get('perPage')) || 10; @@ -59,22 +64,12 @@ export default function TemplatesPage() { type: FolderType.TEMPLATE, }); - const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation(); - const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation(); - - const [folderToMove, setFolderToMove] = useState(null); - const [isMovingFolder, setIsMovingFolder] = useState(false); - const [folderToSettings, setFolderToSettings] = useState(null); - const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false); - const [folderToDelete, setFolderToDelete] = useState(null); - const [isDeletingFolder, setIsDeletingFolder] = useState(false); - useEffect(() => { void refetch(); void refetchFolders(); }, [team?.url]); - const navigateToFolder = (folderId?: string) => { + const navigateToFolder = (folderId?: string | null) => { const templatesPath = formatTemplatesPath(team?.url); if (folderId) { @@ -84,6 +79,33 @@ export default function TemplatesPage() { } }; + const handleNavigate = (folderId: string) => { + navigateToFolder(folderId); + }; + + const handleMove = (folder: TFolderWithSubfolders) => { + setFolderToMove(folder); + setIsMovingFolder(true); + }; + + const handlePin = (folderId: string) => { + void pinFolder({ folderId }); + }; + + const handleUnpin = (folderId: string) => { + void unpinFolder({ folderId }); + }; + + const handleSettings = (folder: TFolderWithSubfolders) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }; + + const handleDelete = (folder: TFolderWithSubfolders) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }; + return (
@@ -126,159 +148,44 @@ export default function TemplatesPage() {
) : ( <> - {foldersData?.folders.some((folder) => folder.pinned) && ( + {foldersData?.folders && foldersData.folders.some((folder) => folder.pinned) && (
- {foldersData?.folders + {foldersData.folders .filter((folder) => folder.pinned) .map((folder) => ( -
-
- - - - - - - - { - setFolderToMove(folder); - setIsMovingFolder(true); - }} - > - Move - - { - void unpinFolder({ folderId: folder.id }); - }} - > - Unpin - - { - setFolderToSettings(folder); - setIsSettingsFolderOpen(true); - }} - > - Settings - - { - setFolderToDelete(folder); - setIsDeletingFolder(true); - }} - > - Delete - - - -
-
+ folder={folder} + onNavigate={handleNavigate} + onMove={handleMove} + onPin={handlePin} + onUnpin={handleUnpin} + onSettings={handleSettings} + onDelete={handleDelete} + /> ))}
)} -
- {foldersData?.folders - .filter((folder) => !folder.pinned) - .map((folder) => ( -
-
- - - - - - - - { - setFolderToMove(folder); - setIsMovingFolder(true); - }} - > - Move - - { - void pinFolder({ folderId: folder.id }); - }} - > - Pin - - { - setFolderToSettings(folder); - setIsSettingsFolderOpen(true); - }} - > - Settings - - { - setFolderToDelete(folder); - setIsDeletingFolder(true); - }} - > - Delete - - - -
-
- ))} +
+
+ {foldersData?.folders + .filter((folder) => !folder.pinned) + .map((folder) => ( + + ))} +
)} @@ -328,7 +235,7 @@ export default function TemplatesPage() {
- - { @@ -353,7 +260,7 @@ export default function TemplatesPage() { }} /> - { diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 3fe29de12..ee0379b73 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -5,24 +5,13 @@ import { match } from 'ts-pattern'; import { prisma } from '@documenso/prisma'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; +import type { TimePeriod } from '@documenso/ui/primitives/data-table/utils/time-filters'; import { DocumentVisibility } from '../../types/document-visibility'; import { type FindResultResponse } from '../../types/search-params'; import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document'; -export type PeriodSelectorValue = - | '' - | 'today' - | 'yesterday' - | 'this-week' - | 'last-week' - | 'this-month' - | 'last-month' - | 'this-quarter' - | 'last-quarter' - | 'this-year' - | 'last-year' - | 'all-time'; +export type PeriodSelectorValue = '' | TimePeriod; export type FindDocumentsOptions = { userId: number; diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 2a14ed30e..ba3478fe8 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -125,7 +125,7 @@ export const documentRouter = router({ templateId, query, source, - status, + status: status ? [status] : undefined, page, perPage, folderId, diff --git a/packages/ui/primitives/data-table/data-table-toolbar.tsx b/packages/ui/primitives/data-table/data-table-toolbar.tsx index ec92440f3..f48bfbcba 100644 --- a/packages/ui/primitives/data-table/data-table-toolbar.tsx +++ b/packages/ui/primitives/data-table/data-table-toolbar.tsx @@ -1,11 +1,11 @@ import type { Table } from '@tanstack/react-table'; -import { Calendar, CircleDashedIcon, ListFilter, X, XCircle } from 'lucide-react'; +import { Calendar, CircleDashedIcon, Globe, ListFilter, X, XCircle } from 'lucide-react'; import { Button } from '../button'; import { Input } from '../input'; import { DataTableFacetedFilter } from './data-table-faceted-filter'; import { DataTableSingleFilter } from './data-table-single-filter'; -import { statuses, timePeriodGroups, timePeriods } from './data/data'; +import { sources, statuses, timePeriodGroups, timePeriods } from './data/data'; interface DataTableToolbarProps { table: Table; @@ -14,9 +14,12 @@ interface DataTableToolbarProps { selectedStatusValues?: string[]; onTimePeriodFilterChange?: (values: string[]) => void; selectedTimePeriodValues?: string[]; + onSourceFilterChange?: (values: string[]) => void; + selectedSourceValues?: string[]; onResetFilters?: () => void; isStatusFiltered?: boolean; isTimePeriodFiltered?: boolean; + isSourceFiltered?: boolean; } export function DataTableToolbar({ @@ -26,12 +29,18 @@ export function DataTableToolbar({ selectedStatusValues, onTimePeriodFilterChange, selectedTimePeriodValues, + onSourceFilterChange, + selectedSourceValues, onResetFilters, isStatusFiltered, isTimePeriodFiltered, + isSourceFiltered, }: DataTableToolbarProps) { const isFiltered = - table.getState().columnFilters.length > 0 || isStatusFiltered || isTimePeriodFiltered; + table.getState().columnFilters.length > 0 || + isStatusFiltered || + isTimePeriodFiltered || + isSourceFiltered; const searchValue = (table.getColumn('title')?.getFilterValue() as string) ?? ''; const handleClearFilter = () => { @@ -91,6 +100,17 @@ export function DataTableToolbar({ /> )} + {table.getColumn('source') && ( + + )} + {isFiltered && (