@nocobase/portal-template-default 1.0.16 → 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-vPRTWiQ5.js → LineDraw-BBB5hwuJ.js} +1 -1
- package/dist/assets/{SymbolDraw-DOl_Tss3.js → SymbolDraw-CKS3uUb_.js} +1 -1
- package/dist/assets/{VisualMapping-j08W3KBm.js → VisualMapping-BCwaW3TD.js} +1 -1
- package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
- package/dist/assets/{auto-login-provider-Bukw7zEe.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-BQn3Mttx.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
- package/dist/assets/{components-BT5mL9gW.js → components-_g-GjQeV.js} +3 -3
- package/dist/assets/{configuration-gate-BJoadoA6.js → configuration-gate-B4XVz7tS.js} +1 -1
- package/dist/assets/{createClipPathFromCoordSys-m8IbnJ1z.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
- package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
- package/dist/assets/{customGraphicKeyframeAnimation-CYLPurwK.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-DSluFaP9.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
- package/dist/assets/{echarts-preview-mRXoEciY.js → echarts-preview-R96Pbqmz.js} +1 -1
- package/dist/assets/{echarts-runtime-CvLr4Slh.js → echarts-runtime-Ce_z2eX_.js} +3 -3
- package/dist/assets/{echarts-runtime-advanced-BC1tvfQ0.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
- package/dist/assets/{echarts-runtime-common-DhNaV5B8.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
- package/dist/assets/{echarts-runtime-components-BiioS8t1.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
- package/dist/assets/{echarts-runtime-hierarchy-CAWqYvFh.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
- package/dist/assets/{floating-RVbHvIiD.js → floating-O9WmiDXk.js} +2 -2
- package/dist/assets/index-BBKV6O5d.css +1 -0
- package/dist/assets/{index-Dw20Lzpp.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-BysibBZu.js → index-DgB7uTMs.js} +2 -2
- package/dist/assets/{index-C-tdHirk.js → index-DroUAK80.js} +1 -1
- package/dist/assets/index-WKvygTBp.js +59 -0
- package/dist/assets/{labelHelper-DbITmOUW.js → labelHelper-Dk5jkiD-.js} +1 -1
- package/dist/assets/ldap-sign-in-form-CKN4l7qM.js +1 -0
- package/dist/assets/{oidc-sign-in-button-Cgpa8Qzj.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
- package/dist/assets/{page-context-AU6RU0Qi.js → page-context-DAiW_dId.js} +4 -4
- 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-C6L782Ue.js → sectorHelper-BuxrrJrA.js} +1 -1
- package/dist/assets/shortcut-BhKXxJLs.js +47 -0
- package/dist/assets/{sms-sign-in-form-Bo2WLZuP.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
- package/dist/assets/{static.browser-CoPxsICt.js → static.browser-kzzpH0Xt.js} +1 -1
- package/dist/assets/switch-BdVxPIVU.js +1 -0
- package/dist/assets/{tool-cards-ipgJ_3Ec.js → tool-cards-CpczIke-.js} +3 -3
- package/dist/assets/{use-authenticator-sign-in-BzFeh7qe.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-PPIMLvGa.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
- package/dist/assets/{wecom-sign-in-button-rUq9RW8R.js → wecom-sign-in-button-Dn_2zDab.js} +1 -1
- 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/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-B2qcpY8z.js +0 -1
- package/dist/assets/auto-login-provider-BMtzvdxG.js +0 -1
- package/dist/assets/auto-redirect-provider-C6GNPg5r.js +0 -1
- package/dist/assets/auto-redirect-provider-D5c6kN2R.js +0 -1
- package/dist/assets/auto-redirect-provider-iTzGEDpU.js +0 -1
- package/dist/assets/calendar-Dv2RNnsr.js +0 -1
- package/dist/assets/card-C9Hpeq1c.js +0 -1
- package/dist/assets/createSeriesDataSimply-DKUV5b5e.js +0 -1
- package/dist/assets/demo-BWGOwqtJ.js +0 -1
- package/dist/assets/demo-BWbcvkya.js +0 -1
- package/dist/assets/demo-DIASSPgn.js +0 -1
- package/dist/assets/demo-DuZ6asUU.js +0 -1
- package/dist/assets/demo-Dxcy4bDE.js +0 -1
- package/dist/assets/demo-YpEC76uw.js +0 -1
- package/dist/assets/demo-wzkoR8T2.js +0 -1
- package/dist/assets/index-CBZbj45X.js +0 -58
- package/dist/assets/index-CgkdZ6Oc.js +0 -686
- package/dist/assets/index-NH3aqqQz.css +0 -1
- package/dist/assets/index-hSBzW-VA.js +0 -43
- package/dist/assets/ldap-sign-in-form-BOMNDTkK.js +0 -1
- package/dist/assets/prompt-card-FWugRgi-.js +0 -1
- package/dist/assets/prompt-output-DMTuTNab.js +0 -1
- package/dist/assets/saml-sign-in-button-qt8bMeoQ.js +0 -1
- package/dist/assets/shortcut-Bu7jT_-w.js +0 -47
- package/dist/assets/switch-DQkkIE7G.js +0 -1
- package/dist/assets/use-dingtalk-sign-in-mrq6K5Yx.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./components";
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ListFilter,
|
|
4
|
+
MessagesSquare,
|
|
5
|
+
PanelsLeftRight,
|
|
6
|
+
PenLine,
|
|
7
|
+
RefreshCw,
|
|
8
|
+
Send,
|
|
9
|
+
Users,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { Link } from "react-router";
|
|
12
|
+
import {
|
|
13
|
+
MailInbox,
|
|
14
|
+
MailUnreadIndicator,
|
|
15
|
+
mailApi,
|
|
16
|
+
useMailUnread,
|
|
17
|
+
useMailCompose,
|
|
18
|
+
type MailColumnId,
|
|
19
|
+
type MailScope,
|
|
20
|
+
type MailUserRecord,
|
|
21
|
+
} from "./components";
|
|
22
|
+
import {
|
|
23
|
+
MailShowcasePage,
|
|
24
|
+
MailShowcaseSection,
|
|
25
|
+
} from "./components/mail-showcase-layout";
|
|
26
|
+
import { Button } from "@/components/ui/button";
|
|
27
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
28
|
+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
29
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
30
|
+
import {
|
|
31
|
+
Sheet,
|
|
32
|
+
SheetContent,
|
|
33
|
+
SheetDescription,
|
|
34
|
+
SheetHeader,
|
|
35
|
+
SheetTitle,
|
|
36
|
+
} from "@/components/ui/sheet";
|
|
37
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
38
|
+
import {
|
|
39
|
+
Table,
|
|
40
|
+
TableBody,
|
|
41
|
+
TableCell,
|
|
42
|
+
TableHead,
|
|
43
|
+
TableHeader,
|
|
44
|
+
TableRow,
|
|
45
|
+
} from "@/components/ui/table";
|
|
46
|
+
|
|
47
|
+
const PERSONAL_COLUMNS: MailColumnId[] = [
|
|
48
|
+
"from",
|
|
49
|
+
"to",
|
|
50
|
+
"subject",
|
|
51
|
+
"boxType",
|
|
52
|
+
"date",
|
|
53
|
+
"isRead",
|
|
54
|
+
"labels",
|
|
55
|
+
];
|
|
56
|
+
const ALL_COLUMNS: MailColumnId[] = [
|
|
57
|
+
"from",
|
|
58
|
+
"user",
|
|
59
|
+
"email",
|
|
60
|
+
"subject",
|
|
61
|
+
"boxType",
|
|
62
|
+
"date",
|
|
63
|
+
"isRead",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const scenarios = [
|
|
67
|
+
{
|
|
68
|
+
title: "My mailbox",
|
|
69
|
+
description:
|
|
70
|
+
"A personal mailbox with folders, filters, and message actions.",
|
|
71
|
+
path: "workspace",
|
|
72
|
+
icon: PanelsLeftRight,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: "Mailbox views",
|
|
76
|
+
description:
|
|
77
|
+
"Switch between the current user's inbox and all connected mailboxes.",
|
|
78
|
+
path: "personal",
|
|
79
|
+
icon: Users,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
title: "Unread indicator",
|
|
83
|
+
description:
|
|
84
|
+
"Show a live unread count in navigation, buttons, or mailbox summaries.",
|
|
85
|
+
path: "unread",
|
|
86
|
+
icon: MessagesSquare,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: "Compose page",
|
|
90
|
+
description: "A standalone compose route with query-string prefilling.",
|
|
91
|
+
path: "compose",
|
|
92
|
+
icon: PenLine,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: "Correspondence per user",
|
|
96
|
+
description:
|
|
97
|
+
"Select a user and inspect all messages exchanged with that address.",
|
|
98
|
+
path: "filtered",
|
|
99
|
+
icon: ListFilter,
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
function useMailUsers() {
|
|
104
|
+
const [users, setUsers] = useState<MailUserRecord[]>([]);
|
|
105
|
+
const [loading, setLoading] = useState(true);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
let active = true;
|
|
109
|
+
mailApi
|
|
110
|
+
.getUsers()
|
|
111
|
+
.then((next) => active && setUsers(next))
|
|
112
|
+
.catch(() => active && setUsers([]))
|
|
113
|
+
.finally(() => active && setLoading(false));
|
|
114
|
+
return () => {
|
|
115
|
+
active = false;
|
|
116
|
+
};
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
return { users, loading };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function userName(user: MailUserRecord) {
|
|
123
|
+
return user.nickname || user.username || user.email || `User #${user.id}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function userInitials(user: MailUserRecord) {
|
|
127
|
+
return userName(user)
|
|
128
|
+
.split(/[\s@._-]+/)
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
.slice(0, 2)
|
|
131
|
+
.map((part) => part[0]?.toUpperCase())
|
|
132
|
+
.join("");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function MailScenarioOverview() {
|
|
136
|
+
return (
|
|
137
|
+
<MailShowcasePage
|
|
138
|
+
title="Mail integration scenarios"
|
|
139
|
+
description="Compose and record-driven sending share one page, while the other examples cover personal mail, audience, and correspondence patterns."
|
|
140
|
+
badge="Overview"
|
|
141
|
+
>
|
|
142
|
+
<MailShowcaseSection
|
|
143
|
+
eyebrow="Component patterns"
|
|
144
|
+
title="Choose a mail workflow to explore"
|
|
145
|
+
description="Each example uses the same public mail components with a different page-level composition."
|
|
146
|
+
>
|
|
147
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
148
|
+
{scenarios.map((scenario, index) => {
|
|
149
|
+
const Icon = scenario.icon;
|
|
150
|
+
return (
|
|
151
|
+
<Link
|
|
152
|
+
key={scenario.path}
|
|
153
|
+
to={scenario.path}
|
|
154
|
+
className="group flex gap-4 rounded-xl border bg-card p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)] transition-colors hover:border-primary/35 hover:bg-muted/30"
|
|
155
|
+
>
|
|
156
|
+
<span className="font-mono text-2xl font-semibold text-muted-foreground/40">
|
|
157
|
+
{String(index + 1).padStart(2, "0")}
|
|
158
|
+
</span>
|
|
159
|
+
<Icon className="mt-1 size-5 text-muted-foreground group-hover:text-primary" />
|
|
160
|
+
<span>
|
|
161
|
+
<span className="block font-semibold">{scenario.title}</span>
|
|
162
|
+
<span className="mt-1 block text-sm leading-5 text-muted-foreground">
|
|
163
|
+
{scenario.description}
|
|
164
|
+
</span>
|
|
165
|
+
</span>
|
|
166
|
+
</Link>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
</div>
|
|
170
|
+
</MailShowcaseSection>
|
|
171
|
+
</MailShowcasePage>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function MailAudienceScenario() {
|
|
176
|
+
const [scope, setScope] = useState<MailScope>("personal");
|
|
177
|
+
return (
|
|
178
|
+
<MailShowcasePage
|
|
179
|
+
title="Mailbox views"
|
|
180
|
+
description="Switch between the signed-in user's inbox and all connected mailboxes."
|
|
181
|
+
badge="Mailbox scope"
|
|
182
|
+
>
|
|
183
|
+
<MailShowcaseSection
|
|
184
|
+
eyebrow="Interactive preview"
|
|
185
|
+
title="Personal and administrative mailbox views"
|
|
186
|
+
description="The audience switch stays in the inbox action row while the table columns adapt to the selected scope."
|
|
187
|
+
>
|
|
188
|
+
<MailInbox
|
|
189
|
+
scope={scope}
|
|
190
|
+
columns={scope === "all" ? ALL_COLUMNS : PERSONAL_COLUMNS}
|
|
191
|
+
toolbarActions={
|
|
192
|
+
<Tabs
|
|
193
|
+
value={scope}
|
|
194
|
+
onValueChange={(value) => setScope(value as MailScope)}
|
|
195
|
+
>
|
|
196
|
+
<TabsList>
|
|
197
|
+
<TabsTrigger value="personal">My inbox</TabsTrigger>
|
|
198
|
+
<TabsTrigger value="all">All users</TabsTrigger>
|
|
199
|
+
</TabsList>
|
|
200
|
+
</Tabs>
|
|
201
|
+
}
|
|
202
|
+
/>
|
|
203
|
+
</MailShowcaseSection>
|
|
204
|
+
</MailShowcasePage>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function MailUnreadScenario() {
|
|
209
|
+
const { count, refresh } = useMailUnread();
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<MailShowcasePage
|
|
213
|
+
title="Unread indicator"
|
|
214
|
+
description="A reusable unread-count indicator that refreshes automatically with the mailbox."
|
|
215
|
+
badge="Live status"
|
|
216
|
+
>
|
|
217
|
+
<MailShowcaseSection
|
|
218
|
+
eyebrow="Interactive preview"
|
|
219
|
+
title="Unread mail at a glance"
|
|
220
|
+
description="The count refreshes every 60 seconds and whenever the browser window becomes active."
|
|
221
|
+
>
|
|
222
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
223
|
+
<div className="flex min-h-40 flex-col justify-between rounded-xl border bg-card p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
|
|
224
|
+
<div>
|
|
225
|
+
<p className="text-sm font-semibold">Icon indicator</p>
|
|
226
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
227
|
+
Use the compact form in navigation and action areas.
|
|
228
|
+
</p>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="flex items-end justify-between">
|
|
231
|
+
<div className="rounded-lg border bg-background p-3">
|
|
232
|
+
<MailUnreadIndicator showZero />
|
|
233
|
+
</div>
|
|
234
|
+
<span className="text-3xl font-semibold tabular-nums">{count}</span>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className="flex min-h-40 flex-col justify-between rounded-xl border bg-card p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
|
|
239
|
+
<div>
|
|
240
|
+
<p className="text-sm font-semibold">Labeled indicator</p>
|
|
241
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
242
|
+
Add a label when the component needs more context.
|
|
243
|
+
</p>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="flex items-center justify-between">
|
|
246
|
+
<MailUnreadIndicator label="Unread mail" showZero />
|
|
247
|
+
<Button variant="outline" size="sm" onClick={refresh}>
|
|
248
|
+
<RefreshCw />
|
|
249
|
+
Refresh
|
|
250
|
+
</Button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</MailShowcaseSection>
|
|
255
|
+
</MailShowcasePage>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function MailCorrespondenceScenario() {
|
|
260
|
+
const { users, loading } = useMailUsers();
|
|
261
|
+
const [selectedUser, setSelectedUser] = useState<MailUserRecord>();
|
|
262
|
+
return (
|
|
263
|
+
<MailShowcasePage
|
|
264
|
+
title="Correspondence per user"
|
|
265
|
+
description="Open a user from the directory to inspect the messages owned by that user's connected mailbox."
|
|
266
|
+
badge="Record action"
|
|
267
|
+
>
|
|
268
|
+
<MailShowcaseSection
|
|
269
|
+
eyebrow="User directory"
|
|
270
|
+
title="Open mailbox correspondence from a business record"
|
|
271
|
+
description="The user table owns the action entry; the reusable inbox opens in a side sheet scoped to that user."
|
|
272
|
+
>
|
|
273
|
+
<div className="overflow-hidden rounded-xl border bg-card shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
|
|
274
|
+
<div className="flex items-center gap-2 border-b border-border/60 px-5 py-4">
|
|
275
|
+
<Users className="size-4 text-muted-foreground" />
|
|
276
|
+
<div>
|
|
277
|
+
<h3 className="text-sm font-semibold">Users</h3>
|
|
278
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
279
|
+
{loading ? "Loading…" : `${users.length} people`}
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
<Table>
|
|
284
|
+
<TableHeader className="bg-muted/45">
|
|
285
|
+
<TableRow>
|
|
286
|
+
<TableHead className="w-[45%]">Name</TableHead>
|
|
287
|
+
<TableHead>Email</TableHead>
|
|
288
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
289
|
+
</TableRow>
|
|
290
|
+
</TableHeader>
|
|
291
|
+
<TableBody>
|
|
292
|
+
{loading
|
|
293
|
+
? Array.from({ length: 4 }).map((_, index) => (
|
|
294
|
+
<TableRow key={index}>
|
|
295
|
+
<TableCell>
|
|
296
|
+
<Skeleton className="h-9 w-44" />
|
|
297
|
+
</TableCell>
|
|
298
|
+
<TableCell>
|
|
299
|
+
<Skeleton className="h-4 w-48" />
|
|
300
|
+
</TableCell>
|
|
301
|
+
<TableCell>
|
|
302
|
+
<Skeleton className="ml-auto h-8 w-32" />
|
|
303
|
+
</TableCell>
|
|
304
|
+
</TableRow>
|
|
305
|
+
))
|
|
306
|
+
: users.map((user) => (
|
|
307
|
+
<TableRow key={user.id}>
|
|
308
|
+
<TableCell>
|
|
309
|
+
<div className="flex items-center gap-3">
|
|
310
|
+
<Avatar>
|
|
311
|
+
<AvatarFallback className="bg-primary/10 text-xs font-semibold text-primary">
|
|
312
|
+
{userInitials(user)}
|
|
313
|
+
</AvatarFallback>
|
|
314
|
+
</Avatar>
|
|
315
|
+
<div className="min-w-0">
|
|
316
|
+
<div className="truncate text-sm font-medium">
|
|
317
|
+
{userName(user)}
|
|
318
|
+
</div>
|
|
319
|
+
{user.username && (
|
|
320
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
321
|
+
@{user.username}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</TableCell>
|
|
327
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
328
|
+
{user.email || "—"}
|
|
329
|
+
</TableCell>
|
|
330
|
+
<TableCell className="text-right">
|
|
331
|
+
<Button
|
|
332
|
+
variant="outline"
|
|
333
|
+
size="sm"
|
|
334
|
+
disabled={!user.email}
|
|
335
|
+
onClick={() => setSelectedUser(user)}
|
|
336
|
+
>
|
|
337
|
+
<MessagesSquare /> Correspondence
|
|
338
|
+
</Button>
|
|
339
|
+
</TableCell>
|
|
340
|
+
</TableRow>
|
|
341
|
+
))}
|
|
342
|
+
</TableBody>
|
|
343
|
+
</Table>
|
|
344
|
+
</div>
|
|
345
|
+
</MailShowcaseSection>
|
|
346
|
+
|
|
347
|
+
<Sheet
|
|
348
|
+
open={Boolean(selectedUser)}
|
|
349
|
+
onOpenChange={(open) => !open && setSelectedUser(undefined)}
|
|
350
|
+
>
|
|
351
|
+
<SheetContent
|
|
352
|
+
side="right"
|
|
353
|
+
className="flex w-full flex-col gap-0 p-0 data-[side=right]:sm:max-w-4xl"
|
|
354
|
+
>
|
|
355
|
+
{selectedUser?.email && (
|
|
356
|
+
<>
|
|
357
|
+
<SheetHeader className="flex-row items-center gap-3 border-b border-border/60 px-6 py-4">
|
|
358
|
+
<Avatar>
|
|
359
|
+
<AvatarFallback className="bg-primary/10 text-xs font-semibold text-primary">
|
|
360
|
+
{userInitials(selectedUser)}
|
|
361
|
+
</AvatarFallback>
|
|
362
|
+
</Avatar>
|
|
363
|
+
<div className="min-w-0">
|
|
364
|
+
<SheetTitle className="truncate">
|
|
365
|
+
{userName(selectedUser)}
|
|
366
|
+
</SheetTitle>
|
|
367
|
+
<SheetDescription className="truncate">
|
|
368
|
+
{selectedUser.email}
|
|
369
|
+
</SheetDescription>
|
|
370
|
+
</div>
|
|
371
|
+
</SheetHeader>
|
|
372
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
373
|
+
<MailInbox
|
|
374
|
+
key={selectedUser.id}
|
|
375
|
+
scope="all"
|
|
376
|
+
userId={selectedUser.id}
|
|
377
|
+
columns={PERSONAL_COLUMNS}
|
|
378
|
+
/>
|
|
379
|
+
</div>
|
|
380
|
+
</>
|
|
381
|
+
)}
|
|
382
|
+
</SheetContent>
|
|
383
|
+
</Sheet>
|
|
384
|
+
</MailShowcasePage>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function MailRecipientSelectionDemo() {
|
|
389
|
+
const { users, loading } = useMailUsers();
|
|
390
|
+
const { openCompose, composeDialog } = useMailCompose();
|
|
391
|
+
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
|
392
|
+
const eligibleUsers = users.filter((user) => Boolean(user.email));
|
|
393
|
+
const selectedUsers = eligibleUsers.filter((user) =>
|
|
394
|
+
selectedIds.has(user.id)
|
|
395
|
+
);
|
|
396
|
+
const allSelected =
|
|
397
|
+
eligibleUsers.length > 0 && selectedUsers.length === eligibleUsers.length;
|
|
398
|
+
|
|
399
|
+
const selectUser = (id: number, selected: boolean) => {
|
|
400
|
+
setSelectedIds((current) => {
|
|
401
|
+
const next = new Set(current);
|
|
402
|
+
if (selected) next.add(id);
|
|
403
|
+
else next.delete(id);
|
|
404
|
+
return next;
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<>
|
|
410
|
+
<div className="overflow-hidden rounded-xl border bg-card">
|
|
411
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
|
|
412
|
+
<span className="text-sm text-muted-foreground">
|
|
413
|
+
{selectedUsers.length
|
|
414
|
+
? `${selectedUsers.length} selected`
|
|
415
|
+
: "Select users to prefill recipients"}
|
|
416
|
+
</span>
|
|
417
|
+
<Button
|
|
418
|
+
size="sm"
|
|
419
|
+
disabled={!selectedUsers.length}
|
|
420
|
+
onClick={() =>
|
|
421
|
+
openCompose({
|
|
422
|
+
to: selectedUsers.map((user) => user.email).join(", "),
|
|
423
|
+
subject:
|
|
424
|
+
selectedUsers.length === 1
|
|
425
|
+
? `Following up, ${userName(selectedUsers[0])}`
|
|
426
|
+
: undefined,
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
>
|
|
430
|
+
<Send /> Email selected
|
|
431
|
+
</Button>
|
|
432
|
+
</div>
|
|
433
|
+
{loading ? (
|
|
434
|
+
<p className="p-8 text-center text-sm text-muted-foreground">
|
|
435
|
+
Loading users…
|
|
436
|
+
</p>
|
|
437
|
+
) : (
|
|
438
|
+
<Table>
|
|
439
|
+
<TableHeader>
|
|
440
|
+
<TableRow>
|
|
441
|
+
<TableHead className="w-12">
|
|
442
|
+
<Checkbox
|
|
443
|
+
checked={allSelected}
|
|
444
|
+
onCheckedChange={(checked) =>
|
|
445
|
+
setSelectedIds(
|
|
446
|
+
checked
|
|
447
|
+
? new Set(eligibleUsers.map((user) => user.id))
|
|
448
|
+
: new Set()
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
aria-label="Select all users with email addresses"
|
|
452
|
+
/>
|
|
453
|
+
</TableHead>
|
|
454
|
+
<TableHead>User</TableHead>
|
|
455
|
+
<TableHead>Email</TableHead>
|
|
456
|
+
<TableHead className="w-36 text-right">Actions</TableHead>
|
|
457
|
+
</TableRow>
|
|
458
|
+
</TableHeader>
|
|
459
|
+
<TableBody>
|
|
460
|
+
{users.map((user) => (
|
|
461
|
+
<TableRow key={user.id}>
|
|
462
|
+
<TableCell>
|
|
463
|
+
<Checkbox
|
|
464
|
+
checked={selectedIds.has(user.id)}
|
|
465
|
+
disabled={!user.email}
|
|
466
|
+
onCheckedChange={(checked) =>
|
|
467
|
+
selectUser(user.id, Boolean(checked))
|
|
468
|
+
}
|
|
469
|
+
aria-label={`Select ${userName(user)}`}
|
|
470
|
+
/>
|
|
471
|
+
</TableCell>
|
|
472
|
+
<TableCell className="font-medium">
|
|
473
|
+
{userName(user)}
|
|
474
|
+
</TableCell>
|
|
475
|
+
<TableCell className="text-muted-foreground">
|
|
476
|
+
{user.email || "No email address"}
|
|
477
|
+
</TableCell>
|
|
478
|
+
<TableCell className="text-right">
|
|
479
|
+
<Button
|
|
480
|
+
variant="outline"
|
|
481
|
+
size="sm"
|
|
482
|
+
disabled={!user.email}
|
|
483
|
+
onClick={() =>
|
|
484
|
+
openCompose({
|
|
485
|
+
to: user.email,
|
|
486
|
+
subject: `Following up, ${userName(user)}`,
|
|
487
|
+
body: `Hi ${userName(user)},<p></p>`,
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
>
|
|
491
|
+
<Send /> Send email
|
|
492
|
+
</Button>
|
|
493
|
+
</TableCell>
|
|
494
|
+
</TableRow>
|
|
495
|
+
))}
|
|
496
|
+
</TableBody>
|
|
497
|
+
</Table>
|
|
498
|
+
)}
|
|
499
|
+
</div>
|
|
500
|
+
{composeDialog}
|
|
501
|
+
</>
|
|
502
|
+
);
|
|
503
|
+
}
|