mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 03:32:14 +10:00
Compare commits
7 Commits
fix/embed-
...
feat/add-o
| Author | SHA1 | Date | |
|---|---|---|---|
| 76e4807c67 | |||
| efd6531d3e | |||
| c9fe134852 | |||
| f2149719e3 | |||
| 161d40cde7 | |||
| 76028771b8 | |||
| 5df1a6602e |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.9.0-rc.0",
|
||||
"version": "1.9.0-rc.2",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.0-rc.0",
|
||||
"version": "1.9.0-rc.2",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@ -40,7 +40,7 @@ export const AdminDocumentResults = () => {
|
||||
const { data: findDocumentsData, isLoading: isFindDocumentsLoading } =
|
||||
trpc.admin.findDocuments.useQuery(
|
||||
{
|
||||
term: debouncedTerm,
|
||||
query: debouncedTerm,
|
||||
page: page || 1,
|
||||
perPage: perPage || 20,
|
||||
},
|
||||
|
||||
@ -11,7 +11,7 @@ import type { DateTimeFormatOptions } from 'luxon';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
@ -35,9 +35,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } =
|
||||
trpc.document.findDocumentAuditLogs.useQuery(
|
||||
|
||||
@ -9,7 +9,7 @@ import { DateTime } from 'luxon';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
@ -24,7 +24,7 @@ import { DataTableActionDropdown } from './data-table-action-dropdown';
|
||||
import { DataTableTitle } from './data-table-title';
|
||||
|
||||
export type DocumentsDataTableProps = {
|
||||
results: FindResultSet<
|
||||
results: FindResultResponse<
|
||||
Document & {
|
||||
Recipient: Recipient[];
|
||||
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||
|
||||
@ -83,7 +83,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
||||
perPage,
|
||||
period,
|
||||
senderIds,
|
||||
search,
|
||||
query: search,
|
||||
});
|
||||
|
||||
const getTabHref = (value: typeof status) => {
|
||||
|
||||
@ -75,7 +75,7 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
|
||||
|
||||
const enabledPrivateDirectTemplates = useMemo(
|
||||
() =>
|
||||
(data?.templates ?? []).filter(
|
||||
(data?.data ?? []).filter(
|
||||
(template): template is DirectTemplate =>
|
||||
template.directLink?.enabled === true && template.type !== TemplateType.PUBLIC,
|
||||
),
|
||||
|
||||
@ -52,7 +52,7 @@ export const PublicTemplatesDataTable = () => {
|
||||
);
|
||||
|
||||
const { directTemplates, publicDirectTemplates, privateDirectTemplates } = useMemo(() => {
|
||||
const directTemplates = (data?.templates ?? []).filter(
|
||||
const directTemplates = (data?.data ?? []).filter(
|
||||
(template): template is DirectTemplate => template.directLink?.enabled === true,
|
||||
);
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { UAParser } from 'ua-parser-js';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { USER_SECURITY_AUDIT_LOG_MAP } from '@documenso/lib/constants/auth';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
@ -33,9 +33,7 @@ export const UserSecurityActivityDataTable = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } =
|
||||
trpc.profile.findUserSecurityAuditLogs.useQuery(
|
||||
|
||||
@ -9,7 +9,7 @@ import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
@ -27,9 +27,7 @@ export const UserPasskeysDataTable = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery(
|
||||
{
|
||||
|
||||
@ -12,7 +12,7 @@ import { DateTime } from 'luxon';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
import { DocumentSource, DocumentStatus as DocumentStatusEnum } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@ -40,7 +40,7 @@ const DOCUMENT_SOURCE_LABELS: { [key in DocumentSource]: MessageDescriptor } = {
|
||||
TEMPLATE_DIRECT_LINK: msg`Direct link`,
|
||||
};
|
||||
|
||||
const ZTemplateSearchParamsSchema = ZBaseTableSearchParamsSchema.extend({
|
||||
const ZDocumentSearchParamsSchema = ZUrlSearchParamsSchema.extend({
|
||||
source: z
|
||||
.nativeEnum(DocumentSource)
|
||||
.optional()
|
||||
@ -49,10 +49,6 @@ const ZTemplateSearchParamsSchema = ZBaseTableSearchParamsSchema.extend({
|
||||
.nativeEnum(DocumentStatusEnum)
|
||||
.optional()
|
||||
.catch(() => undefined),
|
||||
search: z.coerce
|
||||
.string()
|
||||
.optional()
|
||||
.catch(() => undefined),
|
||||
});
|
||||
|
||||
type TemplatePageViewDocumentsTableProps = {
|
||||
@ -69,7 +65,7 @@ export const TemplatePageViewDocumentsTable = ({
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZTemplateSearchParamsSchema.parse(
|
||||
const parsedSearchParams = ZDocumentSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
|
||||
@ -80,7 +76,7 @@ export const TemplatePageViewDocumentsTable = ({
|
||||
teamId: team?.id,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
search: parsedSearchParams.search,
|
||||
query: parsedSearchParams.query,
|
||||
source: parsedSearchParams.source,
|
||||
status: parsedSearchParams.status,
|
||||
},
|
||||
|
||||
@ -29,7 +29,7 @@ export const TemplatesPageView = async ({ searchParams = {}, team }: TemplatesPa
|
||||
const documentRootPath = formatDocumentsPath(team?.url);
|
||||
const templateRootPath = formatTemplatesPath(team?.url);
|
||||
|
||||
const { templates, totalPages } = await findTemplates({
|
||||
const { data: templates, totalPages } = await findTemplates({
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
page: page,
|
||||
|
||||
@ -11,7 +11,7 @@ import { useLingui } from '@lingui/react';
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL, WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
@ -30,13 +30,11 @@ export const CurrentUserTeamsDataTable = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.team.findTeams.useQuery(
|
||||
{
|
||||
term: parsedSearchParams.query,
|
||||
query: parsedSearchParams.query,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
},
|
||||
|
||||
@ -9,7 +9,7 @@ import { useLingui } from '@lingui/react';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
@ -27,15 +27,13 @@ export const PendingUserTeamsDataTable = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const [checkoutPendingTeamId, setCheckoutPendingTeamId] = useState<number | null>(null);
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.team.findTeamsPending.useQuery(
|
||||
{
|
||||
term: parsedSearchParams.query,
|
||||
query: parsedSearchParams.query,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
},
|
||||
|
||||
@ -10,7 +10,7 @@ import { History, MoreHorizontal, Trash2 } from 'lucide-react';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
@ -38,15 +38,13 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
|
||||
const { _, i18n } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } =
|
||||
trpc.team.findTeamMemberInvites.useQuery(
|
||||
{
|
||||
teamId,
|
||||
term: parsedSearchParams.query,
|
||||
query: parsedSearchParams.query,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
},
|
||||
|
||||
@ -10,7 +10,7 @@ import { Edit, MoreHorizontal, Trash2 } from 'lucide-react';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import type { TeamMemberRole } from '@documenso/prisma/client';
|
||||
@ -50,14 +50,12 @@ export const TeamMembersDataTable = ({
|
||||
const searchParams = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.team.findTeamMembers.useQuery(
|
||||
{
|
||||
teamId,
|
||||
term: parsedSearchParams.query,
|
||||
query: parsedSearchParams.query,
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
},
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.9.0-rc.0",
|
||||
"version": "1.9.0-rc.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.9.0-rc.0",
|
||||
"version": "1.9.0-rc.2",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@ -79,7 +79,7 @@
|
||||
},
|
||||
"apps/marketing": {
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.9.0-rc.0",
|
||||
"version": "1.9.0-rc.2",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/assets": "*",
|
||||
@ -492,7 +492,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.0-rc.0",
|
||||
"version": "1.9.0-rc.2",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.9.0-rc.0",
|
||||
"version": "1.9.0-rc.2",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
|
||||
@ -35,6 +35,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 { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
@ -427,7 +428,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
const perPage = Number(args.query.perPage) || 10;
|
||||
|
||||
try {
|
||||
const { templates, totalPages } = await findTemplates({
|
||||
const { data: templates, totalPages } = await findTemplates({
|
||||
page,
|
||||
perPage,
|
||||
userId: user.id,
|
||||
@ -637,69 +638,52 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}),
|
||||
|
||||
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id } = args.params;
|
||||
const { sendEmail = true } = args.body ?? {};
|
||||
|
||||
const document = await getDocumentById({
|
||||
documentId: Number(id),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Document is already complete',
|
||||
},
|
||||
};
|
||||
}
|
||||
const { id: documentId } = args.params;
|
||||
const { sendEmail, sendCompletionEmails } = args.body;
|
||||
|
||||
try {
|
||||
// await setRecipientsForDocument({
|
||||
// userId: user.id,
|
||||
// documentId: Number(id),
|
||||
// recipients: [
|
||||
// {
|
||||
// email: body.signerEmail,
|
||||
// name: body.signerName ?? '',
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
const document = await getDocumentById({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
// await setFieldsForDocument({
|
||||
// documentId: Number(id),
|
||||
// userId: user.id,
|
||||
// fields: body.fields.map((field) => ({
|
||||
// signerEmail: body.signerEmail,
|
||||
// type: field.fieldType,
|
||||
// pageNumber: field.pageNumber,
|
||||
// pageX: field.pageX,
|
||||
// pageY: field.pageY,
|
||||
// pageWidth: field.pageWidth,
|
||||
// pageHeight: field.pageHeight,
|
||||
// })),
|
||||
// });
|
||||
if (!document) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// if (body.emailBody || body.emailSubject) {
|
||||
// await upsertDocumentMeta({
|
||||
// documentId: Number(id),
|
||||
// subject: body.emailSubject ?? '',
|
||||
// message: body.emailBody ?? '',
|
||||
// });
|
||||
// }
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Document is already complete',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
|
||||
|
||||
// Update document email settings if sendCompletionEmails is provided
|
||||
if (typeof sendCompletionEmails === 'boolean') {
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
emailSettings: {
|
||||
...emailSettings,
|
||||
documentCompleted: sendCompletionEmails,
|
||||
ownerDocumentCompleted: sendCompletionEmails,
|
||||
},
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
}
|
||||
|
||||
const { Recipient: recipients, ...sentDocument } = await sendDocument({
|
||||
documentId: Number(id),
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
sendEmail,
|
||||
|
||||
@ -88,8 +88,12 @@ export const ZSendDocumentForSigningMutationSchema = z
|
||||
description:
|
||||
'Whether to send an email to the recipients asking them to action the document. If you disable this, you will need to manually distribute the document to the recipients using the generated signing links.',
|
||||
}),
|
||||
sendCompletionEmails: z.boolean().optional().openapi({
|
||||
description:
|
||||
'Whether to send completion emails when the document is fully signed. This will override the document email settings.',
|
||||
}),
|
||||
})
|
||||
.or(z.literal('').transform(() => ({ sendEmail: true })));
|
||||
.or(z.literal('').transform(() => ({ sendEmail: true, sendCompletionEmails: undefined })));
|
||||
|
||||
export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema;
|
||||
|
||||
|
||||
137
packages/app-tests/e2e/api/v1/document-sending.spec.ts
Normal file
137
packages/app-tests/e2e/api/v1/document-sending.spec.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
test.describe('Document API', () => {
|
||||
test('sendDocument: should respect sendCompletionEmails setting', async ({ request }) => {
|
||||
const user = await seedUser();
|
||||
|
||||
const { document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: ['signer@example.com'],
|
||||
});
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: user.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
// Test with sendCompletionEmails: false
|
||||
const response = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${document.id}/send`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
sendCompletionEmails: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
// Verify email settings were updated
|
||||
const updatedDocument = await prisma.document.findUnique({
|
||||
where: { id: document.id },
|
||||
include: { documentMeta: true },
|
||||
});
|
||||
|
||||
expect(updatedDocument?.documentMeta?.emailSettings).toMatchObject({
|
||||
documentCompleted: false,
|
||||
ownerDocumentCompleted: false,
|
||||
});
|
||||
|
||||
// Test with sendCompletionEmails: true
|
||||
const response2 = await request.post(
|
||||
`${WEBAPP_BASE_URL}/api/v1/documents/${document.id}/send`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
sendCompletionEmails: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(response2.ok()).toBeTruthy();
|
||||
expect(response2.status()).toBe(200);
|
||||
|
||||
// Verify email settings were updated
|
||||
const updatedDocument2 = await prisma.document.findUnique({
|
||||
where: { id: document.id },
|
||||
include: { documentMeta: true },
|
||||
});
|
||||
|
||||
expect(updatedDocument2?.documentMeta?.emailSettings ?? {}).toMatchObject({
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('sendDocument: should not modify email settings when sendCompletionEmails is not provided', async ({
|
||||
request,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
|
||||
const { document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: ['signer@example.com'],
|
||||
});
|
||||
|
||||
// Set initial email settings
|
||||
await prisma.documentMeta.upsert({
|
||||
where: { documentId: document.id },
|
||||
create: {
|
||||
documentId: document.id,
|
||||
emailSettings: {
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: false,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
documentId: document.id,
|
||||
emailSettings: {
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: user.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const response = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${document.id}/send`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
sendEmail: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
// Verify email settings were not modified
|
||||
const updatedDocument = await prisma.document.findUnique({
|
||||
where: { id: document.id },
|
||||
include: { documentMeta: true },
|
||||
});
|
||||
|
||||
expect(updatedDocument?.documentMeta?.emailSettings ?? {}).toMatchObject({
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -46,3 +46,10 @@ export const PASSKEY_TIMEOUT = 60000;
|
||||
* The maximum number of passkeys are user can have.
|
||||
*/
|
||||
export const MAXIMUM_PASSKEYS = 50;
|
||||
|
||||
export const useSecureCookies =
|
||||
process.env.NODE_ENV === 'production' && String(process.env.NEXTAUTH_URL).startsWith('https://');
|
||||
|
||||
const secureCookiePrefix = useSecureCookies ? '__Secure-' : '';
|
||||
|
||||
export const formatSecureCookieName = (name: string) => `${secureCookiePrefix}${name}`;
|
||||
|
||||
@ -13,6 +13,7 @@ import { env } from 'next-runtime-env';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import { formatSecureCookieName, useSecureCookies } from '../constants/auth';
|
||||
import { AppError, AppErrorCode } from '../errors/app-error';
|
||||
import { jobsClient } from '../jobs/client';
|
||||
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
|
||||
@ -26,10 +27,6 @@ import { extractNextAuthRequestMetadata } from '../universal/extract-request-met
|
||||
import { getAuthenticatorOptions } from '../utils/authenticator';
|
||||
import { ErrorCode } from './error-codes';
|
||||
|
||||
const useSecureCookies =
|
||||
process.env.NODE_ENV === 'production' && String(process.env.NEXTAUTH_URL).startsWith('https://');
|
||||
const cookiePrefix = useSecureCookies ? '__Secure-' : '';
|
||||
|
||||
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
adapter: PrismaAdapter(prisma),
|
||||
secret: process.env.NEXTAUTH_SECRET ?? 'secret',
|
||||
@ -437,7 +434,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
},
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: `${cookiePrefix}next-auth.session-token`,
|
||||
name: formatSecureCookieName('next-auth.session-token'),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
@ -446,7 +443,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
},
|
||||
},
|
||||
callbackUrl: {
|
||||
name: `${cookiePrefix}next-auth.callback-url`,
|
||||
name: formatSecureCookieName('next-auth.callback-url'),
|
||||
options: {
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
path: '/',
|
||||
@ -456,7 +453,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
csrfToken: {
|
||||
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
|
||||
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
|
||||
name: `${cookiePrefix}next-auth.csrf-token`,
|
||||
name: formatSecureCookieName('next-auth.csrf-token'),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
@ -465,7 +462,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
},
|
||||
},
|
||||
pkceCodeVerifier: {
|
||||
name: `${cookiePrefix}next-auth.pkce.code_verifier`,
|
||||
name: formatSecureCookieName('next-auth.pkce.code_verifier'),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
@ -474,7 +471,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
},
|
||||
},
|
||||
state: {
|
||||
name: `${cookiePrefix}next-auth.state`,
|
||||
name: formatSecureCookieName('next-auth.state'),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindDocumentsOptions {
|
||||
term?: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
}
|
||||
|
||||
export const findDocuments = async ({ term, page = 1, perPage = 10 }: FindDocumentsOptions) => {
|
||||
const termFilters: Prisma.DocumentWhereInput | undefined = !term
|
||||
export const findDocuments = async ({ query, page = 1, perPage = 10 }: FindDocumentsOptions) => {
|
||||
const termFilters: Prisma.DocumentWhereInput | undefined = !query
|
||||
? undefined
|
||||
: {
|
||||
title: {
|
||||
contains: term,
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
};
|
||||
@ -51,5 +53,5 @@ export const findDocuments = async ({ term, page = 1, perPage = 10 }: FindDocume
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
};
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Passkey } from '@documenso/prisma/client';
|
||||
import { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindPasskeysOptions {
|
||||
userId: number;
|
||||
term?: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
@ -17,7 +18,7 @@ export interface FindPasskeysOptions {
|
||||
|
||||
export const findPasskeys = async ({
|
||||
userId,
|
||||
term = '',
|
||||
query = '',
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
@ -30,9 +31,9 @@ export const findPasskeys = async ({
|
||||
userId,
|
||||
};
|
||||
|
||||
if (term.length > 0) {
|
||||
if (query.length > 0) {
|
||||
whereClause.name = {
|
||||
contains: term,
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
@ -72,5 +73,5 @@ export const findPasskeys = async ({
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultSet<typeof data>;
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentAuditLog } from '@documenso/prisma/client';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import type { DocumentAuditLog, Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export interface FindDocumentAuditLogsOptions {
|
||||
@ -31,7 +31,7 @@ export const findDocumentAuditLogs = async ({
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const documentFilter = await prisma.document.findFirstOrThrow({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
@ -51,6 +51,10 @@ export const findDocumentAuditLogs = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.DocumentAuditLogWhereInput = {
|
||||
documentId,
|
||||
};
|
||||
@ -113,5 +117,5 @@ export const findDocumentAuditLogs = async ({
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
nextCursor,
|
||||
} satisfies FindResultSet<typeof parsedData> & { nextCursor?: string };
|
||||
} satisfies FindResultResponse<typeof parsedData> & { nextCursor?: string };
|
||||
};
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { P, match } from 'ts-pattern';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type {
|
||||
Document,
|
||||
DocumentSource,
|
||||
@ -11,10 +10,11 @@ import type {
|
||||
TeamEmail,
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import type { FindResultSet } from '../../types/find-result-set';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
|
||||
|
||||
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
|
||||
@ -22,7 +22,6 @@ export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
|
||||
export type FindDocumentsOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
term?: string;
|
||||
templateId?: number;
|
||||
source?: DocumentSource;
|
||||
status?: ExtendedDocumentStatus;
|
||||
@ -34,13 +33,12 @@ export type FindDocumentsOptions = {
|
||||
};
|
||||
period?: PeriodSelectorValue;
|
||||
senderIds?: number[];
|
||||
search?: string;
|
||||
query?: string;
|
||||
};
|
||||
|
||||
export const findDocuments = async ({
|
||||
userId,
|
||||
teamId,
|
||||
term,
|
||||
templateId,
|
||||
source,
|
||||
status = ExtendedDocumentStatus.ALL,
|
||||
@ -49,7 +47,7 @@ export const findDocuments = async ({
|
||||
orderBy,
|
||||
period,
|
||||
senderIds,
|
||||
search,
|
||||
query,
|
||||
}: FindDocumentsOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
@ -87,22 +85,11 @@ export const findDocuments = async ({
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
const teamMemberRole = team?.members[0].role ?? null;
|
||||
|
||||
const termFilters = match(term)
|
||||
.with(P.string.minLength(1), () => {
|
||||
return {
|
||||
title: {
|
||||
contains: term,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
} as const;
|
||||
})
|
||||
.otherwise(() => undefined);
|
||||
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
@ -209,7 +196,6 @@ export const findDocuments = async ({
|
||||
}
|
||||
|
||||
const whereAndClause: Prisma.DocumentWhereInput['AND'] = [
|
||||
{ ...termFilters },
|
||||
{ ...filters },
|
||||
{ ...deletedFilter },
|
||||
{ ...searchFilter },
|
||||
@ -290,7 +276,7 @@ export const findDocuments = async ({
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultSet<typeof data>;
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
|
||||
const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
|
||||
@ -72,14 +72,19 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const isDocumentCompletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).documentCompleted;
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
|
||||
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
|
||||
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
|
||||
|
||||
// If the document owner is not a recipient, OR recipient emails are disabled, then send the email to them separately.
|
||||
// Send email to document owner if:
|
||||
// 1. Owner document completed emails are enabled AND
|
||||
// 2. Either:
|
||||
// - The owner is not a recipient, OR
|
||||
// - Recipient emails are disabled
|
||||
if (
|
||||
!document.Recipient.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled
|
||||
isOwnerDocumentCompletedEmailEnabled &&
|
||||
(!document.Recipient.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled)
|
||||
) {
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
|
||||
@ -7,12 +7,12 @@ import { Prisma } from '@documenso/prisma/client';
|
||||
import { TeamMemberInviteSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { type FindResultSet, ZFindResultSet } from '../../types/find-result-set';
|
||||
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindTeamMemberInvitesOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
term?: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
@ -21,7 +21,7 @@ export interface FindTeamMemberInvitesOptions {
|
||||
};
|
||||
}
|
||||
|
||||
export const ZFindTeamMemberInvitesResponseSchema = ZFindResultSet.extend({
|
||||
export const ZFindTeamMemberInvitesResponseSchema = ZFindResultResponse.extend({
|
||||
data: TeamMemberInviteSchema.pick({
|
||||
id: true,
|
||||
teamId: true,
|
||||
@ -36,7 +36,7 @@ export type TFindTeamMemberInvitesResponse = z.infer<typeof ZFindTeamMemberInvit
|
||||
export const findTeamMemberInvites = async ({
|
||||
userId,
|
||||
teamId,
|
||||
term,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
@ -59,10 +59,10 @@ export const findTeamMemberInvites = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const termFilters: Prisma.TeamMemberInviteWhereInput | undefined = match(term)
|
||||
const termFilters: Prisma.TeamMemberInviteWhereInput | undefined = match(query)
|
||||
.with(P.string.minLength(1), () => ({
|
||||
email: {
|
||||
contains: term,
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
}))
|
||||
@ -101,5 +101,5 @@ export const findTeamMemberInvites = async ({
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultSet<typeof data>;
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
|
||||
@ -6,12 +6,13 @@ import type { TeamMember } from '@documenso/prisma/client';
|
||||
import { Prisma } from '@documenso/prisma/client';
|
||||
import { TeamMemberSchema, UserSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { type FindResultSet, ZFindResultSet } from '../../types/find-result-set';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { ZFindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindTeamMembersOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
term?: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
@ -20,7 +21,7 @@ export interface FindTeamMembersOptions {
|
||||
};
|
||||
}
|
||||
|
||||
export const ZFindTeamMembersResponseSchema = ZFindResultSet.extend({
|
||||
export const ZFindTeamMembersResponseSchema = ZFindResultResponse.extend({
|
||||
data: TeamMemberSchema.extend({
|
||||
user: UserSchema.pick({
|
||||
name: true,
|
||||
@ -34,7 +35,7 @@ export type TFindTeamMembersResponse = z.infer<typeof ZFindTeamMembersResponseSc
|
||||
export const findTeamMembers = async ({
|
||||
userId,
|
||||
teamId,
|
||||
term,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
@ -54,11 +55,11 @@ export const findTeamMembers = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const termFilters: Prisma.TeamMemberWhereInput | undefined = match(term)
|
||||
const termFilters: Prisma.TeamMemberWhereInput | undefined = match(query)
|
||||
.with(P.string.minLength(1), () => ({
|
||||
user: {
|
||||
name: {
|
||||
contains: term,
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
@ -109,5 +110,5 @@ export const findTeamMembers = async ({
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultSet<typeof data>;
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
|
||||
@ -5,11 +5,11 @@ import type { Team } from '@documenso/prisma/client';
|
||||
import { Prisma } from '@documenso/prisma/client';
|
||||
import { TeamPendingSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { type FindResultSet, ZFindResultSet } from '../../types/find-result-set';
|
||||
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindTeamsPendingOptions {
|
||||
userId: number;
|
||||
term?: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
@ -18,7 +18,7 @@ export interface FindTeamsPendingOptions {
|
||||
};
|
||||
}
|
||||
|
||||
export const ZFindTeamsPendingResponseSchema = ZFindResultSet.extend({
|
||||
export const ZFindTeamsPendingResponseSchema = ZFindResultResponse.extend({
|
||||
data: TeamPendingSchema.array(),
|
||||
});
|
||||
|
||||
@ -26,7 +26,7 @@ export type TFindTeamsPendingResponse = z.infer<typeof ZFindTeamsPendingResponse
|
||||
|
||||
export const findTeamsPending = async ({
|
||||
userId,
|
||||
term,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
@ -38,9 +38,9 @@ export const findTeamsPending = async ({
|
||||
ownerUserId: userId,
|
||||
};
|
||||
|
||||
if (term && term.length > 0) {
|
||||
if (query && query.length > 0) {
|
||||
whereClause.name = {
|
||||
contains: term,
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
@ -65,5 +65,5 @@ export const findTeamsPending = async ({
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultSet<typeof data>;
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
import { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindTeamsOptions {
|
||||
userId: number;
|
||||
term?: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
@ -16,7 +17,7 @@ export interface FindTeamsOptions {
|
||||
|
||||
export const findTeams = async ({
|
||||
userId,
|
||||
term,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
@ -32,9 +33,9 @@ export const findTeams = async ({
|
||||
},
|
||||
};
|
||||
|
||||
if (term && term.length > 0) {
|
||||
if (query && query.length > 0) {
|
||||
whereClause.name = {
|
||||
contains: term,
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
@ -72,5 +73,5 @@ export const findTeams = async ({
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultSet<typeof maskedData>;
|
||||
} satisfies FindResultResponse<typeof maskedData>;
|
||||
};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma, Template } from '@documenso/prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export type FindTemplatesOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
@ -10,7 +12,7 @@ export type FindTemplatesOptions = {
|
||||
};
|
||||
|
||||
export type FindTemplatesResponse = Awaited<ReturnType<typeof findTemplates>>;
|
||||
export type FindTemplateRow = FindTemplatesResponse['templates'][number];
|
||||
export type FindTemplateRow = FindTemplatesResponse['data'][number];
|
||||
|
||||
export const findTemplates = async ({
|
||||
userId,
|
||||
@ -38,7 +40,7 @@ export const findTemplates = async ({
|
||||
};
|
||||
}
|
||||
|
||||
const [templates, count] = await Promise.all([
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.template.findMany({
|
||||
where: whereFilter,
|
||||
include: {
|
||||
@ -75,7 +77,10 @@ export const findTemplates = async ({
|
||||
]);
|
||||
|
||||
return {
|
||||
templates,
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
};
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { UserSecurityAuditLog, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export type FindUserSecurityAuditLogsOptions = {
|
||||
userId: number;
|
||||
type?: UserSecurityAuditLogType;
|
||||
@ -48,5 +49,5 @@ export const findUserSecurityAuditLogs = async ({
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultSet<typeof data>;
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ export enum DocumentEmailEvents {
|
||||
DocumentPending = 'documentPending',
|
||||
DocumentCompleted = 'documentCompleted',
|
||||
DocumentDeleted = 'documentDeleted',
|
||||
OwnerDocumentCompleted = 'ownerDocumentCompleted',
|
||||
}
|
||||
|
||||
export const ZDocumentEmailSettingsSchema = z
|
||||
@ -18,6 +19,7 @@ export const ZDocumentEmailSettingsSchema = z
|
||||
documentPending: z.boolean().default(true),
|
||||
documentCompleted: z.boolean().default(true),
|
||||
documentDeleted: z.boolean().default(true),
|
||||
ownerDocumentCompleted: z.boolean().default(true),
|
||||
})
|
||||
.strip()
|
||||
.catch(() => ({
|
||||
@ -26,6 +28,7 @@ export const ZDocumentEmailSettingsSchema = z
|
||||
documentPending: true,
|
||||
documentCompleted: true,
|
||||
documentDeleted: true,
|
||||
ownerDocumentCompleted: true,
|
||||
}));
|
||||
|
||||
export type TDocumentEmailSettings = z.infer<typeof ZDocumentEmailSettingsSchema>;
|
||||
@ -48,5 +51,6 @@ export const extractDerivedDocumentEmailSettings = (
|
||||
documentPending: false,
|
||||
documentCompleted: false,
|
||||
documentDeleted: false,
|
||||
ownerDocumentCompleted: emailSettings.ownerDocumentCompleted,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZFindResultSet = z.object({
|
||||
data: z.union([z.array(z.unknown()), z.unknown()]),
|
||||
count: z.number(),
|
||||
currentPage: z.number(),
|
||||
perPage: z.number(),
|
||||
totalPages: z.number(),
|
||||
});
|
||||
|
||||
// Can't infer generics from Zod.
|
||||
export type FindResultSet<T> = {
|
||||
data: T extends Array<unknown> ? T : T[];
|
||||
count: number;
|
||||
currentPage: number;
|
||||
perPage: number;
|
||||
totalPages: number;
|
||||
};
|
||||
@ -1,6 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZBaseTableSearchParamsSchema = z.object({
|
||||
/**
|
||||
* Backend only schema is used for find search params.
|
||||
*
|
||||
* Does not catch, because TRPC Open API won't allow catches as a type.
|
||||
*
|
||||
* Keep this and `ZUrlSearchParamsSchema` in sync.
|
||||
*/
|
||||
export const ZFindSearchParamsSchema = z.object({
|
||||
query: z.string().optional(),
|
||||
page: z.coerce.number().min(1).optional(),
|
||||
perPage: z.coerce.number().min(1).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Frontend schema used to parse search params from URL.
|
||||
*
|
||||
* Keep this and `ZFindSearchParamsSchema` in sync.
|
||||
*/
|
||||
export const ZUrlSearchParamsSchema = z.object({
|
||||
query: z
|
||||
.string()
|
||||
.optional()
|
||||
@ -17,4 +35,19 @@ export const ZBaseTableSearchParamsSchema = z.object({
|
||||
.catch(() => undefined),
|
||||
});
|
||||
|
||||
export type TBaseTableSearchParamsSchema = z.infer<typeof ZBaseTableSearchParamsSchema>;
|
||||
export const ZFindResultResponse = z.object({
|
||||
data: z.union([z.array(z.unknown()), z.unknown()]),
|
||||
count: z.number(),
|
||||
currentPage: z.number(),
|
||||
perPage: z.number(),
|
||||
totalPages: z.number(),
|
||||
});
|
||||
|
||||
// Can't infer generics from Zod.
|
||||
export type FindResultResponse<T> = {
|
||||
data: T extends Array<unknown> ? T : T[];
|
||||
count: number;
|
||||
currentPage: number;
|
||||
perPage: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
@ -16,6 +16,7 @@ export const subscriptionsContainsActivePlan = (
|
||||
subscription.status === SubscriptionStatus.ACTIVE && priceIds.includes(subscription.priceId),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if there is a subscription that is active and is one of the provided product IDs.
|
||||
*/
|
||||
@ -38,5 +39,13 @@ export const subscriptionsContainActiveEnterprisePlan = (
|
||||
return false;
|
||||
}
|
||||
|
||||
return subscriptionsContainsActivePlan(subscriptions, [enterprisePlanId]);
|
||||
const acceptableStatuses: SubscriptionStatus[] = [
|
||||
SubscriptionStatus.ACTIVE,
|
||||
SubscriptionStatus.PAST_DUE,
|
||||
];
|
||||
|
||||
return subscriptions.some(
|
||||
(subscription) =>
|
||||
acceptableStatuses.includes(subscription.status) && enterprisePlanId === subscription.priceId,
|
||||
);
|
||||
};
|
||||
|
||||
@ -24,9 +24,9 @@ import {
|
||||
|
||||
export const adminRouter = router({
|
||||
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
|
||||
const { term, page, perPage } = input;
|
||||
const { query, page, perPage } = input;
|
||||
|
||||
return await findDocuments({ term, page, perPage });
|
||||
return await findDocuments({ query, page, perPage });
|
||||
}),
|
||||
|
||||
updateUser: adminProcedure
|
||||
|
||||
@ -2,10 +2,9 @@ import { Role } from '@prisma/client';
|
||||
import z from 'zod';
|
||||
|
||||
import { ZSiteSettingSchema } from '@documenso/lib/server-only/site-settings/schema';
|
||||
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
|
||||
export const ZAdminFindDocumentsQuerySchema = z.object({
|
||||
term: z.string().optional(),
|
||||
page: z.number().optional().default(1),
|
||||
export const ZAdminFindDocumentsQuerySchema = ZFindSearchParamsSchema.extend({
|
||||
perPage: z.number().optional().default(20),
|
||||
});
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { parse } from 'cookie-es';
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { formatSecureCookieName } from '@documenso/lib/constants/auth';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { jobsClient } from '@documenso/lib/jobs/client';
|
||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
||||
@ -111,7 +112,8 @@ export const authRouter = router({
|
||||
const cookies = parse(ctx.req.headers.cookie ?? '');
|
||||
|
||||
const sessionIdToken =
|
||||
cookies['__Host-next-auth.csrf-token'] || cookies['next-auth.csrf-token'];
|
||||
cookies[formatSecureCookieName('__Host-next-auth.csrf-token')] ||
|
||||
cookies[formatSecureCookieName('next-auth.csrf-token')];
|
||||
|
||||
if (!sessionIdToken) {
|
||||
throw new Error('Missing CSRF token');
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZRegistrationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
||||
|
||||
export const ZCurrentPasswordSchema = z
|
||||
@ -55,7 +55,7 @@ export const ZUpdatePasskeyMutationSchema = z.object({
|
||||
name: z.string().trim().min(1),
|
||||
});
|
||||
|
||||
export const ZFindPasskeysQuerySchema = ZBaseTableSearchParamsSchema.extend({
|
||||
export const ZFindPasskeysQuerySchema = ZFindSearchParamsSchema.extend({
|
||||
orderBy: z
|
||||
.object({
|
||||
column: z.enum(['createdAt', 'updatedAt', 'name']),
|
||||
|
||||
@ -86,13 +86,13 @@ export const documentRouter = router({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { user } = ctx;
|
||||
|
||||
const { search, teamId, templateId, page, perPage, orderBy, source, status } = input;
|
||||
const { query, teamId, templateId, page, perPage, orderBy, source, status } = input;
|
||||
|
||||
const documents = await findDocuments({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
templateId,
|
||||
search,
|
||||
query,
|
||||
source,
|
||||
status,
|
||||
page,
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import {
|
||||
DocumentDistributionMethod,
|
||||
@ -18,13 +18,9 @@ import {
|
||||
RecipientRole,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
export const ZFindDocumentsQuerySchema = ZBaseTableSearchParamsSchema.extend({
|
||||
export const ZFindDocumentsQuerySchema = ZFindSearchParamsSchema.extend({
|
||||
teamId: z.number().min(1).optional(),
|
||||
templateId: z.number().min(1).optional(),
|
||||
search: z
|
||||
.string()
|
||||
.optional()
|
||||
.catch(() => undefined),
|
||||
source: z.nativeEnum(DocumentSource).optional(),
|
||||
status: z.nativeEnum(DocumentStatus).optional(),
|
||||
orderBy: z
|
||||
@ -33,9 +29,9 @@ export const ZFindDocumentsQuerySchema = ZBaseTableSearchParamsSchema.extend({
|
||||
direction: z.enum(['asc', 'desc']),
|
||||
})
|
||||
.optional(),
|
||||
}).omit({ query: true });
|
||||
});
|
||||
|
||||
export const ZFindDocumentAuditLogsQuerySchema = ZBaseTableSearchParamsSchema.extend({
|
||||
export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({
|
||||
documentId: z.number().min(1),
|
||||
cursor: z.string().optional(),
|
||||
filterForRecentActivity: z.boolean().optional(),
|
||||
|
||||
@ -74,20 +74,28 @@ import {
|
||||
} from './schema';
|
||||
|
||||
export const teamRouter = router({
|
||||
getTeams: authenticatedProcedure
|
||||
// Internal endpoint for now.
|
||||
getTeams: authenticatedProcedure.query(async ({ ctx }) => {
|
||||
return await getTeams({ userId: ctx.user.id });
|
||||
}),
|
||||
|
||||
findTeams: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/team',
|
||||
summary: 'Get teams',
|
||||
description: 'Returns all teams that you are a member of',
|
||||
summary: 'Find teams',
|
||||
description: 'Find your teams based on a search criteria',
|
||||
tags: ['Teams'],
|
||||
},
|
||||
})
|
||||
.input(z.void())
|
||||
.input(ZFindTeamsQuerySchema)
|
||||
.output(z.unknown())
|
||||
.query(async ({ ctx }) => {
|
||||
return await getTeams({ userId: ctx.user.id });
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await findTeams({
|
||||
userId: ctx.user.id,
|
||||
...input,
|
||||
});
|
||||
}),
|
||||
|
||||
getTeam: authenticatedProcedure
|
||||
@ -327,14 +335,6 @@ export const teamRouter = router({
|
||||
});
|
||||
}),
|
||||
|
||||
// Todo: Refactor, seems to be a redundant endpoint.
|
||||
findTeams: authenticatedProcedure.input(ZFindTeamsQuerySchema).query(async ({ input, ctx }) => {
|
||||
return await findTeams({
|
||||
userId: ctx.user.id,
|
||||
...input,
|
||||
});
|
||||
}),
|
||||
|
||||
// Internal endpoint for now.
|
||||
createTeamEmailVerification: authenticatedProcedure
|
||||
// .meta({
|
||||
|
||||
@ -2,17 +2,11 @@ import { z } from 'zod';
|
||||
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams';
|
||||
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { ZUpdatePublicProfileMutationSchema } from '../profile-router/schema';
|
||||
|
||||
// Consider refactoring to use ZBaseTableSearchParamsSchema.
|
||||
const GenericFindQuerySchema = z.object({
|
||||
term: z.string().optional(),
|
||||
page: z.number().min(1).optional(),
|
||||
perPage: z.number().min(1).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Restrict team URLs schema.
|
||||
*
|
||||
@ -122,17 +116,17 @@ export const ZFindTeamInvoicesQuerySchema = z.object({
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export const ZFindTeamMemberInvitesQuerySchema = GenericFindQuerySchema.extend({
|
||||
export const ZFindTeamMemberInvitesQuerySchema = ZFindSearchParamsSchema.extend({
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export const ZFindTeamMembersQuerySchema = GenericFindQuerySchema.extend({
|
||||
export const ZFindTeamMembersQuerySchema = ZFindSearchParamsSchema.extend({
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export const ZFindTeamsQuerySchema = GenericFindQuerySchema;
|
||||
export const ZFindTeamsQuerySchema = ZFindSearchParamsSchema;
|
||||
|
||||
export const ZFindTeamsPendingQuerySchema = GenericFindQuerySchema;
|
||||
export const ZFindTeamsPendingQuerySchema = ZFindSearchParamsSchema;
|
||||
|
||||
export const ZGetTeamQuerySchema = z.object({
|
||||
teamId: z.number(),
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import {
|
||||
DocumentDistributionMethod,
|
||||
@ -125,7 +125,7 @@ export const ZSetSigningOrderForTemplateMutationSchema = z.object({
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
});
|
||||
|
||||
export const ZFindTemplatesQuerySchema = ZBaseTableSearchParamsSchema.extend({
|
||||
export const ZFindTemplatesQuerySchema = ZFindSearchParamsSchema.extend({
|
||||
teamId: z.number().optional(),
|
||||
type: z.nativeEnum(TemplateType).optional(),
|
||||
});
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import type { TDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||
import { DocumentEmailEvents } from '@documenso/lib/types/document-email';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Checkbox } from '../../primitives/checkbox';
|
||||
|
||||
type Value = Record<DocumentEmailEvents, boolean>;
|
||||
type Value = TDocumentEmailSettings;
|
||||
|
||||
type DocumentEmailCheckboxesProps = {
|
||||
value: Value;
|
||||
@ -217,6 +218,46 @@ export const DocumentEmailCheckboxes = ({
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
id={DocumentEmailEvents.OwnerDocumentCompleted}
|
||||
className="h-5 w-5"
|
||||
checkClassName="dark:text-white text-primary"
|
||||
checked={value.ownerDocumentCompleted}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...value, [DocumentEmailEvents.OwnerDocumentCompleted]: Boolean(checked) })
|
||||
}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
htmlFor={DocumentEmailEvents.OwnerDocumentCompleted}
|
||||
>
|
||||
<Trans>Send document completed email to the owner</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document completed email to the owner</Trans>
|
||||
</strong>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This will be sent to the document owner once the document has been fully
|
||||
completed.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user