From f776977af811d2d79f22875204e75d5cf1023a74 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 4 Jun 2025 13:28:49 +0200 Subject: [PATCH 1/6] Server References Updated update SixLabors.ImageSharp update Swashbuckle.AspNetCore --- Oqtane.Server/Oqtane.Server.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 2068c0ea..56a682a9 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -46,9 +46,9 @@ - + - + From 4418e27c290b7e449753f4a842607046711585bb Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 5 Jun 2025 09:31:54 -0400 Subject: [PATCH 2/6] rendering optimizations --- .../Controls/Container/ModuleActions.razor | 1 + .../Themes/Controls/Theme/ControlPanel.razor | 1 + Oqtane.Client/UI/ContainerBuilder.razor | 2 +- Oqtane.Client/UI/ModuleInstance.razor | 10 ++++--- Oqtane.Client/UI/PageState.cs | 29 +++++++++++++++++++ Oqtane.Client/UI/Pane.razor | 19 ++++-------- Oqtane.Client/UI/RenderModeBoundary.razor | 4 +-- Oqtane.Client/UI/Routes.razor | 16 ++++++---- Oqtane.Client/UI/SiteRouter.razor | 25 ++++++++++++---- 9 files changed, 74 insertions(+), 33 deletions(-) diff --git a/Oqtane.Client/Themes/Controls/Container/ModuleActions.razor b/Oqtane.Client/Themes/Controls/Container/ModuleActions.razor index 4b973c48..8672c85a 100644 --- a/Oqtane.Client/Themes/Controls/Container/ModuleActions.razor +++ b/Oqtane.Client/Themes/Controls/Container/ModuleActions.razor @@ -20,6 +20,7 @@ protected override void OnParametersSet() { // trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries + // only include properties required by the ModuleActionsInteractive component _pageState = new PageState { Alias = PageState.Alias, diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor index 717cc31e..f38a66c3 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor @@ -91,6 +91,7 @@ } // trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries + // only include properties required by the ControlPanelInteractive component _pageState = new PageState { Alias = PageState.Alias, diff --git a/Oqtane.Client/UI/ContainerBuilder.razor b/Oqtane.Client/UI/ContainerBuilder.razor index 6a769aed..76565c6b 100644 --- a/Oqtane.Client/UI/ContainerBuilder.razor +++ b/Oqtane.Client/UI/ContainerBuilder.razor @@ -6,7 +6,7 @@ @if (ComponentType != null && _visible) { - + @if (_useadminborder) {
diff --git a/Oqtane.Client/UI/ModuleInstance.razor b/Oqtane.Client/UI/ModuleInstance.razor index e9a634d3..0fda480f 100644 --- a/Oqtane.Client/UI/ModuleInstance.razor +++ b/Oqtane.Client/UI/ModuleInstance.razor @@ -10,11 +10,11 @@ @((MarkupString)_comment) @if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static) { - + } else { - + } } @if (PageState.ModuleId == -1) @@ -32,6 +32,7 @@ private bool _prerender; private string _comment; + private PageState _pageState; protected override void OnParametersSet() { @@ -48,11 +49,12 @@ } _comment += " -->"; + _pageState = PageState.Clone(); if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Interactive) { // trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries - // please note that this performance optimization results in the PageState.Pages property not being available for use in Interactive components - PageState.Site.Pages = new List(); + // please note that this performance optimization results in the PageState.Pages property not being available for use in downstream Interactive components + _pageState.Site.Pages = new List(); } } diff --git a/Oqtane.Client/UI/PageState.cs b/Oqtane.Client/UI/PageState.cs index a038a81e..91cf158c 100644 --- a/Oqtane.Client/UI/PageState.cs +++ b/Oqtane.Client/UI/PageState.cs @@ -37,5 +37,34 @@ namespace Oqtane.UI { get { return Site?.Languages; } } + + public PageState Clone() + { + return new PageState + { + Alias = Alias, + Site = Site, + Page = Page, + Modules = Modules, + User = User, + Uri = Uri, + Route = Route, + QueryString = QueryString, + UrlParameters = UrlParameters, + ModuleId = ModuleId, + Action = Action, + EditMode = EditMode, + LastSyncDate = LastSyncDate, + RenderMode = RenderMode, + Runtime = Runtime, + VisitorId = VisitorId, + RemoteIPAddress = RemoteIPAddress, + ReturnUrl = ReturnUrl, + IsInternalNavigation = IsInternalNavigation, + RenderId = RenderId, + Refresh = Refresh, + AllowCookies = AllowCookies + }; + } } } diff --git a/Oqtane.Client/UI/Pane.razor b/Oqtane.Client/UI/Pane.razor index 70226c2e..633cd4fd 100644 --- a/Oqtane.Client/UI/Pane.razor +++ b/Oqtane.Client/UI/Pane.razor @@ -29,7 +29,7 @@ else RenderFragment DynamicComponent { get; set; } protected override void OnParametersSet() - { + { if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList) && PageState.Action == Constants.DefaultAction) { _useadminborder = true; @@ -45,12 +45,6 @@ else { foreach (Module module in PageState.Modules) { - // set renderid - this allows the framework to determine which components should be rendered when PageState changes - if (module.RenderId != PageState.RenderId) - { - module.RenderId = PageState.RenderId; - } - var pane = module.Pane; if (module.ModuleId == PageState.ModuleId && PageState.Action != Constants.DefaultAction) { @@ -101,7 +95,7 @@ else if (authorized) { - CreateComponent(builder, module, module.PageModuleId); + CreateComponent(builder, module); } } } @@ -112,7 +106,7 @@ else // check if user is authorized to view module if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList)) { - CreateComponent(builder, module, -1); + CreateComponent(builder, module); } } } @@ -121,14 +115,11 @@ else }; } - private void CreateComponent(RenderTreeBuilder builder, Module module, int key) + private void CreateComponent(RenderTreeBuilder builder, Module module) { builder.OpenComponent(0, typeof(ContainerBuilder)); builder.AddAttribute(1, "ModuleState", module); - if (key != -1) - { - builder.SetKey(module.PageModuleId); - } + builder.SetKey(module.PageModuleId); builder.CloseComponent(); } } diff --git a/Oqtane.Client/UI/RenderModeBoundary.razor b/Oqtane.Client/UI/RenderModeBoundary.razor index 2c451f29..dbaac7f5 100644 --- a/Oqtane.Client/UI/RenderModeBoundary.razor +++ b/Oqtane.Client/UI/RenderModeBoundary.razor @@ -4,8 +4,8 @@ @inject ILogService LoggingService @inherits ErrorBoundary - - + + @if (CurrentException is null) { @if (ModuleType != null) diff --git a/Oqtane.Client/UI/Routes.razor b/Oqtane.Client/UI/Routes.razor index 20dc0a09..6081966c 100644 --- a/Oqtane.Client/UI/Routes.razor +++ b/Oqtane.Client/UI/Routes.razor @@ -48,12 +48,18 @@ private bool _initialized = false; private bool _installed = false; - private string _display = "display: none;"; + private string _display = ""; private PageState _pageState { get; set; } protected override async Task OnParametersSetAsync() { + if (PageState != null && PageState.RenderMode == RenderModes.Interactive && PageState.Site.Prerender) + { + // prevents flash on initial interactive page load when using prerendering + _display = "display: none;"; + } + SiteState.AntiForgeryToken = AntiForgeryToken; SiteState.AuthorizationToken = AuthorizationToken; SiteState.Platform = Platform; @@ -61,7 +67,7 @@ if (Runtime == Runtimes.Hybrid) { - var installation = await InstallationService.IsInstalled(); + var installation = await InstallationService.IsInstalled(); _installed = installation.Success; if (installation.Alias != null) { @@ -73,8 +79,8 @@ if (PageState != null) { _pageState = PageState; - SiteState.Alias = PageState.Alias; - SiteState.RemoteIPAddress = (PageState != null) ? PageState.RemoteIPAddress : ""; + SiteState.Alias = _pageState.Alias; + SiteState.RemoteIPAddress = _pageState.RemoteIPAddress; _installed = true; } } @@ -85,9 +91,7 @@ { if (firstRender) { - // prevents flash on initial interactive page load _display = ""; - StateHasChanged(); } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 3c3c81fe..a345a7f0 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -71,7 +71,7 @@ { if (PageState == null || PageState.Refresh) { - await Refresh(); + await Refresh(false); } } @@ -79,7 +79,7 @@ { _absoluteUri = args.Location; _isInternalNavigation = true; - await Refresh(); + await Refresh(true); } Task IHandleAfterRender.OnAfterRenderAsync() @@ -93,7 +93,7 @@ } [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] - private async Task Refresh() + private async Task Refresh(bool locationChanged) { Site site = null; Page page = null; @@ -103,6 +103,7 @@ var refresh = false; var lastsyncdate = DateTime.MinValue; var visitorId = -1; + var renderid = Guid.Empty; _error = ""; Route route = new Route(_absoluteUri, SiteState.Alias.Path); @@ -288,11 +289,21 @@ modules = PageState.Modules; } + // renderid allows the framework to determine which module components should be rendered on a page + if (PageState == null || locationChanged) + { + renderid = Guid.NewGuid(); + } + else + { + renderid = PageState.RenderId; + } + // load additional metadata for current page page = ProcessPage(page, site, user, SiteState.Alias, action); // load additional metadata for modules - (page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias); + (page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias, renderid); //cookie consent var _allowCookies = PageState?.AllowCookies; @@ -324,7 +335,7 @@ RemoteIPAddress = SiteState.RemoteIPAddress, ReturnUrl = returnurl, IsInternalNavigation = _isInternalNavigation, - RenderId = Guid.NewGuid(), + RenderId = renderid, Refresh = false, AllowCookies = _allowCookies.GetValueOrDefault(true) }; @@ -447,7 +458,7 @@ return page; } - private (Page Page, List Modules) ProcessModules(Site site, Page page, List modules, int moduleid, string action, string defaultcontainertype, Alias alias) + private (Page Page, List Modules) ProcessModules(Site site, Page page, List modules, int moduleid, string action, string defaultcontainertype, Alias alias, Guid renderid) { var paneindex = new Dictionary(); @@ -592,6 +603,8 @@ { module.ContainerType = defaultcontainertype; } + + module.RenderId = renderid; } foreach (Module module in modules.Where(item => item.PageId == page.PageId)) From 85085bf4c7c29f8f32b6ae8959f4f36fe99c689f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 5 Jun 2025 10:37:25 -0400 Subject: [PATCH 3/6] stop gap fix to mitigate date conversion exceptions on WebAssembly --- Oqtane.Client/Modules/ModuleBase.cs | 32 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 39681067..0dabafd3 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -500,17 +500,24 @@ namespace Oqtane.Modules }; } - // date methods + // date conversion methods public DateTime? UtcToLocal(DateTime? datetime) { TimeZoneInfo timezone = null; - if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + try { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + } + else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + } } - else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + catch { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + // The time zone ID was not found on the local computer } return Utilities.UtcAsLocalDateTime(datetime, timezone); } @@ -518,13 +525,20 @@ namespace Oqtane.Modules public DateTime? LocalToUtc(DateTime? datetime) { TimeZoneInfo timezone = null; - if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + try { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + } + else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + } } - else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + catch { - timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + // The time zone ID was not found on the local computer } return Utilities.LocalDateAndTimeAsUtc(datetime, timezone); } From d4f0805108f8dd5164539d00cc08bd6740356056 Mon Sep 17 00:00:00 2001 From: David Montesinos <90258222+mdmontesinos@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:05:40 +0200 Subject: [PATCH 4/6] fix #5352: remove requests to cookie consent service when not enabled --- Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor index 66626cbc..207de910 100644 --- a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor +++ b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor @@ -103,6 +103,9 @@ { var cookieConsentSetting = SettingService.GetSetting(PageState.Site.Settings, "CookieConsent", string.Empty); _enabled = !string.IsNullOrEmpty(cookieConsentSetting); + + if (!_enabled) return; + _optout = cookieConsentSetting == "optout"; _actioned = await CookieConsentService.IsActionedAsync(); @@ -164,4 +167,4 @@ StateHasChanged(); } } -} \ No newline at end of file +} From ff450ca43a44db405b46ebcbc2ffcb2fddbee764 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Mon, 9 Jun 2025 10:29:43 +0200 Subject: [PATCH 5/6] 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 6/6] 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); }