From 082931121451af02b692d37d474e99902dde74b1 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:37:56 +0300 Subject: [PATCH 01/10] feat: prefill fields via api (#1261) ## Description Configure the advanced field via API. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [x] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## Summary by CodeRabbit - **New Features** - Enhanced API functionality to support field metadata during field creation. - Introduced validation checks for field metadata to ensure necessary information is provided for advanced field types. - **Bug Fixes** - Improved error handling during field creation to return properly formatted error responses. - **Documentation** - Updated API schemas to include field metadata, enhancing data validation and response structures. --- .gitignore | 2 + packages/api/v1/implementation.ts | 85 +++++++++++-------- packages/api/v1/schema.ts | 3 + .../lib/server-only/field/create-field.ts | 49 +++++++++++ 4 files changed, 105 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 3b0569b15f..b95fcc7d2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +packages/prisma/generated/types.ts + # dependencies node_modules .pnp diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index 74afa80c07..ad9aaaac45 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -31,6 +31,7 @@ import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/tem import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; import { findTemplates } from '@documenso/lib/server-only/template/find-templates'; import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; +import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; @@ -869,7 +870,17 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { createField: authenticatedMiddleware(async (args, user, team) => { const { id: documentId } = args.params; - const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body; + const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } = + args.body; + + if (pageNumber <= 0) { + return { + status: 400, + body: { + message: 'Invalid page number', + }, + }; + } const document = await getDocumentById({ id: Number(documentId), @@ -918,41 +929,47 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { }; } - const field = await createField({ - documentId: Number(documentId), - recipientId: Number(recipientId), - userId: user.id, - teamId: team?.id, - type, - pageNumber, - pageX, - pageY, - pageWidth, - pageHeight, - requestMetadata: extractNextApiRequestMetadata(args.req), - }); + try { + const field = await createField({ + documentId: Number(documentId), + recipientId: Number(recipientId), + userId: user.id, + teamId: team?.id, + type, + pageNumber, + pageX, + pageY, + pageWidth, + pageHeight, + fieldMeta, + requestMetadata: extractNextApiRequestMetadata(args.req), + }); - const remappedField = { - id: field.id, - documentId: field.documentId, - recipientId: field.recipientId ?? -1, - type: field.type, - pageNumber: field.page, - pageX: Number(field.positionX), - pageY: Number(field.positionY), - pageWidth: Number(field.width), - pageHeight: Number(field.height), - customText: field.customText, - inserted: field.inserted, - }; + const remappedField = { + id: field.id, + documentId: field.documentId, + recipientId: field.recipientId ?? -1, + type: field.type, + pageNumber: field.page, + pageX: Number(field.positionX), + pageY: Number(field.positionY), + pageWidth: Number(field.width), + pageHeight: Number(field.height), + customText: field.customText, + fieldMeta: ZFieldMetaSchema.parse(field.fieldMeta), + inserted: field.inserted, + }; - return { - status: 200, - body: { - ...remappedField, - documentId: Number(documentId), - }, - }; + return { + status: 200, + body: { + ...remappedField, + documentId: Number(documentId), + }, + }; + } catch (err) { + return AppError.toRestAPIError(err); + } }), updateField: authenticatedMiddleware(async (args, user, team) => { diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index 90d3f65bfc..42205d5403 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -5,6 +5,7 @@ import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/const import '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { ZUrlSchema } from '@documenso/lib/schemas/common'; +import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { DocumentDataType, FieldType, @@ -300,6 +301,7 @@ export const ZCreateFieldMutationSchema = z.object({ pageY: z.number(), pageWidth: z.number(), pageHeight: z.number(), + fieldMeta: ZFieldMetaSchema, }); export type TCreateFieldMutationSchema = z.infer; @@ -323,6 +325,7 @@ export const ZSuccessfulFieldResponseSchema = z.object({ pageWidth: z.number(), pageHeight: z.number(), customText: z.string(), + fieldMeta: ZFieldMetaSchema, inserted: z.boolean(), }); diff --git a/packages/lib/server-only/field/create-field.ts b/packages/lib/server-only/field/create-field.ts index 7a3aa3959f..9aafc7ab85 100644 --- a/packages/lib/server-only/field/create-field.ts +++ b/packages/lib/server-only/field/create-field.ts @@ -1,6 +1,16 @@ +import { match } from 'ts-pattern'; + import { prisma } from '@documenso/prisma'; import type { FieldType, Team } from '@documenso/prisma/client'; +import { + ZCheckboxFieldMeta, + ZDropdownFieldMeta, + ZNumberFieldMeta, + ZRadioFieldMeta, + ZTextFieldMeta, +} from '../../types/field-meta'; +import type { TFieldMetaSchema as FieldMeta } from '../../types/field-meta'; import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; @@ -15,6 +25,7 @@ export type CreateFieldOptions = { pageY: number; pageWidth: number; pageHeight: number; + fieldMeta?: FieldMeta; requestMetadata?: RequestMetadata; }; @@ -29,6 +40,7 @@ export const createField = async ({ pageY, pageWidth, pageHeight, + fieldMeta, requestMetadata, }: CreateFieldOptions) => { const document = await prisma.document.findFirst({ @@ -85,6 +97,42 @@ export const createField = async ({ }); } + const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(type); + + if (advancedField && !fieldMeta) { + throw new Error( + 'Field meta is required for this type of field. Please provide the appropriate field meta object.', + ); + } + + if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) { + throw new Error('Field meta type does not match the field type'); + } + + const result = match(type) + .with('RADIO', () => { + return ZRadioFieldMeta.safeParse(fieldMeta); + }) + .with('CHECKBOX', () => { + return ZCheckboxFieldMeta.safeParse(fieldMeta); + }) + .with('DROPDOWN', () => { + return ZDropdownFieldMeta.safeParse(fieldMeta); + }) + .with('NUMBER', () => { + return ZNumberFieldMeta.safeParse(fieldMeta); + }) + .with('TEXT', () => { + return ZTextFieldMeta.safeParse(fieldMeta); + }) + .otherwise(() => { + return { success: false, data: {} }; + }); + + if (!result.success) { + throw new Error('Field meta parsing failed'); + } + const field = await prisma.field.create({ data: { documentId, @@ -97,6 +145,7 @@ export const createField = async ({ height: pageHeight, customText: '', inserted: false, + fieldMeta: advancedField ? result.data : undefined, }, include: { Recipient: true, From 75c8772a0236810e17fda16f26a67ffec594ec05 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 27 Aug 2024 20:34:39 +0900 Subject: [PATCH 02/10] feat: web i18n (#1286) --- apps/marketing/package.json | 1 - .../app/(marketing)/singleplayer/client.tsx | 10 +- apps/web/lingui.config.ts | 2 +- apps/web/package.json | 3 +- .../admin/documents/[id]/admin-actions.tsx | 24 +- .../(dashboard)/admin/documents/[id]/page.tsx | 20 +- .../admin/documents/[id]/recipient-item.tsx | 32 +- .../[id]/super-delete-document-dialog.tsx | 35 +- .../admin/documents/document-results.tsx | 14 +- .../app/(dashboard)/admin/documents/page.tsx | 10 +- apps/web/src/app/(dashboard)/admin/layout.tsx | 3 + apps/web/src/app/(dashboard)/admin/nav.tsx | 11 +- .../admin/site-settings/banner-form.tsx | 49 +- .../(dashboard)/admin/site-settings/page.tsx | 13 +- .../src/app/(dashboard)/admin/stats/page.tsx | 81 +- .../(dashboard)/admin/subscriptions/page.tsx | 25 +- .../admin/users/[id]/delete-user-dialog.tsx | 41 +- .../users/[id]/multiselect-role-combobox.tsx | 5 +- .../app/(dashboard)/admin/users/[id]/page.tsx | 30 +- .../admin/users/data-table-users.tsx | 16 +- .../src/app/(dashboard)/admin/users/page.tsx | 10 +- .../[id]/document-page-view-button.tsx | 19 +- .../[id]/document-page-view-dropdown.tsx | 27 +- .../[id]/document-page-view-information.tsx | 22 +- .../document-page-view-recent-activity.tsx | 21 +- .../[id]/document-page-view-recipients.tsx | 28 +- .../documents/[id]/document-page-view.tsx | 43 +- .../documents/[id]/edit-document.tsx | 40 +- .../[id]/edit/document-edit-page-view.tsx | 7 +- .../(dashboard)/documents/[id]/edit/page.tsx | 4 + .../(dashboard)/documents/[id]/loading.tsx | 12 +- .../[id]/logs/document-logs-data-table.tsx | 10 +- .../[id]/logs/document-logs-page-view.tsx | 27 +- .../[id]/logs/download-audit-log-button.tsx | 11 +- .../[id]/logs/download-certificate-button.tsx | 11 +- .../(dashboard)/documents/[id]/logs/page.tsx | 4 + .../app/(dashboard)/documents/[id]/page.tsx | 4 + .../(dashboard)/documents/[id]/sent/page.tsx | 9 +- .../_action-items/resend-document.tsx | 21 +- .../documents/data-table-action-button.tsx | 19 +- .../documents/data-table-action-dropdown.tsx | 33 +- .../documents/data-table-sender-filter.tsx | 11 +- .../app/(dashboard)/documents/data-table.tsx | 16 +- .../documents/delete-document-dialog.tsx | 76 +- .../documents/documents-page-view.tsx | 6 +- .../documents/duplicate-document-dialog.tsx | 23 +- .../app/(dashboard)/documents/empty-state.tsx | 28 +- .../documents/move-document-dialog.tsx | 32 +- .../src/app/(dashboard)/documents/page.tsx | 4 + .../upcoming-profile-claim-teaser.tsx | 11 +- .../(dashboard)/documents/upload-document.tsx | 32 +- apps/web/src/app/(dashboard)/layout.tsx | 3 + .../settings/billing/billing-plans.tsx | 22 +- .../billing/billing-portal-button.tsx | 18 +- .../app/(dashboard)/settings/billing/page.tsx | 19 +- .../src/app/(dashboard)/settings/layout.tsx | 10 +- .../profile/delete-account-dialog.tsx | 56 +- .../app/(dashboard)/settings/profile/page.tsx | 12 +- .../settings/public-profile/page.tsx | 3 + .../public-profile-page-view.tsx | 67 +- .../public-templates-data-table.tsx | 25 +- .../settings/security/activity/page.tsx | 13 +- .../user-security-activity-data-table.tsx | 12 +- .../(dashboard)/settings/security/page.tsx | 62 +- .../passkeys/create-passkey-dialog.tsx | 60 +- .../settings/security/passkeys/page.tsx | 13 +- .../user-passkeys-data-table-actions.tsx | 61 +- .../passkeys/user-passkeys-data-table.tsx | 12 +- .../teams/accept-team-invitation-button.tsx | 14 +- .../teams/decline-team-invitation-button.tsx | 14 +- .../app/(dashboard)/settings/teams/page.tsx | 9 +- .../settings/teams/team-email-usage.tsx | 59 +- .../settings/teams/team-invitations.tsx | 36 +- .../app/(dashboard)/settings/tokens/page.tsx | 52 +- .../settings/webhooks/[id]/page.tsx | 45 +- .../(dashboard)/settings/webhooks/page.tsx | 38 +- .../templates/[id]/edit-template.tsx | 33 +- .../app/(dashboard)/templates/[id]/page.tsx | 4 + .../template-direct-link-dialog-wrapper.tsx | 8 +- .../templates/[id]/template-page-view.tsx | 3 +- .../templates/data-table-action-dropdown.tsx | 11 +- .../templates/data-table-templates.tsx | 56 +- .../templates/delete-template-dialog.tsx | 26 +- .../templates/duplicate-template-dialog.tsx | 24 +- .../app/(dashboard)/templates/empty-state.tsx | 9 +- .../templates/move-template-dialog.tsx | 28 +- .../templates/new-template-dialog.tsx | 27 +- .../src/app/(dashboard)/templates/page.tsx | 4 + .../templates/template-direct-link-badge.tsx | 10 +- .../templates/template-direct-link-dialog.tsx | 143 +- .../templates/templates-page-view.tsx | 6 +- .../templates/use-template-dialog.tsx | 73 +- .../%5F%5Fhtmltopdf/audit-log/page.tsx | 7 +- .../%5F%5Fhtmltopdf/certificate/page.tsx | 13 +- apps/web/src/app/(profile)/layout.tsx | 3 + .../src/app/(profile)/p/[url]/not-found.tsx | 13 +- apps/web/src/app/(profile)/p/[url]/page.tsx | 34 +- apps/web/src/app/(profile)/profile-header.tsx | 13 +- .../d/[token]/configure-direct-template.tsx | 18 +- .../(recipient)/d/[token]/direct-template.tsx | 23 +- .../app/(recipient)/d/[token]/not-found.tsx | 17 +- .../src/app/(recipient)/d/[token]/page.tsx | 7 +- .../d/[token]/sign-direct-template.tsx | 22 +- .../d/[token]/signing-auth-page.tsx | 15 +- apps/web/src/app/(recipient)/layout.tsx | 3 + .../(signing)/sign/[token]/checkbox-field.tsx | 18 +- .../sign/[token]/complete/claim-account.tsx | 42 +- .../complete/document-preview-button.tsx | 3 +- .../sign/[token]/complete/layout.tsx | 4 + .../(signing)/sign/[token]/complete/page.tsx | 48 +- .../app/(signing)/sign/[token]/date-field.tsx | 17 +- .../sign/[token]/document-action-auth-2fa.tsx | 30 +- .../[token]/document-action-auth-account.tsx | 12 +- .../[token]/document-action-auth-dialog.tsx | 5 +- .../[token]/document-action-auth-passkey.tsx | 26 +- .../(signing)/sign/[token]/dropdown-field.tsx | 16 +- .../(signing)/sign/[token]/email-field.tsx | 13 +- .../src/app/(signing)/sign/[token]/form.tsx | 7 +- .../src/app/(signing)/sign/[token]/layout.tsx | 3 + .../app/(signing)/sign/[token]/name-field.tsx | 29 +- .../sign/[token]/no-longer-available.tsx | 31 +- .../(signing)/sign/[token]/number-field.tsx | 20 +- .../src/app/(signing)/sign/[token]/page.tsx | 3 + .../(signing)/sign/[token]/radio-field.tsx | 14 +- .../(signing)/sign/[token]/sign-dialog.tsx | 38 +- .../sign/[token]/signature-field.tsx | 29 +- .../sign/[token]/signing-auth-page.tsx | 17 +- .../sign/[token]/signing-field-container.tsx | 7 +- .../sign/[token]/signing-page-view.tsx | 16 +- .../app/(signing)/sign/[token]/text-field.tsx | 42 +- .../t/[teamUrl]/documents/[id]/edit/page.tsx | 3 + .../t/[teamUrl]/documents/[id]/logs/page.tsx | 3 + .../t/[teamUrl]/documents/[id]/page.tsx | 3 + .../(teams)/t/[teamUrl]/documents/page.tsx | 3 + .../web/src/app/(teams)/t/[teamUrl]/error.tsx | 27 +- .../t/[teamUrl]/layout-billing-banner.tsx | 34 +- .../src/app/(teams)/t/[teamUrl]/layout.tsx | 3 + .../src/app/(teams)/t/[teamUrl]/not-found.tsx | 15 +- .../t/[teamUrl]/settings/billing/page.tsx | 26 +- .../(teams)/t/[teamUrl]/settings/layout.tsx | 9 +- .../t/[teamUrl]/settings/members/page.tsx | 12 +- .../app/(teams)/t/[teamUrl]/settings/page.tsx | 44 +- .../settings/public-profile/page.tsx | 3 + .../settings/team-email-dropdown.tsx | 17 +- .../settings/team-transfer-status.tsx | 42 +- .../t/[teamUrl]/settings/tokens/page.tsx | 54 +- .../[teamUrl]/settings/webhooks/[id]/page.tsx | 38 +- .../t/[teamUrl]/settings/webhooks/page.tsx | 38 +- .../t/[teamUrl]/templates/[id]/page.tsx | 3 + .../(teams)/t/[teamUrl]/templates/page.tsx | 3 + .../articles/signature-disclosure/page.tsx | 9 +- .../(unauthenticated)/check-email/page.tsx | 19 +- .../forgot-password/page.tsx | 26 +- apps/web/src/app/(unauthenticated)/layout.tsx | 3 + .../reset-password/[token]/page.tsx | 23 +- .../(unauthenticated)/reset-password/page.tsx | 19 +- .../src/app/(unauthenticated)/signin/page.tsx | 20 +- .../src/app/(unauthenticated)/signup/page.tsx | 3 + .../team/decline/[token]/page.tsx | 44 +- .../team/invite/[token]/page.tsx | 46 +- .../team/verify/email/[token]/page.tsx | 40 +- .../team/verify/transfer/[token]/page.tsx | 42 +- .../unverified-account/page.tsx | 20 +- .../verify-email/[token]/page.tsx | 52 +- .../(unauthenticated)/verify-email/page.tsx | 16 +- apps/web/src/app/layout.tsx | 41 +- apps/web/src/app/not-found.tsx | 13 +- apps/web/src/app/page.tsx | 6 +- .../avatar/avatar-with-recipient.tsx | 17 +- .../avatar/stack-avatars-with-tooltip.tsx | 23 +- .../(dashboard)/common/command-menu.tsx | 56 +- .../(dashboard)/layout/desktop-nav.tsx | 12 +- .../(dashboard)/layout/menu-switcher.tsx | 38 +- .../(dashboard)/layout/mobile-navigation.tsx | 16 +- .../layout/verify-email-banner.tsx | 32 +- .../period-selector/period-selector.tsx | 18 +- .../settings/layout/activity-back.tsx | 4 +- .../settings/layout/desktop-nav.tsx | 15 +- .../settings/layout/mobile-nav.tsx | 15 +- .../settings/token/delete-token-dialog.tsx | 45 +- .../webhooks/create-webhook-dialog.tsx | 59 +- .../webhooks/delete-webhook-dialog.tsx | 45 +- .../webhooks/trigger-multiselect-combobox.tsx | 7 +- .../(teams)/dialogs/add-team-email-dialog.tsx | 36 +- .../dialogs/create-team-checkout-dialog.tsx | 30 +- .../(teams)/dialogs/create-team-dialog.tsx | 44 +- .../(teams)/dialogs/delete-team-dialog.tsx | 47 +- .../dialogs/delete-team-member-dialog.tsx | 35 +- .../dialogs/invite-team-member-dialog.tsx | 62 +- .../(teams)/dialogs/leave-team-dialog.tsx | 33 +- .../dialogs/remove-team-email-dialog.tsx | 41 +- .../(teams)/dialogs/transfer-team-dialog.tsx | 56 +- .../dialogs/update-team-email-dialog.tsx | 34 +- .../dialogs/update-team-member-dialog.tsx | 41 +- .../(teams)/forms/update-team-form.tsx | 36 +- .../(teams)/settings/layout/desktop-nav.tsx | 13 +- .../(teams)/settings/layout/mobile-nav.tsx | 13 +- .../tables/current-user-teams-data-table.tsx | 19 +- .../pending-user-teams-data-table-actions.tsx | 19 +- .../tables/pending-user-teams-data-table.tsx | 9 +- .../team-billing-invoices-data-table.tsx | 18 +- .../tables/team-member-invites-data-table.tsx | 37 +- .../tables/team-members-data-table.tsx | 24 +- .../tables/teams-member-page-data-table.tsx | 15 +- .../user-settings-teams-page-data-table.tsx | 13 +- .../(teams)/team-billing-portal-button.tsx | 13 +- .../document/document-history-sheet.tsx | 17 +- .../components/formatter/document-status.tsx | 25 +- .../components/formatter/template-type.tsx | 13 +- .../2fa/disable-authenticator-app-dialog.tsx | 33 +- .../2fa/enable-authenticator-app-dialog.tsx | 74 +- .../forms/2fa/recovery-code-list.tsx | 14 +- .../forms/2fa/view-recovery-codes-dialog.tsx | 44 +- .../web/src/components/forms/avatar-image.tsx | 33 +- .../src/components/forms/forgot-password.tsx | 19 +- apps/web/src/components/forms/password.tsx | 30 +- apps/web/src/components/forms/profile.tsx | 28 +- .../forms/public-profile-claim-dialog.tsx | 12 +- .../components/forms/public-profile-form.tsx | 57 +- .../src/components/forms/reset-password.tsx | 26 +- .../forms/send-confirmation-email.tsx | 21 +- apps/web/src/components/forms/signin.tsx | 80 +- apps/web/src/components/forms/signup.tsx | 64 +- apps/web/src/components/forms/token.tsx | 57 +- apps/web/src/components/forms/v2/signup.tsx | 115 +- apps/web/src/components/forms/webhook.tsx | 0 .../web/src/components/partials/not-found.tsx | 15 +- .../manage-public-template-dialog.tsx | 111 +- .../components/ui/user-profile-skeleton.tsx | 3 +- .../src/components/ui/user-profile-timur.tsx | 11 +- apps/web/src/middleware.ts | 16 +- package-lock.json | 4 +- package.json | 1 + .../e2e/templates/direct-templates.spec.ts | 9 +- .../template-document-invite.tsx | 4 +- .../document-created-from-direct-template.tsx | 4 +- packages/email/templates/document-invite.tsx | 4 +- .../lib/client-only/providers/i18n.server.tsx | 9 +- packages/lib/constants/app.ts | 1 + packages/lib/constants/direct-templates.ts | 3 + packages/lib/constants/recipient-roles.ts | 83 +- packages/lib/constants/teams.ts | 11 +- packages/lib/constants/template.ts | 25 +- packages/lib/constants/toast.ts | 13 - .../definitions/emails/send-signing-email.ts | 6 +- .../server-only/document/resend-document.tsx | 6 +- .../recipient/set-recipients-for-template.ts | 8 +- .../template/create-template-direct-link.ts | 8 +- packages/lib/translations/de/common.po | 726 +++ packages/lib/translations/de/marketing.js | 2 +- packages/lib/translations/de/marketing.po | 17 +- packages/lib/translations/de/web.js | 2 +- packages/lib/translations/de/web.po | 4358 ++++++++++++++++ packages/lib/translations/en/common.po | 727 +++ packages/lib/translations/en/marketing.js | 2 +- packages/lib/translations/en/marketing.po | 16 + packages/lib/translations/en/web.js | 2 +- packages/lib/translations/en/web.po | 4359 +++++++++++++++++ packages/lib/utils/document-audit-logs.ts | 5 +- packages/prisma/seed/templates.ts | 2 +- .../document/document-download-button.tsx | 12 +- .../document-global-auth-access-select.tsx | 61 +- .../document-global-auth-action-select.tsx | 78 +- .../document-send-email-message-helper.tsx | 10 +- .../document/document-share-button.tsx | 23 +- .../recipient-action-auth-select.tsx | 44 +- .../recipient/recipient-role-select.tsx | 33 +- packages/ui/package.json | 3 +- packages/ui/primitives/combobox.tsx | 10 +- .../ui/primitives/data-table-pagination.tsx | 21 +- packages/ui/primitives/data-table.tsx | 9 +- packages/ui/primitives/document-dropzone.tsx | 21 +- .../primitives/document-flow/add-fields.tsx | 62 +- .../primitives/document-flow/add-settings.tsx | 39 +- .../document-flow/add-signature.tsx | 25 +- .../primitives/document-flow/add-signers.tsx | 28 +- .../primitives/document-flow/add-subject.tsx | 17 +- .../document-flow/document-flow-root.tsx | 31 +- .../primitives/document-flow/field-icon.tsx | 3 +- .../field-item-advanced-settings.tsx | 17 +- .../checkbox-field.tsx | 26 +- .../dropdown-field.tsx | 24 +- .../number-field.tsx | 48 +- .../radio-field.tsx | 15 +- .../text-field.tsx | 37 +- .../missing-signature-field-dialog.tsx | 13 +- .../send-document-action-dialog.tsx | 17 +- packages/ui/primitives/document-flow/types.ts | 5 +- .../primitives/document-password-dialog.tsx | 20 +- .../ui/primitives/multi-select-combobox.tsx | 13 +- .../signature-pad/signature-pad.tsx | 3 +- .../template-flow/add-template-fields.tsx | 40 +- .../add-template-placeholder-recipients.tsx | 40 +- .../template-flow/add-template-settings.tsx | 52 +- 294 files changed, 14844 insertions(+), 2227 deletions(-) delete mode 100644 apps/web/src/components/forms/webhook.tsx create mode 100644 packages/lib/constants/direct-templates.ts delete mode 100644 packages/lib/constants/toast.ts diff --git a/apps/marketing/package.json b/apps/marketing/package.json index fb6478fca7..a1c25d4596 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -20,7 +20,6 @@ "@documenso/trpc": "*", "@documenso/ui": "*", "@hookform/resolvers": "^3.1.0", - "@lingui/macro": "^4.11.1", "@lingui/react": "^4.11.1", "@openstatus/react": "^0.0.3", "cmdk": "^0.2.1", diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx index 2d8bbe9d92..a3df5af29c 100644 --- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx +++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx @@ -5,6 +5,8 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import { msg } from '@lingui/macro'; + import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { base64 } from '@documenso/lib/universal/base64'; @@ -46,8 +48,8 @@ export const SinglePlayerClient = () => { const documentFlow: Record = { fields: { - title: 'Add document', - description: 'Upload a document and add fields.', + title: msg`Add document`, + description: msg`Upload a document and add fields.`, stepIndex: 1, onBackStep: uploadedFile ? () => { @@ -58,8 +60,8 @@ export const SinglePlayerClient = () => { onNextStep: () => setStep('sign'), }, sign: { - title: 'Sign', - description: 'Enter your details.', + title: msg`Sign`, + description: msg`Enter your details.`, stepIndex: 2, onBackStep: () => setStep('fields'), }, diff --git a/apps/web/lingui.config.ts b/apps/web/lingui.config.ts index f129b49cec..a5862eb98b 100644 --- a/apps/web/lingui.config.ts +++ b/apps/web/lingui.config.ts @@ -8,7 +8,7 @@ const config: LinguiConfig = { locales: APP_I18N_OPTIONS.supportedLangs as unknown as string[], catalogs: [ { - path: '/../../packages/lib/translations/web/{locale}', + path: '/../../packages/lib/translations/{locale}/web', include: ['/apps/web/src'], }, { diff --git a/apps/web/package.json b/apps/web/package.json index f86e5769a1..be73389c8d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,7 +23,6 @@ "@documenso/trpc": "*", "@documenso/ui": "*", "@hookform/resolvers": "^3.1.0", - "@lingui/macro": "^4.11.1", "@lingui/react": "^4.11.1", "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.3", @@ -60,9 +59,9 @@ "zod": "^3.22.4" }, "devDependencies": { + "@documenso/tailwind-config": "*", "@lingui/loader": "^4.11.1", "@lingui/swc-plugin": "4.0.6", - "@documenso/tailwind-config": "*", "@simplewebauthn/types": "^9.0.1", "@types/formidable": "^2.0.6", "@types/luxon": "^3.3.1", diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx index f084b5db50..330f31eb1e 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx @@ -2,6 +2,9 @@ import Link from 'next/link'; +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; + import type { Recipient } from '@documenso/prisma/client'; import { type Document, SigningStatus } from '@documenso/prisma/client'; import { trpc } from '@documenso/trpc/react'; @@ -22,20 +25,21 @@ export type AdminActionsProps = { }; export const AdminActions = ({ className, document, recipients }: AdminActionsProps) => { + const { _ } = useLingui(); const { toast } = useToast(); const { mutate: resealDocument, isLoading: isResealDocumentLoading } = trpc.admin.resealDocument.useMutation({ onSuccess: () => { toast({ - title: 'Success', - description: 'Document resealed', + title: _(msg`Success`), + description: _(msg`Document resealed`), }); }, onError: () => { toast({ - title: 'Error', - description: 'Failed to reseal document', + title: _(msg`Error`), + description: _(msg`Failed to reseal document`), variant: 'destructive', }); }, @@ -54,19 +58,23 @@ export const AdminActions = ({ className, document, recipients }: AdminActionsPr )} onClick={() => resealDocument({ id: document.id })} > - Reseal document + Reseal document - Attempts sealing the document again, useful for after a code change has occurred to - resolve an erroneous document. + + Attempts sealing the document again, useful for after a code change has occurred to + resolve an erroneous document. + ); diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx index 563db8d1b5..48211fbc02 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx @@ -1,5 +1,7 @@ +import { Trans } from '@lingui/macro'; import { DateTime } from 'luxon'; +import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; import { Accordion, @@ -23,6 +25,8 @@ type AdminDocumentDetailsPageProps = { }; export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) { + setupI18nSSR(); + const document = await getEntireDocument({ id: Number(params.id) }); return ( @@ -35,28 +39,34 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument {document.deletedAt && ( - Deleted + Deleted )}
- Created on: + Created on:{' '} +
- Last updated at: + Last updated at:{' '} +

-

Admin Actions

+

+ Admin Actions +


-

Recipients

+

+ Recipients +

diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx index 3bf8c78abe..92304a62e2 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx @@ -2,6 +2,8 @@ import { useRouter } from 'next/navigation'; +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -43,7 +45,9 @@ export type RecipientItemProps = { }; export const RecipientItem = ({ recipient }: RecipientItemProps) => { + const { _ } = useLingui(); const { toast } = useToast(); + const router = useRouter(); const form = useForm({ @@ -64,14 +68,14 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => { }); toast({ - title: 'Recipient updated', - description: 'The recipient has been updated successfully', + title: _(msg`Recipient updated`), + description: _(msg`The recipient has been updated successfully`), }); router.refresh(); } catch (error) { toast({ - title: 'Failed to update recipient', + title: _(msg`Failed to update recipient`), description: error.message, variant: 'destructive', }); @@ -93,7 +97,9 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => { name="name" render={({ field }) => ( - Name + + Name + @@ -109,7 +115,9 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => { name="email" render={({ field }) => ( - Email + + Email + @@ -122,7 +130,7 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
@@ -131,7 +139,9 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
-

Fields

+

+ Fields +

{ cell: ({ row }) =>
{row.original.id}
, }, { - header: 'Type', + header: _(msg`Type`), accessorKey: 'type', cell: ({ row }) =>
{row.original.type}
, }, { - header: 'Inserted', + header: _(msg`Inserted`), accessorKey: 'inserted', cell: ({ row }) =>
{row.original.inserted ? 'True' : 'False'}
, }, { - header: 'Value', + header: _(msg`Value`), accessorKey: 'customText', cell: ({ row }) =>
{row.original.customText}
, }, { - header: 'Signature', + header: _(msg`Signature`), accessorKey: 'signature', cell: ({ row }) => (
diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx index 63ad88a3f7..337796959d 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx @@ -4,6 +4,9 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; + import type { Document } from '@documenso/prisma/client'; import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; @@ -26,7 +29,9 @@ export type SuperDeleteDocumentDialogProps = { }; export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialogProps) => { + const { _ } = useLingui(); const { toast } = useToast(); + const router = useRouter(); const [reason, setReason] = useState(''); @@ -43,7 +48,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo await deleteDocument({ id: document.id, reason }); toast({ - title: 'Document deleted', + title: _(msg`Document deleted`), description: 'The Document has been deleted successfully.', duration: 5000, }); @@ -52,13 +57,13 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo } catch (err) { if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { toast({ - title: 'An error occurred', + title: _(msg`An error occurred`), description: err.message, variant: 'destructive', }); } else { toast({ - title: 'An unknown error occurred', + title: _(msg`An unknown error occurred`), variant: 'destructive', description: err.message ?? @@ -76,31 +81,41 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo variant="neutral" >
- Delete Document + + Delete Document + - Delete the document. This action is irreversible so proceed with caution. + + Delete the document. This action is irreversible so proceed with caution. +
- + - Delete Document + + Delete Document + - This action is not reversible. Please be certain. + This action is not reversible. Please be certain.
- To confirm, please enter the reason + + To confirm, please enter the reason + - {isDeletingDocument ? 'Deleting document...' : 'Delete Document'} + Delete document diff --git a/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx b/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx index b7e2359817..3226ed2c79 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx @@ -5,6 +5,8 @@ import { useState } from 'react'; import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; +import { msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; import { Loader } from 'lucide-react'; import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; @@ -23,6 +25,8 @@ import { LocaleDate } from '~/components/formatter/locale-date'; // export type AdminDocumentResultsProps = {}; export const AdminDocumentResults = () => { + const { _ } = useLingui(); + const searchParams = useSearchParams(); const updateSearchParams = useUpdateSearchParams(); @@ -56,7 +60,7 @@ export const AdminDocumentResults = () => {
setTerm(e.target.value)} /> @@ -65,12 +69,12 @@ export const AdminDocumentResults = () => { , }, { - header: 'Title', + header: _(msg`Title`), accessorKey: 'title', cell: ({ row }) => { return ( @@ -84,12 +88,12 @@ export const AdminDocumentResults = () => { }, }, { - header: 'Status', + header: _(msg`Status`), accessorKey: 'status', cell: ({ row }) => , }, { - header: 'Owner', + header: _(msg`Owner`), accessorKey: 'owner', cell: ({ row }) => { const avatarFallbackText = row.original.User.name diff --git a/apps/web/src/app/(dashboard)/admin/documents/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/page.tsx index 96e4dcef82..7f21bf5fa4 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/page.tsx @@ -1,9 +1,17 @@ +import { Trans } from '@lingui/macro'; + +import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; + import { AdminDocumentResults } from './document-results'; export default function AdminDocumentsPage() { + setupI18nSSR(); + return (
-

Manage documents

+

+ Manage documents +

diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index 12330679d1..c489c34a1d 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { redirect } from 'next/navigation'; +import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; @@ -12,6 +13,8 @@ export type AdminSectionLayoutProps = { }; export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) { + setupI18nSSR(); + const { user } = await getRequiredServerComponentSession(); if (!isAdmin(user)) { diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx index 080cb97412..cf0bb81f2c 100644 --- a/apps/web/src/app/(dashboard)/admin/nav.tsx +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -5,6 +5,7 @@ import type { HTMLAttributes } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { Trans } from '@lingui/macro'; import { BarChart3, FileStack, Settings, Users, Wallet2 } from 'lucide-react'; import { cn } from '@documenso/ui/lib/utils'; @@ -33,7 +34,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Stats + Stats @@ -47,7 +48,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Users + Users @@ -61,7 +62,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Documents + Documents @@ -75,7 +76,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Subscriptions + Subscriptions @@ -89,7 +90,7 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { > - Site Settings + Site Settings
diff --git a/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx b/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx index 351e146ff1..d68eed63b3 100644 --- a/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx +++ b/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx @@ -3,6 +3,8 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; import { useForm } from 'react-hook-form'; import type { z } from 'zod'; @@ -37,8 +39,10 @@ export type BannerFormProps = { }; export function BannerForm({ banner }: BannerFormProps) { - const router = useRouter(); const { toast } = useToast(); + const { _ } = useLingui(); + + const router = useRouter(); const form = useForm({ resolver: zodResolver(ZBannerFormSchema), @@ -67,8 +71,8 @@ export function BannerForm({ banner }: BannerFormProps) { }); toast({ - title: 'Banner Updated', - description: 'Your banner has been updated successfully.', + title: _(msg`Banner Updated`), + description: _(msg`Your banner has been updated successfully.`), duration: 5000, }); @@ -76,16 +80,17 @@ export function BannerForm({ banner }: BannerFormProps) { } catch (err) { if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { toast({ - title: 'An error occurred', + title: _(msg`An error occurred`), description: err.message, variant: 'destructive', }); } else { toast({ - title: 'An unknown error occurred', + title: _(msg`An unknown error occurred`), variant: 'destructive', - description: - 'We encountered an unknown error while attempting to update the banner. Please try again later.', + description: _( + msg`We encountered an unknown error while attempting to update the banner. Please try again later.`, + ), }); } } @@ -93,10 +98,14 @@ export function BannerForm({ banner }: BannerFormProps) { return (
-

Site Banner

+

+ Site Banner +

- The site banner is a message that is shown at the top of the site. It can be used to display - important information to your users. + + The site banner is a message that is shown at the top of the site. It can be used to + display important information to your users. +

@@ -110,7 +119,9 @@ export function BannerForm({ banner }: BannerFormProps) { name="enabled" render={({ field }) => ( - Enabled + + Enabled +
@@ -131,7 +142,9 @@ export function BannerForm({ banner }: BannerFormProps) { name="data.bgColor" render={({ field }) => ( - Background Color + + Background Color +
@@ -149,7 +162,9 @@ export function BannerForm({ banner }: BannerFormProps) { name="data.textColor" render={({ field }) => ( - Text Color + + Text Color +
@@ -170,14 +185,16 @@ export function BannerForm({ banner }: BannerFormProps) { name="data.content" render={({ field }) => ( - Content + + Content +