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
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ export function ActionSummaryCard({

case ActionType.CALL_WEBHOOK:
summaryContent = `Call webhook: ${action.url?.value || "unset"}`;
tooltipText =
"Sends email details and rule execution data to your webhook endpoint when this rule is triggered.";
break;

case ActionType.TRACK_THREAD:
Expand Down
8 changes: 8 additions & 0 deletions apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import { FolderSelector } from "@/components/FolderSelector";
import { useFolders } from "@/hooks/useFolders";
import type { OutlookFolder } from "@/utils/outlook/folders";
import { cn } from "@/utils";
import { WebhookDocumentationLink } from "@/components/WebhookDocumentation";

export function Rule({
ruleId,
Expand Down Expand Up @@ -1271,7 +1272,14 @@ function ActionCard({
registerProps={register(
`actions.${index}.${field.name}.value`,
)}
placeholder={field.placeholder}
/>
{field.name === "url" &&
action.type === ActionType.CALL_WEBHOOK && (
<div className="mt-2">
<WebhookDocumentationLink />
</div>
)}
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";

export function RulesTab() {
return (
<div>
<div className="mt-8">
<RulesPrompt />

<Tabs defaultValue="list" searchParam="format">
Expand Down
12 changes: 7 additions & 5 deletions apps/web/app/(app)/[emailAccountId]/settings/WebhookSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function WebhookSection() {
<FormSection>
<FormSectionLeft
title="Webhooks (Developers)"
description="API webhook secret for request verification. Include this in the X-Webhook-Secret header when setting up webhook endpoints."
description="API webhook secret for request verification. Include this in the X-Webhook-Secret header when setting up webhook endpoints. Set webhook URLs for individual rules in the Assistant=>Rules tab."
/>

<div className="col-span-2">
Expand All @@ -26,10 +26,12 @@ export function WebhookSection() {
<CopyInput value={data.webhookSecret} />
)}

<RegenerateSecretButton
hasSecret={!!data.webhookSecret}
mutate={mutate}
/>
<div className="flex items-center gap-3">
<RegenerateSecretButton
hasSecret={!!data.webhookSecret}
mutate={mutate}
/>
</div>
</div>
)}
</LoadingContent>
Expand Down
164 changes: 164 additions & 0 deletions apps/web/components/WebhookDocumentation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"use client";

import { useState } from "react";
import { Copy, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";

export function WebhookDocumentationDialog({
children,
}: {
children: React.ReactNode;
}) {
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Webhook Payload</DialogTitle>
</DialogHeader>
<WebhookPayloadDocumentation />
</DialogContent>
</Dialog>
);
}

export function WebhookPayloadDocumentation() {
const [copied, setCopied] = useState(false);

const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
Comment on lines +35 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove console usage; add clipboard fallback + user feedback

Project guideline forbids console usage. Add a graceful fallback when navigator.clipboard is unavailable/blocked and surface an error toast.

Apply this diff:

 import { useState } from "react";
 import { Copy, Check } from "lucide-react";
 import { Button } from "@/components/ui/button";
+import { toastError } from "@/components/Toast";
@@
   const copyToClipboard = async (text: string) => {
     try {
       await navigator.clipboard.writeText(text);
       setCopied(true);
       setTimeout(() => setCopied(false), 2000);
-    } catch (err) {
-      console.error("Failed to copy text: ", err);
+    } catch {
+      // Fallback for insecure contexts or denied permissions
+      try {
+        const el = document.createElement("textarea");
+        el.value = text;
+        el.setAttribute("readonly", "");
+        el.style.position = "absolute";
+        el.style.left = "-9999px";
+        document.body.appendChild(el);
+        el.select();
+        document.execCommand("copy");
+        document.body.removeChild(el);
+        setCopied(true);
+        setTimeout(() => setCopied(false), 2000);
+      } catch {
+        toastError({ description: "Failed to copy to clipboard." });
+      }
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
import { useState } from "react";
import { Copy, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toastError } from "@/components/Toast";
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback for insecure contexts or denied permissions
try {
const el = document.createElement("textarea");
el.value = text;
el.setAttribute("readonly", "");
el.style.position = "absolute";
el.style.left = "-9999px";
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
toastError({ description: "Failed to copy to clipboard." });
}
}
};
🤖 Prompt for AI Agents
In apps/web/components/WebhookDocumentation.tsx around lines 35 to 43, remove
the console.error usage and implement a graceful clipboard fallback and
user-facing error toast: first try navigator.clipboard.writeText if available,
otherwise copy by creating a hidden textarea, selecting its content and using
document.execCommand('copy'); on success setCopied(true) and clear it after 2s
as before; on any failure call the project's toast/error notification API (do
not use console) with a clear message like "Failed to copy to clipboard" and
include optional error detail, and ensure any created DOM nodes are cleaned up
and proper permission errors are handled.


const payloadExample = {
email: {
threadId: "thread_abc123",
messageId: "message_xyz789",
subject: "Important Contract Document",
from: "client@company.com",
cc: "team@company.com",
bcc: "archive@company.com",
headerMessageId: "<CAF=4sK9...@mail.gmail.com>",
},
executedRule: {
id: "exec_rule_123",
ruleId: "rule_456",
reason: "Email matched rule: Archive contracts",
automated: true,
createdAt: "2024-01-15T10:30:00.000Z",
},
};

const payloadJson = JSON.stringify(payloadExample, null, 2);

return (
<div className="space-y-4">
<div className="text-sm text-muted-foreground">
When a rule with a webhook action is triggered, we'll send a POST
request to your URL with the following payload:
</div>

<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-medium">Webhook Payload Structure</h4>
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(payloadJson)}
>
{copied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>

<pre className="bg-muted p-4 rounded-md text-sm overflow-x-auto">
<code>{payloadJson}</code>
</pre>

<div className="grid gap-4 md:grid-cols-2">
<div>
<h5 className="font-medium mb-2">Email Fields</h5>
<div className="space-y-1 text-sm text-muted-foreground">
<div>
<code>threadId</code> - Gmail/Outlook thread ID
</div>
<div>
<code>messageId</code> - Unique message ID
</div>
<div>
<code>subject</code> - Email subject line
</div>
<div>
<code>from</code> - Sender's email address
</div>
<div>
<code>cc/bcc</code> - Optional CC/BCC recipients
</div>
<div>
<code>headerMessageId</code> - Email Message-ID header
</div>
</div>
</div>

<div>
<h5 className="font-medium mb-2">Rule Execution Fields</h5>
<div className="space-y-1 text-sm text-muted-foreground">
<div>
<code>id</code> - Execution ID
</div>
<div>
<code>ruleId</code> - Rule that was triggered
</div>
<div>
<code>reason</code> - Why the rule was triggered
</div>
<div>
<code>automated</code> - Whether rule ran automatically
</div>
<div>
<code>createdAt</code> - When the rule was executed (ISO 8601)
</div>
</div>
</div>
</div>

<div className="p-3 bg-blue-50 dark:bg-blue-950/30 rounded-md">
<div className="text-sm text-blue-600 dark:text-blue-400">
<strong>Authentication:</strong> Each request includes an{" "}
<code>X-Webhook-Secret</code> header with your webhook secret for
verification.
</div>
</div>
</div>
</div>
);
}

export function WebhookDocumentationLink() {
return (
<WebhookDocumentationDialog>
<Button
variant="link"
size="xs"
className="h-auto p-0 text-xs text-blue-600 hover:text-blue-800"
>
View payload structure
</Button>
</WebhookDocumentationDialog>
);
}
2 changes: 2 additions & 0 deletions apps/web/utils/action-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const actionInputs: Record<
label: string;
textArea?: boolean;
expandable?: boolean;
placeholder?: string;
}[];
Comment on lines +25 to 26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify placeholder is actually rendered and URL is validated.
Ensure RuleForm passes field.placeholder to the Input and Zod validates https URLs for CALL_WEBHOOK.


🏁 Script executed:

#!/bin/bash
# 1) Confirm placeholder is wired into RuleForm inputs
rg -nP -C3 'placeholder\s*=' apps/web/app/\(app\)/\[emailAccountId]/assistant/RuleForm.tsx

# 2) Locate rule.validation and check CALL_WEBHOOK url uses z.string().url()
fd -t f 'rule.validation.*\.ts' apps/web | xargs -I {} rg -nP -C3 'CALL_WEBHOOK|url\s*:\s*z\.string\(\)\.url\(\)' {}

Length of output: 2072


Enforce URL format in Zod schema for CALL_WEBHOOK
Placeholder wiring in RuleForm is correct. In apps/web/utils/rule.validation.ts, the url field for ActionType.CALL_WEBHOOK only checks !data.url?.value?.trim(); replace this manual check with a Zod constraint such as z.string().url() (or restrict to HTTPS) to validate URL format.

🤖 Prompt for AI Agents
In apps/web/utils/action-item.ts around lines 25 to 26, the current handling
leaves URL format validation to a manual check elsewhere; replace that with a
Zod constraint on the CALL_WEBHOOK url field by using z.string().url() (or
z.string().regex(/^https:\/\//) if you want to enforce HTTPS only), ensuring the
schema requires a non-empty string and validates format, update any optional
chaining to match the schema (make the field required if needed) and provide a
clear Zod error message for invalid URLs so the manual !data.url?.value?.trim()
check can be removed.

}
> = {
Expand Down Expand Up @@ -139,6 +140,7 @@ export const actionInputs: Record<
{
name: "url",
label: "URL",
placeholder: "https://example.com/webhook",
},
],
},
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.6.12
v2.6.13
Loading