mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
fix: close an untouched all-day event without asking to discard changes
This commit is contained in:
@@ -82,7 +82,7 @@ import { SafeAreaModal } from '../SafeAreaModal';
|
||||
import { useKeyboardObstruction } from '../../lib/use-keyboard-obstruction';
|
||||
import { editorKeyboardInset } from '../../lib/calendar-editor-keyboard';
|
||||
import ToggleSwitch from '../ToggleSwitch';
|
||||
import { askBeforeDiscard, editorFormChanged, editorFormSnapshot } from '../../lib/calendar-editor-discard';
|
||||
import { askBeforeDiscard, editorFormChanged, editorFormSnapshot, editorTimesSnapshot } from '../../lib/calendar-editor-discard';
|
||||
import { saveOfferingWithoutInvitations } from '../../lib/calendar-scheduling-denied';
|
||||
|
||||
type RecurrenceOption = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom';
|
||||
@@ -269,7 +269,7 @@ export function EventModal({
|
||||
// What the form holds against what it opened with: Close and Back ask
|
||||
// before they drop changes (webmail 13f9f27c4).
|
||||
const formSnapshot = React.useMemo(() => editorFormSnapshot({
|
||||
title, description, allDay, start, end, calendarId, attendees, recurrence, customRule,
|
||||
title, description, times: editorTimesSnapshot({ allDay, start, end }), calendarId, attendees, recurrence, customRule,
|
||||
reminders, useDefaults, preserved, sendInvitations, location, videoUrl,
|
||||
}), [title, description, allDay, start, end, calendarId, attendees, recurrence, customRule,
|
||||
reminders, useDefaults, preserved, sendInvitations, location, videoUrl]);
|
||||
|
||||
@@ -3,8 +3,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
const alert = vi.fn();
|
||||
vi.mock('react-native', () => ({ Alert: { alert: (...args: unknown[]) => alert(...args) } }));
|
||||
|
||||
import { askBeforeDiscard, editorFormChanged, editorFormSnapshot } from '../calendar-editor-discard';
|
||||
import { askBeforeDiscard, editorFormChanged, editorFormSnapshot, editorTimesSnapshot } from '../calendar-editor-discard';
|
||||
import { emptyTaskEditor, taskEditorChanged, taskEditorFromTask } from '../task-editor';
|
||||
import { editorTimesFromEvent } from '../event-editor-times';
|
||||
import type { CalendarEvent } from '../../api/types';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const t = (_key: string, fallback?: string) => fallback ?? '';
|
||||
|
||||
@@ -13,6 +17,48 @@ const buttons = (): Button[] => alert.mock.calls.at(-1)![2] as Button[];
|
||||
|
||||
beforeEach(() => alert.mockReset());
|
||||
|
||||
describe('the event editor times in the dirty check', () => {
|
||||
// What the editor does with an all-day event it opens: seed the times,
|
||||
// then its all-day effect sets start and end to midnight.
|
||||
const opened = (event: Partial<CalendarEvent>) => {
|
||||
const seeded = editorTimesFromEvent(event as CalendarEvent);
|
||||
const s = new Date(seeded.start); s.setHours(0, 0, 0, 0);
|
||||
const e = new Date(seeded.end); e.setHours(0, 0, 0, 0);
|
||||
if (e < s) e.setTime(s.getTime());
|
||||
return { seeded, normalized: { allDay: seeded.allDay, start: s, end: e } };
|
||||
};
|
||||
const snap = (times: { allDay: boolean; start: Date; end: Date }) =>
|
||||
editorFormSnapshot({ title: 'Company holiday', times: editorTimesSnapshot(times) });
|
||||
|
||||
it('finds no change in an all-day event opened and left alone', () => {
|
||||
// "Company holiday" as Stalwart 0.16 returns it: a day, P1D.
|
||||
const { seeded, normalized } = opened({
|
||||
'@type': 'Event', showWithoutTime: true, start: '2026-10-03T00:00:00', duration: 'P1D',
|
||||
} as Partial<CalendarEvent>);
|
||||
expect(seeded.end.getHours()).toBe(23); // the inclusive last day, 23:59:59.999
|
||||
expect(editorFormChanged(snap(seeded), snap(normalized))).toBe(false);
|
||||
// Two days, and one with a time zone attached.
|
||||
const two = opened({ showWithoutTime: true, start: '2026-10-03T00:00:00', duration: 'P2D' });
|
||||
expect(editorFormChanged(snap(two.seeded), snap(two.normalized))).toBe(false);
|
||||
const zoned = opened({ showWithoutTime: true, start: '2026-10-03T00:00:00', duration: 'P1D', timeZone: 'Europe/Berlin' });
|
||||
expect(editorFormChanged(snap(zoned.seeded), snap(zoned.normalized))).toBe(false);
|
||||
});
|
||||
|
||||
it('still sees another day, and a moved timed event', () => {
|
||||
const { normalized } = opened({ showWithoutTime: true, start: '2026-10-03T00:00:00', duration: 'P1D' });
|
||||
const nextDay = { ...normalized, end: new Date(normalized.end.getTime() + 86_400_000) };
|
||||
expect(editorFormChanged(snap(normalized), snap(nextDay))).toBe(true);
|
||||
const timed = { allDay: false, start: new Date(2026, 9, 3, 9), end: new Date(2026, 9, 3, 10) };
|
||||
expect(editorFormChanged(snap(timed), snap({ ...timed, end: new Date(2026, 9, 3, 10, 30) }))).toBe(true);
|
||||
expect(editorFormChanged(snap(timed), snap({ ...timed, allDay: true }))).toBe(true);
|
||||
});
|
||||
|
||||
it('is what the event editor compares', () => {
|
||||
const src = fs.readFileSync(path.resolve(__dirname, '../../components/calendar/EventModal.tsx'), 'utf8');
|
||||
expect(src).toMatch(/times: editorTimesSnapshot\(\{ allDay, start, end \}\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('askBeforeDiscard', () => {
|
||||
it('closes at once when nothing was changed', () => {
|
||||
const close = vi.fn();
|
||||
|
||||
@@ -11,6 +11,23 @@ export function editorFormSnapshot(fields: Record<string, unknown>): string {
|
||||
return JSON.stringify(fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* The editor's times as the dirty check compares them. An all-day event
|
||||
* compares by its first and last day: the editor seeds its last day as
|
||||
* 23:59:59.999 (the exclusive end minus 1 ms) and then sets both to
|
||||
* midnight, so comparing instants asked "Discard changes?" for an all-day
|
||||
* event opened and closed untouched. A timed one compares by the instant.
|
||||
*/
|
||||
export function editorTimesSnapshot(times: { allDay: boolean; start: Date; end: Date }): Record<string, unknown> {
|
||||
if (!times.allDay) return { allDay: false, start: times.start, end: times.end };
|
||||
return { allDay: true, start: dayKey(times.start), end: dayKey(times.end) };
|
||||
}
|
||||
|
||||
function dayKey(date: Date): string {
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
/** Whether the form holds changes against `baseline`, the form it opened with (null: not seeded yet). */
|
||||
export function editorFormChanged(baseline: string | null, current: string): boolean {
|
||||
return baseline !== null && baseline !== current;
|
||||
|
||||
Reference in New Issue
Block a user