mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 10:42:01 +10:00
fix: wip
This commit is contained in:
@ -37,7 +37,7 @@ import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialo
|
||||
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
|
||||
import { DocumentMoveDialog } from '~/components/dialogs/document-move-dialog';
|
||||
import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
|
||||
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
|
||||
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type DocumentsTableActionDropdownProps = {
|
||||
|
||||
@ -18,10 +18,10 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
|
||||
import { DocumentsTableActionButton } from './documents-table-action-button';
|
||||
import { DocumentsTableActionDropdown } from './documents-table-action-dropdown';
|
||||
|
||||
|
||||
@ -0,0 +1,162 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Plural, Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { File } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
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 { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
export type TeamSettingsBillingInvoicesTableProps = {
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const TeamSettingsBillingInvoicesTable = ({
|
||||
teamId,
|
||||
}: TeamSettingsBillingInvoicesTableProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.team.findTeamInvoices.useQuery(
|
||||
{
|
||||
teamId,
|
||||
},
|
||||
{
|
||||
placeholderData: (previousData) => previousData,
|
||||
},
|
||||
);
|
||||
|
||||
const formatCurrency = (currency: string, amount: number) => {
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
});
|
||||
|
||||
return formatter.format(amount);
|
||||
};
|
||||
|
||||
const results = {
|
||||
data: data?.data ?? [],
|
||||
perPage: 100,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
header: _(msg`Invoice`),
|
||||
accessorKey: 'created',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex max-w-xs items-center gap-2">
|
||||
<File className="h-6 w-6" />
|
||||
|
||||
<div className="flex flex-col text-sm">
|
||||
<span className="text-foreground/80 font-semibold">
|
||||
{DateTime.fromSeconds(row.original.created).toFormat('MMMM yyyy')}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
<Plural value={row.original.quantity} one="# Seat" other="# Seats" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: _(msg`Status`),
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => {
|
||||
const { status, paid } = row.original;
|
||||
|
||||
if (!status) {
|
||||
return paid ? <Trans>Paid</Trans> : <Trans>Unpaid</Trans>;
|
||||
}
|
||||
|
||||
return status.charAt(0).toUpperCase() + status.slice(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: _(msg`Amount`),
|
||||
accessorKey: 'total',
|
||||
cell: ({ row }) => formatCurrency(row.original.currency, row.original.total / 100),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
disabled={typeof row.original.hostedInvoicePdf !== 'string'}
|
||||
>
|
||||
<Link to={row.original.hostedInvoicePdf ?? ''} target="_blank">
|
||||
<Trans>View</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
disabled={typeof row.original.hostedInvoicePdf !== 'string'}
|
||||
>
|
||||
<Link to={row.original.invoicePdf ?? ''} target="_blank">
|
||||
<Trans>Download</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={results.data}
|
||||
perPage={results.perPage}
|
||||
currentPage={results.currentPage}
|
||||
totalPages={results.totalPages}
|
||||
error={{
|
||||
enable: isLoadingError,
|
||||
}}
|
||||
skeleton={{
|
||||
enable: isLoading,
|
||||
rows: 3,
|
||||
component: (
|
||||
<>
|
||||
<TableCell className="w-1/3 py-4 pr-4">
|
||||
<div className="flex w-full flex-row items-center">
|
||||
<Skeleton className="h-7 w-7 flex-shrink-0 rounded" />
|
||||
|
||||
<div className="ml-2 flex flex-grow flex-col">
|
||||
<Skeleton className="h-4 w-1/2 max-w-[8rem]" />
|
||||
<Skeleton className="mt-1 h-4 w-2/3 max-w-[12rem]" />
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-12 rounded-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-20 rounded-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-row justify-end space-x-2">
|
||||
<Skeleton className="h-10 w-20 rounded" />
|
||||
<Skeleton className="h-10 w-16 rounded" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
@ -26,12 +26,22 @@ export type TemplatesTableActionDropdownProps = {
|
||||
};
|
||||
templateRootPath: string;
|
||||
teamId?: number;
|
||||
onDelete?: () => Promise<void> | void;
|
||||
onMove?: ({
|
||||
templateId,
|
||||
teamUrl,
|
||||
}: {
|
||||
templateId: number;
|
||||
teamUrl: string;
|
||||
}) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const TemplatesTableActionDropdown = ({
|
||||
row,
|
||||
templateRootPath,
|
||||
teamId,
|
||||
onDelete,
|
||||
onMove,
|
||||
}: TemplatesTableActionDropdownProps) => {
|
||||
const { user } = useSession();
|
||||
|
||||
@ -104,12 +114,14 @@ export const TemplatesTableActionDropdown = ({
|
||||
templateId={row.id}
|
||||
open={isMoveDialogOpen}
|
||||
onOpenChange={setMoveDialogOpen}
|
||||
onMove={onMove}
|
||||
/>
|
||||
|
||||
<TemplateDeleteDialog
|
||||
id={row.id}
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@ -17,7 +17,7 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
import { TemplateType } from '~/components/formatter/template-type';
|
||||
import { TemplateType } from '~/components/general/template/template-type';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { TemplateUseDialog } from '../dialogs/template-use-dialog';
|
||||
|
||||
@ -0,0 +1,168 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
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 { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
import { TeamLeaveDialog } from '~/components/dialogs/team-leave-dialog';
|
||||
|
||||
export const UserSettingsCurrentTeamsDataTable = () => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.team.findTeams.useQuery(
|
||||
{
|
||||
query: parsedSearchParams.query,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
},
|
||||
{
|
||||
placeholderData: (previousData) => previousData,
|
||||
},
|
||||
);
|
||||
|
||||
const onPaginationChange = (page: number, perPage: number) => {
|
||||
updateSearchParams({
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
};
|
||||
|
||||
const results = data ?? {
|
||||
data: [],
|
||||
perPage: 10,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
header: _(msg`Team`),
|
||||
accessorKey: 'name',
|
||||
cell: ({ row }) => (
|
||||
<Link to={`/t/${row.original.url}`} preventScrollReset={true}>
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(row.original.avatarImageId)}
|
||||
avatarClass="h-12 w-12"
|
||||
avatarFallback={row.original.name.slice(0, 1).toUpperCase()}
|
||||
primaryText={
|
||||
<span className="text-foreground/80 font-semibold">{row.original.name}</span>
|
||||
}
|
||||
secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/t/${row.original.url}`}
|
||||
/>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: _(msg`Role`),
|
||||
accessorKey: 'role',
|
||||
cell: ({ row }) =>
|
||||
row.original.ownerUserId === row.original.currentTeamMember.userId
|
||||
? _(msg`Owner`)
|
||||
: _(TEAM_MEMBER_ROLE_MAP[row.original.currentTeamMember.role]),
|
||||
},
|
||||
{
|
||||
header: _(msg`Member Since`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end space-x-2">
|
||||
{canExecuteTeamAction('MANAGE_TEAM', row.original.currentTeamMember.role) && (
|
||||
<Button variant="outline" asChild>
|
||||
<Link to={`/t/${row.original.url}/settings`}>
|
||||
<Trans>Manage</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<TeamLeaveDialog
|
||||
teamId={row.original.id}
|
||||
teamName={row.original.name}
|
||||
teamAvatarImageId={row.original.avatarImageId}
|
||||
role={row.original.currentTeamMember.role}
|
||||
trigger={
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={row.original.ownerUserId === row.original.currentTeamMember.userId}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<Trans>Leave</Trans>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={results.data}
|
||||
perPage={results.perPage}
|
||||
currentPage={results.currentPage}
|
||||
totalPages={results.totalPages}
|
||||
onPaginationChange={onPaginationChange}
|
||||
error={{
|
||||
enable: isLoadingError,
|
||||
}}
|
||||
skeleton={{
|
||||
enable: isLoading,
|
||||
rows: 3,
|
||||
component: (
|
||||
<>
|
||||
<TableCell className="w-1/3 py-4 pr-4">
|
||||
<div className="flex w-full flex-row items-center">
|
||||
<Skeleton className="h-12 w-12 flex-shrink-0 rounded-full" />
|
||||
|
||||
<div className="ml-2 flex flex-grow flex-col">
|
||||
<Skeleton className="h-4 w-1/2 max-w-[8rem]" />
|
||||
<Skeleton className="mt-1 h-4 w-2/3 max-w-[12rem]" />
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-12 rounded-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-20 rounded-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-row justify-end space-x-2">
|
||||
<Skeleton className="h-10 w-20 rounded" />
|
||||
<Skeleton className="h-10 w-16 rounded" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,58 @@
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type UserSettingsPendingTeamsTableActionsProps = {
|
||||
className?: string;
|
||||
pendingTeamId: number;
|
||||
onPayClick: (pendingTeamId: number) => void;
|
||||
};
|
||||
|
||||
export const UserSettingsPendingTeamsTableActions = ({
|
||||
className,
|
||||
pendingTeamId,
|
||||
onPayClick,
|
||||
}: UserSettingsPendingTeamsTableActionsProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: deleteTeamPending, isPending: deletingTeam } =
|
||||
trpc.team.deleteTeamPending.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Pending team deleted.`),
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`We encountered an unknown error while attempting to delete the pending team. Please try again later.`,
|
||||
),
|
||||
duration: 10000,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<fieldset disabled={deletingTeam} className={cn('flex justify-end space-x-2', className)}>
|
||||
<Button variant="outline" onClick={() => onPayClick(pendingTeamId)}>
|
||||
<Trans>Pay</Trans>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
loading={deletingTeam}
|
||||
onClick={async () => deleteTeamPending({ pendingTeamId: pendingTeamId })}
|
||||
>
|
||||
<Trans>Remove</Trans>
|
||||
</Button>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,149 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
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 { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
import { TeamCheckoutCreateDialog } from '~/components/dialogs/team-checkout-create-dialog';
|
||||
|
||||
import { UserSettingsPendingTeamsTableActions } from './user-settings-pending-teams-table-actions';
|
||||
|
||||
export const UserSettingsPendingTeamsDataTable = () => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const [checkoutPendingTeamId, setCheckoutPendingTeamId] = useState<number | null>(null);
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.team.findTeamsPending.useQuery(
|
||||
{
|
||||
query: parsedSearchParams.query,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
},
|
||||
{
|
||||
placeholderData: (previousData) => previousData,
|
||||
},
|
||||
);
|
||||
|
||||
const onPaginationChange = (page: number, perPage: number) => {
|
||||
updateSearchParams({
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
};
|
||||
|
||||
const results = data ?? {
|
||||
data: [],
|
||||
perPage: 10,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
header: _(msg`Team`),
|
||||
accessorKey: 'name',
|
||||
cell: ({ row }) => (
|
||||
<AvatarWithText
|
||||
avatarClass="h-12 w-12"
|
||||
avatarFallback={row.original.name.slice(0, 1).toUpperCase()}
|
||||
primaryText={
|
||||
<span className="text-foreground/80 font-semibold">{row.original.name}</span>
|
||||
}
|
||||
secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/t/${row.original.url}`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: _(msg`Created on`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => (
|
||||
<UserSettingsPendingTeamsTableActions
|
||||
className="justify-end"
|
||||
pendingTeamId={row.original.id}
|
||||
onPayClick={setCheckoutPendingTeamId}
|
||||
/>
|
||||
),
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const searchParamCheckout = searchParams?.get('checkout');
|
||||
|
||||
if (searchParamCheckout && !isNaN(parseInt(searchParamCheckout))) {
|
||||
setCheckoutPendingTeamId(parseInt(searchParamCheckout));
|
||||
updateSearchParams({ checkout: null });
|
||||
}
|
||||
}, [searchParams, updateSearchParams]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={results.data}
|
||||
perPage={results.perPage}
|
||||
currentPage={results.currentPage}
|
||||
totalPages={results.totalPages}
|
||||
onPaginationChange={onPaginationChange}
|
||||
error={{
|
||||
enable: isLoadingError,
|
||||
}}
|
||||
skeleton={{
|
||||
enable: isLoading,
|
||||
rows: 3,
|
||||
component: (
|
||||
<>
|
||||
<TableCell className="w-1/3 py-4 pr-4">
|
||||
<div className="flex w-full flex-row items-center">
|
||||
<Skeleton className="h-12 w-12 flex-shrink-0 rounded-full" />
|
||||
|
||||
<div className="ml-2 flex flex-grow flex-col">
|
||||
<Skeleton className="h-4 w-1/2 max-w-[8rem]" />
|
||||
<Skeleton className="mt-1 h-4 w-2/3 max-w-[12rem]" />
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-12 rounded-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-row justify-end space-x-2">
|
||||
<Skeleton className="h-10 w-16 rounded" />
|
||||
<Skeleton className="h-10 w-20 rounded" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
|
||||
</DataTable>
|
||||
|
||||
<TeamCheckoutCreateDialog
|
||||
pendingTeamId={checkoutPendingTeamId}
|
||||
onClose={() => setCheckoutPendingTeamId(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,92 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
import { UserSettingsCurrentTeamsDataTable } from './user-settings-current-teams-table';
|
||||
import { UserSettingsPendingTeamsDataTable } from './user-settings-pending-teams-table';
|
||||
|
||||
export const UserSettingsTeamsPageDataTable = () => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
|
||||
|
||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 500);
|
||||
|
||||
const currentTab = searchParams?.get('tab') === 'pending' ? 'pending' : 'active';
|
||||
|
||||
const { data } = trpc.team.findTeamsPending.useQuery(
|
||||
{},
|
||||
{
|
||||
placeholderData: (previousData) => previousData,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle debouncing the search query.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!pathname) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
|
||||
params.set('query', debouncedSearchQuery);
|
||||
|
||||
if (debouncedSearchQuery === '') {
|
||||
params.delete('query');
|
||||
}
|
||||
|
||||
void navigate(`${pathname}?${params.toString()}`);
|
||||
}, [debouncedSearchQuery, pathname, navigate, searchParams]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="my-4 flex flex-row items-center justify-between space-x-4">
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={_(msg`Search`)}
|
||||
/>
|
||||
|
||||
<Tabs value={currentTab} className="flex-shrink-0 overflow-x-auto">
|
||||
<TabsList>
|
||||
<TabsTrigger className="min-w-[60px]" value="active" asChild>
|
||||
<Link to={pathname ?? '/'}>
|
||||
<Trans>Active</Trans>
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger className="min-w-[60px]" value="pending" asChild>
|
||||
<Link to={`${pathname}?tab=pending`}>
|
||||
<Trans>Pending</Trans>
|
||||
{data && data.count > 0 && (
|
||||
<span className="ml-1 hidden opacity-50 md:inline-block">{data.count}</span>
|
||||
)}
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{currentTab === 'pending' ? (
|
||||
<UserSettingsPendingTeamsDataTable />
|
||||
) : (
|
||||
<UserSettingsCurrentTeamsDataTable />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user