Skip to content

Commit 70312a8

Browse files
authored
Merge pull request #96 from dafthunk-com/89-rework-deployment-http-integration-card
89 rework deployment http integration card
2 parents 12a9469 + 602961e commit 70312a8

16 files changed

Lines changed: 663 additions & 668 deletions

apps/api/src/db/migrations/meta/0005_snapshot.json

Lines changed: 74 additions & 227 deletions
Large diffs are not rendered by default.

apps/api/src/db/migrations/meta/_journal.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,4 @@
4545
"breakpoints": true
4646
}
4747
]
48-
}
48+
}

apps/api/src/db/queries.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,14 +1202,9 @@ export async function getDueCronTriggers(
12021202
selectedDeployment: selectedDeployment,
12031203
})
12041204
.from(cronTriggers)
1205-
.where(
1206-
and(eq(cronTriggers.active, true), lte(cronTriggers.nextRunAt, now))
1207-
)
1205+
.where(and(eq(cronTriggers.active, true), lte(cronTriggers.nextRunAt, now)))
12081206
.innerJoin(workflows, eq(workflows.id, cronTriggers.workflowId))
1209-
.innerJoin(
1210-
latestByWorkflow,
1211-
eq(latestByWorkflow.workflowId, workflows.id)
1212-
)
1207+
.innerJoin(latestByWorkflow, eq(latestByWorkflow.workflowId, workflows.id))
12131208
.innerJoin(
12141209
latestDeployment,
12151210
and(

apps/api/src/routes/workflows.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
createDatabase,
2929
createHandle,
3030
createWorkflow,
31-
type CronTriggerInsert,
3231
deleteWorkflow,
3332
ExecutionStatus,
3433
getCronTrigger,

apps/web/src/components/deployments/email-integration-card.tsx

Lines changed: 0 additions & 75 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React from "react";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Tooltip,
6+
TooltipContent,
7+
TooltipTrigger,
8+
} from "@/components/ui/tooltip";
9+
import { cn } from "@/utils/utils";
10+
11+
export interface ActionBarGroupProps {
12+
children: React.ReactNode;
13+
vertical?: boolean;
14+
className?: string;
15+
}
16+
17+
export function ActionBarGroup({
18+
children,
19+
vertical = false,
20+
className = "",
21+
}: ActionBarGroupProps) {
22+
const horizontalClasses =
23+
"flex items-center gap-0.5 [&>*:first-child]:rounded-l-lg [&>*:first-child]:rounded-r-none [&>*:last-child]:rounded-r-lg [&>*:last-child]:rounded-l-none [&>*:only-child]:rounded-lg";
24+
const verticalClasses =
25+
"flex flex-col items-center gap-0.5 [&>*:first-child]:rounded-t-lg [&>*:first-child]:rounded-b-none [&>*:last-child]:rounded-b-lg [&>*:last-child]:rounded-t-none [&>*:only-child]:rounded-lg";
26+
27+
return (
28+
<div
29+
className={cn(vertical ? verticalClasses : horizontalClasses, className)}
30+
>
31+
{children}
32+
</div>
33+
);
34+
}
35+
36+
export interface ActionBarButtonProps {
37+
onClick: (e: React.MouseEvent) => void;
38+
disabled?: boolean;
39+
className?: string;
40+
tooltip?: React.ReactNode;
41+
children: React.ReactNode;
42+
tooltipSide?: "top" | "bottom" | "left" | "right";
43+
}
44+
45+
export function ActionBarButton({
46+
onClick,
47+
disabled = false,
48+
className = "",
49+
tooltip,
50+
children,
51+
tooltipSide = "top",
52+
}: ActionBarButtonProps) {
53+
const trigger = (
54+
<Button
55+
onClick={onClick}
56+
disabled={disabled}
57+
className={cn("h-10 px-3 rounded-none", className, {
58+
"opacity-50 cursor-not-allowed": disabled,
59+
})}
60+
>
61+
{children}
62+
</Button>
63+
);
64+
if (!tooltip) {
65+
return trigger;
66+
}
67+
return (
68+
<Tooltip delayDuration={0}>
69+
<div className="bg-background rounded-none overflow-hidden">
70+
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
71+
<TooltipContent side={tooltipSide}>{tooltip}</TooltipContent>
72+
</div>
73+
</Tooltip>
74+
);
75+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Mail } from "lucide-react";
2+
import { toast } from "sonner";
3+
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogDescription,
9+
DialogHeader,
10+
DialogTitle,
11+
} from "@/components/ui/dialog";
12+
import { Input } from "@/components/ui/input";
13+
import { Label } from "@/components/ui/label";
14+
15+
interface EmailTriggerDialogProps {
16+
isOpen: boolean;
17+
onClose: (open: boolean) => void;
18+
orgHandle: string;
19+
workflowHandle: string;
20+
deploymentVersion?: string;
21+
emailDomain?: string;
22+
}
23+
24+
export function EmailTriggerDialog({
25+
isOpen,
26+
onClose,
27+
orgHandle,
28+
workflowHandle,
29+
deploymentVersion,
30+
emailDomain = "dafthunk.com",
31+
}: EmailTriggerDialogProps) {
32+
let emailAddress = `workflow+${orgHandle}+${workflowHandle}`;
33+
if (deploymentVersion && deploymentVersion !== "latest") {
34+
emailAddress += `+${deploymentVersion}`;
35+
}
36+
emailAddress += `@${emailDomain}`;
37+
38+
const handleCopyEmail = () => {
39+
navigator.clipboard.writeText(emailAddress);
40+
toast.success("Email address copied to clipboard");
41+
};
42+
43+
const description = deploymentVersion
44+
? `Send emails to this address to trigger version ${deploymentVersion} of the workflow.`
45+
: "Send emails to this address to trigger the workflow. This will always trigger the latest deployed version of the workflow.";
46+
47+
return (
48+
<Dialog open={isOpen} onOpenChange={onClose}>
49+
<DialogContent className="max-w-2xl">
50+
<DialogHeader>
51+
<DialogTitle className="text-xl flex items-center gap-2">
52+
<Mail className="h-5 w-5" />
53+
Email Trigger
54+
</DialogTitle>
55+
<DialogDescription>{description}</DialogDescription>
56+
</DialogHeader>
57+
58+
<div className="space-y-4">
59+
<div className="space-y-2">
60+
<Label htmlFor="email-address">Workflow Email Address</Label>
61+
<div className="flex items-center gap-2">
62+
<Input
63+
id="email-address"
64+
value={emailAddress}
65+
readOnly
66+
className="font-mono h-9"
67+
/>
68+
<Button variant="outline" onClick={handleCopyEmail}>
69+
Copy
70+
</Button>
71+
</div>
72+
</div>
73+
74+
<div className="space-y-4">
75+
<div>
76+
<h4 className="text-sm font-medium mb-2">How it works:</h4>
77+
<ul className="list-disc pl-5 space-y-1 text-sm text-muted-foreground">
78+
<li>
79+
The email subject will be used as the 'subject' input for the
80+
workflow
81+
</li>
82+
<li>
83+
The email body (text version) will be used as the 'body' input
84+
for the workflow
85+
</li>
86+
<li>
87+
Your workflow must be configured to accept these inputs for
88+
the integration to work properly
89+
</li>
90+
<li>
91+
Only the text version of the email body is processed; HTML
92+
content is ignored
93+
</li>
94+
</ul>
95+
</div>
96+
97+
<div>
98+
<h4 className="text-sm font-medium mb-2">Example usage:</h4>
99+
<div className="bg-muted p-3 rounded-md text-sm font-mono">
100+
To: {emailAddress}
101+
<br />
102+
Subject: Process my document
103+
<br />
104+
Body: Please analyze the attached data and provide insights.
105+
</div>
106+
</div>
107+
</div>
108+
</div>
109+
</DialogContent>
110+
</Dialog>
111+
);
112+
}

apps/web/src/components/workflow/execution-email-dialog.tsx

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ import {
1515
import { Button } from "@/components/ui/button";
1616
import { Input } from "@/components/ui/input";
1717
import { Label } from "@/components/ui/label";
18-
import { Textarea } from "@/components/ui/textarea"; // For body field
18+
import { Textarea } from "@/components/ui/textarea";
1919

20-
// Define the Zod schema as the primary source of truth for the form's shape
21-
const emailDialogZodSchema = z.object({
20+
const executionEmailDialogZodSchema = z.object({
2221
from: z
2322
.string()
2423
.email({ message: "Invalid email address for From field." })
@@ -27,45 +26,38 @@ const emailDialogZodSchema = z.object({
2726
body: z.string().min(1, { message: "Body is required." }),
2827
});
2928

30-
// Infer the TypeScript type from the Zod schema. This will be used by the form.
31-
type EmailDialogFormShape = z.infer<typeof emailDialogZodSchema>;
29+
type ExecutionEmailDialogFormShape = z.infer<
30+
typeof executionEmailDialogZodSchema
31+
>;
3232

33-
// The EmailData interface is exported for use by parent components/services.
34-
// It must be structurally identical to EmailDialogFormShape.
3533
export interface EmailData {
3634
from: string;
3735
subject: string;
3836
body: string;
3937
}
4038

41-
// Helper type assertion (optional, for development, can be removed)
42-
// This ensures EmailData and EmailDialogFormShape are compatible.
43-
type Assert<T, U extends T> = U;
44-
type _SchemaMatchesData = Assert<EmailData, EmailDialogFormShape>;
45-
type _DataMatchesSchema = Assert<EmailDialogFormShape, EmailData>;
46-
47-
type EmailDialogProps = {
39+
type ExecutionEmailDialogProps = {
4840
isOpen: boolean;
4941
onClose: () => void;
5042
// The onSubmit prop uses EmailData, which must align with EmailDialogFormShape
5143
onSubmit: (formData: EmailData) => void;
5244
onCancel?: () => void;
5345
};
5446

55-
export function EmailDialog({
47+
export function ExecutionEmailDialog({
5648
isOpen,
5749
onClose,
5850
onSubmit,
5951
onCancel,
60-
}: EmailDialogProps) {
52+
}: ExecutionEmailDialogProps) {
6153
const {
6254
control,
6355
handleSubmit,
6456
reset,
6557
formState: { errors, isDirty, isValid },
66-
} = useForm<EmailDialogFormShape>({
67-
// Use the inferred type from the Zod schema
68-
resolver: zodResolver(emailDialogZodSchema), // Use the Zod schema directly
58+
} = useForm<ExecutionEmailDialogFormShape>({
59+
// @ts-expect-error - zodResolver is not typed correctly
60+
resolver: zodResolver(executionEmailDialogZodSchema),
6961
mode: "onChange",
7062
defaultValues: {
7163
from: "",
@@ -85,7 +77,9 @@ export function EmailDialog({
8577
}, [isOpen, reset]);
8678

8779
// processSubmit now works with EmailDialogFormShape
88-
const processSubmit: SubmitHandler<EmailDialogFormShape> = (data) => {
80+
const processSubmit: SubmitHandler<ExecutionEmailDialogFormShape> = (
81+
data
82+
) => {
8983
// onSubmit expects EmailData. Since EmailDialogFormShape and EmailData are structurally identical,
9084
// this assignment is safe.
9185
onSubmit(data);

0 commit comments

Comments
 (0)