@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.
- package/apps/shared/components/admin/diagnostics/SettingsTable.tsx +13 -2
- package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +24 -4
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +3 -3
- package/apps/shared/components/calendar/EventDetails.tsx +10 -0
- package/apps/shared/components/calendar/EventFormParts.tsx +10 -0
- package/apps/shared/components/calendar/MonthView.tsx +10 -4
- package/apps/shared/components/contacts/ContactCard.tsx +4 -2
- package/apps/shared/components/contacts/ContactsSidebar.tsx +8 -2
- package/apps/shared/components/contacts/ContactsToolbar.tsx +6 -0
- package/apps/shared/components/mail/ConversationThreadPane.tsx +34 -2
- package/apps/shared/components/mail/compose/ComposeContext.tsx +19 -1
- package/apps/shared/components/mail/compose/draftResume.ts +54 -0
- package/apps/www/calendar/index.tsx +4 -4
- package/apps/www/contacts/index.tsx +75 -3
- package/apps/www/tasks/index.tsx +1 -1
- package/dist/apps/shared/components/admin/diagnostics/SettingsTable.js +3 -2
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.js +6 -1
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -3
- package/dist/apps/shared/components/calendar/EventDetails.js +2 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +2 -1
- package/dist/apps/shared/components/calendar/MonthView.d.ts +1 -1
- package/dist/apps/shared/components/calendar/MonthView.js +4 -4
- package/dist/apps/shared/components/contacts/ContactCard.js +4 -1
- package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +6 -2
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +3 -3
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +35 -1
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +2 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +17 -1
- package/dist/apps/shared/components/mail/compose/draftResume.d.ts +10 -0
- package/dist/apps/shared/components/mail/compose/draftResume.js +48 -0
- package/dist/apps/www/calendar/index.js +4 -4
- package/dist/apps/www/contacts/index.js +63 -5
- package/dist/apps/www/tasks/index.js +1 -1
- package/dist/lib/calendar/recurrence.js +1 -1
- package/dist/lib/components/overlays/overlayStack.d.ts +6 -0
- package/dist/lib/components/overlays/overlayStack.js +10 -0
- package/dist/lib/components/pickers/MiniDatePicker.d.ts +1 -1
- package/dist/lib/components/pickers/MiniDatePicker.js +4 -4
- package/dist/lib/contacts/contactsApi.d.ts +12 -0
- package/dist/lib/contacts/contactsApi.js +8 -0
- package/lib/calendar/recurrence.ts +1 -1
- package/lib/components/overlays/overlayStack.ts +11 -0
- package/lib/components/pickers/MiniDatePicker.tsx +4 -4
- package/lib/contacts/contactsApi.ts +16 -0
- 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
|
}
|
|
@@ -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"
|
|
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`,
|
|
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"
|
|
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`,
|
|
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:
|
|
27
|
-
const gridEnd = endOfWeek(endOfMonth(shownMonth), { weekStartsOn:
|
|
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"
|
|
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"
|
|
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`,
|
|
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:
|
|
35
|
-
const gridEnd = endOfWeek(endOfMonth(shownMonth), { weekStartsOn:
|
|
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