@rapidmx/web-client 0.32.0 → 0.33.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 (47) hide show
  1. package/apps/shared/components/admin/diagnostics/SettingsTable.tsx +13 -2
  2. package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +24 -4
  3. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +3 -3
  4. package/apps/shared/components/calendar/EventDetails.tsx +10 -0
  5. package/apps/shared/components/calendar/EventFormParts.tsx +10 -0
  6. package/apps/shared/components/calendar/MonthView.tsx +10 -4
  7. package/apps/shared/components/contacts/ContactCard.tsx +4 -2
  8. package/apps/shared/components/contacts/ContactsSidebar.tsx +8 -2
  9. package/apps/shared/components/contacts/ContactsToolbar.tsx +6 -0
  10. package/apps/shared/components/mail/ConversationThreadPane.tsx +34 -2
  11. package/apps/shared/components/mail/compose/ComposeContext.tsx +19 -1
  12. package/apps/shared/components/mail/compose/draftResume.ts +54 -0
  13. package/apps/www/calendar/index.tsx +4 -4
  14. package/apps/www/contacts/index.tsx +75 -3
  15. package/apps/www/tasks/index.tsx +1 -1
  16. package/dist/apps/shared/components/admin/diagnostics/SettingsTable.js +3 -2
  17. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.js +6 -1
  18. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -3
  19. package/dist/apps/shared/components/calendar/EventDetails.js +2 -1
  20. package/dist/apps/shared/components/calendar/EventFormParts.js +2 -1
  21. package/dist/apps/shared/components/calendar/MonthView.d.ts +1 -1
  22. package/dist/apps/shared/components/calendar/MonthView.js +4 -4
  23. package/dist/apps/shared/components/contacts/ContactCard.js +4 -1
  24. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +6 -2
  25. package/dist/apps/shared/components/contacts/ContactsSidebar.js +3 -3
  26. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  27. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -1
  28. package/dist/apps/shared/components/mail/ConversationThreadPane.js +35 -1
  29. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +2 -0
  30. package/dist/apps/shared/components/mail/compose/ComposeContext.js +17 -1
  31. package/dist/apps/shared/components/mail/compose/draftResume.d.ts +10 -0
  32. package/dist/apps/shared/components/mail/compose/draftResume.js +48 -0
  33. package/dist/apps/www/calendar/index.js +4 -4
  34. package/dist/apps/www/contacts/index.js +63 -5
  35. package/dist/apps/www/tasks/index.js +1 -1
  36. package/dist/lib/calendar/recurrence.js +1 -1
  37. package/dist/lib/components/overlays/overlayStack.d.ts +6 -0
  38. package/dist/lib/components/overlays/overlayStack.js +10 -0
  39. package/dist/lib/components/pickers/MiniDatePicker.d.ts +1 -1
  40. package/dist/lib/components/pickers/MiniDatePicker.js +4 -4
  41. package/dist/lib/contacts/contactsApi.d.ts +12 -0
  42. package/dist/lib/contacts/contactsApi.js +8 -0
  43. package/lib/calendar/recurrence.ts +1 -1
  44. package/lib/components/overlays/overlayStack.ts +11 -0
  45. package/lib/components/pickers/MiniDatePicker.tsx +4 -4
  46. package/lib/contacts/contactsApi.ts +16 -0
  47. package/package.json +1 -1
@@ -7,7 +7,7 @@ import { pageTitle } from "../../shared/navigation/pageTitle.js";
7
7
  import { useNavigate } from "../../shared/navigation/index.js";
8
8
  import { useEffect, useMemo, useRef, useState } from "react";
9
9
  import { ApiRequestError } from "../../../lib/util/api.js";
10
- import { createContact, deleteContact, listContacts, listContactsInFolders, listDeletedContacts, setContactFavorite, updateContact, } from "../../../lib/contacts/contactsApi.js";
10
+ import { createContact, deleteContact, listContacts, ensureSuggestedContacts, listContactsInFolders, listDeletedContacts, setContactFavorite, updateContact, } from "../../../lib/contacts/contactsApi.js";
11
11
  import { VCARD_MAX_CHARS, VCARD_MAX_CONTACTS, contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
12
12
  import { withoutDuplicates } from "../../shared/components/contacts/participantDetails.js";
13
13
  import useIsMobile from "../../../lib/util/useIsMobile.js";
@@ -68,6 +68,9 @@ function ContactsContent({ userUid }) {
68
68
  const client = useApiClient();
69
69
  const [contacts, setContacts] = useState([]);
70
70
  const [deletedContacts, setDeletedContacts] = useState([]);
71
+ // The mailbox's Suggested Contacts folder - the people it has corresponded with - once the server has made it; `null` while unknown (an older server has none).
72
+ const [suggestedFolderUid, setSuggestedFolderUid] = useState(null);
73
+ const [suggestedContacts, setSuggestedContacts] = useState([]);
71
74
  const [deletedLoading, setDeletedLoading] = useState(false);
72
75
  const [deletedError, setDeletedError] = useState(null);
73
76
  const [loading, setLoading] = useState(true);
@@ -97,6 +100,9 @@ function ContactsContent({ userUid }) {
97
100
  }
98
101
  setLoading(true);
99
102
  setError(null);
103
+ if (suggestedFolderUid) {
104
+ void loadSuggested(suggestedFolderUid);
105
+ }
100
106
  return listAllPages((page) => listContacts(folderUid, { limit: LIST_PAGE_SIZE, page }, client))
101
107
  .then((result) => {
102
108
  setContacts(result.items);
@@ -108,6 +114,29 @@ function ContactsContent({ userUid }) {
108
114
  useEffect(() => {
109
115
  void reload();
110
116
  }, [folderUid]);
117
+ /** Reads the Suggested Contacts folder's contacts. A failure leaves the list as it was. */
118
+ function loadSuggested(suggestedUid) {
119
+ return listAllPages((page) => listContacts(suggestedUid, { limit: LIST_PAGE_SIZE, page }, client))
120
+ .then((result) => setSuggestedContacts(result.items))
121
+ .catch(() => undefined);
122
+ }
123
+ // Asks the server to turn the people this mailbox has corresponded with into suggested contacts (it does so once for each person, so this is cheap when
124
+ // nothing is new), then lists them. An older server, or one that refuses, simply has no such view.
125
+ useEffect(() => {
126
+ let cancelled = false;
127
+ // `ContactsShell` only renders this with a resolved mailbox (see the Deleted view's comment above).
128
+ void ensureSuggestedContacts(mailboxUid, client)
129
+ .then(async (result) => {
130
+ if (!cancelled) {
131
+ setSuggestedFolderUid(result.folderUid);
132
+ await loadSuggested(result.folderUid);
133
+ }
134
+ })
135
+ .catch(() => undefined);
136
+ return () => {
137
+ cancelled = true;
138
+ };
139
+ }, [mailboxUid]);
111
140
  // restapi only honors `?deleted=true` for a caller with both delete and update rights on the contacts *folder*
112
141
  // (its ACL, which inherits the mailbox's but can carry its own records) - anyone else silently gets the *live*
113
142
  // contacts back, which the Deleted view would then present as deleted. There is no client API for the caller's
@@ -165,6 +194,8 @@ function ContactsContent({ userUid }) {
165
194
  return contacts;
166
195
  case "favorites":
167
196
  return contacts.filter((c) => c.favorite);
197
+ case "suggested":
198
+ return suggestedContacts;
168
199
  case "list":
169
200
  return contacts.filter((c) => c.contactListUid === view.uid);
170
201
  case "category":
@@ -172,7 +203,7 @@ function ContactsContent({ userUid }) {
172
203
  case "deleted":
173
204
  return deletedContacts;
174
205
  }
175
- }, [contacts, deletedContacts, view]);
206
+ }, [contacts, deletedContacts, suggestedContacts, view]);
176
207
  const filtered = useMemo(() => {
177
208
  const q = query.trim().toLowerCase();
178
209
  if (!q) {
@@ -188,7 +219,7 @@ function ContactsContent({ userUid }) {
188
219
  });
189
220
  return arr;
190
221
  }, [filtered, sortColumn, sortDesc]);
191
- const selected = contacts.find((c) => c.uid === selectedUid) ?? null;
222
+ const selected = contacts.find((c) => c.uid === selectedUid) ?? suggestedContacts.find((c) => c.uid === selectedUid) ?? null;
192
223
  const isDeletedView = view.type === "deleted";
193
224
  const checkedContacts = sorted.filter((c) => checkedUids.has(c.uid));
194
225
  const allChecked = sorted.length > 0 && sorted.every((c) => checkedUids.has(c.uid));
@@ -344,6 +375,33 @@ function ContactsContent({ userUid }) {
344
375
  clearPinnedSignerCache();
345
376
  await reload();
346
377
  });
378
+ // A suggested contact becomes one of the mailbox's own: a copy in its contacts folder, and the suggestion removed (the person is not suggested again).
379
+ const handleAddToContacts = singleFlight(async () => {
380
+ for (const contact of checkedContacts) {
381
+ try {
382
+ await createContact({
383
+ mailboxUid: contact.mailboxUid,
384
+ folderUid: folderUid,
385
+ displayName: contact.displayName,
386
+ givenName: contact.givenName,
387
+ surname: contact.surname,
388
+ emails: contact.emails,
389
+ phones: contact.phones,
390
+ addresses: contact.addresses,
391
+ company: contact.company,
392
+ jobTitle: contact.jobTitle,
393
+ notes: contact.notes,
394
+ categories: contact.categories,
395
+ }, client);
396
+ await deleteContact(contact.uid, contact.version, client);
397
+ }
398
+ catch (err) {
399
+ notifyApiError(err, "Couldn't add some of the contacts");
400
+ }
401
+ }
402
+ setCheckedUids(new Set());
403
+ await reload();
404
+ });
347
405
  function handleExportVCard() {
348
406
  downloadTextFile(checkedContacts.length === 1 ? `${checkedContacts[0].displayName}.vcf` : "contacts.vcf", checkedContacts.length === 1 ? contactToVCard(checkedContacts[0]) : contactsToVCardFile(checkedContacts));
349
407
  }
@@ -392,10 +450,10 @@ function ContactsContent({ userUid }) {
392
450
  clearPinnedSignerCache();
393
451
  await reload();
394
452
  }
395
- return (_jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex flex-col lg:flex-row md:flex-none md:h-[calc(100dvh_-_var(--rr-header-h,4rem))] md:overflow-hidden", children: [_jsx(ContactsSidebar, { mailboxUid: mailboxUid, contacts: contacts, active: view, onSelect: handleSelectView, showDeleted: canViewDeleted }), _jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex", children: [_jsxs("div", { className: [
453
+ return (_jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex flex-col lg:flex-row md:flex-none md:h-[calc(100dvh_-_var(--rr-header-h,4rem))] md:overflow-hidden", children: [_jsx(ContactsSidebar, { mailboxUid: mailboxUid, contacts: contacts, active: view, onSelect: handleSelectView, showDeleted: canViewDeleted, suggestedCount: suggestedFolderUid ? suggestedContacts.length : undefined }), _jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex", children: [_jsxs("div", { className: [
396
454
  "w-full md:w-[clamp(16rem,32vw,26rem)] min-w-0 md:shrink-0 md:border-r border-border md:flex flex-col min-h-0",
397
455
  mode === "new" ? "hidden md:flex" : "flex",
398
- ].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button",
456
+ ].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onAddToContacts: view.type === "suggested" ? handleAddToContacts : undefined, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button",
399
457
  // A deleted contact has no pane to show (it can't be restored or opened), and its route would answer 404.
400
458
  disabled: isDeletedView, onClick: () => handleSelectRow(contact), className: "flex w-full min-w-0 items-center gap-2 text-left disabled:cursor-default", children: [_jsx(ContactPhotoAvatar, { lazy: true, displayName: contact.displayName, size: 28, contact: contact, email: contact.emails[0]?.address }), _jsxs("span", { className: "min-w-0 truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "max-w-0 truncate px-3 py-2 text-text-muted", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 min-h-0 flex-col md:flex", mode === "new" ? "flex" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "m-6 mb-0 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: [savedElsewhere.displayName, " was added to", " ", mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s contacts" })] })), mode === "new" && mailboxUid && folderUid ? (_jsx(ContactForm, { mailboxUid: mailboxUid, folderUid: folderUid, mailboxes: writableMailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-6", children: _jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected), canWrite: mailboxWritable, onKeysChanged: () => void reload(), onChanged: () => void reload(), canResolveKeys: ownsMailbox || delegateCanUpdate }) })) : (_jsx("div", { className: "flex flex-1 items-center justify-center p-6 text-center", children: _jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }) }))] })] }), _jsxs(Modal, { open: confirmingBulkDelete, onClose: () => setConfirmingBulkDelete(false), title: "Delete contacts", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Delete ", checkedContacts.length, " selected ", checkedContacts.length === 1 ? "contact" : "contacts", "? They move to Deleted contacts."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingBulkDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleBulkDelete, children: "Delete" })] })] }), isMobile && mode !== "new" && _jsx(FloatingActionButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: handleNew })] }));
401
459
  }
@@ -42,7 +42,7 @@ function bucketFor(task, now) {
42
42
  if (isToday(due)) {
43
43
  return "Today";
44
44
  }
45
- if (!isAfter(due, endOfWeek(now, { weekStartsOn: 1 }))) {
45
+ if (!isAfter(due, endOfWeek(now, { weekStartsOn: 0 }))) {
46
46
  return "This Week";
47
47
  }
48
48
  return "Later";
@@ -261,7 +261,7 @@ export function describeRecurrence(rule) {
261
261
  const rrule = buildRRule(rule, new Date());
262
262
  return rrule.toText();
263
263
  }
264
- export const WEEKDAY_CODES = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
264
+ export const WEEKDAY_CODES = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
265
265
  export const WEEKDAY_LABELS = {
266
266
  MO: "Mon",
267
267
  TU: "Tue",
@@ -16,6 +16,12 @@ import { RefObject } from "react";
16
16
  * belongs above it regardless of effect ordering - see `push()`.
17
17
  */
18
18
  export declare const OverlayDepthContext: import("react").Context<number>;
19
+ /**
20
+ * Closes every open overlay at once - what a caller does when it hands the reader over to something outside them (a compose window opened from a
21
+ * contact card, which sits on top of the event card it was reached from): leaving the cards underneath open would leave the reader a dialog they
22
+ * never asked to keep, in front of the window they just opened. Each overlay's own `onClose` runs, as for Escape.
23
+ */
24
+ export declare function closeAllOverlays(): void;
19
25
  /**
20
26
  * Wires one open overlay into the stack: on open, remembers the previously focused element and moves
21
27
  * focus into `dialogRef`'s element; while open *and topmost*, Escape calls `onClose` and Tab/Shift+Tab
@@ -97,6 +97,16 @@ function push(entry) {
97
97
  }
98
98
  stack.splice(index, 0, entry);
99
99
  }
100
+ /**
101
+ * Closes every open overlay at once - what a caller does when it hands the reader over to something outside them (a compose window opened from a
102
+ * contact card, which sits on top of the event card it was reached from): leaving the cards underneath open would leave the reader a dialog they
103
+ * never asked to keep, in front of the window they just opened. Each overlay's own `onClose` runs, as for Escape.
104
+ */
105
+ export function closeAllOverlays() {
106
+ for (const entry of [...stack]) {
107
+ entry.onEscape();
108
+ }
109
+ }
100
110
  function remove(entry) {
101
111
  stack.splice(stack.indexOf(entry), 1);
102
112
  if (stack.length === 0) {
@@ -5,7 +5,7 @@ export interface MiniDatePickerProps {
5
5
  onSelect: (date: Date) => void;
6
6
  }
7
7
  /** A compact month-grid date picker for the Calendar sidebar, reusing `MonthView`'s own grid-generation
8
- * approach (`date-fns`'s `startOfWeek`/`endOfWeek`/`eachDayOfInterval`, Monday-start) rather than
8
+ * approach (`date-fns`'s `startOfWeek`/`endOfWeek`/`eachDayOfInterval`, Sunday-start) rather than
9
9
  * re-deriving it — this component just renders that same grid far smaller and without any event data.
10
10
  * Its own displayed month tracks `selected` (so navigating the main view keeps this in sync) but can
11
11
  * also be paged independently via the arrows without moving the main view until a day is actually
@@ -5,9 +5,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useState } from "react";
7
7
  import { addMonths, eachDayOfInterval, endOfMonth, endOfWeek, format, isSameDay, isSameMonth, isToday, startOfMonth, startOfWeek } from "date-fns";
8
- const WEEKDAY_LABELS = ["M", "T", "W", "T", "F", "S", "S"];
8
+ const WEEKDAY_LABELS = ["S", "M", "T", "W", "T", "F", "S"];
9
9
  /** A compact month-grid date picker for the Calendar sidebar, reusing `MonthView`'s own grid-generation
10
- * approach (`date-fns`'s `startOfWeek`/`endOfWeek`/`eachDayOfInterval`, Monday-start) rather than
10
+ * approach (`date-fns`'s `startOfWeek`/`endOfWeek`/`eachDayOfInterval`, Sunday-start) rather than
11
11
  * re-deriving it — this component just renders that same grid far smaller and without any event data.
12
12
  * Its own displayed month tracks `selected` (so navigating the main view keeps this in sync) but can
13
13
  * also be paged independently via the arrows without moving the main view until a day is actually
@@ -23,8 +23,8 @@ export default function MiniDatePicker({ selected, onSelect }) {
23
23
  setTrackedSelectedMonth(startOfMonth(selected));
24
24
  setShownMonth(startOfMonth(selected));
25
25
  }
26
- const gridStart = startOfWeek(startOfMonth(shownMonth), { weekStartsOn: 1 });
27
- const gridEnd = endOfWeek(endOfMonth(shownMonth), { weekStartsOn: 1 });
26
+ const gridStart = startOfWeek(startOfMonth(shownMonth), { weekStartsOn: 0 });
27
+ const gridEnd = endOfWeek(endOfMonth(shownMonth), { weekStartsOn: 0 });
28
28
  const days = eachDayOfInterval({ start: gridStart, end: gridEnd });
29
29
  return (_jsxs("nav", { "aria-label": "Mini calendar", className: "p-2", children: [_jsxs("div", { className: "flex items-center justify-between mb-1.5", children: [_jsx("button", { type: "button", "aria-label": "Previous month", onClick: () => setShownMonth((m) => addMonths(m, -1)), className: "w-6 h-6 text-xs rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("span", { className: "text-xs font-semibold", children: format(shownMonth, "MMMM yyyy") }), _jsx("button", { type: "button", "aria-label": "Next month", onClick: () => setShownMonth((m) => addMonths(m, 1)), className: "w-6 h-6 text-xs rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsxs("div", { className: "grid grid-cols-7 gap-0.5", children: [WEEKDAY_LABELS.map((label, i) => (_jsx("div", { className: "text-[10px] text-text-muted text-center", children: label }, i))), days.map((day) => (_jsx("button", { type: "button", onClick: () => {
30
30
  onSelect(day);
@@ -122,6 +122,18 @@ export declare function fetchSignerKeyState(folderUids: string[], address: strin
122
122
  * is browse-only until that lands.
123
123
  */
124
124
  export declare function listDeletedContacts(folderUid: string, params?: ListParams, client?: ApiClient): Promise<Contact[]>;
125
+ /** What `ensureSuggestedContacts()` did: the mailbox's Suggested Contacts folder, how many contacts it just added, and how many people are still waiting for the next call. */
126
+ export interface SuggestedContactsResult {
127
+ folderUid: string;
128
+ created: number;
129
+ remaining: number;
130
+ }
131
+ /**
132
+ * Makes the people `mailboxUid` has exchanged mail or invitations with into contacts of its Suggested Contacts folder (a folder of their own, kept apart
133
+ * from the mailbox's contacts), and returns that folder. Idempotent: a person already a contact, or already suggested once, is never added again, so it
134
+ * is called whenever the contacts are shown.
135
+ */
136
+ export declare function ensureSuggestedContacts(mailboxUid: string, client?: ApiClient): Promise<SuggestedContactsResult>;
125
137
  export declare function getContact(uid: string, client?: ApiClient): Promise<Contact>;
126
138
  export interface ContactInput {
127
139
  mailboxUid: string;
@@ -100,6 +100,14 @@ export async function fetchSignerKeyState(folderUids, address, client) {
100
100
  export function listDeletedContacts(folderUid, params = {}, client) {
101
101
  return withClient(client, `/mail/contacts?${buildQuery(params, { folderUid, deleted: "true", sort: CONTACT_SORT })}`);
102
102
  }
103
+ /**
104
+ * Makes the people `mailboxUid` has exchanged mail or invitations with into contacts of its Suggested Contacts folder (a folder of their own, kept apart
105
+ * from the mailbox's contacts), and returns that folder. Idempotent: a person already a contact, or already suggested once, is never added again, so it
106
+ * is called whenever the contacts are shown.
107
+ */
108
+ export function ensureSuggestedContacts(mailboxUid, client) {
109
+ return withClient(client, `/mail/directory/suggested-contacts?${buildQuery({}, { mailboxUid })}`, { method: "POST" });
110
+ }
103
111
  export function getContact(uid, client) {
104
112
  return withClient(client, `/mail/contacts/${encodeURIComponent(uid)}`);
105
113
  }
@@ -324,7 +324,7 @@ export function describeRecurrence(rule: RecurrenceRule): string {
324
324
  return rrule.toText();
325
325
  }
326
326
 
327
- export const WEEKDAY_CODES: WeekdayCode[] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
327
+ export const WEEKDAY_CODES: WeekdayCode[] = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
328
328
  export const WEEKDAY_LABELS: Record<WeekdayCode, string> = {
329
329
  MO: "Mon",
330
330
  TU: "Tue",
@@ -115,6 +115,17 @@ function push(entry: OverlayEntry) {
115
115
  stack.splice(index, 0, entry);
116
116
  }
117
117
 
118
+ /**
119
+ * Closes every open overlay at once - what a caller does when it hands the reader over to something outside them (a compose window opened from a
120
+ * contact card, which sits on top of the event card it was reached from): leaving the cards underneath open would leave the reader a dialog they
121
+ * never asked to keep, in front of the window they just opened. Each overlay's own `onClose` runs, as for Escape.
122
+ */
123
+ export function closeAllOverlays(): void {
124
+ for (const entry of [...stack]) {
125
+ entry.onEscape();
126
+ }
127
+ }
128
+
118
129
  function remove(entry: OverlayEntry) {
119
130
  stack.splice(stack.indexOf(entry), 1);
120
131
  if (stack.length === 0) {
@@ -11,10 +11,10 @@ export interface MiniDatePickerProps {
11
11
  onSelect: (date: Date) => void;
12
12
  }
13
13
 
14
- const WEEKDAY_LABELS = ["M", "T", "W", "T", "F", "S", "S"];
14
+ const WEEKDAY_LABELS = ["S", "M", "T", "W", "T", "F", "S"];
15
15
 
16
16
  /** A compact month-grid date picker for the Calendar sidebar, reusing `MonthView`'s own grid-generation
17
- * approach (`date-fns`'s `startOfWeek`/`endOfWeek`/`eachDayOfInterval`, Monday-start) rather than
17
+ * approach (`date-fns`'s `startOfWeek`/`endOfWeek`/`eachDayOfInterval`, Sunday-start) rather than
18
18
  * re-deriving it — this component just renders that same grid far smaller and without any event data.
19
19
  * Its own displayed month tracks `selected` (so navigating the main view keeps this in sync) but can
20
20
  * also be paged independently via the arrows without moving the main view until a day is actually
@@ -31,8 +31,8 @@ export default function MiniDatePicker({ selected, onSelect }: MiniDatePickerPro
31
31
  setShownMonth(startOfMonth(selected));
32
32
  }
33
33
 
34
- const gridStart = startOfWeek(startOfMonth(shownMonth), { weekStartsOn: 1 });
35
- const gridEnd = endOfWeek(endOfMonth(shownMonth), { weekStartsOn: 1 });
34
+ const gridStart = startOfWeek(startOfMonth(shownMonth), { weekStartsOn: 0 });
35
+ const gridEnd = endOfWeek(endOfMonth(shownMonth), { weekStartsOn: 0 });
36
36
  const days = eachDayOfInterval({ start: gridStart, end: gridEnd });
37
37
 
38
38
  return (
@@ -203,6 +203,22 @@ export function listDeletedContacts(folderUid: string, params: ListParams = {},
203
203
  );
204
204
  }
205
205
 
206
+ /** What `ensureSuggestedContacts()` did: the mailbox's Suggested Contacts folder, how many contacts it just added, and how many people are still waiting for the next call. */
207
+ export interface SuggestedContactsResult {
208
+ folderUid: string;
209
+ created: number;
210
+ remaining: number;
211
+ }
212
+
213
+ /**
214
+ * Makes the people `mailboxUid` has exchanged mail or invitations with into contacts of its Suggested Contacts folder (a folder of their own, kept apart
215
+ * from the mailbox's contacts), and returns that folder. Idempotent: a person already a contact, or already suggested once, is never added again, so it
216
+ * is called whenever the contacts are shown.
217
+ */
218
+ export function ensureSuggestedContacts(mailboxUid: string, client?: ApiClient): Promise<SuggestedContactsResult> {
219
+ return withClient(client, `/mail/directory/suggested-contacts?${buildQuery({}, { mailboxUid })}`, { method: "POST" });
220
+ }
221
+
206
222
  export function getContact(uid: string, client?: ApiClient): Promise<Contact> {
207
223
  return withClient(client, `/mail/contacts/${encodeURIComponent(uid)}`);
208
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidmx/web-client",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "RapidMX's webmail and admin console React UI for web-based clients.",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/rapidmx/web-client",