chore: minor changes

This commit is contained in:
Ephraim Atta-Duncan
2025-06-05 11:41:26 +00:00
parent 9739a0ca96
commit 2c064d5aff
12 changed files with 169 additions and 550 deletions

View File

@ -218,6 +218,9 @@ export const TemplatePageViewDocumentsTable = ({
cell: ({ row }) => <DocumentStatusComponent status={row.original.status} />, cell: ({ row }) => <DocumentStatusComponent status={row.original.status} />,
size: 140, size: 140,
filterFn: (row, id, value) => { filterFn: (row, id, value) => {
if (!value || !Array.isArray(value) || value.length === 0) {
return true;
}
return value.includes(row.getValue(id)); return value.includes(row.getValue(id));
}, },
}, },
@ -265,6 +268,9 @@ export const TemplatePageViewDocumentsTable = ({
</div> </div>
), ),
filterFn: (row, id, value) => { filterFn: (row, id, value) => {
if (!value || !Array.isArray(value) || value.length === 0) {
return true;
}
return value.includes(row.getValue(id)); return value.includes(row.getValue(id));
}, },
}, },

View File

@ -1,6 +1,6 @@
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Bird, CheckCircle2 } from 'lucide-react'; import { Bird, CheckCircle2, XCircle } from 'lucide-react';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
@ -33,7 +33,7 @@ export const DocumentsTableEmptyState = ({ status }: DocumentsTableEmptyStatePro
.with(ExtendedDocumentStatus.REJECTED, () => ({ .with(ExtendedDocumentStatus.REJECTED, () => ({
title: msg`No rejected documents`, title: msg`No rejected documents`,
message: msg`There are no rejected documents. Documents that have been declined will appear here.`, message: msg`There are no rejected documents. Documents that have been declined will appear here.`,
icon: CheckCircle2, icon: XCircle,
})) }))
.with(ExtendedDocumentStatus.INBOX, () => ({ .with(ExtendedDocumentStatus.INBOX, () => ({
title: msg`Your inbox is empty`, title: msg`Your inbox is empty`,

View File

@ -1,62 +1,113 @@
import { useMemo, useTransition } from 'react'; import { useMemo, useTransition } from 'react';
import { i18n } from '@lingui/core';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Link } from 'react-router'; import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import type { TFindDocumentsInternalResponse } from '@documenso/trpc/server/document-router/schema';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; 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 { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { DataTable } from '@documenso/ui/primitives/data-table/data-table';
import {
type TimePeriod,
isDateInPeriod,
} from '@documenso/ui/primitives/data-table/utils/time-filters';
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { DocumentStatus } from '~/components/general/document/document-status'; import { DocumentStatus } from '~/components/general/document/document-status';
import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with-tooltip';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
import { DocumentsTableActionButton } from './documents-table-action-button'; import { DocumentsTableActionButton } from './documents-table-action-button';
import { DocumentsTableActionDropdown } from './documents-table-action-dropdown'; import { DocumentsTableActionDropdown } from './documents-table-action-dropdown';
import { DocumentsTableEmptyState } from './documents-table-empty-state';
export type DocumentsTableProps = { export type DataTableProps = {
data?: TFindDocumentsResponse; data?: TFindDocumentsInternalResponse;
isLoading?: boolean; isLoading?: boolean;
isLoadingError?: boolean; isLoadingError?: boolean;
onMoveDocument?: (documentId: number) => void; onMoveDocument?: (documentId: number) => void;
}; };
type DocumentsTableRow = TFindDocumentsResponse['data'][number]; type DocumentsTableRow = TFindDocumentsInternalResponse['data'][number];
export const DocumentsTable = ({ export function DocumentsDataTable({
data, data,
isLoading, isLoading,
isLoadingError, isLoadingError,
onMoveDocument, onMoveDocument,
}: DocumentsTableProps) => { }: DataTableProps) {
const { _, i18n } = useLingui(); const { _ } = useLingui();
const team = useOptionalCurrentTeam(); const team = useOptionalCurrentTeam();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams(); const updateSearchParams = useUpdateSearchParams();
const [searchParams] = useSearchParams();
const handleStatusFilterChange = (values: string[]) => {
startTransition(() => {
if (values.length === 0) {
updateSearchParams({ status: undefined, page: undefined });
} else {
updateSearchParams({ status: values.join(','), page: undefined });
}
});
};
const currentStatus = searchParams.get('status');
const selectedStatusValues = currentStatus ? currentStatus.split(',').filter(Boolean) : [];
const handleResetFilters = () => {
startTransition(() => {
updateSearchParams({ status: 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 columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
{ {
header: _(msg`Created`), header: 'Created',
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => cell: ({ row }) =>
i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }), 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`), header: _(msg`Title`),
accessorKey: 'title',
cell: ({ row }) => <DataTableTitle row={row.original} teamUrl={team?.url} />, cell: ({ row }) => <DataTableTitle row={row.original} teamUrl={team?.url} />,
}, },
{ {
@ -79,6 +130,12 @@ export const DocumentsTable = ({
accessorKey: 'status', accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.original.status} />, cell: ({ row }) => <DocumentStatus status={row.original.status} />,
size: 140, size: 140,
filterFn: (row, id, value) => {
if (!value || !Array.isArray(value) || value.length === 0) {
return true;
}
return value.includes(row.getValue(id));
},
}, },
{ {
header: _(msg`Actions`), header: _(msg`Actions`),
@ -112,18 +169,30 @@ export const DocumentsTable = ({
totalPages: 1, totalPages: 1,
}; };
const getEmptyStateStatus = (): ExtendedDocumentStatus => {
if (selectedStatusValues.length > 0) {
return selectedStatusValues[0] as ExtendedDocumentStatus;
}
return ExtendedDocumentStatus.ALL;
};
return ( return (
<div className="relative"> <div className="relative">
<DataTable <DataTable
columns={columns}
data={results.data} data={results.data}
columns={columns}
perPage={results.perPage} perPage={results.perPage}
currentPage={results.currentPage} currentPage={results.currentPage}
totalPages={results.totalPages} totalPages={results.totalPages}
onPaginationChange={onPaginationChange} onPaginationChange={onPaginationChange}
columnVisibility={{ stats={data?.stats}
sender: team !== undefined, onStatusFilterChange={handleStatusFilterChange}
}} selectedStatusValues={selectedStatusValues}
onTimePeriodFilterChange={handleTimePeriodFilterChange}
selectedTimePeriodValues={selectedTimePeriodValues}
onResetFilters={handleResetFilters}
isStatusFiltered={isStatusFiltered}
isTimePeriodFiltered={isTimePeriodFiltered}
error={{ error={{
enable: isLoadingError || false, enable: isLoadingError || false,
}} }}
@ -152,6 +221,10 @@ export const DocumentsTable = ({
</> </>
), ),
}} }}
emptyState={{
enable: !isLoading && !isLoadingError,
component: <DocumentsTableEmptyState status={getEmptyStateStatus()} />,
}}
> >
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />} {(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable> </DataTable>
@ -163,7 +236,7 @@ export const DocumentsTable = ({
)} )}
</div> </div>
); );
}; }
type DataTableTitleProps = { type DataTableTitleProps = {
row: DocumentsTableRow; row: DocumentsTableRow;

View File

@ -1,288 +0,0 @@
import { useMemo, useTransition } from 'react';
import { i18n } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DateTime } from 'luxon';
import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import type { TFindDocumentsInternalResponse } from '@documenso/trpc/server/document-router/schema';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { DataTable } from '@documenso/ui/primitives/data-table/data-table';
import {
type TimePeriod,
isDateInPeriod,
} from '@documenso/ui/primitives/data-table/utils/time-filters';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { DocumentStatus } from '~/components/general/document/document-status';
import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with-tooltip';
import { useOptionalCurrentTeam } from '~/providers/team';
import { DocumentsTableActionButton } from '../documents-table-action-button';
import { DocumentsTableActionDropdown } from '../documents-table-action-dropdown';
import { DocumentsTableEmptyState } from '../documents-table-empty-state';
export type DataTableProps = {
data?: TFindDocumentsInternalResponse;
isLoading?: boolean;
isLoadingError?: boolean;
onMoveDocument?: (documentId: number) => void;
};
type DocumentsTableRow = TFindDocumentsInternalResponse['data'][number];
export function DocumentsDataTable({
data,
isLoading,
isLoadingError,
onMoveDocument,
}: DataTableProps) {
const { _ } = useLingui();
const team = useOptionalCurrentTeam();
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
const [searchParams] = useSearchParams();
const handleStatusFilterChange = (values: string[]) => {
startTransition(() => {
if (values.length === 0) {
updateSearchParams({ status: undefined, page: undefined });
} else {
updateSearchParams({ status: values.join(','), page: undefined });
}
});
};
const currentStatus = searchParams.get('status');
const selectedStatusValues = currentStatus ? currentStatus.split(',').filter(Boolean) : [];
const handleResetFilters = () => {
startTransition(() => {
updateSearchParams({ status: 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 columns = useMemo(() => {
return [
{
header: 'Created',
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 }) => <DataTableTitle row={row.original} teamUrl={team?.url} />,
},
{
id: 'sender',
header: _(msg`Sender`),
cell: ({ row }) => row.original.user.name ?? row.original.user.email,
},
{
header: _(msg`Recipient`),
accessorKey: 'recipient',
cell: ({ row }) => (
<StackAvatarsWithTooltip
recipients={row.original.recipients}
documentStatus={row.original.status}
/>
),
},
{
header: _(msg`Status`),
accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
size: 140,
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
},
{
header: _(msg`Actions`),
cell: ({ row }) =>
(!row.original.deletedAt || isDocumentCompleted(row.original.status)) && (
<div className="flex items-center gap-x-4">
<DocumentsTableActionButton row={row.original} />
<DocumentsTableActionDropdown
row={row.original}
onMoveDocument={onMoveDocument ? () => onMoveDocument(row.original.id) : undefined}
/>
</div>
),
},
] satisfies DataTableColumnDef<DocumentsTableRow>[];
}, [team, onMoveDocument]);
const onPaginationChange = (page: number, perPage: number) => {
startTransition(() => {
updateSearchParams({
page,
perPage,
});
});
};
const results = data ?? {
data: [],
perPage: 10,
currentPage: 1,
totalPages: 1,
};
const getEmptyStateStatus = (): ExtendedDocumentStatus => {
if (selectedStatusValues.length > 0) {
return selectedStatusValues[0] as ExtendedDocumentStatus;
}
return ExtendedDocumentStatus.ALL;
};
return (
<div className="relative">
<DataTable
data={results.data}
columns={columns}
perPage={results.perPage}
currentPage={results.currentPage}
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
columnVisibility={{
sender: team !== undefined,
}}
stats={data?.stats}
onStatusFilterChange={handleStatusFilterChange}
selectedStatusValues={selectedStatusValues}
onTimePeriodFilterChange={handleTimePeriodFilterChange}
selectedTimePeriodValues={selectedTimePeriodValues}
onResetFilters={handleResetFilters}
isStatusFiltered={isStatusFiltered}
isTimePeriodFiltered={isTimePeriodFiltered}
error={{
enable: isLoadingError || false,
}}
skeleton={{
enable: isLoading || false,
rows: 5,
component: (
<>
<TableCell>
<Skeleton className="h-4 w-40 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-20 rounded-full" />
</TableCell>
<TableCell className="py-4">
<div className="flex w-full flex-row items-center">
<Skeleton className="h-10 w-10 flex-shrink-0 rounded-full" />
</div>
</TableCell>
<TableCell>
<Skeleton className="h-4 w-20 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-10 w-24 rounded" />
</TableCell>
</>
),
}}
emptyState={{
enable: !isLoading && !isLoadingError,
component: <DocumentsTableEmptyState status={getEmptyStateStatus()} />,
}}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isPending && (
<div className="bg-background/50 absolute inset-0 flex items-center justify-center">
<Loader className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
)}
</div>
);
}
type DataTableTitleProps = {
row: DocumentsTableRow;
teamUrl?: string;
};
export const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
const { user } = useSession();
const recipient = row.recipients.find((recipient) => recipient.email === user.email);
const isOwner = row.user.id === user.id;
const isRecipient = !!recipient;
const isCurrentTeamDocument = teamUrl && row.team?.url === teamUrl;
const documentsPath = formatDocumentsPath(isCurrentTeamDocument ? teamUrl : undefined);
const formatPath = row.folderId
? `${documentsPath}/f/${row.folderId}/${row.id}`
: `${documentsPath}/${row.id}`;
return match({
isOwner,
isRecipient,
isCurrentTeamDocument,
})
.with({ isOwner: true }, { isCurrentTeamDocument: true }, () => (
<Link
to={formatPath}
title={row.title}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
>
{row.title}
</Link>
))
.with({ isRecipient: true }, () => (
<Link
to={`/sign/${recipient?.token}`}
title={row.title}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
>
{row.title}
</Link>
))
.otherwise(() => (
<span className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]">
{row.title}
</span>
));
};

View File

@ -26,7 +26,7 @@ import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialo
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper'; import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
import { DocumentUploadDropzone } from '~/components/general/document/document-upload'; import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
import { FolderCard } from '~/components/general/folder/folder-card'; import { FolderCard } from '~/components/general/folder/folder-card';
import { DocumentsDataTable } from '~/components/tables/documents-table/data-table'; import { DocumentsDataTable } from '~/components/tables/documents-table';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta'; import { appMetaTags } from '~/utils/meta';

View File

@ -24,7 +24,7 @@ import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialo
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper'; import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
import { DocumentUploadDropzone } from '~/components/general/document/document-upload'; import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
import { FolderCard } from '~/components/general/folder/folder-card'; import { FolderCard } from '~/components/general/folder/folder-card';
import { DocumentsDataTable } from '~/components/tables/documents-table/data-table'; import { DocumentsDataTable } from '~/components/tables/documents-table';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta'; import { appMetaTags } from '~/utils/meta';

View File

@ -11,8 +11,6 @@ import { DocumentVisibility } from '../../types/document-visibility';
import { type FindResultResponse } from '../../types/search-params'; import { type FindResultResponse } from '../../types/search-params';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document'; import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
export type PeriodSelectorValue = '' | TimePeriod;
export type FindDocumentsOptions = { export type FindDocumentsOptions = {
userId: number; userId: number;
teamId?: number; teamId?: number;
@ -25,7 +23,7 @@ export type FindDocumentsOptions = {
column: keyof Omit<Document, 'document'>; column: keyof Omit<Document, 'document'>;
direction: 'asc' | 'desc'; direction: 'asc' | 'desc';
}; };
period?: PeriodSelectorValue; period?: TimePeriod;
senderIds?: number[]; senderIds?: number[];
query?: string; query?: string;
folderId?: string; folderId?: string;
@ -236,58 +234,71 @@ export const findDocuments = async ({
if (period && period !== 'all-time') { if (period && period !== 'all-time') {
const now = DateTime.now(); const now = DateTime.now();
let startDate: DateTime;
let endDate: DateTime;
switch (period) { const { startDate, endDate } = match(period)
case 'today': .with('today', () => ({
startDate = now.startOf('day'); startDate: now.startOf('day'),
endDate = now.endOf('day'); endDate: now.startOf('day').plus({ days: 1 }),
break; }))
case 'yesterday': .with('yesterday', () => {
startDate = now.minus({ days: 1 }).startOf('day'); const yesterday = now.minus({ days: 1 });
endDate = now.minus({ days: 1 }).endOf('day'); return {
break; startDate: yesterday.startOf('day'),
case 'this-week': endDate: yesterday.startOf('day').plus({ days: 1 }),
startDate = now.startOf('week'); };
endDate = now.endOf('week'); })
break; .with('this-week', () => ({
case 'last-week': startDate: now.startOf('week'),
startDate = now.minus({ weeks: 1 }).startOf('week'); endDate: now.startOf('week').plus({ weeks: 1 }),
endDate = now.minus({ weeks: 1 }).endOf('week'); }))
break; .with('last-week', () => {
case 'this-month': const lastWeek = now.minus({ weeks: 1 });
startDate = now.startOf('month'); return {
endDate = now.endOf('month'); startDate: lastWeek.startOf('week'),
break; endDate: lastWeek.startOf('week').plus({ weeks: 1 }),
case 'last-month': };
startDate = now.minus({ months: 1 }).startOf('month'); })
endDate = now.minus({ months: 1 }).endOf('month'); .with('this-month', () => ({
break; startDate: now.startOf('month'),
case 'this-quarter': endDate: now.startOf('month').plus({ months: 1 }),
startDate = now.startOf('quarter'); }))
endDate = now.endOf('quarter'); .with('last-month', () => {
break; const lastMonth = now.minus({ months: 1 });
case 'last-quarter': return {
startDate = now.minus({ quarters: 1 }).startOf('quarter'); startDate: lastMonth.startOf('month'),
endDate = now.minus({ quarters: 1 }).endOf('quarter'); endDate: lastMonth.startOf('month').plus({ months: 1 }),
break; };
case 'this-year': })
startDate = now.startOf('year'); .with('this-quarter', () => ({
endDate = now.endOf('year'); startDate: now.startOf('quarter'),
break; endDate: now.startOf('quarter').plus({ quarters: 1 }),
case 'last-year': }))
startDate = now.minus({ years: 1 }).startOf('year'); .with('last-quarter', () => {
endDate = now.minus({ years: 1 }).endOf('year'); const lastQuarter = now.minus({ quarters: 1 });
break; return {
default: startDate: lastQuarter.startOf('quarter'),
startDate = now.startOf('day'); endDate: lastQuarter.startOf('quarter').plus({ quarters: 1 }),
endDate = now.endOf('day'); };
} })
.with('this-year', () => ({
startDate: now.startOf('year'),
endDate: now.startOf('year').plus({ years: 1 }),
}))
.with('last-year', () => {
const lastYear = now.minus({ years: 1 });
return {
startDate: lastYear.startOf('year'),
endDate: lastYear.startOf('year').plus({ years: 1 }),
};
})
.otherwise(() => ({
startDate: now.startOf('day'),
endDate: now.startOf('day').plus({ days: 1 }),
}));
whereClause.createdAt = { whereClause.createdAt = {
gte: startDate.toJSDate(), gte: startDate.toJSDate(),
lte: endDate.toJSDate(), lt: endDate.toJSDate(),
}; };
} }

View File

@ -1,167 +0,0 @@
import React, { useMemo } from 'react';
import { Trans } from '@lingui/react/macro';
import type {
ColumnDef,
PaginationState,
Table as TTable,
Updater,
VisibilityState,
} from '@tanstack/react-table';
import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { Skeleton } from './skeleton';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './table';
export type DataTableChildren<TData> = (_table: TTable<TData>) => React.ReactNode;
export type { ColumnDef as DataTableColumnDef } from '@tanstack/react-table';
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
columnVisibility?: VisibilityState;
data: TData[];
perPage?: number;
currentPage?: number;
totalPages?: number;
onPaginationChange?: (_page: number, _perPage: number) => void;
onClearFilters?: () => void;
hasFilters?: boolean;
children?: DataTableChildren<TData>;
skeleton?: {
enable: boolean;
rows: number;
component?: React.ReactNode;
};
error?: {
enable: boolean;
component?: React.ReactNode;
};
}
export function DataTable<TData, TValue>({
columns,
columnVisibility,
data,
error,
perPage,
currentPage,
totalPages,
skeleton,
hasFilters,
onClearFilters,
onPaginationChange,
children,
}: DataTableProps<TData, TValue>) {
const pagination = useMemo<PaginationState>(() => {
if (currentPage !== undefined && perPage !== undefined) {
return {
pageIndex: currentPage - 1,
pageSize: perPage,
};
}
return {
pageIndex: 0,
pageSize: 0,
};
}, [currentPage, perPage]);
const manualPagination = Boolean(currentPage !== undefined && totalPages !== undefined);
const onTablePaginationChange = (updater: Updater<PaginationState>) => {
if (typeof updater === 'function') {
const newState = updater(pagination);
onPaginationChange?.(newState.pageIndex + 1, newState.pageSize);
} else {
onPaginationChange?.(updater.pageIndex + 1, updater.pageSize);
}
};
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
state: {
pagination: manualPagination ? pagination : undefined,
columnVisibility,
},
manualPagination,
pageCount: totalPages,
onPaginationChange: onTablePaginationChange,
});
return (
<>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width: `${cell.column.getSize()}px`,
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : error?.enable ? (
<TableRow>
{error.component ?? (
<TableCell colSpan={columns.length} className="h-32 text-center">
<Trans>Something went wrong.</Trans>
</TableCell>
)}
</TableRow>
) : skeleton?.enable ? (
Array.from({ length: skeleton.rows }).map((_, i) => (
<TableRow key={`skeleton-row-${i}`}>{skeleton.component ?? <Skeleton />}</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-32 text-center">
<p>
<Trans>No results found</Trans>
</p>
{hasFilters && onClearFilters !== undefined && (
<button
onClick={() => onClearFilters()}
className="text-foreground mt-1 text-sm"
>
<Trans>Clear filters</Trans>
</button>
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{children && <div className="mt-8 w-full">{children(table)}</div>}
</>
);
}

View File

@ -64,7 +64,7 @@ export function DataTableFacetedFilter<TData, TValue>({
key={option.value} key={option.value}
className={cn( className={cn(
'rounded-sm border-none px-2 py-0.5 font-normal', 'rounded-sm border-none px-2 py-0.5 font-normal',
option.bgColor ? option.bgColor : 'variant-secondary', option.bgColor ? option.bgColor : 'bg-secondary',
)} )}
> >
{option.label} {option.label}
@ -130,20 +130,6 @@ export function DataTableFacetedFilter<TData, TValue>({
); );
})} })}
</CommandGroup> </CommandGroup>
{/* Option to clear filters, disabled for now since it makes the ui clanky. */}
{/* {selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)} */}
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>

View File

@ -44,8 +44,7 @@ export function DataTableSingleFilter<TData, TValue>({
selectedValues, selectedValues,
}: DataTableSingleFilterProps<TData, TValue>) { }: DataTableSingleFilterProps<TData, TValue>) {
const filterValue = column?.getFilterValue() as string[] | undefined; const filterValue = column?.getFilterValue() as string[] | undefined;
const selectedValue = const selectedValue = selectedValues?.[0] || (filterValue?.[0] ?? undefined);
selectedValues?.[0] || (filterValue && filterValue.length > 0 ? filterValue[0] : undefined);
const selectedOption = options.find((option) => option.value === selectedValue); const selectedOption = options.find((option) => option.value === selectedValue);
const handleValueChange = (value: string) => { const handleValueChange = (value: string) => {

View File

@ -29,7 +29,6 @@ import { DataTableToolbar } from './data-table-toolbar';
interface DataTableProps<TData, TValue> { interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
data: TData[]; data: TData[];
columnVisibility?: VisibilityState;
perPage?: number; perPage?: number;
currentPage?: number; currentPage?: number;
totalPages?: number; totalPages?: number;
@ -126,7 +125,7 @@ export function DataTable<TData, TValue>({
pagination: manualPagination ? pagination : undefined, pagination: manualPagination ? pagination : undefined,
}, },
manualPagination, manualPagination,
pageCount: totalPages, pageCount: manualPagination ? totalPages : undefined,
initialState: { initialState: {
pagination: { pagination: {
pageSize: 10, pageSize: 10,

View File

@ -101,15 +101,15 @@ export function getDateRangeForPeriod(
} }
export function isDateInPeriod(date: Date, period: TimePeriod): boolean { export function isDateInPeriod(date: Date, period: TimePeriod): boolean {
const dateTime = DateTime.fromJSDate(date);
if (period === 'all-time') { if (period === 'all-time') {
return true; return true;
} }
const dateTime = DateTime.fromJSDate(date);
const range = getDateRangeForPeriod(period); const range = getDateRangeForPeriod(period);
if (!range) { if (!range) {
return true; return false;
} }
return dateTime >= range.start && dateTime <= range.end; return dateTime >= range.start && dateTime <= range.end;