Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement email receivers in workflow-triggers page #1148

Merged
merged 2 commits into from
Jan 8, 2025
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 @@ -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