Selectors with description
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import type {Prisma} from '@plunk/db';
|
||||
import {EmailSourceType, EmailStatus} from '@plunk/db';
|
||||
import {EmailSourceType, EmailStatus, TrackingMode} from '@plunk/db';
|
||||
import {createServiceMocks, factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
// Mock MeterService
|
||||
@@ -16,7 +16,7 @@ describe('Email Processor', () => {
|
||||
const _serviceMocks = createServiceMocks();
|
||||
|
||||
beforeEach(async () => {
|
||||
const {project} = await factories.createUserWithProject({}, {trackingEnabled: true});
|
||||
const {project} = await factories.createUserWithProject({}, {tracking: TrackingMode.ENABLED});
|
||||
projectId = project.id;
|
||||
});
|
||||
|
||||
|
||||
@@ -89,6 +89,9 @@ export function createEmailWorker() {
|
||||
? {name: email.toName, email: email.contact.email}
|
||||
: email.contact.email;
|
||||
|
||||
// Determine tracking based on project settings and email type
|
||||
const shouldTrack = EmailService.shouldTrackEmail(email.project.tracking, email.sourceType);
|
||||
|
||||
// Send via AWS SES
|
||||
const result = await sendRawEmail({
|
||||
from: {
|
||||
@@ -101,7 +104,7 @@ export function createEmailWorker() {
|
||||
html: compiledHtml,
|
||||
},
|
||||
reply: email.replyTo || undefined,
|
||||
tracking: email.project.trackingEnabled, // Use project's tracking preference
|
||||
tracking: shouldTrack,
|
||||
attachments: email.attachments as {filename: string; content: string; contentType: string}[] | null,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {Contact, Email, Prisma, Project} from '@plunk/db';
|
||||
import {EmailSourceType, EmailStatus} from '@plunk/db';
|
||||
import {EmailSourceType, EmailStatus, TrackingMode} from '@plunk/db';
|
||||
import signale from 'signale';
|
||||
|
||||
import {DASHBOARD_URI, LANDING_URI, STRIPE_ENABLED} from '../app/constants.js';
|
||||
@@ -365,6 +365,9 @@ export class EmailService {
|
||||
? (email.attachments as Array<{filename: string; content: string; contentType: string}>)
|
||||
: undefined;
|
||||
|
||||
// Determine tracking based on project settings and email type
|
||||
const shouldTrack = this.shouldTrackEmail(email.project.tracking, email.sourceType);
|
||||
|
||||
// Send via AWS SES
|
||||
const result = await sendRawEmail({
|
||||
from: {
|
||||
@@ -379,7 +382,7 @@ export class EmailService {
|
||||
reply: email.replyTo || undefined,
|
||||
headers: customHeaders,
|
||||
attachments: attachments,
|
||||
tracking: email.project.trackingEnabled, // Use project's tracking preference
|
||||
tracking: shouldTrack,
|
||||
});
|
||||
|
||||
// Mark as sent with SES message ID
|
||||
@@ -565,6 +568,23 @@ export class EmailService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an email should be tracked based on project tracking mode and email source type
|
||||
*/
|
||||
public static shouldTrackEmail(trackingMode: TrackingMode, sourceType: EmailSourceType): boolean {
|
||||
switch (trackingMode) {
|
||||
case TrackingMode.ENABLED:
|
||||
return true;
|
||||
case TrackingMode.DISABLED:
|
||||
return false;
|
||||
case TrackingMode.MARKETING_ONLY:
|
||||
// Track only campaigns and workflows (marketing), not transactional emails
|
||||
return sourceType !== EmailSourceType.TRANSACTIONAL;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile HTML email with optional unsubscribe footer and badge
|
||||
* Adds unsubscribe link and Plunk badge for free tier users (only when billing is enabled)
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemWithDescription,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
@@ -346,8 +347,16 @@ export function TeamSettings({projectId, currentUserRole, currentUserId}: TeamSe
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="MEMBER">Member - Can view and use the project</SelectItem>
|
||||
<SelectItem value="ADMIN">Admin - Can manage settings and members</SelectItem>
|
||||
<SelectItemWithDescription
|
||||
value="MEMBER"
|
||||
title="Member"
|
||||
description="Can view and use the project"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value="ADMIN"
|
||||
title="Admin"
|
||||
description="Can manage settings and members"
|
||||
/>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
||||
@@ -2,6 +2,7 @@ import {useEffect, useState} from 'react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {zodResolver} from '@hookform/resolvers/zod';
|
||||
import {ProjectSchemas} from '@plunk/shared';
|
||||
import {TrackingMode} from '@plunk/db';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -24,7 +25,11 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Switch,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItemWithDescription,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
@@ -158,7 +163,7 @@ export default function Settings() {
|
||||
resolver: zodResolver(ProjectSchemas.update),
|
||||
defaultValues: {
|
||||
name: activeProject?.name || '',
|
||||
trackingEnabled: activeProject?.trackingEnabled ?? true,
|
||||
tracking: activeProject?.tracking ?? TrackingMode.ENABLED,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -167,7 +172,7 @@ export default function Settings() {
|
||||
if (activeProject) {
|
||||
form.reset({
|
||||
name: activeProject.name,
|
||||
trackingEnabled: activeProject.trackingEnabled ?? true,
|
||||
tracking: activeProject.tracking ?? TrackingMode.ENABLED,
|
||||
});
|
||||
}
|
||||
}, [activeProject, form]);
|
||||
@@ -395,23 +400,42 @@ export default function Settings() {
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Email Tracking Toggle - only show if feature is available */}
|
||||
{/* Email Tracking Mode - only show if feature is available */}
|
||||
{trackingToggleEnabled && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="trackingEnabled"
|
||||
name="tracking"
|
||||
render={({field}) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border border-neutral-200 p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Email Tracking</FormLabel>
|
||||
<FormDescription>
|
||||
Enable open and click tracking for emails sent from this project. When disabled,
|
||||
emails will be sent without tracking pixels.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormItem>
|
||||
<FormLabel>Email Tracking</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select tracking mode" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItemWithDescription
|
||||
value={TrackingMode.ENABLED}
|
||||
title="Enabled"
|
||||
description="Track opens and clicks for all emails"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value={TrackingMode.DISABLED}
|
||||
title="Disabled"
|
||||
description="No tracking for any emails"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value={TrackingMode.MARKETING_ONLY}
|
||||
title="Marketing Only"
|
||||
description="Track only campaigns and workflow emails, not transactional"
|
||||
/>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Control how email opens and clicks are tracked for this project.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -659,7 +683,11 @@ export default function Settings() {
|
||||
|
||||
{/* Team Tab */}
|
||||
<TabsContent value="team">
|
||||
<TeamSettings projectId={activeProject.id} currentUserRole={currentUserRole} currentUserId={user?.id || ''} />
|
||||
<TeamSettings
|
||||
projectId={activeProject.id}
|
||||
currentUserRole={currentUserRole}
|
||||
currentUserId={user?.id || ''}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Domains Tab */}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemWithDescription,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
StickySaveBar,
|
||||
@@ -232,8 +232,16 @@ export default function TemplateEditorPage() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MARKETING">Marketing</SelectItem>
|
||||
<SelectItem value="TRANSACTIONAL">Transactional</SelectItem>
|
||||
<SelectItemWithDescription
|
||||
value="MARKETING"
|
||||
title="Marketing"
|
||||
description="Includes unsubscribe link, respects opt-out"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value="TRANSACTIONAL"
|
||||
title="Transactional"
|
||||
description="For receipts, alerts - sent regardless of opt-out"
|
||||
/>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-neutral-500 mt-1">
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemWithDescription,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@plunk/ui';
|
||||
@@ -74,117 +74,127 @@ export default function CreateTemplatePage() {
|
||||
return (
|
||||
<>
|
||||
<NextSeo title="Create Template" />
|
||||
<DashboardLayout>
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 sm:gap-4">
|
||||
<Link href="/templates">
|
||||
<Button variant="ghost" size="sm">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<DashboardLayout>
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 sm:gap-4">
|
||||
<Link href="/templates">
|
||||
<Button variant="ghost" size="sm">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Create Template</h1>
|
||||
<p className="text-neutral-500 mt-1 text-sm sm:text-base">
|
||||
Create a reusable email template for campaigns and workflows
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSubmit} disabled={saving} className="w-full sm:w-auto">
|
||||
<Save className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{saving ? 'Creating...' : 'Create Template'}</span>
|
||||
<span className="sm:hidden">{saving ? 'Creating...' : 'Create'}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Create Template</h1>
|
||||
<p className="text-neutral-500 mt-1 text-sm sm:text-base">Create a reusable email template for campaigns and workflows</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSubmit} disabled={saving} className="w-full sm:w-auto">
|
||||
<Save className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{saving ? 'Creating...' : 'Create Template'}</span>
|
||||
<span className="sm:hidden">{saving ? 'Creating...' : 'Create'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Template Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Template Settings</CardTitle>
|
||||
<CardDescription>Configure your template details and email settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Template Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Template Settings</CardTitle>
|
||||
<CardDescription>Configure your template details and email settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Template Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
required
|
||||
placeholder="Welcome Email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="type">Template Type *</Label>
|
||||
<Select value={type} onValueChange={value => setType(value as 'MARKETING' | 'TRANSACTIONAL')}>
|
||||
<SelectTrigger id="type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItemWithDescription
|
||||
value="MARKETING"
|
||||
title="Marketing"
|
||||
description="Includes unsubscribe link, respects opt-out"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value="TRANSACTIONAL"
|
||||
title="Transactional"
|
||||
description="For receipts, alerts - sent regardless of opt-out"
|
||||
/>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="name">Template Name *</Label>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="name"
|
||||
id="description"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
required
|
||||
placeholder="Welcome Email"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Sent to new subscribers"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="type">Template Type *</Label>
|
||||
<Select value={type} onValueChange={value => setType(value as 'MARKETING' | 'TRANSACTIONAL')}>
|
||||
<SelectTrigger id="type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MARKETING">Marketing</SelectItem>
|
||||
<SelectItem value="TRANSACTIONAL">Transactional</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label htmlFor="subject">Subject Line *</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
type="text"
|
||||
value={subject}
|
||||
onChange={e => setSubject(e.target.value)}
|
||||
required
|
||||
placeholder="Welcome to our platform!"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Sent to new subscribers"
|
||||
<EmailSettings
|
||||
from={from}
|
||||
fromName={fromName}
|
||||
replyTo={replyTo}
|
||||
onFromChange={setFrom}
|
||||
onFromNameChange={setFromName}
|
||||
onReplyToChange={setReplyTo}
|
||||
fromNamePlaceholder={activeProject?.name || 'Your Company'}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="subject">Subject Line *</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
type="text"
|
||||
value={subject}
|
||||
onChange={e => setSubject(e.target.value)}
|
||||
required
|
||||
placeholder="Welcome to our platform!"
|
||||
{/* Email Body */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Email Body</CardTitle>
|
||||
<CardDescription>Create your email using the visual editor or paste custom HTML</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EmailEditor
|
||||
value={body}
|
||||
onChange={setBody}
|
||||
placeholder="<h1>Welcome!</h1><p>Thanks for subscribing to our newsletter.</p>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EmailSettings
|
||||
from={from}
|
||||
fromName={fromName}
|
||||
replyTo={replyTo}
|
||||
onFromChange={setFrom}
|
||||
onFromNameChange={setFromName}
|
||||
onReplyToChange={setReplyTo}
|
||||
fromNamePlaceholder={activeProject?.name || 'Your Company'}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Email Body */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Email Body</CardTitle>
|
||||
<CardDescription>Create your email using the visual editor or paste custom HTML</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EmailEditor
|
||||
value={body}
|
||||
onChange={setBody}
|
||||
placeholder="<h1>Welcome!</h1><p>Thanks for subscribing to our newsletter.</p>"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemWithDescription,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Switch
|
||||
@@ -1076,13 +1077,37 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="SEND_EMAIL">Send Email</SelectItem>
|
||||
<SelectItem value="DELAY">Delay - Wait for time</SelectItem>
|
||||
<SelectItem value="WAIT_FOR_EVENT">Wait for Event</SelectItem>
|
||||
<SelectItem value="CONDITION">Condition - If/else branching</SelectItem>
|
||||
<SelectItem value="WEBHOOK">Webhook - Call external API</SelectItem>
|
||||
<SelectItem value="UPDATE_CONTACT">Update Contact</SelectItem>
|
||||
<SelectItem value="EXIT">Exit - End workflow</SelectItem>
|
||||
<SelectItemWithDescription
|
||||
value="SEND_EMAIL"
|
||||
title="Send Email"
|
||||
description="Send an email using a template"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value="DELAY"
|
||||
title="Delay"
|
||||
description="Wait for a specified amount of time"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value="WAIT_FOR_EVENT"
|
||||
title="Wait for Event"
|
||||
description="Pause until a specific event occurs"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value="CONDITION"
|
||||
title="Condition"
|
||||
description="If/else branching based on contact data"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value="WEBHOOK"
|
||||
title="Webhook"
|
||||
description="Call an external API endpoint"
|
||||
/>
|
||||
<SelectItemWithDescription
|
||||
value="UPDATE_CONTACT"
|
||||
title="Update Contact"
|
||||
description="Modify contact data fields"
|
||||
/>
|
||||
<SelectItemWithDescription value="EXIT" title="Exit" description="End the workflow for this contact" />
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-neutral-500 mt-1">
|
||||
|
||||
+4
-1
@@ -7,6 +7,9 @@ CREATE TYPE "Role" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TemplateType" AS ENUM ('TRANSACTIONAL', 'MARKETING');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TrackingMode" AS ENUM ('ENABLED', 'DISABLED', 'MARKETING_ONLY');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CampaignStatus" AS ENUM ('DRAFT', 'SCHEDULED', 'SENDING', 'SENT', 'CANCELLED');
|
||||
|
||||
@@ -55,7 +58,7 @@ CREATE TABLE "projects" (
|
||||
"billingLimitWorkflows" INTEGER,
|
||||
"billingLimitCampaigns" INTEGER,
|
||||
"billingLimitTransactional" INTEGER,
|
||||
"trackingEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"tracking" "TrackingMode" NOT NULL DEFAULT 'ENABLED',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
@@ -54,7 +54,7 @@ model Project {
|
||||
billingLimitTransactional Int? // Max transactional emails per month
|
||||
|
||||
// Email Tracking
|
||||
trackingEnabled Boolean @default(true) // Enable/disable open and click tracking
|
||||
tracking TrackingMode @default(ENABLED) // Open and click tracking mode
|
||||
|
||||
// Relations
|
||||
members Membership[]
|
||||
@@ -636,6 +636,12 @@ enum TemplateType {
|
||||
MARKETING
|
||||
}
|
||||
|
||||
enum TrackingMode {
|
||||
ENABLED // Track opens and clicks for all emails
|
||||
DISABLED // No tracking for any emails
|
||||
MARKETING_ONLY // Track only marketing/campaign emails, not transactional
|
||||
}
|
||||
|
||||
enum CampaignStatus {
|
||||
DRAFT
|
||||
SCHEDULED
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {CampaignAudienceType, TemplateType, WorkflowStepType, WorkflowTriggerType} from '@plunk/db';
|
||||
import {CampaignAudienceType, TemplateType, TrackingMode, WorkflowStepType, WorkflowTriggerType} from '@plunk/db';
|
||||
import {z} from 'zod';
|
||||
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null(), z.date()]);
|
||||
@@ -59,7 +59,7 @@ export const ProjectSchemas = {
|
||||
}),
|
||||
update: z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
trackingEnabled: z.boolean().optional(),
|
||||
tracking: z.nativeEnum(TrackingMode).optional(),
|
||||
}),
|
||||
} as const;
|
||||
|
||||
@@ -223,11 +223,7 @@ export const WorkflowStepConfigSchemas = {
|
||||
),
|
||||
waitForEvent: z.object({
|
||||
eventName: z.string().min(1),
|
||||
timeout: z
|
||||
.number()
|
||||
.positive()
|
||||
.max(31536000, 'Timeout cannot exceed 365 days (31,536,000 seconds)')
|
||||
.optional(),
|
||||
timeout: z.number().positive().max(31536000, 'Timeout cannot exceed 365 days (31,536,000 seconds)').optional(),
|
||||
}),
|
||||
condition: z.object({
|
||||
field: z.string().min(1),
|
||||
|
||||
@@ -122,6 +122,37 @@ const SelectItem = React.forwardRef<
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
interface SelectItemWithDescriptionProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const SelectItemWithDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
SelectItemWithDescriptionProps
|
||||
>(({className, title, description, ...props}, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default select-none items-start rounded-sm py-2.5 pl-8 pr-3 text-sm outline-none focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 top-3 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<SelectPrimitive.ItemText className="font-medium">{title}</SelectPrimitive.ItemText>
|
||||
{description && <span className="text-xs text-neutral-500 leading-tight">{description}</span>}
|
||||
</div>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItemWithDescription.displayName = 'SelectItemWithDescription';
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
@@ -138,6 +169,7 @@ export {
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectItemWithDescription,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
PrismaClient,
|
||||
Role,
|
||||
TemplateType,
|
||||
TrackingMode,
|
||||
WorkflowExecutionStatus,
|
||||
WorkflowStepType,
|
||||
WorkflowTriggerType
|
||||
@@ -34,7 +35,7 @@ export interface UserFactoryOptions {
|
||||
export interface ProjectFactoryOptions {
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
trackingEnabled?: boolean;
|
||||
tracking?: TrackingMode;
|
||||
billingLimitWorkflows?: number | null;
|
||||
billingLimitCampaigns?: number | null;
|
||||
billingLimitTransactional?: number | null;
|
||||
@@ -127,7 +128,7 @@ export class TestFactories {
|
||||
public: `pk_${uniqueId()}`,
|
||||
secret: `sk_${uniqueId()}`,
|
||||
disabled: options.disabled || false,
|
||||
trackingEnabled: options.trackingEnabled ?? true,
|
||||
tracking: options.tracking ?? TrackingMode.ENABLED,
|
||||
billingLimitWorkflows: options.billingLimitWorkflows,
|
||||
billingLimitCampaigns: options.billingLimitCampaigns,
|
||||
billingLimitTransactional: options.billingLimitTransactional,
|
||||
|
||||
Reference in New Issue
Block a user