@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.
- package/AGENTS.md +8 -0
- package/README.MD +4 -0
- package/dist/assets/{LineDraw-iG-ADes-.js → LineDraw-BBB5hwuJ.js} +1 -1
- package/dist/assets/{SymbolDraw-DXoSw1tM.js → SymbolDraw-CKS3uUb_.js} +1 -1
- package/dist/assets/{VisualMapping-C_ee4Eht.js → VisualMapping-BCwaW3TD.js} +1 -1
- package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
- package/dist/assets/{auto-login-provider-5Uxza9Ms.js → auto-login-provider-DO5Il6Dp.js} +2 -2
- package/dist/assets/auto-login-provider-TQ08B6-i.js +1 -0
- package/dist/assets/auto-redirect-provider-ButXp0A3.js +1 -0
- package/dist/assets/auto-redirect-provider-DnGGP2Vn.js +1 -0
- package/dist/assets/auto-redirect-provider-ja-vFPaF.js +1 -0
- package/dist/assets/calendar-C7xnRgOT.js +1 -0
- package/dist/assets/{cas-sign-in-button-CyqG1QME.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
- package/dist/assets/{components-D-ptYjX_.js → components-_g-GjQeV.js} +3 -3
- package/dist/assets/{configuration-gate-C08mkkiX.js → configuration-gate-B4XVz7tS.js} +1 -1
- package/dist/assets/{createClipPathFromCoordSys-wj34-b1B.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
- package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
- package/dist/assets/{customGraphicKeyframeAnimation-DWrhrHL_.js → customGraphicKeyframeAnimation-DMU5crAu.js} +1 -1
- package/dist/assets/demo-BPlae2bp.js +1 -0
- package/dist/assets/demo-Bj_Y0RrW.js +1 -0
- package/dist/assets/demo-CKPAQOib.js +1 -0
- package/dist/assets/demo-CXRTPvIx.js +1 -0
- package/dist/assets/demo-DJuVDu1n.js +1 -0
- package/dist/assets/demo-DUmWU7ik.js +1 -0
- package/dist/assets/demo-NhIzFRE1.js +1 -0
- package/dist/assets/{dingtalk-sign-in-button-DSWsSruv.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
- package/dist/assets/{echarts-preview-DxHNfq1C.js → echarts-preview-R96Pbqmz.js} +1 -1
- package/dist/assets/{echarts-runtime-D8kXn6ME.js → echarts-runtime-Ce_z2eX_.js} +3 -3
- package/dist/assets/{echarts-runtime-advanced-QY8v95qK.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
- package/dist/assets/{echarts-runtime-common-D-yqAK-r.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
- package/dist/assets/{echarts-runtime-components-P4-i8jPU.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
- package/dist/assets/{echarts-runtime-hierarchy-ByQZ-DbP.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
- package/dist/assets/{floating-BFVEyPEp.js → floating-O9WmiDXk.js} +2 -2
- package/dist/assets/index-BBKV6O5d.css +1 -0
- package/dist/assets/{index-Btk35Nx-.js → index-CPzehPGe.js} +3 -3
- package/dist/assets/index-Cc6awvdg.js +1064 -0
- package/dist/assets/index-ChA0z9ke.js +38 -0
- package/dist/assets/{index-um5hdKdZ.js → index-DgB7uTMs.js} +2 -2
- package/dist/assets/{index-CwNxd66J.js → index-DroUAK80.js} +1 -1
- package/dist/assets/index-WKvygTBp.js +59 -0
- package/dist/assets/{labelHelper-DQqRy3a_.js → labelHelper-Dk5jkiD-.js} +1 -1
- package/dist/assets/ldap-sign-in-form-CKN4l7qM.js +1 -0
- package/dist/assets/{oidc-sign-in-button-CQdRcR_p.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
- package/dist/assets/{page-context-CnQ4GYM7.js → page-context-DAiW_dId.js} +3 -3
- package/dist/assets/prompt-card-rGZqpqkF.js +1 -0
- package/dist/assets/prompt-output-B4h3uLWf.js +1 -0
- package/dist/assets/saml-sign-in-button-B4ugaLIb.js +1 -0
- package/dist/assets/{sectorHelper-DN_Vv6OL.js → sectorHelper-BuxrrJrA.js} +1 -1
- package/dist/assets/shortcut-BhKXxJLs.js +47 -0
- package/dist/assets/{sms-sign-in-form-DTX9GV2I.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
- package/dist/assets/{static.browser-jI1psK0d.js → static.browser-kzzpH0Xt.js} +8 -8
- package/dist/assets/switch-BdVxPIVU.js +1 -0
- package/dist/assets/{tool-cards-DFttLAfo.js → tool-cards-CpczIke-.js} +3 -3
- package/dist/assets/{use-authenticator-sign-in-CRGjzXju.js → use-authenticator-sign-in-CIj210ry.js} +1 -1
- package/dist/assets/use-dingtalk-sign-in-BW4kkXD3.js +1 -0
- package/dist/assets/{use-wecom-sign-in-C9j8wfCD.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
- package/dist/assets/wecom-sign-in-button-Dn_2zDab.js +1 -0
- package/dist/index.html +2 -2
- package/dist/index.raw.html +2 -2
- package/package.json +9 -1
- package/src/App.tsx +5 -2
- package/src/app/extension.ts +2 -0
- package/src/app/extensions.tsx +18 -6
- package/src/app/route-access-guard.tsx +22 -0
- package/src/app/route-runtime.ts +173 -0
- package/src/app/routes.tsx +2 -2
- package/src/components/app-shell/header.tsx +56 -1
- package/src/components/app-shell/sidebar.tsx +14 -2
- package/src/components/auth/sign-up-form.tsx +287 -59
- package/src/components/auth/types.ts +23 -1
- package/src/extensions/nocobase-acl/README.md +28 -14
- package/src/extensions/nocobase-acl/demo/index.tsx +87 -15
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +1 -0
- package/src/extensions/nocobase-mail/README.md +74 -0
- package/src/extensions/nocobase-mail/components/index.ts +79 -0
- package/src/extensions/nocobase-mail/components/mail-api.ts +423 -0
- package/src/extensions/nocobase-mail/components/mail-attachment-list.tsx +67 -0
- package/src/extensions/nocobase-mail/components/mail-compose-attachments.tsx +166 -0
- package/src/extensions/nocobase-mail/components/mail-compose.tsx +1071 -0
- package/src/extensions/nocobase-mail/components/mail-detail.tsx +653 -0
- package/src/extensions/nocobase-mail/components/mail-draft-autosave.ts +26 -0
- package/src/extensions/nocobase-mail/components/mail-empty.tsx +48 -0
- package/src/extensions/nocobase-mail/components/mail-filters.tsx +277 -0
- package/src/extensions/nocobase-mail/components/mail-inbox.tsx +544 -0
- package/src/extensions/nocobase-mail/components/mail-inline-images.ts +69 -0
- package/src/extensions/nocobase-mail/components/mail-label-badge.tsx +25 -0
- package/src/extensions/nocobase-mail/components/mail-label-manager.tsx +271 -0
- package/src/extensions/nocobase-mail/components/mail-labels-editor.tsx +267 -0
- package/src/extensions/nocobase-mail/components/mail-mass-tracking.tsx +271 -0
- package/src/extensions/nocobase-mail/components/mail-note-editor.tsx +99 -0
- package/src/extensions/nocobase-mail/components/mail-recipient-input.tsx +260 -0
- package/src/extensions/nocobase-mail/components/mail-reply-quote.ts +28 -0
- package/src/extensions/nocobase-mail/components/mail-rich-editor.tsx +287 -0
- package/src/extensions/nocobase-mail/components/mail-send-actions.tsx +238 -0
- package/src/extensions/nocobase-mail/components/mail-senders.ts +61 -0
- package/src/extensions/nocobase-mail/components/mail-settings-drawer.tsx +708 -0
- package/src/extensions/nocobase-mail/components/mail-showcase-layout.tsx +64 -0
- package/src/extensions/nocobase-mail/components/mail-signature-manager.tsx +258 -0
- package/src/extensions/nocobase-mail/components/mail-signatures.ts +25 -0
- package/src/extensions/nocobase-mail/components/mail-table.tsx +476 -0
- package/src/extensions/nocobase-mail/components/mail-template-manager.tsx +204 -0
- package/src/extensions/nocobase-mail/components/mail-toolbar.tsx +155 -0
- package/src/extensions/nocobase-mail/components/mail-unread.tsx +111 -0
- package/src/extensions/nocobase-mail/components/types.ts +297 -0
- package/src/extensions/nocobase-mail/components/use-mail-compose.tsx +262 -0
- package/src/extensions/nocobase-mail/components/use-mail-mass-messages.ts +54 -0
- package/src/extensions/nocobase-mail/components/use-mail-messages.ts +113 -0
- package/src/extensions/nocobase-mail/components/use-mail-signatures.ts +122 -0
- package/src/extensions/nocobase-mail/components/use-mail-templates.ts +54 -0
- package/src/extensions/nocobase-mail/extension.tsx +105 -0
- package/src/extensions/nocobase-mail/index.ts +1 -0
- package/src/extensions/nocobase-mail/mail-demo-pages.tsx +503 -0
- package/src/extensions/nocobase-mail/mail-pages.tsx +201 -0
- package/src/extensions/nocobase-mail/tests/mail-quote-html-regression.mjs +51 -0
- package/src/extensions/nocobase-mail/tests/mail-regression.mjs +649 -0
- package/src/extensions/nocobase-users-example/app-routes.tsx +121 -0
- package/src/extensions/nocobase-users-example/extension.tsx +2 -100
- package/src/extensions/nocobase-users-example/index.ts +1 -0
- package/src/lib/nocobase/acl/menu.ts +17 -3
- package/src/lib/nocobase/acl/types.ts +4 -0
- package/src/locales/en-US.ts +2 -1
- package/src/locales/zh-CN.ts +2 -1
- package/src/providers/runtime-config.ts +18 -0
- package/src/routes.tsx +10 -0
- package/src/vite-env.d.ts +3 -0
- package/vite.config.ts +8 -0
- package/dist/assets/auth-method-demo-DaOCuvsq.js +0 -1
- package/dist/assets/auto-login-provider-DyaPGK2D.js +0 -1
- package/dist/assets/auto-redirect-provider-CfwRvYOI.js +0 -1
- package/dist/assets/auto-redirect-provider-Cr3jmYts.js +0 -1
- package/dist/assets/auto-redirect-provider-QkJQRBbG.js +0 -1
- package/dist/assets/calendar-DjDRfD5t.js +0 -1
- package/dist/assets/card-DuQBOS6A.js +0 -1
- package/dist/assets/createSeriesDataSimply-D0A6SAVQ.js +0 -1
- package/dist/assets/demo-BYWHeKUe.js +0 -1
- package/dist/assets/demo-CPWoNtx8.js +0 -1
- package/dist/assets/demo-CyvJk5zV.js +0 -1
- package/dist/assets/demo-D4OiKplc.js +0 -1
- package/dist/assets/demo-DBueJg0m.js +0 -1
- package/dist/assets/demo-DkIhyiRK.js +0 -1
- package/dist/assets/demo-tpTjO0sC.js +0 -1
- package/dist/assets/index-BgCbGDMc.js +0 -43
- package/dist/assets/index-MB-jfQld.js +0 -686
- package/dist/assets/index-NH3aqqQz.css +0 -1
- package/dist/assets/index-gFtCXH3o.js +0 -58
- package/dist/assets/ldap-sign-in-form--6Gt3yGq.js +0 -1
- package/dist/assets/prompt-card-B2RS4nt2.js +0 -1
- package/dist/assets/prompt-output-DZHRM2ft.js +0 -1
- package/dist/assets/saml-sign-in-button-BHL5a0JV.js +0 -1
- package/dist/assets/shortcut-C5x5LQWU.js +0 -47
- package/dist/assets/switch-CRL8PmRI.js +0 -1
- package/dist/assets/use-dingtalk-sign-in-8WYzJ2jf.js +0 -1
- package/dist/assets/wecom-sign-in-button-mIS1zGSU.js +0 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Loader2, Plus, Trash2 } from "lucide-react";
|
|
3
|
+
import { toast } from "sonner";
|
|
4
|
+
import { mailApi } from "./mail-api";
|
|
5
|
+
import type { MailLabel } from "./types";
|
|
6
|
+
import { LABEL_COLOR_OPTIONS, LABEL_SWATCH_CLASSES } from "./types";
|
|
7
|
+
import { Button } from "@/components/ui/button";
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
} from "@/components/ui/dialog";
|
|
14
|
+
import { Input } from "@/components/ui/input";
|
|
15
|
+
import { Label } from "@/components/ui/label";
|
|
16
|
+
import {
|
|
17
|
+
AlertDialog,
|
|
18
|
+
AlertDialogAction,
|
|
19
|
+
AlertDialogCancel,
|
|
20
|
+
AlertDialogContent,
|
|
21
|
+
AlertDialogDescription,
|
|
22
|
+
AlertDialogFooter,
|
|
23
|
+
AlertDialogHeader,
|
|
24
|
+
AlertDialogTitle,
|
|
25
|
+
} from "@/components/ui/alert-dialog";
|
|
26
|
+
import { cn } from "@/lib/utils";
|
|
27
|
+
|
|
28
|
+
type LabelValues = Pick<MailLabel, "label" | "color" | "description">;
|
|
29
|
+
const EMPTY: LabelValues = { label: "", color: "default", description: "" };
|
|
30
|
+
|
|
31
|
+
export interface MailLabelManagerProps {
|
|
32
|
+
open: boolean;
|
|
33
|
+
onOpenChange: (open: boolean) => void;
|
|
34
|
+
labels: MailLabel[];
|
|
35
|
+
onChange: (labels: MailLabel[]) => void;
|
|
36
|
+
embedded?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function MailLabelManager({
|
|
40
|
+
open,
|
|
41
|
+
onOpenChange,
|
|
42
|
+
labels,
|
|
43
|
+
onChange,
|
|
44
|
+
embedded = false,
|
|
45
|
+
}: MailLabelManagerProps) {
|
|
46
|
+
const [selectedId, setSelectedId] = useState<number | null>(null);
|
|
47
|
+
const [form, setForm] = useState<LabelValues>(EMPTY);
|
|
48
|
+
const [busy, setBusy] = useState(false);
|
|
49
|
+
const [confirmDeleteId, setConfirmDeleteId] = useState<number | null>(null);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!open) return;
|
|
53
|
+
const first = labels[0];
|
|
54
|
+
setSelectedId(first?.id ?? null);
|
|
55
|
+
setForm(
|
|
56
|
+
first
|
|
57
|
+
? {
|
|
58
|
+
label: first.label,
|
|
59
|
+
color: first.color,
|
|
60
|
+
description: first.description ?? "",
|
|
61
|
+
}
|
|
62
|
+
: EMPTY
|
|
63
|
+
);
|
|
64
|
+
// Initialize once per opening; keep the current edit stable after a mutation.
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
}, [open]);
|
|
67
|
+
|
|
68
|
+
const select = (label: MailLabel) => {
|
|
69
|
+
setSelectedId(label.id);
|
|
70
|
+
setForm({
|
|
71
|
+
label: label.label,
|
|
72
|
+
color: label.color,
|
|
73
|
+
description: label.description ?? "",
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const save = async () => {
|
|
78
|
+
const name = form.label.trim();
|
|
79
|
+
if (!name) return;
|
|
80
|
+
setBusy(true);
|
|
81
|
+
try {
|
|
82
|
+
if (selectedId === null) {
|
|
83
|
+
const created = await mailApi.createLabel({ ...form, label: name });
|
|
84
|
+
onChange([...labels, created]);
|
|
85
|
+
setSelectedId(created.id);
|
|
86
|
+
} else {
|
|
87
|
+
const updated = await mailApi.updateLabel(selectedId, {
|
|
88
|
+
...form,
|
|
89
|
+
label: name,
|
|
90
|
+
});
|
|
91
|
+
onChange(
|
|
92
|
+
labels.map((item) =>
|
|
93
|
+
item.id === selectedId ? { ...item, ...updated } : item
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
toast.success("Label saved");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
toast.error(
|
|
100
|
+
error instanceof Error ? error.message : "Failed to save label"
|
|
101
|
+
);
|
|
102
|
+
} finally {
|
|
103
|
+
setBusy(false);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const remove = async () => {
|
|
108
|
+
if (confirmDeleteId === null) return;
|
|
109
|
+
setBusy(true);
|
|
110
|
+
try {
|
|
111
|
+
await mailApi.deleteLabel(confirmDeleteId);
|
|
112
|
+
onChange(labels.filter((item) => item.id !== confirmDeleteId));
|
|
113
|
+
if (selectedId === confirmDeleteId) {
|
|
114
|
+
setSelectedId(null);
|
|
115
|
+
setForm(EMPTY);
|
|
116
|
+
}
|
|
117
|
+
setConfirmDeleteId(null);
|
|
118
|
+
toast.success("Label deleted");
|
|
119
|
+
} catch (error) {
|
|
120
|
+
toast.error(
|
|
121
|
+
error instanceof Error ? error.message : "Failed to delete label"
|
|
122
|
+
);
|
|
123
|
+
} finally {
|
|
124
|
+
setBusy(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const editor = (
|
|
129
|
+
<div className="grid gap-4 sm:grid-cols-[180px_1fr]">
|
|
130
|
+
<div className="flex flex-col gap-2">
|
|
131
|
+
<div className="flex max-h-80 flex-col gap-0.5 overflow-y-auto">
|
|
132
|
+
{labels.map((label) => (
|
|
133
|
+
<div
|
|
134
|
+
key={label.id}
|
|
135
|
+
className={cn(
|
|
136
|
+
"group flex items-center rounded-md text-sm hover:bg-muted/60",
|
|
137
|
+
selectedId === label.id && "bg-muted font-medium"
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={() => select(label)}
|
|
143
|
+
className="flex min-w-0 flex-1 items-center gap-2 px-2.5 py-2 text-left"
|
|
144
|
+
>
|
|
145
|
+
<span
|
|
146
|
+
className={cn(
|
|
147
|
+
"size-2.5 shrink-0 rounded-full",
|
|
148
|
+
LABEL_SWATCH_CLASSES[label.color] ??
|
|
149
|
+
LABEL_SWATCH_CLASSES.default
|
|
150
|
+
)}
|
|
151
|
+
/>
|
|
152
|
+
<span className="truncate">{label.label}</span>
|
|
153
|
+
</button>
|
|
154
|
+
<Button
|
|
155
|
+
variant="ghost"
|
|
156
|
+
size="icon-sm"
|
|
157
|
+
title={`Delete ${label.label}`}
|
|
158
|
+
aria-label={`Delete ${label.label}`}
|
|
159
|
+
disabled={busy}
|
|
160
|
+
onClick={() => setConfirmDeleteId(label.id)}
|
|
161
|
+
className="mr-1 shrink-0 text-muted-foreground opacity-60 hover:text-destructive sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100"
|
|
162
|
+
>
|
|
163
|
+
<Trash2 />
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
<Button
|
|
169
|
+
variant="outline"
|
|
170
|
+
size="sm"
|
|
171
|
+
onClick={() => {
|
|
172
|
+
setSelectedId(null);
|
|
173
|
+
setForm(EMPTY);
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
<Plus /> New
|
|
177
|
+
</Button>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex flex-col gap-3">
|
|
180
|
+
<div className="space-y-1.5">
|
|
181
|
+
<Label>Name</Label>
|
|
182
|
+
<Input
|
|
183
|
+
value={form.label}
|
|
184
|
+
onChange={(event) =>
|
|
185
|
+
setForm((prev) => ({ ...prev, label: event.target.value }))
|
|
186
|
+
}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="space-y-1.5">
|
|
190
|
+
<Label>Description</Label>
|
|
191
|
+
<Input
|
|
192
|
+
value={form.description ?? ""}
|
|
193
|
+
onChange={(event) =>
|
|
194
|
+
setForm((prev) => ({ ...prev, description: event.target.value }))
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="space-y-2">
|
|
199
|
+
<Label>Color</Label>
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
{LABEL_COLOR_OPTIONS.map((color) => (
|
|
202
|
+
<button
|
|
203
|
+
key={color}
|
|
204
|
+
type="button"
|
|
205
|
+
title={color}
|
|
206
|
+
onClick={() => setForm((prev) => ({ ...prev, color }))}
|
|
207
|
+
className={cn(
|
|
208
|
+
"size-5 rounded-full",
|
|
209
|
+
LABEL_SWATCH_CLASSES[color],
|
|
210
|
+
form.color === color &&
|
|
211
|
+
"ring-2 ring-foreground ring-offset-2 ring-offset-background"
|
|
212
|
+
)}
|
|
213
|
+
/>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div className="mt-2 flex items-center justify-end">
|
|
218
|
+
<Button
|
|
219
|
+
size="sm"
|
|
220
|
+
disabled={!form.label.trim() || busy}
|
|
221
|
+
onClick={() => void save()}
|
|
222
|
+
>
|
|
223
|
+
{busy && <Loader2 className="animate-spin" />} Save
|
|
224
|
+
</Button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<>
|
|
232
|
+
{embedded ? (
|
|
233
|
+
editor
|
|
234
|
+
) : (
|
|
235
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
236
|
+
<DialogContent className="sm:max-w-2xl">
|
|
237
|
+
<DialogHeader>
|
|
238
|
+
<DialogTitle>Manage labels</DialogTitle>
|
|
239
|
+
</DialogHeader>
|
|
240
|
+
{editor}
|
|
241
|
+
</DialogContent>
|
|
242
|
+
</Dialog>
|
|
243
|
+
)}
|
|
244
|
+
<AlertDialog
|
|
245
|
+
open={confirmDeleteId !== null}
|
|
246
|
+
onOpenChange={(nextOpen) => {
|
|
247
|
+
if (!nextOpen) setConfirmDeleteId(null);
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
<AlertDialogContent>
|
|
251
|
+
<AlertDialogHeader>
|
|
252
|
+
<AlertDialogTitle>Delete this label?</AlertDialogTitle>
|
|
253
|
+
<AlertDialogDescription>
|
|
254
|
+
This removes the label from every message that uses it.
|
|
255
|
+
</AlertDialogDescription>
|
|
256
|
+
</AlertDialogHeader>
|
|
257
|
+
<AlertDialogFooter>
|
|
258
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
259
|
+
<AlertDialogAction
|
|
260
|
+
variant="destructive"
|
|
261
|
+
disabled={busy}
|
|
262
|
+
onClick={() => void remove()}
|
|
263
|
+
>
|
|
264
|
+
Delete
|
|
265
|
+
</AlertDialogAction>
|
|
266
|
+
</AlertDialogFooter>
|
|
267
|
+
</AlertDialogContent>
|
|
268
|
+
</AlertDialog>
|
|
269
|
+
</>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useGetIdentity } from "@refinedev/core";
|
|
3
|
+
import { Check, Loader2, Plus, Settings2, Tag as TagIcon } from "lucide-react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import type { MailLabel, MailMessage } from "./types";
|
|
6
|
+
import { LABEL_COLOR_OPTIONS, LABEL_SWATCH_CLASSES } from "./types";
|
|
7
|
+
import { mailApi } from "./mail-api";
|
|
8
|
+
import { MailLabelBadge } from "./mail-label-badge";
|
|
9
|
+
import { MailLabelManager } from "./mail-label-manager";
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { Input } from "@/components/ui/input";
|
|
12
|
+
import {
|
|
13
|
+
Popover,
|
|
14
|
+
PopoverContent,
|
|
15
|
+
PopoverTrigger,
|
|
16
|
+
} from "@/components/ui/popover";
|
|
17
|
+
import { cn } from "@/lib/utils";
|
|
18
|
+
|
|
19
|
+
export function MailLabelsEditor({
|
|
20
|
+
message,
|
|
21
|
+
onChange,
|
|
22
|
+
}: {
|
|
23
|
+
message: MailMessage;
|
|
24
|
+
onChange?: (labels: MailLabel[]) => void;
|
|
25
|
+
}) {
|
|
26
|
+
const { data: identity } = useGetIdentity<{ id: number | string }>();
|
|
27
|
+
const userId = identity?.id;
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const [allLabels, setAllLabels] = useState<MailLabel[]>([]);
|
|
30
|
+
const [loading, setLoading] = useState(false);
|
|
31
|
+
const [selectedIds, setSelectedIds] = useState<number[]>(
|
|
32
|
+
message.labels?.map((l) => l.id) ?? []
|
|
33
|
+
);
|
|
34
|
+
const [newLabel, setNewLabel] = useState("");
|
|
35
|
+
const [newColor, setNewColor] = useState<string>("default");
|
|
36
|
+
const [creating, setCreating] = useState(false);
|
|
37
|
+
const [managerOpen, setManagerOpen] = useState(false);
|
|
38
|
+
const commitQueue = useRef<Promise<unknown>>(Promise.resolve());
|
|
39
|
+
const commitSequence = useRef(0);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setSelectedIds(message.labels?.map((l) => l.id) ?? []);
|
|
43
|
+
}, [message.labels]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!open) return;
|
|
47
|
+
let active = true;
|
|
48
|
+
setLoading(true);
|
|
49
|
+
mailApi
|
|
50
|
+
.getLabels(userId)
|
|
51
|
+
.then((labels) => active && setAllLabels(labels))
|
|
52
|
+
.catch(() => active && setAllLabels([]))
|
|
53
|
+
.finally(() => active && setLoading(false));
|
|
54
|
+
return () => {
|
|
55
|
+
active = false;
|
|
56
|
+
};
|
|
57
|
+
}, [open, userId]);
|
|
58
|
+
|
|
59
|
+
const resolveLabels = (ids: number[], from?: MailLabel[]): MailLabel[] => {
|
|
60
|
+
const source = from ?? allLabels;
|
|
61
|
+
return ids
|
|
62
|
+
.map(
|
|
63
|
+
(id) =>
|
|
64
|
+
source.find((l) => l.id === id) ??
|
|
65
|
+
message.labels?.find((l) => l.id === id)
|
|
66
|
+
)
|
|
67
|
+
.filter((l): l is MailLabel => Boolean(l));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const commit = (
|
|
71
|
+
ids: number[],
|
|
72
|
+
from?: MailLabel[],
|
|
73
|
+
rollbackIds = selectedIds
|
|
74
|
+
) => {
|
|
75
|
+
const sequence = ++commitSequence.current;
|
|
76
|
+
const request = commitQueue.current
|
|
77
|
+
.catch(() => undefined)
|
|
78
|
+
.then(() => mailApi.setMessageLabels(message.id, ids));
|
|
79
|
+
commitQueue.current = request;
|
|
80
|
+
return request
|
|
81
|
+
.then(() => {
|
|
82
|
+
if (sequence === commitSequence.current) {
|
|
83
|
+
onChange?.(resolveLabels(ids, from));
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
.catch((error) => {
|
|
87
|
+
if (sequence === commitSequence.current) {
|
|
88
|
+
setSelectedIds(rollbackIds);
|
|
89
|
+
onChange?.(resolveLabels(rollbackIds, from));
|
|
90
|
+
toast.error(
|
|
91
|
+
error instanceof Error ? error.message : "Failed to update labels"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const toggle = (id: number) => {
|
|
98
|
+
const next = selectedIds.includes(id)
|
|
99
|
+
? selectedIds.filter((x) => x !== id)
|
|
100
|
+
: [...selectedIds, id];
|
|
101
|
+
setSelectedIds(next);
|
|
102
|
+
void commit(next, undefined, selectedIds);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const createLabel = async () => {
|
|
106
|
+
const name = newLabel.trim();
|
|
107
|
+
if (!name) return;
|
|
108
|
+
setCreating(true);
|
|
109
|
+
try {
|
|
110
|
+
const label = await mailApi.createLabel({ label: name, color: newColor });
|
|
111
|
+
const updatedAll = [...allLabels, label];
|
|
112
|
+
setAllLabels(updatedAll);
|
|
113
|
+
const next = [...selectedIds, label.id];
|
|
114
|
+
setSelectedIds(next);
|
|
115
|
+
await commit(next, updatedAll);
|
|
116
|
+
setNewLabel("");
|
|
117
|
+
setNewColor("default");
|
|
118
|
+
} catch (error) {
|
|
119
|
+
toast.error(
|
|
120
|
+
error instanceof Error ? error.message : "Failed to create label"
|
|
121
|
+
);
|
|
122
|
+
} finally {
|
|
123
|
+
setCreating(false);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleLabelsManaged = (labels: MailLabel[]) => {
|
|
128
|
+
setAllLabels(labels);
|
|
129
|
+
const validIds = selectedIds.filter((id) => labels.some((label) => label.id === id));
|
|
130
|
+
if (validIds.length !== selectedIds.length) {
|
|
131
|
+
setSelectedIds(validIds);
|
|
132
|
+
void commit(validIds, labels, selectedIds);
|
|
133
|
+
} else {
|
|
134
|
+
onChange?.(resolveLabels(validIds, labels));
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<>
|
|
140
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
141
|
+
<PopoverTrigger
|
|
142
|
+
render={
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
className="inline-flex flex-wrap items-center gap-1.5 rounded-md py-0.5 transition-opacity hover:opacity-80"
|
|
146
|
+
/>
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
{message.labels?.length ? (
|
|
150
|
+
message.labels.map((label) => (
|
|
151
|
+
<MailLabelBadge key={label.id} label={label} />
|
|
152
|
+
))
|
|
153
|
+
) : (
|
|
154
|
+
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground">
|
|
155
|
+
<TagIcon className="size-3" />
|
|
156
|
+
Add label
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</PopoverTrigger>
|
|
160
|
+
<PopoverContent align="start" className="w-64 gap-2">
|
|
161
|
+
<div className="px-1 text-xs font-semibold text-muted-foreground">
|
|
162
|
+
Labels
|
|
163
|
+
</div>
|
|
164
|
+
<div className="flex max-h-52 flex-col gap-0.5 overflow-y-auto">
|
|
165
|
+
{loading ? (
|
|
166
|
+
<div className="flex items-center justify-center py-4">
|
|
167
|
+
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
|
168
|
+
</div>
|
|
169
|
+
) : allLabels.length ? (
|
|
170
|
+
allLabels.map((label) => {
|
|
171
|
+
const checked = selectedIds.includes(label.id);
|
|
172
|
+
return (
|
|
173
|
+
<div
|
|
174
|
+
key={label.id}
|
|
175
|
+
role="button"
|
|
176
|
+
tabIndex={0}
|
|
177
|
+
onClick={() => toggle(label.id)}
|
|
178
|
+
onKeyDown={(e) => {
|
|
179
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
toggle(label.id);
|
|
182
|
+
}
|
|
183
|
+
}}
|
|
184
|
+
className={cn(
|
|
185
|
+
"flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-muted/60",
|
|
186
|
+
checked && "bg-muted/40"
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
<span
|
|
190
|
+
className={cn(
|
|
191
|
+
"flex size-4 shrink-0 items-center justify-center rounded-[4px] border transition-colors",
|
|
192
|
+
checked
|
|
193
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
194
|
+
: "border-input"
|
|
195
|
+
)}
|
|
196
|
+
>
|
|
197
|
+
{checked && <Check className="size-3" />}
|
|
198
|
+
</span>
|
|
199
|
+
<MailLabelBadge label={label} />
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
})
|
|
203
|
+
) : (
|
|
204
|
+
<div className="py-3 text-center text-xs text-muted-foreground">
|
|
205
|
+
No labels yet
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
<div className="border-t border-border/60 pt-2">
|
|
210
|
+
<div className="mb-1.5 flex items-center gap-1.5">
|
|
211
|
+
{LABEL_COLOR_OPTIONS.map((color) => (
|
|
212
|
+
<button
|
|
213
|
+
key={color}
|
|
214
|
+
type="button"
|
|
215
|
+
title={color}
|
|
216
|
+
onClick={() => setNewColor(color)}
|
|
217
|
+
className={cn(
|
|
218
|
+
"size-4 rounded-full transition-all",
|
|
219
|
+
LABEL_SWATCH_CLASSES[color],
|
|
220
|
+
newColor === color
|
|
221
|
+
? "ring-2 ring-foreground ring-offset-1 ring-offset-background"
|
|
222
|
+
: "hover:scale-110"
|
|
223
|
+
)}
|
|
224
|
+
/>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
<div className="flex items-center gap-1.5">
|
|
228
|
+
<Input
|
|
229
|
+
value={newLabel}
|
|
230
|
+
onChange={(e) => setNewLabel(e.target.value)}
|
|
231
|
+
onKeyDown={(e) => {
|
|
232
|
+
if (e.key === "Enter") {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
void createLabel();
|
|
235
|
+
}
|
|
236
|
+
}}
|
|
237
|
+
placeholder="New label…"
|
|
238
|
+
className="h-8 text-sm"
|
|
239
|
+
/>
|
|
240
|
+
<Button
|
|
241
|
+
size="icon-sm"
|
|
242
|
+
onClick={() => void createLabel()}
|
|
243
|
+
disabled={creating || !newLabel.trim()}
|
|
244
|
+
>
|
|
245
|
+
{creating ? <Loader2 className="animate-spin" /> : <Plus />}
|
|
246
|
+
</Button>
|
|
247
|
+
</div>
|
|
248
|
+
<Button
|
|
249
|
+
variant="ghost"
|
|
250
|
+
size="sm"
|
|
251
|
+
className="mt-2 w-full justify-start"
|
|
252
|
+
onClick={() => { setOpen(false); setManagerOpen(true); }}
|
|
253
|
+
>
|
|
254
|
+
<Settings2 /> Manage labels
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
</PopoverContent>
|
|
258
|
+
</Popover>
|
|
259
|
+
<MailLabelManager
|
|
260
|
+
open={managerOpen}
|
|
261
|
+
onOpenChange={setManagerOpen}
|
|
262
|
+
labels={allLabels}
|
|
263
|
+
onChange={handleLabelsManaged}
|
|
264
|
+
/>
|
|
265
|
+
</>
|
|
266
|
+
);
|
|
267
|
+
}
|