mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-30 14:57:59 +00:00
react-table went from 8 to 9 with nothing migrated. 9 has no useReactTable and no getCoreRowModel, so the one component every list in the app renders through could not be imported, and ColumnDef takes the table's features as its first type argument, so all 42 column lists stopped typing their rows: 217 of the 498 errors in the app's typecheck. DataTable now names the four features it uses — paging, row selection, column visibility, column sizing — and calls useTable. DataTableColumnDef is ColumnDef over those features, so a column list still says only what a row is. The second type parameter goes: 9's table takes columns of unknown value, and no caller ever passed one. Two things the types turned up. `enableSorting: false` sat on two select columns of a table that has never sorted; without the sorting feature the option does not exist, and neither does the sorting. The organisation insights table erased its three row types through `as unknown as` to feed one DataTable; it renders one per view, typed, and the casts are gone. The server rejects any other view before the component renders. data-table.test.tsx renders the table and drives the instance: headers and cells, a hidden column, a sized one, one-based paging read from the props and reported back, selection by the caller's row id. Against the old file it fails 7 of 7 at the import. Co-authored-by: Hanzo Dev <[email protected]>
311 lines
11 KiB
TypeScript
311 lines
11 KiB
TypeScript
import { useMemo, useTransition } from 'react';
|
|
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react';
|
|
import { Trans } from '@lingui/react/macro';
|
|
import { AlertTriangle, Globe2Icon, InfoIcon, Link2Icon, Loader, LockIcon } from 'lucide-react';
|
|
import { Link } from 'react-router';
|
|
|
|
import { useUpdateSearchParams } from '@hanzo/esign-lib/client-only/hooks/use-update-search-params';
|
|
import { useCurrentOrganisation } from '@hanzo/esign-lib/client-only/providers/organisation';
|
|
import { useLimits } from '@hanzo/esign-lib/server-only/limits/provider/client';
|
|
import { formatTemplatesPath } from '@hanzo/esign-lib/utils/teams';
|
|
import type { TFindTemplatesResponse } from '@hanzo/esign-trpc/server/template-router/schema';
|
|
import { Alert, AlertDescription, AlertTitle } from '@hanzo/esign-ui/primitives/alert';
|
|
import { Checkbox } from '@hanzo/esign-ui/primitives/checkbox';
|
|
import type { DataTableColumnDef, RowSelectionState } from '@hanzo/esign-ui/primitives/data-table';
|
|
import { DataTable } from '@hanzo/esign-ui/primitives/data-table';
|
|
import { DataTablePagination } from '@hanzo/esign-ui/primitives/data-table-pagination';
|
|
import { Skeleton } from '@hanzo/esign-ui/primitives/skeleton';
|
|
import { TableCell } from '@hanzo/esign-ui/primitives/table';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@hanzo/esign-ui/primitives/tooltip';
|
|
|
|
import { TemplateType } from '~/components/general/template/template-type';
|
|
import { useCurrentTeam } from '~/providers/team';
|
|
|
|
import { TemplateUseDialog } from '../dialogs/template-use-dialog';
|
|
import { TemplateDirectLinkBadge } from '../general/template/template-direct-link-badge';
|
|
import { TemplatesTableActionDropdown } from './templates-table-action-dropdown';
|
|
|
|
type TemplatesTableProps = {
|
|
data?: TFindTemplatesResponse;
|
|
isLoading?: boolean;
|
|
isLoadingError?: boolean;
|
|
documentRootPath: string;
|
|
templateRootPath: string;
|
|
enableSelection?: boolean;
|
|
rowSelection?: RowSelectionState;
|
|
onRowSelectionChange?: (selection: RowSelectionState) => void;
|
|
};
|
|
|
|
type TemplatesTableRow = TFindTemplatesResponse['data'][number];
|
|
|
|
export const TemplatesTable = ({
|
|
data,
|
|
isLoading,
|
|
isLoadingError,
|
|
documentRootPath,
|
|
templateRootPath,
|
|
enableSelection,
|
|
rowSelection,
|
|
onRowSelectionChange,
|
|
}: TemplatesTableProps) => {
|
|
const { _, i18n } = useLingui();
|
|
const { remaining } = useLimits();
|
|
|
|
const team = useCurrentTeam();
|
|
const organisation = useCurrentOrganisation();
|
|
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
const updateSearchParams = useUpdateSearchParams();
|
|
|
|
const formatTemplateLink = (row: TemplatesTableRow) => {
|
|
const path = formatTemplatesPath(team.url);
|
|
|
|
return `${path}/${row.envelopeId}`;
|
|
};
|
|
|
|
const columns = useMemo(() => {
|
|
const cols: DataTableColumnDef<TemplatesTableRow>[] = [];
|
|
|
|
if (enableSelection) {
|
|
cols.push({
|
|
id: 'select',
|
|
header: ({ table }) => (
|
|
<Checkbox
|
|
checked={table.getIsAllPageRowsSelected()}
|
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
aria-label={_(msg`Select all`)}
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
),
|
|
cell: ({ row }) => (
|
|
<Checkbox
|
|
checked={row.getIsSelected()}
|
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
aria-label={_(msg`Select row`)}
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
),
|
|
enableHiding: false,
|
|
size: 40,
|
|
});
|
|
}
|
|
|
|
cols.push(
|
|
{
|
|
header: _(msg`Created`),
|
|
accessorKey: 'createdAt',
|
|
cell: ({ row }) => i18n.date(row.original.createdAt),
|
|
},
|
|
{
|
|
header: _(msg`Title`),
|
|
cell: ({ row }) => (
|
|
<Link
|
|
to={formatTemplateLink(row.original)}
|
|
className="block max-w-[10rem] cursor-pointer truncate font-medium hover:underline md:max-w-[20rem]"
|
|
>
|
|
{row.original.title}
|
|
</Link>
|
|
),
|
|
},
|
|
{
|
|
header: () => (
|
|
<div className="flex flex-row items-center">
|
|
<Trans>Type</Trans>
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<InfoIcon className="mx-2 h-4 w-4" />
|
|
</TooltipTrigger>
|
|
|
|
<TooltipContent className="text-foreground max-w-md space-y-2 !p-0">
|
|
<ul className="text-muted-foreground space-y-0.5 divide-y [&>li]:p-4">
|
|
<li>
|
|
<h2 className="mb-2 flex flex-row items-center font-semibold">
|
|
<Globe2Icon className="text-foreground mr-2 h-5 w-5" />
|
|
<Trans>Public</Trans>
|
|
</h2>
|
|
|
|
<p>
|
|
<Trans>
|
|
Public templates are connected to your public profile. Any modifications to
|
|
public templates will also appear in your public profile.
|
|
</Trans>
|
|
</p>
|
|
</li>
|
|
<li>
|
|
<div className="mb-2 flex w-fit flex-row items-center rounded border border-neutral-300 bg-neutral-200 px-1.5 py-0.5 text-xs dark:border-neutral-500 dark:bg-neutral-600">
|
|
<Link2Icon className="mr-1 h-3 w-3" />
|
|
<Trans>direct link</Trans>
|
|
</div>
|
|
|
|
<p>
|
|
<Trans>
|
|
Direct link templates contain one dynamic recipient placeholder. Anyone with
|
|
access to this link can sign the document, and it will then appear on your
|
|
documents page.
|
|
</Trans>
|
|
</p>
|
|
</li>
|
|
<li>
|
|
<h2 className="mb-2 flex flex-row items-center font-semibold">
|
|
<LockIcon className="text-muted-foreground mr-2 h-5 w-5" />
|
|
{team?.id ? <Trans>Team Only</Trans> : <Trans>Private</Trans>}
|
|
</h2>
|
|
|
|
<p>
|
|
{team?.id ? (
|
|
<Trans>
|
|
Team only templates are not linked anywhere and are visible only to your
|
|
team.
|
|
</Trans>
|
|
) : (
|
|
<Trans>Private templates can only be modified and viewed by you.</Trans>
|
|
)}
|
|
</p>
|
|
</li>
|
|
</ul>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
),
|
|
accessorKey: 'type',
|
|
cell: ({ row }) => (
|
|
<div className="flex flex-row items-center">
|
|
<TemplateType type={row.original.type} />
|
|
|
|
{row.original.directLink?.token && (
|
|
<TemplateDirectLinkBadge
|
|
className="ml-2"
|
|
token={row.original.directLink.token}
|
|
enabled={row.original.directLink.enabled}
|
|
/>
|
|
)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
header: _(msg`Actions`),
|
|
accessorKey: 'actions',
|
|
cell: ({ row }) => {
|
|
return (
|
|
<div className="flex items-center gap-x-4">
|
|
<TemplateUseDialog
|
|
envelopeId={row.original.envelopeId}
|
|
templateId={row.original.id}
|
|
templateSigningOrder={row.original.templateMeta?.signingOrder}
|
|
documentDistributionMethod={row.original.templateMeta?.distributionMethod}
|
|
recipients={row.original.recipients}
|
|
documentRootPath={documentRootPath}
|
|
/>
|
|
|
|
<TemplatesTableActionDropdown
|
|
row={row.original}
|
|
teamId={team?.id}
|
|
templateRootPath={templateRootPath}
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
);
|
|
|
|
return cols;
|
|
}, [documentRootPath, team?.id, templateRootPath, enableSelection]);
|
|
|
|
const onPaginationChange = (page: number, perPage: number) => {
|
|
startTransition(() => {
|
|
updateSearchParams({
|
|
page,
|
|
perPage,
|
|
});
|
|
});
|
|
};
|
|
|
|
const results = data ?? {
|
|
data: [],
|
|
perPage: 10,
|
|
currentPage: 1,
|
|
totalPages: 1,
|
|
};
|
|
|
|
return (
|
|
<div className="relative">
|
|
{remaining.documents === 0 && (
|
|
<Alert variant="warning" className="mb-4">
|
|
<AlertTriangle className="h-4 w-4" />
|
|
<AlertTitle>
|
|
<Trans>Document Limit Exceeded!</Trans>
|
|
</AlertTitle>
|
|
<AlertDescription className="mt-2">
|
|
<Trans>
|
|
You have reached your document limit.{' '}
|
|
<Link
|
|
className="underline underline-offset-4"
|
|
to={`/o/${organisation.url}/settings/billing`}
|
|
>
|
|
Upgrade your account to continue!
|
|
</Link>
|
|
</Trans>
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<DataTable
|
|
columns={columns}
|
|
data={results.data}
|
|
perPage={results.perPage}
|
|
currentPage={results.currentPage}
|
|
totalPages={results.totalPages}
|
|
onPaginationChange={onPaginationChange}
|
|
enableRowSelection={enableSelection}
|
|
rowSelection={rowSelection}
|
|
onRowSelectionChange={onRowSelectionChange}
|
|
getRowId={(row) => row.envelopeId}
|
|
error={{
|
|
enable: isLoadingError || false,
|
|
}}
|
|
skeleton={{
|
|
enable: isLoading || false,
|
|
rows: 5,
|
|
component: (
|
|
<>
|
|
{enableSelection && (
|
|
<TableCell className="w-10">
|
|
<Skeleton className="h-4 w-4 rounded" />
|
|
</TableCell>
|
|
)}
|
|
<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>
|
|
</>
|
|
),
|
|
}}
|
|
>
|
|
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
|
|
</DataTable>
|
|
|
|
{isPending && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
|
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|