feat: implement hover functionality for calendar events and enhance email deletion confirmation
This commit is contained in:
@@ -211,16 +211,35 @@ export default function CalendarPage() {
|
|||||||
setShowEventModal(true);
|
setShowEventModal(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelectEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => {
|
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
setDetailEvent(event);
|
|
||||||
setDetailAnchorRect(anchorRect);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeDetail = useCallback(() => {
|
const closeDetail = useCallback(() => {
|
||||||
|
if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; }
|
||||||
setDetailEvent(null);
|
setDetailEvent(null);
|
||||||
setDetailAnchorRect(null);
|
setDetailAnchorRect(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectEvent = useCallback((event: CalendarEvent, _anchorRect: DOMRect) => {
|
||||||
|
// Click opens the sidebar for viewing/editing
|
||||||
|
closeDetail();
|
||||||
|
openEditModal(event);
|
||||||
|
}, [closeDetail, openEditModal]);
|
||||||
|
|
||||||
|
const handleHoverEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => {
|
||||||
|
if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; }
|
||||||
|
// Don't show hover popover if the sidebar is already open for this event
|
||||||
|
if (showEventModal && editEvent?.id === event.id) return;
|
||||||
|
setDetailEvent(event);
|
||||||
|
setDetailAnchorRect(anchorRect);
|
||||||
|
}, [showEventModal, editEvent]);
|
||||||
|
|
||||||
|
const handleHoverLeave = useCallback(() => {
|
||||||
|
hoverTimerRef.current = setTimeout(() => {
|
||||||
|
setDetailEvent(null);
|
||||||
|
setDetailAnchorRect(null);
|
||||||
|
}, 200);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleEditFromDetail = useCallback(() => {
|
const handleEditFromDetail = useCallback(() => {
|
||||||
if (detailEvent) {
|
if (detailEvent) {
|
||||||
const ev = detailEvent;
|
const ev = detailEvent;
|
||||||
@@ -581,6 +600,8 @@ export default function CalendarPage() {
|
|||||||
calendars={calendars}
|
calendars={calendars}
|
||||||
onSelectDate={handleSelectDate}
|
onSelectDate={handleSelectDate}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
|
onHoverEvent={handleHoverEvent}
|
||||||
|
onHoverLeave={handleHoverLeave}
|
||||||
firstDayOfWeek={firstDayOfWeek}
|
firstDayOfWeek={firstDayOfWeek}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
@@ -593,6 +614,8 @@ export default function CalendarPage() {
|
|||||||
calendars={calendars}
|
calendars={calendars}
|
||||||
onSelectDate={handleSelectDate}
|
onSelectDate={handleSelectDate}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
|
onHoverEvent={handleHoverEvent}
|
||||||
|
onHoverLeave={handleHoverLeave}
|
||||||
onCreateAtTime={openCreateModal}
|
onCreateAtTime={openCreateModal}
|
||||||
firstDayOfWeek={firstDayOfWeek}
|
firstDayOfWeek={firstDayOfWeek}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
@@ -606,6 +629,8 @@ export default function CalendarPage() {
|
|||||||
events={visibleEvents}
|
events={visibleEvents}
|
||||||
calendars={calendars}
|
calendars={calendars}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
|
onHoverEvent={handleHoverEvent}
|
||||||
|
onHoverLeave={handleHoverLeave}
|
||||||
onCreateAtTime={openCreateModal}
|
onCreateAtTime={openCreateModal}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
@@ -618,6 +643,8 @@ export default function CalendarPage() {
|
|||||||
events={visibleEvents}
|
events={visibleEvents}
|
||||||
calendars={calendars}
|
calendars={calendars}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
|
onHoverEvent={handleHoverEvent}
|
||||||
|
onHoverLeave={handleHoverLeave}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -739,6 +766,8 @@ export default function CalendarPage() {
|
|||||||
onClose={closeDetail}
|
onClose={closeDetail}
|
||||||
onSaveNote={handleSaveNoteFromDetail}
|
onSaveNote={handleSaveNoteFromDetail}
|
||||||
onRsvp={handleRsvpFromDetail}
|
onRsvp={handleRsvpFromDetail}
|
||||||
|
onMouseEnter={() => { if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } }}
|
||||||
|
onMouseLeave={handleHoverLeave}
|
||||||
currentUserEmails={currentUserEmails}
|
currentUserEmails={currentUserEmails}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
|||||||
+29
-4
@@ -454,10 +454,35 @@ export default function Home() {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!client || !selectedEmail) return;
|
if (!client || !selectedEmail) return;
|
||||||
|
|
||||||
try {
|
// Check if we're currently in the trash folder
|
||||||
await deleteEmail(client, selectedEmail.id);
|
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
|
||||||
} catch (error) {
|
const isInTrash = currentMailbox?.role === 'trash';
|
||||||
console.error("Failed to delete email:", error);
|
|
||||||
|
if (isInTrash) {
|
||||||
|
// In trash: confirm before permanently deleting
|
||||||
|
const confirmed = await confirmDialog({
|
||||||
|
title: t('email_list.permanent_delete_confirm_title'),
|
||||||
|
message: t('email_list.permanent_delete_confirm_message'),
|
||||||
|
confirmText: t('email_list.permanent_delete'),
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteEmail(client, selectedEmail.id, true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to permanently delete email:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not in trash: always move to trash
|
||||||
|
const trashMailbox = mailboxes.find(m => m.role === 'trash' && !m.isShared);
|
||||||
|
if (trashMailbox) {
|
||||||
|
try {
|
||||||
|
await moveToMailbox(client, selectedEmail.id, trashMailbox.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to move email to trash:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ interface CalendarAgendaViewProps {
|
|||||||
events: CalendarEvent[];
|
events: CalendarEvent[];
|
||||||
calendars: Calendar[];
|
calendars: Calendar[];
|
||||||
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||||
|
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||||
|
onHoverLeave?: () => void;
|
||||||
timeFormat?: "12h" | "24h";
|
timeFormat?: "12h" | "24h";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +30,8 @@ export function CalendarAgendaView({
|
|||||||
events,
|
events,
|
||||||
calendars,
|
calendars,
|
||||||
onSelectEvent,
|
onSelectEvent,
|
||||||
|
onHoverEvent,
|
||||||
|
onHoverLeave,
|
||||||
timeFormat = "24h",
|
timeFormat = "24h",
|
||||||
}: CalendarAgendaViewProps) {
|
}: CalendarAgendaViewProps) {
|
||||||
const t = useTranslations("calendar");
|
const t = useTranslations("calendar");
|
||||||
@@ -140,6 +144,8 @@ export function CalendarAgendaView({
|
|||||||
<button
|
<button
|
||||||
key={ev.id}
|
key={ev.id}
|
||||||
onClick={(e) => onSelectEvent(ev, e.currentTarget.getBoundingClientRect())}
|
onClick={(e) => onSelectEvent(ev, e.currentTarget.getBoundingClientRect())}
|
||||||
|
onMouseEnter={(e) => onHoverEvent?.(ev, e.currentTarget.getBoundingClientRect())}
|
||||||
|
onMouseLeave={() => onHoverLeave?.()}
|
||||||
className="w-full flex items-start gap-3 px-4 py-3 hover:bg-muted/50 transition-colors text-left"
|
className="w-full flex items-start gap-3 px-4 py-3 hover:bg-muted/50 transition-colors text-left"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center pt-0.5 min-w-[60px]">
|
<div className="flex flex-col items-center pt-0.5 min-w-[60px]">
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ interface CalendarDayViewProps {
|
|||||||
events: CalendarEvent[];
|
events: CalendarEvent[];
|
||||||
calendars: Calendar[];
|
calendars: Calendar[];
|
||||||
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||||
|
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||||
|
onHoverLeave?: () => void;
|
||||||
onCreateAtTime: (date: Date, endDate?: Date) => void;
|
onCreateAtTime: (date: Date, endDate?: Date) => void;
|
||||||
timeFormat?: "12h" | "24h";
|
timeFormat?: "12h" | "24h";
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
@@ -28,6 +30,8 @@ export function CalendarDayView({
|
|||||||
events,
|
events,
|
||||||
calendars,
|
calendars,
|
||||||
onSelectEvent,
|
onSelectEvent,
|
||||||
|
onHoverEvent,
|
||||||
|
onHoverLeave,
|
||||||
onCreateAtTime,
|
onCreateAtTime,
|
||||||
timeFormat = "24h",
|
timeFormat = "24h",
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -134,6 +138,8 @@ export function CalendarDayView({
|
|||||||
calendar={calendarMap.get(calId)}
|
calendar={calendarMap.get(calId)}
|
||||||
variant="chip"
|
variant="chip"
|
||||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||||
|
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||||
|
onMouseLeave={onHoverLeave}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -205,6 +211,8 @@ export function CalendarDayView({
|
|||||||
calendar={calendarMap.get(calId)}
|
calendar={calendarMap.get(calId)}
|
||||||
variant="block"
|
variant="block"
|
||||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||||
|
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||||
|
onMouseLeave={onHoverLeave}
|
||||||
draggable
|
draggable
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState, useCallback, type DragEvent } from "react";
|
import { useMemo, useState, useCallback, useRef, useEffect, type DragEvent } from "react";
|
||||||
import { useTranslations, useFormatter } from "next-intl";
|
import { useTranslations, useFormatter } from "next-intl";
|
||||||
import {
|
import {
|
||||||
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
|
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
|
||||||
@@ -20,6 +20,8 @@ interface CalendarMonthViewProps {
|
|||||||
calendars: Calendar[];
|
calendars: Calendar[];
|
||||||
onSelectDate: (date: Date) => void;
|
onSelectDate: (date: Date) => void;
|
||||||
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||||
|
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||||
|
onHoverLeave?: () => void;
|
||||||
firstDayOfWeek?: number;
|
firstDayOfWeek?: number;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
}
|
}
|
||||||
@@ -30,6 +32,8 @@ export function CalendarMonthView({
|
|||||||
calendars,
|
calendars,
|
||||||
onSelectDate,
|
onSelectDate,
|
||||||
onSelectEvent,
|
onSelectEvent,
|
||||||
|
onHoverEvent,
|
||||||
|
onHoverLeave,
|
||||||
firstDayOfWeek = 1,
|
firstDayOfWeek = 1,
|
||||||
isMobile,
|
isMobile,
|
||||||
}: CalendarMonthViewProps) {
|
}: CalendarMonthViewProps) {
|
||||||
@@ -88,6 +92,32 @@ export function CalendarMonthView({
|
|||||||
}, [days]);
|
}, [days]);
|
||||||
|
|
||||||
const [dropDayKey, setDropDayKey] = useState<string | null>(null);
|
const [dropDayKey, setDropDayKey] = useState<string | null>(null);
|
||||||
|
const [overflowDay, setOverflowDay] = useState<{ key: string; events: CalendarEvent[]; anchorRect: DOMRect; dayLabel: string } | null>(null);
|
||||||
|
const overflowRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!overflowDay) return;
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (overflowRef.current && !overflowRef.current.contains(e.target as Node)) {
|
||||||
|
setOverflowDay(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") setOverflowDay(null);
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
};
|
||||||
|
}, [overflowDay]);
|
||||||
|
|
||||||
|
const handleMoreClick = useCallback((e: React.MouseEvent, dayKey: string, dayEvents: CalendarEvent[], dayLabel: string) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
|
setOverflowDay({ key: dayKey, events: dayEvents, anchorRect: rect, dayLabel });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCellDragOver = useCallback((e: DragEvent<HTMLDivElement>, dayKey: string) => {
|
const handleCellDragOver = useCallback((e: DragEvent<HTMLDivElement>, dayKey: string) => {
|
||||||
if (!e.dataTransfer.types.includes("application/x-calendar-event")) return;
|
if (!e.dataTransfer.types.includes("application/x-calendar-event")) return;
|
||||||
@@ -214,14 +244,20 @@ export function CalendarMonthView({
|
|||||||
calendar={calendarMap.get(calId)}
|
calendar={calendarMap.get(calId)}
|
||||||
variant="chip"
|
variant="chip"
|
||||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||||
|
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||||
|
onMouseLeave={onHoverLeave}
|
||||||
draggable
|
draggable
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{dayEvents.length > maxVisible && (
|
{dayEvents.length > maxVisible && (
|
||||||
<div className="text-[10px] text-muted-foreground px-1">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-[10px] text-muted-foreground px-1 hover:text-foreground hover:underline cursor-pointer text-left w-full"
|
||||||
|
onClick={(e) => handleMoreClick(e, key, dayEvents, fullDateLabel)}
|
||||||
|
>
|
||||||
{t("events.more", { count: dayEvents.length - maxVisible })}
|
{t("events.more", { count: dayEvents.length - maxVisible })}
|
||||||
</div>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -231,6 +267,47 @@ export function CalendarMonthView({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{overflowDay && (() => {
|
||||||
|
const viewportW = typeof window !== "undefined" ? window.innerWidth : 1024;
|
||||||
|
const viewportH = typeof window !== "undefined" ? window.innerHeight : 768;
|
||||||
|
const popoverW = 260;
|
||||||
|
const popoverMaxH = 320;
|
||||||
|
let left = overflowDay.anchorRect.left;
|
||||||
|
let top = overflowDay.anchorRect.bottom + 4;
|
||||||
|
|
||||||
|
if (left + popoverW > viewportW - 8) left = viewportW - popoverW - 8;
|
||||||
|
if (left < 8) left = 8;
|
||||||
|
if (top + popoverMaxH > viewportH - 8) top = overflowDay.anchorRect.top - popoverMaxH - 4;
|
||||||
|
if (top < 8) top = 8;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={overflowRef}
|
||||||
|
className="fixed z-50 bg-background border border-border rounded-lg shadow-lg p-3 space-y-1 overflow-y-auto"
|
||||||
|
style={{ left, top, width: popoverW, maxHeight: popoverMaxH }}
|
||||||
|
role="dialog"
|
||||||
|
aria-label={overflowDay.dayLabel}
|
||||||
|
>
|
||||||
|
<div className="text-xs font-semibold text-foreground mb-2">{overflowDay.dayLabel}</div>
|
||||||
|
{overflowDay.events.map((ev) => {
|
||||||
|
const calId = Object.keys(ev.calendarIds)[0];
|
||||||
|
return (
|
||||||
|
<EventCard
|
||||||
|
key={ev.id}
|
||||||
|
event={ev}
|
||||||
|
calendar={calendarMap.get(calId)}
|
||||||
|
variant="chip"
|
||||||
|
onClick={(rect) => { setOverflowDay(null); onSelectEvent(ev, rect); }}
|
||||||
|
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||||
|
onMouseLeave={onHoverLeave}
|
||||||
|
draggable
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ interface CalendarWeekViewProps {
|
|||||||
calendars: Calendar[];
|
calendars: Calendar[];
|
||||||
onSelectDate: (date: Date) => void;
|
onSelectDate: (date: Date) => void;
|
||||||
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||||
|
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||||
|
onHoverLeave?: () => void;
|
||||||
onCreateAtTime: (date: Date, endDate?: Date) => void;
|
onCreateAtTime: (date: Date, endDate?: Date) => void;
|
||||||
firstDayOfWeek?: number;
|
firstDayOfWeek?: number;
|
||||||
timeFormat?: "12h" | "24h";
|
timeFormat?: "12h" | "24h";
|
||||||
@@ -33,6 +35,8 @@ export function CalendarWeekView({
|
|||||||
calendars,
|
calendars,
|
||||||
onSelectDate,
|
onSelectDate,
|
||||||
onSelectEvent,
|
onSelectEvent,
|
||||||
|
onHoverEvent,
|
||||||
|
onHoverLeave,
|
||||||
onCreateAtTime,
|
onCreateAtTime,
|
||||||
firstDayOfWeek = 1,
|
firstDayOfWeek = 1,
|
||||||
timeFormat = "24h",
|
timeFormat = "24h",
|
||||||
@@ -162,6 +166,8 @@ export function CalendarWeekView({
|
|||||||
calendar={calendarMap.get(calId)}
|
calendar={calendarMap.get(calId)}
|
||||||
variant="chip"
|
variant="chip"
|
||||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||||
|
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||||
|
onMouseLeave={onHoverLeave}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -280,6 +286,8 @@ export function CalendarWeekView({
|
|||||||
calendar={calendarMap.get(calId)}
|
calendar={calendarMap.get(calId)}
|
||||||
variant="block"
|
variant="block"
|
||||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||||
|
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||||
|
onMouseLeave={onHoverLeave}
|
||||||
draggable
|
draggable
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface EventCardProps {
|
|||||||
calendar?: Calendar;
|
calendar?: Calendar;
|
||||||
variant: "chip" | "block";
|
variant: "chip" | "block";
|
||||||
onClick?: (anchorRect: DOMRect) => void;
|
onClick?: (anchorRect: DOMRect) => void;
|
||||||
|
onMouseEnter?: (anchorRect: DOMRect) => void;
|
||||||
|
onMouseLeave?: () => void;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
draggable?: boolean;
|
draggable?: boolean;
|
||||||
}
|
}
|
||||||
@@ -56,7 +58,7 @@ function createEventDragPreview(title: string, timeRange: string, color: string)
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EventCard({ event, calendar, variant, onClick, isSelected, draggable: isDraggable }: EventCardProps) {
|
export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onMouseLeave, isSelected, draggable: isDraggable }: EventCardProps) {
|
||||||
const t = useTranslations("calendar");
|
const t = useTranslations("calendar");
|
||||||
const [isBeingDragged, setIsBeingDragged] = useState(false);
|
const [isBeingDragged, setIsBeingDragged] = useState(false);
|
||||||
const color = getEventColor(event, calendar);
|
const color = getEventColor(event, calendar);
|
||||||
@@ -101,6 +103,8 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
|
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
|
||||||
|
onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())}
|
||||||
|
onMouseLeave={() => onMouseLeave?.()}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
{...dragProps}
|
{...dragProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -124,6 +128,8 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
|
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
|
||||||
|
onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())}
|
||||||
|
onMouseLeave={() => onMouseLeave?.()}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
{...dragProps}
|
{...dragProps}
|
||||||
data-calendar-event
|
data-calendar-event
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ interface EventDetailPopoverProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSaveNote: (note: string) => void;
|
onSaveNote: (note: string) => void;
|
||||||
onRsvp?: (status: CalendarParticipant["participationStatus"]) => void;
|
onRsvp?: (status: CalendarParticipant["participationStatus"]) => void;
|
||||||
|
onMouseEnter?: () => void;
|
||||||
|
onMouseLeave?: () => void;
|
||||||
currentUserEmails?: string[];
|
currentUserEmails?: string[];
|
||||||
timeFormat?: "12h" | "24h";
|
timeFormat?: "12h" | "24h";
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
@@ -119,6 +121,8 @@ export function EventDetailPopover({
|
|||||||
onClose,
|
onClose,
|
||||||
onSaveNote,
|
onSaveNote,
|
||||||
onRsvp,
|
onRsvp,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
currentUserEmails = [],
|
currentUserEmails = [],
|
||||||
timeFormat = "24h",
|
timeFormat = "24h",
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -203,17 +207,14 @@ export function EventDetailPopover({
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleScroll = () => onClose();
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
document.addEventListener("scroll", handleScroll, true);
|
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
document.removeEventListener("scroll", handleScroll, true);
|
|
||||||
};
|
};
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
@@ -252,6 +253,8 @@ export function EventDetailPopover({
|
|||||||
ref={popoverRef}
|
ref={popoverRef}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-label={event.title || t("events.no_title")}
|
aria-label={event.title || t("events.no_title")}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed z-[60] bg-background border border-border shadow-xl overflow-hidden transition-[opacity,transform] duration-150 ease-out",
|
"fixed z-[60] bg-background border border-border shadow-xl overflow-hidden transition-[opacity,transform] duration-150 ease-out",
|
||||||
isMobile
|
isMobile
|
||||||
|
|||||||
@@ -146,10 +146,19 @@ export function EmailList({
|
|||||||
const handleBatchDelete = async () => {
|
const handleBatchDelete = async () => {
|
||||||
if (!client || isProcessing) return;
|
if (!client || isProcessing) return;
|
||||||
|
|
||||||
|
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
|
||||||
|
const isInTrash = currentMailbox?.role === 'trash';
|
||||||
|
|
||||||
const confirmed = await confirmDialog({
|
const confirmed = await confirmDialog({
|
||||||
title: t('batch_actions.delete_confirm_title'),
|
title: isInTrash
|
||||||
message: t('batch_actions.delete_confirm_message', { count: selectedEmailIds.size }),
|
? t('permanent_delete_confirm_title')
|
||||||
confirmText: t('batch_actions.delete'),
|
: t('batch_actions.delete_confirm_title'),
|
||||||
|
message: isInTrash
|
||||||
|
? t('permanent_delete_confirm_batch_message', { count: selectedEmailIds.size })
|
||||||
|
: t('batch_actions.delete_confirm_message', { count: selectedEmailIds.size }),
|
||||||
|
confirmText: isInTrash
|
||||||
|
? t('permanent_delete')
|
||||||
|
: t('batch_actions.delete'),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|||||||
@@ -120,8 +120,14 @@
|
|||||||
"mark_read": "Als gelesen markieren",
|
"mark_read": "Als gelesen markieren",
|
||||||
"mark_unread": "Als ungelesen markieren",
|
"mark_unread": "Als ungelesen markieren",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
|
"delete_confirm_title": "E-Mails löschen",
|
||||||
|
"delete_confirm_message": "Möchten Sie {count, plural, one {1 E-Mail} other {# E-Mails}} wirklich löschen?",
|
||||||
"clear_selection": "Auswahl aufheben"
|
"clear_selection": "Auswahl aufheben"
|
||||||
}
|
},
|
||||||
|
"permanent_delete": "Endgültig löschen",
|
||||||
|
"permanent_delete_confirm_title": "Endgültig löschen",
|
||||||
|
"permanent_delete_confirm_message": "Diese E-Mail wird endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"permanent_delete_confirm_batch_message": "Diese {count, plural, one {1 E-Mail} other {# E-Mails}} werden endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
"no_email_selected": "Keine E-Mail ausgewählt",
|
"no_email_selected": "Keine E-Mail ausgewählt",
|
||||||
|
|||||||
@@ -120,8 +120,14 @@
|
|||||||
"mark_read": "Mark as read",
|
"mark_read": "Mark as read",
|
||||||
"mark_unread": "Mark as unread",
|
"mark_unread": "Mark as unread",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"delete_confirm_title": "Delete emails",
|
||||||
|
"delete_confirm_message": "Are you sure you want to delete {count, plural, one {1 email} other {# emails}}?",
|
||||||
"clear_selection": "Clear selection"
|
"clear_selection": "Clear selection"
|
||||||
}
|
},
|
||||||
|
"permanent_delete": "Delete permanently",
|
||||||
|
"permanent_delete_confirm_title": "Permanently delete",
|
||||||
|
"permanent_delete_confirm_message": "This email will be permanently deleted. This action cannot be undone.",
|
||||||
|
"permanent_delete_confirm_batch_message": "These {count, plural, one {1 email} other {# emails}} will be permanently deleted. This action cannot be undone."
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
"no_email_selected": "No email selected",
|
"no_email_selected": "No email selected",
|
||||||
|
|||||||
@@ -120,8 +120,14 @@
|
|||||||
"mark_read": "Marcar como leído",
|
"mark_read": "Marcar como leído",
|
||||||
"mark_unread": "Marcar como no leído",
|
"mark_unread": "Marcar como no leído",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
|
"delete_confirm_title": "Eliminar correos",
|
||||||
|
"delete_confirm_message": "¿Estás seguro de que quieres eliminar {count, plural, one {1 correo} other {# correos}}?",
|
||||||
"clear_selection": "Limpiar selección"
|
"clear_selection": "Limpiar selección"
|
||||||
}
|
},
|
||||||
|
"permanent_delete": "Eliminar permanentemente",
|
||||||
|
"permanent_delete_confirm_title": "Eliminar permanentemente",
|
||||||
|
"permanent_delete_confirm_message": "Este correo se eliminará permanentemente. Esta acción no se puede deshacer.",
|
||||||
|
"permanent_delete_confirm_batch_message": "Estos {count, plural, one {1 correo} other {# correos}} se eliminarán permanentemente. Esta acción no se puede deshacer."
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
"no_email_selected": "Ningún correo seleccionado",
|
"no_email_selected": "Ningún correo seleccionado",
|
||||||
|
|||||||
@@ -120,8 +120,14 @@
|
|||||||
"mark_read": "Marquer comme lu",
|
"mark_read": "Marquer comme lu",
|
||||||
"mark_unread": "Marquer comme non lu",
|
"mark_unread": "Marquer comme non lu",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
|
"delete_confirm_title": "Supprimer les e-mails",
|
||||||
|
"delete_confirm_message": "Voulez-vous vraiment supprimer {count, plural, one {1 e-mail} other {# e-mails}} ?",
|
||||||
"clear_selection": "Effacer la sélection"
|
"clear_selection": "Effacer la sélection"
|
||||||
}
|
},
|
||||||
|
"permanent_delete": "Supprimer définitivement",
|
||||||
|
"permanent_delete_confirm_title": "Supprimer définitivement",
|
||||||
|
"permanent_delete_confirm_message": "Cet e-mail sera définitivement supprimé. Cette action est irréversible.",
|
||||||
|
"permanent_delete_confirm_batch_message": "Ces {count, plural, one {1 e-mail} other {# e-mails}} seront définitivement supprimés. Cette action est irréversible."
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
"no_email_selected": "Aucun email sélectionné",
|
"no_email_selected": "Aucun email sélectionné",
|
||||||
|
|||||||
@@ -120,8 +120,14 @@
|
|||||||
"mark_read": "Segna come letto",
|
"mark_read": "Segna come letto",
|
||||||
"mark_unread": "Segna come non letto",
|
"mark_unread": "Segna come non letto",
|
||||||
"delete": "Elimina",
|
"delete": "Elimina",
|
||||||
|
"delete_confirm_title": "Elimina e-mail",
|
||||||
|
"delete_confirm_message": "Sei sicuro di voler eliminare {count, plural, one {1 e-mail} other {# e-mail}}?",
|
||||||
"clear_selection": "Cancella selezione"
|
"clear_selection": "Cancella selezione"
|
||||||
}
|
},
|
||||||
|
"permanent_delete": "Elimina definitivamente",
|
||||||
|
"permanent_delete_confirm_title": "Elimina definitivamente",
|
||||||
|
"permanent_delete_confirm_message": "Questa e-mail verrà eliminata definitivamente. Questa azione non può essere annullata.",
|
||||||
|
"permanent_delete_confirm_batch_message": "Queste {count, plural, one {1 e-mail} other {# e-mail}} verranno eliminate definitivamente. Questa azione non può essere annullata."
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
"no_email_selected": "Nessun messaggio selezionato",
|
"no_email_selected": "Nessun messaggio selezionato",
|
||||||
|
|||||||
@@ -120,8 +120,14 @@
|
|||||||
"mark_read": "既読にする",
|
"mark_read": "既読にする",
|
||||||
"mark_unread": "未読にする",
|
"mark_unread": "未読にする",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
|
"delete_confirm_title": "メールを削除",
|
||||||
|
"delete_confirm_message": "{count, plural, one {1件} other {#件}}のメールを削除しますか?",
|
||||||
"clear_selection": "選択を解除"
|
"clear_selection": "選択を解除"
|
||||||
}
|
},
|
||||||
|
"permanent_delete": "完全に削除",
|
||||||
|
"permanent_delete_confirm_title": "完全に削除",
|
||||||
|
"permanent_delete_confirm_message": "このメールは完全に削除されます。この操作は取り消せません。",
|
||||||
|
"permanent_delete_confirm_batch_message": "{count, plural, one {1件} other {#件}}のメールが完全に削除されます。この操作は取り消せません。"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
"no_email_selected": "メールが選択されていません",
|
"no_email_selected": "メールが選択されていません",
|
||||||
|
|||||||
@@ -120,8 +120,14 @@
|
|||||||
"mark_read": "Markeren als gelezen",
|
"mark_read": "Markeren als gelezen",
|
||||||
"mark_unread": "Markeren als ongelezen",
|
"mark_unread": "Markeren als ongelezen",
|
||||||
"delete": "Verwijderen",
|
"delete": "Verwijderen",
|
||||||
|
"delete_confirm_title": "E-mails verwijderen",
|
||||||
|
"delete_confirm_message": "Weet u zeker dat u {count, plural, one {1 e-mail} other {# e-mails}} wilt verwijderen?",
|
||||||
"clear_selection": "Selectie wissen"
|
"clear_selection": "Selectie wissen"
|
||||||
}
|
},
|
||||||
|
"permanent_delete": "Definitief verwijderen",
|
||||||
|
"permanent_delete_confirm_title": "Definitief verwijderen",
|
||||||
|
"permanent_delete_confirm_message": "Deze e-mail wordt definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt.",
|
||||||
|
"permanent_delete_confirm_batch_message": "Deze {count, plural, one {1 e-mail} other {# e-mails}} worden definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt."
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
"no_email_selected": "Geen e-mail geselecteerd",
|
"no_email_selected": "Geen e-mail geselecteerd",
|
||||||
|
|||||||
@@ -120,8 +120,14 @@
|
|||||||
"mark_read": "Marcar como lido",
|
"mark_read": "Marcar como lido",
|
||||||
"mark_unread": "Marcar como não lido",
|
"mark_unread": "Marcar como não lido",
|
||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
|
"delete_confirm_title": "Excluir e-mails",
|
||||||
|
"delete_confirm_message": "Tem certeza que deseja excluir {count, plural, one {1 e-mail} other {# e-mails}}?",
|
||||||
"clear_selection": "Limpar seleção"
|
"clear_selection": "Limpar seleção"
|
||||||
}
|
},
|
||||||
|
"permanent_delete": "Excluir permanentemente",
|
||||||
|
"permanent_delete_confirm_title": "Excluir permanentemente",
|
||||||
|
"permanent_delete_confirm_message": "Este e-mail será permanentemente excluído. Esta ação não pode ser desfeita.",
|
||||||
|
"permanent_delete_confirm_batch_message": "Estes {count, plural, one {1 e-mail} other {# e-mails}} serão permanentemente excluídos. Esta ação não pode ser desfeita."
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
"no_email_selected": "Nenhum e-mail selecionado",
|
"no_email_selected": "Nenhum e-mail selecionado",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ interface EmailStore {
|
|||||||
fetchEmailContent: (client: JMAPClient, emailId: string) => Promise<Email | null>;
|
fetchEmailContent: (client: JMAPClient, emailId: string) => Promise<Email | null>;
|
||||||
fetchQuota: (client: JMAPClient) => Promise<void>;
|
fetchQuota: (client: JMAPClient) => Promise<void>;
|
||||||
sendEmail: (client: JMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string) => Promise<void>;
|
sendEmail: (client: JMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string) => Promise<void>;
|
||||||
deleteEmail: (client: JMAPClient, emailId: string) => Promise<void>;
|
deleteEmail: (client: JMAPClient, emailId: string, forceDelete?: boolean) => Promise<void>;
|
||||||
markAsRead: (client: JMAPClient, emailId: string, read: boolean) => Promise<void>;
|
markAsRead: (client: JMAPClient, emailId: string, read: boolean) => Promise<void>;
|
||||||
moveToMailbox: (client: JMAPClient, emailId: string, mailboxId: string) => Promise<void>;
|
moveToMailbox: (client: JMAPClient, emailId: string, mailboxId: string) => Promise<void>;
|
||||||
searchEmails: (client: JMAPClient, query: string) => Promise<void>;
|
searchEmails: (client: JMAPClient, query: string) => Promise<void>;
|
||||||
@@ -384,7 +384,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteEmail: async (client, emailId) => {
|
deleteEmail: async (client, emailId, forceDelete) => {
|
||||||
try {
|
try {
|
||||||
// Get the email to check if it's unread and which mailboxes it belongs to
|
// Get the email to check if it's unread and which mailboxes it belongs to
|
||||||
const email = get().emails.find(e => e.id === emailId);
|
const email = get().emails.find(e => e.id === emailId);
|
||||||
@@ -401,8 +401,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
const currentMailbox = mailboxes.find(mb => mb.id === selectedMailboxId);
|
const currentMailbox = mailboxes.find(mb => mb.id === selectedMailboxId);
|
||||||
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
|
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
|
||||||
|
|
||||||
// If deleteAction is 'trash', try to move to trash mailbox
|
// If deleteAction is 'trash' and not forced permanent delete, try to move to trash mailbox
|
||||||
if (deleteAction === 'trash') {
|
if (deleteAction === 'trash' && !forceDelete) {
|
||||||
// Find trash mailbox for the correct account
|
// Find trash mailbox for the correct account
|
||||||
const trashMailbox = mailboxes.find(mb => {
|
const trashMailbox = mailboxes.find(mb => {
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user