Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,14 @@ package final class MeetingFormViewModel {
}

/// Scheduled meetings start at the next available picker interval.
/// When editing a meeting whose start lies in the past, its original day
/// stays selectable unless it is recurring; recurring meetings are moved
/// to their next editable occurrence so the backend receives a non-past start date.
/// Edited meetings may start up to 24 hours in the past, as allowed by the backend.
var startDateRange: PartialRangeFrom<Date> {
var earliest = currentDateProvider.now
let earliest = currentDateProvider.now
if case .scheduled = mode {
return Self.nextSelectableStartDate(after: earliest)...
}
if case let .edit(meeting) = mode, meeting.recurrence == nil {
earliest = min(earliest, meeting.start)
if case .edit = mode {
return earliest.addingTimeInterval(-TimeInterval.oneDay)...
}
return Calendar.current.startOfDay(for: earliest)...
}
Expand Down Expand Up @@ -278,12 +276,13 @@ package final class MeetingFormViewModel {
}

private static func editableTimeRange(for meeting: Meeting, now: Date) -> (start: Date, end: Date) {
guard meeting.recurrence != nil, meeting.start < now else {
let earliestStart = now.addingTimeInterval(-TimeInterval.oneDay)
guard meeting.recurrence != nil, meeting.start < earliestStart else {
return (meeting.start, meeting.end)
}
Comment thread
Copilot marked this conversation as resolved.

guard let nextOccurrence = MeetingOccurrencePaginator()
.occurrences(for: [meeting], startingAt: now, offset: 0, limit: 1)
.occurrences(for: [meeting], startingAt: earliestStart, offset: 0, limit: 1)
.first else {
return (meeting.start, meeting.end)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,19 +442,50 @@ struct MeetingFormViewModelTests {
#expect(viewModel.repeatOption == expected)
}

@Test("startDateRange allows the original start day when editing a meeting that started in the past")
func startDateRange_EditModePastMeeting() {
@Test("startDateRange starts exactly 24 hours ago when editing", arguments: [false, true])
func startDateRange_EditModePastMeeting(isRecurring: Bool) {
// Given
let meeting = makeEditableMeeting(
start: dateProviderMock.now.addingTimeInterval(-3 * 86_400),
recurrence: nil
recurrence: isRecurring ? MeetingRecurrence(frequency: .daily, interval: 1) : nil
)

// When
let viewModel = makeViewModel(mode: .edit(meeting))

// Then
#expect(viewModel.startDateRange.lowerBound == Calendar.current.startOfDay(for: meeting.start))
#expect(viewModel.startDateRange.lowerBound == dateProviderMock.now.addingTimeInterval(-86_400))
}
Comment thread
Copilot marked this conversation as resolved.

@Test("editing preserves and submits recurring start times within 24 hours", arguments: [0.0, 3600.0, 86_400.0])
func editMode_RecentRecurringMeetingPreservesDates(secondsAgo: TimeInterval) async throws {
let original = makeEditableMeeting(start: dateProviderMock.now.addingTimeInterval(-secondsAgo))
let viewModel = makeViewModel(mode: .edit(original))
updateMeetingUseCaseMock
.invokeMeetingMeetingTitleStringStartTimeDateEndTimeDateRecurrenceMeetingRecurrenceParticipantsMeetingMemberMeetingReturnValue =
meeting

#expect(viewModel.startDate == original.start)
#expect(viewModel.endDate == original.end)

await viewModel.submit()

let arguments = updateMeetingUseCaseMock
.invokeMeetingMeetingTitleStringStartTimeDateEndTimeDateRecurrenceMeetingRecurrenceParticipantsMeetingMemberMeetingReceivedArguments
#expect(arguments?.startTime == original.start)
#expect(arguments?.endTime == original.end)
}

@Test("recurring meetings just outside the 24-hour window advance to an allowed occurrence")
func editMode_RecurringMeetingOutsideWindow() {
let original = makeEditableMeeting(
start: dateProviderMock.now.addingTimeInterval(-86_401),
recurrence: MeetingRecurrence(frequency: .daily, interval: 1)
)
let viewModel = makeViewModel(mode: .edit(original))

#expect(viewModel.startDate == original.start.addingTimeInterval(86_400))
#expect(viewModel.endDate == original.end.addingTimeInterval(86_400))
}

@Test("edit mode pre-fills recurring meetings that started in the past with the next occurrence")
Expand All @@ -465,16 +496,16 @@ struct MeetingFormViewModelTests {
start: originalStart,
recurrence: MeetingRecurrence(frequency: .daily, interval: 1)
)
let expectedStart = try makeDate(year: 2026, month: 7, day: 7, hour: 9, minute: 0)
let expectedEnd = try makeDate(year: 2026, month: 7, day: 7, hour: 10, minute: 0)
let expectedStart = try makeDate(year: 2026, month: 7, day: 6, hour: 9, minute: 0)
let expectedEnd = try makeDate(year: 2026, month: 7, day: 6, hour: 10, minute: 0)

// When
let viewModel = makeViewModel(mode: .edit(meeting))

// Then
#expect(viewModel.startDate == expectedStart)
#expect(viewModel.endDate == expectedEnd)
#expect(viewModel.startDateRange.lowerBound == Calendar.current.startOfDay(for: dateProviderMock.now))
#expect(viewModel.startDateRange.lowerBound == dateProviderMock.now.addingTimeInterval(-86_400))
}

@Test("submit in edit mode sends the next occurrence time for a recurring meeting that started in the past")
Expand All @@ -489,8 +520,8 @@ struct MeetingFormViewModelTests {
updateMeetingUseCaseMock
.invokeMeetingMeetingTitleStringStartTimeDateEndTimeDateRecurrenceMeetingRecurrenceParticipantsMeetingMemberMeetingReturnValue =
meeting
let expectedStart = try makeDate(year: 2026, month: 7, day: 7, hour: 9, minute: 0)
let expectedEnd = try makeDate(year: 2026, month: 7, day: 7, hour: 10, minute: 0)
let expectedStart = try makeDate(year: 2026, month: 7, day: 6, hour: 9, minute: 0)
let expectedEnd = try makeDate(year: 2026, month: 7, day: 6, hour: 10, minute: 0)

// When
await viewModel.submit()
Expand Down
Loading