@nocobase/portal-template-default 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.MD +4 -0
  3. package/dist/assets/{LineDraw-iG-ADes-.js → LineDraw-BBB5hwuJ.js} +1 -1
  4. package/dist/assets/{SymbolDraw-DXoSw1tM.js → SymbolDraw-CKS3uUb_.js} +1 -1
  5. package/dist/assets/{VisualMapping-C_ee4Eht.js → VisualMapping-BCwaW3TD.js} +1 -1
  6. package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
  7. package/dist/assets/{auto-login-provider-5Uxza9Ms.js → auto-login-provider-DO5Il6Dp.js} +2 -2
  8. package/dist/assets/auto-login-provider-TQ08B6-i.js +1 -0
  9. package/dist/assets/auto-redirect-provider-ButXp0A3.js +1 -0
  10. package/dist/assets/auto-redirect-provider-DnGGP2Vn.js +1 -0
  11. package/dist/assets/auto-redirect-provider-ja-vFPaF.js +1 -0
  12. package/dist/assets/calendar-C7xnRgOT.js +1 -0
  13. package/dist/assets/{cas-sign-in-button-CyqG1QME.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
  14. package/dist/assets/{components-D-ptYjX_.js → components-_g-GjQeV.js} +3 -3
  15. package/dist/assets/{configuration-gate-C08mkkiX.js → configuration-gate-B4XVz7tS.js} +1 -1
  16. package/dist/assets/{createClipPathFromCoordSys-wj34-b1B.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
  17. package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
  18. package/dist/assets/{customGraphicKeyframeAnimation-DWrhrHL_.js → customGraphicKeyframeAnimation-DMU5crAu.js} +1 -1
  19. package/dist/assets/demo-BPlae2bp.js +1 -0
  20. package/dist/assets/demo-Bj_Y0RrW.js +1 -0
  21. package/dist/assets/demo-CKPAQOib.js +1 -0
  22. package/dist/assets/demo-CXRTPvIx.js +1 -0
  23. package/dist/assets/demo-DJuVDu1n.js +1 -0
  24. package/dist/assets/demo-DUmWU7ik.js +1 -0
  25. package/dist/assets/demo-NhIzFRE1.js +1 -0
  26. package/dist/assets/{dingtalk-sign-in-button-DSWsSruv.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
  27. package/dist/assets/{echarts-preview-DxHNfq1C.js → echarts-preview-R96Pbqmz.js} +1 -1
  28. package/dist/assets/{echarts-runtime-D8kXn6ME.js → echarts-runtime-Ce_z2eX_.js} +3 -3
  29. package/dist/assets/{echarts-runtime-advanced-QY8v95qK.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
  30. package/dist/assets/{echarts-runtime-common-D-yqAK-r.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
  31. package/dist/assets/{echarts-runtime-components-P4-i8jPU.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
  32. package/dist/assets/{echarts-runtime-hierarchy-ByQZ-DbP.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
  33. package/dist/assets/{floating-BFVEyPEp.js → floating-O9WmiDXk.js} +2 -2
  34. package/dist/assets/index-BBKV6O5d.css +1 -0
  35. package/dist/assets/{index-Btk35Nx-.js → index-CPzehPGe.js} +3 -3
  36. package/dist/assets/index-Cc6awvdg.js +1064 -0
  37. package/dist/assets/index-ChA0z9ke.js +38 -0
  38. package/dist/assets/{index-um5hdKdZ.js → index-DgB7uTMs.js} +2 -2
  39. package/dist/assets/{index-CwNxd66J.js → index-DroUAK80.js} +1 -1
  40. package/dist/assets/index-WKvygTBp.js +59 -0
  41. package/dist/assets/{labelHelper-DQqRy3a_.js → labelHelper-Dk5jkiD-.js} +1 -1
  42. package/dist/assets/ldap-sign-in-form-CKN4l7qM.js +1 -0
  43. package/dist/assets/{oidc-sign-in-button-CQdRcR_p.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
  44. package/dist/assets/{page-context-CnQ4GYM7.js → page-context-DAiW_dId.js} +3 -3
  45. package/dist/assets/prompt-card-rGZqpqkF.js +1 -0
  46. package/dist/assets/prompt-output-B4h3uLWf.js +1 -0
  47. package/dist/assets/saml-sign-in-button-B4ugaLIb.js +1 -0
  48. package/dist/assets/{sectorHelper-DN_Vv6OL.js → sectorHelper-BuxrrJrA.js} +1 -1
  49. package/dist/assets/shortcut-BhKXxJLs.js +47 -0
  50. package/dist/assets/{sms-sign-in-form-DTX9GV2I.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
  51. package/dist/assets/{static.browser-jI1psK0d.js → static.browser-kzzpH0Xt.js} +8 -8
  52. package/dist/assets/switch-BdVxPIVU.js +1 -0
  53. package/dist/assets/{tool-cards-DFttLAfo.js → tool-cards-CpczIke-.js} +3 -3
  54. package/dist/assets/{use-authenticator-sign-in-CRGjzXju.js → use-authenticator-sign-in-CIj210ry.js} +1 -1
  55. package/dist/assets/use-dingtalk-sign-in-BW4kkXD3.js +1 -0
  56. package/dist/assets/{use-wecom-sign-in-C9j8wfCD.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
  57. package/dist/assets/wecom-sign-in-button-Dn_2zDab.js +1 -0
  58. package/dist/index.html +2 -2
  59. package/dist/index.raw.html +2 -2
  60. package/package.json +9 -1
  61. package/src/App.tsx +5 -2
  62. package/src/app/extension.ts +2 -0
  63. package/src/app/extensions.tsx +18 -6
  64. package/src/app/route-access-guard.tsx +22 -0
  65. package/src/app/route-runtime.ts +173 -0
  66. package/src/app/routes.tsx +2 -2
  67. package/src/components/app-shell/header.tsx +56 -1
  68. package/src/components/app-shell/sidebar.tsx +14 -2
  69. package/src/components/auth/sign-up-form.tsx +287 -59
  70. package/src/components/auth/types.ts +23 -1
  71. package/src/extensions/nocobase-acl/README.md +28 -14
  72. package/src/extensions/nocobase-acl/demo/index.tsx +87 -15
  73. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +1 -0
  74. package/src/extensions/nocobase-mail/README.md +74 -0
  75. package/src/extensions/nocobase-mail/components/index.ts +79 -0
  76. package/src/extensions/nocobase-mail/components/mail-api.ts +423 -0
  77. package/src/extensions/nocobase-mail/components/mail-attachment-list.tsx +67 -0
  78. package/src/extensions/nocobase-mail/components/mail-compose-attachments.tsx +166 -0
  79. package/src/extensions/nocobase-mail/components/mail-compose.tsx +1071 -0
  80. package/src/extensions/nocobase-mail/components/mail-detail.tsx +653 -0
  81. package/src/extensions/nocobase-mail/components/mail-draft-autosave.ts +26 -0
  82. package/src/extensions/nocobase-mail/components/mail-empty.tsx +48 -0
  83. package/src/extensions/nocobase-mail/components/mail-filters.tsx +277 -0
  84. package/src/extensions/nocobase-mail/components/mail-inbox.tsx +544 -0
  85. package/src/extensions/nocobase-mail/components/mail-inline-images.ts +69 -0
  86. package/src/extensions/nocobase-mail/components/mail-label-badge.tsx +25 -0
  87. package/src/extensions/nocobase-mail/components/mail-label-manager.tsx +271 -0
  88. package/src/extensions/nocobase-mail/components/mail-labels-editor.tsx +267 -0
  89. package/src/extensions/nocobase-mail/components/mail-mass-tracking.tsx +271 -0
  90. package/src/extensions/nocobase-mail/components/mail-note-editor.tsx +99 -0
  91. package/src/extensions/nocobase-mail/components/mail-recipient-input.tsx +260 -0
  92. package/src/extensions/nocobase-mail/components/mail-reply-quote.ts +28 -0
  93. package/src/extensions/nocobase-mail/components/mail-rich-editor.tsx +287 -0
  94. package/src/extensions/nocobase-mail/components/mail-send-actions.tsx +238 -0
  95. package/src/extensions/nocobase-mail/components/mail-senders.ts +61 -0
  96. package/src/extensions/nocobase-mail/components/mail-settings-drawer.tsx +708 -0
  97. package/src/extensions/nocobase-mail/components/mail-showcase-layout.tsx +64 -0
  98. package/src/extensions/nocobase-mail/components/mail-signature-manager.tsx +258 -0
  99. package/src/extensions/nocobase-mail/components/mail-signatures.ts +25 -0
  100. package/src/extensions/nocobase-mail/components/mail-table.tsx +476 -0
  101. package/src/extensions/nocobase-mail/components/mail-template-manager.tsx +204 -0
  102. package/src/extensions/nocobase-mail/components/mail-toolbar.tsx +155 -0
  103. package/src/extensions/nocobase-mail/components/mail-unread.tsx +111 -0
  104. package/src/extensions/nocobase-mail/components/types.ts +297 -0
  105. package/src/extensions/nocobase-mail/components/use-mail-compose.tsx +262 -0
  106. package/src/extensions/nocobase-mail/components/use-mail-mass-messages.ts +54 -0
  107. package/src/extensions/nocobase-mail/components/use-mail-messages.ts +113 -0
  108. package/src/extensions/nocobase-mail/components/use-mail-signatures.ts +122 -0
  109. package/src/extensions/nocobase-mail/components/use-mail-templates.ts +54 -0
  110. package/src/extensions/nocobase-mail/extension.tsx +105 -0
  111. package/src/extensions/nocobase-mail/index.ts +1 -0
  112. package/src/extensions/nocobase-mail/mail-demo-pages.tsx +503 -0
  113. package/src/extensions/nocobase-mail/mail-pages.tsx +201 -0
  114. package/src/extensions/nocobase-mail/tests/mail-quote-html-regression.mjs +51 -0
  115. package/src/extensions/nocobase-mail/tests/mail-regression.mjs +649 -0
  116. package/src/extensions/nocobase-users-example/app-routes.tsx +121 -0
  117. package/src/extensions/nocobase-users-example/extension.tsx +2 -100
  118. package/src/extensions/nocobase-users-example/index.ts +1 -0
  119. package/src/lib/nocobase/acl/menu.ts +17 -3
  120. package/src/lib/nocobase/acl/types.ts +4 -0
  121. package/src/locales/en-US.ts +2 -1
  122. package/src/locales/zh-CN.ts +2 -1
  123. package/src/providers/runtime-config.ts +18 -0
  124. package/src/routes.tsx +10 -0
  125. package/src/vite-env.d.ts +3 -0
  126. package/vite.config.ts +8 -0
  127. package/dist/assets/auth-method-demo-DaOCuvsq.js +0 -1
  128. package/dist/assets/auto-login-provider-DyaPGK2D.js +0 -1
  129. package/dist/assets/auto-redirect-provider-CfwRvYOI.js +0 -1
  130. package/dist/assets/auto-redirect-provider-Cr3jmYts.js +0 -1
  131. package/dist/assets/auto-redirect-provider-QkJQRBbG.js +0 -1
  132. package/dist/assets/calendar-DjDRfD5t.js +0 -1
  133. package/dist/assets/card-DuQBOS6A.js +0 -1
  134. package/dist/assets/createSeriesDataSimply-D0A6SAVQ.js +0 -1
  135. package/dist/assets/demo-BYWHeKUe.js +0 -1
  136. package/dist/assets/demo-CPWoNtx8.js +0 -1
  137. package/dist/assets/demo-CyvJk5zV.js +0 -1
  138. package/dist/assets/demo-D4OiKplc.js +0 -1
  139. package/dist/assets/demo-DBueJg0m.js +0 -1
  140. package/dist/assets/demo-DkIhyiRK.js +0 -1
  141. package/dist/assets/demo-tpTjO0sC.js +0 -1
  142. package/dist/assets/index-BgCbGDMc.js +0 -43
  143. package/dist/assets/index-MB-jfQld.js +0 -686
  144. package/dist/assets/index-NH3aqqQz.css +0 -1
  145. package/dist/assets/index-gFtCXH3o.js +0 -58
  146. package/dist/assets/ldap-sign-in-form--6Gt3yGq.js +0 -1
  147. package/dist/assets/prompt-card-B2RS4nt2.js +0 -1
  148. package/dist/assets/prompt-output-DZHRM2ft.js +0 -1
  149. package/dist/assets/saml-sign-in-button-BHL5a0JV.js +0 -1
  150. package/dist/assets/shortcut-C5x5LQWU.js +0 -47
  151. package/dist/assets/switch-CRL8PmRI.js +0 -1
  152. package/dist/assets/use-dingtalk-sign-in-8WYzJ2jf.js +0 -1
  153. package/dist/assets/wecom-sign-in-button-mIS1zGSU.js +0 -1
@@ -0,0 +1,708 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { useGetIdentity } from "@refinedev/core";
3
+ import {
4
+ AtSign,
5
+ FileText,
6
+ Loader2,
7
+ Mail,
8
+ MoreHorizontal,
9
+ PenLine,
10
+ Plus,
11
+ RefreshCw,
12
+ Settings,
13
+ Star,
14
+ Tags,
15
+ Trash2,
16
+ } from "lucide-react";
17
+ import { toast } from "sonner";
18
+ import { mailApi } from "./mail-api";
19
+ import { MailLabelManager } from "./mail-label-manager";
20
+ import { MailTemplateManager } from "./mail-template-manager";
21
+ import { MailSignatureManager } from "./mail-signature-manager";
22
+ import { useMailTemplates } from "./use-mail-templates";
23
+ import { useMailSignatures } from "./use-mail-signatures";
24
+ import type { MailAccount, MailLabel } from "./types";
25
+ import { Button } from "@/components/ui/button";
26
+ import { Badge } from "@/components/ui/badge";
27
+ import { Input } from "@/components/ui/input";
28
+ import { Label } from "@/components/ui/label";
29
+ import {
30
+ Sheet,
31
+ SheetContent,
32
+ SheetDescription,
33
+ SheetHeader,
34
+ SheetTitle,
35
+ } from "@/components/ui/sheet";
36
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
37
+ import {
38
+ Select,
39
+ SelectContent,
40
+ SelectItem,
41
+ SelectTrigger,
42
+ SelectValue,
43
+ } from "@/components/ui/select";
44
+ import {
45
+ DropdownMenu,
46
+ DropdownMenuContent,
47
+ DropdownMenuItem,
48
+ DropdownMenuSeparator,
49
+ DropdownMenuTrigger,
50
+ } from "@/components/ui/dropdown-menu";
51
+ import {
52
+ Dialog,
53
+ DialogContent,
54
+ DialogFooter,
55
+ DialogHeader,
56
+ DialogTitle,
57
+ } from "@/components/ui/dialog";
58
+ import {
59
+ AlertDialog,
60
+ AlertDialogAction,
61
+ AlertDialogCancel,
62
+ AlertDialogContent,
63
+ AlertDialogDescription,
64
+ AlertDialogFooter,
65
+ AlertDialogHeader,
66
+ AlertDialogTitle,
67
+ } from "@/components/ui/alert-dialog";
68
+
69
+ export interface MailSettingsDrawerProps {
70
+ open: boolean;
71
+ onOpenChange: (open: boolean) => void;
72
+ }
73
+
74
+ function EmptySetting({ children }: { children: string }) {
75
+ return (
76
+ <div className="rounded-xl border border-dashed px-4 py-10 text-center text-sm text-muted-foreground">
77
+ {children}
78
+ </div>
79
+ );
80
+ }
81
+
82
+ export function MailSettingsDrawer({
83
+ open,
84
+ onOpenChange,
85
+ }: MailSettingsDrawerProps) {
86
+ const { data: identity } = useGetIdentity<{ id: number | string }>();
87
+ const [accounts, setAccounts] = useState<MailAccount[]>([]);
88
+ const [labels, setLabels] = useState<MailLabel[]>([]);
89
+ const [loading, setLoading] = useState(false);
90
+ const [syncingEmail, setSyncingEmail] = useState<string>();
91
+ const [selectedAccountId, setSelectedAccountId] = useState<number>();
92
+ const [deleteAccount, setDeleteAccount] = useState<MailAccount>();
93
+ const [resyncAccount, setResyncAccount] = useState<MailAccount>();
94
+ const [senderAccount, setSenderAccount] = useState<MailAccount>();
95
+ const [aliasAccount, setAliasAccount] = useState<MailAccount>();
96
+ const [syncingAliases, setSyncingAliases] = useState(false);
97
+ const [senderName, setSenderName] = useState("");
98
+ const [savingSender, setSavingSender] = useState(false);
99
+ const templates = useMailTemplates();
100
+ const selectedAccount = accounts.find(
101
+ (account) => account.id === selectedAccountId
102
+ );
103
+ const signatures = useMailSignatures(selectedAccount, (updated) => {
104
+ setAccounts((current) =>
105
+ current.map((account) => (account.id === updated.id ? updated : account))
106
+ );
107
+ });
108
+
109
+ const loadSettings = useCallback(async () => {
110
+ setLoading(true);
111
+ try {
112
+ const [nextAccounts, nextLabels] = await Promise.all([
113
+ mailApi.getAccounts(),
114
+ mailApi.getLabels(identity?.id),
115
+ ]);
116
+ setAccounts(nextAccounts);
117
+ setLabels(nextLabels);
118
+ setSelectedAccountId((current) =>
119
+ nextAccounts.some((account) => account.id === current)
120
+ ? current
121
+ : nextAccounts[0]?.id
122
+ );
123
+ } catch (error) {
124
+ toast.error(
125
+ error instanceof Error ? error.message : "Failed to load mail settings"
126
+ );
127
+ } finally {
128
+ setLoading(false);
129
+ }
130
+ }, [identity?.id]);
131
+
132
+ useEffect(() => {
133
+ if (open) void loadSettings();
134
+ }, [loadSettings, open]);
135
+
136
+ useEffect(() => {
137
+ if (!open) return;
138
+ const refreshOnFocus = () => void loadSettings();
139
+ window.addEventListener("focus", refreshOnFocus);
140
+ return () => window.removeEventListener("focus", refreshOnFocus);
141
+ }, [loadSettings, open]);
142
+
143
+ const openOAuth = async (
144
+ type: "google" | "microsoft",
145
+ options?: { email?: string; reauthorize?: boolean }
146
+ ) => {
147
+ try {
148
+ const url = await mailApi.getOAuthUrl(type, options);
149
+ if (!url) throw new Error("Authorization URL was not returned");
150
+ window.open(url, "_blank", "noopener,noreferrer");
151
+ } catch (error) {
152
+ toast.error(
153
+ error instanceof Error ? error.message : "Failed to start authorization"
154
+ );
155
+ }
156
+ };
157
+
158
+ const reauthorize = (account: MailAccount) => {
159
+ if (account.type !== "google" && account.type !== "microsoft") {
160
+ toast.error(`Unsupported mailbox type: ${account.type}`);
161
+ return;
162
+ }
163
+ void openOAuth(account.type, { email: account.email, reauthorize: true });
164
+ };
165
+
166
+ const syncAccount = async (account: MailAccount) => {
167
+ setSyncingEmail(account.email);
168
+ try {
169
+ await mailApi.sync([account.email]);
170
+ toast.success(`${account.email} synced`);
171
+ } catch (error) {
172
+ toast.error(
173
+ error instanceof Error ? error.message : "Mailbox sync failed"
174
+ );
175
+ } finally {
176
+ setSyncingEmail(undefined);
177
+ }
178
+ };
179
+
180
+ const confirmDeleteAccount = async () => {
181
+ if (!deleteAccount) return;
182
+ setSyncingEmail(deleteAccount.email);
183
+ try {
184
+ await mailApi.deleteAccount(deleteAccount.email);
185
+ toast.success("Mailbox deleted");
186
+ setDeleteAccount(undefined);
187
+ await loadSettings();
188
+ } catch (error) {
189
+ toast.error(
190
+ error instanceof Error ? error.message : "Mailbox deletion failed"
191
+ );
192
+ } finally {
193
+ setSyncingEmail(undefined);
194
+ }
195
+ };
196
+
197
+ const confirmResyncAccount = async () => {
198
+ if (!resyncAccount) return;
199
+ setSyncingEmail(resyncAccount.email);
200
+ try {
201
+ await mailApi.resyncAccount(resyncAccount.email);
202
+ toast.success("Mailbox resync completed");
203
+ setResyncAccount(undefined);
204
+ } catch (error) {
205
+ toast.error(
206
+ error instanceof Error ? error.message : "Mailbox resync failed"
207
+ );
208
+ } finally {
209
+ setSyncingEmail(undefined);
210
+ }
211
+ };
212
+
213
+ const editSenderName = (account: MailAccount) => {
214
+ setSenderAccount(account);
215
+ setSenderName(
216
+ typeof account.config?.displayName === "string"
217
+ ? account.config.displayName
218
+ : ""
219
+ );
220
+ };
221
+
222
+ const syncAliases = async () => {
223
+ if (!aliasAccount) return;
224
+ setSyncingAliases(true);
225
+ try {
226
+ const identities = await mailApi.syncAliases(aliasAccount.email);
227
+ const updated = { ...aliasAccount, identities };
228
+ setAliasAccount(updated);
229
+ setAccounts((current) =>
230
+ current.map((account) =>
231
+ account.id === updated.id ? updated : account
232
+ )
233
+ );
234
+ toast.success("Aliases synced");
235
+ } catch (error) {
236
+ toast.error(
237
+ error instanceof Error ? error.message : "Failed to sync aliases"
238
+ );
239
+ } finally {
240
+ setSyncingAliases(false);
241
+ }
242
+ };
243
+
244
+ const saveSenderName = async () => {
245
+ if (!senderAccount) return;
246
+ setSavingSender(true);
247
+ try {
248
+ const config = {
249
+ ...(senderAccount.config ?? {}),
250
+ displayName: senderName.trim(),
251
+ };
252
+ const updated = await mailApi.updateAccount(senderAccount.id, { config });
253
+ setAccounts((current) =>
254
+ current.map((account) =>
255
+ account.id === senderAccount.id
256
+ ? { ...account, ...updated, config: updated.config ?? config }
257
+ : account
258
+ )
259
+ );
260
+ setSenderAccount(undefined);
261
+ toast.success("Sender name saved");
262
+ } catch (error) {
263
+ toast.error(
264
+ error instanceof Error ? error.message : "Failed to save sender name"
265
+ );
266
+ } finally {
267
+ setSavingSender(false);
268
+ }
269
+ };
270
+
271
+ return (
272
+ <>
273
+ <Sheet open={open} onOpenChange={onOpenChange}>
274
+ <SheetContent
275
+ side="right"
276
+ className="w-full gap-0 p-0 data-[side=right]:sm:max-w-3xl"
277
+ >
278
+ <SheetHeader className="border-b px-6 py-5">
279
+ <div className="flex items-center gap-2">
280
+ <Settings className="size-4 text-muted-foreground" />
281
+ <SheetTitle>Mail settings</SheetTitle>
282
+ </div>
283
+ <SheetDescription>
284
+ Manage personal mailboxes and reusable message content.
285
+ </SheetDescription>
286
+ </SheetHeader>
287
+
288
+ <Tabs defaultValue="mailbox" className="min-h-0 flex-1 gap-0">
289
+ <div className="border-b px-6 py-3">
290
+ <TabsList variant="line" className="h-9">
291
+ <TabsTrigger value="mailbox">
292
+ <Mail /> Mailbox
293
+ </TabsTrigger>
294
+ <TabsTrigger value="labels">
295
+ <Tags /> Labels
296
+ </TabsTrigger>
297
+ <TabsTrigger value="templates">
298
+ <FileText /> Templates
299
+ </TabsTrigger>
300
+ <TabsTrigger value="signature">
301
+ <Star /> Signature
302
+ </TabsTrigger>
303
+ </TabsList>
304
+ </div>
305
+
306
+ <div className="min-h-0 flex-1 overflow-y-auto p-6">
307
+ <TabsContent value="mailbox" className="space-y-4">
308
+ <div className="flex items-center justify-between gap-3">
309
+ <div>
310
+ <h3 className="font-semibold">Connected mailboxes</h3>
311
+ <p className="mt-1 text-xs text-muted-foreground">
312
+ View and sync the mailboxes available to your account.
313
+ </p>
314
+ </div>
315
+ <div className="flex items-center gap-2">
316
+ <Button
317
+ variant="outline"
318
+ size="sm"
319
+ onClick={() => void loadSettings()}
320
+ disabled={loading}
321
+ >
322
+ <RefreshCw
323
+ className={loading ? "animate-spin" : undefined}
324
+ />{" "}
325
+ Refresh
326
+ </Button>
327
+ <DropdownMenu>
328
+ <DropdownMenuTrigger
329
+ render={
330
+ <Button size="sm">
331
+ <Plus /> Link email
332
+ </Button>
333
+ }
334
+ />
335
+ <DropdownMenuContent align="end" className="w-44">
336
+ <DropdownMenuItem
337
+ onClick={() => void openOAuth("google")}
338
+ >
339
+ <Mail /> Gmail
340
+ </DropdownMenuItem>
341
+ <DropdownMenuItem
342
+ onClick={() => void openOAuth("microsoft")}
343
+ >
344
+ <Mail /> Outlook
345
+ </DropdownMenuItem>
346
+ </DropdownMenuContent>
347
+ </DropdownMenu>
348
+ </div>
349
+ </div>
350
+ {loading && !accounts.length ? (
351
+ <div className="flex justify-center py-12">
352
+ <Loader2 className="animate-spin text-muted-foreground" />
353
+ </div>
354
+ ) : accounts.length ? (
355
+ <div className="divide-y overflow-hidden rounded-xl border bg-card">
356
+ {accounts.map((account) => (
357
+ <div
358
+ key={account.id}
359
+ className="flex items-center gap-4 px-4 py-3.5"
360
+ >
361
+ <span className="flex size-9 items-center justify-center rounded-lg bg-muted">
362
+ <Mail className="size-4" />
363
+ </span>
364
+ <span className="min-w-0 flex-1">
365
+ <span className="block truncate text-sm font-medium">
366
+ {account.email}
367
+ </span>
368
+ <span className="mt-0.5 block text-xs text-muted-foreground">
369
+ {account.identities?.filter(
370
+ (identity) => !identity.isPrimary
371
+ ).length ?? 0}{" "}
372
+ aliases
373
+ </span>
374
+ </span>
375
+ <Badge variant="outline" className="capitalize">
376
+ {account.type}
377
+ </Badge>
378
+ <Button
379
+ variant="outline"
380
+ size="sm"
381
+ disabled={syncingEmail === account.email}
382
+ onClick={() => void syncAccount(account)}
383
+ >
384
+ <RefreshCw
385
+ className={
386
+ syncingEmail === account.email
387
+ ? "animate-spin"
388
+ : undefined
389
+ }
390
+ />
391
+ Sync
392
+ </Button>
393
+ <DropdownMenu>
394
+ <DropdownMenuTrigger
395
+ render={
396
+ <Button
397
+ variant="ghost"
398
+ size="icon-sm"
399
+ title={`Manage ${account.email}`}
400
+ >
401
+ <MoreHorizontal />
402
+ </Button>
403
+ }
404
+ />
405
+ <DropdownMenuContent align="end" className="w-44">
406
+ <DropdownMenuItem
407
+ onClick={() => reauthorize(account)}
408
+ >
409
+ <RefreshCw /> Reauthorize
410
+ </DropdownMenuItem>
411
+ <DropdownMenuItem
412
+ onClick={() => setAliasAccount(account)}
413
+ >
414
+ <AtSign /> Aliases
415
+ </DropdownMenuItem>
416
+ <DropdownMenuItem
417
+ disabled={account.type === "microsoft"}
418
+ onClick={() => editSenderName(account)}
419
+ >
420
+ <PenLine /> Sender name
421
+ </DropdownMenuItem>
422
+ <DropdownMenuItem
423
+ onClick={() => setResyncAccount(account)}
424
+ >
425
+ <RefreshCw /> Resync
426
+ </DropdownMenuItem>
427
+ <DropdownMenuSeparator />
428
+ <DropdownMenuItem
429
+ variant="destructive"
430
+ onClick={() => setDeleteAccount(account)}
431
+ >
432
+ <Trash2 /> Delete
433
+ </DropdownMenuItem>
434
+ </DropdownMenuContent>
435
+ </DropdownMenu>
436
+ </div>
437
+ ))}
438
+ </div>
439
+ ) : (
440
+ <EmptySetting>No connected mailboxes</EmptySetting>
441
+ )}
442
+ </TabsContent>
443
+
444
+ <TabsContent value="labels" className="space-y-4">
445
+ <div>
446
+ <h3 className="font-semibold">Labels</h3>
447
+ <p className="mt-1 text-xs text-muted-foreground">
448
+ Create and organize reusable message labels.
449
+ </p>
450
+ </div>
451
+ <MailLabelManager
452
+ embedded
453
+ open
454
+ onOpenChange={() => undefined}
455
+ labels={labels}
456
+ onChange={setLabels}
457
+ />
458
+ </TabsContent>
459
+
460
+ <TabsContent value="templates" className="space-y-4">
461
+ <div>
462
+ <h3 className="font-semibold">Templates</h3>
463
+ <p className="mt-1 text-xs text-muted-foreground">
464
+ Manage reusable content for new messages.
465
+ </p>
466
+ </div>
467
+ <MailTemplateManager
468
+ embedded
469
+ open
470
+ onOpenChange={() => undefined}
471
+ templates={templates.templates}
472
+ onCreate={templates.create}
473
+ onUpdate={templates.update}
474
+ onRemove={templates.remove}
475
+ />
476
+ </TabsContent>
477
+
478
+ <TabsContent value="signature" className="space-y-4">
479
+ <div className="flex flex-wrap items-end justify-between gap-3">
480
+ <div className="min-w-56 space-y-1.5">
481
+ <h3 className="font-semibold">Signature</h3>
482
+ <Select
483
+ value={
484
+ selectedAccountId
485
+ ? String(selectedAccountId)
486
+ : undefined
487
+ }
488
+ onValueChange={(value) =>
489
+ setSelectedAccountId(Number(value))
490
+ }
491
+ >
492
+ <SelectTrigger>
493
+ <SelectValue placeholder="Select mailbox" />
494
+ </SelectTrigger>
495
+ <SelectContent>
496
+ {accounts.map((account) => (
497
+ <SelectItem
498
+ key={account.id}
499
+ value={String(account.id)}
500
+ >
501
+ {account.email}
502
+ </SelectItem>
503
+ ))}
504
+ </SelectContent>
505
+ </Select>
506
+ </div>
507
+ </div>
508
+ {selectedAccount ? (
509
+ <MailSignatureManager
510
+ embedded
511
+ open
512
+ onOpenChange={() => undefined}
513
+ signatures={signatures.signatures}
514
+ onCreate={signatures.create}
515
+ onUpdate={signatures.update}
516
+ onRemove={signatures.remove}
517
+ onSetDefault={signatures.setDefault}
518
+ />
519
+ ) : (
520
+ <EmptySetting>Select a mailbox</EmptySetting>
521
+ )}
522
+ </TabsContent>
523
+ </div>
524
+ </Tabs>
525
+ </SheetContent>
526
+ </Sheet>
527
+
528
+ <Dialog
529
+ open={Boolean(senderAccount)}
530
+ onOpenChange={(nextOpen) => !nextOpen && setSenderAccount(undefined)}
531
+ >
532
+ <DialogContent className="sm:max-w-md">
533
+ <DialogHeader>
534
+ <DialogTitle>Sender name</DialogTitle>
535
+ </DialogHeader>
536
+ <div className="space-y-2">
537
+ <Label htmlFor="mail-sender-name">Display name</Label>
538
+ <Input
539
+ id="mail-sender-name"
540
+ value={senderName}
541
+ onChange={(event) => setSenderName(event.target.value)}
542
+ placeholder="Name shown to recipients"
543
+ />
544
+ <p className="text-xs text-muted-foreground">
545
+ Applies to {senderAccount?.email}. Outlook does not support this
546
+ setting.
547
+ </p>
548
+ </div>
549
+ <DialogFooter>
550
+ <Button
551
+ variant="outline"
552
+ onClick={() => setSenderAccount(undefined)}
553
+ >
554
+ Cancel
555
+ </Button>
556
+ <Button
557
+ disabled={savingSender}
558
+ onClick={() => void saveSenderName()}
559
+ >
560
+ {savingSender && <Loader2 className="animate-spin" />} Save
561
+ </Button>
562
+ </DialogFooter>
563
+ </DialogContent>
564
+ </Dialog>
565
+
566
+ <Dialog
567
+ open={Boolean(aliasAccount)}
568
+ onOpenChange={(nextOpen) => !nextOpen && setAliasAccount(undefined)}
569
+ >
570
+ <DialogContent className="sm:max-w-lg">
571
+ <DialogHeader>
572
+ <DialogTitle>Mailbox aliases</DialogTitle>
573
+ </DialogHeader>
574
+ <div className="space-y-4">
575
+ <div className="flex items-center justify-between gap-3 rounded-lg border bg-muted/30 p-3">
576
+ <div className="min-w-0">
577
+ <p className="truncate text-sm font-medium">
578
+ {aliasAccount?.email}
579
+ </p>
580
+ <p className="mt-0.5 text-xs text-muted-foreground">
581
+ Primary sender address
582
+ </p>
583
+ </div>
584
+ <Badge variant="outline">Primary</Badge>
585
+ </div>
586
+
587
+ {aliasAccount?.type === "microsoft" ? (
588
+ <div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
589
+ Outlook does not expose sender aliases through this integration.
590
+ </div>
591
+ ) : (
592
+ <>
593
+ <div className="flex items-center justify-between gap-3">
594
+ <div>
595
+ <h4 className="text-sm font-semibold">Available aliases</h4>
596
+ <p className="mt-0.5 text-xs text-muted-foreground">
597
+ Synced from Gmail and available in the sender selector.
598
+ </p>
599
+ </div>
600
+ <Button
601
+ variant="outline"
602
+ size="sm"
603
+ disabled={syncingAliases}
604
+ onClick={() => void syncAliases()}
605
+ >
606
+ <RefreshCw
607
+ className={syncingAliases ? "animate-spin" : undefined}
608
+ />
609
+ Sync aliases
610
+ </Button>
611
+ </div>
612
+ {aliasAccount?.identities?.filter(
613
+ (mailIdentity) =>
614
+ !mailIdentity.isPrimary &&
615
+ mailIdentity.email.toLocaleLowerCase() !==
616
+ aliasAccount.email.toLocaleLowerCase()
617
+ ).length ? (
618
+ <div className="divide-y overflow-hidden rounded-lg border">
619
+ {aliasAccount.identities
620
+ .filter(
621
+ (mailIdentity) =>
622
+ !mailIdentity.isPrimary &&
623
+ mailIdentity.email.toLocaleLowerCase() !==
624
+ aliasAccount.email.toLocaleLowerCase()
625
+ )
626
+ .map((mailIdentity) => (
627
+ <div
628
+ key={mailIdentity.id}
629
+ className="flex items-center gap-3 px-3 py-2.5"
630
+ >
631
+ <AtSign className="size-4 text-muted-foreground" />
632
+ <span className="min-w-0 flex-1 truncate text-sm">
633
+ {mailIdentity.email}
634
+ </span>
635
+ {mailIdentity.name ? (
636
+ <span className="truncate text-xs text-muted-foreground">
637
+ {mailIdentity.name}
638
+ </span>
639
+ ) : null}
640
+ </div>
641
+ ))}
642
+ </div>
643
+ ) : (
644
+ <EmptySetting>No aliases found</EmptySetting>
645
+ )}
646
+ </>
647
+ )}
648
+ </div>
649
+ <DialogFooter>
650
+ <Button variant="outline" onClick={() => setAliasAccount(undefined)}>
651
+ Close
652
+ </Button>
653
+ </DialogFooter>
654
+ </DialogContent>
655
+ </Dialog>
656
+
657
+ <AlertDialog
658
+ open={Boolean(resyncAccount)}
659
+ onOpenChange={(nextOpen) => !nextOpen && setResyncAccount(undefined)}
660
+ >
661
+ <AlertDialogContent>
662
+ <AlertDialogHeader>
663
+ <AlertDialogTitle>Resync this mailbox?</AlertDialogTitle>
664
+ <AlertDialogDescription>
665
+ This performs a full sync for {resyncAccount?.email}, may take
666
+ some time, and rebuilds its conversation threads.
667
+ </AlertDialogDescription>
668
+ </AlertDialogHeader>
669
+ <AlertDialogFooter>
670
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
671
+ <AlertDialogAction
672
+ disabled={Boolean(syncingEmail)}
673
+ onClick={() => void confirmResyncAccount()}
674
+ >
675
+ Resync
676
+ </AlertDialogAction>
677
+ </AlertDialogFooter>
678
+ </AlertDialogContent>
679
+ </AlertDialog>
680
+
681
+ <AlertDialog
682
+ open={Boolean(deleteAccount)}
683
+ onOpenChange={(nextOpen) => !nextOpen && setDeleteAccount(undefined)}
684
+ >
685
+ <AlertDialogContent>
686
+ <AlertDialogHeader>
687
+ <AlertDialogTitle>Delete this mailbox?</AlertDialogTitle>
688
+ <AlertDialogDescription>
689
+ This removes {deleteAccount?.email} and its locally stored
690
+ messages. The operation may take some time and cannot be undone
691
+ here.
692
+ </AlertDialogDescription>
693
+ </AlertDialogHeader>
694
+ <AlertDialogFooter>
695
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
696
+ <AlertDialogAction
697
+ variant="destructive"
698
+ disabled={Boolean(syncingEmail)}
699
+ onClick={() => void confirmDeleteAccount()}
700
+ >
701
+ Delete
702
+ </AlertDialogAction>
703
+ </AlertDialogFooter>
704
+ </AlertDialogContent>
705
+ </AlertDialog>
706
+ </>
707
+ );
708
+ }