send email upon generate virtual api key

This commit is contained in:
miloschwartz
2026-08-14 16:51:10 -04:00
parent 111d4b1b8c
commit a3dfb30a42
10 changed files with 689 additions and 17 deletions
+53 -2
View File
@@ -54,6 +54,8 @@ import {
getBudgetRowsErrors,
type BudgetRow
} from "@app/components/BudgetsEditor";
import VirtualApiKeyEmailSection from "@app/components/VirtualApiKeyEmailSection";
import type { Tag } from "@app/components/tags/tag-input";
export type CreatedVirtualApiKey = {
virtualApiKeyId: string;
@@ -101,6 +103,9 @@ export default function CreateVirtualApiKeyForm({
>([]);
const [pendingBudgetRows, setPendingBudgetRows] = useState<BudgetRow[]>([]);
const [attemptedBudgetsSave, setAttemptedBudgetsSave] = useState(false);
const [sendEmail, setSendEmail] = useState(false);
const [sendToAttributedUser, setSendToAttributedUser] = useState(false);
const [emailTags, setEmailTags] = useState<Tag[]>([]);
const formSchema = z.object({
name: z.string().min(1),
@@ -123,6 +128,9 @@ export default function CreateVirtualApiKeyForm({
setSelectedResources([]);
setPendingBudgetRows([]);
setAttemptedBudgetsSave(false);
setSendEmail(false);
setSendToAttributedUser(false);
setEmailTags([]);
form.reset();
}
@@ -141,6 +149,20 @@ export default function CreateVirtualApiKeyForm({
return;
}
if (
env.email.emailEnabled &&
sendEmail &&
!sendToAttributedUser &&
emailTags.length === 0
) {
toast({
variant: "destructive",
title: t("virtualApiKeysEmailRecipientsRequired"),
description: t("virtualApiKeysEmailRecipientsRequired")
});
return;
}
return onSubmit(values);
}
@@ -157,7 +179,16 @@ export default function CreateVirtualApiKeyForm({
allResources,
resourceIds: allResources
? []
: selectedResources.map((r) => r.resourceId)
: selectedResources.map((r) => r.resourceId),
sendEmail: env.email.emailEnabled && sendEmail,
sendToAttributedUser:
env.email.emailEnabled &&
sendEmail &&
sendToAttributedUser,
emails:
env.email.emailEnabled && sendEmail
? emailTags.map((tag) => tag.text)
: []
}
)
.catch((e) => {
@@ -473,6 +504,26 @@ export default function CreateVirtualApiKeyForm({
</div>
)}
</div>
<VirtualApiKeyEmailSection
emailEnabled={
env.email.emailEnabled
}
mode="create"
sendEmail={sendEmail}
onSendEmailChange={setSendEmail}
sendToAttributedUser={
sendToAttributedUser
}
onSendToAttributedUserChange={
setSendToAttributedUser
}
hasAssociatedUser={
!!selectedUser
}
emailTags={emailTags}
onEmailTagsChange={setEmailTags}
/>
</div>
<div className="space-y-4 mt-4">
@@ -505,7 +556,7 @@ export default function CreateVirtualApiKeyForm({
</CredenzaClose>
<Button
type="button"
onClick={form.handleSubmit(onSubmit)}
onClick={form.handleSubmit(handleFormSubmit)}
loading={loading}
disabled={credential !== null || loading}
>
+50 -1
View File
@@ -60,6 +60,8 @@ import {
} from "@app/components/BudgetsEditor";
import { aiBudgetQueries } from "@app/lib/queries";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import VirtualApiKeyEmailSection from "@app/components/VirtualApiKeyEmailSection";
import type { Tag } from "@app/components/tags/tag-input";
type FormProps = {
open: boolean;
@@ -114,6 +116,9 @@ export default function EditVirtualApiKeyForm({
const [credentialLoading, setCredentialLoading] = useState(false);
const [pendingBudgetRows, setPendingBudgetRows] = useState<BudgetRow[]>([]);
const [attemptedBudgetsSave, setAttemptedBudgetsSave] = useState(false);
const [sendEmail, setSendEmail] = useState(false);
const [sendToAttributedUser, setSendToAttributedUser] = useState(false);
const [emailTags, setEmailTags] = useState<Tag[]>([]);
const budgetScope = {
type: "virtualApiKey" as const,
@@ -156,6 +161,9 @@ export default function EditVirtualApiKeyForm({
setSelectedResources(
virtualApiKey.allResources ? [] : resourcesFromRow(virtualApiKey)
);
setSendEmail(false);
setSendToAttributedUser(false);
setEmailTags([]);
form.reset({
allResources: virtualApiKey.allResources
});
@@ -236,6 +244,20 @@ export default function EditVirtualApiKeyForm({
return;
}
if (
env.email.emailEnabled &&
sendEmail &&
!sendToAttributedUser &&
emailTags.length === 0
) {
toast({
variant: "destructive",
title: t("virtualApiKeysEmailRecipientsRequired"),
description: t("virtualApiKeysEmailRecipientsRequired")
});
return;
}
return onSubmit(values);
}
@@ -254,7 +276,16 @@ export default function EditVirtualApiKeyForm({
allResources: values.allResources,
resourceIds: values.allResources
? []
: selectedResources.map((r) => r.resourceId)
: selectedResources.map((r) => r.resourceId),
sendEmail: env.email.emailEnabled && sendEmail,
sendToAttributedUser:
env.email.emailEnabled &&
sendEmail &&
sendToAttributedUser,
emails:
env.email.emailEnabled && sendEmail
? emailTags.map((tag) => tag.text)
: []
}
)
.catch((e) => {
@@ -521,6 +552,24 @@ export default function EditVirtualApiKeyForm({
</div>
)}
</div>
<VirtualApiKeyEmailSection
emailEnabled={
env.email.emailEnabled
}
mode="edit"
sendEmail={sendEmail}
onSendEmailChange={setSendEmail}
sendToAttributedUser={
sendToAttributedUser
}
onSendToAttributedUserChange={
setSendToAttributedUser
}
hasAssociatedUser={!!selectedUser}
emailTags={emailTags}
onEmailTagsChange={setEmailTags}
/>
</div>
<div className="space-y-4 mt-4">
@@ -0,0 +1,151 @@
"use client";
import { useEffect, useState } from "react";
import { Checkbox } from "@app/components/ui/checkbox";
import { FormLabel } from "@app/components/ui/form";
import { TagInput, type Tag } from "@app/components/tags/tag-input";
import { useTranslations } from "next-intl";
type VirtualApiKeyEmailSectionProps = {
emailEnabled: boolean;
mode: "create" | "edit";
sendEmail: boolean;
onSendEmailChange: (value: boolean) => void;
sendToAttributedUser: boolean;
onSendToAttributedUserChange: (value: boolean) => void;
hasAssociatedUser: boolean;
emailTags: Tag[];
onEmailTagsChange: (tags: Tag[]) => void;
};
export default function VirtualApiKeyEmailSection({
emailEnabled,
mode,
sendEmail,
onSendEmailChange,
sendToAttributedUser,
onSendToAttributedUserChange,
hasAssociatedUser,
emailTags,
onEmailTagsChange
}: VirtualApiKeyEmailSectionProps) {
const t = useTranslations();
const [activeEmailTagIndex, setActiveEmailTagIndex] = useState<
number | null
>(null);
useEffect(() => {
if (!hasAssociatedUser && sendToAttributedUser) {
onSendToAttributedUserChange(false);
}
}, [hasAssociatedUser, sendToAttributedUser, onSendToAttributedUserChange]);
const checkboxId =
mode === "create"
? "virtual-api-key-send-email"
: "edit-virtual-api-key-send-email";
const sendToUserId =
mode === "create"
? "virtual-api-key-send-to-user"
: "edit-virtual-api-key-send-to-user";
return (
<div className="space-y-3">
<div className="flex items-start space-x-2">
<Checkbox
id={checkboxId}
checked={emailEnabled ? sendEmail : false}
disabled={!emailEnabled}
onCheckedChange={(val) => {
if (emailEnabled) {
onSendEmailChange(val === true);
}
}}
className="mt-0.5"
/>
<div className="space-y-1">
<label
htmlFor={checkboxId}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t(
mode === "create"
? "virtualApiKeysEmailOnGenerate"
: "virtualApiKeysEmailThisKey"
)}
</label>
<p className="text-sm text-muted-foreground">
{emailEnabled
? t(
mode === "create"
? "virtualApiKeysEmailOnGenerateDescription"
: "virtualApiKeysEmailThisKeyDescription"
)
: t("virtualApiKeysEmailSmtpRequiredDescription")}
</p>
</div>
</div>
{emailEnabled && sendEmail && (
<div className="space-y-4 pl-6">
<div className="flex items-start space-x-2">
<Checkbox
id={sendToUserId}
checked={sendToAttributedUser}
disabled={!hasAssociatedUser}
onCheckedChange={(val) =>
onSendToAttributedUserChange(val === true)
}
className="mt-0.5"
/>
<div className="space-y-1">
<label
htmlFor={sendToUserId}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t("virtualApiKeysEmailSendToUser")}
</label>
<p className="text-sm text-muted-foreground">
{hasAssociatedUser
? t(
"virtualApiKeysEmailSendToUserDescription"
)
: t(
"virtualApiKeysEmailSendToUserDisabled"
)}
</p>
</div>
</div>
<div className="space-y-2">
<FormLabel>
{t("virtualApiKeysEmailAdditional")}
</FormLabel>
<TagInput
activeTagIndex={activeEmailTagIndex}
setActiveTagIndex={setActiveEmailTagIndex}
placeholder={t(
"virtualApiKeysEmailAdditionalPlaceholder"
)}
size="sm"
tags={emailTags}
setTags={(newTags) => {
const next =
typeof newTags === "function"
? newTags(emailTags)
: newTags;
onEmailTagsChange(next as Tag[]);
}}
allowDuplicates={false}
sortTags
validateTag={(tag) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(tag)
}
delimiterList={[",", "Enter"]}
/>
</div>
</div>
)}
</div>
);
}