Skip to content

Commit c0b3783

Browse files
authored
support for rules and actions for mailboxes (#389)
1 parent f4c3643 commit c0b3783

File tree

32 files changed

+2438
-86
lines changed

32 files changed

+2438
-86
lines changed

apps/web/app/dashboard/(unified)/(mail)/mail/[identityPublicId]/[mailboxSlug]/layout.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import { SidebarTrigger } from "@/components/ui/sidebar";
2-
import { Separator } from "@/components/ui/separator";
3-
import MailboxSearch from "@/components/mailbox/default/mailbox-search";
41
import React, { ReactNode } from "react";
5-
import { isSignedIn } from "@/lib/actions/auth";
2+
import MailboxSearchHeader from "@/components/mailbox/mailbox-search-header";
63

74
type LayoutProps = {
85
children: ReactNode;
@@ -18,24 +15,10 @@ export default async function DashboardLayout({
1815
thread,
1916
params,
2017
}: LayoutProps) {
21-
const { identityPublicId, mailboxSlug } = await params;
22-
const user = await isSignedIn();
2318

2419
return (
2520
<>
26-
<header className="bg-background sticky top-0 flex shrink-0 items-center gap-2 border-b p-4">
27-
<SidebarTrigger className="-ml-1" />
28-
<Separator
29-
orientation="vertical"
30-
className="mr-2 data-[orientation=vertical]:h-4"
31-
/>
32-
33-
<MailboxSearch
34-
user={user}
35-
publicId={identityPublicId}
36-
mailboxSlug={mailboxSlug}
37-
/>
38-
</header>
21+
<MailboxSearchHeader params={params} />
3922

4023
{thread}
4124
{children}
Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,15 @@
1-
import { SidebarTrigger } from "@/components/ui/sidebar";
2-
import { Separator } from "@/components/ui/separator";
3-
import MailboxSearch from "@/components/mailbox/default/mailbox-search";
41
import React, { ReactNode } from "react";
5-
import { isSignedIn } from "@/lib/actions/auth";
2+
import MailboxSearchHeader from "@/components/mailbox/mailbox-search-header";
63

74
type LayoutProps = {
8-
children: ReactNode;
9-
params: Promise<Record<string, string>>;
5+
children: ReactNode;
6+
params: Promise<Record<string, string>>;
107
};
118

12-
export default async function DashboardLayout({
13-
children,
14-
params,
15-
}: LayoutProps) {
16-
const { identityPublicId, mailboxSlug } = await params;
9+
export default async function DashboardLayout({ children, params }: LayoutProps) {
1710

18-
const user = await isSignedIn();
19-
20-
return (
21-
<>
22-
<header className="bg-background sticky top-0 flex shrink-0 items-center gap-2 border-b p-4">
23-
<SidebarTrigger className="-ml-1" />
24-
<Separator
25-
orientation="vertical"
26-
className="mr-2 data-[orientation=vertical]:h-4"
27-
/>
28-
29-
<MailboxSearch
30-
user={user}
31-
publicId={identityPublicId}
32-
mailboxSlug={mailboxSlug}
33-
/>
34-
</header>
35-
36-
{children}
37-
</>
38-
);
11+
return <>
12+
<MailboxSearchHeader params={params} />
13+
{children}
14+
</>
3915
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Loading from "@/app/loading";
2+
export default Loading;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import {Button} from "@mantine/core";
3+
import {Trash2} from "lucide-react";
4+
import SectionCard from "@/components/mailbox/settings/settings-section-card";
5+
6+
function Page() {
7+
return <>
8+
<SectionCard
9+
title="Danger zone"
10+
description="Be careful — these actions can’t be undone."
11+
footer={
12+
<div className="flex items-center justify-end">
13+
<Button color="red" leftSection={<Trash2 size={16} />}>
14+
Delete identity
15+
</Button>
16+
</div>
17+
}
18+
>
19+
<div className="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-900 dark:border-red-900/40 dark:bg-red-950/30 dark:text-red-100">
20+
Deleting an identity will remove its mailboxes, rules, and related
21+
data.
22+
</div>
23+
</SectionCard>
24+
</>
25+
}
26+
27+
export default Page;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { ReactNode } from "react";
2+
import MailboxSearchHeader from "@/components/mailbox/mailbox-search-header";
3+
import {Mail} from "lucide-react";
4+
import SettingsTabs from "@/components/mailbox/settings/settings-tabs";
5+
import { Container } from "@/components/common/containers";
6+
import { rlsClient } from "@/lib/actions/clients";
7+
import { identities } from "@db";
8+
import { eq } from "drizzle-orm";
9+
10+
type LayoutProps = {
11+
children: ReactNode;
12+
params: Promise<Record<string, string>>;
13+
};
14+
15+
16+
17+
export default async function Layout({
18+
children,
19+
params,
20+
}: LayoutProps) {
21+
22+
const paramsResolved = await params;
23+
const rls = await rlsClient();
24+
const [identity] = await rls((tx) =>
25+
tx
26+
.select()
27+
.from(identities)
28+
.where(eq(identities.publicId, paramsResolved.identityPublicId))
29+
);
30+
31+
const identityLabel = identity?.value;
32+
33+
34+
return <>
35+
<MailboxSearchHeader params={params} />
36+
37+
<Container variant="wide" className="my-10">
38+
<div className="mb-6">
39+
<h1 className="text-xl font-semibold text-neutral-900 dark:text-neutral-50">
40+
Settings
41+
</h1>
42+
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
43+
Identity settings for this mailbox
44+
</p>
45+
</div>
46+
47+
<div className={"grid grid-cols-1 gap-6 lg:grid-cols-[280px_1fr]"}>
48+
<div className={"rounded-2xl border border-neutral-200 bg-white p-3 dark:border-neutral-800 dark:bg-neutral-900"}>
49+
<div className={"px-3 pb-3 pt-2"}>
50+
<div className={"flex items-center gap-3"}>
51+
<div className={"flex h-10 w-10 items-center justify-center rounded-xl bg-brand/10 dark:bg-brand/50 text-brand dark:text-brand-foreground"}>
52+
<Mail size={18} />
53+
</div>
54+
<div className={"min-w-0"}>
55+
<div className={"truncate text-sm font-semibold text-neutral-900 dark:text-neutral-50"}>
56+
{identityLabel}
57+
</div>
58+
<div className={"truncate text-xs text-neutral-600 dark:text-neutral-400"}>
59+
Identity
60+
</div>
61+
</div>
62+
</div>
63+
</div>
64+
65+
<SettingsTabs />
66+
</div>
67+
68+
69+
<div className="space-y-6">
70+
{children}
71+
</div>
72+
73+
</div>
74+
</Container>
75+
76+
</>
77+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from "react";
2+
import SettingsGeneral from "@/components/mailbox/settings/settings-general";
3+
import { FormState, handleAction } from "@schema";
4+
import { decode } from "decode-formdata";
5+
import { rlsClient } from "@/lib/actions/clients";
6+
import { identities } from "@db";
7+
import { eq } from "drizzle-orm";
8+
import { revalidatePath } from "next/cache";
9+
10+
11+
async function Page({params}: {params: Promise<Record<string, string>>}) {
12+
13+
const paramsResolved = await params;
14+
const rls = await rlsClient();
15+
const [identity] = await rls((tx) =>
16+
tx
17+
.select()
18+
.from(identities)
19+
.where(eq(identities.publicId, paramsResolved.identityPublicId))
20+
);
21+
22+
const updateName = async (_prev: FormState, formData: FormData): Promise<FormState> => {
23+
"use server";
24+
return handleAction(async () => {
25+
const decodedForm = decode(formData);
26+
const rls = await rlsClient();
27+
await rls((tx) =>
28+
tx
29+
.update(identities)
30+
.set({
31+
displayName: decodedForm.displayName as string,
32+
})
33+
.where(eq(identities.id, decodedForm.id as string)),
34+
);
35+
revalidatePath(String(decodedForm.pathname))
36+
return {success: true, message: "Display name updated."};
37+
})
38+
}
39+
40+
return <SettingsGeneral updateName={updateName} identity={identity} />
41+
}
42+
43+
export default Page;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Loading from "@/app/loading";
2+
export default Loading;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from "react";
2+
import { eq, ne } from "drizzle-orm";
3+
import { identities, labels } from "@db";
4+
import SectionCard from "@/components/mailbox/settings/settings-section-card";
5+
import CreateMailRuleForm from "@/components/mailbox/settings/rules/create-rule-form";
6+
import { createRule, fetchMailRules } from "@/lib/actions/mail-rules";
7+
import { rlsClient } from "@/lib/actions/clients";
8+
import MailRulesList from "@/components/mailbox/settings/rules/mail-rules-list";
9+
import { Divider } from "@mantine/core";
10+
11+
async function Page({ params }: { params: { identityPublicId: string } }) {
12+
13+
const resolvedParams = await params;
14+
const rls = await rlsClient();
15+
16+
const [identity] = await rls((tx) =>
17+
tx
18+
.select()
19+
.from(identities)
20+
.where(eq(identities.publicId, resolvedParams.identityPublicId)),
21+
);
22+
23+
const appLabels = await rls((tx) =>
24+
tx.select().from(labels).where(ne(labels.isSystem, true))
25+
);
26+
27+
28+
if (!identity) {
29+
return (
30+
<SectionCard title="Rules" description="Create filters to automatically process incoming mail.">
31+
<div className="text-sm text-neutral-600 dark:text-neutral-400">
32+
Identity not found.
33+
</div>
34+
</SectionCard>
35+
);
36+
}
37+
38+
const rules = await fetchMailRules(identity.id);
39+
40+
return (
41+
<SectionCard
42+
title={"Rules"}
43+
description={"Create filters to automatically process incoming mail."}
44+
>
45+
<MailRulesList rules={rules} />
46+
{rules.length > 0 && <Divider my={"xl"} variant={"dashed"} label={<span className={"text-sm"}>Add New Label</span>} labelPosition={"left"} />}
47+
<CreateMailRuleForm identityId={identity.id} action={createRule} appLabels={appLabels} />
48+
49+
</SectionCard>
50+
);
51+
}
52+
53+
export default Page;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Loading from "@/app/loading";
2+
export default Loading;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import SectionCard from "@/components/mailbox/settings/settings-section-card";
3+
4+
function Page() {
5+
return <>
6+
<SectionCard
7+
title="Subscriptions"
8+
description="Subscriptions detected from List-Unsubscribe headers."
9+
>
10+
<div className="rounded-xl border border-dashed border-neutral-300 p-6 text-sm text-neutral-600 dark:border-neutral-700 dark:text-neutral-400">
11+
Show a searchable list here (domain/list-id, status, last seen,
12+
unsubscribe).
13+
</div>
14+
</SectionCard>
15+
</>
16+
}
17+
18+
export default Page;

0 commit comments

Comments
 (0)