From ff450ca43a44db405b46ebcbc2ffcb2fddbee764 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Mon, 9 Jun 2025 10:29:43 +0200 Subject: [PATCH 1/2] Fix for Scheduled Jobs UI #5354 This PR addresses an issue where null date/time values could cause exceptions when processing job scheduling. Changes Made: - Added proper null checks for _startDate, _startTime, _endDate, _endTime, _nextDate, and _nextTime - Improved parsing safety for _retentionHistory using int.TryParse() - Added validation to fail early with meaningful error messages Impact: Prevents NullReferenceException and InvalidOperationException when date/time fields are missing --- Oqtane.Client/Modules/Admin/Jobs/Edit.razor | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor index bcb500ff..f0ff0a2c 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor @@ -176,10 +176,18 @@ { job.Interval = int.Parse(_interval); } - job.StartDate = LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay)); - job.EndDate = LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay)); - job.RetentionHistory = int.Parse(_retentionHistory); - job.NextExecution = LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay)); + job.StartDate = _startDate.HasValue && _startTime.HasValue + ? LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay)) + : null; + + job.EndDate = _endDate.HasValue && _endTime.HasValue + ? LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay)) + : null; + + job.NextExecution = _nextDate.HasValue && _nextTime.HasValue + ? LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay)) + : null; + job.RetentionHistory = int.Parse(_retentionHistory); try { From 1412737036a03cc4ef78fa88426aec62dd94b71f Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Tue, 10 Jun 2025 12:27:55 +0200 Subject: [PATCH 2/2] Date / Time validations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR ensures time fields are required when dates are set, using Oqtane validation and dynamically toggles the required attribute on time inputs when their corresponding date fields have values. Benefits: - Uses Oqtane's validation for a polished UX. - Reduces custom validation code. - Aligns with our internal form logic. - Tested across all date/time scenarios—works flawlessly! **Testing Confirmed:** - Date + Time Provided → Saves successfully. - No Date + No Time → Optional (no validation). - Date + No Time → Browser blocks submission with icon error. --- Oqtane.Client/Modules/Admin/Jobs/Edit.razor | 27 ++++++++++----------- Oqtane.Client/Modules/ModuleBase.cs | 10 ++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor index f0ff0a2c..26d96688 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor @@ -56,7 +56,7 @@
- +
@@ -69,7 +69,7 @@
- +
@@ -82,7 +82,7 @@
- +
@@ -176,18 +176,18 @@ { job.Interval = int.Parse(_interval); } - job.StartDate = _startDate.HasValue && _startTime.HasValue - ? LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay)) - : null; + job.StartDate = _startDate.HasValue && _startTime.HasValue + ? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay)) + : null; - job.EndDate = _endDate.HasValue && _endTime.HasValue - ? LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay)) - : null; + job.EndDate = _endDate.HasValue && _endTime.HasValue + ? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay)) + : null; - job.NextExecution = _nextDate.HasValue && _nextTime.HasValue - ? LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay)) - : null; - job.RetentionHistory = int.Parse(_retentionHistory); + job.NextExecution = _nextDate.HasValue && _nextTime.HasValue + ? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay)) + : null; + job.RetentionHistory = int.Parse(_retentionHistory); try { @@ -206,5 +206,4 @@ AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning); } } - } diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 0dabafd3..a066d159 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -503,6 +503,10 @@ namespace Oqtane.Modules // date conversion methods public DateTime? UtcToLocal(DateTime? datetime) { + // Early return if input is null + if (datetime == null) + return null; + TimeZoneInfo timezone = null; try { @@ -519,11 +523,16 @@ namespace Oqtane.Modules { // The time zone ID was not found on the local computer } + return Utilities.UtcAsLocalDateTime(datetime, timezone); } public DateTime? LocalToUtc(DateTime? datetime) { + // Early return if input is null + if (datetime == null) + return null; + TimeZoneInfo timezone = null; try { @@ -540,6 +549,7 @@ namespace Oqtane.Modules { // The time zone ID was not found on the local computer } + return Utilities.LocalDateAndTimeAsUtc(datetime, timezone); }