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) => (
-
-
-
navigateToFolder(folder.id)}
- >
-
-
-
-
- {folder._count.templates || 0} templates
- •
- {folder._count.subfolders} folders
-
-
-
-
-
-
-
- •••
-
-
-
- {
- 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) => (
-
-
-
navigateToFolder(folder.id)}
- >
-
-
-
{folder.name}
-
- {folder._count.templates || 0} templates
- •
- {folder._count.subfolders} folders
-
-
-
-
-
-
-
- •••
-
-
-
- {
- 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 && (
Reset
diff --git a/packages/ui/primitives/data-table/data-table.tsx b/packages/ui/primitives/data-table/data-table.tsx
index 0eb01579e..14698631f 100644
--- a/packages/ui/primitives/data-table/data-table.tsx
+++ b/packages/ui/primitives/data-table/data-table.tsx
@@ -40,9 +40,12 @@ interface DataTableProps {
selectedStatusValues?: string[];
onTimePeriodFilterChange?: (values: string[]) => void;
selectedTimePeriodValues?: string[];
+ onSourceFilterChange?: (values: string[]) => void;
+ selectedSourceValues?: string[];
onResetFilters?: () => void;
isStatusFiltered?: boolean;
isTimePeriodFiltered?: boolean;
+ isSourceFiltered?: boolean;
skeleton?: {
enable: boolean;
rows: number;
@@ -73,9 +76,12 @@ export function DataTable({
selectedStatusValues,
onTimePeriodFilterChange,
selectedTimePeriodValues,
+ onSourceFilterChange,
+ selectedSourceValues,
onResetFilters,
isStatusFiltered,
isTimePeriodFiltered,
+ isSourceFiltered,
emptyState,
}: DataTableProps) {
const [rowSelection, setRowSelection] = React.useState({});
@@ -150,9 +156,12 @@ export function DataTable({
selectedStatusValues={selectedStatusValues}
onTimePeriodFilterChange={onTimePeriodFilterChange}
selectedTimePeriodValues={selectedTimePeriodValues}
+ onSourceFilterChange={onSourceFilterChange}
+ selectedSourceValues={selectedSourceValues}
onResetFilters={onResetFilters}
isStatusFiltered={isStatusFiltered}
isTimePeriodFiltered={isTimePeriodFiltered}
+ isSourceFiltered={isSourceFiltered}
/>
{table.getRowModel().rows?.length || error?.enable || skeleton?.enable ? (
diff --git a/packages/ui/primitives/data-table/data/data.tsx b/packages/ui/primitives/data-table/data/data.tsx
index 491435ec2..f1dad96d9 100644
--- a/packages/ui/primitives/data-table/data/data.tsx
+++ b/packages/ui/primitives/data-table/data/data.tsx
@@ -1,4 +1,4 @@
-import { CheckCircle2, Clock, File, Inbox, XCircle } from 'lucide-react';
+import { CheckCircle2, Clock, File, FileText, Inbox, Link, XCircle } from 'lucide-react';
export const statuses = [
{
@@ -19,8 +19,8 @@ export const statuses = [
value: 'PENDING',
label: 'Pending',
icon: Clock,
- color: 'text-water-700 dark:text-water-300',
- bgColor: 'bg-water-100 dark:bg-water-100 text-water-700 dark:text-water-700',
+ color: 'text-blue-700 dark:text-blue-300',
+ bgColor: 'bg-blue-100 dark:bg-blue-100 text-blue-700 dark:text-blue-700',
},
{
value: 'COMPLETED',
@@ -38,6 +38,23 @@ export const statuses = [
},
];
+export const sources = [
+ {
+ value: 'TEMPLATE',
+ label: 'Template',
+ icon: FileText,
+ color: 'text-blue-700 dark:text-blue-300',
+ bgColor: 'bg-blue-100 dark:bg-blue-100 text-blue-700 dark:text-blue-700',
+ },
+ {
+ value: 'DIRECT_LINK',
+ label: 'Direct Link',
+ icon: Link,
+ color: 'text-green-700 dark:text-green-300',
+ bgColor: 'bg-green-100 dark:bg-green-100 text-green-700 dark:text-green-700',
+ },
+];
+
export const timePeriods = [
{
value: 'today',
diff --git a/packages/ui/primitives/data-table/utils/time-filters.ts b/packages/ui/primitives/data-table/utils/time-filters.ts
index 25f39b45f..2acf7aa3a 100644
--- a/packages/ui/primitives/data-table/utils/time-filters.ts
+++ b/packages/ui/primitives/data-table/utils/time-filters.ts
@@ -1,17 +1,20 @@
import { DateTime } from 'luxon';
-export type TimePeriod =
- | 'today'
- | 'this-week'
- | 'this-month'
- | 'this-quarter'
- | 'this-year'
- | 'yesterday'
- | 'last-week'
- | 'last-month'
- | 'last-quarter'
- | 'last-year'
- | 'all-time';
+export const timePeriods = [
+ 'today',
+ 'this-week',
+ 'this-month',
+ 'this-quarter',
+ 'this-year',
+ 'yesterday',
+ 'last-week',
+ 'last-month',
+ 'last-quarter',
+ 'last-year',
+ 'all-time',
+] as const;
+
+export type TimePeriod = (typeof timePeriods)[number];
export function getDateRangeForPeriod(
period: TimePeriod,