fix: dialog close on refresh (#1135)

Previously dialogs would be closed upon refocusing the browser tab due to router refetches occuring which would cause data-table columns to re-render. This is now resolved by extracting the column definitions outside of the returning render and into a memo hook.
This commit is contained in:
David Nguyen
2024-08-27 22:13:52 +09:00
committed by GitHub
parent 75c8772a02
commit 38a4b0f299
19 changed files with 1069 additions and 978 deletions

View File

@ -1,5 +1,7 @@
'use client';
import { useMemo } from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
@ -14,6 +16,7 @@ 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';
@ -58,70 +61,74 @@ export const CurrentUserTeamsDataTable = () => {
totalPages: 1,
};
const columns = useMemo(() => {
return [
{
header: _(msg`Team`),
accessorKey: 'name',
cell: ({ row }) => (
<Link href={`/t/${row.original.url}`} scroll={false}>
<AvatarWithText
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${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={`${WEBAPP_BASE_URL}/t/${row.original.url}`}
/>
</Link>
),
},
{
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) =>
row.original.ownerUserId === row.original.currentTeamMember.userId
? 'Owner'
: _(TEAM_MEMBER_ROLE_MAP[row.original.currentTeamMember.role]),
},
{
header: _(msg`Member Since`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate 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 href={`/t/${row.original.url}/settings`}>
<Trans>Manage</Trans>
</Link>
</Button>
)}
<LeaveTeamDialog
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={[
{
header: _(msg`Team`),
accessorKey: 'name',
cell: ({ row }) => (
<Link href={`/t/${row.original.url}`} scroll={false}>
<AvatarWithText
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${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={`${WEBAPP_BASE_URL}/t/${row.original.url}`}
/>
</Link>
),
},
{
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) =>
row.original.ownerUserId === row.original.currentTeamMember.userId
? 'Owner'
: _(TEAM_MEMBER_ROLE_MAP[row.original.currentTeamMember.role]),
},
{
header: _(msg`Member Since`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate 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 href={`/t/${row.original.url}/settings`}>
<Trans>Manage</Trans>
</Link>
</Button>
)}
<LeaveTeamDialog
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>
),
},
]}
columns={columns}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}

View File

@ -1,6 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
@ -12,6 +12,7 @@ import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { ZBaseTableSearchParamsSchema } 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';
@ -59,6 +60,40 @@ export const PendingUserTeamsDataTable = () => {
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={`${WEBAPP_BASE_URL}/t/${row.original.url}`}
/>
),
},
{
header: _(msg`Created on`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
id: 'actions',
cell: ({ row }) => (
<PendingUserTeamsDataTableActions
className="justify-end"
pendingTeamId={row.original.id}
onPayClick={setCheckoutPendingTeamId}
/>
),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
useEffect(() => {
const searchParamCheckout = searchParams?.get('checkout');
@ -71,37 +106,7 @@ export const PendingUserTeamsDataTable = () => {
return (
<>
<DataTable
columns={[
{
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={`${WEBAPP_BASE_URL}/t/${row.original.url}`}
/>
),
},
{
header: _(msg`Created on`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
id: 'actions',
cell: ({ row }) => (
<PendingUserTeamsDataTableActions
className="justify-end"
pendingTeamId={row.original.id}
onPayClick={setCheckoutPendingTeamId}
/>
),
},
]}
columns={columns}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}

View File

@ -1,5 +1,7 @@
'use client';
import { useMemo } from 'react';
import Link from 'next/link';
import { Plural, Trans, msg } from '@lingui/macro';
@ -9,6 +11,7 @@ import { DateTime } from 'luxon';
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';
@ -46,72 +49,76 @@ export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesData
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 href={row.original.hostedInvoicePdf ?? ''} target="_blank">
<Trans>View</Trans>
</Link>
</Button>
<Button
variant="outline"
asChild
disabled={typeof row.original.hostedInvoicePdf !== 'string'}
>
<Link href={row.original.invoicePdf ?? ''} target="_blank">
<Trans>Download</Trans>
</Link>
</Button>
</div>
),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
return (
<DataTable
columns={[
{
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 href={row.original.hostedInvoicePdf ?? ''} target="_blank">
<Trans>View</Trans>
</Link>
</Button>
<Button
variant="outline"
asChild
disabled={typeof row.original.hostedInvoicePdf !== 'string'}
>
<Link href={row.original.invoicePdf ?? ''} target="_blank">
<Trans>Download</Trans>
</Link>
</Button>
</div>
),
},
]}
columns={columns}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}

View File

@ -1,5 +1,7 @@
'use client';
import { useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
@ -11,6 +13,7 @@ import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
import { ZBaseTableSearchParamsSchema } 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 {
@ -102,74 +105,78 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
totalPages: 1,
};
const columns = useMemo(() => {
return [
{
header: _(msg`Team Member`),
cell: ({ row }) => {
return (
<AvatarWithText
avatarClass="h-12 w-12"
avatarFallback={row.original.email.slice(0, 1).toUpperCase()}
primaryText={
<span className="text-foreground/80 font-semibold">{row.original.email}</span>
}
/>
);
},
},
{
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) => _(TEAM_MEMBER_ROLE_MAP[row.original.role]) ?? row.original.role,
},
{
header: _(msg`Invited At`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: _(msg`Actions`),
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>
<Trans>Actions</Trans>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={async () =>
resendTeamMemberInvitation({
teamId,
invitationId: row.original.id,
})
}
>
<History className="mr-2 h-4 w-4" />
<Trans>Resend</Trans>
</DropdownMenuItem>
<DropdownMenuItem
onClick={async () =>
deleteTeamMemberInvitations({
teamId,
invitationIds: [row.original.id],
})
}
>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Remove</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
return (
<DataTable
columns={[
{
header: _(msg`Team Member`),
cell: ({ row }) => {
return (
<AvatarWithText
avatarClass="h-12 w-12"
avatarFallback={row.original.email.slice(0, 1).toUpperCase()}
primaryText={
<span className="text-foreground/80 font-semibold">{row.original.email}</span>
}
/>
);
},
},
{
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) => _(TEAM_MEMBER_ROLE_MAP[row.original.role]) ?? row.original.role,
},
{
header: _(msg`Invited At`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: _(msg`Actions`),
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>
<Trans>Actions</Trans>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={async () =>
resendTeamMemberInvitation({
teamId,
invitationId: row.original.id,
})
}
>
<History className="mr-2 h-4 w-4" />
<Trans>Resend</Trans>
</DropdownMenuItem>
<DropdownMenuItem
onClick={async () =>
deleteTeamMemberInvitations({
teamId,
invitationIds: [row.original.id],
})
}
>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Remove</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
]}
columns={columns}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}

View File

@ -1,5 +1,7 @@
'use client';
import { useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
@ -14,6 +16,7 @@ import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
import type { TeamMemberRole } from '@documenso/prisma/client';
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 {
@ -79,100 +82,104 @@ export const TeamMembersDataTable = ({
totalPages: 1,
};
const columns = useMemo(() => {
return [
{
header: _(msg`Team Member`),
cell: ({ row }) => {
const avatarFallbackText = row.original.user.name
? extractInitials(row.original.user.name)
: row.original.user.email.slice(0, 1).toUpperCase();
return (
<AvatarWithText
avatarClass="h-12 w-12"
avatarFallback={avatarFallbackText}
primaryText={
<span className="text-foreground/80 font-semibold">{row.original.user.name}</span>
}
secondaryText={row.original.user.email}
/>
);
},
},
{
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) =>
teamOwnerUserId === row.original.userId
? 'Owner'
: _(TEAM_MEMBER_ROLE_MAP[row.original.role]),
},
{
header: _(msg`Member Since`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: _(msg`Actions`),
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>
<Trans>Actions</Trans>
</DropdownMenuLabel>
<UpdateTeamMemberDialog
currentUserTeamRole={currentUserTeamRole}
teamId={row.original.teamId}
teamMemberId={row.original.id}
teamMemberName={row.original.user.name ?? ''}
teamMemberRole={row.original.role}
trigger={
<DropdownMenuItem
disabled={
teamOwnerUserId === row.original.userId ||
!isTeamRoleWithinUserHierarchy(currentUserTeamRole, row.original.role)
}
onSelect={(e) => e.preventDefault()}
title="Update team member role"
>
<Edit className="mr-2 h-4 w-4" />
<Trans>Update role</Trans>
</DropdownMenuItem>
}
/>
<DeleteTeamMemberDialog
teamId={teamId}
teamName={teamName}
teamMemberId={row.original.id}
teamMemberName={row.original.user.name ?? ''}
teamMemberEmail={row.original.user.email}
trigger={
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
disabled={
teamOwnerUserId === row.original.userId ||
!isTeamRoleWithinUserHierarchy(currentUserTeamRole, row.original.role)
}
title={_(msg`Remove team member`)}
>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Remove</Trans>
</DropdownMenuItem>
}
/>
</DropdownMenuContent>
</DropdownMenu>
),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
return (
<DataTable
columns={[
{
header: _(msg`Team Member`),
cell: ({ row }) => {
const avatarFallbackText = row.original.user.name
? extractInitials(row.original.user.name)
: row.original.user.email.slice(0, 1).toUpperCase();
return (
<AvatarWithText
avatarClass="h-12 w-12"
avatarFallback={avatarFallbackText}
primaryText={
<span className="text-foreground/80 font-semibold">{row.original.user.name}</span>
}
secondaryText={row.original.user.email}
/>
);
},
},
{
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) =>
teamOwnerUserId === row.original.userId
? 'Owner'
: _(TEAM_MEMBER_ROLE_MAP[row.original.role]),
},
{
header: _(msg`Member Since`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: _(msg`Actions`),
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>
<Trans>Actions</Trans>
</DropdownMenuLabel>
<UpdateTeamMemberDialog
currentUserTeamRole={currentUserTeamRole}
teamId={row.original.teamId}
teamMemberId={row.original.id}
teamMemberName={row.original.user.name ?? ''}
teamMemberRole={row.original.role}
trigger={
<DropdownMenuItem
disabled={
teamOwnerUserId === row.original.userId ||
!isTeamRoleWithinUserHierarchy(currentUserTeamRole, row.original.role)
}
onSelect={(e) => e.preventDefault()}
title="Update team member role"
>
<Edit className="mr-2 h-4 w-4" />
<Trans>Update role</Trans>
</DropdownMenuItem>
}
/>
<DeleteTeamMemberDialog
teamId={teamId}
teamName={teamName}
teamMemberId={row.original.id}
teamMemberName={row.original.user.name ?? ''}
teamMemberEmail={row.original.user.email}
trigger={
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
disabled={
teamOwnerUserId === row.original.userId ||
!isTeamRoleWithinUserHierarchy(currentUserTeamRole, row.original.role)
}
title={_(msg`Remove team member`)}
>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Remove</Trans>
</DropdownMenuItem>
}
/>
</DropdownMenuContent>
</DropdownMenu>
),
},
]}
columns={columns}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}