diff --git a/apps/web/prisma/migrations/20260227040924_add_webhook_domain_filters/migration.sql b/apps/web/prisma/migrations/20260227040924_add_webhook_domain_filters/migration.sql new file mode 100644 index 00000000..da6978a3 --- /dev/null +++ b/apps/web/prisma/migrations/20260227040924_add_webhook_domain_filters/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Webhook" ADD COLUMN "domainIds" INTEGER[] DEFAULT ARRAY[]::INTEGER[]; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 58e26f68..1492adf8 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -469,6 +469,7 @@ enum WebhookCallStatus { model Webhook { id String @id @default(cuid()) teamId Int + domainIds Int[] @default([]) url String description String? secret String diff --git a/apps/web/src/app/(dashboard)/campaigns/schedule-campaign.tsx b/apps/web/src/app/(dashboard)/campaigns/schedule-campaign.tsx index f141f0fe..3d34980d 100644 --- a/apps/web/src/app/(dashboard)/campaigns/schedule-campaign.tsx +++ b/apps/web/src/app/(dashboard)/campaigns/schedule-campaign.tsx @@ -35,10 +35,10 @@ export const ScheduleCampaign: React.FC<{ const [scheduleInput, setScheduleInput] = useState( initialScheduledAtDate ? format(initialScheduledAtDate, "yyyy-MM-dd HH:mm") - : "" + : "", ); const [selectedDate, setSelectedDate] = useState( - initialScheduledAtDate ?? new Date() + initialScheduledAtDate ?? new Date(), ); const [isConfirmNow, setIsConfirmNow] = useState(false); const [error, setError] = useState(null); @@ -86,7 +86,7 @@ export const ScheduleCampaign: React.FC<{ onError: (error) => { setError(error.message || "Failed to schedule campaign"); }, - } + }, ); }; diff --git a/apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx b/apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx index a5185d1f..5ab787f7 100644 --- a/apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx +++ b/apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx @@ -1,6 +1,6 @@ "use client"; -import { Webhook, WebhookCallStatus } from "@prisma/client"; +import { WebhookCallStatus, type Webhook } from "@prisma/client"; import { formatDistanceToNow } from "date-fns"; import { Copy, Eye, EyeOff } from "lucide-react"; import { useState } from "react"; @@ -20,6 +20,7 @@ export function WebhookInfo({ webhook }: { webhook: Webhook }) { webhookId: webhook.id, limit: 50, }); + const domainsQuery = api.domain.domains.useQuery(); const calls = callsQuery.data?.items ?? []; const last7DaysCalls = calls.filter( @@ -38,6 +39,13 @@ export function WebhookInfo({ webhook }: { webhook: Webhook }) { c.status === WebhookCallStatus.IN_PROGRESS, ).length; + const domainNameById = new Map( + (domainsQuery.data ?? []).map((domain) => [domain.id, domain.name]), + ); + const selectedDomainLabels = (webhook.domainIds ?? []).map( + (domainId) => domainNameById.get(domainId) ?? `Domain #${domainId}`, + ); + const handleCopySecret = () => { navigator.clipboard.writeText(webhook.secret); toast.success("Secret copied to clipboard"); @@ -66,6 +74,27 @@ export function WebhookInfo({ webhook }: { webhook: Webhook }) { )} +
+ Domains +
+ {(webhook.domainIds ?? []).length === 0 ? ( + All domains + ) : ( + <> + {selectedDomainLabels.slice(0, 2).map((domainName, index) => ( + + {domainName} + + ))} + {(webhook.domainIds ?? []).length > 2 && ( + + +{(webhook.domainIds ?? []).length - 2} more + + )} + + )} +
+
Status
diff --git a/apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx b/apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx index 26dc26de..e2995edd 100644 --- a/apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx +++ b/apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx @@ -50,6 +50,7 @@ const webhookSchema = z.object({ eventTypes: z.array(EVENT_TYPES_ENUM, { required_error: "Select at least one event", }), + domainIds: z.array(z.number().int().positive()), }); type WebhookFormValues = z.infer; @@ -67,6 +68,7 @@ export function AddWebhook() { const [open, setOpen] = useState(false); const [allEventsSelected, setAllEventsSelected] = useState(false); const createWebhookMutation = api.webhook.create.useMutation(); + const domainsQuery = api.domain.domains.useQuery(); const limitsQuery = api.limits.get.useQuery({ type: LimitReason.WEBHOOK }); const { openModal } = useUpgradeModalStore((s) => s.action); @@ -77,6 +79,7 @@ export function AddWebhook() { defaultValues: { url: "", eventTypes: [], + domainIds: [], }, }); @@ -106,6 +109,7 @@ export function AddWebhook() { { url: values.url, eventTypes: allEventsSelected ? [] : selectedEvents, + domainIds: values.domainIds, }, { onSuccess: async () => { @@ -113,6 +117,7 @@ export function AddWebhook() { form.reset({ url: "", eventTypes: [], + domainIds: [], }); setAllEventsSelected(false); setOpen(false); @@ -315,6 +320,85 @@ export function AddWebhook() { ); }} /> + { + const selectedDomainIds = field.value ?? []; + const selectedDomains = + domainsQuery.data?.filter((domain) => + selectedDomainIds.includes(domain.id), + ) ?? []; + + const selectedDomainsLabel = + selectedDomainIds.length === 0 + ? "All domains" + : selectedDomainIds.length === 1 + ? (selectedDomains[0]?.name ?? "1 domain selected") + : `${selectedDomainIds.length} domains selected`; + + const handleToggleDomain = (domainId: number) => { + const exists = selectedDomainIds.includes(domainId); + const next = exists + ? selectedDomainIds.filter((id) => id !== domainId) + : [...selectedDomainIds, domainId]; + field.onChange(next); + }; + + return ( + + Domains + + + + + + +
+ field.onChange([])} + onSelect={(event) => event.preventDefault()} + className="mb-2 px-2 font-medium" + > + All domains + + {domainsQuery.data?.map((domain) => ( + + handleToggleDomain(domain.id) + } + onSelect={(event) => event.preventDefault()} + className="pl-3 pr-2" + > + {domain.name} + + ))} +
+
+
+
+ + Leave this as all domains to receive events from every + domain. + +
+ ); + }} + />
+ + +
+ field.onChange([])} + onSelect={(event) => event.preventDefault()} + className="mb-2 px-2 font-medium" + > + All domains + + {domainsQuery.data?.map((domain) => ( + + handleToggleDomain(domain.id) + } + onSelect={(event) => event.preventDefault()} + className="pl-3 pr-2" + > + {domain.name} + + ))} +
+
+ + + + Leave this as all domains to receive events from every + domain. + + + ); + }} + />