From ed7a0011c733e7e8c554446a28b7f4e6d7269743 Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Wed, 21 Jan 2026 03:43:24 +0000 Subject: [PATCH 001/189] fix: sync envelope state after direct link changes (#2257) --- .../dialogs/template-direct-link-dialog.tsx | 18 +- .../envelope-editor-recipient-form.tsx | 520 ++++++++---------- .../envelope-editor/envelope-editor.tsx | 4 +- .../hooks/use-editor-recipients.ts | 106 ++++ .../providers/envelope-editor-provider.tsx | 13 + 5 files changed, 370 insertions(+), 291 deletions(-) create mode 100644 packages/lib/client-only/hooks/use-editor-recipients.ts diff --git a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx index 4e267b0e5..b644045d6 100644 --- a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -54,6 +54,8 @@ type TemplateDirectLinkDialogProps = { directLink?: Pick | null; recipients: Recipient[]; trigger?: React.ReactNode; + onCreateSuccess?: () => Promise | void; + onDeleteSuccess?: () => Promise | void; }; type TemplateDirectLinkStep = 'ONBOARD' | 'SELECT_RECIPIENT' | 'MANAGE' | 'CONFIRM_DELETE'; @@ -63,6 +65,8 @@ export const TemplateDirectLinkDialog = ({ directLink, recipients, trigger, + onCreateSuccess, + onDeleteSuccess, }: TemplateDirectLinkDialogProps) => { const { toast } = useToast(); const { quota, remaining } = useLimits(); @@ -97,6 +101,7 @@ export const TemplateDirectLinkDialog = ({ } = trpcReact.template.createTemplateDirectLink.useMutation({ onSuccess: async (data) => { await revalidate(); + await onCreateSuccess?.(); setToken(data.token); setIsEnabled(data.enabled); @@ -142,6 +147,7 @@ export const TemplateDirectLinkDialog = ({ trpcReact.template.deleteTemplateDirectLink.useMutation({ onSuccess: async () => { await revalidate(); + await onDeleteSuccess?.(); setOpen(false); setToken(null); @@ -234,7 +240,7 @@ export const TemplateDirectLinkDialog = ({

{_(step.title)}

-

{_(step.description)}

+

{_(step.description)}

))} @@ -320,13 +326,13 @@ export const TemplateDirectLinkDialog = ({ onClick={async () => onRecipientTableRowClick(row.id)} > -
+

{row.name}

-

{row.email}

+

{row.email}

- + {_(RECIPIENT_ROLES_DESCRIPTION[row.role].roleName)} @@ -350,7 +356,7 @@ export const TemplateDirectLinkDialog = ({
{validDirectTemplateRecipients.length !== 0 && ( -

+

Or

)} @@ -392,7 +398,7 @@ export const TemplateDirectLinkDialog = ({ - + Disabling direct link signing will prevent anyone from accessing the link. diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx index 7b17e9d97..ab83b3189 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DragDropContext, @@ -7,28 +7,23 @@ import { Droppable, type SensorAPI, } from '@hello-pangea/dnd'; -import { zodResolver } from '@hookform/resolvers/zod'; import { plural } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client'; import { motion } from 'framer-motion'; import { GripVerticalIcon, HelpCircleIcon, PlusIcon, SparklesIcon, TrashIcon } from 'lucide-react'; -import { useFieldArray, useForm, useWatch } from 'react-hook-form'; +import { useFieldArray, useWatch } from 'react-hook-form'; import { useRevalidator, useSearchParams } from 'react-router'; -import { isDeepEqual, prop, sortBy } from 'remeda'; -import { z } from 'zod'; +import { isDeepEqual } from 'remeda'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; +import { ZEditorRecipientsFormSchema } from '@documenso/lib/client-only/hooks/use-editor-recipients'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useSession } from '@documenso/lib/client-only/providers/session'; import type { TDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema'; -import { - ZRecipientActionAuthTypesSchema, - ZRecipientAuthOptionsSchema, -} from '@documenso/lib/types/document-auth'; -import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient'; +import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth'; import { nanoid } from '@documenso/lib/universal/id'; import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients'; import { trpc } from '@documenso/trpc/react'; @@ -67,26 +62,9 @@ import { AiFeaturesEnableDialog } from '~/components/dialogs/ai-features-enable- import { AiRecipientDetectionDialog } from '~/components/dialogs/ai-recipient-detection-dialog'; import { useCurrentTeam } from '~/providers/team'; -const ZEnvelopeRecipientsForm = z.object({ - signers: z.array( - z.object({ - formId: z.string().min(1), - id: z.number().optional(), - email: ZRecipientEmailSchema, - name: z.string(), - role: z.nativeEnum(RecipientRole), - signingOrder: z.number().optional(), - actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]), - }), - ), - signingOrder: z.nativeEnum(DocumentSigningOrder), - allowDictateNextSigner: z.boolean().default(false), -}); - -type TEnvelopeRecipientsForm = z.infer; - export const EnvelopeEditorRecipientForm = () => { - const { envelope, setRecipientsDebounced, updateEnvelope } = useCurrentEnvelopeEditor(); + const { envelope, setRecipientsDebounced, updateEnvelope, editorRecipients } = + useCurrentEnvelopeEditor(); const organisation = useCurrentOrganisation(); const team = useCurrentTeam(); @@ -145,7 +123,6 @@ export const EnvelopeEditorRecipientForm = () => { const debouncedRecipientSearchQuery = useDebouncedValue(recipientSearchQuery, 500); - const initialId = useId(); const $sensorApi = useRef(null); const isFirstRender = useRef(true); const { recipients, fields } = envelope; @@ -161,42 +138,7 @@ export const EnvelopeEditorRecipientForm = () => { const recipientSuggestions = recipientSuggestionsData?.results || []; - const defaultRecipients = [ - { - formId: initialId, - name: '', - email: '', - role: RecipientRole.SIGNER, - signingOrder: 1, - actionAuth: [], - }, - ]; - - const form = useForm({ - resolver: zodResolver(ZEnvelopeRecipientsForm), - mode: 'onChange', // Used for autosave purposes, maybe can try onBlur instead? - defaultValues: { - signers: - recipients.length > 0 - ? sortBy( - recipients.map((recipient, index) => ({ - id: recipient.id, - formId: String(recipient.id), - name: recipient.name, - email: recipient.email, - role: recipient.role, - signingOrder: recipient.signingOrder ?? index + 1, - actionAuth: - ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? undefined, - })), - [prop('signingOrder'), 'asc'], - [prop('id'), 'asc'], - ) - : defaultRecipients, - signingOrder: envelope.documentMeta.signingOrder, - allowDictateNextSigner: envelope.documentMeta.allowDictateNextSigner, - }, - }); + const { form } = editorRecipients; const recipientHasAuthSettings = useMemo(() => { const recipientHasAuthOptions = recipients.find((recipient) => { @@ -588,7 +530,7 @@ export const EnvelopeEditorRecipientForm = () => { return; } - const validatedFormValues = ZEnvelopeRecipientsForm.safeParse(formValues); + const validatedFormValues = ZEditorRecipientsFormSchema.safeParse(formValues); if (!validatedFormValues.success) { return; @@ -848,246 +790,205 @@ export const EnvelopeEditorRecipientForm = () => { ref={provided.innerRef} className="flex w-full flex-col gap-y-2" > - {signers.map((signer, index) => ( - - {(provided, snapshot) => ( -
- { + const isDirectRecipient = + envelope.type === EnvelopeType.TEMPLATE && + envelope.directLink !== null && + signer.id === envelope.directLink.directTemplateRecipientId; + + return ( + + {(provided, snapshot) => ( +
-
- {isSigningOrderSequential && ( + +
+ {isSigningOrderSequential && ( + ( + + + + { + field.onChange(e); + handleSigningOrderChange(index, e.target.value); + }} + onBlur={(e) => { + field.onBlur(); + handleSigningOrderChange(index, e.target.value); + }} + disabled={ + snapshot.isDragging || + isSubmitting || + !canRecipientBeModified(signer.id) + } + /> + + + + )} + /> + )} + ( - + {!showAdvancedSettings && index === 0 && ( + + Email + + )} + - { - field.onChange(e); - handleSigningOrderChange(index, e.target.value); - }} - onBlur={(e) => { - field.onBlur(); - handleSigningOrderChange(index, e.target.value); - }} + + handleRecipientAutoCompleteSelect(index, suggestion) + } + onSearchQueryChange={(query) => { + field.onChange(query); + setRecipientSearchQuery(query); + }} + loading={isLoading} + data-testid="signer-email-input" + maxLength={254} /> + )} /> - )} - ( - - {!showAdvancedSettings && index === 0 && ( - - Email - - )} - - - - handleRecipientAutoCompleteSelect(index, suggestion) - } - onSearchQueryChange={(query) => { - field.onChange(query); - setRecipientSearchQuery(query); - }} - loading={isLoading} - data-testid="signer-email-input" - maxLength={254} - /> - - - - - )} - /> - - ( - - {!showAdvancedSettings && index === 0 && ( - - Name - - )} - - - - handleRecipientAutoCompleteSelect(index, suggestion) - } - onSearchQueryChange={(query) => { - field.onChange(query); - setRecipientSearchQuery(query); - }} - loading={isLoading} - maxLength={255} - /> - - - - - )} - /> - - ( - - - { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - handleRoleChange(index, value as RecipientRole); - field.onChange(value); - }} - disabled={ - snapshot.isDragging || - isSubmitting || - !canRecipientBeModified(signer.id) - } - /> - - - - - )} - /> - - -
- - {showAdvancedSettings && - organisation.organisationClaim.flags.cfr21 && ( ( + {!showAdvancedSettings && index === 0 && ( + + Name + + )} + + + + handleRecipientAutoCompleteSelect(index, suggestion) + } + onSearchQueryChange={(query) => { + field.onChange(query); + setRecipientSearchQuery(query); + }} + loading={isLoading} + maxLength={255} + /> + + + + + )} + /> + + ( + - { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + handleRoleChange(index, value as RecipientRole); + field.onChange(value); + }} disabled={ snapshot.isDragging || isSubmitting || @@ -1100,12 +1001,63 @@ export const EnvelopeEditorRecipientForm = () => { )} /> - )} -
-
- )} - - ))} + + +
+ + {showAdvancedSettings && + organisation.organisationClaim.flags.cfr21 && ( + ( + + + + + + + + )} + /> + )} +
+
+ )} +
+ ); + })} {provided.placeholder}
diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor.tsx index af5eecdd6..cf4da6970 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor.tsx @@ -81,7 +81,7 @@ export default function EnvelopeEditor() { isAutosaving, flushAutosave, relativePath, - editorFields, + syncEnvelope, } = useCurrentEnvelopeEditor(); const [searchParams, setSearchParams] = useSearchParams(); @@ -278,6 +278,8 @@ export default function EnvelopeEditor() { templateId={mapSecondaryIdToTemplateId(envelope.secondaryId)} directLink={envelope.directLink} recipients={envelope.recipients} + onCreateSuccess={async () => await syncEnvelope()} + onDeleteSuccess={async () => await syncEnvelope()} trigger={ + + +
+ + + ); +}; diff --git a/apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx b/apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx new file mode 100644 index 000000000..be50e85a2 --- /dev/null +++ b/apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx @@ -0,0 +1,256 @@ +import { useEffect, useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { Plural, useLingui } from '@lingui/react/macro'; +import { Trans } from '@lingui/react/macro'; +import { EnvelopeType } from '@prisma/client'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { match } from 'ts-pattern'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type EnvelopesBulkMoveDialogProps = { + envelopeIds: string[]; + envelopeType: EnvelopeType; + open: boolean; + onOpenChange: (open: boolean) => void; + currentFolderId?: string; + onSuccess?: () => void; +} & Omit; + +const ZBulkMoveFormSchema = z.object({ + folderId: z.string().nullable(), +}); + +type TBulkMoveFormSchema = z.infer; + +export const EnvelopesBulkMoveDialog = ({ + envelopeIds, + envelopeType, + open, + onOpenChange, + currentFolderId, + onSuccess, + ...props +}: EnvelopesBulkMoveDialogProps) => { + const { t } = useLingui(); + const { toast } = useToast(); + + const [searchTerm, setSearchTerm] = useState(''); + + const form = useForm({ + resolver: zodResolver(ZBulkMoveFormSchema), + defaultValues: { + folderId: currentFolderId ?? null, + }, + }); + + const isDocument = envelopeType === EnvelopeType.DOCUMENT; + + const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFoldersInternal.useQuery( + { + parentId: currentFolderId, + type: envelopeType, + }, + { + enabled: open, + }, + ); + + const { mutateAsync: bulkMoveEnvelopes } = trpc.envelope.bulk.move.useMutation(); + + const trpcUtils = trpc.useUtils(); + + useEffect(() => { + if (open) { + setSearchTerm(''); + + form.reset({ + folderId: currentFolderId, + }); + } + }, [open, currentFolderId]); + + const onSubmit = async (data: TBulkMoveFormSchema) => { + try { + await bulkMoveEnvelopes({ + envelopeIds, + folderId: data.folderId, + envelopeType, + }); + + // Invalidate the appropriate query based on envelope type. + if (isDocument) { + await trpcUtils.document.findDocumentsInternal.invalidate(); + } else { + await trpcUtils.template.findTemplates.invalidate(); + } + + toast({ + description: t`Selected items have been moved.`, + }); + + onSuccess?.(); + onOpenChange(false); + } catch (err) { + const error = AppError.parseError(err); + + const errorMessage = match(error.code) + .with( + AppErrorCode.NOT_FOUND, + () => t`The folder you are trying to move the items to does not exist.`, + ) + .with(AppErrorCode.UNAUTHORIZED, () => t`You are not allowed to move these items.`) + .with(AppErrorCode.INVALID_BODY, () => t`All items must be of the same type.`) + .otherwise(() => t`An error occurred while moving the items.`); + + toast({ + description: errorMessage, + variant: 'destructive', + }); + } + }; + + const filteredFolders = folders?.data.filter((folder) => + folder.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + + return ( + + + + + {isDocument ? ( + Move Documents to Folder + ) : ( + Move Templates to Folder + )} + + + + {isDocument ? ( + + ) : ( + + )} + + + +
+ + setSearchTerm(e.target.value)} + className="pl-8" + /> +
+ +
+ + ( + + + Folder + + + +
+ {isFoldersLoading ? ( +
+ +
+ ) : ( + <> + + + {filteredFolders?.map((folder) => ( + + ))} + + {searchTerm && filteredFolders?.length === 0 && ( +
+ No folders found +
+ )} + + )} +
+
+ +
+ )} + /> + + + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/tables/documents-table.tsx b/apps/remix/app/components/tables/documents-table.tsx index ebd4574c5..a9da6ac2d 100644 --- a/apps/remix/app/components/tables/documents-table.tsx +++ b/apps/remix/app/components/tables/documents-table.tsx @@ -12,7 +12,8 @@ import { useSession } from '@documenso/lib/client-only/providers/session'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/find-documents.types'; -import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; +import { Checkbox } from '@documenso/ui/primitives/checkbox'; +import type { DataTableColumnDef, RowSelectionState } 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'; @@ -30,6 +31,9 @@ export type DocumentsTableProps = { isLoading?: boolean; isLoadingError?: boolean; onMoveDocument?: (documentId: number) => void; + enableSelection?: boolean; + rowSelection?: RowSelectionState; + onRowSelectionChange?: (selection: RowSelectionState) => void; }; type DocumentsTableRow = TFindDocumentsResponse['data'][number]; @@ -39,6 +43,9 @@ export const DocumentsTable = ({ isLoading, isLoadingError, onMoveDocument, + enableSelection, + rowSelection, + onRowSelectionChange, }: DocumentsTableProps) => { const { _, i18n } = useLingui(); @@ -48,7 +55,34 @@ export const DocumentsTable = ({ const updateSearchParams = useUpdateSearchParams(); const columns = useMemo(() => { - return [ + const cols: DataTableColumnDef[] = []; + + if (enableSelection) { + cols.push({ + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label={_(msg`Select all`)} + onClick={(e) => e.stopPropagation()} + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label={_(msg`Select row`)} + onClick={(e) => e.stopPropagation()} + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }); + } + + cols.push( { header: _(msg`Created`), accessorKey: 'createdAt', @@ -93,8 +127,10 @@ export const DocumentsTable = ({
), }, - ] satisfies DataTableColumnDef[]; - }, [team, onMoveDocument]); + ); + + return cols; + }, [team, onMoveDocument, enableSelection]); const onPaginationChange = (page: number, perPage: number) => { startTransition(() => { @@ -132,6 +168,11 @@ export const DocumentsTable = ({ rows: 5, component: ( <> + {enableSelection && ( + + + + )} @@ -152,13 +193,17 @@ export const DocumentsTable = ({ ), }} + enableRowSelection={enableSelection} + rowSelection={rowSelection} + onRowSelectionChange={onRowSelectionChange} + getRowId={(row) => row.envelopeId} > {(table) => } {isPending && ( -
- +
+
)}
diff --git a/apps/remix/app/components/tables/envelopes-table-bulk-action-bar.tsx b/apps/remix/app/components/tables/envelopes-table-bulk-action-bar.tsx new file mode 100644 index 000000000..84ee783b7 --- /dev/null +++ b/apps/remix/app/components/tables/envelopes-table-bulk-action-bar.tsx @@ -0,0 +1,55 @@ +import { useLingui } from '@lingui/react/macro'; +import { Trans } from '@lingui/react/macro'; +import { FolderInputIcon, Trash2Icon, XIcon } from 'lucide-react'; + +import { Button } from '@documenso/ui/primitives/button'; + +export type EnvelopesTableBulkActionBarProps = { + selectedCount: number; + onMoveClick: () => void; + onDeleteClick: () => void; + onClearSelection: () => void; +}; + +export const EnvelopesTableBulkActionBar = ({ + selectedCount, + onMoveClick, + onDeleteClick, + onClearSelection, +}: EnvelopesTableBulkActionBarProps) => { + const { t } = useLingui(); + + if (selectedCount === 0) { + return null; + } + + return ( +
+ + {selectedCount} selected + + +
+ + + + + + +
+ ); +}; diff --git a/apps/remix/app/components/tables/templates-table.tsx b/apps/remix/app/components/tables/templates-table.tsx index 59160ad89..df7ef6dc1 100644 --- a/apps/remix/app/components/tables/templates-table.tsx +++ b/apps/remix/app/components/tables/templates-table.tsx @@ -12,7 +12,8 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org import { formatTemplatesPath } from '@documenso/lib/utils/teams'; import type { TFindTemplatesResponse } from '@documenso/trpc/server/template-router/schema'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; -import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; +import { Checkbox } from '@documenso/ui/primitives/checkbox'; +import type { DataTableColumnDef, RowSelectionState } 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'; @@ -32,6 +33,9 @@ type TemplatesTableProps = { isLoadingError?: boolean; documentRootPath: string; templateRootPath: string; + enableSelection?: boolean; + rowSelection?: RowSelectionState; + onRowSelectionChange?: (selection: RowSelectionState) => void; }; type TemplatesTableRow = TFindTemplatesResponse['data'][number]; @@ -42,6 +46,9 @@ export const TemplatesTable = ({ isLoadingError, documentRootPath, templateRootPath, + enableSelection, + rowSelection, + onRowSelectionChange, }: TemplatesTableProps) => { const { _, i18n } = useLingui(); const { remaining } = useLimits(); @@ -60,7 +67,34 @@ export const TemplatesTable = ({ }; const columns = useMemo(() => { - return [ + const cols: DataTableColumnDef[] = []; + + if (enableSelection) { + cols.push({ + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label={_(msg`Select all`)} + onClick={(e) => e.stopPropagation()} + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label={_(msg`Select row`)} + onClick={(e) => e.stopPropagation()} + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }); + } + + cols.push( { header: _(msg`Created`), accessorKey: 'createdAt', @@ -86,8 +120,8 @@ export const TemplatesTable = ({ - -
-

- {typeof value === 'number' ? value.toLocaleString('en-US') : value} -

+ {children || ( +

+ {typeof value === 'number' ? value.toLocaleString('en-US') : value} +

+ )} ); diff --git a/apps/remix/app/components/tables/admin-claims-table.tsx b/apps/remix/app/components/tables/admin-claims-table.tsx index 425472d0a..cf6d82aac 100644 --- a/apps/remix/app/components/tables/admin-claims-table.tsx +++ b/apps/remix/app/components/tables/admin-claims-table.tsx @@ -6,6 +6,7 @@ import { EditIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react'; import { Link, useSearchParams } from 'react-router'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import type { TLicenseClaim } from '@documenso/lib/types/license'; import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params'; import { SUBSCRIPTION_CLAIM_FEATURE_FLAGS } from '@documenso/lib/types/subscription'; import { trpc } from '@documenso/trpc/react'; @@ -27,7 +28,11 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { ClaimDeleteDialog } from '../dialogs/claim-delete-dialog'; import { ClaimUpdateDialog } from '../dialogs/claim-update-dialog'; -export const AdminClaimsTable = () => { +type AdminClaimsTableProps = { + licenseFlags?: TLicenseClaim; +}; + +export const AdminClaimsTable = ({ licenseFlags }: AdminClaimsTableProps) => { const { t } = useLingui(); const { toast } = useToast(); @@ -97,11 +102,11 @@ export const AdminClaimsTable = () => { ); if (flags.length === 0) { - return

{t`None`}

; + return

{t`None`}

; } return ( -