Skip to content
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
11 changes: 3 additions & 8 deletions apps/web/app/(app)/[emailAccountId]/assistant/BulkRunRules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { runAiRules } from "@/utils/queue/email-actions";
import { sleep } from "@/utils/sleep";
import { PremiumAlertWithData, usePremium } from "@/components/PremiumAlert";
import { SetDateDropdown } from "@/app/(app)/[emailAccountId]/assistant/SetDateDropdown";
import { dateToSeconds } from "@/utils/date";
import { useThreads } from "@/hooks/useThreads";
import { useAiQueueState } from "@/store/ai-queue";
import {
Expand Down Expand Up @@ -155,12 +154,6 @@ async function onRun(
let nextPageToken = "";
const LIMIT = 25;

const startDateInSeconds = dateToSeconds(startDate);
const endDateInSeconds = endDate ? dateToSeconds(endDate) : "";
const q = `after:${startDateInSeconds} ${
endDate ? `before:${endDateInSeconds}` : ""
} is:unread`;

let aborted = false;

function abort() {
Expand All @@ -173,7 +166,9 @@ async function onRun(
type: "inbox",
nextPageToken,
limit: LIMIT,
q,
after: startDate,
before: endDate || undefined,
isUnread: true,
};
const res = await fetchWithAccount({
url: `/api/threads?${
Expand Down
8 changes: 6 additions & 2 deletions apps/web/app/(app)/[emailAccountId]/mail/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ export default function Mail(props: {
previousPageData: ThreadsResponse | null,
) => {
if (previousPageData && !previousPageData.nextPageToken) return null;
const queryParams = new URLSearchParams(query as Record<string, string>);

// Append nextPageToken for subsequent pages
if (pageIndex > 0 && previousPageData?.nextPageToken) {
queryParams.set("nextPageToken", previousPageData.nextPageToken);
query.nextPageToken = previousPageData.nextPageToken;
}

// biome-ignore lint/suspicious/noExplicitAny: params
const queryParams = new URLSearchParams(query as any);

return `/api/threads?${queryParams.toString()}`;
};

Expand Down
127 changes: 74 additions & 53 deletions apps/web/app/(app)/accounts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,75 +24,96 @@ import { PageWrapper } from "@/components/PageWrapper";

export default function AccountsPage() {
const { data, isLoading, error, mutate } = useAccounts();

return (
<PageWrapper>
<PageHeader title="Accounts" description="Manage your email accounts." />

<LoadingContent loading={isLoading} error={error}>
<div className="grid grid-cols-1 gap-4 py-6 md:grid-cols-2 lg:grid-cols-3">
{data?.emailAccounts.map((emailAccount) => (
<AccountItem
key={emailAccount.id}
emailAccount={emailAccount}
onAccountDeleted={mutate}
/>
))}
<AddAccount />
</div>
</LoadingContent>
</PageWrapper>
);
}

function AccountItem({
emailAccount,
onAccountDeleted,
}: {
emailAccount: {
id: string;
name: string | null;
email: string;
image: string | null;
isPrimary: boolean;
};
onAccountDeleted: () => void;
}) {
const { execute, isExecuting } = useAction(deleteEmailAccountAction, {
onSuccess: () => {
toastSuccess({
title: "Email account deleted",
description: "The email account has been deleted successfully.",
});
mutate();
onAccountDeleted();
},
onError: (error) => {
toastError({
title: "Error deleting email account",
description: error.error.serverError || "An unknown error occurred",
});
mutate();
onAccountDeleted();
},
});

return (
<PageWrapper>
<PageHeader title="Accounts" description="Manage your email accounts." />

<LoadingContent loading={isLoading} error={error}>
<div className="grid grid-cols-1 gap-4 py-6 md:grid-cols-2 lg:grid-cols-3">
{data?.emailAccounts.map((emailAccount) => (
<Card key={emailAccount.id}>
<CardHeader className="flex flex-row items-center gap-3 space-y-0">
<Avatar>
<AvatarImage src={emailAccount.image || undefined} />
<AvatarFallback>
{emailAccount.name?.[0] || emailAccount.email?.[0]}
</AvatarFallback>
</Avatar>
<div className="flex flex-col space-y-1.5">
<CardTitle>{emailAccount.name}</CardTitle>
<CardDescription>{emailAccount.email}</CardDescription>
</div>
</CardHeader>
<CardContent className="flex justify-end gap-2">
<Link href={prefixPath(emailAccount.id, "/setup")}>
<Button variant="outline" size="sm" Icon={ArrowRight}>
View
</Button>
</Link>
{!emailAccount.isPrimary && (
<ConfirmDialog
trigger={
<Button
variant="destructiveSoft"
size="sm"
loading={isExecuting}
Icon={Trash2}
>
Delete
</Button>
}
title="Delete Account"
description={`Are you sure you want to delete "${emailAccount.email}"? This will delete all data for it on Inbox Zero.`}
confirmText="Delete"
onConfirm={() => {
execute({ emailAccountId: emailAccount.id });
}}
/>
)}
</CardContent>
</Card>
))}
<AddAccount />
<Card>
<CardHeader className="flex flex-row items-center gap-3 space-y-0">
<Avatar>
<AvatarImage src={emailAccount.image || undefined} />
<AvatarFallback>
{emailAccount.name?.[0] || emailAccount.email?.[0]}
</AvatarFallback>
</Avatar>
<div className="flex flex-col space-y-1.5">
<CardTitle>{emailAccount.name}</CardTitle>
<CardDescription>{emailAccount.email}</CardDescription>
</div>
</LoadingContent>
</PageWrapper>
</CardHeader>
<CardContent className="flex justify-end gap-2">
<Button variant="outline" size="sm" Icon={ArrowRight} asChild>
<Link href={prefixPath(emailAccount.id, "/setup")}>View</Link>
</Button>
{!emailAccount.isPrimary && (
<ConfirmDialog
trigger={
<Button
variant="destructiveSoft"
size="sm"
loading={isExecuting}
Icon={Trash2}
>
Delete
</Button>
}
title="Delete Account"
description={`Are you sure you want to delete "${emailAccount.email}"? This will delete all data for it on Inbox Zero.`}
confirmText="Delete"
onConfirm={() => {
execute({ emailAccountId: emailAccount.id });
}}
/>
)}
</CardContent>
</Card>
);
}
129 changes: 0 additions & 129 deletions apps/web/app/api/google/threads/controller.ts

This file was deleted.

6 changes: 6 additions & 0 deletions apps/web/app/api/threads/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const GET = withEmailProvider(async (request) => {
const nextPageToken = searchParams.get("nextPageToken");
const q = searchParams.get("q");
const labelId = searchParams.get("labelId");
const after = searchParams.get("after");
const before = searchParams.get("before");
const isUnread = searchParams.get("isUnread");

const query = threadsQuery.parse({
limit,
Expand All @@ -34,6 +37,9 @@ export const GET = withEmailProvider(async (request) => {
nextPageToken,
q,
labelId,
after,
before,
isUnread,
});

try {
Expand Down
4 changes: 3 additions & 1 deletion apps/web/app/api/threads/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ export const threadsQuery = z.object({
fromEmail: z.string().nullish(),
limit: z.coerce.number().max(100).nullish(),
type: z.string().nullish(),
q: z.string().nullish(),
nextPageToken: z.string().nullish(),
labelId: z.string().nullish(), // For Google
after: z.coerce.date().nullish(),
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty string (?after=) causes Invalid Date and a 400; preprocess '' to undefined so the filter is ignored instead of failing.

Prompt for AI agents
Address the following comment on apps/web/app/api/threads/validation.ts at line 9:

<comment>Empty string (?after=) causes Invalid Date and a 400; preprocess &#39;&#39; to undefined so the filter is ignored instead of failing.</comment>

<file context>
@@ -4,8 +4,10 @@ export const threadsQuery = z.object({
-  q: z.string().nullish(),
   nextPageToken: z.string().nullish(),
   labelId: z.string().nullish(), // For Google
+  after: z.coerce.date().nullish(),
+  before: z.coerce.date().nullish(),
+  isUnread: z.coerce.boolean().nullish(),
</file context>
Fix with Cubic

before: z.coerce.date().nullish(),
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty string (?before=) will fail date coercion; preprocess '' to undefined to skip the filter.

Prompt for AI agents
Address the following comment on apps/web/app/api/threads/validation.ts at line 10:

<comment>Empty string (?before=) will fail date coercion; preprocess &#39;&#39; to undefined to skip the filter.</comment>

<file context>
@@ -4,8 +4,10 @@ export const threadsQuery = z.object({
   nextPageToken: z.string().nullish(),
   labelId: z.string().nullish(), // For Google
+  after: z.coerce.date().nullish(),
+  before: z.coerce.date().nullish(),
+  isUnread: z.coerce.boolean().nullish(),
 });
</file context>
Fix with Cubic

isUnread: z.coerce.boolean().nullish(),
});
export type ThreadsQuery = z.infer<typeof threadsQuery>;
14 changes: 9 additions & 5 deletions apps/web/hooks/useThreads.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import useSWR from "swr";
import type { ThreadsResponse } from "@/app/api/threads/route";
import type { Thread as EmailThread } from "@/components/email-list/types";
import type { ThreadsQuery } from "@/app/api/threads/validation";

export type Thread = EmailThread;

Expand All @@ -15,11 +16,14 @@ export function useThreads({
limit?: number;
refreshInterval?: number;
}) {
const searchParams = new URLSearchParams();
if (fromEmail) searchParams.set("fromEmail", fromEmail);
if (limit) searchParams.set("limit", limit.toString());
if (type) searchParams.set("type", type);
const url = `/api/threads?${searchParams.toString()}`;
const query: ThreadsQuery = {
fromEmail,
limit,
type,
};

// biome-ignore lint/suspicious/noExplicitAny: params
const url = `/api/threads?${new URLSearchParams(query as any).toString()}`;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing query as any to URLSearchParams will include keys with undefined values (e.g., limit=undefined, type=undefined), altering intended nullish handling and weakening type safety. Filter out nullish keys and stringify values before building the query string.

Prompt for AI agents
Address the following comment on apps/web/hooks/useThreads.ts at line 26:

<comment>Passing query as any to URLSearchParams will include keys with undefined values (e.g., limit=undefined, type=undefined), altering intended nullish handling and weakening type safety. Filter out nullish keys and stringify values before building the query string.</comment>

<file context>
@@ -15,11 +16,14 @@ export function useThreads({
+  };
+
+  // biome-ignore lint/suspicious/noExplicitAny: params
+  const url = `/api/threads?${new URLSearchParams(query as any).toString()}`;
   const { data, isLoading, error, mutate } = useSWR&lt;ThreadsResponse&gt;(url, {
     refreshInterval,
</file context>
Fix with Cubic

const { data, isLoading, error, mutate } = useSWR<ThreadsResponse>(url, {
refreshInterval,
});
Expand Down
Loading
Loading