- {field.name === "label" && !isAiGenerated ? (
+ {field.name === "labelId" && !isAiGenerated ? (
- ) : field.name === "label" && isAiGenerated ? (
+ ) : field.name === "labelId" && isAiGenerated ? (
)}
- {field.name === "label" && (
+ {field.name === "labelId" && (
();
const coldEmailDialog = useDialogState();
@@ -291,6 +293,7 @@ export function Rules({
@@ -473,16 +476,19 @@ export function Rules({
export function ActionBadges({
actions,
provider,
+ labels,
}: {
actions: {
id: string;
type: ActionType;
label?: string | null;
+ labelId?: string | null;
folderName?: string | null;
content?: string | null;
to?: string | null;
}[];
provider: string;
+ labels: Array<{ id: string; name: string }>;
}) {
return (
@@ -499,7 +505,7 @@ export function ActionBadges({
className="w-fit text-nowrap"
>
- {getActionDisplay(action, provider)}
+ {getActionDisplay(action, provider, labels)}
);
})}
diff --git a/apps/web/app/(app)/[emailAccountId]/settings/LabelsSection.tsx b/apps/web/app/(app)/[emailAccountId]/settings/LabelsSection.tsx
deleted file mode 100644
index 37dd5ded94..0000000000
--- a/apps/web/app/(app)/[emailAccountId]/settings/LabelsSection.tsx
+++ /dev/null
@@ -1,387 +0,0 @@
-"use client";
-
-import { useCallback, useMemo, useState } from "react";
-import { type SubmitHandler, useForm } from "react-hook-form";
-import useSwr, { useSWRConfig } from "swr";
-import { capitalCase } from "capital-case";
-import sortBy from "lodash/sortBy";
-import { Button } from "@/components/Button";
-import {
- FormSection,
- FormSectionLeft,
- SubmitButtonWrapper,
-} from "@/components/Form";
-import { Input } from "@/components/Input";
-import { LoadingContent } from "@/components/LoadingContent";
-import { Tag } from "@/components/Tag";
-import { toastError, toastSuccess } from "@/components/Toast";
-import { Toggle } from "@/components/Toggle";
-import { SectionDescription, SectionHeader } from "@/components/Typography";
-import {
- type GmailLabel,
- type GmailLabels,
- GmailProvider,
- useGmail,
-} from "@/providers/GmailProvider";
-import { createLabelAction, updateLabelsAction } from "@/utils/actions/mail";
-import type { Label } from "@prisma/client";
-import type { UserLabelsResponse } from "@/app/api/user/labels/route";
-import { PlusIcon } from "lucide-react";
-import { isErrorMessage } from "@/utils/error";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { useAccount } from "@/providers/EmailAccountProvider";
-
-const recommendedLabels = ["Newsletter", "Receipt", "Calendar"];
-
-type ToggleKey = `toggle-${string}`;
-type DescriptionKey = `description-${string}`;
-
-type Inputs = Record
&
- Record;
-
-export const LabelsSection = () => {
- const { data, isLoading, error } =
- useSwr("/api/user/labels");
-
- return (
-
-
- {data && }
-
-
- );
-};
-
-function LabelsSectionForm(props: { dbLabels: Label[] }) {
- const { userLabels, labelsIsLoading } = useGmail();
-
- return (
-
- {userLabels && (
-
- )}
-
- );
-}
-
-function LabelsSectionFormInner(props: {
- gmailLabels: GmailLabels;
- dbLabels: Label[];
-}) {
- const { gmailLabels, dbLabels } = props;
-
- const userLabels = useMemo(() => {
- return sortBy(
- Object.values(gmailLabels || {})
- .filter((l) => l.type !== "system")
- .map((l) => {
- const dbLabel = dbLabels.find((el) => el.gmailLabelId === l.id);
-
- return {
- ...dbLabel,
- ...l,
- };
- }),
- (l) => (l.enabled ? 0 : 1),
- );
- }, [gmailLabels, dbLabels]);
-
- const defaultValues = Object.fromEntries(
- userLabels.flatMap((l) => {
- return [
- [`toggle-${l.id}`, l.enabled],
- [`description-${l.id}`, l.description],
- ];
- }),
- );
-
- const {
- register,
- watch,
- setValue,
- getValues,
- formState: { errors, isSubmitting },
- } = useForm({
- defaultValues,
- });
-
- const recommendedLabelsToCreate = recommendedLabels.filter(
- (label) =>
- !Object.values(gmailLabels || {})
- .map((l) => l.name.toLowerCase())
- .find((l) => l.indexOf(label.toLowerCase()) > -1),
- );
-
- const { emailAccountId } = useAccount();
-
- return (
-
-
-
-
-
-
Your Labels
-
- Labels help our AI properly categorize your emails. You can add a
- description to help the AI decide how to make use of each one. We
- will only make use of enabled labels. Visit Gmail to delete labels.
-
-
-
- {/* <>
-
Your Gmail Labels
-
- These are all your existing labels.
-
-
-
-
- {Object.values(labels || {}).map((label) => (
-
- {label?.type === "system"
- ? capitalCase(label.name)
- : label.name}
-
- ))}
-
-
- > */}
-
- {!!recommendedLabelsToCreate.length && (
-
-
Suggested Labels
-
- Labels we suggest adding to organize your emails. Click a label
- to add it.
-
-
-
- {recommendedLabelsToCreate.map((label) => (
-
- ))}
-
-
-
- )}
-
-
-
- );
-}
-
-export function LabelItem(props: {
- label: GmailLabel;
- register: any;
- watch: any;
- setValue: any;
- errors: any;
-}) {
- const { label, register, watch, setValue, errors } = props;
-
- return (
-
-
-
- {label?.type === "system" ? capitalCase(label.name) : label.name}
-
-
-
-
-
- );
-}
-
-function AddLabelModal() {
- const [isOpen, setIsOpen] = useState(false);
-
- const { emailAccountId } = useAccount();
-
- const { mutate } = useSWRConfig();
-
- const {
- register,
- handleSubmit,
- formState: { errors, isSubmitting },
- } = useForm<{ name: string; description: string }>();
-
- const onSubmit: SubmitHandler<{ name: string; description: string }> =
- useCallback(
- async (data) => {
- const { name, description } = data;
- try {
- await createLabelAction(emailAccountId, { name, description });
-
- toastSuccess({
- description: `Label "${name}" created!`,
- });
-
- // TODO this doesn't work properly. still needs a page refresh
- // the problem is further up in this file where we're using useGmail
- // refetch labels
- mutate("/api/labels");
- mutate("/api/user/labels");
-
- setIsOpen(false);
- } catch (error) {
- console.error(`Failed to create label "${name}": ${error}`);
- toastError({
- description: `Failed to create label "${name}"`,
- });
- }
- },
- [mutate, emailAccountId],
- );
-
- return (
-
- );
-}
diff --git a/apps/web/app/(landing)/components/page.tsx b/apps/web/app/(landing)/components/page.tsx
index 452bd24835..9b78b32cf0 100644
--- a/apps/web/app/(landing)/components/page.tsx
+++ b/apps/web/app/(landing)/components/page.tsx
@@ -332,6 +332,7 @@ export default function Components() {
},
]}
provider="gmail"
+ labels={[{ id: "label", name: "Label" }]}
/>
diff --git a/apps/web/app/api/user/rules/[id]/route.ts b/apps/web/app/api/user/rules/[id]/route.ts
index 91e67769dd..3952dcd2b9 100644
--- a/apps/web/app/api/user/rules/[id]/route.ts
+++ b/apps/web/app/api/user/rules/[id]/route.ts
@@ -28,8 +28,9 @@ async function getRule({
...rule,
actions: rule.actions.map((action) => ({
...action,
- label: {
- value: action.label,
+ labelId: {
+ value: action.labelId, // Use labelId as value, fall back to name for old rules
+ name: action.label, // Fallback
ai: hasVariables(action.label),
},
subject: { value: action.subject },
diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma
index a536f5760e..038a10adb3 100644
--- a/apps/web/prisma/schema.prisma
+++ b/apps/web/prisma/schema.prisma
@@ -428,7 +428,7 @@ model Action {
ruleId String
rule Rule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
- label String?
+ label String? // labelName - labelId is the source of truth, and we use it when set
labelId String? // Stable ID: Label ID (Gmail) or Category ID (Outlook)
subject String?
content String?
diff --git a/apps/web/utils/action-display.tsx b/apps/web/utils/action-display.tsx
index b8d070c5fd..b59a12e886 100644
--- a/apps/web/utils/action-display.tsx
+++ b/apps/web/utils/action-display.tsx
@@ -20,11 +20,13 @@ export function getActionDisplay(
action: {
type: ActionType;
label?: string | null;
+ labelId?: string | null;
folderName?: string | null;
content?: string | null;
to?: string | null;
},
provider: string,
+ labels: Array<{ id: string; name: string }>,
): string {
const terminology = getEmailTerminology(provider);
switch (action.type) {
@@ -33,10 +35,26 @@ export function getActionDisplay(
return `Draft Reply: ${truncate(action.content, 10)}`;
}
return "Draft Reply";
- case ActionType.LABEL:
- return action.label
- ? `${terminology.label.action} as '${truncate(action.label, 15)}'`
+ case ActionType.LABEL: {
+ let labelName: string | null | undefined = null;
+
+ // Priority 1: Use labelId to look up current name from labels
+ if (action.labelId && labels) {
+ const foundLabel = labels.find((l) => l.id === action.labelId);
+ if (foundLabel) {
+ labelName = foundLabel.name;
+ }
+ }
+
+ // Priority 2: Fallback to stored label name (may be outdated but better than nothing)
+ if (!labelName && action.label) {
+ labelName = action.label;
+ }
+
+ return labelName
+ ? `${terminology.label.action} as '${truncate(labelName, 15)}'`
: terminology.label.action;
+ }
case ActionType.ARCHIVE:
return "Skip Inbox";
case ActionType.MARK_READ:
diff --git a/apps/web/utils/action-item.ts b/apps/web/utils/action-item.ts
index ab64370411..b8d8f03322 100644
--- a/apps/web/utils/action-item.ts
+++ b/apps/web/utils/action-item.ts
@@ -10,7 +10,7 @@ export const actionInputs: Record<
{
fields: {
name:
- | "label"
+ | "labelId"
| "subject"
| "content"
| "to"
@@ -30,7 +30,7 @@ export const actionInputs: Record<
[ActionType.LABEL]: {
fields: [
{
- name: "label",
+ name: "labelId",
label: "Label",
},
],
@@ -187,6 +187,7 @@ type ActionFieldsSelection = Pick<
Prisma.ActionCreateInput,
| "type"
| "label"
+ | "labelId"
| "subject"
| "content"
| "to"
@@ -204,6 +205,7 @@ export function sanitizeActionFields(
const base: ActionFieldsSelection = {
type: action.type,
label: null,
+ labelId: null,
subject: null,
content: null,
to: null,
@@ -233,6 +235,7 @@ export function sanitizeActionFields(
return {
...base,
label: action.label ?? null,
+ labelId: action.labelId ?? null,
};
}
case ActionType.REPLY: {
diff --git a/apps/web/utils/actions/rule.ts b/apps/web/utils/actions/rule.ts
index 08f3d430e3..0578d0c70c 100644
--- a/apps/web/utils/actions/rule.ts
+++ b/apps/web/utils/actions/rule.ts
@@ -40,6 +40,7 @@ import { prefixPath } from "@/utils/path";
import { createRuleHistory } from "@/utils/rule/rule-history";
import { ONE_WEEK_MINUTES } from "@/utils/date";
import { createEmailProvider } from "@/utils/email/provider";
+import { resolveLabelNameAndId } from "@/utils/label/resolve-label";
function getCategoryActionDescription(categoryAction: CategoryAction): string {
switch (categoryAction) {
@@ -64,8 +65,19 @@ async function getActionsFromCategoryAction(
hasDigest: boolean,
provider: string,
): Promise
{
+ const emailProvider = await createEmailProvider({
+ emailAccountId,
+ provider,
+ });
+
+ const { label: labelName, labelId } = await resolveLabelNameAndId({
+ emailProvider,
+ label,
+ labelId: null,
+ });
+
let actions: Prisma.ActionCreateManyRuleInput[] = [
- { type: ActionType.LABEL, label },
+ { type: ActionType.LABEL, label: labelName, labelId },
];
switch (categoryAction) {
@@ -116,7 +128,7 @@ export const createRuleAction = actionClient
.schema(createRuleBody)
.action(
async ({
- ctx: { emailAccountId, logger },
+ ctx: { emailAccountId, logger, provider },
parsedInput: {
name,
runOnThreads,
@@ -128,18 +140,24 @@ export const createRuleAction = actionClient
}) => {
const conditions = flattenConditions(conditionsInput);
+ const resolvedActions = await resolveActionLabels(
+ actions || [],
+ emailAccountId,
+ provider,
+ );
+
try {
const rule = await prisma.rule.create({
data: {
name,
runOnThreads: runOnThreads ?? undefined,
- actions: actions
+ actions: resolvedActions.length
? {
createMany: {
- data: actions.map(
+ data: resolvedActions.map(
({
type,
- label,
+ labelId,
subject,
content,
to,
@@ -151,7 +169,8 @@ export const createRuleAction = actionClient
}) => {
return sanitizeActionFields({
type,
- label: label?.value,
+ label: labelId?.name,
+ labelId: labelId?.value,
subject: subject?.value,
content: content?.value,
to: to?.value,
@@ -213,7 +232,7 @@ export const updateRuleAction = actionClient
.schema(updateRuleBody)
.action(
async ({
- ctx: { emailAccountId, logger },
+ ctx: { emailAccountId, logger, provider },
parsedInput: {
id,
name,
@@ -226,6 +245,12 @@ export const updateRuleAction = actionClient
}) => {
const conditions = flattenConditions(conditionsInput);
+ const resolvedActions = await resolveActionLabels(
+ actions,
+ emailAccountId,
+ provider,
+ );
+
try {
const currentRule = await prisma.rule.findUnique({
where: { id, emailAccountId },
@@ -236,10 +261,11 @@ export const updateRuleAction = actionClient
const currentActions = currentRule.actions;
const actionsToDelete = currentActions.filter(
- (currentAction) => !actions.find((a) => a.id === currentAction.id),
+ (currentAction) =>
+ !resolvedActions.find((a) => a.id === currentAction.id),
);
- const actionsToUpdate = actions.filter((a) => a.id);
- const actionsToCreate = actions.filter((a) => !a.id);
+ const actionsToUpdate = resolvedActions.filter((a) => a.id);
+ const actionsToCreate = resolvedActions.filter((a) => !a.id);
const [updatedRule] = await prisma.$transaction([
// update rule
@@ -280,7 +306,8 @@ export const updateRuleAction = actionClient
where: { id: a.id },
data: sanitizeActionFields({
type: a.type,
- label: a.label?.value,
+ label: a.labelId?.name,
+ labelId: a.labelId?.value,
subject: a.subject?.value,
content: a.content?.value,
to: a.to?.value,
@@ -300,7 +327,8 @@ export const updateRuleAction = actionClient
return {
...sanitizeActionFields({
type: a.type,
- label: a.label?.value,
+ label: a.labelId?.name,
+ labelId: a.labelId?.value,
subject: a.subject?.value,
content: a.content?.value,
to: a.to?.value,
@@ -779,3 +807,41 @@ export async function getRuleNameByExecutedAction(
return executedAction.executedRule?.rule?.name;
}
+
+async function resolveActionLabels<
+ T extends {
+ type: ActionType;
+ labelId?: {
+ name?: string | null;
+ value?: string | null;
+ ai?: boolean | null;
+ } | null;
+ },
+>(actions: T[], emailAccountId: string, provider: string) {
+ const emailProvider = await createEmailProvider({
+ emailAccountId,
+ provider,
+ });
+
+ return Promise.all(
+ actions.map(async (action) => {
+ if (action.type === ActionType.LABEL) {
+ const { label: resolvedLabel, labelId: resolvedLabelId } =
+ await resolveLabelNameAndId({
+ emailProvider,
+ label: action.labelId?.name || null,
+ labelId: action.labelId?.value || null,
+ });
+ return {
+ ...action,
+ labelId: {
+ value: resolvedLabelId,
+ name: resolvedLabel,
+ ai: action.labelId?.ai,
+ },
+ };
+ }
+ return action;
+ }),
+ );
+}
diff --git a/apps/web/utils/actions/rule.validation.ts b/apps/web/utils/actions/rule.validation.ts
index c332d57cc7..eeaed500c6 100644
--- a/apps/web/utils/actions/rule.validation.ts
+++ b/apps/web/utils/actions/rule.validation.ts
@@ -68,6 +68,8 @@ const zodField = z
ai: z.boolean().nullish(),
// only needed for frontend
setManually: z.boolean().nullish(),
+ // label name for backup if no labelId exists (only for label field)
+ name: z.string().nullish(),
})
.nullish();
@@ -75,7 +77,7 @@ const zodAction = z
.object({
id: z.string().optional(),
type: zodActionType,
- label: zodField,
+ labelId: zodField,
subject: zodField,
content: zodField,
to: zodField,
@@ -87,11 +89,11 @@ const zodAction = z
delayInMinutes: delayInMinutesSchema,
})
.superRefine((data, ctx) => {
- if (data.type === ActionType.LABEL && !data.label?.value?.trim()) {
+ if (data.type === ActionType.LABEL && !data.labelId?.value?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Please enter a label name for the Label action",
- path: ["label"],
+ path: ["labelId"],
});
}
if (data.type === ActionType.FORWARD && !data.to?.value?.trim()) {
diff --git a/apps/web/utils/actions/settings.ts b/apps/web/utils/actions/settings.ts
index d69d22a8e7..cf50c20179 100644
--- a/apps/web/utils/actions/settings.ts
+++ b/apps/web/utils/actions/settings.ts
@@ -87,20 +87,28 @@ export const updateReplyTrackingAction = actionClient
.metadata({ name: "updateReplyTracking" })
.schema(z.object({ enabled: z.boolean() }))
.action(async ({ ctx: { emailAccountId }, parsedInput: { enabled } }) => {
- // Find all rules with "To Reply" label first
+ const emailAccount = await prisma.emailAccount.findUnique({
+ where: { id: emailAccountId },
+ select: { needsReplyLabelId: true },
+ });
+
+ // Find all rules with "To Reply" label (by labelId or legacy label name)
const toReplyRules = await prisma.rule.findMany({
where: {
emailAccountId,
actions: {
some: {
type: ActionType.LABEL,
- label: NEEDS_REPLY_LABEL_NAME,
+ OR: [
+ ...(emailAccount?.needsReplyLabelId
+ ? [{ labelId: emailAccount.needsReplyLabelId }]
+ : []),
+ { label: NEEDS_REPLY_LABEL_NAME },
+ ],
},
},
},
- include: {
- actions: true,
- },
+ include: { actions: true },
});
// Prepare the operations
@@ -249,7 +257,9 @@ export const updateSystemLabelsAction = actionClient
rule: { emailAccountId },
type: ActionType.LABEL,
OR: [
- { labelId: oldConfig?.needsReplyLabelId ?? undefined },
+ ...(oldConfig?.needsReplyLabelId
+ ? [{ labelId: oldConfig.needsReplyLabelId }]
+ : []),
{ label: NEEDS_REPLY_LABEL_NAME },
],
},
diff --git a/apps/web/utils/label/resolve-label.ts b/apps/web/utils/label/resolve-label.ts
new file mode 100644
index 0000000000..8ba60c6288
--- /dev/null
+++ b/apps/web/utils/label/resolve-label.ts
@@ -0,0 +1,52 @@
+import type { EmailProvider } from "@/utils/email/types";
+
+/**
+ * Resolves label name and ID pairing for a label action.
+ * - If only label name is provided, looks up the labelId
+ * - If only labelId is provided, looks up the label name
+ * - If both are provided, returns both
+ * - Returns null for both if lookup fails
+ */
+export async function resolveLabelNameAndId({
+ emailProvider,
+ label,
+ labelId,
+}: {
+ emailProvider: EmailProvider;
+ label?: string | null;
+ labelId?: string | null;
+}): Promise<{ label: string | null; labelId: string | null }> {
+ // If we have both, return them
+ if (label && labelId) {
+ return { label, labelId };
+ }
+
+ // If we have label name, look up the ID
+ if (label) {
+ try {
+ const foundLabel = await emailProvider.getLabelByName(label);
+ return {
+ label,
+ labelId: foundLabel?.id ?? null,
+ };
+ } catch {
+ return { label, labelId: null };
+ }
+ }
+
+ // If we have labelId, look up the name
+ if (labelId) {
+ try {
+ const foundLabel = await emailProvider.getLabelById(labelId);
+ return {
+ label: foundLabel?.name ?? null,
+ labelId,
+ };
+ } catch {
+ return { label: null, labelId };
+ }
+ }
+
+ // Neither provided
+ return { label: null, labelId: null };
+}
diff --git a/apps/web/utils/reply-tracker/enable.ts b/apps/web/utils/reply-tracker/enable.ts
index 92f64932c9..dd3172a3cc 100644
--- a/apps/web/utils/reply-tracker/enable.ts
+++ b/apps/web/utils/reply-tracker/enable.ts
@@ -1,6 +1,6 @@
import prisma from "@/utils/prisma";
-import { safeCreateRule } from "@/utils/rule/rule";
import { ActionType, SystemType, type Prisma } from "@prisma/client";
+import { safeCreateRule } from "@/utils/rule/rule";
import {
defaultReplyTrackerInstructions,
NEEDS_REPLY_LABEL_NAME,
@@ -8,6 +8,8 @@ import {
import { createScopedLogger } from "@/utils/logger";
import { RuleName } from "@/utils/rule/consts";
import { SafeError } from "@/utils/error";
+import { createEmailProvider } from "@/utils/email/provider";
+import { resolveLabelNameAndId } from "@/utils/label/resolve-label";
export async function enableReplyTracker({
emailAccountId,
@@ -31,6 +33,7 @@ export async function enableReplyTracker({
email: true,
about: true,
rulesPrompt: true,
+ needsReplyLabelId: true,
user: {
select: {
aiProvider: true,
@@ -70,6 +73,19 @@ export async function enableReplyTracker({
let ruleId: string | null = rule?.id || null;
+ const emailProvider = await createEmailProvider({
+ emailAccountId,
+ provider,
+ });
+
+ // Use the user's configured system label, or fall back to default label name
+ const { label: needsReplyLabel, labelId: needsReplyLabelId } =
+ await resolveLabelNameAndId({
+ emailProvider,
+ label: emailAccount.needsReplyLabelId ? null : NEEDS_REPLY_LABEL_NAME,
+ labelId: emailAccount.needsReplyLabelId,
+ });
+
// If rule found, update/create the label action to NEEDS_REPLY_LABEL
if (rule) {
const labelAction = rule.actions.find((a) => a.type === ActionType.LABEL);
@@ -77,13 +93,17 @@ export async function enableReplyTracker({
if (labelAction) {
await prisma.action.update({
where: { id: labelAction.id },
- data: { label: NEEDS_REPLY_LABEL_NAME },
+ data: {
+ label: needsReplyLabel,
+ labelId: needsReplyLabelId,
+ },
});
} else {
await prisma.action.create({
data: {
type: ActionType.LABEL,
- label: NEEDS_REPLY_LABEL_NAME,
+ label: needsReplyLabel,
+ labelId: needsReplyLabelId,
rule: { connect: { id: rule.id } },
},
});
@@ -144,6 +164,22 @@ export async function createToReplyRule(
addDigest: boolean,
provider: string,
) {
+ const emailProvider = await createEmailProvider({
+ emailAccountId,
+ provider,
+ });
+
+ const emailAccount = await prisma.emailAccount.findUnique({
+ where: { id: emailAccountId },
+ select: { needsReplyLabelId: true },
+ });
+
+ const labelInfo = await resolveLabelNameAndId({
+ emailProvider,
+ label: emailAccount?.needsReplyLabelId ? null : NEEDS_REPLY_LABEL_NAME,
+ labelId: emailAccount?.needsReplyLabelId,
+ });
+
return await safeCreateRule({
result: {
name: RuleName.ToReply,
@@ -155,8 +191,9 @@ export async function createToReplyRule(
actions: [
{
type: ActionType.LABEL,
+ labelId: labelInfo.labelId,
fields: {
- label: NEEDS_REPLY_LABEL_NAME,
+ label: labelInfo.label,
to: null,
subject: null,
content: null,
diff --git a/apps/web/utils/rule/rule.ts b/apps/web/utils/rule/rule.ts
index 510a2bcba7..ad25d8ba43 100644
--- a/apps/web/utils/rule/rule.ts
+++ b/apps/web/utils/rule/rule.ts
@@ -16,6 +16,7 @@ import { createRuleHistory } from "@/utils/rule/rule-history";
import { isMicrosoftProvider } from "@/utils/email/provider-types";
import { createEmailProvider } from "@/utils/email/provider";
import type { EmailProvider } from "@/utils/email/types";
+import { resolveLabelNameAndId } from "@/utils/label/resolve-label";
const logger = createScopedLogger("rule");
@@ -33,6 +34,16 @@ export function partialUpdateRule({
});
}
+// Extended type for system rules that can include labelId
+type CreateRuleWithLabelId = Omit<
+ CreateOrUpdateRuleSchemaWithCategories,
+ "actions"
+> & {
+ actions: (CreateOrUpdateRuleSchemaWithCategories["actions"][number] & {
+ labelId?: string | null;
+ })[];
+};
+
export async function safeCreateRule({
result,
emailAccountId,
@@ -42,7 +53,7 @@ export async function safeCreateRule({
triggerType = "ai_creation",
shouldCreateIfDuplicate,
}: {
- result: CreateOrUpdateRuleSchemaWithCategories;
+ result: CreateRuleWithLabelId;
emailAccountId: string;
provider: string;
categoryNames?: string[] | null;
@@ -377,30 +388,30 @@ export async function removeRuleCategories(
}
async function mapActionFields(
- actions: CreateOrUpdateRuleSchemaWithCategories["actions"],
+ actions: (CreateOrUpdateRuleSchemaWithCategories["actions"][number] & {
+ labelId?: string | null;
+ })[],
provider: string,
emailProvider: EmailProvider,
) {
const actionPromises = actions.map(
async (a): Promise => {
+ let label = a.fields?.label;
let labelId: string | null = null;
- if (a.type === ActionType.LABEL && a.fields?.label) {
- try {
- const matchingLabel = await emailProvider.getLabelByName(
- a.fields.label,
- );
- if (matchingLabel) {
- labelId = matchingLabel.id;
- }
- } catch (error) {
- logger.warn("Failed to lookup labelId", { error });
- }
+ if (a.type === ActionType.LABEL) {
+ const resolved = await resolveLabelNameAndId({
+ emailProvider,
+ label: a.fields?.label || null,
+ labelId: a.labelId || null,
+ });
+ label = resolved.label;
+ labelId = resolved.labelId;
}
return {
type: a.type,
- label: a.fields?.label,
+ label,
labelId,
to: a.fields?.to,
cc: a.fields?.cc,
From a21ecf39860aade6bad6a50ffc156f7866d873a9 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Sun, 5 Oct 2025 18:11:50 +0300
Subject: [PATCH 12/17] fix build
---
.../(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx
index 2ef8cdb088..583eb75008 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx
@@ -37,8 +37,8 @@ export function ActionSummaryCard({
switch (action.type) {
case ActionType.LABEL: {
- const labelValue = action.label?.value || "";
- if (action.label?.ai) {
+ const labelValue = action.labelId?.value || "";
+ if (action.labelId?.ai) {
summaryContent = labelValue
? `AI ${terminology.label.action}: ${labelValue}`
: `AI ${terminology.label.action}`;
From e3325e3fb17c486cf3650f5d616ecb6b2693bd48 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Sun, 5 Oct 2025 18:20:11 +0300
Subject: [PATCH 13/17] fix bad redirect
---
.../assistant/FixWithChat.tsx | 10 +-
.../assistant/ProcessResultDisplay.tsx | 104 +++++++++---------
.../assistant/ProcessRules.tsx | 8 +-
.../assistant/TestCustomEmailForm.tsx | 5 +-
4 files changed, 57 insertions(+), 70 deletions(-)
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/FixWithChat.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/FixWithChat.tsx
index a6d8f82fcb..e863bd0ffd 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/FixWithChat.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/FixWithChat.tsx
@@ -13,7 +13,6 @@ import {
} from "@/components/ui/dialog";
import { LoadingContent } from "@/components/LoadingContent";
import { useRules } from "@/hooks/useRules";
-import { useAccount } from "@/providers/EmailAccountProvider";
import { useModal } from "@/hooks/useModal";
import { NEW_RULE_ID } from "@/app/(app)/[emailAccountId]/assistant/consts";
import { Label } from "@/components/Input";
@@ -35,7 +34,6 @@ export function FixWithChat({
result: RunRulesResult | null;
}) {
const { data, isLoading, error } = useRules();
- const { emailAccountId } = useAccount();
const { isModalOpen, setIsModalOpen } = useModal();
const [selectedRuleId, setSelectedRuleId] = useState(null);
const [explanation, setExplanation] = useState("");
@@ -107,7 +105,6 @@ export function FixWithChat({
{data && !showExplanation ? (
void;
}) {
return (
@@ -224,10 +219,7 @@ function RuleMismatch({
{result ? (
-
+
) : (
No rule matched
)}
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/ProcessResultDisplay.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/ProcessResultDisplay.tsx
index 08908b7521..9e92f3e8f0 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/ProcessResultDisplay.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/ProcessResultDisplay.tsx
@@ -1,6 +1,5 @@
"use client";
-import Link from "next/link";
import { capitalCase } from "capital-case";
import { CheckCircle2Icon, EyeIcon, ExternalLinkIcon } from "lucide-react";
import type { RunRulesResult } from "@/utils/ai/choose-rule/run-rules";
@@ -8,17 +7,15 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { HoverCard } from "@/components/HoverCard";
import { Badge } from "@/components/Badge";
import { isAIRule } from "@/utils/condition";
-import { prefixPath } from "@/utils/path";
import { ActionType } from "@prisma/client";
+import { useRuleDialog } from "./RuleDialog";
export function ProcessResultDisplay({
result,
prefix,
- emailAccountId,
}: {
result: RunRulesResult;
prefix?: string;
- emailAccountId: string;
}) {
if (!result) return null;
@@ -48,6 +45,22 @@ export function ProcessResultDisplay({
);
}
+ return (
+
}
+ >
+
+ {prefix ? prefix : ""}
+ {result.rule.name}
+
+
+
+ );
+}
+
+function ActionSummaryCard({ result }: { result: RunRulesResult }) {
+ const { ruleDialog, RuleDialogComponent } = useRuleDialog();
const MAX_LENGTH = 280;
const aiGeneratedContent = result.actionItems
@@ -91,52 +104,43 @@ export function ProcessResultDisplay({
));
+ if (!result.rule) return null;
+
return (
-
-
-
- Matched rule "{result.rule.name}"
-
- View rule
-
-
-
-
- {isAIRule(result.rule) && (
-
- AI Instructions:
- {result.rule.instructions.substring(0, MAX_LENGTH)}
- {result.rule.instructions.length >= MAX_LENGTH && "..."}
-
- )}
- {!!aiGeneratedContent?.length && (
- {aiGeneratedContent}
- )}
- {!!result.reason && (
-
- Reason:
- {result.reason}
-
- )}
-
-
- }
- >
-
- {prefix ? prefix : ""}
- {result.rule.name}
-
-
-
+ <>
+
+
+
+ Matched rule "{result.rule.name}"
+
+
+
+ {isAIRule(result.rule) && (
+
+ AI Instructions:
+ {result.rule.instructions.substring(0, MAX_LENGTH)}
+ {result.rule.instructions.length >= MAX_LENGTH && "..."}
+
+ )}
+ {!!aiGeneratedContent?.length && (
+ {aiGeneratedContent}
+ )}
+ {!!result.reason && (
+
+ Reason:
+ {result.reason}
+
+ )}
+
+
+
+ >
);
}
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/ProcessRules.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/ProcessRules.tsx
index b57bba24e1..da35f02dee 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/ProcessRules.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/ProcessRules.tsx
@@ -299,7 +299,6 @@ export function ProcessRulesContent({ testMode }: { testMode: boolean }) {
result={allResults[message.id]}
onRun={(rerun) => onRun(message, rerun)}
testMode={testMode}
- emailAccountId={emailAccountId}
setInput={setInput}
/>
))}
@@ -336,7 +335,6 @@ function ProcessRulesRow({
result,
onRun,
testMode,
- emailAccountId,
setInput,
}: {
message: Message;
@@ -345,7 +343,6 @@ function ProcessRulesRow({
result: RunRulesResult;
onRun: (rerun?: boolean) => void;
testMode: boolean;
- emailAccountId: string;
setInput: (input: string) => void;
}) {
return (
@@ -369,10 +366,7 @@ function ProcessRulesRow({
{result ? (
<>
{
{testResult && (
)}
From 6857aa78d797591cf0e3cb746553bc9b4e63d82b Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Sun, 5 Oct 2025 18:25:01 +0300
Subject: [PATCH 14/17] fix up old code
---
.../[emailAccountId]/assistant/RuleForm.tsx | 5 +-
.../assistant/group/Groups.tsx | 150 ------------------
.../assistant/rule/[ruleId]/examples/page.tsx | 2 +-
apps/web/components/GroupedTable.tsx | 2 +-
4 files changed, 3 insertions(+), 156 deletions(-)
delete mode 100644 apps/web/app/(app)/[emailAccountId]/assistant/group/Groups.tsx
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
index 2c0ec953a8..562829924d 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
@@ -286,10 +286,7 @@ export function RuleForm({
onSuccess();
} else {
router.replace(
- prefixPath(
- emailAccountId,
- `/assistant?tab=rule&ruleId=${res.data.rule.id}`,
- ),
+ prefixPath(emailAccountId, `/assistant/rule/${res.data.rule.id}`),
);
router.push(prefixPath(emailAccountId, "/assistant?tab=rules"));
}
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/group/Groups.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/group/Groups.tsx
deleted file mode 100644
index 8755c5c7b1..0000000000
--- a/apps/web/app/(app)/[emailAccountId]/assistant/group/Groups.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-"use client";
-
-import useSWR from "swr";
-import Link from "next/link";
-import { LoadingContent } from "@/components/LoadingContent";
-import { AlertBasic } from "@/components/Alert";
-import {
- Card,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import type { GroupsResponse } from "@/app/api/user/group/route";
-import { Button } from "@/components/ui/button";
-import { ConditionType } from "@/utils/config";
-import { useAccount } from "@/providers/EmailAccountProvider";
-import { prefixPath } from "@/utils/path";
-
-export function Groups() {
- const { data, isLoading, error } = useSWR
("/api/user/group");
- const { emailAccountId } = useAccount();
-
- return (
-
-
-
-
- Groups
-
- Groups organize related emails allowing you to apply actions to
- matching emails. Create custom groups manually or create a preset
- group using our AI.
-
-
-
-
-
- {data?.groups.length ? (
-
- ) : (
-
- )}
-
-
- );
-}
-
-function GroupTable({
- groups,
- emailAccountId,
-}: {
- groups: GroupsResponse["groups"];
- emailAccountId: string;
-}) {
- return (
-
-
-
- Group
- Learned Patterns
- Rule
- Actions
-
-
-
- {groups.map((group) => {
- return (
-
-
-
- {group.name}
-
-
-
- {group._count.items}
-
-
- {group.rule ? (
-
- {group.rule.name || `Rule ${group.rule.id}`}
-
- ) : (
-
- )}
-
-
-
-
-
-
-
-
- );
- })}
-
-
- );
-}
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/rule/[ruleId]/examples/page.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/rule/[ruleId]/examples/page.tsx
index ed8df5ec48..c46cc39a7a 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/rule/[ruleId]/examples/page.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/rule/[ruleId]/examples/page.tsx
@@ -44,7 +44,7 @@ export default function RuleExamplesPage(props: {
View Rule
diff --git a/apps/web/components/GroupedTable.tsx b/apps/web/components/GroupedTable.tsx
index ecddc49725..169ffd8811 100644
--- a/apps/web/components/GroupedTable.tsx
+++ b/apps/web/components/GroupedTable.tsx
@@ -434,7 +434,7 @@ function GroupRow({
From c6e09ce528c0ee5ab4f15c852d6fabbbbe974606 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Sun, 5 Oct 2025 18:51:49 +0300
Subject: [PATCH 15/17] more fixes for labelid
---
.../assistant/ActionSummaryCard.tsx | 15 +++++++++++----
.../(app)/[emailAccountId]/assistant/RuleForm.tsx | 6 +++++-
.../assistant/create/examples.tsx | 4 ++--
.../assistant/settings/SystemLabelsSetting.tsx | 15 ++++++++++++---
apps/web/components/LabelCombobox.tsx | 11 ++++++++---
5 files changed, 38 insertions(+), 13 deletions(-)
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx
index 583eb75008..71ecabac43 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx
@@ -8,15 +8,18 @@ import {
} from "@/app/(app)/[emailAccountId]/assistant/constants";
import { TooltipExplanation } from "@/components/TooltipExplanation";
import { getEmailTerminology } from "@/utils/terminology";
+import type { EmailLabel } from "@/providers/EmailProvider";
export function ActionSummaryCard({
action,
typeOptions,
provider,
+ labels,
}: {
action: CreateRuleBody["actions"][number];
typeOptions: { label: string; value: ActionType }[];
provider: string;
+ labels: EmailLabel[];
}) {
// don't display
if (
@@ -37,13 +40,17 @@ export function ActionSummaryCard({
switch (action.type) {
case ActionType.LABEL: {
- const labelValue = action.labelId?.value || "";
+ const labelId = action.labelId?.value || "";
+ const labelName = labelId
+ ? labels.find((label) => label.id === labelId)?.name
+ : action.labelId?.name || "";
+
if (action.labelId?.ai) {
- summaryContent = labelValue
- ? `AI ${terminology.label.action}: ${labelValue}`
+ summaryContent = labelName
+ ? `AI ${terminology.label.action}: ${labelName}`
: `AI ${terminology.label.action}`;
} else {
- summaryContent = `${terminology.label.action} as "${labelValue || "unset"}"`;
+ summaryContent = `${terminology.label.action} as "${labelName || "unset"}"`;
}
break;
}
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
index 562829924d..b0b37eb061 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
@@ -842,6 +842,7 @@ export function RuleForm({
action={action}
typeOptions={typeOptions}
provider={provider}
+ labels={userLabels}
/>
),
)}
@@ -1123,7 +1124,10 @@ function ActionCard({
userLabels={userLabels || []}
isLoading={isLoading}
mutate={mutate}
- value={value}
+ value={{
+ id: value,
+ name: action.labelId?.name || null,
+ }}
onChangeValue={(newValue: string) => {
setValue(
`actions.${index}.${field.name}.value`,
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/create/examples.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/create/examples.tsx
index 1d159e2dad..b8b0033296 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/create/examples.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/create/examples.tsx
@@ -49,7 +49,7 @@ export const examples: {
],
actions: [
{ type: ActionType.ARCHIVE },
- { type: ActionType.LABEL, label: { value: "Newsletter" } },
+ { type: ActionType.LABEL, labelId: { name: "Newsletter" } },
],
},
},
@@ -68,7 +68,7 @@ export const examples: {
* Customer complaint`,
},
],
- actions: [{ type: ActionType.LABEL, label: { value: "High Priority" } }],
+ actions: [{ type: ActionType.LABEL, labelId: { name: "High Priority" } }],
},
},
{
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/settings/SystemLabelsSetting.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/settings/SystemLabelsSetting.tsx
index 602afe998c..bb8af4de5a 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/settings/SystemLabelsSetting.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/settings/SystemLabelsSetting.tsx
@@ -159,7 +159,10 @@ function SystemLabelsForm({
setValue("needsReplyLabelId", value || undefined, {
shouldDirty: true,
@@ -177,7 +180,10 @@ function SystemLabelsForm({
setValue("awaitingReplyLabelId", value || undefined, {
shouldDirty: true,
@@ -195,7 +201,10 @@ function SystemLabelsForm({
setValue("coldEmailLabelId", value || undefined, {
shouldDirty: true,
diff --git a/apps/web/components/LabelCombobox.tsx b/apps/web/components/LabelCombobox.tsx
index 0c4c1eac95..0d013706ec 100644
--- a/apps/web/components/LabelCombobox.tsx
+++ b/apps/web/components/LabelCombobox.tsx
@@ -15,7 +15,10 @@ export function LabelCombobox({
mutate,
emailAccountId,
}: {
- value: string;
+ value: {
+ id: string | null;
+ name: string | null;
+ };
onChangeValue: (value: string) => void;
userLabels: EmailLabel[];
isLoading: boolean;
@@ -24,7 +27,9 @@ export function LabelCombobox({
}) {
const [search, setSearch] = useState("");
- const selectedLabel = userLabels.find((label) => label.id === value);
+ const selectedLabel = userLabels.find(
+ (label) => label.id === value.id || label.name === value.name,
+ );
return (
Date: Sun, 5 Oct 2025 19:46:46 +0300
Subject: [PATCH 16/17] fix build and comments
---
.../assistant/ProcessResultDisplay.tsx | 107 ++++++++++--------
.../assistant/rule/create/page.tsx | 2 +-
apps/web/utils/rule/rule.ts | 16 +--
3 files changed, 70 insertions(+), 55 deletions(-)
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/ProcessResultDisplay.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/ProcessResultDisplay.tsx
index 9e92f3e8f0..4e814c7e2d 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/ProcessResultDisplay.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/ProcessResultDisplay.tsx
@@ -17,6 +17,8 @@ export function ProcessResultDisplay({
result: RunRulesResult;
prefix?: string;
}) {
+ const { ruleDialog, RuleDialogComponent } = useRuleDialog();
+
if (!result) return null;
if (!result.rule) {
@@ -45,22 +47,36 @@ export function ProcessResultDisplay({
);
}
+ const handleViewRule = (ruleId: string) => {
+ ruleDialog.onOpen({ ruleId });
+ };
+
return (
- }
- >
-
- {prefix ? prefix : ""}
- {result.rule.name}
-
-
-
+ <>
+
+ }
+ >
+
+ {prefix ? prefix : ""}
+ {result.rule.name}
+
+
+
+
+ >
);
}
-function ActionSummaryCard({ result }: { result: RunRulesResult }) {
- const { ruleDialog, RuleDialogComponent } = useRuleDialog();
+function ActionSummaryCard({
+ result,
+ onViewRule,
+}: {
+ result: RunRulesResult;
+ onViewRule: (ruleId: string) => void;
+}) {
const MAX_LENGTH = 280;
const aiGeneratedContent = result.actionItems
@@ -107,40 +123,37 @@ function ActionSummaryCard({ result }: { result: RunRulesResult }) {
if (!result.rule) return null;
return (
- <>
-
-
-
- Matched rule "{result.rule.name}"
-
-
-
- {isAIRule(result.rule) && (
-
- AI Instructions:
- {result.rule.instructions.substring(0, MAX_LENGTH)}
- {result.rule.instructions.length >= MAX_LENGTH && "..."}
-
- )}
- {!!aiGeneratedContent?.length && (
- {aiGeneratedContent}
- )}
- {!!result.reason && (
-
- Reason:
- {result.reason}
-
- )}
-
-
-
- >
+
+
+
+ Matched rule "{result.rule.name}"
+
+
+
+ {isAIRule(result.rule) && (
+
+ AI Instructions:
+ {result.rule.instructions.substring(0, MAX_LENGTH)}
+ {result.rule.instructions.length >= MAX_LENGTH && "..."}
+
+ )}
+ {!!aiGeneratedContent?.length && (
+ {aiGeneratedContent}
+ )}
+ {!!result.reason && (
+
+ Reason:
+ {result.reason}
+
+ )}
+
+
);
}
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/rule/create/page.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/rule/create/page.tsx
index 272ddc47ad..fa36475bf4 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/rule/create/page.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/rule/create/page.tsx
@@ -28,7 +28,7 @@ export default async function CreateRulePage(props: {
? [
{
type: ActionType.LABEL,
- label: { value: searchParams.label },
+ labelId: { name: searchParams.label },
},
]
: [],
diff --git a/apps/web/utils/rule/rule.ts b/apps/web/utils/rule/rule.ts
index ad25d8ba43..9e25e595a5 100644
--- a/apps/web/utils/rule/rule.ts
+++ b/apps/web/utils/rule/rule.ts
@@ -177,11 +177,10 @@ export async function createRule({
triggerType?: "ai_creation" | "manual_creation" | "system_creation";
provider: string;
}) {
- const emailProvider = await createEmailProvider({ emailAccountId, provider });
const mappedActions = await mapActionFields(
result.actions,
provider,
- emailProvider,
+ emailAccountId,
);
const rule = await prisma.rule.create({
@@ -240,7 +239,6 @@ async function updateRule({
triggerType?: "ai_update" | "manual_update" | "system_update";
provider: string;
}) {
- const emailProvider = await createEmailProvider({ emailAccountId, provider });
const rule = await prisma.rule.update({
where: { id: ruleId },
data: {
@@ -251,7 +249,7 @@ async function updateRule({
actions: {
deleteMany: {},
createMany: {
- data: await mapActionFields(result.actions, provider, emailProvider),
+ data: await mapActionFields(result.actions, provider, emailAccountId),
},
},
conditionalOperator: result.condition.conditionalOperator ?? undefined,
@@ -288,14 +286,13 @@ export async function updateRuleActions({
provider: string;
emailAccountId: string;
}) {
- const emailProvider = await createEmailProvider({ emailAccountId, provider });
return prisma.rule.update({
where: { id: ruleId },
data: {
actions: {
deleteMany: {},
createMany: {
- data: await mapActionFields(actions, provider, emailProvider),
+ data: await mapActionFields(actions, provider, emailAccountId),
},
},
},
@@ -392,7 +389,7 @@ async function mapActionFields(
labelId?: string | null;
})[],
provider: string,
- emailProvider: EmailProvider,
+ emailAccountId: string,
) {
const actionPromises = actions.map(
async (a): Promise => {
@@ -400,6 +397,11 @@ async function mapActionFields(
let labelId: string | null = null;
if (a.type === ActionType.LABEL) {
+ const emailProvider = await createEmailProvider({
+ emailAccountId,
+ provider,
+ });
+
const resolved = await resolveLabelNameAndId({
emailProvider,
label: a.fields?.label || null,
From 618da807da7f64e9c27e0ef3c02cf4fa9bcc0326 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Sun, 5 Oct 2025 23:28:53 +0300
Subject: [PATCH 17/17] fix tests
---
.../utils/cold-email/is-cold-email.test.ts | 24 +++++++++----------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/apps/web/utils/cold-email/is-cold-email.test.ts b/apps/web/utils/cold-email/is-cold-email.test.ts
index 34d7163edc..9fe7d15559 100644
--- a/apps/web/utils/cold-email/is-cold-email.test.ts
+++ b/apps/web/utils/cold-email/is-cold-email.test.ts
@@ -93,10 +93,10 @@ describe("blockColdEmail", () => {
type: "coldEmail",
provider: mockProvider,
});
- expect(mockProvider.labelMessage).toHaveBeenCalledWith(
- mockEmail.id,
- "label123",
- );
+ expect(mockProvider.labelMessage).toHaveBeenCalledWith({
+ messageId: mockEmail.id,
+ labelId: "label123",
+ });
});
it("should archive email when coldEmailBlocker is ARCHIVE_AND_LABEL", async () => {
@@ -112,10 +112,10 @@ describe("blockColdEmail", () => {
aiReason: mockAiReason,
});
- expect(mockProvider.labelMessage).toHaveBeenCalledWith(
- mockEmail.id,
- "label123",
- );
+ expect(mockProvider.labelMessage).toHaveBeenCalledWith({
+ messageId: mockEmail.id,
+ labelId: "label123",
+ });
expect(mockProvider.archiveThread).toHaveBeenCalledWith(
mockEmail.threadId,
userWithArchive.email,
@@ -135,10 +135,10 @@ describe("blockColdEmail", () => {
aiReason: mockAiReason,
});
- expect(mockProvider.labelMessage).toHaveBeenCalledWith(
- mockEmail.id,
- "label123",
- );
+ expect(mockProvider.labelMessage).toHaveBeenCalledWith({
+ messageId: mockEmail.id,
+ labelId: "label123",
+ });
expect(mockProvider.archiveThread).toHaveBeenCalledWith(
mockEmail.threadId,
userWithArchiveAndRead.email,