fix(03): CR-03 convert exclusive all-day end to inclusive on edit pre-fill
This commit is contained in:
@@ -74,6 +74,27 @@ function writeLastCalendarUrl(url: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CR-03: convert an EXCLUSIVE all-day end date ('YYYY-MM-DD') to the INCLUSIVE
|
||||||
|
* last day the form displays. The occurrence/expand contract keeps all-day ends
|
||||||
|
* exclusive (matching the DTEND Fastmail stores, RFC-5545 §3.6.1), and
|
||||||
|
* buildVeventString re-advances the inclusive form value by one day on write.
|
||||||
|
* Without this subtraction, round-tripping an edit re-advances an already-exclusive
|
||||||
|
* end, silently growing multi-day all-day events by one day per save. Parses by
|
||||||
|
* UTC components so the roll-back is DST-safe (mirrors vevent.ts's roll-forward).
|
||||||
|
*/
|
||||||
|
function exclusiveEndToInclusiveDate(dateStr: string): string {
|
||||||
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr)
|
||||||
|
if (!m) return dateStr
|
||||||
|
const [, y, mo, d] = m
|
||||||
|
const date = new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d)))
|
||||||
|
date.setUTCDate(date.getUTCDate() - 1)
|
||||||
|
const yy = String(date.getUTCFullYear())
|
||||||
|
const mm = String(date.getUTCMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(date.getUTCDate()).padStart(2, '0')
|
||||||
|
return `${yy}-${mm}-${dd}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an ISO date string (possibly with time + offset) into
|
* Parse an ISO date string (possibly with time + offset) into
|
||||||
* { date: 'YYYY-MM-DD', time: 'HH:MM' }. Falls back to today/09:00 if malformed.
|
* { date: 'YYYY-MM-DD', time: 'HH:MM' }. Falls back to today/09:00 if malformed.
|
||||||
@@ -143,12 +164,17 @@ export function EventForm() {
|
|||||||
|
|
||||||
const initStart = occurrence ? parseDateTime(occurrence.start) : { date: todayIso(), time: '09:00' }
|
const initStart = occurrence ? parseDateTime(occurrence.start) : { date: todayIso(), time: '09:00' }
|
||||||
const initEnd = occurrence ? parseDateTime(occurrence.end) : { date: todayIso(), time: '10:00' }
|
const initEnd = occurrence ? parseDateTime(occurrence.end) : { date: todayIso(), time: '10:00' }
|
||||||
|
// CR-03: occurrence.end for an all-day event is the EXCLUSIVE DTEND; the form's
|
||||||
|
// end-date input is the INCLUSIVE last day. Convert when pre-filling so a re-edit
|
||||||
|
// does not re-advance the span (buildVeventString rolls forward again on write).
|
||||||
|
const initEndDate =
|
||||||
|
occurrence?.allDay ? exclusiveEndToInclusiveDate(initEnd.date) : initEnd.date
|
||||||
|
|
||||||
const [title, setTitle] = useState(occurrence?.title ?? '')
|
const [title, setTitle] = useState(occurrence?.title ?? '')
|
||||||
const [allDay, setAllDay] = useState(occurrence?.allDay ?? false)
|
const [allDay, setAllDay] = useState(occurrence?.allDay ?? false)
|
||||||
const [startDate, setStartDate] = useState(initStart.date)
|
const [startDate, setStartDate] = useState(initStart.date)
|
||||||
const [startTime, setStartTime] = useState(initStart.time)
|
const [startTime, setStartTime] = useState(initStart.time)
|
||||||
const [endDate, setEndDate] = useState(initEnd.date)
|
const [endDate, setEndDate] = useState(initEndDate)
|
||||||
const [endTime, setEndTime] = useState(initEnd.time)
|
const [endTime, setEndTime] = useState(initEnd.time)
|
||||||
const [recurrence, setRecurrence] = useState<RecurrencePreset>('none')
|
const [recurrence, setRecurrence] = useState<RecurrencePreset>('none')
|
||||||
const [location, setLocation] = useState(occurrence?.location ?? '')
|
const [location, setLocation] = useState(occurrence?.location ?? '')
|
||||||
@@ -179,11 +205,15 @@ export function EventForm() {
|
|||||||
if (eventFormOpen) {
|
if (eventFormOpen) {
|
||||||
const startParsed = occurrence ? parseDateTime(occurrence.start) : { date: todayIso(), time: '09:00' }
|
const startParsed = occurrence ? parseDateTime(occurrence.start) : { date: todayIso(), time: '09:00' }
|
||||||
const endParsed = occurrence ? parseDateTime(occurrence.end) : { date: todayIso(), time: '10:00' }
|
const endParsed = occurrence ? parseDateTime(occurrence.end) : { date: todayIso(), time: '10:00' }
|
||||||
|
// CR-03: see exclusiveEndToInclusiveDate — pre-fill the inclusive last day for
|
||||||
|
// all-day events so re-saving an edit does not grow the span by a day each time.
|
||||||
|
const endDateValue =
|
||||||
|
occurrence?.allDay ? exclusiveEndToInclusiveDate(endParsed.date) : endParsed.date
|
||||||
setTitle(occurrence?.title ?? '')
|
setTitle(occurrence?.title ?? '')
|
||||||
setAllDay(occurrence?.allDay ?? false)
|
setAllDay(occurrence?.allDay ?? false)
|
||||||
setStartDate(startParsed.date)
|
setStartDate(startParsed.date)
|
||||||
setStartTime(startParsed.time)
|
setStartTime(startParsed.time)
|
||||||
setEndDate(endParsed.date)
|
setEndDate(endDateValue)
|
||||||
setEndTime(endParsed.time)
|
setEndTime(endParsed.time)
|
||||||
// WR-03 recurrence: derive from occurrence if present; default 'none' only when
|
// WR-03 recurrence: derive from occurrence if present; default 'none' only when
|
||||||
// genuinely absent. Note: occurrence.recurrence is not in CalendarOccurrence type
|
// genuinely absent. Note: occurrence.recurrence is not in CalendarOccurrence type
|
||||||
|
|||||||
Reference in New Issue
Block a user