Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 138 additions & 2 deletions src/components/settings/WebhooksSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { useState } from 'react'
import {
Globe, MessageSquare, Send, Hash, Plus, Trash2, Power, PowerOff,
CheckCircle2, XCircle, ChevronDown, ChevronUp, Loader2, Zap, CheckSquare, Pencil, Columns3, BookOpen,
CheckCircle2, XCircle, ChevronDown, ChevronUp, Loader2, Zap, CheckSquare, Pencil, Columns3, BookOpen, Mail, Server,
} from 'lucide-react'
import { useWorkspace } from '@/hooks/useWorkspace'
import { useWebhooks, useCreateWebhook, useUpdateWebhook, useDeleteWebhook, useWebhookLogs } from '@/hooks/useWebhooks'
Expand All @@ -23,7 +23,7 @@ function GitHubIcon({ className }: { className?: string }) {
)
}

type DestinationType = 'http' | 'slack' | 'discord' | 'telegram' | 'teams' | 'asana' | 'trello' | 'notion' | 'github'
type DestinationType = 'http' | 'slack' | 'discord' | 'telegram' | 'teams' | 'asana' | 'trello' | 'notion' | 'github' | 'email' | 'smtp'

const DESTINATION_META: Record<DestinationType, { label: string; icon: typeof Globe; color: string; bgColor: string }> = {
http: { label: 'HTTP Webhook', icon: Globe, color: 'text-blue-400', bgColor: 'bg-blue-500/10' },
Expand All @@ -35,6 +35,8 @@ const DESTINATION_META: Record<DestinationType, { label: string; icon: typeof Gl
trello: { label: 'Trello', icon: Columns3, color: 'text-teal-400', bgColor: 'bg-teal-500/10' },
notion: { label: 'Notion', icon: BookOpen, color: 'text-gray-300', bgColor: 'bg-gray-500/10' },
github: { label: 'GitHub Issues', icon: GitHubIcon as unknown as typeof Globe, color: 'text-gray-200', bgColor: 'bg-gray-600/10' },
email: { label: 'Email (Resend)', icon: Mail, color: 'text-amber-400', bgColor: 'bg-amber-500/10' },
smtp: { label: 'SMTP Email', icon: Server, color: 'text-orange-400', bgColor: 'bg-orange-500/10' },
}

const EVENT_OPTIONS = [
Expand Down Expand Up @@ -597,6 +599,140 @@ function DestinationConfigFields({
</div>
</div>
)

case 'email':
return (
<div className="space-y-3">
<div>
<label className="mb-1 block text-sm font-medium text-gray-400">To Email(s)</label>
<input
type="text"
value={config.to ?? ''}
onChange={(e) => updateField('to', e.target.value)}
placeholder="team@company.com, alerts@company.com"
className={inputClass}
/>
<p className="mt-1 text-xs text-gray-600">
Comma-separated list of recipient email addresses.
</p>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-400">
Subject Template <span className="text-gray-600">(optional)</span>
</label>
<input
type="text"
value={config.subject ?? ''}
onChange={(e) => updateField('subject', e.target.value)}
placeholder="{{status}} {{title}}"
className={inputClass}
/>
<p className="mt-1 text-xs text-gray-600">
Use <code className="text-amber-400/70">{'{{title}}'}</code>, <code className="text-amber-400/70">{'{{status}}'}</code>, <code className="text-amber-400/70">{'{{agent}}'}</code> as placeholders.
Uses your <strong>RESEND_API_KEY</strong> env var.
</p>
</div>
</div>
)

case 'smtp':
return (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<div className="col-span-2">
<label className="mb-1 block text-sm font-medium text-gray-400">SMTP Host</label>
<input
type="text"
value={config.host ?? ''}
onChange={(e) => updateField('host', e.target.value)}
placeholder="smtp.gmail.com"
className={inputClass}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-400">Port</label>
<input
type="text"
value={config.port ?? '587'}
onChange={(e) => updateField('port', e.target.value)}
placeholder="587"
className={inputClass}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-sm font-medium text-gray-400">
Username <span className="text-gray-600">(optional)</span>
</label>
<input
type="text"
value={config.user ?? ''}
onChange={(e) => updateField('user', e.target.value)}
placeholder="user@gmail.com"
className={inputClass}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-400">
Password <span className="text-gray-600">(optional)</span>
</label>
<input
type="password"
value={config.pass ?? ''}
onChange={(e) => updateField('pass', e.target.value)}
placeholder="App password"
className={inputClass}
/>
</div>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-400">From Address</label>
<input
type="text"
value={config.from ?? ''}
onChange={(e) => updateField('from', e.target.value)}
placeholder='CrewForm <noreply@example.com>'
className={inputClass}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-400">To Email(s)</label>
<input
type="text"
value={config.to ?? ''}
onChange={(e) => updateField('to', e.target.value)}
placeholder="team@company.com, alerts@company.com"
className={inputClass}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-400">
Subject Template <span className="text-gray-600">(optional)</span>
</label>
<input
type="text"
value={config.subject ?? ''}
onChange={(e) => updateField('subject', e.target.value)}
placeholder="{{status}} {{title}}"
className={inputClass}
/>
<p className="mt-1 text-xs text-gray-600">
Use <code className="text-orange-400/70">{'{{title}}'}</code>, <code className="text-orange-400/70">{'{{status}}'}</code>, <code className="text-orange-400/70">{'{{agent}}'}</code> as placeholders.
</p>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="smtp-tls"
checked={(config.tls ?? 'true') !== 'false'}
onChange={(e) => updateField('tls', e.target.checked ? 'true' : 'false')}
className="h-4 w-4 rounded border-border bg-surface-raised text-brand-primary focus:ring-brand-primary"
/>
<label htmlFor="smtp-tls" className="text-sm text-gray-400">Use TLS</label>
</div>
</div>
)
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/components/shared/ChannelSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 CrewForm

import { Globe, MessageSquare, Hash, Send, Users, CheckSquare, Radio, Columns3, BookOpen } from 'lucide-react'
import { Globe, MessageSquare, Hash, Send, Users, CheckSquare, Radio, Columns3, BookOpen, Mail, Server } from 'lucide-react'
import { useOutputRoutes } from '@/hooks/useChannels'
import { cn } from '@/lib/utils'

Expand All @@ -24,6 +24,8 @@ const DESTINATION_ICONS: Record<string, typeof Globe> = {
trello: Columns3,
notion: BookOpen,
github: GitHubIcon as unknown as typeof Globe,
email: Mail,
smtp: Server,
}

const DESTINATION_COLORS: Record<string, { text: string; bg: string }> = {
Expand All @@ -36,6 +38,8 @@ const DESTINATION_COLORS: Record<string, { text: string; bg: string }> = {
trello: { text: 'text-teal-400', bg: 'bg-teal-500/10' },
notion: { text: 'text-gray-300', bg: 'bg-gray-400/10' },
github: { text: 'text-gray-200', bg: 'bg-gray-600/10' },
email: { text: 'text-amber-400', bg: 'bg-amber-500/10' },
smtp: { text: 'text-orange-400', bg: 'bg-orange-500/10' },
}

interface OutputRouteSelectorProps {
Expand Down
4 changes: 2 additions & 2 deletions src/db/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface OutputRoute {
id: string
workspace_id: string
name: string
destination_type: 'http' | 'slack' | 'discord' | 'telegram' | 'teams' | 'asana' | 'trello' | 'notion' | 'github'
destination_type: 'http' | 'slack' | 'discord' | 'telegram' | 'teams' | 'asana' | 'trello' | 'notion' | 'github' | 'email' | 'smtp'
config: Record<string, unknown>
events: string[]
is_active: boolean
Expand All @@ -34,7 +34,7 @@ export interface WebhookLog {
export interface CreateRouteInput {
workspace_id: string
name: string
destination_type: 'http' | 'slack' | 'discord' | 'telegram' | 'teams' | 'asana' | 'trello' | 'notion' | 'github'
destination_type: 'http' | 'slack' | 'discord' | 'telegram' | 'teams' | 'asana' | 'trello' | 'notion' | 'github' | 'email' | 'smtp'
config: Record<string, unknown>
events: string[]
is_active?: boolean
Expand Down
21 changes: 21 additions & 0 deletions task-runner/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions task-runner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@
"@supabase/supabase-js": "^2.97.0",
"dotenv": "^17.3.1",
"langfuse": "^3.38.20",
"nodemailer": "^8.0.7",
"openai": "^6.25.0",
"resend": "^6.9.3",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^25.3.0",
"@types/nodemailer": "^8.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
Expand Down
Loading
Loading