@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.
Files changed (150) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.MD +4 -0
  3. package/dist/assets/{LineDraw-vPRTWiQ5.js → LineDraw-BBB5hwuJ.js} +1 -1
  4. package/dist/assets/{SymbolDraw-DOl_Tss3.js → SymbolDraw-CKS3uUb_.js} +1 -1
  5. package/dist/assets/{VisualMapping-j08W3KBm.js → VisualMapping-BCwaW3TD.js} +1 -1
  6. package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
  7. package/dist/assets/{auto-login-provider-Bukw7zEe.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-BQn3Mttx.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
  14. package/dist/assets/{components-BT5mL9gW.js → components-_g-GjQeV.js} +3 -3
  15. package/dist/assets/{configuration-gate-BJoadoA6.js → configuration-gate-B4XVz7tS.js} +1 -1
  16. package/dist/assets/{createClipPathFromCoordSys-m8IbnJ1z.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
  17. package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
  18. package/dist/assets/{customGraphicKeyframeAnimation-CYLPurwK.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-DSluFaP9.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
  27. package/dist/assets/{echarts-preview-mRXoEciY.js → echarts-preview-R96Pbqmz.js} +1 -1
  28. package/dist/assets/{echarts-runtime-CvLr4Slh.js → echarts-runtime-Ce_z2eX_.js} +3 -3
  29. package/dist/assets/{echarts-runtime-advanced-BC1tvfQ0.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
  30. package/dist/assets/{echarts-runtime-common-DhNaV5B8.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
  31. package/dist/assets/{echarts-runtime-components-BiioS8t1.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
  32. package/dist/assets/{echarts-runtime-hierarchy-CAWqYvFh.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
  33. package/dist/assets/{floating-RVbHvIiD.js → floating-O9WmiDXk.js} +2 -2
  34. package/dist/assets/index-BBKV6O5d.css +1 -0
  35. package/dist/assets/{index-Dw20Lzpp.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-BysibBZu.js → index-DgB7uTMs.js} +2 -2
  39. package/dist/assets/{index-C-tdHirk.js → index-DroUAK80.js} +1 -1
  40. package/dist/assets/index-WKvygTBp.js +59 -0
  41. package/dist/assets/{labelHelper-DbITmOUW.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-Cgpa8Qzj.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
  44. package/dist/assets/{page-context-AU6RU0Qi.js → page-context-DAiW_dId.js} +4 -4
  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-C6L782Ue.js → sectorHelper-BuxrrJrA.js} +1 -1
  49. package/dist/assets/shortcut-BhKXxJLs.js +47 -0
  50. package/dist/assets/{sms-sign-in-form-Bo2WLZuP.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
  51. package/dist/assets/{static.browser-CoPxsICt.js → static.browser-kzzpH0Xt.js} +1 -1
  52. package/dist/assets/switch-BdVxPIVU.js +1 -0
  53. package/dist/assets/{tool-cards-ipgJ_3Ec.js → tool-cards-CpczIke-.js} +3 -3
  54. package/dist/assets/{use-authenticator-sign-in-BzFeh7qe.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-PPIMLvGa.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
  57. package/dist/assets/{wecom-sign-in-button-rUq9RW8R.js → wecom-sign-in-button-Dn_2zDab.js} +1 -1
  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/extensions/nocobase-acl/README.md +28 -14
  70. package/src/extensions/nocobase-acl/demo/index.tsx +87 -15
  71. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +1 -0
  72. package/src/extensions/nocobase-mail/README.md +74 -0
  73. package/src/extensions/nocobase-mail/components/index.ts +79 -0
  74. package/src/extensions/nocobase-mail/components/mail-api.ts +423 -0
  75. package/src/extensions/nocobase-mail/components/mail-attachment-list.tsx +67 -0
  76. package/src/extensions/nocobase-mail/components/mail-compose-attachments.tsx +166 -0
  77. package/src/extensions/nocobase-mail/components/mail-compose.tsx +1071 -0
  78. package/src/extensions/nocobase-mail/components/mail-detail.tsx +653 -0
  79. package/src/extensions/nocobase-mail/components/mail-draft-autosave.ts +26 -0
  80. package/src/extensions/nocobase-mail/components/mail-empty.tsx +48 -0
  81. package/src/extensions/nocobase-mail/components/mail-filters.tsx +277 -0
  82. package/src/extensions/nocobase-mail/components/mail-inbox.tsx +544 -0
  83. package/src/extensions/nocobase-mail/components/mail-inline-images.ts +69 -0
  84. package/src/extensions/nocobase-mail/components/mail-label-badge.tsx +25 -0
  85. package/src/extensions/nocobase-mail/components/mail-label-manager.tsx +271 -0
  86. package/src/extensions/nocobase-mail/components/mail-labels-editor.tsx +267 -0
  87. package/src/extensions/nocobase-mail/components/mail-mass-tracking.tsx +271 -0
  88. package/src/extensions/nocobase-mail/components/mail-note-editor.tsx +99 -0
  89. package/src/extensions/nocobase-mail/components/mail-recipient-input.tsx +260 -0
  90. package/src/extensions/nocobase-mail/components/mail-reply-quote.ts +28 -0
  91. package/src/extensions/nocobase-mail/components/mail-rich-editor.tsx +287 -0
  92. package/src/extensions/nocobase-mail/components/mail-send-actions.tsx +238 -0
  93. package/src/extensions/nocobase-mail/components/mail-senders.ts +61 -0
  94. package/src/extensions/nocobase-mail/components/mail-settings-drawer.tsx +708 -0
  95. package/src/extensions/nocobase-mail/components/mail-showcase-layout.tsx +64 -0
  96. package/src/extensions/nocobase-mail/components/mail-signature-manager.tsx +258 -0
  97. package/src/extensions/nocobase-mail/components/mail-signatures.ts +25 -0
  98. package/src/extensions/nocobase-mail/components/mail-table.tsx +476 -0
  99. package/src/extensions/nocobase-mail/components/mail-template-manager.tsx +204 -0
  100. package/src/extensions/nocobase-mail/components/mail-toolbar.tsx +155 -0
  101. package/src/extensions/nocobase-mail/components/mail-unread.tsx +111 -0
  102. package/src/extensions/nocobase-mail/components/types.ts +297 -0
  103. package/src/extensions/nocobase-mail/components/use-mail-compose.tsx +262 -0
  104. package/src/extensions/nocobase-mail/components/use-mail-mass-messages.ts +54 -0
  105. package/src/extensions/nocobase-mail/components/use-mail-messages.ts +113 -0
  106. package/src/extensions/nocobase-mail/components/use-mail-signatures.ts +122 -0
  107. package/src/extensions/nocobase-mail/components/use-mail-templates.ts +54 -0
  108. package/src/extensions/nocobase-mail/extension.tsx +105 -0
  109. package/src/extensions/nocobase-mail/index.ts +1 -0
  110. package/src/extensions/nocobase-mail/mail-demo-pages.tsx +503 -0
  111. package/src/extensions/nocobase-mail/mail-pages.tsx +201 -0
  112. package/src/extensions/nocobase-mail/tests/mail-quote-html-regression.mjs +51 -0
  113. package/src/extensions/nocobase-mail/tests/mail-regression.mjs +649 -0
  114. package/src/extensions/nocobase-users-example/app-routes.tsx +121 -0
  115. package/src/extensions/nocobase-users-example/extension.tsx +2 -100
  116. package/src/extensions/nocobase-users-example/index.ts +1 -0
  117. package/src/lib/nocobase/acl/menu.ts +17 -3
  118. package/src/lib/nocobase/acl/types.ts +4 -0
  119. package/src/locales/en-US.ts +2 -1
  120. package/src/locales/zh-CN.ts +2 -1
  121. package/src/providers/runtime-config.ts +18 -0
  122. package/src/routes.tsx +10 -0
  123. package/src/vite-env.d.ts +3 -0
  124. package/vite.config.ts +8 -0
  125. package/dist/assets/auth-method-demo-B2qcpY8z.js +0 -1
  126. package/dist/assets/auto-login-provider-BMtzvdxG.js +0 -1
  127. package/dist/assets/auto-redirect-provider-C6GNPg5r.js +0 -1
  128. package/dist/assets/auto-redirect-provider-D5c6kN2R.js +0 -1
  129. package/dist/assets/auto-redirect-provider-iTzGEDpU.js +0 -1
  130. package/dist/assets/calendar-Dv2RNnsr.js +0 -1
  131. package/dist/assets/card-C9Hpeq1c.js +0 -1
  132. package/dist/assets/createSeriesDataSimply-DKUV5b5e.js +0 -1
  133. package/dist/assets/demo-BWGOwqtJ.js +0 -1
  134. package/dist/assets/demo-BWbcvkya.js +0 -1
  135. package/dist/assets/demo-DIASSPgn.js +0 -1
  136. package/dist/assets/demo-DuZ6asUU.js +0 -1
  137. package/dist/assets/demo-Dxcy4bDE.js +0 -1
  138. package/dist/assets/demo-YpEC76uw.js +0 -1
  139. package/dist/assets/demo-wzkoR8T2.js +0 -1
  140. package/dist/assets/index-CBZbj45X.js +0 -58
  141. package/dist/assets/index-CgkdZ6Oc.js +0 -686
  142. package/dist/assets/index-NH3aqqQz.css +0 -1
  143. package/dist/assets/index-hSBzW-VA.js +0 -43
  144. package/dist/assets/ldap-sign-in-form-BOMNDTkK.js +0 -1
  145. package/dist/assets/prompt-card-FWugRgi-.js +0 -1
  146. package/dist/assets/prompt-output-DMTuTNab.js +0 -1
  147. package/dist/assets/saml-sign-in-button-qt8bMeoQ.js +0 -1
  148. package/dist/assets/shortcut-Bu7jT_-w.js +0 -47
  149. package/dist/assets/switch-DQkkIE7G.js +0 -1
  150. package/dist/assets/use-dingtalk-sign-in-mrq6K5Yx.js +0 -1
@@ -0,0 +1,423 @@
1
+ import { nocobaseClient } from "@/lib/nocobase/client";
2
+ import type {
3
+ MailAccount,
4
+ MailIdentity,
5
+ MailLabel,
6
+ MailListParams,
7
+ MailListResponse,
8
+ MailMassSendSettings,
9
+ MailMassListResponse,
10
+ MailMassMessage,
11
+ MailMessage,
12
+ MailNote,
13
+ MailSendPayload,
14
+ MailTemplate,
15
+ MailUploadedAttachment,
16
+ MailUserRecord,
17
+ } from "./types";
18
+
19
+ function parseListResponse(payload: unknown): MailListResponse {
20
+ if (!payload || typeof payload !== "object") return { rows: [], count: 0 };
21
+ const obj = payload as {
22
+ data?: unknown;
23
+ meta?: { count?: number };
24
+ rows?: unknown;
25
+ count?: number;
26
+ };
27
+ if (Array.isArray(obj.data)) {
28
+ return { rows: obj.data, count: obj.meta?.count ?? obj.data.length };
29
+ }
30
+ if (Array.isArray(obj.rows)) {
31
+ return { rows: obj.rows, count: obj.count ?? obj.rows.length };
32
+ }
33
+ if (Array.isArray(payload)) {
34
+ return { rows: payload, count: payload.length };
35
+ }
36
+ return { rows: [], count: 0 };
37
+ }
38
+
39
+ export const mailApi = {
40
+ listMessages(params: MailListParams = {}): Promise<MailListResponse> {
41
+ const filter: Record<string, unknown> = {};
42
+ if (params.boxType) filter.boxType = { $in: [params.boxType] };
43
+ if (params.isRead !== undefined) filter.isRead = params.isRead;
44
+ if (params.labelId) filter["labels.id"] = { $in: [params.labelId] };
45
+ if (params.userId !== undefined) filter.userId = params.userId;
46
+ if (params.search) {
47
+ filter.$or = [
48
+ { subject: { $includes: params.search } },
49
+ { from: { $includes: params.search } },
50
+ { to: { $includes: params.search } },
51
+ ];
52
+ }
53
+ if (params.filter) Object.assign(filter, params.filter);
54
+
55
+ const action = params.scope === "personal" ? "listPerson" : "list";
56
+
57
+ return nocobaseClient
58
+ .action<unknown>("mailMessages", action, {
59
+ query: {
60
+ page: params.page ?? 1,
61
+ pageSize: params.pageSize ?? 20,
62
+ subjectMerge: true,
63
+ appends: "user",
64
+ ...(Object.keys(filter).length
65
+ ? { filter: JSON.stringify(filter) }
66
+ : {}),
67
+ sort: params.sort ?? "-relatedMessageLatestDate",
68
+ },
69
+ unwrap: "none",
70
+ })
71
+ .then(parseListResponse);
72
+ },
73
+
74
+ getMessage(id: number | string): Promise<MailMessage> {
75
+ return nocobaseClient.action<MailMessage>("mailMessages", "get", {
76
+ query: { filterByTk: id, appends: "children,labels,note" },
77
+ });
78
+ },
79
+
80
+ trashMessages(
81
+ ids: (number | string)[],
82
+ moveToTrash = true
83
+ ): Promise<unknown> {
84
+ return nocobaseClient.action("mailMessages", "trash", {
85
+ body: { filterByTk: ids, moveToTrash },
86
+ });
87
+ },
88
+
89
+ destroyMessages(ids: (number | string)[]): Promise<unknown> {
90
+ return nocobaseClient.action("mailMessages", "destroy", {
91
+ query: { filterByTk: ids },
92
+ });
93
+ },
94
+
95
+ cancelScheduled(id: number | string): Promise<unknown> {
96
+ return nocobaseClient.action("mailMessages", "cancelTimelySend", {
97
+ query: { id },
98
+ });
99
+ },
100
+
101
+ setRead(mailIds: string[], isRead: boolean): Promise<unknown> {
102
+ return nocobaseClient.action("mail", "messageSetReaded", {
103
+ body: { mailIds, isRead },
104
+ });
105
+ },
106
+
107
+ send(payload: MailSendPayload): Promise<unknown> {
108
+ return nocobaseClient.action("mail", "messageSend", { body: payload });
109
+ },
110
+
111
+ massSend(
112
+ payload: MailSendPayload,
113
+ settings: MailMassSendSettings = {}
114
+ ): Promise<unknown> {
115
+ return nocobaseClient.action("mailMassMessages", "send", {
116
+ body: {
117
+ data: payload,
118
+ mailSendSetting: settings,
119
+ },
120
+ });
121
+ },
122
+
123
+ async listMassMessages(
124
+ parentId: number | null = null
125
+ ): Promise<MailMassListResponse> {
126
+ const pageSize = 100;
127
+ const rows: MailMassMessage[] = [];
128
+ let page = 1;
129
+ let count = 0;
130
+
131
+ do {
132
+ const payload = await nocobaseClient.action<unknown>(
133
+ "mailMassMessages",
134
+ "list",
135
+ {
136
+ query: {
137
+ page,
138
+ pageSize,
139
+ filter: JSON.stringify({ parentId }),
140
+ sort: "-createdAt",
141
+ },
142
+ unwrap: "none",
143
+ }
144
+ );
145
+ const parsed = parseListResponse(payload);
146
+ const nextRows = parsed.rows as unknown as MailMassMessage[];
147
+ rows.push(...nextRows);
148
+ count = parsed.count;
149
+ if (!nextRows.length) break;
150
+ page += 1;
151
+ } while (rows.length < count);
152
+
153
+ return { rows, count };
154
+ },
155
+
156
+ cancelMassMessage(id: number | string): Promise<unknown> {
157
+ return nocobaseClient.action("mailMassMessages", "cancel", {
158
+ body: { id },
159
+ });
160
+ },
161
+
162
+ resendMassMessage(id: number | string): Promise<unknown> {
163
+ return nocobaseClient.action("mailMassMessages", "resend", {
164
+ body: { id },
165
+ });
166
+ },
167
+
168
+ uploadAttachment(file: File): Promise<MailUploadedAttachment> {
169
+ const formData = new FormData();
170
+ formData.append("file", file);
171
+ return nocobaseClient
172
+ .action<MailUploadedAttachment>("mail", "messageAttachmentUpload", {
173
+ body: formData,
174
+ })
175
+ .then((attachment) => ({
176
+ ...attachment,
177
+ // Google reads originalname/mimetype; Microsoft reads filename/mimeType.
178
+ // Keep both pairs aligned so recipients see the real file name and type.
179
+ filename: attachment.originalname,
180
+ mimeType: attachment.mimetype,
181
+ }));
182
+ },
183
+
184
+ saveDraft(payload: MailSendPayload): Promise<{ id: number }> {
185
+ return nocobaseClient.action<{ id: number }>("mail", "messageSavingDraft", {
186
+ body: payload,
187
+ });
188
+ },
189
+
190
+ sync(emails: string[]): Promise<unknown> {
191
+ return nocobaseClient.action("mail", "messagesSync", { body: { emails } });
192
+ },
193
+
194
+ unreadCount(): Promise<number> {
195
+ return nocobaseClient
196
+ .action<unknown>("mail", "messageUnreadCount")
197
+ .then((value) => {
198
+ if (typeof value === "number") return value;
199
+ if (value && typeof value === "object" && "count" in value) {
200
+ const count = Number((value as { count?: unknown }).count);
201
+ return Number.isFinite(count) ? count : 0;
202
+ }
203
+ return 0;
204
+ });
205
+ },
206
+
207
+ getAccounts(): Promise<MailAccount[]> {
208
+ return nocobaseClient.action<MailAccount[]>("mail", "getMailAccounts");
209
+ },
210
+
211
+ findDraft(values: {
212
+ accountEmail: string;
213
+ identityEmail: string;
214
+ to: string[];
215
+ }): Promise<MailMessage | undefined> {
216
+ return nocobaseClient
217
+ .action<MailMessage | undefined>("mailMessages", "get", {
218
+ query: {
219
+ filter: JSON.stringify({
220
+ isDraft: true,
221
+ from: values.identityEmail,
222
+ email: values.accountEmail,
223
+ to: values.to,
224
+ }),
225
+ appends: "labels,note",
226
+ },
227
+ })
228
+ .then((draft) => draft || undefined);
229
+ },
230
+
231
+ getOAuthUrl(
232
+ type: "google" | "microsoft",
233
+ options: { email?: string; reauthorize?: boolean } = {}
234
+ ): Promise<string> {
235
+ return nocobaseClient
236
+ .action<{ url?: string }>("mail", "oauth2url", {
237
+ query: { type, ...options },
238
+ })
239
+ .then((response) => response.url || "");
240
+ },
241
+
242
+ deleteAccount(email: string): Promise<unknown> {
243
+ return nocobaseClient.action("mail", "deleteMailAccount", {
244
+ body: { email },
245
+ });
246
+ },
247
+
248
+ resyncAccount(email: string): Promise<unknown> {
249
+ return nocobaseClient.action("mail", "messagesResync", {
250
+ body: { email },
251
+ });
252
+ },
253
+
254
+ syncAliases(email: string): Promise<MailIdentity[]> {
255
+ return nocobaseClient
256
+ .action<MailIdentity[]>("mail", "accountIdentitiesSync", {
257
+ body: { email },
258
+ })
259
+ .then((identities) => (Array.isArray(identities) ? identities : []));
260
+ },
261
+
262
+ getUsers(): Promise<MailUserRecord[]> {
263
+ return nocobaseClient
264
+ .action<MailUserRecord[]>("users", "list", {
265
+ query: { pageSize: 100 },
266
+ })
267
+ .then((res) => (Array.isArray(res) ? res : []));
268
+ },
269
+
270
+ getLabels(userId?: number | string): Promise<MailLabel[]> {
271
+ const filter =
272
+ userId !== undefined ? { createdBy: { id: userId } } : undefined;
273
+ return nocobaseClient
274
+ .action<MailLabel[]>("mailMessageLabels", "list", {
275
+ query: {
276
+ paginate: false,
277
+ ...(filter ? { filter: JSON.stringify(filter) } : {}),
278
+ },
279
+ })
280
+ .then((res) => (Array.isArray(res) ? res : []));
281
+ },
282
+
283
+ createLabel(values: {
284
+ label: string;
285
+ color: string;
286
+ description?: string;
287
+ }): Promise<MailLabel> {
288
+ return nocobaseClient.action<MailLabel>("mailMessageLabels", "create", {
289
+ body: values,
290
+ });
291
+ },
292
+
293
+ updateLabel(
294
+ id: number | string,
295
+ values: { label: string; color: string; description?: string }
296
+ ): Promise<MailLabel> {
297
+ return nocobaseClient.action<MailLabel>("mailMessageLabels", "update", {
298
+ query: { filterByTk: id },
299
+ body: values,
300
+ });
301
+ },
302
+
303
+ deleteLabel(id: number | string): Promise<unknown> {
304
+ return nocobaseClient.action("mailMessageLabels", "destroy", {
305
+ query: { filterByTk: id },
306
+ });
307
+ },
308
+
309
+ getTemplates(): Promise<MailTemplate[]> {
310
+ return nocobaseClient
311
+ .action<MailTemplate[]>("mailTemplates", "list", {
312
+ query: { paginate: false, sort: "createdAt" },
313
+ })
314
+ .then((res) => (Array.isArray(res) ? res : []));
315
+ },
316
+
317
+ createTemplate(values: {
318
+ name: string;
319
+ content: string;
320
+ }): Promise<MailTemplate> {
321
+ return nocobaseClient.action<MailTemplate>("mailTemplates", "create", {
322
+ body: values,
323
+ });
324
+ },
325
+
326
+ updateTemplate(
327
+ id: number | string,
328
+ values: { name: string; content: string }
329
+ ): Promise<MailTemplate> {
330
+ return nocobaseClient.action<MailTemplate>("mailTemplates", "update", {
331
+ query: { filterByTk: id },
332
+ body: values,
333
+ });
334
+ },
335
+
336
+ deleteTemplate(id: number | string): Promise<unknown> {
337
+ return nocobaseClient.action("mailTemplates", "destroy", {
338
+ query: { filterByTk: id },
339
+ });
340
+ },
341
+
342
+ updateAccount(
343
+ id: number | string,
344
+ values: Partial<MailAccount>
345
+ ): Promise<MailAccount> {
346
+ return nocobaseClient.action<MailAccount>("mailAccounts", "update", {
347
+ query: { filterByTk: id },
348
+ body: values,
349
+ });
350
+ },
351
+
352
+ setMessageLabels(
353
+ messageId: number | string,
354
+ labelIds: number[]
355
+ ): Promise<unknown> {
356
+ return nocobaseClient.action("mailMessages", "update", {
357
+ query: { filterByTk: messageId },
358
+ body: { labels: labelIds },
359
+ });
360
+ },
361
+
362
+ setTodo(messageId: number | string, isTodo: boolean): Promise<unknown> {
363
+ return nocobaseClient.action("mailMessages", "update", {
364
+ query: { filterByTk: messageId },
365
+ body: { isTodo },
366
+ });
367
+ },
368
+
369
+ setBoxType(messageId: number | string, boxType: string): Promise<unknown> {
370
+ return nocobaseClient.action("mailMessages", "update", {
371
+ query: { filterByTk: messageId },
372
+ body: { boxType },
373
+ });
374
+ },
375
+
376
+ createNote(messageId: number | string, note: string): Promise<MailNote> {
377
+ return nocobaseClient.action<MailNote>("mailMessageNotes", "create", {
378
+ body: { mailMessageId: messageId, note },
379
+ });
380
+ },
381
+
382
+ updateNote(
383
+ noteId: number | string,
384
+ messageId: number | string,
385
+ note: string
386
+ ): Promise<MailNote> {
387
+ return nocobaseClient.action<MailNote>("mailMessageNotes", "update", {
388
+ query: { filterByTk: noteId },
389
+ body: { mailMessageId: messageId, note },
390
+ });
391
+ },
392
+
393
+ attachmentUrl(messageId: number | string, attachmentId: string): string {
394
+ return nocobaseClient
395
+ .buildUrl("mail:messageAttachmentGet", {
396
+ id: messageId,
397
+ attachmentId,
398
+ })
399
+ .toString();
400
+ },
401
+
402
+ inlineImageUrl(messageId: number | string, contentId: string): string {
403
+ return nocobaseClient
404
+ .buildUrl("mail:messageContentPreview", { messageId, contentId })
405
+ .toString();
406
+ },
407
+
408
+ async fetchInlineImage(
409
+ messageId: number | string,
410
+ contentId: string
411
+ ): Promise<Blob> {
412
+ const response = await fetch(this.inlineImageUrl(messageId, contentId), {
413
+ headers: nocobaseClient.getHeaders({
414
+ withAclMeta: false,
415
+ headers: { Accept: "image/*" },
416
+ }),
417
+ });
418
+ if (!response.ok) {
419
+ throw new Error(`Inline image request failed (${response.status})`);
420
+ }
421
+ return response.blob();
422
+ },
423
+ };
@@ -0,0 +1,67 @@
1
+ import { Download, FileText, Image as ImageIcon, Paperclip } from "lucide-react";
2
+ import type { MailAttachment } from "./types";
3
+ import { mailApi } from "./mail-api";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function formatSize(size?: number) {
7
+ if (!size) return "";
8
+ if (size < 1024) return `${size} B`;
9
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
10
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
11
+ }
12
+
13
+ function isImage(mimeType: string) {
14
+ return mimeType.startsWith("image/");
15
+ }
16
+
17
+ export function MailAttachmentList({
18
+ messageId,
19
+ attachments,
20
+ className,
21
+ }: {
22
+ messageId: number | string;
23
+ attachments: MailAttachment[];
24
+ className?: string;
25
+ }) {
26
+ if (!attachments?.length) return null;
27
+
28
+ return (
29
+ <div className={cn("flex flex-col gap-2", className)}>
30
+ <div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
31
+ <Paperclip className="size-3.5" />
32
+ {attachments.length} attachment{attachments.length > 1 ? "s" : ""}
33
+ </div>
34
+ <div className="flex flex-wrap gap-2">
35
+ {attachments.map((att) => (
36
+ <a
37
+ key={att.attachmentId}
38
+ href={mailApi.attachmentUrl(messageId, att.attachmentId)}
39
+ target="_blank"
40
+ rel="noreferrer"
41
+ className={cn(
42
+ "group flex items-center gap-2.5 rounded-lg border border-border/80 bg-card px-3 py-2",
43
+ "shadow-[0_1px_2px_rgba(0,0,0,0.04)] transition-all hover:border-primary/40 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
44
+ )}
45
+ >
46
+ {isImage(att.mimeType) ? (
47
+ <ImageIcon className="size-4 shrink-0 text-sky-500" />
48
+ ) : (
49
+ <FileText className="size-4 shrink-0 text-muted-foreground" />
50
+ )}
51
+ <div className="min-w-0">
52
+ <div className="max-w-[180px] truncate text-xs font-medium text-foreground">
53
+ {att.filename}
54
+ </div>
55
+ {att.size ? (
56
+ <div className="text-[10px] text-muted-foreground">
57
+ {formatSize(att.size)}
58
+ </div>
59
+ ) : null}
60
+ </div>
61
+ <Download className="size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
62
+ </a>
63
+ ))}
64
+ </div>
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,166 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { AlertCircle, Loader2, Paperclip, RefreshCw, X } from "lucide-react";
3
+ import { toast } from "sonner";
4
+ import { mailApi } from "./mail-api";
5
+ import type { MailUploadedAttachment } from "./types";
6
+ import { Button } from "@/components/ui/button";
7
+
8
+ const MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
9
+
10
+ interface AttachmentRow {
11
+ id: string;
12
+ name: string;
13
+ status: "uploading" | "done" | "error";
14
+ file?: File;
15
+ uploaded?: MailUploadedAttachment;
16
+ }
17
+
18
+ function attachmentId() {
19
+ return `attachment-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
20
+ }
21
+
22
+ export interface MailComposeAttachmentsProps {
23
+ value: MailUploadedAttachment[];
24
+ onChange: (value: MailUploadedAttachment[]) => void;
25
+ onBusyChange?: (busy: boolean) => void;
26
+ onDirty?: () => void;
27
+ disabled?: boolean;
28
+ }
29
+
30
+ export function MailComposeAttachments({
31
+ value,
32
+ onChange,
33
+ onBusyChange,
34
+ onDirty,
35
+ disabled,
36
+ }: MailComposeAttachmentsProps) {
37
+ const inputRef = useRef<HTMLInputElement>(null);
38
+ const onChangeRef = useRef(onChange);
39
+ const onBusyChangeRef = useRef(onBusyChange);
40
+ const [rows, setRows] = useState<AttachmentRow[]>(() =>
41
+ value.map((attachment) => ({
42
+ id: attachmentId(),
43
+ name: attachment.originalname,
44
+ status: "done",
45
+ uploaded: attachment,
46
+ }))
47
+ );
48
+
49
+ useEffect(() => {
50
+ onChangeRef.current = onChange;
51
+ onBusyChangeRef.current = onBusyChange;
52
+ }, [onChange, onBusyChange]);
53
+
54
+ useEffect(() => {
55
+ onChangeRef.current(
56
+ rows.flatMap((row) =>
57
+ row.status === "done" && row.uploaded ? [row.uploaded] : []
58
+ )
59
+ );
60
+ onBusyChangeRef.current?.(rows.some((row) => row.status !== "done"));
61
+ }, [rows]);
62
+
63
+ const upload = async (id: string, file: File) => {
64
+ setRows((prev) =>
65
+ prev.map((row) =>
66
+ row.id === id ? { ...row, status: "uploading", file } : row
67
+ )
68
+ );
69
+ try {
70
+ const uploaded = await mailApi.uploadAttachment(file);
71
+ setRows((prev) =>
72
+ prev.map((row) =>
73
+ row.id === id ? { ...row, status: "done", uploaded } : row
74
+ )
75
+ );
76
+ } catch (error) {
77
+ setRows((prev) =>
78
+ prev.map((row) => (row.id === id ? { ...row, status: "error" } : row))
79
+ );
80
+ toast.error(
81
+ error instanceof Error ? error.message : `${file.name} upload failed`
82
+ );
83
+ }
84
+ };
85
+
86
+ const addFiles = (files: File[]) => {
87
+ const accepted = files.filter((file) => {
88
+ if (file.size <= MAX_ATTACHMENT_SIZE) return true;
89
+ toast.error(`${file.name} exceeds the 25 MB attachment limit`);
90
+ return false;
91
+ });
92
+ const additions = accepted.map((file) => ({
93
+ id: attachmentId(),
94
+ name: file.name,
95
+ status: "uploading" as const,
96
+ file,
97
+ }));
98
+ if (additions.length) onDirty?.();
99
+ setRows((prev) => [...prev, ...additions]);
100
+ additions.forEach((row) => void upload(row.id, row.file));
101
+ };
102
+
103
+ return (
104
+ <div className="space-y-2">
105
+ <input
106
+ ref={inputRef}
107
+ type="file"
108
+ multiple
109
+ className="hidden"
110
+ onChange={(event) => {
111
+ if (event.target.files?.length) addFiles(Array.from(event.target.files));
112
+ event.target.value = "";
113
+ }}
114
+ />
115
+ {rows.length > 0 && (
116
+ <div className="flex min-w-0 flex-wrap gap-2">
117
+ {rows.map((row) => (
118
+ <span
119
+ key={row.id}
120
+ className="flex min-w-0 max-w-full items-center gap-1.5 rounded-md border border-border/80 bg-muted/50 px-2 py-1 text-xs"
121
+ >
122
+ {row.status === "uploading" ? (
123
+ <Loader2 className="size-3 animate-spin text-muted-foreground" />
124
+ ) : row.status === "error" ? (
125
+ <AlertCircle className="size-3 text-destructive" />
126
+ ) : (
127
+ <Paperclip className="size-3 text-muted-foreground" />
128
+ )}
129
+ <span className="min-w-0 max-w-52 truncate">{row.name}</span>
130
+ {row.status === "error" && row.file && (
131
+ <button
132
+ type="button"
133
+ title="Retry upload"
134
+ onClick={() => void upload(row.id, row.file!)}
135
+ className="text-muted-foreground hover:text-foreground"
136
+ >
137
+ <RefreshCw className="size-3" />
138
+ </button>
139
+ )}
140
+ <button
141
+ type="button"
142
+ title="Remove attachment"
143
+ onClick={() => {
144
+ onDirty?.();
145
+ setRows((prev) => prev.filter((item) => item.id !== row.id));
146
+ }}
147
+ className="text-muted-foreground hover:text-foreground"
148
+ >
149
+ <X className="size-3" />
150
+ </button>
151
+ </span>
152
+ ))}
153
+ </div>
154
+ )}
155
+ <Button
156
+ variant="ghost"
157
+ size="icon-sm"
158
+ title="Attach files"
159
+ disabled={disabled}
160
+ onClick={() => inputRef.current?.click()}
161
+ >
162
+ <Paperclip />
163
+ </Button>
164
+ </div>
165
+ );
166
+ }