@rapidmx/web-client 0.2.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/LICENSE +382 -0
- package/apps/admin/_404.tsx +9 -0
- package/apps/admin/_500.tsx +9 -0
- package/apps/admin/_layout.tsx +41 -0
- package/apps/admin/audit-log/index.tsx +187 -0
- package/apps/admin/branding/index.tsx +404 -0
- package/apps/admin/distribution-lists/[uid].tsx +70 -0
- package/apps/admin/distribution-lists/index.tsx +115 -0
- package/apps/admin/distribution-lists/new/index.tsx +114 -0
- package/apps/admin/domains/[uid].tsx +259 -0
- package/apps/admin/domains/index.tsx +125 -0
- package/apps/admin/domains/new/index.tsx +84 -0
- package/apps/admin/index.tsx +82 -0
- package/apps/admin/ingest-queue/index.tsx +116 -0
- package/apps/admin/mailboxes/[uid].tsx +126 -0
- package/apps/admin/mailboxes/new/index.tsx +291 -0
- package/apps/admin/quarantine/index.tsx +171 -0
- package/apps/admin/transport-rules/[uid].tsx +143 -0
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +75 -0
- package/apps/admin/transport-rules/index.tsx +117 -0
- package/apps/admin/transport-rules/new/index.tsx +105 -0
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +117 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +186 -0
- package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +73 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +154 -0
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +124 -0
- package/apps/shared/components/booking/AvailabilityEditor.tsx +118 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +119 -0
- package/apps/shared/components/calendar/EventModal.tsx +513 -0
- package/apps/shared/components/calendar/MonthView.tsx +139 -0
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +180 -0
- package/apps/shared/components/calendar/ResourcePicker.tsx +80 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +137 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +198 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +211 -0
- package/apps/shared/components/contacts/ContactDetailPane.tsx +99 -0
- package/apps/shared/components/contacts/ContactForm.tsx +314 -0
- package/apps/shared/components/contacts/ContactsSidebar.tsx +242 -0
- package/apps/shared/components/contacts/ContactsToolbar.tsx +115 -0
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +197 -0
- package/apps/shared/components/layout/AppShell.tsx +165 -0
- package/apps/shared/components/layout/BrandingChrome.tsx +25 -0
- package/apps/shared/components/layout/MailboxProvisioning.tsx +110 -0
- package/apps/shared/components/layout/UserMenu.tsx +150 -0
- package/apps/shared/components/mail/ConversationList.tsx +61 -0
- package/apps/shared/components/mail/ConversationThreadPane.tsx +174 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +390 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +114 -0
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +418 -0
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +518 -0
- package/apps/shared/components/mail/compose/EmojiPicker.tsx +64 -0
- package/apps/shared/components/mail/compose/GifPicker.tsx +82 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +83 -0
- package/apps/shared/components/mail/compose/ScheduleSendPicker.tsx +64 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +287 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +276 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +223 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +201 -0
- package/apps/shared/components/tasks/TasksToolbar.tsx +68 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +199 -0
- package/apps/shared/styles/app.css +265 -0
- package/apps/www/_404.tsx +9 -0
- package/apps/www/_500.tsx +9 -0
- package/apps/www/_layout.tsx +45 -0
- package/apps/www/calendar/index.tsx +407 -0
- package/apps/www/contacts/[uid].tsx +88 -0
- package/apps/www/contacts/index.tsx +436 -0
- package/apps/www/index.tsx +375 -0
- package/apps/www/messages/[uid].tsx +70 -0
- package/apps/www/settings/auto-reply/index.tsx +129 -0
- package/apps/www/settings/booking-types/[uid].tsx +300 -0
- package/apps/www/settings/booking-types/index.tsx +98 -0
- package/apps/www/settings/booking-types/new/index.tsx +212 -0
- package/apps/www/settings/filters/[uid].tsx +155 -0
- package/apps/www/settings/filters/_mailFilterRuleConfig.tsx +102 -0
- package/apps/www/settings/filters/index.tsx +99 -0
- package/apps/www/settings/filters/new/index.tsx +126 -0
- package/apps/www/settings/focused-inbox/index.tsx +148 -0
- package/apps/www/settings/read-receipts/index.tsx +125 -0
- package/apps/www/settings/signatures/[uid].tsx +167 -0
- package/apps/www/settings/signatures/index.tsx +85 -0
- package/apps/www/settings/signatures/new/index.tsx +129 -0
- package/apps/www/settings/signatures/signatureDefaults.ts +36 -0
- package/apps/www/tasks/index.tsx +530 -0
- package/dist/apps/admin/_404.js +4 -0
- package/dist/apps/admin/_500.js +4 -0
- package/dist/apps/admin/_layout.js +10 -0
- package/dist/apps/admin/audit-log/index.js +66 -0
- package/dist/apps/admin/branding/index.js +126 -0
- package/dist/apps/admin/distribution-lists/[uid].js +34 -0
- package/dist/apps/admin/distribution-lists/index.js +31 -0
- package/dist/apps/admin/distribution-lists/new/index.js +51 -0
- package/dist/apps/admin/domains/[uid].js +102 -0
- package/dist/apps/admin/domains/index.js +31 -0
- package/dist/apps/admin/domains/new/index.js +41 -0
- package/dist/apps/admin/index.js +32 -0
- package/dist/apps/admin/ingest-queue/index.js +48 -0
- package/dist/apps/admin/mailboxes/[uid].js +59 -0
- package/dist/apps/admin/mailboxes/new/index.js +93 -0
- package/dist/apps/admin/quarantine/index.js +66 -0
- package/dist/apps/admin/transport-rules/[uid].js +87 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +37 -0
- package/dist/apps/admin/transport-rules/index.js +32 -0
- package/dist/apps/admin/transport-rules/new/index.js +50 -0
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +64 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +94 -0
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.js +16 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +59 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +67 -0
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +45 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +44 -0
- package/dist/apps/shared/components/calendar/EventModal.js +167 -0
- package/dist/apps/shared/components/calendar/MonthView.js +38 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +55 -0
- package/dist/apps/shared/components/calendar/ResourcePicker.js +36 -0
- package/dist/apps/shared/components/calendar/SplitDayView.js +44 -0
- package/dist/apps/shared/components/calendar/TimeGridView.js +58 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +96 -0
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -0
- package/dist/apps/shared/components/contacts/ContactForm.js +85 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +105 -0
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +32 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +93 -0
- package/dist/apps/shared/components/layout/AppShell.js +68 -0
- package/dist/apps/shared/components/layout/BrandingChrome.js +18 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +75 -0
- package/dist/apps/shared/components/layout/UserMenu.js +59 -0
- package/dist/apps/shared/components/mail/ConversationList.js +17 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +112 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +153 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +55 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +88 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +261 -0
- package/dist/apps/shared/components/mail/compose/EmojiPicker.js +25 -0
- package/dist/apps/shared/components/mail/compose/GifPicker.js +36 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +60 -0
- package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.js +33 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +139 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +78 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +100 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +81 -0
- package/dist/apps/shared/components/tasks/TasksToolbar.js +15 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +92 -0
- package/dist/apps/shared/styles/app.css +265 -0
- package/dist/apps/www/_404.js +4 -0
- package/dist/apps/www/_500.js +4 -0
- package/dist/apps/www/_layout.js +8 -0
- package/dist/apps/www/calendar/index.js +228 -0
- package/dist/apps/www/contacts/[uid].js +62 -0
- package/dist/apps/www/contacts/index.js +262 -0
- package/dist/apps/www/index.js +228 -0
- package/dist/apps/www/messages/[uid].js +47 -0
- package/dist/apps/www/settings/auto-reply/index.js +54 -0
- package/dist/apps/www/settings/booking-types/[uid].js +129 -0
- package/dist/apps/www/settings/booking-types/index.js +29 -0
- package/dist/apps/www/settings/booking-types/new/index.js +86 -0
- package/dist/apps/www/settings/filters/[uid].js +94 -0
- package/dist/apps/www/settings/filters/_mailFilterRuleConfig.js +65 -0
- package/dist/apps/www/settings/filters/index.js +30 -0
- package/dist/apps/www/settings/filters/new/index.js +61 -0
- package/dist/apps/www/settings/focused-inbox/index.js +69 -0
- package/dist/apps/www/settings/read-receipts/index.js +51 -0
- package/dist/apps/www/settings/signatures/[uid].js +96 -0
- package/dist/apps/www/settings/signatures/index.js +34 -0
- package/dist/apps/www/settings/signatures/new/index.js +63 -0
- package/dist/apps/www/settings/signatures/signatureDefaults.js +30 -0
- package/dist/apps/www/tasks/index.js +280 -0
- package/package.json +87 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { deleteBookingType, getBookingType, updateBookingType, } from "@rapidmx/react-shared/booking/bookingApi.js";
|
|
9
|
+
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
10
|
+
import AvailabilityEditor from "../../../shared/components/booking/AvailabilityEditor.js";
|
|
11
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
13
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
14
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
15
|
+
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
16
|
+
export default function BookingTypeDetailPage(props) {
|
|
17
|
+
return (_jsx(SettingsShell, { ...props, active: "booking-types", children: _jsx(BookingTypeDetailContent, { uid: props.params.uid }) }));
|
|
18
|
+
}
|
|
19
|
+
function BookingTypeDetailContent({ uid }) {
|
|
20
|
+
const { mailboxUid } = useSettingsShell();
|
|
21
|
+
const [bookingType, setBookingType] = useState(null);
|
|
22
|
+
const [name, setName] = useState("");
|
|
23
|
+
const [description, setDescription] = useState("");
|
|
24
|
+
const [hostDisplayName, setHostDisplayName] = useState("");
|
|
25
|
+
const [durationMinutes, setDurationMinutes] = useState(30);
|
|
26
|
+
const [timezone, setTimezone] = useState("");
|
|
27
|
+
const [availability, setAvailability] = useState([]);
|
|
28
|
+
const [minimumNoticeMinutes, setMinimumNoticeMinutes] = useState(60);
|
|
29
|
+
const [bookingWindowDays, setBookingWindowDays] = useState(30);
|
|
30
|
+
const [requiresApproval, setRequiresApproval] = useState(false);
|
|
31
|
+
const [enabled, setEnabled] = useState(true);
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
const [error, setError] = useState(null);
|
|
34
|
+
const [saving, setSaving] = useState(false);
|
|
35
|
+
const [saved, setSaved] = useState(false);
|
|
36
|
+
const [copied, setCopied] = useState(false);
|
|
37
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
38
|
+
const [deleting, setDeleting] = useState(false);
|
|
39
|
+
const [deleteError, setDeleteError] = useState(null);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
setError(null);
|
|
43
|
+
getBookingType(uid)
|
|
44
|
+
.then((bt) => {
|
|
45
|
+
setBookingType(bt);
|
|
46
|
+
setName(bt.name);
|
|
47
|
+
setDescription(bt.description ?? "");
|
|
48
|
+
setHostDisplayName(bt.hostDisplayName);
|
|
49
|
+
setDurationMinutes(bt.durationMinutes);
|
|
50
|
+
setTimezone(bt.timezone);
|
|
51
|
+
setAvailability(bt.availability);
|
|
52
|
+
setMinimumNoticeMinutes(bt.minimumNoticeMinutes);
|
|
53
|
+
setBookingWindowDays(bt.bookingWindowDays);
|
|
54
|
+
setRequiresApproval(bt.requiresApproval);
|
|
55
|
+
setEnabled(bt.enabled);
|
|
56
|
+
})
|
|
57
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this booking link."))
|
|
58
|
+
.finally(() => setLoading(false));
|
|
59
|
+
}, [uid]);
|
|
60
|
+
async function handleSubmit(e) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
setError(null);
|
|
63
|
+
setSaved(false);
|
|
64
|
+
setSaving(true);
|
|
65
|
+
try {
|
|
66
|
+
const updated = await updateBookingType({
|
|
67
|
+
uid: bookingType.uid,
|
|
68
|
+
version: bookingType.version,
|
|
69
|
+
name,
|
|
70
|
+
description: description.trim() || undefined,
|
|
71
|
+
hostDisplayName,
|
|
72
|
+
durationMinutes,
|
|
73
|
+
timezone,
|
|
74
|
+
availability,
|
|
75
|
+
minimumNoticeMinutes,
|
|
76
|
+
bookingWindowDays,
|
|
77
|
+
requiresApproval,
|
|
78
|
+
enabled,
|
|
79
|
+
});
|
|
80
|
+
setBookingType(updated);
|
|
81
|
+
setSaved(true);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not save this booking link.");
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
setSaving(false);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function handleCopy(value) {
|
|
91
|
+
try {
|
|
92
|
+
await navigator.clipboard.writeText(value);
|
|
93
|
+
setCopied(true);
|
|
94
|
+
setTimeout(() => setCopied(false), 2000);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Clipboard access can be denied by the browser — the value is still selectable/copyable by hand.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function handleDelete() {
|
|
101
|
+
setDeleting(true);
|
|
102
|
+
setDeleteError(null);
|
|
103
|
+
try {
|
|
104
|
+
await deleteBookingType(bookingType.uid, bookingType.version);
|
|
105
|
+
window.location.href = `/settings/booking-types?mailboxUid=${encodeURIComponent(mailboxUid)}`;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this booking link.");
|
|
109
|
+
setDeleting(false);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (loading) {
|
|
113
|
+
return _jsx("p", { className: "p-6 text-sm text-text-muted", children: "Loading\u2026" });
|
|
114
|
+
}
|
|
115
|
+
if (error || !bookingType) {
|
|
116
|
+
// `error` is always set whenever `bookingType` is falsy here: the load effect above destructures
|
|
117
|
+
// every field off the resolved value directly (`bt.name`, `bt.hostDisplayName`, ...), so a
|
|
118
|
+
// successful-but-empty response throws into `.catch()` — which sets `error` — before this
|
|
119
|
+
// component ever renders past the `loading` guard above. `!bookingType` alone (with `error` still
|
|
120
|
+
// `null`) is therefore unreachable, unlike e.g. `messages/[uid].tsx`'s identical-looking check,
|
|
121
|
+
// which stores its fetched value whole rather than destructuring it.
|
|
122
|
+
return (_jsx("div", { className: "p-6", children: _jsx(Alert, { children: error }) }));
|
|
123
|
+
}
|
|
124
|
+
// `bookingType` only ever becomes non-null via the client-only fetch above, so by the time this line
|
|
125
|
+
// runs `window` is always defined — the `!bookingType` early return just above covers every real SSR
|
|
126
|
+
// pass, where `bookingType` is still `null`.
|
|
127
|
+
const publicUrl = `${window.location.origin}/book/${encodeURIComponent(bookingType.slug)}`;
|
|
128
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-2xl", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 mb-1", children: [_jsxs("div", { children: [_jsx("a", { href: `/settings/booking-types?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 All booking links" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: bookingType.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete" })] }), _jsxs("div", { className: "flex items-center gap-2 mb-5", children: [_jsx("code", { className: "flex-1 text-xs bg-surface-alt border border-border rounded-sm py-2 px-3 overflow-x-auto whitespace-nowrap", children: publicUrl }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => handleCopy(publicUrl), children: copied ? "Copied" : "Copy" })] }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-1", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm mb-3", children: [_jsx("input", { type: "checkbox", checked: enabled, onChange: (e) => setEnabled(e.target.checked) }), "Enabled (publicly bookable)"] }), _jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }), _jsx(FormField, { label: "Description (optional)", htmlFor: "description", children: _jsx("textarea", { id: "description", className: INPUT_CLASS, rows: 2, value: description, onChange: (e) => setDescription(e.target.value) }) }), _jsx(FormField, { label: "Host name shown to visitors", htmlFor: "hostDisplayName", children: _jsx("input", { id: "hostDisplayName", type: "text", className: INPUT_CLASS, value: hostDisplayName, onChange: (e) => setHostDisplayName(e.target.value) }) }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsx(FormField, { label: "Duration (minutes)", htmlFor: "durationMinutes", children: _jsx("input", { id: "durationMinutes", type: "number", min: 1, className: INPUT_CLASS, value: durationMinutes, onChange: (e) => setDurationMinutes(Number(e.target.value)) }) }), _jsx(FormField, { label: "Timezone", htmlFor: "timezone", children: _jsx("input", { id: "timezone", type: "text", className: INPUT_CLASS, value: timezone, onChange: (e) => setTimezone(e.target.value) }) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsx(FormField, { label: "Minimum notice (minutes)", htmlFor: "minimumNoticeMinutes", children: _jsx("input", { id: "minimumNoticeMinutes", type: "number", min: 0, className: INPUT_CLASS, value: minimumNoticeMinutes, onChange: (e) => setMinimumNoticeMinutes(Number(e.target.value)) }) }), _jsx(FormField, { label: "Booking window (days ahead)", htmlFor: "bookingWindowDays", children: _jsx("input", { id: "bookingWindowDays", type: "number", min: 1, className: INPUT_CLASS, value: bookingWindowDays, onChange: (e) => setBookingWindowDays(Number(e.target.value)) }) })] }), _jsxs("label", { className: "flex items-center gap-2 text-sm my-3", children: [_jsx("input", { type: "checkbox", checked: requiresApproval, onChange: (e) => setRequiresApproval(e.target.checked) }), "Require my approval before confirming a booking"] }), _jsxs("div", { className: "mb-4", children: [_jsx("span", { className: "block text-sm font-semibold mb-1.5 text-text", children: "Weekly availability" }), _jsx(AvailabilityEditor, { value: availability, onChange: setAvailability })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] }), _jsxs(Modal, { open: confirmingDelete, onClose: () => setConfirmingDelete(false), title: "Delete booking link", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: bookingType.name }), "? Its public link will stop working immediately. This cannot be undone."] }), deleteError && _jsx(Alert, { children: deleteError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: deleting, onClick: () => setConfirmingDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: deleting, disabled: deleting, onClick: handleDelete, children: "Delete" })] })] })] }) }));
|
|
129
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { listBookingTypes } from "@rapidmx/react-shared/booking/bookingApi.js";
|
|
9
|
+
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
export default function SettingsBookingTypesPage(props) {
|
|
13
|
+
return (_jsx(SettingsShell, { ...props, active: "booking-types", children: _jsx(BookingTypesContent, {}) }));
|
|
14
|
+
}
|
|
15
|
+
function BookingTypesContent() {
|
|
16
|
+
const { mailboxUid } = useSettingsShell();
|
|
17
|
+
const [bookingTypes, setBookingTypes] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
// `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same established
|
|
21
|
+
// non-null pattern as `apps/www/settings/filters/index.tsx`.
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
listBookingTypes(mailboxUid)
|
|
24
|
+
.then(setBookingTypes)
|
|
25
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load your booking links."))
|
|
26
|
+
.finally(() => setLoading(false));
|
|
27
|
+
}, [mailboxUid]);
|
|
28
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsxs("div", { className: "flex items-center justify-between mb-5", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight", children: "Booking Links" }), _jsx("a", { href: `/settings/booking-types/new?mailboxUid=${encodeURIComponent(mailboxUid)}`, children: _jsx(Button, { type: "button", className: "!w-auto", children: "+ New booking link" }) })] }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Share a link and let anyone pick a real open slot on your calendar \u2014 no account needed on their end." }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : bookingTypes.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No booking links yet." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Name", "Link", "Duration", "Enabled", ""].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: bookingTypes.map((bookingType) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: bookingType.name }), _jsxs("td", { className: "py-2.5 px-2.5 border-b border-border", children: ["/book/", bookingType.slug] }), _jsxs("td", { className: "py-2.5 px-2.5 border-b border-border", children: [bookingType.durationMinutes, " min"] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: bookingType.enabled ? "Yes" : "No" }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-right", children: _jsx("a", { href: `/settings/booking-types/${encodeURIComponent(bookingType.uid)}?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-primary-dark hover:underline font-medium", children: "View" }) })] }, bookingType.uid))) })] }) }))] }) }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
|
+
import { createBookingType } from "@rapidmx/react-shared/booking/bookingApi.js";
|
|
10
|
+
import SettingsShell, { useSettingsShell } from "../../../../shared/components/settings/layout/SettingsShell.js";
|
|
11
|
+
import AvailabilityEditor from "../../../../shared/components/booking/AvailabilityEditor.js";
|
|
12
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
13
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
14
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
15
|
+
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
16
|
+
export default function NewBookingTypePage(props) {
|
|
17
|
+
return (_jsx(SettingsShell, { ...props, active: "booking-types", children: _jsx(NewBookingTypeForm, {}) }));
|
|
18
|
+
}
|
|
19
|
+
function NewBookingTypeForm() {
|
|
20
|
+
const { mailboxUid, mailboxes } = useSettingsShell();
|
|
21
|
+
// Same established non-null pattern as `apps/www/settings/filters/new/index.tsx`.
|
|
22
|
+
const mailbox = mailboxes.find((mb) => mb.uid === mailboxUid);
|
|
23
|
+
const [calendarFolderUid, setCalendarFolderUid] = useState(null);
|
|
24
|
+
const [folderError, setFolderError] = useState(null);
|
|
25
|
+
const [slug, setSlug] = useState("");
|
|
26
|
+
const [name, setName] = useState("");
|
|
27
|
+
const [description, setDescription] = useState("");
|
|
28
|
+
const [hostDisplayName, setHostDisplayName] = useState(mailbox.displayName);
|
|
29
|
+
const [durationMinutes, setDurationMinutes] = useState(30);
|
|
30
|
+
const [timezone, setTimezone] = useState(mailbox.timezone);
|
|
31
|
+
const [availability, setAvailability] = useState([]);
|
|
32
|
+
const [minimumNoticeMinutes, setMinimumNoticeMinutes] = useState(60);
|
|
33
|
+
const [bookingWindowDays, setBookingWindowDays] = useState(30);
|
|
34
|
+
const [requiresApproval, setRequiresApproval] = useState(false);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
36
|
+
const [saving, setSaving] = useState(false);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
listFolders(mailboxUid)
|
|
39
|
+
.then((folders) => {
|
|
40
|
+
const calendar = folders.find((f) => f.type === "calendar");
|
|
41
|
+
if (!calendar) {
|
|
42
|
+
setFolderError("This mailbox has no Calendar folder yet.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setCalendarFolderUid(calendar.uid);
|
|
46
|
+
})
|
|
47
|
+
.catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's folders."));
|
|
48
|
+
}, [mailboxUid]);
|
|
49
|
+
async function handleSubmit(e) {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
setError(null);
|
|
52
|
+
if (!slug.trim() || !name.trim() || !hostDisplayName.trim()) {
|
|
53
|
+
setError("Slug, name, and host name are all required.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!calendarFolderUid) {
|
|
57
|
+
setError("This mailbox has no Calendar folder yet.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setSaving(true);
|
|
61
|
+
try {
|
|
62
|
+
const created = await createBookingType({
|
|
63
|
+
mailboxUid: mailboxUid,
|
|
64
|
+
calendarFolderUid,
|
|
65
|
+
slug: slug.trim(),
|
|
66
|
+
name: name.trim(),
|
|
67
|
+
description: description.trim() || undefined,
|
|
68
|
+
hostDisplayName: hostDisplayName.trim(),
|
|
69
|
+
durationMinutes,
|
|
70
|
+
timezone,
|
|
71
|
+
availability,
|
|
72
|
+
minimumNoticeMinutes,
|
|
73
|
+
bookingWindowDays,
|
|
74
|
+
requiresApproval,
|
|
75
|
+
});
|
|
76
|
+
window.location.href = `/settings/booking-types/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid)}`;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not create this booking link.");
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
setSaving(false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-2xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "New booking link" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Anyone with the link can pick an open slot from your live availability below \u2014 no account needed on their end." }), error && _jsx(Alert, { children: error }), folderError && _jsx(Alert, { children: folderError }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-1", children: [_jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value), placeholder: "30 Minute Intro Call" }) }), _jsx(FormField, { label: "Slug (used in the public link)", htmlFor: "slug", children: _jsx("input", { id: "slug", type: "text", className: INPUT_CLASS, value: slug, onChange: (e) => setSlug(e.target.value), placeholder: "intro-call" }) }), _jsx(FormField, { label: "Description (optional)", htmlFor: "description", children: _jsx("textarea", { id: "description", className: INPUT_CLASS, rows: 2, value: description, onChange: (e) => setDescription(e.target.value) }) }), _jsx(FormField, { label: "Host name shown to visitors", htmlFor: "hostDisplayName", children: _jsx("input", { id: "hostDisplayName", type: "text", className: INPUT_CLASS, value: hostDisplayName, onChange: (e) => setHostDisplayName(e.target.value) }) }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsx(FormField, { label: "Duration (minutes)", htmlFor: "durationMinutes", children: _jsx("input", { id: "durationMinutes", type: "number", min: 1, className: INPUT_CLASS, value: durationMinutes, onChange: (e) => setDurationMinutes(Number(e.target.value)) }) }), _jsx(FormField, { label: "Timezone", htmlFor: "timezone", children: _jsx("input", { id: "timezone", type: "text", className: INPUT_CLASS, value: timezone, onChange: (e) => setTimezone(e.target.value), placeholder: "America/New_York" }) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsx(FormField, { label: "Minimum notice (minutes)", htmlFor: "minimumNoticeMinutes", children: _jsx("input", { id: "minimumNoticeMinutes", type: "number", min: 0, className: INPUT_CLASS, value: minimumNoticeMinutes, onChange: (e) => setMinimumNoticeMinutes(Number(e.target.value)) }) }), _jsx(FormField, { label: "Booking window (days ahead)", htmlFor: "bookingWindowDays", children: _jsx("input", { id: "bookingWindowDays", type: "number", min: 1, className: INPUT_CLASS, value: bookingWindowDays, onChange: (e) => setBookingWindowDays(Number(e.target.value)) }) })] }), _jsxs("label", { className: "flex items-center gap-2 text-sm my-3", children: [_jsx("input", { type: "checkbox", checked: requiresApproval, onChange: (e) => setRequiresApproval(e.target.checked) }), "Require my approval before confirming a booking"] }), _jsxs("div", { className: "mb-4", children: [_jsx("span", { className: "block text-sm font-semibold mb-1.5 text-text", children: "Weekly availability" }), _jsx(AvailabilityEditor, { value: availability, onChange: setAvailability })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Create" }) })] })] }) }));
|
|
86
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
|
+
import { getMailFilterRule, updateMailFilterRule, } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
|
|
10
|
+
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
11
|
+
import RuleBuilder from "../../../shared/components/rules/RuleBuilder.js";
|
|
12
|
+
import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "./_mailFilterRuleConfig.js";
|
|
13
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
14
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
15
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
16
|
+
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
17
|
+
export default function MailFilterDetailPage(props) {
|
|
18
|
+
return (_jsx(SettingsShell, { ...props, active: "filters", children: _jsx(MailFilterDetailContent, { uid: props.params.uid }) }));
|
|
19
|
+
}
|
|
20
|
+
function MailFilterDetailContent({ uid }) {
|
|
21
|
+
const { mailboxUid } = useSettingsShell();
|
|
22
|
+
const [original, setOriginal] = useState(null);
|
|
23
|
+
const [name, setName] = useState("");
|
|
24
|
+
const [rule, setRule] = useState(null);
|
|
25
|
+
const [folders, setFolders] = useState(null);
|
|
26
|
+
const [loading, setLoading] = useState(true);
|
|
27
|
+
const [error, setError] = useState(null);
|
|
28
|
+
const [saving, setSaving] = useState(false);
|
|
29
|
+
const [saved, setSaved] = useState(false);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
// `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same
|
|
32
|
+
// established non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
|
|
33
|
+
listFolders(mailboxUid).then(setFolders).catch(() => setFolders([]));
|
|
34
|
+
}, [mailboxUid]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setLoading(true);
|
|
37
|
+
setError(null);
|
|
38
|
+
getMailFilterRule(uid)
|
|
39
|
+
.then((loaded) => {
|
|
40
|
+
setOriginal(loaded);
|
|
41
|
+
if (loaded) {
|
|
42
|
+
setName(loaded.name);
|
|
43
|
+
setRule({
|
|
44
|
+
enabled: loaded.enabled,
|
|
45
|
+
sequence: loaded.sequence,
|
|
46
|
+
stopProcessingRules: loaded.stopProcessingRules,
|
|
47
|
+
conditions: loaded.conditions,
|
|
48
|
+
actions: loaded.actions,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this mail filter."))
|
|
53
|
+
.finally(() => setLoading(false));
|
|
54
|
+
}, [uid]);
|
|
55
|
+
// Only ever invoked from the form below, which itself only renders once `original`/`rule` are
|
|
56
|
+
// loaded (the early returns above cover every other state) — the non-null assertions reflect that
|
|
57
|
+
// real invariant, matching `TransportRuleDetailContent.handleSubmit`'s identical pattern.
|
|
58
|
+
async function handleSubmit(e) {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
setError(null);
|
|
61
|
+
if (!name.trim()) {
|
|
62
|
+
setError("A name is required.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
setSaving(true);
|
|
66
|
+
setSaved(false);
|
|
67
|
+
try {
|
|
68
|
+
const updated = await updateMailFilterRule({
|
|
69
|
+
uid: original.uid,
|
|
70
|
+
version: original.version,
|
|
71
|
+
name: name.trim(),
|
|
72
|
+
...rule,
|
|
73
|
+
});
|
|
74
|
+
setOriginal(updated);
|
|
75
|
+
setSaved(true);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not save this mail filter.");
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
setSaving(false);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (loading || folders === null) {
|
|
85
|
+
return _jsx("p", { className: "p-6 text-sm text-text-muted", children: "Loading\u2026" });
|
|
86
|
+
}
|
|
87
|
+
if (error && !original) {
|
|
88
|
+
return (_jsx("div", { className: "p-6", children: _jsx(Alert, { children: error }) }));
|
|
89
|
+
}
|
|
90
|
+
if (!original || !rule) {
|
|
91
|
+
return (_jsx("div", { className: "p-6", children: _jsx(Alert, { children: "Mail filter not found." }) }));
|
|
92
|
+
}
|
|
93
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("a", { href: `/settings/filters?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 All mail filters" }), _jsx("h1", { className: "text-lg font-bold tracking-tight mt-1 mb-5", children: original.name }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }) }), _jsx(RuleBuilder, { value: rule, onChange: setRule, conditionFields: MAIL_FILTER_CONDITION_FIELDS, actionTypes: buildMailFilterActionTypes(folders) }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save changes" }) })] })] }) }));
|
|
94
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const FIELD_INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
3
|
+
/** Folder types a mail filter can't meaningfully move/copy a message into — a mailbox's single
|
|
4
|
+
* well-known `calendar`/`contacts`/`tasks`/`notes` folder holds a different entity type entirely, not
|
|
5
|
+
* messages. */
|
|
6
|
+
const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]);
|
|
7
|
+
export const MAIL_FILTER_CONDITION_FIELDS = [
|
|
8
|
+
{ key: "fromContains", label: "From contains", kind: "list" },
|
|
9
|
+
{ key: "subjectContains", label: "Subject contains", kind: "list" },
|
|
10
|
+
{ key: "bodyContains", label: "Body contains", kind: "list" },
|
|
11
|
+
{ key: "toCcContains", label: "Any To/Cc recipient equals", kind: "list" },
|
|
12
|
+
{ key: "hasAttachment", label: "Has an attachment", kind: "boolean" },
|
|
13
|
+
{
|
|
14
|
+
key: "importance",
|
|
15
|
+
label: "Importance",
|
|
16
|
+
kind: "select",
|
|
17
|
+
options: [
|
|
18
|
+
{ value: "low", label: "Low" },
|
|
19
|
+
{ value: "normal", label: "Normal" },
|
|
20
|
+
{ value: "high", label: "High" },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Builds the mail-filter action-type registry for `RuleBuilder`, closing over `folders` so the
|
|
26
|
+
* move/copy-to-folder actions can render a real folder picker. `RuleBuilder`'s own `render()` contract
|
|
27
|
+
* is synchronous, so this resolves `folders` once up front (the caller already loads them for the page
|
|
28
|
+
* itself) rather than each action row fetching independently.
|
|
29
|
+
*/
|
|
30
|
+
export function buildMailFilterActionTypes(folders) {
|
|
31
|
+
const destinationFolders = folders.filter((f) => !NON_MAIL_FOLDER_TYPES.has(f.type));
|
|
32
|
+
const folderSelect = (action, onChange) => (_jsxs("select", { "aria-label": "Destination folder", className: FIELD_INPUT_CLASS, value: action.folderUid ?? "", onChange: (e) => onChange({ ...action, folderUid: e.target.value }), children: [_jsx("option", { value: "", children: "Choose a folder\u2026" }), destinationFolders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid)))] }));
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
value: "move_to_folder",
|
|
36
|
+
label: "Move to folder",
|
|
37
|
+
createDefault: () => ({ type: "move_to_folder", folderUid: "" }),
|
|
38
|
+
render: folderSelect,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
value: "copy_to_folder",
|
|
42
|
+
label: "Copy to folder",
|
|
43
|
+
createDefault: () => ({ type: "copy_to_folder", folderUid: "" }),
|
|
44
|
+
render: folderSelect,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: "delete",
|
|
48
|
+
label: "Delete the message",
|
|
49
|
+
createDefault: () => ({ type: "delete" }),
|
|
50
|
+
render: () => null,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: "mark_as_read",
|
|
54
|
+
label: "Mark as read",
|
|
55
|
+
createDefault: () => ({ type: "mark_as_read" }),
|
|
56
|
+
render: () => null,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
value: "forward",
|
|
60
|
+
label: "Forward to",
|
|
61
|
+
createDefault: () => ({ type: "forward", forwardTo: "" }),
|
|
62
|
+
render: (action, onChange) => (_jsx("input", { type: "email", "aria-label": "Forward to address", className: FIELD_INPUT_CLASS, placeholder: "assistant@example.com", value: action.forwardTo ?? "", onChange: (e) => onChange({ ...action, forwardTo: e.target.value }) })),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { listMailFilterRules } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
|
|
9
|
+
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
export default function SettingsFiltersPage(props) {
|
|
13
|
+
return (_jsx(SettingsShell, { ...props, active: "filters", children: _jsx(FiltersContent, {}) }));
|
|
14
|
+
}
|
|
15
|
+
function FiltersContent() {
|
|
16
|
+
const { mailboxUid } = useSettingsShell();
|
|
17
|
+
const [rules, setRules] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
// `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same established
|
|
21
|
+
// non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
listMailFilterRules(mailboxUid)
|
|
24
|
+
.then(setRules)
|
|
25
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load mail filters."))
|
|
26
|
+
.finally(() => setLoading(false));
|
|
27
|
+
}, [mailboxUid]);
|
|
28
|
+
const sorted = [...rules].sort((a, b) => a.sequence - b.sequence);
|
|
29
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsxs("div", { className: "flex items-center justify-between mb-5", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight", children: "Mail Filters" }), _jsx("a", { href: `/settings/filters/new?mailboxUid=${encodeURIComponent(mailboxUid)}`, children: _jsx(Button, { type: "button", className: "!w-auto", children: "+ New filter" }) })] }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Applied in order once a message is verdicted safe to deliver, before it reaches your Inbox." }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No mail filters yet." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Sequence", "Name", "Enabled", "Actions", ""].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: sorted.map((rule) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: rule.sequence }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: rule.name }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: rule.enabled ? "Yes" : "No" }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: rule.actions.length }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-right", children: _jsx("a", { href: `/settings/filters/${encodeURIComponent(rule.uid)}?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-primary-dark hover:underline font-medium", children: "View" }) })] }, rule.uid))) })] }) }))] }) }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
|
+
import { createMailFilterRule, } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
|
|
10
|
+
import SettingsShell, { useSettingsShell } from "../../../../shared/components/settings/layout/SettingsShell.js";
|
|
11
|
+
import RuleBuilder from "../../../../shared/components/rules/RuleBuilder.js";
|
|
12
|
+
import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "../_mailFilterRuleConfig.js";
|
|
13
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
14
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
15
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
16
|
+
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
17
|
+
export default function NewMailFilterPage(props) {
|
|
18
|
+
return (_jsx(SettingsShell, { ...props, active: "filters", children: _jsx(NewMailFilterForm, {}) }));
|
|
19
|
+
}
|
|
20
|
+
function NewMailFilterForm() {
|
|
21
|
+
const { mailboxUid } = useSettingsShell();
|
|
22
|
+
const [folders, setFolders] = useState(null);
|
|
23
|
+
const [folderError, setFolderError] = useState(null);
|
|
24
|
+
const [name, setName] = useState("");
|
|
25
|
+
const [rule, setRule] = useState({
|
|
26
|
+
enabled: true,
|
|
27
|
+
sequence: 0,
|
|
28
|
+
stopProcessingRules: false,
|
|
29
|
+
conditions: {},
|
|
30
|
+
actions: [],
|
|
31
|
+
});
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
const [saving, setSaving] = useState(false);
|
|
34
|
+
// `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same established
|
|
35
|
+
// non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
listFolders(mailboxUid)
|
|
38
|
+
.then(setFolders)
|
|
39
|
+
.catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's folders."));
|
|
40
|
+
}, [mailboxUid]);
|
|
41
|
+
async function handleSubmit(e) {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
setError(null);
|
|
44
|
+
if (!name.trim()) {
|
|
45
|
+
setError("A name is required.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setSaving(true);
|
|
49
|
+
try {
|
|
50
|
+
const created = await createMailFilterRule({ mailboxUid: mailboxUid, name: name.trim(), ...rule });
|
|
51
|
+
window.location.href = `/settings/filters/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid)}`;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not create the mail filter.");
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
setSaving(false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "New mail filter" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Applied in order once a message is verdicted safe to deliver, before it reaches your Inbox." }), error && _jsx(Alert, { children: error }), folderError && _jsx(Alert, { children: folderError }), folders === null ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value), placeholder: "File newsletters" }) }) }), _jsx(RuleBuilder, { value: rule, onChange: setRule, conditionFields: MAIL_FILTER_CONDITION_FIELDS, actionTypes: buildMailFilterActionTypes(folders) }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Create filter" }), _jsx("a", { href: `/settings/filters?mailboxUid=${encodeURIComponent(mailboxUid)}`, children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", children: "Cancel" }) })] })] }))] }) }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { createFocusedInboxOverride, deleteFocusedInboxOverride, listFocusedInboxOverrides, } from "@rapidmx/react-shared/mail/focusedInboxOverridesApi.js";
|
|
9
|
+
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
export default function SettingsFocusedInboxPage(props) {
|
|
13
|
+
return (_jsx(SettingsShell, { ...props, active: "focused-inbox", children: _jsx(FocusedInboxContent, {}) }));
|
|
14
|
+
}
|
|
15
|
+
function FocusedInboxContent() {
|
|
16
|
+
const { mailboxUid } = useSettingsShell();
|
|
17
|
+
const [overrides, setOverrides] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
const [saving, setSaving] = useState(false);
|
|
21
|
+
const [newSender, setNewSender] = useState("");
|
|
22
|
+
const [newClassifyAs, setNewClassifyAs] = useState("other");
|
|
23
|
+
function reload() {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
// `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same
|
|
27
|
+
// established non-null pattern as `apps/www/settings/filters/index.tsx`.
|
|
28
|
+
listFocusedInboxOverrides(mailboxUid)
|
|
29
|
+
.then(setOverrides)
|
|
30
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load your Focused Inbox rules."))
|
|
31
|
+
.finally(() => setLoading(false));
|
|
32
|
+
}
|
|
33
|
+
useEffect(reload, [mailboxUid]);
|
|
34
|
+
async function handleAdd(e) {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
const senderAddress = newSender.trim();
|
|
37
|
+
if (!senderAddress) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setSaving(true);
|
|
41
|
+
setError(null);
|
|
42
|
+
try {
|
|
43
|
+
await createFocusedInboxOverride({ mailboxUid: mailboxUid, senderAddress, classifyAs: newClassifyAs });
|
|
44
|
+
setNewSender("");
|
|
45
|
+
reload();
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not add this rule.");
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
setSaving(false);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function handleRemove(override) {
|
|
55
|
+
setSaving(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
try {
|
|
58
|
+
await deleteFocusedInboxOverride(override.uid, override.version);
|
|
59
|
+
reload();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not remove this rule.");
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setSaving(false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Focused Inbox" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Mail from a sender listed here always goes to the matching half of your Inbox, overriding the automatic Focused/Other classification. Add one directly, or use \u201CAlways for this sender\u201D when moving a message." }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("ul", { className: "flex flex-col gap-2 mb-4", children: [overrides.length === 0 && _jsx("li", { className: "text-sm text-text-muted", children: "No rules yet." }), overrides.map((override) => (_jsxs("li", { className: "flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: override.senderAddress }), _jsxs("div", { className: "text-xs text-text-muted", children: ["Always ", override.classifyAs === "other" ? "Other" : "Focused"] })] }), _jsx("button", { type: "button", onClick: () => handleRemove(override), disabled: saving, className: "text-sm text-danger hover:underline disabled:opacity-55", children: "Remove" })] }, override.uid)))] })), _jsxs("form", { onSubmit: handleAdd, className: "flex gap-2", children: [_jsx("input", { type: "text", "aria-label": "Sender address", value: newSender, onChange: (e) => setNewSender(e.target.value), placeholder: "sender@example.com", className: "flex-1 text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary" }), _jsxs("select", { "aria-label": "Classify as", value: newClassifyAs, onChange: (e) => setNewClassifyAs(e.target.value), className: "text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary", children: [_jsx("option", { value: "other", children: "Other" }), _jsx("option", { value: "focused", children: "Focused" })] }), _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Add" })] })] }) }));
|
|
69
|
+
}
|