feat: P2.2 Create Appointment from Email + P2.4 Calendar Dashlet + P2.12 Action Wheel + P2.14 Share Files

- P2.2: 'Create Appointment' button in email viewer → pre-fills event modal
  with subject, body, participants, date. calendar-store newEventPrefill state.
- P2.4: MiniCalendarDashlet in sidebar bottom — month grid with event dots,
  day click navigates to calendar. Collapsible, respect firstDayOfWeek.
- P2.12: Custom radial menu (components/ui/radial-menu.tsx) — circular SVG
  menu with keyboard nav, animations. Wired into email-list, contact-list,
  file-browser, calendar-month-view right-click handlers.
- P2.14: 'Send as Attachment' button in file browser — opens compose tab
  with selected files pre-attached via Pro tab store.
This commit is contained in:
Bernd Rodler
2026-08-07 13:15:04 +02:00
parent 67f61f18d0
commit 83e29b3ef1
14 changed files with 933 additions and 42 deletions
+27 -2
View File
@@ -97,6 +97,7 @@ export default function CalendarPage() {
setSelectedDate, setViewMode, toggleCalendarVisibility, updateCalendar, shareCalendar,
removeCalendar, clearCalendarEvents,
refreshAllSubscriptions, icalSubscriptions,
newEventPrefill, setNewEventPrefill,
} = useCalendarStore();
const calendarEnabled = usePolicyStore((s) => s.isFeatureEnabled('calendarEnabled'));
const calendarTasksEnabled = usePolicyStore((s) => s.isFeatureEnabled('calendarTasksEnabled'));
@@ -464,6 +465,19 @@ export default function CalendarPage() {
setShowEventModal(true);
}, [selectedDate, setSelectedDate]);
useEffect(() => {
if (!newEventPrefill) return;
setEditEvent(null);
if (newEventPrefill.date) {
const d = new Date(newEventPrefill.date);
if (!isNaN(d.getTime())) {
setDefaultModalDate(d);
setSelectedDate(d);
}
}
setShowEventModal(true);
}, [newEventPrefill, setSelectedDate]);
const openEditModal = useCallback((event: CalendarEvent) => {
setEditEvent(event);
setDefaultModalDate(undefined);
@@ -1198,6 +1212,9 @@ export default function CalendarPage() {
onContextMenuEvent={handleContextMenuEvent}
onContextMenuEmpty={handleContextMenuEmpty}
onCreateAtTime={openCreateModal}
onEditEvent={openEditModal}
onDeleteEvent={handleDeleteContextMenu}
onDuplicateEvent={handleDuplicateContextMenu}
firstDayOfWeek={firstDayOfWeek}
isMobile={isMobile}
pendingPreview={pendingPreview}
@@ -1490,10 +1507,14 @@ export default function CalendarPage() {
onDelete={handleDeleteEvent}
onDuplicate={handleDuplicateEvent}
onRsvp={handleRsvp}
onClose={() => { setShowEventModal(false); setEditEvent(null); setPendingPreview(null); setDefaultCalendarIdForCreate(undefined); setDefaultModalAllDay(false); }}
onClose={() => { setShowEventModal(false); setEditEvent(null); setPendingPreview(null); setDefaultCalendarIdForCreate(undefined); setDefaultModalAllDay(false); setNewEventPrefill(null); }}
onPreviewChange={setPendingPreview}
currentUserEmails={currentUserEmails}
isMobile={false}
prefillTitle={editEvent ? undefined : newEventPrefill?.title}
prefillDescription={editEvent ? undefined : newEventPrefill?.description}
prefillParticipants={editEvent ? undefined : newEventPrefill?.participants}
prefillDate={editEvent ? undefined : newEventPrefill?.date}
/>
</div>
)}
@@ -1620,9 +1641,13 @@ export default function CalendarPage() {
onDelete={handleDeleteEvent}
onDuplicate={handleDuplicateEvent}
onRsvp={handleRsvp}
onClose={() => { setShowEventModal(false); setEditEvent(null); setDefaultCalendarIdForCreate(undefined); setDefaultModalAllDay(false); }}
onClose={() => { setShowEventModal(false); setEditEvent(null); setDefaultCalendarIdForCreate(undefined); setDefaultModalAllDay(false); setNewEventPrefill(null); }}
currentUserEmails={currentUserEmails}
isMobile={true}
prefillTitle={editEvent ? undefined : newEventPrefill?.title}
prefillDescription={editEvent ? undefined : newEventPrefill?.description}
prefillParticipants={editEvent ? undefined : newEventPrefill?.participants}
prefillDate={editEvent ? undefined : newEventPrefill?.date}
/>
)}
+27
View File
@@ -11,6 +11,7 @@ import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
import { useAccountStore } from "@/stores/account-store";
import { useEmailStore } from "@/stores/email-store";
import { useFileStore } from "@/stores/file-store";
import { useProTabStore } from "@/stores/pro-tab-store";
import { toast } from "@/stores/toast-store";
import { cn, formatFileSize } from "@/lib/utils";
import { NavigationRail } from "@/components/layout/navigation-rail";
@@ -411,6 +412,31 @@ export default function FilesPage() {
await shareResource(id, principalId, rights);
}, [shareResource]);
const handleSendAsAttachment = useCallback((names: string[]) => {
const store = useFileStore.getState();
const fileAtts = names
.map((name) => {
const r = store.resources.find((res) => res.name === name);
if (!r || r.isDirectory || !r.blobId) return null;
return {
blobId: r.blobId,
name: r.name,
type: r.contentType || "application/octet-stream",
size: r.contentLength,
};
})
.filter(Boolean) as Array<{ blobId: string; name: string; type: string; size: number }>;
if (fileAtts.length === 0) return;
useProTabStore.getState().openComposeTab({
sessionId: Date.now(),
mode: "compose",
replyTo: { attachments: fileAtts },
title: fileAtts.length === 1 ? fileAtts[0].name : `${fileAtts.length} attachments`,
});
}, []);
// Pro shell only: all connected accounts are equal top-level entries at
// the root. The root path "/" itself is a cross-account picker - no
// account's files are shown until the user enters one.
@@ -554,6 +580,7 @@ export default function FilesPage() {
ownAccountId={filesAccountId}
sharingEnabled={sharingEnabled}
onShare={handleShare}
onSendAsAttachment={handleSendAsAttachment}
/>
</div>
)}