@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.
Files changed (168) hide show
  1. package/LICENSE +382 -0
  2. package/apps/admin/_404.tsx +9 -0
  3. package/apps/admin/_500.tsx +9 -0
  4. package/apps/admin/_layout.tsx +41 -0
  5. package/apps/admin/audit-log/index.tsx +187 -0
  6. package/apps/admin/branding/index.tsx +404 -0
  7. package/apps/admin/distribution-lists/[uid].tsx +70 -0
  8. package/apps/admin/distribution-lists/index.tsx +115 -0
  9. package/apps/admin/distribution-lists/new/index.tsx +114 -0
  10. package/apps/admin/domains/[uid].tsx +259 -0
  11. package/apps/admin/domains/index.tsx +125 -0
  12. package/apps/admin/domains/new/index.tsx +84 -0
  13. package/apps/admin/index.tsx +82 -0
  14. package/apps/admin/ingest-queue/index.tsx +116 -0
  15. package/apps/admin/mailboxes/[uid].tsx +126 -0
  16. package/apps/admin/mailboxes/new/index.tsx +291 -0
  17. package/apps/admin/quarantine/index.tsx +171 -0
  18. package/apps/admin/transport-rules/[uid].tsx +143 -0
  19. package/apps/admin/transport-rules/_transportRuleConfig.tsx +75 -0
  20. package/apps/admin/transport-rules/index.tsx +117 -0
  21. package/apps/admin/transport-rules/new/index.tsx +105 -0
  22. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +117 -0
  23. package/apps/shared/components/admin/layout/AdminShell.tsx +186 -0
  24. package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +73 -0
  25. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +154 -0
  26. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +124 -0
  27. package/apps/shared/components/booking/AvailabilityEditor.tsx +118 -0
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +119 -0
  29. package/apps/shared/components/calendar/EventModal.tsx +513 -0
  30. package/apps/shared/components/calendar/MonthView.tsx +139 -0
  31. package/apps/shared/components/calendar/RecurrenceEditor.tsx +180 -0
  32. package/apps/shared/components/calendar/ResourcePicker.tsx +80 -0
  33. package/apps/shared/components/calendar/SplitDayView.tsx +137 -0
  34. package/apps/shared/components/calendar/TimeGridView.tsx +198 -0
  35. package/apps/shared/components/calendar/layout/CalendarShell.tsx +211 -0
  36. package/apps/shared/components/contacts/ContactDetailPane.tsx +99 -0
  37. package/apps/shared/components/contacts/ContactForm.tsx +314 -0
  38. package/apps/shared/components/contacts/ContactsSidebar.tsx +242 -0
  39. package/apps/shared/components/contacts/ContactsToolbar.tsx +115 -0
  40. package/apps/shared/components/contacts/layout/ContactsShell.tsx +197 -0
  41. package/apps/shared/components/layout/AppShell.tsx +165 -0
  42. package/apps/shared/components/layout/BrandingChrome.tsx +25 -0
  43. package/apps/shared/components/layout/MailboxProvisioning.tsx +110 -0
  44. package/apps/shared/components/layout/UserMenu.tsx +150 -0
  45. package/apps/shared/components/mail/ConversationList.tsx +61 -0
  46. package/apps/shared/components/mail/ConversationThreadPane.tsx +174 -0
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +390 -0
  48. package/apps/shared/components/mail/compose/ComposeContext.tsx +114 -0
  49. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +418 -0
  50. package/apps/shared/components/mail/compose/ComposeWindow.tsx +518 -0
  51. package/apps/shared/components/mail/compose/EmojiPicker.tsx +64 -0
  52. package/apps/shared/components/mail/compose/GifPicker.tsx +82 -0
  53. package/apps/shared/components/mail/compose/RichTextEditor.tsx +83 -0
  54. package/apps/shared/components/mail/compose/ScheduleSendPicker.tsx +64 -0
  55. package/apps/shared/components/mail/layout/MailShell.tsx +287 -0
  56. package/apps/shared/components/rules/RuleBuilder.tsx +276 -0
  57. package/apps/shared/components/settings/layout/SettingsShell.tsx +223 -0
  58. package/apps/shared/components/tasks/TasksSidebar.tsx +201 -0
  59. package/apps/shared/components/tasks/TasksToolbar.tsx +68 -0
  60. package/apps/shared/components/tasks/layout/TasksShell.tsx +199 -0
  61. package/apps/shared/styles/app.css +265 -0
  62. package/apps/www/_404.tsx +9 -0
  63. package/apps/www/_500.tsx +9 -0
  64. package/apps/www/_layout.tsx +45 -0
  65. package/apps/www/calendar/index.tsx +407 -0
  66. package/apps/www/contacts/[uid].tsx +88 -0
  67. package/apps/www/contacts/index.tsx +436 -0
  68. package/apps/www/index.tsx +375 -0
  69. package/apps/www/messages/[uid].tsx +70 -0
  70. package/apps/www/settings/auto-reply/index.tsx +129 -0
  71. package/apps/www/settings/booking-types/[uid].tsx +300 -0
  72. package/apps/www/settings/booking-types/index.tsx +98 -0
  73. package/apps/www/settings/booking-types/new/index.tsx +212 -0
  74. package/apps/www/settings/filters/[uid].tsx +155 -0
  75. package/apps/www/settings/filters/_mailFilterRuleConfig.tsx +102 -0
  76. package/apps/www/settings/filters/index.tsx +99 -0
  77. package/apps/www/settings/filters/new/index.tsx +126 -0
  78. package/apps/www/settings/focused-inbox/index.tsx +148 -0
  79. package/apps/www/settings/read-receipts/index.tsx +125 -0
  80. package/apps/www/settings/signatures/[uid].tsx +167 -0
  81. package/apps/www/settings/signatures/index.tsx +85 -0
  82. package/apps/www/settings/signatures/new/index.tsx +129 -0
  83. package/apps/www/settings/signatures/signatureDefaults.ts +36 -0
  84. package/apps/www/tasks/index.tsx +530 -0
  85. package/dist/apps/admin/_404.js +4 -0
  86. package/dist/apps/admin/_500.js +4 -0
  87. package/dist/apps/admin/_layout.js +10 -0
  88. package/dist/apps/admin/audit-log/index.js +66 -0
  89. package/dist/apps/admin/branding/index.js +126 -0
  90. package/dist/apps/admin/distribution-lists/[uid].js +34 -0
  91. package/dist/apps/admin/distribution-lists/index.js +31 -0
  92. package/dist/apps/admin/distribution-lists/new/index.js +51 -0
  93. package/dist/apps/admin/domains/[uid].js +102 -0
  94. package/dist/apps/admin/domains/index.js +31 -0
  95. package/dist/apps/admin/domains/new/index.js +41 -0
  96. package/dist/apps/admin/index.js +32 -0
  97. package/dist/apps/admin/ingest-queue/index.js +48 -0
  98. package/dist/apps/admin/mailboxes/[uid].js +59 -0
  99. package/dist/apps/admin/mailboxes/new/index.js +93 -0
  100. package/dist/apps/admin/quarantine/index.js +66 -0
  101. package/dist/apps/admin/transport-rules/[uid].js +87 -0
  102. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +37 -0
  103. package/dist/apps/admin/transport-rules/index.js +32 -0
  104. package/dist/apps/admin/transport-rules/new/index.js +50 -0
  105. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +64 -0
  106. package/dist/apps/shared/components/admin/layout/AdminShell.js +94 -0
  107. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.js +16 -0
  108. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +59 -0
  109. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +67 -0
  110. package/dist/apps/shared/components/booking/AvailabilityEditor.js +45 -0
  111. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +44 -0
  112. package/dist/apps/shared/components/calendar/EventModal.js +167 -0
  113. package/dist/apps/shared/components/calendar/MonthView.js +38 -0
  114. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +55 -0
  115. package/dist/apps/shared/components/calendar/ResourcePicker.js +36 -0
  116. package/dist/apps/shared/components/calendar/SplitDayView.js +44 -0
  117. package/dist/apps/shared/components/calendar/TimeGridView.js +58 -0
  118. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +96 -0
  119. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -0
  120. package/dist/apps/shared/components/contacts/ContactForm.js +85 -0
  121. package/dist/apps/shared/components/contacts/ContactsSidebar.js +105 -0
  122. package/dist/apps/shared/components/contacts/ContactsToolbar.js +32 -0
  123. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +93 -0
  124. package/dist/apps/shared/components/layout/AppShell.js +68 -0
  125. package/dist/apps/shared/components/layout/BrandingChrome.js +18 -0
  126. package/dist/apps/shared/components/layout/MailboxProvisioning.js +75 -0
  127. package/dist/apps/shared/components/layout/UserMenu.js +59 -0
  128. package/dist/apps/shared/components/mail/ConversationList.js +17 -0
  129. package/dist/apps/shared/components/mail/ConversationThreadPane.js +112 -0
  130. package/dist/apps/shared/components/mail/MessageDetailPane.js +153 -0
  131. package/dist/apps/shared/components/mail/compose/ComposeContext.js +55 -0
  132. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +88 -0
  133. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +261 -0
  134. package/dist/apps/shared/components/mail/compose/EmojiPicker.js +25 -0
  135. package/dist/apps/shared/components/mail/compose/GifPicker.js +36 -0
  136. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +60 -0
  137. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.js +33 -0
  138. package/dist/apps/shared/components/mail/layout/MailShell.js +139 -0
  139. package/dist/apps/shared/components/rules/RuleBuilder.js +78 -0
  140. package/dist/apps/shared/components/settings/layout/SettingsShell.js +100 -0
  141. package/dist/apps/shared/components/tasks/TasksSidebar.js +81 -0
  142. package/dist/apps/shared/components/tasks/TasksToolbar.js +15 -0
  143. package/dist/apps/shared/components/tasks/layout/TasksShell.js +92 -0
  144. package/dist/apps/shared/styles/app.css +265 -0
  145. package/dist/apps/www/_404.js +4 -0
  146. package/dist/apps/www/_500.js +4 -0
  147. package/dist/apps/www/_layout.js +8 -0
  148. package/dist/apps/www/calendar/index.js +228 -0
  149. package/dist/apps/www/contacts/[uid].js +62 -0
  150. package/dist/apps/www/contacts/index.js +262 -0
  151. package/dist/apps/www/index.js +228 -0
  152. package/dist/apps/www/messages/[uid].js +47 -0
  153. package/dist/apps/www/settings/auto-reply/index.js +54 -0
  154. package/dist/apps/www/settings/booking-types/[uid].js +129 -0
  155. package/dist/apps/www/settings/booking-types/index.js +29 -0
  156. package/dist/apps/www/settings/booking-types/new/index.js +86 -0
  157. package/dist/apps/www/settings/filters/[uid].js +94 -0
  158. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.js +65 -0
  159. package/dist/apps/www/settings/filters/index.js +30 -0
  160. package/dist/apps/www/settings/filters/new/index.js +61 -0
  161. package/dist/apps/www/settings/focused-inbox/index.js +69 -0
  162. package/dist/apps/www/settings/read-receipts/index.js +51 -0
  163. package/dist/apps/www/settings/signatures/[uid].js +96 -0
  164. package/dist/apps/www/settings/signatures/index.js +34 -0
  165. package/dist/apps/www/settings/signatures/new/index.js +63 -0
  166. package/dist/apps/www/settings/signatures/signatureDefaults.js +30 -0
  167. package/dist/apps/www/tasks/index.js +280 -0
  168. package/package.json +87 -0
@@ -0,0 +1,155 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { FormEvent, useEffect, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { Folder, listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import {
9
+ MailFilterAction,
10
+ MailFilterConditions,
11
+ MailFilterRule,
12
+ getMailFilterRule,
13
+ updateMailFilterRule,
14
+ } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
15
+ import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
16
+ import RuleBuilder, { RuleBuilderValue } from "../../../shared/components/rules/RuleBuilder.js";
17
+ import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "./_mailFilterRuleConfig.js";
18
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
19
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
20
+ import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
21
+
22
+ const INPUT_CLASS =
23
+ "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
24
+
25
+ export type MailFilterDetailPageProps = Omit<SettingsShellProps, "active"> & { params: { uid: string } };
26
+
27
+ export default function MailFilterDetailPage(props: MailFilterDetailPageProps) {
28
+ return (
29
+ <SettingsShell {...props} active="filters">
30
+ <MailFilterDetailContent uid={props.params.uid} />
31
+ </SettingsShell>
32
+ );
33
+ }
34
+
35
+ function MailFilterDetailContent({ uid }: { uid: string }) {
36
+ const { mailboxUid } = useSettingsShell();
37
+ const [original, setOriginal] = useState<MailFilterRule | null>(null);
38
+ const [name, setName] = useState("");
39
+ const [rule, setRule] = useState<RuleBuilderValue<MailFilterConditions, MailFilterAction> | null>(null);
40
+ const [folders, setFolders] = useState<Folder[] | null>(null);
41
+ const [loading, setLoading] = useState(true);
42
+ const [error, setError] = useState<string | null>(null);
43
+ const [saving, setSaving] = useState(false);
44
+ const [saved, setSaved] = useState(false);
45
+
46
+ useEffect(() => {
47
+ // `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same
48
+ // established non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
49
+ listFolders(mailboxUid!).then(setFolders).catch(() => setFolders([]));
50
+ }, [mailboxUid]);
51
+
52
+ useEffect(() => {
53
+ setLoading(true);
54
+ setError(null);
55
+ getMailFilterRule(uid)
56
+ .then((loaded) => {
57
+ setOriginal(loaded);
58
+ if (loaded) {
59
+ setName(loaded.name);
60
+ setRule({
61
+ enabled: loaded.enabled,
62
+ sequence: loaded.sequence,
63
+ stopProcessingRules: loaded.stopProcessingRules,
64
+ conditions: loaded.conditions,
65
+ actions: loaded.actions,
66
+ });
67
+ }
68
+ })
69
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this mail filter."))
70
+ .finally(() => setLoading(false));
71
+ }, [uid]);
72
+
73
+ // Only ever invoked from the form below, which itself only renders once `original`/`rule` are
74
+ // loaded (the early returns above cover every other state) — the non-null assertions reflect that
75
+ // real invariant, matching `TransportRuleDetailContent.handleSubmit`'s identical pattern.
76
+ async function handleSubmit(e: FormEvent) {
77
+ e.preventDefault();
78
+ setError(null);
79
+
80
+ if (!name.trim()) {
81
+ setError("A name is required.");
82
+ return;
83
+ }
84
+
85
+ setSaving(true);
86
+ setSaved(false);
87
+ try {
88
+ const updated = await updateMailFilterRule({
89
+ uid: original!.uid,
90
+ version: original!.version,
91
+ name: name.trim(),
92
+ ...rule!,
93
+ });
94
+ setOriginal(updated);
95
+ setSaved(true);
96
+ } catch (err) {
97
+ setError(err instanceof ApiRequestError ? err.message : "Could not save this mail filter.");
98
+ } finally {
99
+ setSaving(false);
100
+ }
101
+ }
102
+
103
+ if (loading || folders === null) {
104
+ return <p className="p-6 text-sm text-text-muted">Loading&hellip;</p>;
105
+ }
106
+ if (error && !original) {
107
+ return (
108
+ <div className="p-6">
109
+ <Alert>{error}</Alert>
110
+ </div>
111
+ );
112
+ }
113
+ if (!original || !rule) {
114
+ return (
115
+ <div className="p-6">
116
+ <Alert>Mail filter not found.</Alert>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ return (
122
+ <div className="flex-1 min-w-0 overflow-y-auto p-6">
123
+ <div className="max-w-3xl">
124
+ <a href={`/settings/filters?mailboxUid=${encodeURIComponent(mailboxUid!)}`} className="text-sm text-primary-dark hover:underline">
125
+ &larr; All mail filters
126
+ </a>
127
+ <h1 className="text-lg font-bold tracking-tight mt-1 mb-5">{original.name}</h1>
128
+
129
+ {error && <Alert>{error}</Alert>}
130
+ {saved && !error && <div className="mb-4 text-sm text-success font-medium">Saved.</div>}
131
+
132
+ <form onSubmit={handleSubmit} className="flex flex-col gap-5">
133
+ <div className="bg-surface border border-border rounded-md p-6">
134
+ <FormField label="Name" htmlFor="name">
135
+ <input id="name" type="text" className={INPUT_CLASS} value={name} onChange={(e) => setName(e.target.value)} />
136
+ </FormField>
137
+ </div>
138
+
139
+ <RuleBuilder
140
+ value={rule}
141
+ onChange={setRule}
142
+ conditionFields={MAIL_FILTER_CONDITION_FIELDS}
143
+ actionTypes={buildMailFilterActionTypes(folders)}
144
+ />
145
+
146
+ <div>
147
+ <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
148
+ Save changes
149
+ </Button>
150
+ </div>
151
+ </form>
152
+ </div>
153
+ </div>
154
+ );
155
+ }
@@ -0,0 +1,102 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React from "react";
6
+ import { Folder, FolderType } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import { MailFilterAction } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
8
+ import { ActionTypeDef, ConditionFieldDef } from "../../../shared/components/rules/RuleBuilder.js";
9
+
10
+ const FIELD_INPUT_CLASS =
11
+ "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
12
+
13
+ /** Folder types a mail filter can't meaningfully move/copy a message into — a mailbox's single
14
+ * well-known `calendar`/`contacts`/`tasks`/`notes` folder holds a different entity type entirely, not
15
+ * messages. */
16
+ const NON_MAIL_FOLDER_TYPES = new Set<FolderType>(["calendar", "contacts", "tasks", "notes"]);
17
+
18
+ export const MAIL_FILTER_CONDITION_FIELDS: ConditionFieldDef[] = [
19
+ { key: "fromContains", label: "From contains", kind: "list" },
20
+ { key: "subjectContains", label: "Subject contains", kind: "list" },
21
+ { key: "bodyContains", label: "Body contains", kind: "list" },
22
+ { key: "toCcContains", label: "Any To/Cc recipient equals", kind: "list" },
23
+ { key: "hasAttachment", label: "Has an attachment", kind: "boolean" },
24
+ {
25
+ key: "importance",
26
+ label: "Importance",
27
+ kind: "select",
28
+ options: [
29
+ { value: "low", label: "Low" },
30
+ { value: "normal", label: "Normal" },
31
+ { value: "high", label: "High" },
32
+ ],
33
+ },
34
+ ];
35
+
36
+ /**
37
+ * Builds the mail-filter action-type registry for `RuleBuilder`, closing over `folders` so the
38
+ * move/copy-to-folder actions can render a real folder picker. `RuleBuilder`'s own `render()` contract
39
+ * is synchronous, so this resolves `folders` once up front (the caller already loads them for the page
40
+ * itself) rather than each action row fetching independently.
41
+ */
42
+ export function buildMailFilterActionTypes(folders: Folder[]): ActionTypeDef<MailFilterAction>[] {
43
+ const destinationFolders = folders.filter((f) => !NON_MAIL_FOLDER_TYPES.has(f.type));
44
+
45
+ const folderSelect = (action: MailFilterAction, onChange: (next: MailFilterAction) => void) => (
46
+ <select
47
+ aria-label="Destination folder"
48
+ className={FIELD_INPUT_CLASS}
49
+ value={action.folderUid ?? ""}
50
+ onChange={(e) => onChange({ ...action, folderUid: e.target.value })}
51
+ >
52
+ <option value="">Choose a folder&hellip;</option>
53
+ {destinationFolders.map((folder) => (
54
+ <option key={folder.uid} value={folder.uid}>
55
+ {folder.name}
56
+ </option>
57
+ ))}
58
+ </select>
59
+ );
60
+
61
+ return [
62
+ {
63
+ value: "move_to_folder",
64
+ label: "Move to folder",
65
+ createDefault: () => ({ type: "move_to_folder", folderUid: "" }),
66
+ render: folderSelect,
67
+ },
68
+ {
69
+ value: "copy_to_folder",
70
+ label: "Copy to folder",
71
+ createDefault: () => ({ type: "copy_to_folder", folderUid: "" }),
72
+ render: folderSelect,
73
+ },
74
+ {
75
+ value: "delete",
76
+ label: "Delete the message",
77
+ createDefault: () => ({ type: "delete" }),
78
+ render: () => null,
79
+ },
80
+ {
81
+ value: "mark_as_read",
82
+ label: "Mark as read",
83
+ createDefault: () => ({ type: "mark_as_read" }),
84
+ render: () => null,
85
+ },
86
+ {
87
+ value: "forward",
88
+ label: "Forward to",
89
+ createDefault: () => ({ type: "forward", forwardTo: "" }),
90
+ render: (action, onChange) => (
91
+ <input
92
+ type="email"
93
+ aria-label="Forward to address"
94
+ className={FIELD_INPUT_CLASS}
95
+ placeholder="assistant@example.com"
96
+ value={action.forwardTo ?? ""}
97
+ onChange={(e) => onChange({ ...action, forwardTo: e.target.value })}
98
+ />
99
+ ),
100
+ },
101
+ ];
102
+ }
@@ -0,0 +1,99 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { useEffect, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { MailFilterRule, listMailFilterRules } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
8
+ import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
9
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
+
12
+ export type SettingsFiltersPageProps = Omit<SettingsShellProps, "active">;
13
+
14
+ export default function SettingsFiltersPage(props: SettingsFiltersPageProps) {
15
+ return (
16
+ <SettingsShell {...props} active="filters">
17
+ <FiltersContent />
18
+ </SettingsShell>
19
+ );
20
+ }
21
+
22
+ function FiltersContent() {
23
+ const { mailboxUid } = useSettingsShell();
24
+ const [rules, setRules] = useState<MailFilterRule[]>([]);
25
+ const [loading, setLoading] = useState(true);
26
+ const [error, setError] = useState<string | null>(null);
27
+
28
+ // `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same established
29
+ // non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
30
+ useEffect(() => {
31
+ listMailFilterRules(mailboxUid!)
32
+ .then(setRules)
33
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load mail filters."))
34
+ .finally(() => setLoading(false));
35
+ }, [mailboxUid]);
36
+
37
+ const sorted = [...rules].sort((a, b) => a.sequence - b.sequence);
38
+
39
+ return (
40
+ <div className="flex-1 min-w-0 overflow-y-auto p-6">
41
+ <div className="max-w-3xl">
42
+ <div className="flex items-center justify-between mb-5">
43
+ <h1 className="text-lg font-bold tracking-tight">Mail Filters</h1>
44
+ <a href={`/settings/filters/new?mailboxUid=${encodeURIComponent(mailboxUid!)}`}>
45
+ <Button type="button" className="!w-auto">
46
+ + New filter
47
+ </Button>
48
+ </a>
49
+ </div>
50
+ <p className="text-sm text-text-muted mb-4">
51
+ Applied in order once a message is verdicted safe to deliver, before it reaches your Inbox.
52
+ </p>
53
+
54
+ {error && <Alert>{error}</Alert>}
55
+
56
+ {loading ? (
57
+ <p className="text-sm text-text-muted">Loading&hellip;</p>
58
+ ) : sorted.length === 0 ? (
59
+ <p className="text-sm text-text-muted">No mail filters yet.</p>
60
+ ) : (
61
+ <div className="overflow-x-auto">
62
+ <table className="w-full text-sm border-collapse">
63
+ <thead>
64
+ <tr>
65
+ {["Sequence", "Name", "Enabled", "Actions", ""].map((h) => (
66
+ <th
67
+ key={h}
68
+ className="text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border"
69
+ >
70
+ {h}
71
+ </th>
72
+ ))}
73
+ </tr>
74
+ </thead>
75
+ <tbody>
76
+ {sorted.map((rule) => (
77
+ <tr key={rule.uid}>
78
+ <td className="py-2.5 px-2.5 border-b border-border">{rule.sequence}</td>
79
+ <td className="py-2.5 px-2.5 border-b border-border">{rule.name}</td>
80
+ <td className="py-2.5 px-2.5 border-b border-border">{rule.enabled ? "Yes" : "No"}</td>
81
+ <td className="py-2.5 px-2.5 border-b border-border">{rule.actions.length}</td>
82
+ <td className="py-2.5 px-2.5 border-b border-border text-right">
83
+ <a
84
+ href={`/settings/filters/${encodeURIComponent(rule.uid)}?mailboxUid=${encodeURIComponent(mailboxUid!)}`}
85
+ className="text-primary-dark hover:underline font-medium"
86
+ >
87
+ View
88
+ </a>
89
+ </td>
90
+ </tr>
91
+ ))}
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+ )}
96
+ </div>
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,126 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { FormEvent, useEffect, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { Folder, listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import {
9
+ MailFilterAction,
10
+ MailFilterConditions,
11
+ createMailFilterRule,
12
+ } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
13
+ import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../../shared/components/settings/layout/SettingsShell.js";
14
+ import RuleBuilder, { RuleBuilderValue } from "../../../../shared/components/rules/RuleBuilder.js";
15
+ import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "../_mailFilterRuleConfig.js";
16
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
17
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
18
+ import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
19
+
20
+ const INPUT_CLASS =
21
+ "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
22
+
23
+ export type NewMailFilterPageProps = Omit<SettingsShellProps, "active">;
24
+
25
+ export default function NewMailFilterPage(props: NewMailFilterPageProps) {
26
+ return (
27
+ <SettingsShell {...props} active="filters">
28
+ <NewMailFilterForm />
29
+ </SettingsShell>
30
+ );
31
+ }
32
+
33
+ function NewMailFilterForm() {
34
+ const { mailboxUid } = useSettingsShell();
35
+ const [folders, setFolders] = useState<Folder[] | null>(null);
36
+ const [folderError, setFolderError] = useState<string | null>(null);
37
+ const [name, setName] = useState("");
38
+ const [rule, setRule] = useState<RuleBuilderValue<MailFilterConditions, MailFilterAction>>({
39
+ enabled: true,
40
+ sequence: 0,
41
+ stopProcessingRules: false,
42
+ conditions: {},
43
+ actions: [],
44
+ });
45
+ const [error, setError] = useState<string | null>(null);
46
+ const [saving, setSaving] = useState(false);
47
+
48
+ // `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same established
49
+ // non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
50
+ useEffect(() => {
51
+ listFolders(mailboxUid!)
52
+ .then(setFolders)
53
+ .catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's folders."));
54
+ }, [mailboxUid]);
55
+
56
+ async function handleSubmit(e: FormEvent) {
57
+ e.preventDefault();
58
+ setError(null);
59
+
60
+ if (!name.trim()) {
61
+ setError("A name is required.");
62
+ return;
63
+ }
64
+
65
+ setSaving(true);
66
+ try {
67
+ const created = await createMailFilterRule({ mailboxUid: mailboxUid!, name: name.trim(), ...rule });
68
+ window.location.href = `/settings/filters/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid!)}`;
69
+ } catch (err) {
70
+ setError(err instanceof ApiRequestError ? err.message : "Could not create the mail filter.");
71
+ } finally {
72
+ setSaving(false);
73
+ }
74
+ }
75
+
76
+ return (
77
+ <div className="flex-1 min-w-0 overflow-y-auto p-6">
78
+ <div className="max-w-3xl">
79
+ <h1 className="text-lg font-bold tracking-tight mb-1">New mail filter</h1>
80
+ <p className="text-sm text-text-muted mb-5">
81
+ Applied in order once a message is verdicted safe to deliver, before it reaches your Inbox.
82
+ </p>
83
+
84
+ {error && <Alert>{error}</Alert>}
85
+ {folderError && <Alert>{folderError}</Alert>}
86
+
87
+ {folders === null ? (
88
+ <p className="text-sm text-text-muted">Loading&hellip;</p>
89
+ ) : (
90
+ <form onSubmit={handleSubmit} className="flex flex-col gap-5">
91
+ <div className="bg-surface border border-border rounded-md p-6">
92
+ <FormField label="Name" htmlFor="name">
93
+ <input
94
+ id="name"
95
+ type="text"
96
+ className={INPUT_CLASS}
97
+ value={name}
98
+ onChange={(e) => setName(e.target.value)}
99
+ placeholder="File newsletters"
100
+ />
101
+ </FormField>
102
+ </div>
103
+
104
+ <RuleBuilder
105
+ value={rule}
106
+ onChange={setRule}
107
+ conditionFields={MAIL_FILTER_CONDITION_FIELDS}
108
+ actionTypes={buildMailFilterActionTypes(folders)}
109
+ />
110
+
111
+ <div className="flex gap-3">
112
+ <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
113
+ Create filter
114
+ </Button>
115
+ <a href={`/settings/filters?mailboxUid=${encodeURIComponent(mailboxUid!)}`}>
116
+ <Button type="button" variant="secondary" className="!w-auto">
117
+ Cancel
118
+ </Button>
119
+ </a>
120
+ </div>
121
+ </form>
122
+ )}
123
+ </div>
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,148 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { FormEvent, useEffect, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { MessageClassification } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import {
9
+ FocusedInboxOverride,
10
+ createFocusedInboxOverride,
11
+ deleteFocusedInboxOverride,
12
+ listFocusedInboxOverrides,
13
+ } from "@rapidmx/react-shared/mail/focusedInboxOverridesApi.js";
14
+ import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
15
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
16
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
17
+
18
+ export type SettingsFocusedInboxPageProps = Omit<SettingsShellProps, "active">;
19
+
20
+ export default function SettingsFocusedInboxPage(props: SettingsFocusedInboxPageProps) {
21
+ return (
22
+ <SettingsShell {...props} active="focused-inbox">
23
+ <FocusedInboxContent />
24
+ </SettingsShell>
25
+ );
26
+ }
27
+
28
+ function FocusedInboxContent() {
29
+ const { mailboxUid } = useSettingsShell();
30
+ const [overrides, setOverrides] = useState<FocusedInboxOverride[]>([]);
31
+ const [loading, setLoading] = useState(true);
32
+ const [error, setError] = useState<string | null>(null);
33
+ const [saving, setSaving] = useState(false);
34
+ const [newSender, setNewSender] = useState("");
35
+ const [newClassifyAs, setNewClassifyAs] = useState<MessageClassification>("other");
36
+
37
+ function reload() {
38
+ setLoading(true);
39
+ setError(null);
40
+ // `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same
41
+ // established non-null pattern as `apps/www/settings/filters/index.tsx`.
42
+ listFocusedInboxOverrides(mailboxUid!)
43
+ .then(setOverrides)
44
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load your Focused Inbox rules."))
45
+ .finally(() => setLoading(false));
46
+ }
47
+
48
+ useEffect(reload, [mailboxUid]);
49
+
50
+ async function handleAdd(e: FormEvent) {
51
+ e.preventDefault();
52
+ const senderAddress = newSender.trim();
53
+ if (!senderAddress) {
54
+ return;
55
+ }
56
+ setSaving(true);
57
+ setError(null);
58
+ try {
59
+ await createFocusedInboxOverride({ mailboxUid: mailboxUid!, senderAddress, classifyAs: newClassifyAs });
60
+ setNewSender("");
61
+ reload();
62
+ } catch (err) {
63
+ setError(err instanceof ApiRequestError ? err.message : "Could not add this rule.");
64
+ } finally {
65
+ setSaving(false);
66
+ }
67
+ }
68
+
69
+ async function handleRemove(override: FocusedInboxOverride) {
70
+ setSaving(true);
71
+ setError(null);
72
+ try {
73
+ await deleteFocusedInboxOverride(override.uid, override.version);
74
+ reload();
75
+ } catch (err) {
76
+ setError(err instanceof ApiRequestError ? err.message : "Could not remove this rule.");
77
+ } finally {
78
+ setSaving(false);
79
+ }
80
+ }
81
+
82
+ return (
83
+ <div className="flex-1 min-w-0 overflow-y-auto p-6">
84
+ <div className="max-w-xl">
85
+ <h1 className="text-lg font-bold tracking-tight mb-1">Focused Inbox</h1>
86
+ <p className="text-sm text-text-muted mb-4">
87
+ Mail from a sender listed here always goes to the matching half of your Inbox, overriding
88
+ the automatic Focused/Other classification. Add one directly, or use &ldquo;Always for this
89
+ sender&rdquo; when moving a message.
90
+ </p>
91
+
92
+ {error && <Alert>{error}</Alert>}
93
+
94
+ {loading ? (
95
+ <p className="text-sm text-text-muted">Loading&hellip;</p>
96
+ ) : (
97
+ <ul className="flex flex-col gap-2 mb-4">
98
+ {overrides.length === 0 && <li className="text-sm text-text-muted">No rules yet.</li>}
99
+ {overrides.map((override) => (
100
+ <li
101
+ key={override.uid}
102
+ className="flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm"
103
+ >
104
+ <div>
105
+ <div className="text-sm font-medium">{override.senderAddress}</div>
106
+ <div className="text-xs text-text-muted">
107
+ Always {override.classifyAs === "other" ? "Other" : "Focused"}
108
+ </div>
109
+ </div>
110
+ <button
111
+ type="button"
112
+ onClick={() => handleRemove(override)}
113
+ disabled={saving}
114
+ className="text-sm text-danger hover:underline disabled:opacity-55"
115
+ >
116
+ Remove
117
+ </button>
118
+ </li>
119
+ ))}
120
+ </ul>
121
+ )}
122
+
123
+ <form onSubmit={handleAdd} className="flex gap-2">
124
+ <input
125
+ type="text"
126
+ aria-label="Sender address"
127
+ value={newSender}
128
+ onChange={(e) => setNewSender(e.target.value)}
129
+ placeholder="sender@example.com"
130
+ className="flex-1 text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary"
131
+ />
132
+ <select
133
+ aria-label="Classify as"
134
+ value={newClassifyAs}
135
+ onChange={(e) => setNewClassifyAs(e.target.value as MessageClassification)}
136
+ className="text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary"
137
+ >
138
+ <option value="other">Other</option>
139
+ <option value="focused">Focused</option>
140
+ </select>
141
+ <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
142
+ Add
143
+ </Button>
144
+ </form>
145
+ </div>
146
+ </div>
147
+ );
148
+ }