mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
fix: let a Pro tab move or split when dropped on a composer or calendar pane
This commit is contained in:
@@ -328,10 +328,11 @@ export function CalendarMonthView({
|
||||
}, []);
|
||||
|
||||
const handleCellDrop = useCallback(async (e: DragEvent<HTMLDivElement>, day: Date) => {
|
||||
e.preventDefault();
|
||||
setDropDayKey(null);
|
||||
// Anything else (a Pro tab or message moving to this pane) is not ours.
|
||||
const json = e.dataTransfer.getData("application/x-calendar-event");
|
||||
if (!json) return;
|
||||
e.preventDefault();
|
||||
setDropDayKey(null);
|
||||
try {
|
||||
const data = JSON.parse(json);
|
||||
const originalStart = parseISO(data.originalStart);
|
||||
|
||||
@@ -79,6 +79,7 @@ import { useProMultiAccountIdentities, stripCrossAccountIdentityPrefix } from "@
|
||||
import { useAccountStore } from "@/stores/account-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { ComposerPanelToggle, ComposerPluginSidebar, useComposerSidebars } from "./composer-plugin-sidebars";
|
||||
import { dragKindFromTypes } from "@/components/pro/pro-shell-drop";
|
||||
import { ComposerSelectionMenu, useComposerSelectionMenu } from "./composer-selection-menu";
|
||||
import { PluginSlot } from "@/components/plugins/plugin-slot";
|
||||
import { usePluginSlotOffers } from "@/hooks/use-plugin-slot-offers";
|
||||
@@ -168,6 +169,10 @@ import { usePolicyStore } from "@/stores/policy-store";
|
||||
import { registerComposeFlush } from "@/lib/compose-flush-registry";
|
||||
import { toUnicodeEmail } from "@/lib/idn";
|
||||
|
||||
// A Pro tab, message or folder dragged across a composer tab is on its way to
|
||||
// the pane or the split edge: those drags belong to the shell.
|
||||
const isShellDrag = (e: React.DragEvent) => dragKindFromTypes(e.dataTransfer.types) !== null;
|
||||
|
||||
/**
|
||||
* Derives the text/plain alternative from the composer's HTML body, preserving
|
||||
* line structure from block elements and <br> tags. Paragraph spacing is on so
|
||||
@@ -2484,6 +2489,7 @@ export function EmailComposer({
|
||||
}, [clearDragState]);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||
if (isShellDrag(e)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
@@ -2493,18 +2499,21 @@ export function EmailComposer({
|
||||
}, [resetDragTimeout]);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
if (isShellDrag(e)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resetDragTimeout();
|
||||
}, [resetDragTimeout]);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
if (isShellDrag(e)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resetDragTimeout();
|
||||
}, [resetDragTimeout]);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
if (isShellDrag(e)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clearDragState();
|
||||
@@ -4670,6 +4679,7 @@ export function EmailComposer({
|
||||
}}
|
||||
onDragLeave={() => setIsDraggingChipOverCc(false)}
|
||||
onDrop={(e) => {
|
||||
if (isShellDrag(e)) return;
|
||||
e.preventDefault();
|
||||
setIsDraggingChipOverCc(false);
|
||||
const raw = e.dataTransfer.getData('application/x-recipient-chip');
|
||||
@@ -4694,6 +4704,7 @@ export function EmailComposer({
|
||||
}}
|
||||
onDragLeave={() => setIsDraggingChipOverBcc(false)}
|
||||
onDrop={(e) => {
|
||||
if (isShellDrag(e)) return;
|
||||
e.preventDefault();
|
||||
setIsDraggingChipOverBcc(false);
|
||||
const raw = e.dataTransfer.getData('application/x-recipient-chip');
|
||||
@@ -6023,6 +6034,7 @@ export function RecipientChipInput({
|
||||
// Insert the dragged chip at `target`. Same-field is a local reorder;
|
||||
// cross-field routes through onMoveChip with the destination index (#593).
|
||||
const performDrop = (e: React.DragEvent, target: number) => {
|
||||
if (isShellDrag(e)) return;
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
setDropIndex(null);
|
||||
|
||||
@@ -215,6 +215,21 @@ describe.each<["0.16" | "1.0"]>([["0.16"], ["1.0"]])("useTimeGridInteractions mo
|
||||
// The grid places events by wall clock (7845b2ca). On 2026-10-25 Berlin's
|
||||
// 02:00-03:00 happens twice, so a resize across it has to land where the
|
||||
// edge was dropped, not an hour off (#1348).
|
||||
describe("useTimeGridInteractions drops that are not events", () => {
|
||||
it("leaves a Pro tab dropped on a column to the shell", async () => {
|
||||
const { result } = renderHook(() => useTimeGridInteractions({
|
||||
hourHeight: 60,
|
||||
onCreateRange: () => {},
|
||||
errorMessages,
|
||||
} as never));
|
||||
const preventDefault = vi.fn();
|
||||
const e = { preventDefault, dataTransfer: { getData: () => "" } } as unknown as DragEvent<HTMLDivElement>;
|
||||
await act(() => result.current.handleColumnDrop(e, new Date(2026, 8, 3)));
|
||||
// The shell's own drop handler stands down on a prevented event.
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTimeGridInteractions resize on a DST change day (#1348)", () => {
|
||||
const original = useCalendarStore.getState().updateEvent;
|
||||
const originalTimeZone = useSettingsStore.getState().timeZone;
|
||||
|
||||
@@ -398,10 +398,11 @@ export function useTimeGridInteractions({
|
||||
}, []);
|
||||
|
||||
const handleColumnDrop = useCallback(async (e: DragEvent<HTMLDivElement>, day: Date) => {
|
||||
e.preventDefault();
|
||||
setDropTarget(null);
|
||||
// Anything else (a Pro tab or message moving to this pane) is not ours.
|
||||
const json = e.dataTransfer.getData("application/x-calendar-event");
|
||||
if (!json) return;
|
||||
e.preventDefault();
|
||||
setDropTarget(null);
|
||||
try {
|
||||
const data = JSON.parse(json);
|
||||
const minutes = snapDragMinutes(e);
|
||||
|
||||
Reference in New Issue
Block a user