nexus-shared 4.0.0 → 4.1.0

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 (163) hide show
  1. package/CHANGELOG.md +795 -0
  2. package/README.md +220 -14
  3. package/dist/Client.Index.d.ts +13 -0
  4. package/dist/Client.Index.js +13 -0
  5. package/dist/Components/Chats/ChatComposer.d.ts +7 -1
  6. package/dist/Components/Chats/ChatComposer.js +9 -6
  7. package/dist/Components/Chats/ChatParts.d.ts +15 -2
  8. package/dist/Components/Chats/ChatParts.js +15 -4
  9. package/dist/Components/Chats/ChatPopup.d.ts +46 -0
  10. package/dist/Components/Chats/ChatPopup.js +34 -0
  11. package/dist/Components/Chats/ChatThread.d.ts +9 -1
  12. package/dist/Components/Chats/ChatThread.js +54 -6
  13. package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
  14. package/dist/Components/Dashboards/DashboardBoard.js +594 -0
  15. package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
  16. package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
  17. package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
  18. package/dist/Components/Dashboards/WidgetFrame.js +45 -0
  19. package/dist/Components/Documents/Button.d.ts +6 -1
  20. package/dist/Components/Documents/Button.js +2 -2
  21. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  22. package/dist/Components/Documents/TabButtons.js +5 -4
  23. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  24. package/dist/Components/Domains/DomainCard.js +182 -0
  25. package/dist/Components/Forms/ApiForm.js +5 -1
  26. package/dist/Components/Forms/Crud.js +72 -24
  27. package/dist/Components/Forms/CrudActions.d.ts +5 -4
  28. package/dist/Components/Forms/CrudActions.js +42 -12
  29. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  30. package/dist/Components/Forms/CrudHistory.js +14 -6
  31. package/dist/Components/Forms/CrudParts.d.ts +11 -4
  32. package/dist/Components/Forms/CrudParts.js +76 -10
  33. package/dist/Components/Forms/CrudRowForms.js +65 -5
  34. package/dist/Components/Forms/CrudTree.js +7 -7
  35. package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
  36. package/dist/Components/Forms/CrudWorkspace.js +19 -11
  37. package/dist/Components/Forms/Form.d.ts +3 -2
  38. package/dist/Components/Forms/Form.js +33 -26
  39. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  40. package/dist/Components/Forms/FormQuestion.js +28 -0
  41. package/dist/Components/Forms/SubmitForm.js +2 -15
  42. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  43. package/dist/Components/Inputs/ComputedInput.js +179 -0
  44. package/dist/Components/Inputs/Dropdown.js +4 -2
  45. package/dist/Components/Inputs/GroupForm.js +3 -2
  46. package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
  47. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  48. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  49. package/dist/Components/Inputs/PictureInput.js +8 -3
  50. package/dist/Components/Inputs/RowsInput.d.ts +14 -1
  51. package/dist/Components/Inputs/RowsInput.js +78 -5
  52. package/dist/Components/Inputs/TabularForm.js +16 -3
  53. package/dist/Components/Inputs/TreeView.d.ts +24 -1
  54. package/dist/Components/Inputs/TreeView.js +84 -7
  55. package/dist/Components/Layouts/Popup.d.ts +22 -2
  56. package/dist/Components/Layouts/Popup.js +108 -6
  57. package/dist/Components/Messages/MailClient.d.ts +50 -0
  58. package/dist/Components/Messages/MailClient.js +410 -0
  59. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  60. package/dist/Components/Messages/MailColumns.js +92 -0
  61. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  62. package/dist/Components/Messages/MailCompose.js +154 -0
  63. package/dist/Components/Messages/MailReader.d.ts +43 -0
  64. package/dist/Components/Messages/MailReader.js +207 -0
  65. package/dist/Components/Messages/MailThread.d.ts +71 -0
  66. package/dist/Components/Messages/MailThread.js +185 -0
  67. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  68. package/dist/Components/Messages/MessageBell.js +76 -0
  69. package/dist/Components/Viewers/DataTable.js +7 -5
  70. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  71. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  72. package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
  73. package/dist/Components/Viewers/DataTableParts.js +2 -2
  74. package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
  75. package/dist/Components/Viewers/OwnerFiles.js +142 -31
  76. package/dist/Helpers/ApiModules.js +4 -0
  77. package/dist/Helpers/AuditEntries.d.ts +50 -0
  78. package/dist/Helpers/AuditEntries.js +96 -0
  79. package/dist/Helpers/ChatBackend.d.ts +8 -1
  80. package/dist/Helpers/ChatBackend.js +18 -0
  81. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  82. package/dist/Helpers/ChatHelpers.js +4 -1
  83. package/dist/Helpers/CrudColumns.d.ts +44 -6
  84. package/dist/Helpers/CrudColumns.js +63 -13
  85. package/dist/Helpers/CrudHelpers.d.ts +36 -1
  86. package/dist/Helpers/CrudHelpers.js +61 -0
  87. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  88. package/dist/Helpers/CrudPickerPlan.js +46 -0
  89. package/dist/Helpers/CrudRowForms.d.ts +18 -1
  90. package/dist/Helpers/CrudRowForms.js +35 -2
  91. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  92. package/dist/Helpers/DashboardHelpers.js +305 -0
  93. package/dist/Helpers/DomainCards.d.ts +140 -0
  94. package/dist/Helpers/DomainCards.js +370 -0
  95. package/dist/Helpers/FileAdapter.d.ts +82 -2
  96. package/dist/Helpers/FileAdapter.js +72 -1
  97. package/dist/Helpers/FormStore.d.ts +11 -1
  98. package/dist/Helpers/FormStore.js +66 -19
  99. package/dist/Helpers/InputConditions.d.ts +6 -0
  100. package/dist/Helpers/InputConditions.js +24 -0
  101. package/dist/Helpers/InputMemory.js +40 -30
  102. package/dist/Helpers/InputNarrowing.d.ts +38 -6
  103. package/dist/Helpers/InputNarrowing.js +102 -21
  104. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  105. package/dist/Helpers/MailClientHelpers.js +211 -0
  106. package/dist/Helpers/MessageBackend.d.ts +152 -0
  107. package/dist/Helpers/MessageBackend.js +329 -0
  108. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  109. package/dist/Helpers/MessageHelpers.js +573 -0
  110. package/dist/Helpers/MessageUnread.d.ts +95 -0
  111. package/dist/Helpers/MessageUnread.js +245 -0
  112. package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
  113. package/dist/Helpers/NexusDriveAdapter.js +32 -3
  114. package/dist/Helpers/NumberHelpers.d.ts +61 -0
  115. package/dist/Helpers/NumberHelpers.js +124 -8
  116. package/dist/Helpers/PagedPicker.d.ts +42 -0
  117. package/dist/Helpers/PagedPicker.js +85 -0
  118. package/dist/Helpers/RowsStore.d.ts +24 -0
  119. package/dist/Helpers/RowsStore.js +79 -10
  120. package/dist/Helpers/TableHelpers.d.ts +15 -2
  121. package/dist/Helpers/TableHelpers.js +28 -1
  122. package/dist/Helpers/TableStore.d.ts +7 -3
  123. package/dist/Helpers/TableStore.js +9 -4
  124. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  125. package/dist/Helpers/ToolbarFit.js +30 -0
  126. package/dist/Helpers/TreeStore.d.ts +75 -6
  127. package/dist/Helpers/TreeStore.js +182 -18
  128. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  129. package/dist/Interfaces/ApiInterfaces.js +10 -1
  130. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  131. package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
  132. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  133. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  134. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  135. package/dist/Interfaces/DomainInterfaces.js +1 -0
  136. package/dist/Interfaces/FileInterfaces.d.ts +21 -0
  137. package/dist/Interfaces/FileInterfaces.js +2 -0
  138. package/dist/Interfaces/FormInterfaces.d.ts +78 -11
  139. package/dist/Interfaces/InputInterfaces.d.ts +12 -5
  140. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  141. package/dist/Interfaces/MailInterfaces.js +1 -0
  142. package/dist/Interfaces/TableInterfaces.d.ts +28 -2
  143. package/dist/Services/JoinSources.d.ts +47 -6
  144. package/dist/Services/JoinSources.js +68 -33
  145. package/dist/Services/MessageApi.d.ts +18 -0
  146. package/dist/Services/MessageApi.js +64 -0
  147. package/dist/Shared.Index.d.ts +12 -0
  148. package/dist/Shared.Index.js +12 -0
  149. package/package.json +7 -1
  150. package/src/Styles/Nexus.Button.css +29 -0
  151. package/src/Styles/Nexus.Chat.css +114 -22
  152. package/src/Styles/Nexus.Crud.css +119 -28
  153. package/src/Styles/Nexus.Dashboard.css +763 -0
  154. package/src/Styles/Nexus.Domain.css +384 -0
  155. package/src/Styles/Nexus.Editor.css +20 -0
  156. package/src/Styles/Nexus.Files.css +27 -0
  157. package/src/Styles/Nexus.Index.css +3 -0
  158. package/src/Styles/Nexus.Input.css +31 -0
  159. package/src/Styles/Nexus.Mail.css +1062 -0
  160. package/src/Styles/Nexus.Popup.css +58 -0
  161. package/src/Styles/Nexus.Table.css +1458 -1449
  162. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  163. package/src/Styles/Nexus.Tree.css +10 -0
@@ -0,0 +1,329 @@
1
+ import { NexusModule } from "../Interfaces/ApiInterfaces.js";
2
+ import { apiEndpoint } from "./ApiRoutes.js";
3
+ import { NexusChatBackend } from "./ChatBackend.js";
4
+ /*
5
+ * How a mailbox talks to a backend: which endpoint serves each action, what each request sends, and how each answer is
6
+ * read. `MessageBackend` is what an app implements for its own backend; `NexusMessageBackend` is the Nexus Message
7
+ * module's `Mail` controller, and runs when nothing is registered. Nothing here calls anything - `apiMessageSource` does
8
+ * that - so this file is pure and can be checked without a server.
9
+ *
10
+ * A thread's lines are the chat's: the history and a reply answer with the chat's own rows, read by the chat's backend,
11
+ * so a mail thread is drawn by the chat's thread unchanged.
12
+ */
13
+ /** The actions a mailbox needs from a backend. */
14
+ export const MAIL_ACTIONS = {
15
+ /** A page of one list: inbox, unread, sent, archived. */
16
+ inbox: "Inbox",
17
+ /** A thread's head: its subject and its people. */
18
+ thread: "Thread",
19
+ /** A page of a thread's lines. */
20
+ history: "History",
21
+ /** A new message. */
22
+ compose: "Create",
23
+ /** A reply in a thread. */
24
+ reply: "Reply",
25
+ /** How far the reader has read a thread. */
26
+ read: "Read",
27
+ /** Threads put aside. */
28
+ archive: "Archive",
29
+ /** Threads brought back. */
30
+ unarchive: "Unarchive",
31
+ /** How much is unread, and the newest of it. */
32
+ unread: "Unread",
33
+ /** The people a message can be sent to. */
34
+ people: "People",
35
+ /** Who a new message goes to by default. */
36
+ defaults: "Defaults",
37
+ /** The counts beside a mail client's folders and types. */
38
+ folders: "Folders",
39
+ /** Whole conversations marked read. */
40
+ markRead: "MarkRead",
41
+ /** Conversations marked unread again. */
42
+ markUnread: "MarkUnread",
43
+ /** Conversations starred. */
44
+ star: "Star",
45
+ /** Stars taken away. */
46
+ unstar: "Unstar",
47
+ };
48
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49
+ const text = (value) => (typeof value === "string" && value.trim() !== "" ? value : null);
50
+ const count = (value) => {
51
+ const number = Number(value);
52
+ return Number.isFinite(number) ? number : 0;
53
+ };
54
+ const idOf = (value) => (typeof value === "string" || typeof value === "number" ? value : null);
55
+ /** Rows of an answer: an array, or the `results` of a page. */
56
+ function rowsOf(result) {
57
+ const list = Array.isArray(result) ? result : isRecord(result) && Array.isArray(result.results) ? result.results : [];
58
+ return list.filter(isRecord);
59
+ }
60
+ /**
61
+ * What a mailbox needs from a backend. Extend `NexusMessageBackend` for an app whose mail API differs, and register it
62
+ * once beside the app's API modules:
63
+ *
64
+ * @example
65
+ * class AppMessageBackend extends NexusMessageBackend {
66
+ * override readonly module = AppModule.Messages;
67
+ * }
68
+ * defineMessageBackend(new AppMessageBackend());
69
+ */
70
+ export class MessageBackend {
71
+ /** What opening a thread sends, as the call's `params`: its id in the path. */
72
+ threadParams(threadId) {
73
+ return { id: typeof threadId === "number" ? threadId : String(threadId) };
74
+ }
75
+ /** The optional doors this backend has: a mail client offers Mark read, Mark unread and stars only where they are. */
76
+ supports(action) {
77
+ return action === "folders" ? typeof this.readFolders === "function" : false;
78
+ }
79
+ }
80
+ /** The Nexus numbers of a conversation's kind (`Mail/Inbox`'s and `Mail/Folders`' `kind`). */
81
+ const NEXUS_THREAD_KINDS = { mail: 1, chat: 2, domain: 3 };
82
+ /** The Nexus numbers of a person's role on a thread (`EChatParticipantRoles`): 0 is a plain member of a chat. */
83
+ const NEXUS_ROLES = { 1: "from", 2: "to", 3: "cc", 4: "bcc" };
84
+ /** The Nexus numbers of the lists (`Mail/Inbox`'s `list`). */
85
+ const NEXUS_LISTS = { inbox: 1, unread: 2, sent: 3, archived: 4 };
86
+ /**
87
+ * The Nexus Message module's `Mail` controller (`App/Api/Mail/...`): POST `Inbox`, `History`, `Create`, `Reply` and
88
+ * `People`; GET `Thread/{id}`, `Unread` and `Defaults`; PUT `Read`, `Archive` and `Unarchive`. Pages answer
89
+ * `{ results, totalCount, filterCount }`; a person's role and a message's kind come as the backend's numbers and are
90
+ * read as this package's words. Override `module` or `controller` for another place, or any reader for another shape.
91
+ */
92
+ export class NexusMessageBackend extends MessageBackend {
93
+ /** The module the mail endpoints live in. `NexusModule.Message`. */
94
+ module = NexusModule.Message;
95
+ /** The controller the actions hang off. */
96
+ controller = "Mail";
97
+ /** A thread's lines are the chat's rows, read as the chat reads them. */
98
+ chat = new NexusChatBackend();
99
+ endpoint(action) {
100
+ // The Nexus module's value, as `NexusChatBackend` takes it: an app on modules of its own registers its own backend.
101
+ const module = this.module;
102
+ const path = `${this.controller}/${MAIL_ACTIONS[action]}`;
103
+ // `subject` words the failure toasts; the thread shows its own messages, so its calls send `toast: false`.
104
+ switch (action) {
105
+ case "thread":
106
+ return apiEndpoint.get(module, `${path}/{id}`, { action: "load", subject: "message" });
107
+ case "unread":
108
+ case "defaults":
109
+ return apiEndpoint.get(module, path, { action: "load", subject: "messages" });
110
+ case "read":
111
+ return apiEndpoint.put(module, path, { action: "update", subject: "message" });
112
+ case "folders":
113
+ return apiEndpoint.get(module, path, { action: "load", subject: "messages" });
114
+ case "archive":
115
+ case "unarchive":
116
+ case "markRead":
117
+ case "markUnread":
118
+ case "star":
119
+ case "unstar":
120
+ return apiEndpoint.put(module, path, { action: "update", subject: "messages" });
121
+ case "compose":
122
+ return apiEndpoint.post(module, path, { action: "send", subject: "message" });
123
+ case "reply":
124
+ return apiEndpoint.post(module, path, { action: "send", subject: "reply" });
125
+ default:
126
+ return apiEndpoint.post(module, path, { action: "load", subject: action === "people" ? "people" : "messages" });
127
+ }
128
+ }
129
+ inboxRequest(request) {
130
+ return {
131
+ list: NEXUS_LISTS[request.list],
132
+ ...(request.search?.trim() && { searchKeyword: request.search.trim() }),
133
+ ...(request.domainType !== undefined && { domainType: request.domainType }),
134
+ ...(request.domainType !== undefined && request.parentId !== undefined && { parentId: request.parentId }),
135
+ ...(request.threadKind && { kind: NEXUS_THREAD_KINDS[request.threadKind] }),
136
+ skipCount: request.skip,
137
+ maxCount: request.take,
138
+ };
139
+ }
140
+ readInbox(result) {
141
+ const rows = rowsOf(result).map(row => this.readInboxRow(row));
142
+ if (!isRecord(result))
143
+ return { rows };
144
+ const total = result.totalCount === undefined ? rows.length : count(result.totalCount);
145
+ return { rows, total, matches: result.filterCount === undefined ? total : count(result.filterCount) };
146
+ }
147
+ readInboxRow(row) {
148
+ const one = isRecord(row) ? row : {};
149
+ const domainType = this.toDomainType(one.domainType);
150
+ const fromAvatar = text(one.fromAvatar ?? one.fromPicture);
151
+ return {
152
+ id: idOf(one.id) ?? "",
153
+ subject: text(one.subject ?? one.title) ?? "",
154
+ fromId: idOf(one.fromId) ?? "",
155
+ fromName: text(one.fromName) ?? "",
156
+ myRole: this.toRole(one.myRole),
157
+ // `kind` on the wire is the conversation's (Mail 1, Chat 2, Domain 3); what it opened with is `messageType`.
158
+ kind: this.toHeadKind(one.messageType ?? one.headMessageType),
159
+ domainType,
160
+ domainLabel: text(one.domainLabel),
161
+ lastMessageTime: text(one.lastMessageTime ?? one.lastMessageAt) ?? "",
162
+ lastAuthorName: text(one.lastAuthorName),
163
+ lastPreview: typeof one.lastPreview === "string" ? one.lastPreview : "",
164
+ messageCount: count(one.messageCount),
165
+ unreadCount: count(one.unreadCount),
166
+ archived: one.archived === true,
167
+ threadKind: this.toThreadKind(one.kind, domainType),
168
+ ...(typeof (one.starred ?? one.isStarred) === "boolean" && { starred: (one.starred ?? one.isStarred) === true }),
169
+ ...(fromAvatar && { fromAvatar }),
170
+ };
171
+ }
172
+ readThread(result) {
173
+ const row = isRecord(result) ? result : {};
174
+ const people = (value) => (Array.isArray(value) ? value.filter(isRecord).map(person => this.toPerson(person)) : []);
175
+ const domain = isRecord(row.domain) ? this.toDomain(row.domain) : null;
176
+ return {
177
+ id: idOf(row.id) ?? "",
178
+ subject: text(row.subject ?? row.title) ?? "",
179
+ from: isRecord(row.from) ? this.toPerson(row.from) : { id: "", name: "Unknown" },
180
+ to: people(row.to),
181
+ cc: people(row.cc),
182
+ bcc: people(row.bcc),
183
+ myRole: this.toRole(row.myRole),
184
+ archived: row.archived === true,
185
+ canSend: row.canSend !== false,
186
+ createdTime: text(row.createdTime ?? row.creationTime) ?? "",
187
+ messageCount: count(row.messageCount),
188
+ unreadCount: count(row.unreadCount),
189
+ ...(domain && { domain }),
190
+ threadKind: this.toThreadKind(row.kind, domain?.domainType ?? null),
191
+ };
192
+ }
193
+ historyRequest(threadId, request) {
194
+ return { id: threadId, beforeId: request.before ?? null, afterId: request.after ?? null, maxCount: request.take };
195
+ }
196
+ readHistory(result) {
197
+ // The chat's own History answer, each line with its `messageType`, which the chat's reader takes as its kind.
198
+ return this.chat.readHistory(result);
199
+ }
200
+ replyRequest(threadId, draft) {
201
+ // A Reply is a mail line (`messageType` 1) only when asked: the backend's default is a chat line, as its older callers send.
202
+ return {
203
+ id: threadId,
204
+ body: draft.body,
205
+ clientId: draft.clientId,
206
+ ...(draft.replyToId !== undefined && draft.replyToId !== null && { replyToId: draft.replyToId }),
207
+ ...(draft.kind === "mail" && { messageType: 1 }),
208
+ };
209
+ }
210
+ readReply(result, draft) {
211
+ // The answer is the chat's line; its author is in it, so the scope the chat's reader falls back on is never read.
212
+ return this.chat.readMessage(result, { domain: 0, parentId: "", userId: "" }, draft);
213
+ }
214
+ readMarkRequest(threadId, upTo) {
215
+ return { id: threadId, messageId: upTo };
216
+ }
217
+ composeRequest(draft) {
218
+ return {
219
+ to: draft.to,
220
+ ...(draft.cc?.length && { cc: draft.cc }),
221
+ ...(draft.bcc?.length && { bcc: draft.bcc }),
222
+ subject: draft.subject,
223
+ body: draft.body,
224
+ clientId: draft.clientId,
225
+ };
226
+ }
227
+ readComposed(result) {
228
+ const row = isRecord(result) ? result : {};
229
+ return { id: idOf(row.id) ?? "", messageId: idOf(row.messageId) ?? "" };
230
+ }
231
+ markRequest(ids) {
232
+ return { ids };
233
+ }
234
+ readChanged(result) {
235
+ return isRecord(result) ? count(result.count ?? result.changed) : count(result);
236
+ }
237
+ readUnread(result) {
238
+ const row = isRecord(result) ? result : {};
239
+ return { count: count(row.count), newest: Array.isArray(row.newest) ? row.newest.filter(isRecord).map(one => this.readInboxRow(one)) : [] };
240
+ }
241
+ peopleRequest(query) {
242
+ return { ...(query.search.trim() && { searchKeyword: query.search.trim() }), skipCount: query.skip, maxCount: query.pageSize };
243
+ }
244
+ readPeople(result) {
245
+ const items = rowsOf(result).map(row => this.toPerson(row));
246
+ if (Array.isArray(result) || !isRecord(result))
247
+ return { items, hasMore: false, total: items.length };
248
+ // The dropdown reads a full page as one with more after it; the total is shown as "25 of 1,240".
249
+ return { items, total: count(result.filterCount ?? result.totalCount) };
250
+ }
251
+ readDefaults(result) {
252
+ const row = isRecord(result) ? result : {};
253
+ const people = (value) => (Array.isArray(value) ? value.filter(isRecord).map(person => this.toPerson(person)) : []);
254
+ return { to: people(row.to), cc: people(row.cc), bcc: people(row.bcc) };
255
+ }
256
+ readFolders(result) {
257
+ const row = isRecord(result) ? result : {};
258
+ const lists = Object.entries(NEXUS_LISTS);
259
+ const folders = (Array.isArray(row.folders) ? row.folders : []).filter(isRecord).flatMap(one => {
260
+ const written = String(one.list ?? "").toLowerCase();
261
+ const list = lists.find(([word, number]) => word === written || number === Number(one.list))?.[0];
262
+ return list ? [{ list, unread: count(one.unread) }] : [];
263
+ });
264
+ const kinds = (Array.isArray(row.kinds) ? row.kinds : []).filter(isRecord).map(one => {
265
+ const domainType = this.toDomainType(one.domainType);
266
+ return { threadKind: this.toThreadKind(one.kind, domainType), domainType: this.toThreadKind(one.kind, domainType) === "domain" ? domainType : null, unread: count(one.unread), total: count(one.total) };
267
+ });
268
+ return { folders, kinds };
269
+ }
270
+ supports(action) {
271
+ // The Nexus Message API's doors (card MAIL-hub). An app on an older one turns them off in a subclass.
272
+ return action === "folders" || action === "markRead" || action === "markUnread" || action === "star";
273
+ }
274
+ /** A conversation's kind: the wire's number (Mail 1, Chat 2, Domain 3) or a word; without one, mail or its record's kind. */
275
+ toThreadKind(value, domainType) {
276
+ const written = String(value ?? "").toLowerCase();
277
+ if (written === "2" || written === "chat")
278
+ return "chat";
279
+ if (written === "3" || written === "domain")
280
+ return "domain";
281
+ if (written === "1" || written === "mail")
282
+ return "mail";
283
+ return domainType === null ? "mail" : "domain";
284
+ }
285
+ /** A person. */
286
+ toPerson(row) {
287
+ return {
288
+ id: idOf(row.id ?? row.userId) ?? "",
289
+ name: text(row.name ?? row.fullName ?? row.userName) ?? "Unknown",
290
+ ...(row.isActive === false && { isActive: false }),
291
+ ...(text(row.avatar ?? row.picture) && { avatar: text(row.avatar ?? row.picture) }),
292
+ };
293
+ }
294
+ /** A role, from the backend's `EChatParticipantRoles` (1 From, 2 To, 3 Cc, 4 Bcc) or a word. A plain member reads as To. */
295
+ toRole(value) {
296
+ const written = String(value ?? "").toLowerCase();
297
+ if (written === "from" || written === "to" || written === "cc" || written === "bcc")
298
+ return written;
299
+ return NEXUS_ROLES[Number(value)] ?? "to";
300
+ }
301
+ /** What a thread opened with: the backend's `EChatMessageTypes` (1 Mail, 2 Notification) or a word. */
302
+ toHeadKind(value) {
303
+ const written = String(value ?? "").toLowerCase();
304
+ return written === "2" || written === "notification" ? "notification" : "mail";
305
+ }
306
+ /** A domain type is the app's own value: kept as it came, a number or a word, and none when it is none. */
307
+ toDomainType(value) {
308
+ if (typeof value === "number")
309
+ return value === 0 ? null : value;
310
+ return text(value);
311
+ }
312
+ /** The record a thread is about. */
313
+ toDomain(row) {
314
+ const domainType = this.toDomainType(row.domainType);
315
+ if (domainType === null)
316
+ return null;
317
+ return { domainType, id: idOf(row.id), label: text(row.label) };
318
+ }
319
+ }
320
+ let registered;
321
+ /** Makes an app's mail backend the one every mailbox uses, and returns it. Call it once, beside `defineApiModules`. */
322
+ export function defineMessageBackend(backend) {
323
+ registered = backend;
324
+ return backend;
325
+ }
326
+ /** The app's mail backend: the one given to `defineMessageBackend`, else the Nexus one. */
327
+ export function getMessageBackend() {
328
+ return (registered ??= new NexusMessageBackend());
329
+ }
@@ -0,0 +1,160 @@
1
+ import type { ChatConversationType, ChatDomainValue, ChatId, ChatMessage, ChatMessageKind } from "../Interfaces/ChatInterfaces.ts";
2
+ import type { MailInboxRow, MailList, MailPerson, MailRole, MailThreadHead, MessageSource } from "../Interfaces/MailInterfaces.ts";
3
+ import type { TableLoader } from "../Interfaces/TableInterfaces.ts";
4
+ import { type PagedPickerParams } from "./PagedPicker.ts";
5
+ /** A role's name on screen. */
6
+ export declare const MAIL_ROLE_LABELS: Readonly<Record<MailRole, string>>;
7
+ /** A list's name on screen. */
8
+ export declare const MAIL_LIST_LABELS: Readonly<Record<MailList, string>>;
9
+ /** What a person copied unseen reads over the reply box: their reply makes them a Cc, seen by everyone. */
10
+ export declare const MAIL_BCC_NOTE = "You were sent a blind copy. Replying shows you to everyone.";
11
+ /** The most people on one thread together, the sender aside. */
12
+ export declare const MAIL_PEOPLE_MAX = 50;
13
+ /** The longest subject. */
14
+ export declare const MAIL_SUBJECT_MAX = 100;
15
+ /** The longest message. */
16
+ export declare const MAIL_BODY_MAX = 4000;
17
+ /**
18
+ * The most unread lines `unread()` counts: a backend counts to here and stops (the Message module's `Mail/Unread`), so
19
+ * a bell writes "99+" from 100 on and says "more than 99".
20
+ */
21
+ export declare const MAIL_UNREAD_MAX = 100;
22
+ /** A list of people as one line: the first few names and how many more ("Kiran Shrestha, Ram Karki, Sita Rai +2"). */
23
+ export interface MailPeopleLine {
24
+ /** The people named. */
25
+ shown: readonly MailPerson[];
26
+ /** How many are left out. */
27
+ more: number;
28
+ /** The line: the names, then "+2". */
29
+ text: string;
30
+ /** Every name, for a tooltip and a screen reader: "Kiran Shrestha, Ram Karki and Sita Rai". */
31
+ full: string;
32
+ }
33
+ /** Folds a list of people to its first `max` names and a count of the rest: a To line with 50 people stays one line. */
34
+ export declare function mailPeopleLine(people: readonly MailPerson[], max?: number): MailPeopleLine;
35
+ /** The note a reader of a thread sees over the reply box, or `null`: a person copied unseen who may reply. */
36
+ export declare function mailBccNote(head: Pick<MailThreadHead, "myRole" | "canSend"> | null | undefined): string | null;
37
+ /** An id for a new message, made once per form so a retried send writes it once. */
38
+ export declare function newMailClientId(): string;
39
+ /**
40
+ * The people a new message can be sent to, as a dropdown's params: the source's search, paged by the server, every
41
+ * person named once seen. One for every To, Cc and Bcc box of a page.
42
+ *
43
+ * @example
44
+ * const people = mailPeoplePicker(mail);
45
+ * { type: EInputTypes.Dropdown, name: "to", label: "To", multiple: true, required: true, ...people }
46
+ */
47
+ export declare function mailPeoplePicker(source: Pick<MessageSource, "people">): PagedPickerParams<MailPerson>;
48
+ /** What a New message form opens with: the default people by id, and the message's own id for a retried send. */
49
+ export interface MailComposeValues extends Record<string, unknown> {
50
+ to: ChatId[];
51
+ cc: ChatId[];
52
+ bcc: ChatId[];
53
+ clientId: string;
54
+ }
55
+ /**
56
+ * A New message form's opening values, from the source's defaults (the reader's supervisor in To, or the people they
57
+ * chose): a CRUD's `form.createDefaults`. The people are handed to the picker, so the boxes name them at once. Give the
58
+ * form a `clientId` of its own in `newValues` too: a form whose defaults could not be read opens on those alone.
59
+ *
60
+ * @example form={{ newValues: () => ({ clientId: newMailClientId() }), createDefaults: mailComposeDefaults(mail, people) }}
61
+ */
62
+ export declare function mailComposeDefaults(source: Pick<MessageSource, "defaults">, picker?: PagedPickerParams<MailPerson>): (signal: AbortSignal) => Promise<MailComposeValues>;
63
+ /**
64
+ * A data table's loader over one list of a mailbox, paged by the source: `<DataTable paging="server" load={...} />`.
65
+ * `about` narrows it to the threads about one record.
66
+ */
67
+ export declare function mailInboxLoader(source: Pick<MessageSource, "inbox">, list: MailList, about?: {
68
+ domainType: ChatDomainValue;
69
+ parentId: ChatId;
70
+ }): TableLoader<MailInboxRow>;
71
+ /** A line of an inbox row's preview: the Markdown as text, on one line. */
72
+ export declare function mailPreviewText(preview: string | null | undefined): string;
73
+ /** One line of a thread `localMessageSource` starts with. */
74
+ export interface LocalMailLine {
75
+ /** Who wrote it; `null` for a line the system wrote. */
76
+ authorId: ChatId | null;
77
+ body: string;
78
+ /** Default: `mail` for a thread's first line, `chat` after it. */
79
+ kind?: ChatMessageKind;
80
+ /** When, as a UTC moment. */
81
+ createdAt: string;
82
+ /** The line it answers, by its place in the thread (0 the first). */
83
+ replyTo?: number;
84
+ }
85
+ /** A thread `localMessageSource` starts with. */
86
+ export interface LocalMailThread {
87
+ id?: ChatId;
88
+ /** A chat's name, when it has one: a chat without one is called after its other people. */
89
+ subject: string;
90
+ /**
91
+ * A chat, not a mail: `one-to-one` or `group`. Its lines are chat lines, it lists under the mailbox's Chat, and a mail
92
+ * client opens it in a chat popup. `from` started it, `to` are the others.
93
+ */
94
+ chat?: ChatConversationType;
95
+ /** Who had starred it. */
96
+ starredBy?: readonly ChatId[];
97
+ from: ChatId;
98
+ to: readonly ChatId[];
99
+ cc?: readonly ChatId[];
100
+ bcc?: readonly ChatId[];
101
+ /** The record it is about. */
102
+ domain?: {
103
+ domainType: ChatDomainValue;
104
+ id?: ChatId | null;
105
+ label?: string | null;
106
+ } | null;
107
+ /** Oldest first. The first is the mail itself. */
108
+ lines: readonly LocalMailLine[];
109
+ /** How far each person had read, as the place of the last line they read (0 the first), by id. Default: nothing, but their own lines. */
110
+ read?: Readonly<Record<string, number>>;
111
+ /** Who had put it aside. */
112
+ archivedBy?: readonly ChatId[];
113
+ /** Nobody may reply any more. */
114
+ closed?: boolean;
115
+ }
116
+ /** The parts of a local mailbox that can be made to fail, to show what a page does then. */
117
+ export type LocalMailFailure = "inbox" | "thread" | "history" | "reply" | "compose" | "people" | "defaults" | "unread" | "folders" | "marks";
118
+ export interface LocalMessageSourceOptions {
119
+ /** Who is reading. */
120
+ userId: ChatId;
121
+ /** Everyone in the company: who can be written to, and whose names the lines carry. */
122
+ people: readonly MailPerson[];
123
+ threads?: readonly LocalMailThread[];
124
+ /** Who a new message goes to by default, per person. */
125
+ defaults?: Readonly<Record<string, Partial<Record<"to" | "cc" | "bcc", readonly ChatId[]>>>>;
126
+ /** Milliseconds each call waits, so a demo shows its loading states. Default 0. */
127
+ delay?: number;
128
+ /** The moment a new line is written at. Default now. */
129
+ now?: () => string;
130
+ }
131
+ /** What `localMessageSource` gives besides a `MessageSource`: other readers of the same mailbox, and a module's post. */
132
+ export interface LocalMessageSource extends MessageSource {
133
+ /** The same mailbox, read by somebody else. */
134
+ as(userId: ChatId): MessageSource;
135
+ /**
136
+ * Writes a line as a module does after its own write (approved, sent back): a notification by default, the people
137
+ * it names added to the thread or raised (never lowered), the thread brought back to everyone else's inbox.
138
+ */
139
+ post(threadId: ChatId, line: {
140
+ authorId: ChatId | null;
141
+ body: string;
142
+ kind?: ChatMessageKind;
143
+ to?: readonly ChatId[];
144
+ cc?: readonly ChatId[];
145
+ }): ChatMessage;
146
+ /** The parts that answer with a failure until taken out again. */
147
+ readonly failing: Set<LocalMailFailure>;
148
+ }
149
+ /**
150
+ * A `MessageSource` in memory: a whole mailbox without a backend, for a demo page or a test. It keeps a backend's
151
+ * rules: a person sees the people copied unseen only on a thread they sent, or their own row; a reply by a person
152
+ * copied unseen makes them a Cc; Archive is each person's own and a line by someone else brings the thread back; a line
153
+ * is unread for everyone but its author (a line the system wrote for nobody); a new message or a reply sent again with
154
+ * the same id is written once. `as(userId)` reads the same mailbox as somebody else.
155
+ *
156
+ * The lists: **Inbox** every thread the reader is on and has not archived, **Unread** those with unread lines,
157
+ * **Sent** the ones they started, **Archived** the ones they put aside; newest line first; the search looks in the
158
+ * subject and the record's words.
159
+ */
160
+ export declare function localMessageSource(options: LocalMessageSourceOptions): LocalMessageSource;