diff --git a/apps/web/src/pages/templates/create.tsx b/apps/web/src/pages/templates/create.tsx
index fa0039a..36bfaa7 100644
--- a/apps/web/src/pages/templates/create.tsx
+++ b/apps/web/src/pages/templates/create.tsx
@@ -9,7 +9,7 @@ import {
Label,
Select,
SelectContent,
- SelectItem,
+ SelectItemWithDescription,
SelectTrigger,
SelectValue,
} from '@plunk/ui';
@@ -74,117 +74,127 @@ export default function CreateTemplatePage() {
return (
<>
diff --git a/packages/db/prisma/migrations/20251205144203_init/migration.sql b/packages/db/prisma/migrations/20251207125935_init/migration.sql
similarity index 99%
rename from packages/db/prisma/migrations/20251205144203_init/migration.sql
rename to packages/db/prisma/migrations/20251207125935_init/migration.sql
index 00797a7..7c537c7 100644
--- a/packages/db/prisma/migrations/20251205144203_init/migration.sql
+++ b/packages/db/prisma/migrations/20251207125935_init/migration.sql
@@ -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,
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index 53c95d1..035d19d 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -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
diff --git a/packages/shared/src/schemas/index.ts b/packages/shared/src/schemas/index.ts
index 371e39a..181b7c9 100644
--- a/packages/shared/src/schemas/index.ts
+++ b/packages/shared/src/schemas/index.ts
@@ -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),
diff --git a/packages/ui/src/components/atoms/Select.tsx b/packages/ui/src/components/atoms/Select.tsx
index 29ac1cc..7234db8 100644
--- a/packages/ui/src/components/atoms/Select.tsx
+++ b/packages/ui/src/components/atoms/Select.tsx
@@ -122,6 +122,37 @@ const SelectItem = React.forwardRef<
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
+interface SelectItemWithDescriptionProps extends React.ComponentPropsWithoutRef