Skip to content

Commit 78cc316

Browse files
committed
address PR comments
1 parent 0c20c32 commit 78cc316

4 files changed

Lines changed: 74 additions & 1 deletion

File tree

WireCalling/Sources/WireCallingUI/Resources/Localization/en.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,6 @@
9494
"wireMeetings.schedule.members.retry.button" = "Retry";
9595
"wireMeetings.schedule.members.selected.title" = "Selected";
9696
"wireMeetings.schedule.members.contacts.title" = "Contacts";
97+
98+
"wireMeetings.schedule.error.expiredStartDate.title" = "Choose a later start time";
99+
"wireMeetings.schedule.error.expiredStartDate.message" = "Meetings can only be updated with a start time up to 24 hours in the past. Choose a later start time and save again.";

WireCalling/Sources/WireCallingUI/Views/WireMeetings/MeetingForm/MeetingFormView.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ struct MeetingFormView: View {
7777
dismissButton: .default(Text(Strings.Error.Alert.ok))
7878
)
7979
}
80+
.alert(
81+
Strings.Error.ExpiredStartDate.title,
82+
isPresented: $viewModel.hasExpiredStartDateError
83+
) {
84+
Button(Strings.Error.Alert.ok) {
85+
expandedField = .startDate
86+
}
87+
} message: {
88+
Text(Strings.Error.ExpiredStartDate.message)
89+
}
8090
.alert(
8191
Strings.Error.ConversationName.title,
8292
isPresented: $viewModel.hasConversationNameUpdateError
@@ -141,7 +151,7 @@ struct MeetingFormView: View {
141151
dateTimeRow(
142152
label: Strings.Time.starts,
143153
date: $viewModel.startDate,
144-
range: viewModel.startDateRange,
154+
range: viewModel.startDatePickerRange,
145155
maximumDate: nil,
146156
dateField: .startDate,
147157
timeField: .startTime

WireCalling/Sources/WireCallingUI/Views/WireMeetings/MeetingForm/MeetingFormViewModel.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ package final class MeetingFormViewModel {
104104
return Calendar.current.startOfDay(for: earliest)...
105105
}
106106

107+
/// Keep the current selection representable even if it expires while the form is open.
108+
/// Submission validates against the backend's current window, not this display range.
109+
var startDatePickerRange: PartialRangeFrom<Date> {
110+
guard mode.isEdit else { return startDateRange }
111+
return min(startDate, startDateRange.lowerBound)...
112+
}
113+
107114
/// Acceptance: the end picker must stay on the start date, with 23:45 as the latest available time.
108115
var endDateRange: ClosedRange<Date> {
109116
let latestEndDate = Self.latestEndDate(for: startDate)
@@ -126,6 +133,8 @@ package final class MeetingFormViewModel {
126133
/// logged; the view shows a generic alert.
127134
var hasError = false
128135

136+
var hasExpiredStartDateError = false
137+
129138
/// Set when the meeting was saved but its dedicated conversation could not be renamed.
130139
var hasConversationNameUpdateError = false
131140
private var meetingPendingConversationNameUpdate: Meeting?
@@ -204,6 +213,11 @@ package final class MeetingFormViewModel {
204213
// unexpectedly). Consider making load(pageSize:) return/throw on failure so reloadLoadedMeetings() can restore
205214
// futureOffset (and possibly coalesce missed reloads while isLoading is true).
206215
guard !isLoading else { return }
216+
hasExpiredStartDateError = false
217+
if mode.isEdit, startDate < currentDateProvider.now.addingTimeInterval(-TimeInterval.oneDay) {
218+
hasExpiredStartDateError = true
219+
return
220+
}
207221
isLoading = true
208222
hasError = false
209223
hasConversationNameUpdateError = false

WireCalling/Tests/WireCallingTests/WireMeetings/PresentationTests/MeetingFormViewModelTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,52 @@ struct MeetingFormViewModelTests {
457457
#expect(viewModel.startDateRange.lowerBound == dateProviderMock.now.addingTimeInterval(-86_400))
458458
}
459459

460+
@Test("editing an expired non-recurring meeting preserves its dates but prevents submission")
461+
func editMode_ExpiredNonRecurringMeeting() async {
462+
let original = makeEditableMeeting(
463+
start: dateProviderMock.now.addingTimeInterval(-3 * 86_400),
464+
recurrence: nil
465+
)
466+
let viewModel = makeViewModel(mode: .edit(original))
467+
468+
#expect(viewModel.startDate == original.start)
469+
#expect(viewModel.startDatePickerRange.contains(viewModel.startDate))
470+
471+
await viewModel.submit()
472+
473+
#expect(viewModel.hasExpiredStartDateError)
474+
#expect(!viewModel.isLoading)
475+
let rejectedArguments = updateMeetingUseCaseMock
476+
.invokeMeetingMeetingTitleStringStartTimeDateEndTimeDateRecurrenceMeetingRecurrenceParticipantsMeetingMemberMeetingReceivedArguments
477+
#expect(rejectedArguments == nil)
478+
}
479+
480+
@Test("saving rechecks the current window and allows retry after correcting the start time")
481+
func submit_EditModeStartDateExpiresWhileEditing() async {
482+
let original = makeEditableMeeting(start: dateProviderMock.now.addingTimeInterval(-86_400))
483+
let viewModel = makeViewModel(mode: .edit(original))
484+
dateProviderMock.now = dateProviderMock.now.addingTimeInterval(1)
485+
486+
#expect(viewModel.startDatePickerRange.contains(viewModel.startDate))
487+
await viewModel.submit()
488+
489+
#expect(viewModel.hasExpiredStartDateError)
490+
let rejectedArguments = updateMeetingUseCaseMock
491+
.invokeMeetingMeetingTitleStringStartTimeDateEndTimeDateRecurrenceMeetingRecurrenceParticipantsMeetingMemberMeetingReceivedArguments
492+
#expect(rejectedArguments == nil)
493+
494+
viewModel.startDate = dateProviderMock.now
495+
updateMeetingUseCaseMock
496+
.invokeMeetingMeetingTitleStringStartTimeDateEndTimeDateRecurrenceMeetingRecurrenceParticipantsMeetingMemberMeetingReturnValue =
497+
meeting
498+
await viewModel.submit()
499+
500+
#expect(!viewModel.hasExpiredStartDateError)
501+
let savedArguments = updateMeetingUseCaseMock
502+
.invokeMeetingMeetingTitleStringStartTimeDateEndTimeDateRecurrenceMeetingRecurrenceParticipantsMeetingMemberMeetingReceivedArguments
503+
#expect(savedArguments?.startTime == dateProviderMock.now)
504+
}
505+
460506
@Test("editing preserves and submits recurring start times within 24 hours", arguments: [0.0, 3600.0, 86_400.0])
461507
func editMode_RecentRecurringMeetingPreservesDates(secondsAgo: TimeInterval) async throws {
462508
let original = makeEditableMeeting(start: dateProviderMock.now.addingTimeInterval(-secondsAgo))

0 commit comments

Comments
 (0)