mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
Merge branch 'main' into power-signer
This commit is contained in:
@ -4,15 +4,14 @@ import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
|
||||
|
||||
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
|
||||
const planTypes = typeof plan === 'string' ? [plan] : plan;
|
||||
const planTypes: string[] = typeof plan === 'string' ? [plan] : plan;
|
||||
|
||||
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
|
||||
|
||||
const { data: prices } = await stripe.prices.search({
|
||||
query,
|
||||
const prices = await stripe.prices.list({
|
||||
expand: ['data.product'],
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return prices.filter((price) => price.type === 'recurring');
|
||||
return prices.data.filter(
|
||||
(price) => price.type === 'recurring' && planTypes.includes(price.metadata.plan),
|
||||
);
|
||||
};
|
||||
|
||||
56
packages/ee/server-only/util/is-community-plan.ts
Normal file
56
packages/ee/server-only/util/is-community-plan.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Subscription } from '@documenso/prisma/client';
|
||||
|
||||
import { getCommunityPlanPriceIds } from '../stripe/get-community-plan-prices';
|
||||
|
||||
export type IsCommunityPlanOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the user or team is on the community plan.
|
||||
*/
|
||||
export const isCommunityPlan = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: IsCommunityPlanOptions): Promise<boolean> => {
|
||||
let subscriptions: Subscription[] = [];
|
||||
|
||||
if (teamId) {
|
||||
subscriptions = await prisma.team
|
||||
.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
select: {
|
||||
owner: {
|
||||
include: {
|
||||
subscriptions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((team) => team.owner.subscriptions);
|
||||
} else {
|
||||
subscriptions = await prisma.user
|
||||
.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
subscriptions: true,
|
||||
},
|
||||
})
|
||||
.then((user) => user.subscriptions);
|
||||
}
|
||||
|
||||
if (subscriptions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const communityPlanPriceIds = await getCommunityPlanPriceIds();
|
||||
|
||||
return subscriptionsContainsActivePlan(subscriptions, communityPlanPriceIds);
|
||||
};
|
||||
@ -12,6 +12,7 @@ import {
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import {
|
||||
@ -72,6 +73,13 @@ export const completeDocumentWithToken = async ({
|
||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||
}
|
||||
|
||||
if (recipient.signingStatus === SigningStatus.REJECTED) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Recipient has already rejected the document',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||
|
||||
|
||||
@ -88,6 +88,7 @@ export const findDocuments = async ({
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ externalId: { contains: query, mode: 'insensitive' } },
|
||||
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||
],
|
||||
|
||||
@ -34,6 +34,14 @@ export const searchDocumentsWithKeyword = async ({
|
||||
userId: userId,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
externalId: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
userId: userId,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
@ -88,6 +96,23 @@ export const searchDocumentsWithKeyword = async ({
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
externalId: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
teamId: {
|
||||
not: null,
|
||||
},
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `expires` on the `Session` table. All the data in the column will be lost.
|
||||
- Added the required column `expiresAt` to the `Session` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `updatedAt` to the `Session` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "password" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Session" DROP COLUMN "expires",
|
||||
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
ADD COLUMN "ipAddress" TEXT,
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
ADD COLUMN "userAgent" TEXT;
|
||||
@ -270,18 +270,25 @@ model Account {
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
password String?
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId Int
|
||||
expires DateTime
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum DocumentStatus {
|
||||
|
||||
@ -5,6 +5,18 @@ import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||
|
||||
import { prisma } from '..';
|
||||
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
||||
import { seedPendingDocument } from './documents';
|
||||
import { seedDirectTemplate, seedTemplate } from './templates';
|
||||
|
||||
const createDocumentData = async ({ documentData }: { documentData: string }) => {
|
||||
return prisma.documentData.create({
|
||||
data: {
|
||||
type: DocumentDataType.BYTES_64,
|
||||
data: documentData,
|
||||
initialData: documentData,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const seedDatabase = async () => {
|
||||
const examplePdf = fs
|
||||
@ -39,35 +51,80 @@ export const seedDatabase = async () => {
|
||||
update: {},
|
||||
});
|
||||
|
||||
const examplePdfData = await prisma.documentData.upsert({
|
||||
where: {
|
||||
id: 'clmn0kv5k0000pe04vcqg5zla',
|
||||
},
|
||||
create: {
|
||||
id: 'clmn0kv5k0000pe04vcqg5zla',
|
||||
type: DocumentDataType.BYTES_64,
|
||||
data: examplePdf,
|
||||
initialData: examplePdf,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const documentData = await createDocumentData({ documentData: examplePdf });
|
||||
|
||||
await prisma.document.create({
|
||||
data: {
|
||||
source: DocumentSource.DOCUMENT,
|
||||
title: 'Example Document',
|
||||
documentDataId: examplePdfData.id,
|
||||
userId: exampleUser.id,
|
||||
recipients: {
|
||||
create: {
|
||||
name: String(adminUser.name),
|
||||
email: adminUser.email,
|
||||
token: Math.random().toString(36).slice(2, 9),
|
||||
await prisma.document.create({
|
||||
data: {
|
||||
source: DocumentSource.DOCUMENT,
|
||||
title: `Example Document ${i}`,
|
||||
documentDataId: documentData.id,
|
||||
userId: exampleUser.id,
|
||||
recipients: {
|
||||
create: {
|
||||
name: String(adminUser.name),
|
||||
email: adminUser.email,
|
||||
token: Math.random().toString(36).slice(2, 9),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const documentData = await createDocumentData({ documentData: examplePdf });
|
||||
|
||||
await prisma.document.create({
|
||||
data: {
|
||||
source: DocumentSource.DOCUMENT,
|
||||
title: `Document ${i}`,
|
||||
documentDataId: documentData.id,
|
||||
userId: adminUser.id,
|
||||
recipients: {
|
||||
create: {
|
||||
name: String(exampleUser.name),
|
||||
email: exampleUser.email,
|
||||
token: Math.random().toString(36).slice(2, 9),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await seedPendingDocument(exampleUser, [adminUser], {
|
||||
key: 'example-pending',
|
||||
createDocumentOptions: {
|
||||
title: 'Pending Document',
|
||||
},
|
||||
});
|
||||
|
||||
await seedPendingDocument(adminUser, [exampleUser], {
|
||||
key: 'admin-pending',
|
||||
createDocumentOptions: {
|
||||
title: 'Pending Document',
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
seedTemplate({
|
||||
title: 'Template 1',
|
||||
userId: exampleUser.id,
|
||||
}),
|
||||
seedDirectTemplate({
|
||||
title: 'Direct Template 1',
|
||||
userId: exampleUser.id,
|
||||
}),
|
||||
|
||||
seedTemplate({
|
||||
title: 'Template 1',
|
||||
userId: adminUser.id,
|
||||
}),
|
||||
seedDirectTemplate({
|
||||
title: 'Direct Template 1',
|
||||
userId: adminUser.id,
|
||||
}),
|
||||
]);
|
||||
|
||||
const testUsers = [
|
||||
'test@documenso.com',
|
||||
'test2@documenso.com',
|
||||
|
||||
@ -12,6 +12,7 @@ import { match } from 'ts-pattern';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
import { useSignerColors } from '../../lib/signer-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
@ -185,11 +186,35 @@ export const FieldItem = ({
|
||||
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
|
||||
[field.fieldMeta],
|
||||
);
|
||||
|
||||
const radioHasValues = useMemo(
|
||||
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
|
||||
[field.fieldMeta],
|
||||
);
|
||||
|
||||
const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => {
|
||||
if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type === FieldType.RADIO) {
|
||||
const parsed = ZRadioFieldMeta.parse(fieldMeta);
|
||||
return parsed.values?.some((value) => value.checked) ?? false;
|
||||
}
|
||||
|
||||
if (type === FieldType.CHECKBOX) {
|
||||
const parsed = ZCheckboxFieldMeta.parse(fieldMeta);
|
||||
return parsed.values?.some((value) => value.checked) ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const fieldHasCheckedValues = useMemo(
|
||||
() => hasCheckedValues(field.fieldMeta, field.type),
|
||||
[field.fieldMeta, field.type],
|
||||
);
|
||||
|
||||
const fixedSize = checkBoxHasValues || radioHasValues;
|
||||
|
||||
return createPortal(
|
||||
@ -229,6 +254,21 @@ export const FieldItem = ({
|
||||
onMove?.(d.node);
|
||||
}}
|
||||
>
|
||||
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
|
||||
field.fieldMeta?.label && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
|
||||
{
|
||||
'bg-foreground/5 border-primary border': !fieldHasCheckedValues,
|
||||
'bg-documenso-200 border-primary border': fieldHasCheckedValues,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{field.fieldMeta.label}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full w-full items-center justify-center bg-white',
|
||||
|
||||
@ -126,6 +126,18 @@ export const CheckboxFieldAdvancedSettings = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="mb-2">
|
||||
<Label>
|
||||
<Trans>Label</Trans>
|
||||
</Label>
|
||||
<Input
|
||||
id="label"
|
||||
className="bg-background mt-2"
|
||||
placeholder={_(msg`Field label`)}
|
||||
value={fieldState.label}
|
||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-x-4">
|
||||
<div className="flex w-2/3 flex-col">
|
||||
<Label>
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
|
||||
|
||||
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
||||
@ -27,6 +28,8 @@ export const RadioFieldAdvancedSettings = ({
|
||||
handleFieldChange,
|
||||
handleErrors,
|
||||
}: RadioFieldAdvancedSettingsProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [showValidation, setShowValidation] = useState(false);
|
||||
const [values, setValues] = useState(
|
||||
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
|
||||
@ -102,6 +105,18 @@ export const RadioFieldAdvancedSettings = ({
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>Label</Trans>
|
||||
</Label>
|
||||
<Input
|
||||
id="label"
|
||||
className="bg-background mt-2"
|
||||
placeholder={_(msg`Field label`)}
|
||||
value={fieldState.label}
|
||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Switch
|
||||
className="bg-background"
|
||||
|
||||
Reference in New Issue
Block a user