Skip to content

Commit

Permalink
feat: implement email receivers in workflow-triggers page (#1148)
Browse files Browse the repository at this point in the history
* feat: implement email receiver api service

- initialize types, api routes, and service methods for email receivers

Signed-off-by: Ryan Hopper-Lowe <[email protected]>

* feat: implement email receivers in workflow triggers UI

---------

Signed-off-by: Ryan Hopper-Lowe <[email protected]>
  • Loading branch information
ryanhopperlowe authored Jan 8, 2025
1 parent 200e057 commit dc18360
Show file tree
Hide file tree
Showing 12 changed files with 556 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ export function CreateWorkflowTrigger() {
Set up a workflow to run on an interval.
</TooltipContent>
</Tooltip>

<Tooltip>
<TooltipTrigger asChild>
<Link
to={$path("/workflow-triggers/email/create")}
as="button"
variant="outline"
>
Email
</Link>
</TooltipTrigger>
</Tooltip>
</div>
</DialogContent>
</Dialog>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { toast } from "sonner";
import { mutate } from "swr";

import { WorkflowTriggerType } from "~/lib/model/workflow-trigger";
import { CronJobApiService } from "~/lib/service/api/cronjobApiService";
import { EmailReceiverApiService } from "~/lib/service/api/emailReceiverApiService";
import { WebhookApiService } from "~/lib/service/api/webhookApiService";

import { ConfirmationDialog } from "~/components/composed/ConfirmationDialog";
Expand All @@ -16,7 +18,7 @@ export function DeleteWorkflowTrigger({
}: {
id: string;
name?: string;
type: "webhook" | "schedule";
type: WorkflowTriggerType;
}) {
const deleteWebhook = useAsync(WebhookApiService.deleteWebhook, {
onSuccess: () => {
Expand All @@ -32,16 +34,21 @@ export function DeleteWorkflowTrigger({
},
});

const handleConfirmDelete = async () => {
if (type === "webhook") {
await deleteWebhook.executeAsync(id);
} else {
await deleteCronjob.executeAsync(id);
const deleteEmailReceiver = useAsync(
EmailReceiverApiService.deleteEmailReceiver,
{
onSuccess: () => {
mutate(EmailReceiverApiService.getEmailReceivers.key());
toast.success("Email workflow trigger has been deleted.");
},
}
};
);

const { interceptAsync, dialogProps } = useConfirmationDialog();

const handleConfirmDelete = async () =>
await getDeleteFunction().executeAsync(id);

return (
<>
<DropdownMenuItem
Expand Down Expand Up @@ -73,4 +80,17 @@ export function DeleteWorkflowTrigger({
/>
</>
);

function getDeleteFunction() {
switch (type) {
case "webhook":
return deleteWebhook;
case "schedule":
return deleteCronjob;
case "email":
return deleteEmailReceiver;
default:
throw new Error(`Unknown workflow trigger type: ${type}`);
}
}
}
195 changes: 195 additions & 0 deletions ui/admin/app/components/workflow-triggers/EmailReceiverForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router";
import { $path } from "safe-routes";
import { toast } from "sonner";
import useSWR, { mutate } from "swr";
import { z } from "zod";

import { EmailReceiver } from "~/lib/model/email-receivers";
import { EmailReceiverApiService } from "~/lib/service/api/emailReceiverApiService";
import { WorkflowService } from "~/lib/service/api/workflowService";

import {
ControlledCustomInput,
ControlledInput,
} from "~/components/form/controlledInputs";
import { Button } from "~/components/ui/button";
import { Form } from "~/components/ui/form";
import { ScrollArea } from "~/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { useAsync } from "~/hooks/useAsync";

const formSchema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string(),
alias: z.string(),
workflow: z.string().min(1, "Workflow is required"),
allowedSenders: z.array(z.string()),
});

export type EmailRecieverFormValues = z.infer<typeof formSchema>;

type EmailRecieverFormProps = {
emailReceiver?: EmailReceiver;
};

export function EmailReceiverForm({ emailReceiver }: EmailRecieverFormProps) {
const navigate = useNavigate();
const getWorkflows = useSWR(WorkflowService.getWorkflows.key(), () =>
WorkflowService.getWorkflows()
);

const handleSubmitSuccess = () => {
if (emailReceiver) {
mutate(
EmailReceiverApiService.getEmailReceiverById(emailReceiver.id)
);
}
mutate(EmailReceiverApiService.getEmailReceivers.key());
navigate($path("/workflow-triggers"));
};

const form = useForm<EmailRecieverFormValues>({
resolver: zodResolver(formSchema),
mode: "onChange",
defaultValues: {
name: emailReceiver?.name || "",
description: emailReceiver?.description || "",
alias: emailReceiver?.alias || "",
workflow: emailReceiver?.workflow || "",
allowedSenders: emailReceiver?.allowedSenders || [],
},
});

const createEmailReceiver = useAsync(
EmailReceiverApiService.createEmailReceiver,
{
onSuccess: handleSubmitSuccess,
onError: () => {
toast.error("Failed to create email receiver.");
},
}
);

const updateEmailReceiver = useAsync(
EmailReceiverApiService.updateEmailReceiver,
{
onSuccess: handleSubmitSuccess,
onError: () => {
toast.error("Failed to update email receiver.");
},
}
);

useEffect(() => {
if (emailReceiver) {
form.reset(emailReceiver);
}
}, [emailReceiver, form]);

const handleSubmit = form.handleSubmit((values: EmailRecieverFormValues) =>
emailReceiver?.id
? updateEmailReceiver.execute(emailReceiver.id, values)
: createEmailReceiver.execute(values)
);

const workflows = getWorkflows.data;
const isEdit = !!emailReceiver?.id;
const loading =
createEmailReceiver.isLoading || updateEmailReceiver.isLoading;

return (
<ScrollArea className="h-full">
<Form {...form}>
<form
className="space-y-8 p-8 max-w-3xl mx-auto"
onSubmit={handleSubmit}
>
<h2>{isEdit ? "Edit" : "Create"} Email Receiver</h2>

<ControlledInput
control={form.control}
name="name"
label="Name"
/>

<ControlledInput
control={form.control}
name="description"
label="Description (Optional)"
/>

<ControlledInput
control={form.control}
name="alias"
label="Alias (Optional)"
/>

<ControlledCustomInput
control={form.control}
name="workflow"
label="Workflow"
description="The workflow that will be called when an email is received."
>
{({ field: { ref: _, ...field }, className }) => (
<Select
defaultValue={field.value}
onValueChange={field.onChange}
key={field.value}
>
<SelectTrigger className={className}>
<SelectValue placeholder="Select a workflow" />
</SelectTrigger>

<SelectContent>
{getWorkflowOptions()}
</SelectContent>
</Select>
)}
</ControlledCustomInput>

<Button
className="w-full"
type="submit"
disabled={loading}
loading={loading}
>
{isEdit ? "Update" : "Create"} Email Receiver
</Button>
</form>
</Form>
</ScrollArea>
);

function getWorkflowOptions() {
const workflow = form.watch("workflow");

if (getWorkflows.isLoading)
return (
<SelectItem value={workflow || "loading"} disabled>
Loading workflows...
</SelectItem>
);

if (!workflows?.length)
return (
<SelectItem value={workflow || "empty"} disabled>
No workflows found
</SelectItem>
);

return workflows.map((workflow) => (
<SelectItem key={workflow.id} value={workflow.id}>
{workflow.name}
</SelectItem>
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,22 @@ import { Link } from "~/components/ui/link";
import { DeleteWorkflowTrigger } from "~/components/workflow-triggers/DeleteWorkflowTrigger";

export function WorkflowTriggerActions({ item }: { item: WorkflowTrigger }) {
const path =
item.type === "webhook"
? $path("/workflow-triggers/webhooks/:webhook", {
webhook: item.id,
})
: $path("/workflow-triggers/schedule/:trigger", {
trigger: item.id,
});
let path: string = "";

if (item.type === "webhook") {
path = $path("/workflow-triggers/webhooks/:webhook", {
webhook: item.id,
});
} else if (item.type === "schedule") {
path = $path("/workflow-triggers/schedule/:trigger", {
trigger: item.id,
});
} else if (item.type === "email") {
path = $path("/workflow-triggers/email/:receiver", {
receiver: item.id,
});
}

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down
18 changes: 18 additions & 0 deletions ui/admin/app/lib/model/email-receivers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { EntityMeta } from "~/lib/model/primitives";

type EmailReceiverBase = {
name: string;
description: string;
alias?: string;
workflow: string;
allowedSenders?: string[];
};

export type EmailReceiver = EntityMeta &
EmailReceiverBase & {
aliasAssigned?: boolean;
emailAddress?: string;
};

export type CreateEmailReceiver = EmailReceiverBase;
export type UpdateEmailReceiver = EmailReceiverBase;
Loading

0 comments on commit dc18360

Please sign in to comment.