@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,201 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { FormEvent, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { HiOutlineBars3 } from "react-icons/hi2";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { Task, TaskList, createTaskList, listTaskLists } from "@rapidmx/react-shared/tasks/tasksApi.js";
|
|
9
|
+
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
10
|
+
|
|
11
|
+
export type TasksView =
|
|
12
|
+
| { type: "myDay" }
|
|
13
|
+
| { type: "important" }
|
|
14
|
+
| { type: "planned" }
|
|
15
|
+
| { type: "assignedToMe" }
|
|
16
|
+
| { type: "flagged" }
|
|
17
|
+
| { type: "all" }
|
|
18
|
+
| { type: "list"; uid: string; name: string };
|
|
19
|
+
|
|
20
|
+
export function tasksViewKey(view: TasksView): string {
|
|
21
|
+
return view.type === "list" ? `list:${view.uid}` : view.type;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TasksSidebarProps {
|
|
25
|
+
mailboxUid?: string;
|
|
26
|
+
/** The caller's own (incomplete) tasks — used to compute every smart-filter's count. Deliberately not
|
|
27
|
+
* fetched again here; the parent already has this loaded for the main list. */
|
|
28
|
+
tasks: Task[];
|
|
29
|
+
userUid?: string;
|
|
30
|
+
active: TasksView;
|
|
31
|
+
onSelect: (view: TasksView) => void;
|
|
32
|
+
/** Bumped by the parent to force a contact-list-style re-fetch of task lists (mirrors
|
|
33
|
+
* `ContactsSidebar`'s identical prop) — not currently used by any caller, kept for parity/future use. */
|
|
34
|
+
refreshToken?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function NavItem({ label, count, active, onClick }: { label: string; count?: number; active: boolean; onClick: () => void }) {
|
|
38
|
+
return (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={onClick}
|
|
42
|
+
aria-current={active ? "true" : undefined}
|
|
43
|
+
className={[
|
|
44
|
+
"w-full flex items-center justify-between gap-2 text-sm text-left px-2.5 py-1.5 rounded-sm",
|
|
45
|
+
active ? "bg-primary/10 text-primary-dark font-semibold" : "text-text hover:bg-surface-alt",
|
|
46
|
+
].join(" ")}
|
|
47
|
+
>
|
|
48
|
+
<span className="truncate">{label}</span>
|
|
49
|
+
{count !== undefined && <span className="text-xs text-text-muted shrink-0">{count}</span>}
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Tasks' Outlook To-Do-style left sidebar. "My Day"/"Assigned to me" are real persisted fields
|
|
56
|
+
* (`Task.myDay`/`assignedTo`, from Phase 0); "Important"/"Planned" are pure smart-filters derived from
|
|
57
|
+
* already-loaded fields (`priority === "high"`/`dueDate != null`) with no backend concept of their own;
|
|
58
|
+
* "Flagged email" is a `Message` smart-filter (see `flaggedMessages.ts`), not a `Task` one at all — the
|
|
59
|
+
* page renders it as a distinct list, not the task table. "Tasks" is the flat, unfiltered default. Custom
|
|
60
|
+
* lists are real `TaskList` records, fetched here (same pattern as `ContactsSidebar`'s contact lists).
|
|
61
|
+
*/
|
|
62
|
+
export default function TasksSidebar({ mailboxUid, tasks, userUid, active, onSelect, refreshToken }: TasksSidebarProps) {
|
|
63
|
+
const [lists, setLists] = useState<TaskList[]>([]);
|
|
64
|
+
const [listsError, setListsError] = useState<string | null>(null);
|
|
65
|
+
const [addingList, setAddingList] = useState(false);
|
|
66
|
+
const [newListName, setNewListName] = useState("");
|
|
67
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!mailboxUid) {
|
|
71
|
+
setLists([]);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setListsError(null);
|
|
75
|
+
listTaskLists(mailboxUid, { limit: 200 })
|
|
76
|
+
.then(setLists)
|
|
77
|
+
.catch((err) => setListsError(err instanceof ApiRequestError ? err.message : "Could not load task lists."));
|
|
78
|
+
}, [mailboxUid, refreshToken]);
|
|
79
|
+
|
|
80
|
+
const myDayCount = useMemo(() => tasks.filter((t) => t.myDay).length, [tasks]);
|
|
81
|
+
const importantCount = useMemo(() => tasks.filter((t) => t.priority === "high").length, [tasks]);
|
|
82
|
+
const plannedCount = useMemo(() => tasks.filter((t) => t.dueDate != null).length, [tasks]);
|
|
83
|
+
const assignedToMeCount = useMemo(() => tasks.filter((t) => t.assignedTo === userUid).length, [tasks, userUid]);
|
|
84
|
+
const listCounts = useMemo(() => {
|
|
85
|
+
const counts = new Map<string, number>();
|
|
86
|
+
for (const t of tasks) {
|
|
87
|
+
if (t.taskListUid) {
|
|
88
|
+
counts.set(t.taskListUid, (counts.get(t.taskListUid) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return counts;
|
|
92
|
+
}, [tasks]);
|
|
93
|
+
|
|
94
|
+
async function handleAddList(e: FormEvent) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
if (!mailboxUid || !newListName.trim()) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const created = await createTaskList({ mailboxUid, name: newListName.trim() });
|
|
101
|
+
setLists((prev) => [...prev, created].sort((a, b) => a.name.localeCompare(b.name)));
|
|
102
|
+
setNewListName("");
|
|
103
|
+
setAddingList(false);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
setListsError(err instanceof ApiRequestError ? err.message : "Could not create this list.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleSelect(view: TasksView) {
|
|
110
|
+
onSelect(view);
|
|
111
|
+
setDrawerOpen(false);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A function, not a plain JSX constant — rendered both in the desktop `<nav>` and the mobile
|
|
115
|
+
// `Drawer` (see `ContactsSidebar` for the identical pattern and the duplicate-id reasoning behind
|
|
116
|
+
// the `idPrefix`).
|
|
117
|
+
const navContent = (idPrefix: string) => (
|
|
118
|
+
<>
|
|
119
|
+
<div className="flex flex-col gap-0.5">
|
|
120
|
+
<NavItem label="My Day" count={myDayCount} active={active.type === "myDay"} onClick={() => handleSelect({ type: "myDay" })} />
|
|
121
|
+
<NavItem
|
|
122
|
+
label="Important"
|
|
123
|
+
count={importantCount}
|
|
124
|
+
active={active.type === "important"}
|
|
125
|
+
onClick={() => handleSelect({ type: "important" })}
|
|
126
|
+
/>
|
|
127
|
+
<NavItem label="Planned" count={plannedCount} active={active.type === "planned"} onClick={() => handleSelect({ type: "planned" })} />
|
|
128
|
+
<NavItem
|
|
129
|
+
label="Assigned to me"
|
|
130
|
+
count={assignedToMeCount}
|
|
131
|
+
active={active.type === "assignedToMe"}
|
|
132
|
+
onClick={() => handleSelect({ type: "assignedToMe" })}
|
|
133
|
+
/>
|
|
134
|
+
<NavItem label="Flagged email" active={active.type === "flagged"} onClick={() => handleSelect({ type: "flagged" })} />
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<NavItem label="Tasks" count={tasks.length} active={active.type === "all"} onClick={() => handleSelect({ type: "all" })} />
|
|
138
|
+
|
|
139
|
+
<div>
|
|
140
|
+
<div className="flex items-center justify-between px-2.5 mb-1">
|
|
141
|
+
<h2 className="text-xs font-bold uppercase tracking-wide text-text-muted">Lists</h2>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => setAddingList(true)}
|
|
145
|
+
aria-label="New list"
|
|
146
|
+
className="text-xs font-bold text-primary-dark hover:underline"
|
|
147
|
+
>
|
|
148
|
+
+
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
{listsError && <p className="px-2.5 text-xs text-danger">{listsError}</p>}
|
|
152
|
+
<div className="flex flex-col gap-0.5">
|
|
153
|
+
{lists.map((list) => (
|
|
154
|
+
<NavItem
|
|
155
|
+
key={list.uid}
|
|
156
|
+
label={list.name}
|
|
157
|
+
count={listCounts.get(list.uid) ?? 0}
|
|
158
|
+
active={active.type === "list" && active.uid === list.uid}
|
|
159
|
+
onClick={() => handleSelect({ type: "list", uid: list.uid, name: list.name })}
|
|
160
|
+
/>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
{addingList && (
|
|
164
|
+
<form onSubmit={handleAddList} className="flex items-center gap-1 px-2.5 mt-1">
|
|
165
|
+
<input
|
|
166
|
+
type="text"
|
|
167
|
+
autoFocus
|
|
168
|
+
aria-label="New list name"
|
|
169
|
+
id={`${idPrefix}-new-list-name`}
|
|
170
|
+
value={newListName}
|
|
171
|
+
onChange={(e) => setNewListName(e.target.value)}
|
|
172
|
+
className="flex-1 text-xs py-1 px-1.5 border border-border rounded-sm bg-surface"
|
|
173
|
+
/>
|
|
174
|
+
<button type="submit" className="text-xs font-semibold text-primary-dark">
|
|
175
|
+
Add
|
|
176
|
+
</button>
|
|
177
|
+
</form>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<>
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
|
|
188
|
+
aria-label="Open tasks menu"
|
|
189
|
+
onClick={() => setDrawerOpen(true)}
|
|
190
|
+
>
|
|
191
|
+
<HiOutlineBars3 size={20} aria-hidden="true" />
|
|
192
|
+
</button>
|
|
193
|
+
<nav aria-label="Tasks" className="hidden md:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto">
|
|
194
|
+
{navContent("desktop")}
|
|
195
|
+
</nav>
|
|
196
|
+
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Tasks">
|
|
197
|
+
<div className="flex flex-col gap-4">{navContent("mobile")}</div>
|
|
198
|
+
</Drawer>
|
|
199
|
+
</>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import type { IconType } from "react-icons";
|
|
7
|
+
import { HiOutlineCheckCircle, HiOutlineListBullet, HiOutlineSun, HiOutlineTableCells, HiOutlineTrash } from "react-icons/hi2";
|
|
8
|
+
|
|
9
|
+
export type TasksViewMode = "list" | "grid";
|
|
10
|
+
|
|
11
|
+
export interface TasksToolbarProps {
|
|
12
|
+
viewMode: TasksViewMode;
|
|
13
|
+
onViewModeChange: (mode: TasksViewMode) => void;
|
|
14
|
+
selectedCount: number;
|
|
15
|
+
onComplete: () => void;
|
|
16
|
+
onDelete: () => void;
|
|
17
|
+
onAddToMyDay: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ToolbarButton({
|
|
21
|
+
label,
|
|
22
|
+
icon: Icon,
|
|
23
|
+
active,
|
|
24
|
+
disabled,
|
|
25
|
+
onClick,
|
|
26
|
+
}: {
|
|
27
|
+
label: string;
|
|
28
|
+
icon: IconType;
|
|
29
|
+
active?: boolean;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
onClick: () => void;
|
|
32
|
+
}) {
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
aria-label={label}
|
|
37
|
+
aria-pressed={active}
|
|
38
|
+
title={label}
|
|
39
|
+
disabled={disabled}
|
|
40
|
+
onClick={onClick}
|
|
41
|
+
className={[
|
|
42
|
+
"flex flex-col items-center gap-1 text-xs px-2.5 py-1.5 rounded-sm disabled:opacity-40 disabled:cursor-not-allowed",
|
|
43
|
+
active ? "bg-primary/10 text-primary-dark" : "text-text-muted hover:not-disabled:bg-surface-alt hover:not-disabled:text-text",
|
|
44
|
+
].join(" ")}
|
|
45
|
+
>
|
|
46
|
+
<Icon size={18} aria-hidden="true" />
|
|
47
|
+
<span>{label}</span>
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Tasks' toolbar: a Grid/List view-mode toggle (local, not persisted) plus bulk actions for whatever
|
|
53
|
+
* rows are checked (Complete, Add to My Day, Delete) — deliberately presentational, matching
|
|
54
|
+
* `ContactsToolbar`'s convention, since the page owns the actual task data/selection. */
|
|
55
|
+
export default function TasksToolbar({ viewMode, onViewModeChange, selectedCount, onComplete, onDelete, onAddToMyDay }: TasksToolbarProps) {
|
|
56
|
+
const hasSelection = selectedCount > 0;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div role="toolbar" aria-label="Tasks actions" className="border-b border-border bg-surface-alt flex items-center gap-0.5 px-2 py-1">
|
|
60
|
+
<ToolbarButton label="List" icon={HiOutlineListBullet} active={viewMode === "list"} onClick={() => onViewModeChange("list")} />
|
|
61
|
+
<ToolbarButton label="Grid" icon={HiOutlineTableCells} active={viewMode === "grid"} onClick={() => onViewModeChange("grid")} />
|
|
62
|
+
<div className="w-px self-stretch my-1 bg-border" aria-hidden="true" />
|
|
63
|
+
<ToolbarButton label="Complete" icon={HiOutlineCheckCircle} disabled={!hasSelection} onClick={onComplete} />
|
|
64
|
+
<ToolbarButton label="Add to My Day" icon={HiOutlineSun} disabled={!hasSelection} onClick={onAddToMyDay} />
|
|
65
|
+
<ToolbarButton label="Delete" icon={HiOutlineTrash} disabled={!hasSelection} onClick={onDelete} />
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { HiOutlineBars3 } from "react-icons/hi2";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
9
|
+
import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
12
|
+
import AppShell, { AppShellProps } from "../../layout/AppShell.js";
|
|
13
|
+
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
14
|
+
|
|
15
|
+
export type TasksShellProps = Omit<AppShellProps, "active">;
|
|
16
|
+
|
|
17
|
+
export interface TasksShellContextValue {
|
|
18
|
+
/** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
|
|
19
|
+
mailboxUid?: string;
|
|
20
|
+
/** The selected mailbox's single `tasks`-type folder. */
|
|
21
|
+
folderUid?: string;
|
|
22
|
+
mailboxes: Mailbox[];
|
|
23
|
+
/** The caller's own uid — exposed here (rather than only as `AppShell`'s prop) so the page content
|
|
24
|
+
* can compute "assigned to me" without needing it threaded through separately. */
|
|
25
|
+
userUid?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TasksShellContext = createContext<TasksShellContextValue>({ mailboxes: [] });
|
|
29
|
+
|
|
30
|
+
/** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
|
|
31
|
+
export function useTasksShell(): TasksShellContextValue {
|
|
32
|
+
return useContext(TasksShellContext);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Status = "checking" | "error" | "ready";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The Tasks app's shell — structurally identical to `ContactsShell` (a mailbox has exactly one
|
|
39
|
+
* well-known `tasks` folder, guaranteed to exist by `BaseMailboxRoute.create()`'s eager provisioning
|
|
40
|
+
* in `@rapidmx/restapi`), just bound to a different folder type. Kept as its own small component
|
|
41
|
+
* (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
|
|
42
|
+
* grow its own app-specific controls later without threading extra props through a shared one.
|
|
43
|
+
*/
|
|
44
|
+
export default function TasksShell({
|
|
45
|
+
userUid,
|
|
46
|
+
authServerUrl,
|
|
47
|
+
impersonating,
|
|
48
|
+
impersonationBaseUrl,
|
|
49
|
+
trusted,
|
|
50
|
+
children,
|
|
51
|
+
}: PropsWithChildren<TasksShellProps>) {
|
|
52
|
+
const [status, setStatus] = useState<Status>("checking");
|
|
53
|
+
const [error, setError] = useState<string | null>(null);
|
|
54
|
+
const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
|
|
55
|
+
const [folders, setFolders] = useState<Folder[]>([]);
|
|
56
|
+
const [folderError, setFolderError] = useState<string | null>(null);
|
|
57
|
+
const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
|
|
58
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!userUid) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
listMailboxes({ limit: 100 })
|
|
69
|
+
.then((result) => {
|
|
70
|
+
setMailboxes(result);
|
|
71
|
+
setStatus("ready");
|
|
72
|
+
})
|
|
73
|
+
.catch((err) => {
|
|
74
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
|
|
75
|
+
setStatus("error");
|
|
76
|
+
});
|
|
77
|
+
}, [userUid]);
|
|
78
|
+
|
|
79
|
+
const mailboxUid: string | undefined =
|
|
80
|
+
(requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
81
|
+
mailboxes[0]?.uid;
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!mailboxUid) {
|
|
85
|
+
setFolders([]);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setFolderError(null);
|
|
89
|
+
listFolders(mailboxUid)
|
|
90
|
+
.then(setFolders)
|
|
91
|
+
.catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's tasks folder."));
|
|
92
|
+
}, [mailboxUid]);
|
|
93
|
+
|
|
94
|
+
const folderUid: string | undefined = folders.find((f) => f.type === "tasks")?.uid;
|
|
95
|
+
|
|
96
|
+
const contextValue = useMemo<TasksShellContextValue>(
|
|
97
|
+
() => ({ mailboxUid, folderUid, mailboxes, userUid }),
|
|
98
|
+
[mailboxUid, folderUid, mailboxes, userUid],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
|
|
102
|
+
// for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
|
|
103
|
+
if (userUid && status === "ready" && !mailboxUid) {
|
|
104
|
+
return <MailboxProvisioning />;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let inner: ReactNode = null;
|
|
108
|
+
if (userUid && status === "checking") {
|
|
109
|
+
// Renders immediately (no network round trip needed) so switching into Tasks never shows a
|
|
110
|
+
// blank pane while `listMailboxes()` is in flight — the real sidebar (My Day/Important/Planned/
|
|
111
|
+
// lists) lives in `apps/www/tasks/index.tsx`'s own content, so this mimics its rough shape rather
|
|
112
|
+
// than the (minimal, mailbox-switcher-only) shape of this shell's own sidebar.
|
|
113
|
+
inner = (
|
|
114
|
+
<aside className="w-56 shrink-0 bg-surface border-r border-border flex flex-col p-3 gap-4">
|
|
115
|
+
<Skeleton height="h-9" />
|
|
116
|
+
<SkeletonList count={5} />
|
|
117
|
+
</aside>
|
|
118
|
+
);
|
|
119
|
+
} else if (userUid && status === "error") {
|
|
120
|
+
inner = (
|
|
121
|
+
<div className="flex-1 flex items-center justify-center p-8">
|
|
122
|
+
<div className="w-full max-w-md">
|
|
123
|
+
<Alert>{error}</Alert>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
} else if (userUid && status === "ready") {
|
|
128
|
+
// A function, not a plain JSX constant — see MailShell/ContactsShell/CalendarShell's identical
|
|
129
|
+
// comment: it's rendered twice (desktop `<aside>` + mobile `Drawer`), possibly simultaneously
|
|
130
|
+
// mounted, so the `<select>`'s `id`/its `<label>`'s `htmlFor` need a distinct value per instance.
|
|
131
|
+
const sidebarContent = (idPrefix: string) => (
|
|
132
|
+
<>
|
|
133
|
+
{mailboxes.length > 1 && (
|
|
134
|
+
<div>
|
|
135
|
+
<label
|
|
136
|
+
className="block text-xs font-bold uppercase tracking-wide text-text-muted mb-1"
|
|
137
|
+
htmlFor={`${idPrefix}-tasks-mailbox-switcher`}
|
|
138
|
+
>
|
|
139
|
+
Mailbox
|
|
140
|
+
</label>
|
|
141
|
+
<select
|
|
142
|
+
id={`${idPrefix}-tasks-mailbox-switcher`}
|
|
143
|
+
className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
|
|
144
|
+
value={mailboxUid}
|
|
145
|
+
onChange={(e) => {
|
|
146
|
+
window.location.href = `/tasks?mailboxUid=${encodeURIComponent(e.target.value)}`;
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{mailboxes.map((mb) => (
|
|
150
|
+
<option key={mb.uid} value={mb.uid}>
|
|
151
|
+
{mb.displayName}
|
|
152
|
+
{mb.ownerUserUid ? "" : " (shared)"}
|
|
153
|
+
</option>
|
|
154
|
+
))}
|
|
155
|
+
</select>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
{folderError && <Alert>{folderError}</Alert>}
|
|
159
|
+
</>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
inner = (
|
|
163
|
+
<>
|
|
164
|
+
<aside className="hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
|
|
165
|
+
{sidebarContent("desktop")}
|
|
166
|
+
</aside>
|
|
167
|
+
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox">
|
|
168
|
+
<div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
|
|
169
|
+
</Drawer>
|
|
170
|
+
<main className="flex-1 min-w-0 flex flex-col">
|
|
171
|
+
{(mailboxes.length > 1 || folderError) && (
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
|
|
175
|
+
aria-label="Open mailbox switcher"
|
|
176
|
+
onClick={() => setDrawerOpen(true)}
|
|
177
|
+
>
|
|
178
|
+
<HiOutlineBars3 size={20} aria-hidden="true" />
|
|
179
|
+
</button>
|
|
180
|
+
)}
|
|
181
|
+
<TasksShellContext.Provider value={contextValue}>{children}</TasksShellContext.Provider>
|
|
182
|
+
</main>
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<AppShell
|
|
189
|
+
active="tasks"
|
|
190
|
+
userUid={userUid}
|
|
191
|
+
authServerUrl={authServerUrl}
|
|
192
|
+
impersonating={impersonating}
|
|
193
|
+
impersonationBaseUrl={impersonationBaseUrl}
|
|
194
|
+
trusted={trusted}
|
|
195
|
+
>
|
|
196
|
+
{inner}
|
|
197
|
+
</AppShell>
|
|
198
|
+
);
|
|
199
|
+
}
|