From 0295b66c22f4eb030043c4ed6427ac6bbcd78a5b Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Tue, 20 Jan 2026 15:07:42 -0500 Subject: [PATCH 01/75] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f5c9ca3..96492eeb 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline # Latest Release -[10.0.4](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4) was released on January 20, 2026 and is a maintenance release including 27 pull requests by 3 different contributors, pushing the total number of project commits all-time over 7500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[10.0.4](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4) was released on January 20, 2026 and is a maintenance release including 29 pull requests by 3 different contributors, pushing the total number of project commits all-time over 7500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. # Try It Now! From 256af74e0c44be263ffe018741fc41c18a1d7341 Mon Sep 17 00:00:00 2001 From: David Montesinos Date: Fri, 23 Jan 2026 09:34:06 +0100 Subject: [PATCH 02/75] Maximum upload file size parameter for FileManager --- .../Modules/Controls/FileManager.razor | 41 +++++++++++++++---- .../Modules/Controls/FileManager.resx | 6 +++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 4bd49824..ea60ac22 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -61,6 +61,12 @@ { } + @if (MaxUploadFileSize > 0) + { +
+ @string.Format(Localizer["File.MaxSize"], MaxUploadFileSize) +
+ }
@@ -163,6 +169,9 @@ [Parameter] public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB + [Parameter] + public int MaxUploadFileSize { get; set; } = -1; // optional - maximum upload file size in MB + [Parameter] public EventCallback OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded @@ -381,16 +390,39 @@ if (uploads.Length > 0) { string restricted = ""; + string tooLarge = ""; foreach (var upload in uploads) { - var filename = upload.Split(':')[0]; + var fileparts = upload.Split(':'); + var filename = fileparts[0]; + + if (MaxUploadFileSize > 0) + { + var filesizeBytes = long.Parse(fileparts[1]); + var filesizeMB = (double)filesizeBytes / (1024 * 1024); + if (filesizeMB > MaxUploadFileSize) + { + tooLarge += (tooLarge == "" ? "" : ",") + filename; + } + } + var extension = (filename.LastIndexOf(".") != -1) ? filename.Substring(filename.LastIndexOf(".") + 1) : ""; if (!PageState.Site.UploadableFiles.Split(',').Contains(extension.ToLower())) { restricted += (restricted == "" ? "" : ",") + extension; } } - if (restricted == "") + if (restricted != "") + { + _message = string.Format(Localizer["Message.File.Restricted"], restricted); + _messagetype = MessageType.Warning; + } + else if (tooLarge != "") + { + _message = string.Format(Localizer["Message.File.TooLarge"], tooLarge, MaxUploadFileSize); + _messagetype = MessageType.Warning; + } + else { CancellationTokenSource tokenSource = new CancellationTokenSource(); @@ -490,11 +522,6 @@ tokenSource.Dispose(); } } - else - { - _message = string.Format(Localizer["Message.File.Restricted"], restricted); - _messagetype = MessageType.Warning; - } } else { diff --git a/Oqtane.Client/Resources/Modules/Controls/FileManager.resx b/Oqtane.Client/Resources/Modules/Controls/FileManager.resx index c2ccac05..2759abb5 100644 --- a/Oqtane.Client/Resources/Modules/Controls/FileManager.resx +++ b/Oqtane.Client/Resources/Modules/Controls/FileManager.resx @@ -144,4 +144,10 @@ Files With Extension Of {0} Are Restricted From Upload. Please Contact Your Administrator For More Information. + + Maximum upload file size: {0} MB + + + File(s) {0} exceed(s) the maximum upload size of {1} MB + \ No newline at end of file From 3be2b9c720869e313618226ec5d50e6ee43855d1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 27 Jan 2026 16:51:30 -0500 Subject: [PATCH 03/75] introducing Site Groups --- .../OqtaneServiceCollectionExtensions.cs | 2 + .../Modules/Admin/Register/Index.razor | 26 +- Oqtane.Client/Modules/Admin/Site/Index.razor | 575 ++++++++++--- Oqtane.Client/Modules/Admin/Sites/Add.razor | 104 +-- .../Modules/Admin/UserProfile/Index.razor | 24 +- Oqtane.Client/Modules/Admin/Users/Add.razor | 25 +- Oqtane.Client/Modules/Admin/Users/Edit.razor | 24 +- .../HtmlText/Services/HtmlTextService.cs | 14 + .../HtmlText/Services/IHtmlTextService.cs | 20 - .../Modules/Admin/Register/Index.resx | 6 + .../Resources/Modules/Admin/Site/Index.resx | 78 +- .../Resources/Modules/Admin/Sites/Add.resx | 4 +- .../Modules/Admin/UserProfile/Index.resx | 6 + .../Resources/Modules/Admin/Users/Add.resx | 6 + .../Resources/Modules/Admin/Users/Edit.resx | 6 + Oqtane.Client/Services/LocalizationService.cs | 6 +- .../Services/SiteGroupDefinitionService.cs | 83 ++ Oqtane.Client/Services/SiteGroupService.cs | 104 +++ .../Themes/Controls/Theme/ControlPanel.razor | 7 +- .../Controls/Theme/LanguageSelector.razor | 37 + .../Themes/OqtaneTheme/Themes/Default.razor | 1 + Oqtane.Client/UI/SiteRouter.razor | 11 +- Oqtane.Server/Components/App.razor | 11 +- .../Controllers/LocalizationController.cs | 19 +- .../Controllers/SiteGroupController.cs | 123 +++ .../SiteGroupDefinitionController.cs | 114 +++ .../OqtaneServiceCollectionExtensions.cs | 4 + .../Infrastructure/DatabaseManager.cs | 3 +- .../Infrastructure/Jobs/NotificationJob.cs | 16 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 778 ++++++++++++++++++ .../Infrastructure/LocalizationManager.cs | 4 +- .../SiteGroupDefinitionEntityBuilder.cs | 48 ++ .../EntityBuilders/SiteGroupEntityBuilder.cs | 52 ++ .../Tenant/10010001_AddSiteGroups.cs | 31 + .../Tenant/10010002_AddCultureCode.cs | 31 + .../Tenant/10010003_RemoveHomePageId.cs | 28 + .../HtmlText/Manager/HtmlTextManager.cs | 88 +- .../HtmlText/Repository/HtmlTextRepository.cs | 30 +- .../Repository/IHtmlTextRepository.cs | 14 - Oqtane.Server/Modules/ISynchronizable.cs | 13 + .../Repository/Context/TenantDBContext.cs | 2 + .../SiteGroupDefinitionRepository.cs | 75 ++ .../Repository/SiteGroupRepository.cs | 88 ++ Oqtane.Server/Services/SiteService.cs | 47 +- Oqtane.Shared/Models/Language.cs | 9 +- Oqtane.Shared/Models/Site.cs | 16 +- Oqtane.Shared/Models/SiteGroup.cs | 49 ++ Oqtane.Shared/Models/SiteGroupDefinition.cs | 35 + Oqtane.Shared/Models/User.cs | 6 + Oqtane.Shared/Shared/Constants.cs | 5 +- Oqtane.Shared/Shared/EntityNames.cs | 2 + 51 files changed, 2558 insertions(+), 352 deletions(-) delete mode 100644 Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs create mode 100644 Oqtane.Client/Services/SiteGroupDefinitionService.cs create mode 100644 Oqtane.Client/Services/SiteGroupService.cs create mode 100644 Oqtane.Client/Themes/Controls/Theme/LanguageSelector.razor create mode 100644 Oqtane.Server/Controllers/SiteGroupController.cs create mode 100644 Oqtane.Server/Controllers/SiteGroupDefinitionController.cs create mode 100644 Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs create mode 100644 Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs create mode 100644 Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs create mode 100644 Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs create mode 100644 Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs create mode 100644 Oqtane.Server/Migrations/Tenant/10010003_RemoveHomePageId.cs delete mode 100644 Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs create mode 100644 Oqtane.Server/Modules/ISynchronizable.cs create mode 100644 Oqtane.Server/Repository/SiteGroupDefinitionRepository.cs create mode 100644 Oqtane.Server/Repository/SiteGroupRepository.cs create mode 100644 Oqtane.Shared/Models/SiteGroup.cs create mode 100644 Oqtane.Shared/Models/SiteGroupDefinition.cs diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 82186385..70188b2d 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -57,6 +57,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); // providers diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 2f7c47e7..4f99e453 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -3,10 +3,11 @@ @inherits ModuleBase @inject NavigationManager NavigationManager @inject IUserService UserService -@inject IStringLocalizer Localizer -@inject IStringLocalizer SharedLocalizer @inject ISettingService SettingService @inject ITimeZoneService TimeZoneService +@inject ILanguageService LanguageService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer @if (_initialized) { @@ -71,6 +72,18 @@
+
+ +
+ +
+

@@ -95,6 +108,8 @@ @code { private bool _initialized = false; private List _timezones; + private IEnumerable _languages; + private string _passwordrequirements; private string _username = string.Empty; private ElementReference form; @@ -106,6 +121,7 @@ private string _email = string.Empty; private string _displayname = string.Empty; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private bool _userCreated = false; private bool _allowsitelogin = true; @@ -113,10 +129,13 @@ protected override async Task OnInitializedAsync() { + _timezones = TimeZoneService.GetTimeZones(); + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); + _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); - _timezones = TimeZoneService.GetTimeZones(); _timezoneid = PageState.Site.TimeZoneId; + _culturecode = PageState.Site.CultureCode; _initialized = true; } @@ -147,6 +166,7 @@ Email = _email, DisplayName = (_displayname == string.Empty ? _username : _displayname), TimeZoneId = _timezoneid, + CultureCode = _culturecode, PhotoFileId = null }; user = await UserService.AddUserAsync(user); diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index d49d90ed..852062cd 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -11,12 +11,15 @@ @inject IThemeService ThemeService @inject ISettingService SettingService @inject ITimeZoneService TimeZoneService +@inject ILocalizationService LocalizationService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject INotificationService NotificationService @inject IJobService JobService @inject IStringLocalizer SharedLocalizer @inject IOutputCacheService CacheService +@inject ISiteGroupDefinitionService SiteGroupDefinitionService +@inject ISiteGroupService SiteGroupService @if (_initialized) { @@ -29,22 +32,7 @@
- -
- -
-
-
- +
+
+ +
+ +
+
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) {
@@ -333,57 +333,6 @@ @if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { -
-
-
- -
- - -
-   -   - @Localizer["AliasName"] - @Localizer["AliasDefault"] -
- - @if (context.AliasId != _aliasid) - { - - @if (_aliasid == -1) - { - - } - - - @if (_aliasid == -1) - { - - } - - @context.Name - @((context.IsDefault) ? SharedLocalizer["Yes"] : SharedLocalizer["No"]) - } - else - { - - - - - - - - - } - -
-
-
-
-
@@ -438,6 +387,170 @@
+
+
+ @if (!_addAlias) + { +
+ +
+
+ + @if (!_addAlias) + { + + } +
+
+
+ } + @if (_aliasid != -1 || _addAlias) + { +
+ +
+ +
+
+
+ +
+ +
+
+ + } +
+
+
+ @if (_aliasid != -1 || _addAlias) + { + + } + @if (_aliasid != -1 && !_addAlias) + { + + } + @if (_addAlias) + { + + } +
+
+
+
+
+
+ @if (!_addSiteGroupDefinition) + { +
+ +
+
+ + @if (!_addSiteGroupDefinition) + { + + } +
+
+
+ } + @if (_siteGroupDefinitionId != -1 || _addSiteGroupDefinition) + { + @if (!_addSiteGroupDefinition) + { +
+ +
+ +
+
+ } + @if (_member == "Primary") + { +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ } + @if (_member == "Secondary") + { +
+ +
+ +
+
+
+ +
+ +
+
+ } + } +
+
+
+ @if ((_siteGroupDefinitionId != -1 || _addSiteGroupDefinition) && _member != "False") + { + + } + @if ((_siteGroupDefinitionId != -1 && !_addSiteGroupDefinition) && _member != "False") + { + + } + @if (_addSiteGroupDefinition) + { + + } +
+
+
+
@@ -464,6 +577,10 @@
+ @if (_siteGroupDefinitions.Any(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) + { + + }

@@ -478,10 +595,11 @@ private List _containers = new List(); private List _pages; private List _timezones; + private IEnumerable _cultures; private string _name = string.Empty; - private string _homepageid = "-"; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private string _isdeleted; private string _sitemap = ""; private string _siteguid = ""; @@ -531,6 +649,7 @@ private int _aliasid = -1; private string _aliasname; private string _defaultalias; + private bool _addAlias = false; private string _rendermode = RenderModes.Interactive; private string _enhancednavigation = "True"; @@ -538,6 +657,16 @@ private string _prerender = "True"; private string _hybrid = "False"; + private List _siteGroupDefinitions = new List(); + private int _siteGroupDefinitionId = -1; + private string _groupName = string.Empty; + private string _member = "Primary"; + private string _synchronization = "True"; + private string _synchronize = "True"; + private string _notifyRoleName = RoleNames.Admin; + private string _localization = "False"; + private bool _addSiteGroupDefinition = false; + private string _tenant = string.Empty; private string _database = string.Empty; private string _connectionstring = string.Empty; @@ -564,16 +693,14 @@ if (site != null) { _timezones = TimeZoneService.GetTimeZones(); + _cultures = await LocalizationService.GetCulturesAsync(false); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _name = site.Name; _timezoneid = site.TimeZoneId; - if (site.HomePageId != null) - { - _homepageid = site.HomePageId.Value.ToString(); - } + _culturecode = site.CultureCode; _isdeleted = site.IsDeleted.ToString(); _sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/sitemap.xml"; _siteguid = site.SiteGuid; @@ -652,7 +779,7 @@ } // aliases - await GetAliases(); + await LoadAliases(); // hosting model _rendermode = site.RenderMode; @@ -676,6 +803,12 @@ } } + // site groups + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + await LoadSiteGroups(); + } + // audit _createdby = site.CreatedBy; _createdon = site.CreatedOn; @@ -743,7 +876,7 @@ { site.Name = _name; site.TimeZoneId = _timezoneid; - site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null); + site.CultureCode = _culturecode; site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); // appearance @@ -992,95 +1125,100 @@ } } - private async Task GetAliases() + private async Task LoadAliases() { - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + _aliases = await AliasService.GetAliasesAsync(); + _aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList(); + _aliasid = -1; + _addAlias = false; + } + + private async void AliasChanged(ChangeEventArgs e) + { + _aliasid = int.Parse(e.Value.ToString()); + if (_aliasid != -1) { - _aliases = await AliasService.GetAliasesAsync(); - _aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList(); + var alias = _aliases.FirstOrDefault(item => item.AliasId == _aliasid); + if (alias != null) + { + _aliasname = alias.Name; + _defaultalias = alias.IsDefault.ToString(); + } } + else + { + _aliasname = ""; + _defaultalias = "False"; + } + StateHasChanged(); } private void AddAlias() { - _aliases.Add(new Alias { AliasId = 0, Name = "", IsDefault = false }); - _aliasid = 0; + _aliasid = -1; _aliasname = ""; _defaultalias = "False"; + _addAlias = true; StateHasChanged(); } - private void EditAlias(Alias alias) + private async Task DeleteAlias() { - _aliasid = alias.AliasId; - _aliasname = alias.Name; - _defaultalias = alias.IsDefault.ToString(); + await AliasService.DeleteAliasAsync(_aliasid); + await LoadAliases(); StateHasChanged(); } - private async Task DeleteAlias(Alias alias) - { - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) - { - await AliasService.DeleteAliasAsync(alias.AliasId); - await GetAliases(); - StateHasChanged(); - } - } - private async Task SaveAlias() { - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + if (!string.IsNullOrEmpty(_aliasname)) { - if (!string.IsNullOrEmpty(_aliasname)) + var aliases = await AliasService.GetAliasesAsync(); + + int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase); + if (protocolIndex != -1) { - var aliases = await AliasService.GetAliasesAsync(); + _aliasname = _aliasname.Substring(protocolIndex + 3); + } - int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase); - if (protocolIndex != -1) + var alias = aliases.FirstOrDefault(item => item.Name == _aliasname); + + bool unique = (alias == null || alias.AliasId == _aliasid); + + if (unique) + { + if (_aliasid == 0) { - _aliasname = _aliasname.Substring(protocolIndex + 3); + alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) }; + await AliasService.AddAliasAsync(alias); } - - var alias = aliases.FirstOrDefault(item => item.Name == _aliasname); - - bool unique = (alias == null || alias.AliasId == _aliasid); - - if (unique) + else { - if (_aliasid == 0) + alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid); + if (alias != null) { - alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) }; - await AliasService.AddAliasAsync(alias); - } - else - { - alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid); - if (alias != null) - { - alias.Name = _aliasname; - alias.IsDefault = bool.Parse(_defaultalias); - await AliasService.UpdateAliasAsync(alias); - } + alias.Name = _aliasname; + alias.IsDefault = bool.Parse(_defaultalias); + await AliasService.UpdateAliasAsync(alias); } + } - await GetAliases(); - _aliasid = -1; - _aliasname = ""; - StateHasChanged(); - } - else // Duplicate alias - { - AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning); - await ScrollToPageTop(); - } + await LoadAliases(); + _aliasid = -1; + _aliasname = ""; + StateHasChanged(); + } + else // Duplicate alias + { + AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning); + await ScrollToPageTop(); } } } private async Task CancelAlias() { - await GetAliases(); + await LoadAliases(); _aliasid = -1; _aliasname = ""; StateHasChanged(); @@ -1090,4 +1228,181 @@ await CacheService.EvictByTag(Constants.SitemapOutputCacheTag); AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success); } + + private async Task LoadSiteGroups() + { + _siteGroupDefinitions = await SiteGroupDefinitionService.GetSiteGroupDefinitionsAsync(); + _siteGroupDefinitionId = -1; + _addSiteGroupDefinition = false; + StateHasChanged(); + } + + private async void SiteGroupChanged(ChangeEventArgs e) + { + _siteGroupDefinitionId = int.Parse(e.Value.ToString()); + if (_siteGroupDefinitionId != -1) + { + var group = _siteGroupDefinitions.FirstOrDefault(item => item.SiteGroupDefinitionId == _siteGroupDefinitionId); + if (group != null) + { + _groupName = group.Name; + _member = (group.PrimarySiteId == PageState.Site.SiteId) ? "Primary" : "Secondary"; + _synchronization = group.Synchronization.ToString(); + _localization = group.Localization.ToString(); + } + + var siteGroup = await SiteGroupService.GetSiteGroupAsync(PageState.Site.SiteId, _siteGroupDefinitionId); + if (siteGroup != null) + { + _synchronize = siteGroup.Synchronize.ToString(); + _notifyRoleName = siteGroup.NotifyRoleName; + } + else + { + _member = "False"; + _synchronize = "True"; + _notifyRoleName = RoleNames.Admin; + } + } + StateHasChanged(); + } + + private async void MemberChanged(ChangeEventArgs e) + { + _member = e.Value.ToString(); + StateHasChanged(); + } + + private async Task AddSiteGroup() + { + _groupName = ""; + _member = "Primary"; + _addSiteGroupDefinition = true; + } + + private async Task SaveSiteGroup() + { + SiteGroupDefinition siteGroupDefinition = null; + + if (_siteGroupDefinitionId == -1) + { + if (!string.IsNullOrEmpty(_groupName)) + { + siteGroupDefinition = new Models.SiteGroupDefinition + { + Name = _groupName, + PrimarySiteId = PageState.Site.SiteId, + Synchronization = bool.Parse(_synchronization), + Localization = bool.Parse(_localization), + Synchronize = false + }; + siteGroupDefinition = await SiteGroupDefinitionService.AddSiteGroupDefinitionAsync(siteGroupDefinition); + } + } + else + { + siteGroupDefinition = _siteGroupDefinitions.FirstOrDefault(item => item.SiteGroupDefinitionId == _siteGroupDefinitionId); + if (siteGroupDefinition != null && !string.IsNullOrEmpty(_groupName)) + { + if (_member == "False") + { + var siteGroup = await SiteGroupService.GetSiteGroupAsync(PageState.Site.SiteId, siteGroupDefinition.SiteGroupDefinitionId); + if (siteGroup != null) + { + await SiteGroupService.DeleteSiteGroupAsync(siteGroup.SiteGroupId); + } + siteGroupDefinition = null; + } + else + { + siteGroupDefinition.Name = _groupName; + siteGroupDefinition.PrimarySiteId = (_member == "Primary") ? PageState.Site.SiteId : siteGroupDefinition.PrimarySiteId; + siteGroupDefinition.Synchronization = bool.Parse(_synchronization); + siteGroupDefinition.Localization = bool.Parse(_localization); + siteGroupDefinition = await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(siteGroupDefinition); + } + } + else + { + siteGroupDefinition = null; + } + } + + if (siteGroupDefinition != null) + { + var siteGroup = await SiteGroupService.GetSiteGroupAsync(PageState.Site.SiteId, siteGroupDefinition.SiteGroupDefinitionId); + if (siteGroup == null) + { + siteGroup = new SiteGroup + { + SiteGroupDefinitionId = siteGroupDefinition.SiteGroupDefinitionId, + SiteId = PageState.Site.SiteId, + Synchronize = bool.Parse(_synchronize), + NotifyRoleName= _notifyRoleName + }; + await SiteGroupService.AddSiteGroupAsync(siteGroup); + } + else + { + siteGroup.Synchronize = bool.Parse(_synchronize); + siteGroup.NotifyRoleName = _notifyRoleName; + await SiteGroupService.UpdateSiteGroupAsync(siteGroup); + } + + await LoadSiteGroups(); + } + else + { + AddModuleMessage(Localizer["Message.Required.GroupName"], MessageType.Warning); + await ScrollToPageTop(); + } + } + + private async Task CancelSiteGroup() + { + _groupName = ""; + await LoadSiteGroups(); + } + + private async Task DeleteSiteGroup() + { + if (_siteGroupDefinitionId != -1) + { + var siteGroup = await SiteGroupService.GetSiteGroupAsync(PageState.Site.SiteId, _siteGroupDefinitionId); + if (siteGroup != null) + { + await SiteGroupService.DeleteSiteGroupAsync(siteGroup.SiteGroupDefinitionId); + } + + var siteGroups = await SiteGroupService.GetSiteGroupsAsync(-1, _siteGroupDefinitionId); + if (!siteGroups.Any()) + { + await SiteGroupDefinitionService.DeleteSiteGroupDefinitionAsync(_siteGroupDefinitionId); + } + + await LoadSiteGroups(); + } + } + + private async Task SynchronizeSite() + { + // enable synchronization job if it is not enabled already + var jobs = await JobService.GetJobsAsync(); + var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server"); + if (job != null && !job.IsEnabled) + { + job.IsEnabled = true; + await JobService.UpdateJobAsync(job); + } + + // mark secondary sites for synchronization + foreach (var group in _siteGroupDefinitions.Where(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) + { + group.Synchronize = true; + await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(group); + } + + AddModuleMessage(Localizer["Message.Site.Synchronize"], MessageType.Success); + await ScrollToPageTop(); + } } diff --git a/Oqtane.Client/Modules/Admin/Sites/Add.razor b/Oqtane.Client/Modules/Admin/Sites/Add.razor index 383a839e..e97aadfc 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Add.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Add.razor @@ -6,7 +6,6 @@ @inject ITenantService TenantService @inject IAliasService AliasService @inject ISiteService SiteService -@inject IThemeService ThemeService @inject ISiteTemplateService SiteTemplateService @inject IUserService UserService @inject IInstallationService InstallationService @@ -29,33 +28,9 @@ else
- +
- -
-
-
- -
- -
-
-
- -
- +
@@ -70,26 +45,6 @@ else
-
- -
- -
-
-
- -
- -
-
@@ -186,9 +141,6 @@ else private bool _showConnectionString = false; private string _connectionString = string.Empty; - private List _themeList; - private List _themes = new List(); - private List _containers = new List(); private List _siteTemplates; private List _tenants; private string _tenantid = "-"; @@ -200,11 +152,7 @@ else private string _name = string.Empty; private string _urls = string.Empty; - private string _themetype = "-"; - private string _containertype = "-"; private string _sitetemplatetype = "-"; - private string _rendermode = RenderModes.Static; - private string _runtime = Runtimes.Server; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; @@ -215,19 +163,11 @@ else { _tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString(); } - _urls = PageState.Alias.Name; - _themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId); - _themes = ThemeService.GetThemeControls(_themeList); - if (_themes.Any(item => item.TypeName == Constants.DefaultTheme)) - { - _themetype = Constants.DefaultTheme; - _containers = ThemeService.GetContainerControls(_themeList, _themetype); - _containertype = _containers.First().TypeName; - } + _urls = PageState.Alias.Name + "/sitename"; _siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync(); - if (_siteTemplates.Any(item => item.TypeName == Constants.DefaultSiteTemplate)) + if (_siteTemplates.Any(item => item.TypeName == Constants.EmptySiteTemplate)) { - _sitetemplatetype = Constants.DefaultSiteTemplate; + _sitetemplatetype = Constants.EmptySiteTemplate; } _databases = await DatabaseService.GetDatabasesAsync(); @@ -281,37 +221,13 @@ else StateHasChanged(); } - private async void ThemeChanged(ChangeEventArgs e) - { - try - { - _themetype = (string)e.Value; - if (_themetype != "-") - { - _containers = ThemeService.GetContainerControls(_themeList, _themetype); - _containertype = _containers.First().TypeName; - } - else - { - _containers = new List(); - _containertype = "-"; - } - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message); - AddModuleMessage(Localizer["Error.Theme.LoadContainers"], MessageType.Error); - } - } - private async Task SaveSite() { validated = true; var interop = new Interop(JSRuntime); if (await interop.FormValid(form)) { - if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-") + if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _sitetemplatetype != "-") { _urls = Regex.Replace(_urls, @"\r\n?|\n", ","); var duplicates = new List(); @@ -399,12 +315,12 @@ else { config.SiteName = _name; config.Aliases = _urls; - config.DefaultTheme = _themetype; - config.DefaultContainer = _containertype; + config.DefaultTheme = Constants.DefaultTheme; + config.DefaultContainer = Constants.DefaultContainer; config.DefaultAdminContainer = ""; config.SiteTemplate = _sitetemplatetype; - config.RenderMode = _rendermode; - config.Runtime = _runtime; + config.RenderMode = RenderModes.Static; + config.Runtime = Runtimes.Server; config.Register = false; ShowProgressIndicator(); diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index 705de755..cf8bac43 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -10,6 +10,7 @@ @inject IFileService FileService @inject IFolderService FolderService @inject ITimeZoneService TimeZoneService +@inject ILanguageService LanguageService @inject IJSRuntime jsRuntime @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @@ -58,6 +59,18 @@
+
+ +
+ +
+
@@ -448,6 +461,9 @@ @code { public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; + private List _timezones; + private IEnumerable _languages; + private bool _initialized = false; private bool _allowtwofactor = false; private bool _allowpasskeys = false; @@ -458,8 +474,8 @@ private string _displayname = string.Empty; private FileManager _filemanager; private int _folderid = -1; - private List _timezones; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private int _photofileid = -1; private File _photo = null; private string _imagefiles = string.Empty; @@ -493,12 +509,15 @@ if (PageState.User != null) { + _timezones = TimeZoneService.GetTimeZones(); + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); + // identity section _username = PageState.User.Username; _email = PageState.User.Email; _displayname = PageState.User.DisplayName; - _timezones = TimeZoneService.GetTimeZones(); _timezoneid = PageState.User.TimeZoneId; + _culturecode = PageState.User.CultureCode; var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); if (folder != null) { @@ -572,6 +591,7 @@ user.Email = _email; user.DisplayName = (_displayname == string.Empty ? _username : _displayname); user.TimeZoneId = _timezoneid; + user.CultureCode = _culturecode; user.PhotoFileId = _filemanager.GetFileId(); if (user.PhotoFileId == -1) { diff --git a/Oqtane.Client/Modules/Admin/Users/Add.razor b/Oqtane.Client/Modules/Admin/Users/Add.razor index 6e19d832..0eea6f43 100644 --- a/Oqtane.Client/Modules/Admin/Users/Add.razor +++ b/Oqtane.Client/Modules/Admin/Users/Add.razor @@ -6,6 +6,7 @@ @inject IProfileService ProfileService @inject ISettingService SettingService @inject ITimeZoneService TimeZoneService +@inject ILanguageService LanguageService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -55,6 +56,18 @@
+
+ +
+ +
+
@@ -129,12 +142,15 @@ @code { private List _timezones; + private IEnumerable _languages; + private bool _initialized = false; private string _username = string.Empty; private string _email = string.Empty; private string _confirmed = "True"; private string _displayname = string.Empty; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private string _notify = "True"; private List _profiles; private Dictionary _settings; @@ -147,6 +163,11 @@ try { _timezones = TimeZoneService.GetTimeZones(); + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); + + _timezoneid = PageState.Site.TimeZoneId; + _culturecode = PageState.Site.CultureCode; + _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); foreach (var profile in _profiles) { @@ -157,8 +178,9 @@ profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}")); } } + _settings = new Dictionary(); - _timezoneid = PageState.Site.TimeZoneId; + _initialized = true; } catch (Exception ex) @@ -194,6 +216,7 @@ user.EmailConfirmed = bool.Parse(_confirmed); user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.TimeZoneId = _timezoneid; + user.CultureCode = _culturecode; user.PhotoFileId = null; user.SuppressNotification = !bool.Parse(_notify); diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index db0ea36b..d629c8bb 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -7,6 +7,7 @@ @inject ISettingService SettingService @inject IFileService FileService @inject ITimeZoneService TimeZoneService +@inject ILanguageService LanguageService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -55,6 +56,18 @@
+
+ +
+ +
+
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) {
@@ -224,13 +237,16 @@ private bool _allowpasskeys = false; private bool _allowexternallogin = false; + private List _timezones; + private IEnumerable _languages; + private int _userid; private string _username = string.Empty; private string _email = string.Empty; private string _confirmed = string.Empty; private string _displayname = string.Empty; - private List _timezones; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private string _isdeleted; private string _lastlogin; private string _lastipaddress; @@ -270,12 +286,15 @@ var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId); if (user != null) { + _timezones = TimeZoneService.GetTimeZones(); + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); + _username = user.Username; _email = user.Email; _confirmed = user.EmailConfirmed.ToString(); _displayname = user.DisplayName; - _timezones = TimeZoneService.GetTimeZones(); _timezoneid = PageState.User.TimeZoneId; + _culturecode = PageState.User.CultureCode; _isdeleted = user.IsDeleted.ToString(); _lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn)); _lastipaddress = user.LastIPAddress; @@ -344,6 +363,7 @@ user.EmailConfirmed = bool.Parse(_confirmed); user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.TimeZoneId = _timezoneid; + user.CultureCode = _culturecode; if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); diff --git a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs index f2f0674c..600374cf 100644 --- a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs +++ b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs @@ -7,6 +7,20 @@ using Oqtane.Shared; namespace Oqtane.Modules.HtmlText.Services { + [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] + public interface IHtmlTextService + { + Task> GetHtmlTextsAsync(int moduleId); + + Task GetHtmlTextAsync(int moduleId); + + Task GetHtmlTextAsync(int htmlTextId, int moduleId); + + Task AddHtmlTextAsync(Models.HtmlText htmltext); + + Task DeleteHtmlTextAsync(int htmlTextId, int moduleId); + } + [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] public class HtmlTextService : ServiceBase, IHtmlTextService, IClientService { diff --git a/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs deleted file mode 100644 index 838d5240..00000000 --- a/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Oqtane.Documentation; - -namespace Oqtane.Modules.HtmlText.Services -{ - [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] - public interface IHtmlTextService - { - Task> GetHtmlTextsAsync(int moduleId); - - Task GetHtmlTextAsync(int moduleId); - - Task GetHtmlTextAsync(int htmlTextId, int moduleId); - - Task AddHtmlTextAsync(Models.HtmlText htmltext); - - Task DeleteHtmlTextAsync(int htmlTextId, int moduleId); - } -} diff --git a/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx index 47e95add..8ebb5968 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx @@ -186,4 +186,10 @@ Your time zone + + Language: + + + Your preferred language. Note that you will only be able to choose from languages supported on this site. + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 3d41770b..380e951a 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -319,13 +319,13 @@ The default alias for the site. Requests for non-default aliases will be redirected to the default alias. - Default Alias: + Default? Site Urls - - Url + + Url: Default? @@ -495,4 +495,76 @@ Use TLS When Available + + A url for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder). + + + Group: + + + The site groups in this tenant (database) + + + Member? + + + Indicates if the current site is a member of the selected group + + + Name: + + + Name of the site group + + + Synchronization? + + + Specifies if the group supports content synchronization between the primary site and other sites in the group + + + Localization? + + + Specifies if each site that is part of the group contains content which is localized in a different language + + + Primary + + + Secondary + + + Compare + + + Update + + + Synchronization: + + + Specifies the synchronization approach from the primary site to the current site + + + Notify Role: + + + Optionally specifies a role in the current site whose users should be notified of content changes in the primary site + + + Delete Site Group + + + Are You Sure You Wish To Delete {0}? + + + Group Name Is Required + + + Site Submitted For Synchronization + + + Synchronize + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx b/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx index d32522e9..b09916e1 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx @@ -133,7 +133,7 @@ Select Theme - The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder). + The primary url for the site. This can be a domain name (ie. domain.com), subdomain (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder). Select the default container for the site @@ -142,7 +142,7 @@ Database: - Urls: + Url: Default Theme: diff --git a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx index b725fd7f..0486bed8 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx @@ -288,4 +288,10 @@ Passkey Could Not Be Created + + Language: + + + Your preferred language. Note that you will only be able to choose from languages supported on this site. + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Add.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Add.resx index faf88ee8..f3e4bada 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Add.resx @@ -168,4 +168,10 @@ Indicates if the user's email is verified + + Language: + + + + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx index 807b932d..3e80cf7c 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx @@ -252,4 +252,10 @@ You Do Not Have Any External Logins For This Site + + Language: + + + The user's preferred language. Note that you will only be able to choose from languages supported on this site. + \ No newline at end of file diff --git a/Oqtane.Client/Services/LocalizationService.cs b/Oqtane.Client/Services/LocalizationService.cs index ee537d60..97948b40 100644 --- a/Oqtane.Client/Services/LocalizationService.cs +++ b/Oqtane.Client/Services/LocalizationService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Oqtane.Documentation; @@ -26,6 +27,9 @@ namespace Oqtane.Services private string Apiurl => CreateApiUrl("Localization"); - public async Task> GetCulturesAsync(bool installed) => await GetJsonAsync>($"{Apiurl}?installed={installed}"); + public async Task> GetCulturesAsync(bool installed) + { + return await GetJsonAsync>($"{Apiurl}?installed={installed}"); + } } } diff --git a/Oqtane.Client/Services/SiteGroupDefinitionService.cs b/Oqtane.Client/Services/SiteGroupDefinitionService.cs new file mode 100644 index 00000000..4f9728e6 --- /dev/null +++ b/Oqtane.Client/Services/SiteGroupDefinitionService.cs @@ -0,0 +1,83 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Collections.Generic; +using Oqtane.Documentation; +using Oqtane.Shared; +using System.Linq; + +namespace Oqtane.Services +{ + /// + /// Service to manage s on a + /// + public interface ISiteGroupDefinitionService + { + /// + /// Get all s + /// + /// + Task> GetSiteGroupDefinitionsAsync(); + + /// + /// Get one specific + /// + /// ID-reference of a + /// + Task GetSiteGroupDefinitionAsync(int siteGroupDefinitionId); + + /// + /// Add / save a new to the database. + /// + /// + /// + Task AddSiteGroupDefinitionAsync(SiteGroupDefinition siteGroupDefinition); + + /// + /// Update a in the database. + /// + /// + /// + Task UpdateSiteGroupDefinitionAsync(SiteGroupDefinition siteGroupDefinition); + + /// + /// Delete a in the database. + /// + /// ID-reference of a + /// + Task DeleteSiteGroupDefinitionAsync(int siteGroupDefinitionId); + } + + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class SiteGroupDefinitionService : ServiceBase, ISiteGroupDefinitionService + { + public SiteGroupDefinitionService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("SiteGroupDefinition"); + + public async Task> GetSiteGroupDefinitionsAsync() + { + return await GetJsonAsync>($"{Apiurl}", Enumerable.Empty().ToList()); + } + + public async Task GetSiteGroupDefinitionAsync(int siteGroupDefinitionId) + { + return await GetJsonAsync($"{Apiurl}/{siteGroupDefinitionId}"); + } + + public async Task AddSiteGroupDefinitionAsync(SiteGroupDefinition siteGroupDefinition) + { + return await PostJsonAsync(Apiurl, siteGroupDefinition); + } + + public async Task UpdateSiteGroupDefinitionAsync(SiteGroupDefinition siteGroupDefinition) + { + return await PutJsonAsync($"{Apiurl}/{siteGroupDefinition.SiteGroupDefinitionId}", siteGroupDefinition); + } + + public async Task DeleteSiteGroupDefinitionAsync(int siteGroupDefinitionId) + { + await DeleteAsync($"{Apiurl}/{siteGroupDefinitionId}"); + } + } +} diff --git a/Oqtane.Client/Services/SiteGroupService.cs b/Oqtane.Client/Services/SiteGroupService.cs new file mode 100644 index 00000000..8cae99f7 --- /dev/null +++ b/Oqtane.Client/Services/SiteGroupService.cs @@ -0,0 +1,104 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Collections.Generic; +using Oqtane.Documentation; +using Oqtane.Shared; +using System.Linq; + +namespace Oqtane.Services +{ + /// + /// Service to manage s on a + /// + public interface ISiteGroupService + { + /// + /// Get all s + /// + /// + Task> GetSiteGroupsAsync(int siteId, int siteGroupDefinitionId); + + /// + /// Get one specific + /// + /// ID-reference of a + /// + Task GetSiteGroupAsync(int siteSiteGroupDefinitionId); + + /// + /// Get one specific + /// + /// ID-reference of a + /// ID-reference of a + /// + Task GetSiteGroupAsync(int siteId, int siteGroupDefinitionId); + + /// + /// Add / save a new to the database. + /// + /// + /// + Task AddSiteGroupAsync(SiteGroup siteGroup); + + /// + /// Update a in the database. + /// + /// + /// + Task UpdateSiteGroupAsync(SiteGroup siteGroup); + + /// + /// Delete a in the database. + /// + /// ID-reference of a + /// + Task DeleteSiteGroupAsync(int siteSiteGroupDefinitionId); + } + + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class SiteGroupService : ServiceBase, ISiteGroupService + { + public SiteGroupService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("SiteGroup"); + + public async Task> GetSiteGroupsAsync(int siteId, int siteGroupDefinitionId) + { + return await GetJsonAsync>($"{Apiurl}?siteid={siteId}&groupid={siteGroupDefinitionId}", Enumerable.Empty().ToList()); + } + + public async Task GetSiteGroupAsync(int siteSiteGroupDefinitionId) + { + return await GetJsonAsync($"{Apiurl}/{siteSiteGroupDefinitionId}"); + } + + public async Task GetSiteGroupAsync(int siteId, int siteGroupDefinitionId) + { + var siteGroups = await GetSiteGroupsAsync(siteId, siteGroupDefinitionId); + if (siteGroups != null && siteGroups.Count > 0) + { + return siteGroups[0]; + } + else + { + return null; + } + } + + public async Task AddSiteGroupAsync(SiteGroup siteGroup) + { + return await PostJsonAsync(Apiurl, siteGroup); + } + + public async Task UpdateSiteGroupAsync(SiteGroup siteGroup) + { + return await PutJsonAsync($"{Apiurl}/{siteGroup.SiteGroupDefinitionId}", siteGroup); + } + + public async Task DeleteSiteGroupAsync(int siteSiteGroupDefinitionId) + { + await DeleteAsync($"{Apiurl}/{siteSiteGroupDefinitionId}"); + } + } +} diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor index 762e457a..78fb7a40 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor @@ -4,11 +4,6 @@ @inject IPageService PageService @inject ISettingService SettingService -@if (ShowLanguageSwitcher) -{ - -} - @if (ShowEditMode && (_showEditMode || (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered)))) {
@@ -53,9 +48,11 @@ [Parameter] public string BodyClass { get; set; } = "offcanvas-body overflow-auto"; + // deprecated in 10.1.0 - UI culture is set in user's profile [Parameter] public bool ShowLanguageSwitcher { get; set; } = true; + // deprecated in 10.1.0 - UI culture is set in user's profile [Parameter] public string LanguageDropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right diff --git a/Oqtane.Client/Themes/Controls/Theme/LanguageSelector.razor b/Oqtane.Client/Themes/Controls/Theme/LanguageSelector.razor new file mode 100644 index 00000000..4a8e1277 --- /dev/null +++ b/Oqtane.Client/Themes/Controls/Theme/LanguageSelector.razor @@ -0,0 +1,37 @@ +@using System.Globalization +@using Oqtane.Models +@using System.Linq +@namespace Oqtane.Themes.Controls +@inherits ThemeControlBase +@inject ILocalizationService LocalizationService +@inject NavigationManager NavigationManager + +@if (PageState.Site.Languages.Count() > 1) +{ +
+ + +
+} + +@code{ + private string MenuAlignment = string.Empty; + + [Parameter] + public string DropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right + + [Parameter] + public string ButtonClass { get; set; } = "btn-outline-secondary"; + + protected override async Task OnParametersSetAsync() + { + MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty; + } +} diff --git a/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor b/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor index f4fe08d6..b54570ed 100644 --- a/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor +++ b/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor @@ -11,6 +11,7 @@ +
diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index fb7d4fc3..64e2f59d 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -233,15 +233,8 @@ if (page == null && route.PagePath == "") // naked path refers to site home page { - if (site.HomePageId != null) - { - page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId); - } - if (page == null) - { - // fallback to use the first page in the collection - page = site.Pages.FirstOrDefault(); - } + // fallback to use the first page in the collection + page = site.Pages.FirstOrDefault(); } if (page == null) { diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index fe668866..666dcef1 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -157,15 +157,8 @@ var page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase)); if (page == null && route.PagePath == "") // naked path refers to site home page { - if (site.HomePageId != null) - { - page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId); - } - if (page == null) - { - // fallback to use the first page in the collection - page = site.Pages.FirstOrDefault(); - } + // fallback to use the first page in the collection + page = site.Pages.FirstOrDefault(); } if (page == null) { diff --git a/Oqtane.Server/Controllers/LocalizationController.cs b/Oqtane.Server/Controllers/LocalizationController.cs index 5a572ba2..7dd1452d 100644 --- a/Oqtane.Server/Controllers/LocalizationController.cs +++ b/Oqtane.Server/Controllers/LocalizationController.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Mvc; +using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Models; +using Oqtane.Repository; using Oqtane.Shared; namespace Oqtane.Controllers @@ -13,10 +15,16 @@ namespace Oqtane.Controllers public class LocalizationController : Controller { private readonly ILocalizationManager _localizationManager; + private readonly ISiteRepository _siteRepository; + private readonly ISiteGroupRepository _siteGroupRepository; + private readonly IAliasRepository _aliasRepository; - public LocalizationController(ILocalizationManager localizationManager) + public LocalizationController(ILocalizationManager localizationManager, ISiteRepository siteRepository, ISiteGroupRepository siteGroupRepository, IAliasRepository aliasRepository) { _localizationManager = localizationManager; + _siteRepository = siteRepository; + _siteGroupRepository = siteGroupRepository; + _aliasRepository = aliasRepository; } // GET: api/localization @@ -32,13 +40,20 @@ namespace Oqtane.Controllers { culturecodes = _localizationManager.GetSupportedCultures(); } + var cultures = culturecodes.Select(c => new Culture { Name = CultureInfo.GetCultureInfo(c).Name, DisplayName = CultureInfo.GetCultureInfo(c).DisplayName, IsDefault = _localizationManager.GetDefaultCulture() .Equals(CultureInfo.GetCultureInfo(c).Name, StringComparison.OrdinalIgnoreCase) - }); + }).ToList(); + + if (cultures.Count == 0) + { + cultures.Add(new Culture { Name = "en", DisplayName = "English", IsDefault = true }); + } + return cultures.OrderBy(item => item.DisplayName); } } diff --git a/Oqtane.Server/Controllers/SiteGroupController.cs b/Oqtane.Server/Controllers/SiteGroupController.cs new file mode 100644 index 00000000..e59f57d8 --- /dev/null +++ b/Oqtane.Server/Controllers/SiteGroupController.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class SiteGroupController : Controller + { + private readonly ISiteGroupRepository _siteGroupRepository; + private readonly ISyncManager _syncManager; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public SiteGroupController(ISiteGroupRepository siteGroupRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + { + _siteGroupRepository = siteGroupRepository; + _syncManager = syncManager; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET: api/?siteid=x&groupid=y + [HttpGet] + [Authorize(Roles = RoleNames.Host)] + public IEnumerable Get(string siteid, string groupid) + { + if (int.TryParse(siteid, out int SiteId) && int.TryParse(groupid, out int SiteGroupDefinitionId)) + { + return _siteGroupRepository.GetSiteGroups(SiteId, SiteGroupDefinitionId).ToList(); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Get Attempt for SiteId {SiteId} And SiteGroupDefinitionId {SiteGroupDefinitionId}", siteid, groupid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // GET api//5 + [HttpGet("{id}")] + [Authorize(Roles = RoleNames.Host)] + public SiteGroup Get(int id) + { + var siteGroup = _siteGroupRepository.GetSiteGroup(id); + if (siteGroup != null) + { + return siteGroup; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + return null; + } + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Host)] + public SiteGroup Post([FromBody] SiteGroup siteGroup) + { + if (ModelState.IsValid) + { + siteGroup = _siteGroupRepository.AddSiteGroup(siteGroup); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupDefinitionId, SyncEventActions.Create); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Added {SiteGroup}", siteGroup); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Post Attempt {SiteGroup}", siteGroup); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroup = null; + } + return siteGroup; + } + + // PUT api//5 + [HttpPut("{id}")] + [Authorize(Roles = RoleNames.Host)] + public SiteGroup Put(int id, [FromBody] SiteGroup siteGroup) + { + if (ModelState.IsValid && siteGroup.SiteGroupDefinitionId == id && _siteGroupRepository.GetSiteGroup(siteGroup.SiteGroupDefinitionId, false) != null) + { + siteGroup = _siteGroupRepository.UpdateSiteGroup(siteGroup); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupDefinitionId, SyncEventActions.Update); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Updated {SiteGroup}", siteGroup); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Put Attempt {SiteGroup}", siteGroup); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroup = null; + } + return siteGroup; + } + + // DELETE api//5 + [HttpDelete("{id}")] + [Authorize(Roles = RoleNames.Host)] + public void Delete(int id) + { + var siteGroup = _siteGroupRepository.GetSiteGroup(id); + if (siteGroup != null) + { + _siteGroupRepository.DeleteSiteGroup(id); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupDefinitionId, SyncEventActions.Delete); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Deleted {SiteGroupDefinitionId}", id); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Delete Attempt {SiteGroupDefinitionId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + } +} diff --git a/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs b/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs new file mode 100644 index 00000000..ed419755 --- /dev/null +++ b/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Oqtane.Enums; +using Oqtane.Models; +using Oqtane.Shared; +using Oqtane.Infrastructure; +using Oqtane.Repository; +using System.Net; +using System.Linq; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class SiteGroupDefinitionController : Controller + { + private readonly ISiteGroupDefinitionRepository _siteGroupDefinitionRepository; + private readonly ISyncManager _syncManager; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public SiteGroupDefinitionController(ISiteGroupDefinitionRepository siteGroupDefinitionRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + { + _siteGroupDefinitionRepository = siteGroupDefinitionRepository; + _syncManager = syncManager; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET: api/ + [HttpGet] + [Authorize(Roles = RoleNames.Host)] + public IEnumerable Get() + { + return _siteGroupDefinitionRepository.GetSiteGroupDefinitions().ToList(); + } + + // GET api//5 + [HttpGet("{id}")] + [Authorize(Roles = RoleNames.Host)] + public SiteGroupDefinition Get(int id) + { + var group = _siteGroupDefinitionRepository.GetSiteGroupDefinition(id); + if (group != null) + { + return group; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + return null; + } + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Host)] + public SiteGroupDefinition Post([FromBody] SiteGroupDefinition siteGroupDefinition) + { + if (ModelState.IsValid) + { + siteGroupDefinition = _siteGroupDefinitionRepository.AddSiteGroupDefinition(siteGroupDefinition); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupDefinition, siteGroupDefinition.SiteGroupDefinitionId, SyncEventActions.Create); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Definition Added {Group}", siteGroupDefinition); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Definition Post Attempt {Group}", siteGroupDefinition); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroupDefinition = null; + } + return siteGroupDefinition; + } + + // PUT api//5 + [HttpPut("{id}")] + [Authorize(Roles = RoleNames.Host)] + public SiteGroupDefinition Put(int id, [FromBody] SiteGroupDefinition siteGroupDefinition) + { + if (ModelState.IsValid && siteGroupDefinition.SiteGroupDefinitionId == id && _siteGroupDefinitionRepository.GetSiteGroupDefinition(siteGroupDefinition.SiteGroupDefinitionId, false) != null) + { + siteGroupDefinition = _siteGroupDefinitionRepository.UpdateSiteGroupDefinition(siteGroupDefinition); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupDefinition, siteGroupDefinition.SiteGroupDefinitionId, SyncEventActions.Update); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Definition Updated {Group}", siteGroupDefinition); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Definition Put Attempt {Group}", siteGroupDefinition); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroupDefinition = null; + } + return siteGroupDefinition; + } + + // DELETE api//5 + [HttpDelete("{id}")] + [Authorize(Roles = RoleNames.Host)] + public void Delete(int id) + { + var siteGroupDefinition = _siteGroupDefinitionRepository.GetSiteGroupDefinition(id); + if (siteGroupDefinition != null) + { + _siteGroupDefinitionRepository.DeleteSiteGroupDefinition(id); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupDefinition, siteGroupDefinition.SiteGroupDefinitionId, SyncEventActions.Delete); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Definition Deleted {siteGroupDefinitionId}", id); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Definition Delete Attempt {siteGroupDefinitionId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 8d0c3f75..a541ebfd 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -233,6 +233,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); @@ -282,6 +284,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); // managers services.AddTransient(); diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 87d6aab0..69d37b3f 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -594,7 +594,8 @@ namespace Oqtane.Infrastructure Prerender = (rendermode == RenderModes.Interactive), Hybrid = false, EnhancedNavigation = true, - TenantId = tenant.TenantId + CultureCode = "en", + TenantId = tenant.TenantId // required for site creation }; site = sites.AddSite(site); diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index 5d0ba275..48589187 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -121,11 +121,23 @@ namespace Oqtane.Infrastructure { if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic") { - // it is possible to use basic without any authentication (not recommended) if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") { - await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), + try + { + await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), settingRepository.GetSettingValue(settings, "SMTPPassword", "")); + } + catch (Exception ex) + { + log += "SMTP Not Configured Properly In Site Settings - Basic Authentication Failed Using Username And Password - " + ex.Message + "
"; + valid = false; + } + + } + else + { + // it is possible to use basic without any authentication (not recommended) } } else diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs new file mode 100644 index 00000000..8325d1a1 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -0,0 +1,778 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Modules; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public class SynchronizationJob : HostedServiceBase + { + // JobType = "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server" + + // synchronization only supports sites in the same tenant (database) + // module title is used as a key to identify module instances on a page (ie. using "-" as a module title is problematic ie. content as configuration) + // relies on Module.ModifiedOn to be set if the module content changes (for efficiency) + // modules must implement ISynchronizable interface (new interface as IPortable was generally only implemented in an additive manner) + + // define settings that should not be synchronized (should be extensible in the future) + List excludedSettings = new List() { + new Setting { EntityName = EntityNames.Site, SettingName = "Search_LastIndexedOn" } + }; + + public SynchronizationJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + Name = "Synchronization Job"; + Frequency = "m"; // minute + Interval = 1; + IsEnabled = false; + } + + // job is executed for each tenant in installation + public override string ExecuteJob(IServiceProvider provider) + { + string log = ""; + + var siteGroupDefinitionRepository = provider.GetRequiredService(); + var siteGroupRepository = provider.GetRequiredService(); + var siteRepository = provider.GetRequiredService(); + var aliasRepository = provider.GetRequiredService(); + var tenantManager = provider.GetRequiredService(); + var settingRepository = provider.GetRequiredService(); + + List siteGroups = null; + List sites = null; + List aliases = null; + + // get groups + var groups = siteGroupDefinitionRepository.GetSiteGroupDefinitions(); + + // iterate through groups which need to be synchronized + foreach (var group in groups.Where(item => item.Synchronize)) + { + // get data + if (siteGroups == null) + { + siteGroups = siteGroupRepository.GetSiteGroups().ToList(); + sites = siteRepository.GetSites().ToList(); + aliases = aliasRepository.GetAliases().ToList(); + } + + var aliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == group.PrimarySiteId && item.IsDefault).Name; + log += $"Processing Primary Site: {sites.First(item => item.SiteId == group.PrimarySiteId).Name} - {aliasName}
"; + + // get primary site + var primarySite = sites.FirstOrDefault(item => item.SiteId == group.PrimarySiteId); + if (primarySite != null) + { + // update flag to prevent job from processing group again + group.Synchronize = false; + siteGroupDefinitionRepository.UpdateSiteGroupDefinition(group); + + // iterate through sites in group + foreach (var siteGroup in siteGroups.Where(item => item.SiteGroupDefinitionId == group.SiteGroupDefinitionId && item.SiteId != group.PrimarySiteId)) + { + // get secondary site + var secondarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.SiteId); + if (secondarySite != null) + { + // get default alias for site + siteGroup.AliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroup.SiteId && item.IsDefault).Name; + + // initialize SynchronizedOn + if (siteGroup.SynchronizedOn == null) + { + siteGroup.SynchronizedOn = DateTime.MinValue; + } + + // replicate site + var siteLog = ReplicateSite(provider, tenantManager, settingRepository, siteGroup, primarySite, secondarySite); + + // set synchronized on date/time + siteGroup.SynchronizedOn = DateTime.UtcNow; + siteGroupRepository.UpdateSiteGroup(siteGroup); + + log += $"Processed Target Site: {secondarySite.Name} - {siteGroup.AliasName}
" + siteLog; + } + else + { + log += $"Site Group Has A SiteId {siteGroup.SiteId} Which Does Not Exist
"; + } + } + } + else + { + log += $"Site Group Has A PrimarySiteId {group.PrimarySiteId} Which Does Not Exist
"; + } + } + + if (string.IsNullOrEmpty(log)) + { + log = "No Site Groups Require Replication
"; + } + + return log; + } + + private string ReplicateSite(IServiceProvider provider, ITenantManager tenantManager, ISettingRepository settingRepository, SiteGroup siteGroup, Site primarySite, Site secondarySite) + { + var log = ""; + + // replicate roles/users + log += ReplicateRoles(provider, settingRepository, siteGroup, primarySite.SiteId, secondarySite.SiteId); + + // replicate folders/files + log += ReplicateFolders(provider, settingRepository, siteGroup, primarySite.SiteId, secondarySite.SiteId); + + // replicate pages/modules + log += ReplicatePages(provider, settingRepository, tenantManager, siteGroup, primarySite.SiteId, secondarySite.SiteId); + + // replicate site + if (primarySite.ModifiedOn > siteGroup.SynchronizedOn) + { + secondarySite.TimeZoneId = primarySite.TimeZoneId; + if (secondarySite.LogoFileId != primarySite.LogoFileId) + { + secondarySite.LogoFileId = ResolveFileId(provider, primarySite.LogoFileId, secondarySite.SiteId); + } + if (secondarySite.FaviconFileId != primarySite.FaviconFileId) + { + secondarySite.FaviconFileId = ResolveFileId(provider, primarySite.FaviconFileId, secondarySite.SiteId); ; + } + secondarySite.DefaultThemeType = primarySite.DefaultThemeType; + secondarySite.DefaultContainerType = primarySite.DefaultContainerType; + secondarySite.AdminContainerType = primarySite.AdminContainerType; + secondarySite.PwaIsEnabled = primarySite.PwaIsEnabled; + if (secondarySite.PwaAppIconFileId != primarySite.PwaAppIconFileId) + { + secondarySite.PwaAppIconFileId = ResolveFileId(provider, primarySite.PwaAppIconFileId, secondarySite.SiteId); ; + } + if (secondarySite.PwaSplashIconFileId != primarySite.PwaSplashIconFileId) + { + secondarySite.PwaSplashIconFileId = ResolveFileId(provider, primarySite.PwaSplashIconFileId, secondarySite.SiteId); ; + } + secondarySite.AllowRegistration = primarySite.AllowRegistration; + secondarySite.VisitorTracking = primarySite.VisitorTracking; + secondarySite.CaptureBrokenUrls = primarySite.CaptureBrokenUrls; + secondarySite.SiteGuid = primarySite.SiteGuid; + secondarySite.RenderMode = primarySite.RenderMode; + secondarySite.Runtime = primarySite.Runtime; + secondarySite.Prerender = primarySite.Prerender; + secondarySite.Hybrid = primarySite.Hybrid; + secondarySite.EnhancedNavigation = primarySite.EnhancedNavigation; + secondarySite.Version = primarySite.Version; + secondarySite.HeadContent = primarySite.HeadContent; + secondarySite.BodyContent = primarySite.BodyContent; + secondarySite.CreatedBy = primarySite.CreatedBy; + secondarySite.CreatedOn = primarySite.CreatedOn; + secondarySite.ModifiedBy = primarySite.ModifiedBy; + secondarySite.ModifiedOn = primarySite.ModifiedOn; + secondarySite.IsDeleted = primarySite.IsDeleted; + secondarySite.DeletedBy = primarySite.DeletedBy; + secondarySite.DeletedOn = primarySite.DeletedOn; + + var siteRepository = provider.GetRequiredService(); + if (siteGroup.Synchronize) + { + siteRepository.UpdateSite(secondarySite); + } + log += Log(siteGroup, $"Secondary Site Updated: {secondarySite.Name}"); + } + + // site settings + log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Site, primarySite.SiteId, secondarySite.SiteId); + + if (siteGroup.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) + { + // clear cache for secondary site if any content was replicated + var syncManager = provider.GetRequiredService(); + var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySite.SiteId }; + syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySite.SiteId, SyncEventActions.Refresh); + } + + if (!string.IsNullOrEmpty(log) && !string.IsNullOrEmpty(siteGroup.NotifyRoleName)) + { + // send change log to users in role + SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, siteGroup.NotifyRoleName, log); + } + + return log; + } + + private int? ResolveFileId(IServiceProvider provider, int? fileId, int siteId) + { + if (fileId != null) + { + var fileRepository = provider.GetRequiredService(); + var file = fileRepository.GetFile(fileId.Value); + fileId = fileRepository.GetFile(siteId, file.Folder.Path, file.Name).FileId; + } + return fileId; + } + + private string ReplicateRoles(IServiceProvider provider, ISettingRepository settingRepository, SiteGroup siteGroup, int primarySiteId, int secondarySiteId) + { + // get roles + var roleRepository = provider.GetRequiredService(); + var primaryRoles = roleRepository.GetRoles(primarySiteId); + var secondaryRoles = roleRepository.GetRoles(secondarySiteId).ToList(); + var log = ""; + + foreach (var primaryRole in primaryRoles) + { + var role = secondaryRoles.FirstOrDefault(item => item.Name == primaryRole.Name); + + var secondaryRole = role; + if (secondaryRole == null) + { + secondaryRole = new Role(); + secondaryRole.SiteId = secondarySiteId; + } + + if (role == null || primaryRole.ModifiedOn > siteGroup.SynchronizedOn) + { + // set all properties + secondaryRole.Name = primaryRole.Name; + secondaryRole.Description = primaryRole.Description; + secondaryRole.IsAutoAssigned = primaryRole.IsAutoAssigned; + secondaryRole.IsSystem = primaryRole.IsSystem; + + if (role == null) + { + if (siteGroup.Synchronize) + { + roleRepository.AddRole(secondaryRole); + } + log += Log(siteGroup, $"Role Added: {secondaryRole.Name}"); + } + else + { + if (siteGroup.Synchronize) + { + roleRepository.UpdateRole(secondaryRole); + } + log += Log(siteGroup, $"Role Updated: {secondaryRole.Name}"); + secondaryRoles.Remove(role); + } + } + } + + // remove roles in the secondary site which do not exist in the primary site + foreach (var secondaryRole in secondaryRoles.Where(item => !primaryRoles.Select(item => item.Name).Contains(item.Name))) + { + if (siteGroup.Synchronize) + { + roleRepository.DeleteRole(secondaryRole.RoleId); + } + log += Log(siteGroup, $"Role Deleted: {secondaryRole.Name}"); + } + + // settings + log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Role, primarySiteId, secondarySiteId); + + return log; + } + + private string ReplicateFolders(IServiceProvider provider, ISettingRepository settingRepository, SiteGroup siteGroup, int primarySiteId, int secondarySiteId) + { + var folderRepository = provider.GetRequiredService(); + var fileRepository = provider.GetRequiredService(); + var log = ""; + + // get folders (ignore personalized) + var primaryFolders = folderRepository.GetFolders(primarySiteId).Where(item => !item.Path.StartsWith("Users/")); + var secondaryFolders = folderRepository.GetFolders(secondarySiteId).Where(item => !item.Path.StartsWith("Users/")).ToList(); + + // iterate through folders + foreach (var primaryFolder in primaryFolders) + { + var folder = secondaryFolders.FirstOrDefault(item => item.Path == primaryFolder.Path); + + var secondaryFolder = folder; + if (secondaryFolder == null) + { + secondaryFolder = new Folder(); + secondaryFolder.SiteId = secondarySiteId; + } + + if (folder == null || primaryFolder.ModifiedOn > siteGroup.SynchronizedOn) + { + // set all properties + secondaryFolder.ParentId = null; + if (primaryFolder.ParentId != null) + { + var parentFolder = folderRepository.GetFolder(secondarySiteId, primaryFolders.First(item => item.FolderId == primaryFolder.ParentId).Path); + if (parentFolder != null) + { + secondaryFolder.ParentId = parentFolder.FolderId; + } + } + secondaryFolder.Type = primaryFolder.Type; + secondaryFolder.Name = primaryFolder.Name; + secondaryFolder.Order = primaryFolder.Order; + secondaryFolder.ImageSizes = primaryFolder.ImageSizes; + secondaryFolder.Capacity = primaryFolder.Capacity; + secondaryFolder.ImageSizes = primaryFolder.ImageSizes; + secondaryFolder.IsSystem = primaryFolder.IsSystem; + secondaryFolder.PermissionList = ReplicatePermissions(primaryFolder.PermissionList, secondarySiteId); + + if (folder == null) + { + if (siteGroup.Synchronize) + { + folderRepository.AddFolder(secondaryFolder); + } + log += Log(siteGroup, $"Folder Added: {secondaryFolder.Path}"); + } + else + { + if (siteGroup.Synchronize) + { + folderRepository.UpdateFolder(secondaryFolder); + } + log += Log(siteGroup, $"Folder Updated: {secondaryFolder.Path}"); + secondaryFolders.Remove(folder); + } + } + + // folder settings + log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId); + + // get files for folder + var primaryFiles = fileRepository.GetFiles(primaryFolder.FolderId); + var secondaryFiles = fileRepository.GetFiles(secondaryFolder.FolderId).ToList(); + + foreach (var primaryFile in primaryFiles) + { + var file = secondaryFiles.FirstOrDefault(item => item.Name == primaryFile.Name); + + var secondaryFile = file; + if (secondaryFile == null) + { + secondaryFile = new Models.File(); + secondaryFile.FolderId = secondaryFolder.FolderId; + secondaryFile.Name = primaryFile.Name; + } + + if (file == null || primaryFile.ModifiedOn > siteGroup.SynchronizedOn) + { + // set all properties + secondaryFile.Extension = primaryFile.Extension; + secondaryFile.Size = primaryFile.Size; + secondaryFile.ImageHeight = primaryFile.ImageHeight; + secondaryFile.ImageWidth = primaryFile.ImageWidth; + secondaryFile.Description = primaryFile.Description; + + if (file == null) + { + if (siteGroup.Synchronize) + { + fileRepository.AddFile(secondaryFile); + ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + } + log += Log(siteGroup, $"File Added: {siteGroup.AliasName}{secondaryFolder.Path}{secondaryFile.Name}"); + } + else + { + if (siteGroup.Synchronize) + { + fileRepository.UpdateFile(secondaryFile); + ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + } + log += Log(siteGroup, $"File Updated: {siteGroup.AliasName}{secondaryFolder.Path}{secondaryFile.Name}"); + secondaryFiles.Remove(file); + } + } + } + + // remove files in the secondary site which do not exist in the primary site + foreach (var secondaryFile in secondaryFiles.Where(item => !primaryFiles.Select(item => item.Name).Contains(item.Name))) + { + if (siteGroup.Synchronize) + { + fileRepository.DeleteFile(secondaryFile.FileId); + var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); + System.IO.File.Delete(secondaryPath); + } + log += Log(siteGroup, $"File Deleted: {siteGroup.AliasName}{secondaryFolder.Path}{secondaryFile.Name}"); + } + } + + // remove folders in the secondary site which do not exist in the primary site + foreach (var secondaryFolder in secondaryFolders.Where(item => !primaryFolders.Select(item => item.Path).Contains(item.Path))) + { + if (siteGroup.Synchronize) + { + folderRepository.DeleteFolder(secondaryFolder.FolderId); + } + log += Log(siteGroup, $"Folder Deleted: {secondaryFolder.Path}"); + } + + return log; + } + + private void ReplicateFile(IFolderRepository folderRepository, Folder primaryFolder, Models.File primaryFile, Folder secondaryFolder, Models.File secondaryFile) + { + var primaryPath = Path.Combine(folderRepository.GetFolderPath(primaryFolder), primaryFile.Name); + var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); + System.IO.File.Copy(primaryPath, secondaryPath, true); + } + + private string ReplicatePages(IServiceProvider provider, ISettingRepository settingRepository, ITenantManager tenantManager, SiteGroup siteGroup, int primarySiteId, int secondarySiteId) + { + var pageRepository = provider.GetRequiredService(); + var pageModuleRepository = provider.GetRequiredService(); + var moduleRepository = provider.GetRequiredService(); + var log = ""; + + List primaryPageModules = null; + List secondaryPageModules = null; + + int tenantId = tenantManager.GetTenant().TenantId; + + // get pages (ignore personalized) + var primaryPages = pageRepository.GetPages(primarySiteId).Where(item => item.UserId == null); + var secondaryPages = pageRepository.GetPages(secondarySiteId).Where(item => item.UserId == null).ToList(); + + // iterate through primary pages + foreach (var primaryPage in primaryPages) + { + var page = secondaryPages.FirstOrDefault(item => item.Path == primaryPage.Path); + + var secondaryPage = page; + if (secondaryPage == null) + { + secondaryPage = new Page(); + secondaryPage.SiteId = secondarySiteId; + } + + if (page == null || primaryPage.ModifiedOn > siteGroup.SynchronizedOn) + { + // set all properties + secondaryPage.Path = primaryPage.Path; + secondaryPage.Name = primaryPage.Name; + secondaryPage.ParentId = null; + if (primaryPage.ParentId != null) + { + var parentPage = pageRepository.GetPage(primaryPages.First(item => item.PageId == primaryPage.ParentId).Path, secondarySiteId); + if (parentPage != null) + { + secondaryPage.ParentId = parentPage.PageId; + } + } + secondaryPage.Title = primaryPage.Title; + secondaryPage.Order = primaryPage.Order; + secondaryPage.Url = primaryPage.Url; + secondaryPage.ThemeType = primaryPage.ThemeType; + secondaryPage.DefaultContainerType = primaryPage.DefaultContainerType; + secondaryPage.HeadContent = primaryPage.HeadContent; + secondaryPage.BodyContent = primaryPage.BodyContent; + secondaryPage.Icon = primaryPage.Icon; + secondaryPage.IsNavigation = primaryPage.IsNavigation; + secondaryPage.IsClickable = primaryPage.IsClickable; + secondaryPage.UserId = null; + secondaryPage.IsPersonalizable = primaryPage.IsPersonalizable; + secondaryPage.EffectiveDate = primaryPage.EffectiveDate; + secondaryPage.ExpiryDate = primaryPage.ExpiryDate; + secondaryPage.CreatedBy = primaryPage.CreatedBy; + secondaryPage.CreatedOn = primaryPage.CreatedOn; + secondaryPage.ModifiedBy = primaryPage.ModifiedBy; + secondaryPage.ModifiedOn = primaryPage.ModifiedOn; + secondaryPage.DeletedBy = primaryPage.DeletedBy; + secondaryPage.DeletedOn = primaryPage.DeletedOn; + secondaryPage.IsDeleted = primaryPage.IsDeleted; + secondaryPage.PermissionList = ReplicatePermissions(primaryPage.PermissionList, secondarySiteId); + + if (page == null) + { + if (siteGroup.Synchronize) + { + secondaryPage = pageRepository.AddPage(secondaryPage); + } + log += Log(siteGroup, $"Page Added: {siteGroup.AliasName}{secondaryPage.Path}"); + } + else + { + if (siteGroup.Synchronize) + { + secondaryPage = pageRepository.UpdatePage(secondaryPage); + } + log += Log(siteGroup, $"Page Updated: {siteGroup.AliasName}{secondaryPage.Path}"); + secondaryPages.Remove(page); + } + } + + // page settings + log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId); + + // modules + if (primaryPageModules == null) + { + tenantManager.SetAlias(tenantId, primarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions() + primaryPageModules = pageModuleRepository.GetPageModules(primarySiteId).ToList(); + } + if (secondaryPageModules == null) + { + tenantManager.SetAlias(tenantId, secondarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions() + secondaryPageModules = pageModuleRepository.GetPageModules(secondarySiteId).ToList(); + } + foreach (var primaryPageModule in primaryPageModules.Where(item => item.PageId == primaryPage.PageId)) + { + var pageModule = secondaryPageModules.FirstOrDefault(item => item.PageId == secondaryPage.PageId && item.Module.ModuleDefinitionName == primaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == primaryPageModule.Title.ToLower()); + + var secondaryPageModule = pageModule; + if (secondaryPageModule == null) + { + secondaryPageModule = new PageModule(); + secondaryPageModule.PageId = secondaryPage.PageId; + secondaryPageModule.Module = new Module(); + secondaryPageModule.Module.SiteId = secondarySiteId; + secondaryPageModule.Module.ModuleDefinitionName = primaryPageModule.Module.ModuleDefinitionName; + } + + if (pageModule == null || primaryPageModule.ModifiedOn > siteGroup.SynchronizedOn || primaryPageModule.Module.ModifiedOn > siteGroup.SynchronizedOn) + { + // set all properties + secondaryPageModule.Title = primaryPageModule.Title; + secondaryPageModule.Pane = primaryPageModule.Pane; + secondaryPageModule.Order = primaryPageModule.Order; + secondaryPageModule.ContainerType = primaryPageModule.ContainerType; + secondaryPageModule.Header = primaryPageModule.Header; + secondaryPageModule.Footer = primaryPageModule.Footer; + secondaryPageModule.IsDeleted = primaryPageModule.IsDeleted; + secondaryPageModule.Module.PermissionList = ReplicatePermissions(primaryPageModule.Module.PermissionList, secondarySiteId); + secondaryPageModule.Module.AllPages = false; + secondaryPageModule.Module.IsDeleted = false; + + var updateContent = false; + + if (pageModule == null) + { + // check if module exists + var module = secondaryPageModules.FirstOrDefault(item => item.Module.ModuleDefinitionName == primaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == primaryPageModule.Title.ToLower())?.Module; + if (module == null) + { + // add new module + if (siteGroup.Synchronize) + { + module = moduleRepository.AddModule(secondaryPageModule.Module); + updateContent = true; + } + log += Log(siteGroup, $"Module Added: {module.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + } + if (module != null) + { + secondaryPageModule.ModuleId = module.ModuleId; + secondaryPageModule.Module = null; // remove tracking + if (siteGroup.Synchronize) + { + secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); + } + log += Log(siteGroup, $"Page Module Added: {module.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + secondaryPageModule.Module = module; + } + } + else + { + // update existing module + if (primaryPageModule.Module.ModifiedOn > siteGroup.SynchronizedOn) + { + if (siteGroup.Synchronize) + { + moduleRepository.UpdateModule(secondaryPageModule.Module); + updateContent = true; + } + log += Log(siteGroup, $"Module Updated: {secondaryPageModule.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + } + if (primaryPageModule.ModifiedOn > siteGroup.SynchronizedOn) + { + if (siteGroup.Synchronize) + { + secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); + } + log += Log(siteGroup, $"Page Module Updated: {secondaryPageModule.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + secondaryPageModules.Remove(pageModule); + } + } + + // module content + if (updateContent && primaryPageModule.Module.ModuleDefinition.ServerManagerType != "") + { + Type moduleType = Type.GetType(primaryPageModule.Module.ModuleDefinition.ServerManagerType); + if (moduleType != null && moduleType.GetInterface(nameof(ISynchronizable)) != null) + { + try + { + var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); + var primaryModuleContent = ((ISynchronizable)moduleObject).ExtractModule(primaryPageModule.Module); + var secondaryModuleContent = ((ISynchronizable)moduleObject).ExtractModule(secondaryPageModule.Module); + if (primaryModuleContent != secondaryModuleContent) + { + if (siteGroup.Synchronize) + { + ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, primaryModuleContent, primaryPageModule.Module.ModuleDefinition.Version); + } + log += Log(siteGroup, $"Module Content Updated: {secondaryPageModule.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + } + } + catch + { + // error exporting/importing + } + } + } + } + + // module settings + log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Module, primaryPageModule.ModuleId, secondaryPageModule.ModuleId); + } + } + + // remove modules in the secondary site which do not exist in the primary site + foreach (var secondaryPageModule in secondaryPageModules) + { + var primaryPageId = -1; + var secondaryPage = secondaryPages.FirstOrDefault(item => item.PageId == secondaryPageModule.PageId); + if (secondaryPage != null) + { + var primaryPage = primaryPages.FirstOrDefault(item => item.Path == secondaryPage.Path); + if (primaryPage != null) + { + primaryPageId = primaryPage.PageId; + } + } + if (!primaryPageModules.Any(item => item.PageId == primaryPageId && item.Module.ModuleDefinitionName == secondaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == secondaryPageModule.Title.ToLower())) + { + if (siteGroup.Synchronize) + { + pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); + } + log += Log(siteGroup, $"Page Module Deleted: {secondaryPageModule.Title} - {siteGroup.AliasName}{secondaryPageModule.Page.Path}"); + } + } + + // remove pages in the secondary site which do not exist in the primary site + foreach (var secondaryPage in secondaryPages.Where(item => !primaryPages.Select(item => item.Path).Contains(item.Path))) + { + if (siteGroup.Synchronize) + { + pageRepository.DeletePage(secondaryPage.PageId); + } + log += Log(siteGroup, $"Page Deleted: {siteGroup.AliasName}{secondaryPage.Path}"); + } + + if (siteGroup.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) + { + // clear cache for secondary site if any content was replicated + var syncManager = provider.GetRequiredService(); + var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySiteId }; + syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySiteId, SyncEventActions.Refresh); + } + + return log; + } + + private List ReplicatePermissions(List permissionList, int siteId) + { + return permissionList.Select(item => new Permission + { + SiteId = siteId, + PermissionName = item.PermissionName, + RoleName = item.RoleName, + UserId = item.UserId, + IsAuthorized = item.IsAuthorized, + CreatedBy = item.CreatedBy, + CreatedOn = item.CreatedOn, + ModifiedBy = item.ModifiedBy, + ModifiedOn = item.ModifiedOn + }).ToList(); + } + + private string ReplicateSettings(ISettingRepository settingRepository, SiteGroup siteGroup, string entityName, int primaryEntityId, int secondaryEntityId) + { + var log = ""; + var updated = false; + + var secondarySettings = settingRepository.GetSettings(entityName, secondaryEntityId).ToList(); + foreach (var primarySetting in settingRepository.GetSettings(entityName, primaryEntityId)) + { + var secondarySetting = secondarySettings.FirstOrDefault(item => item.SettingName == primarySetting.SettingName); + if (secondarySetting == null) + { + secondarySetting = new Setting(); + secondarySetting.EntityName = primarySetting.EntityName; + secondarySetting.EntityId = secondaryEntityId; + secondarySetting.SettingName = primarySetting.SettingName; + secondarySetting.SettingValue = primarySetting.SettingValue; + secondarySetting.IsPrivate = primarySetting.IsPrivate; + if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + { + settingRepository.AddSetting(secondarySetting); + updated = true; + } + } + else + { + if (secondarySetting.SettingValue != primarySetting.SettingValue || secondarySetting.IsPrivate != primarySetting.IsPrivate) + { + secondarySetting.SettingValue = primarySetting.SettingValue; + secondarySetting.IsPrivate = primarySetting.IsPrivate; + if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + { + settingRepository.UpdateSetting(secondarySetting); + updated = true; + } + } + secondarySettings.Remove(secondarySetting); + } + } + + // any remaining secondary settings need to be deleted + foreach (var secondarySetting in secondarySettings) + { + if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + { + settingRepository.DeleteSetting(secondarySetting.EntityName, secondarySetting.SettingId); + updated = true; + } + } + + if (updated) + { + log += Log(siteGroup, $"{entityName} Settings Updated"); + } + + return log; + } + + private void SendNotifications(IServiceProvider provider, int siteId, string siteName, string roleName, string log) + { + var userRoleRepository = provider.GetRequiredService(); + var notificationRepository = provider.GetRequiredService(); + + foreach (var userRole in userRoleRepository.GetUserRoles(roleName, siteId)) + { + var notification = new Notification(siteId, userRole.User, $"{siteName} Change Log", log); + notificationRepository.AddNotification(notification); + } + } + + private string Log(SiteGroup siteGroup, string content) + { + // not necessary to log initial replication + if (siteGroup.SynchronizedOn != DateTime.MinValue) + { + return content + "
"; + } + else + { + return ""; + } + } + } +} diff --git a/Oqtane.Server/Infrastructure/LocalizationManager.cs b/Oqtane.Server/Infrastructure/LocalizationManager.cs index 3dfc0b54..50bda160 100644 --- a/Oqtane.Server/Infrastructure/LocalizationManager.cs +++ b/Oqtane.Server/Infrastructure/LocalizationManager.cs @@ -41,7 +41,9 @@ namespace Oqtane.Infrastructure public string[] GetSupportedCultures() { - return CultureInfo.GetCultures(CultureTypes.AllCultures).Select(item => item.Name).OrderBy(c => c).ToArray(); + return CultureInfo.GetCultures(CultureTypes.AllCultures) + .Where(item => item.Name.Length == 2) // major languages only (this could be configurable) + .Select(item => item.Name).OrderBy(c => c).ToArray(); } public string[] GetInstalledCultures() diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs new file mode 100644 index 00000000..72974ecb --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class SiteGroupDefinitionEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "SiteGroupDefinition"; + private readonly PrimaryKey _primaryKey = new("PK_SiteGroupDefinition", x => x.SiteGroupDefinitionId); + + public SiteGroupDefinitionEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + } + + protected override SiteGroupDefinitionEntityBuilder BuildTable(ColumnsBuilder table) + { + SiteGroupDefinitionId = AddAutoIncrementColumn(table, "SiteGroupDefinitionId"); + Name = AddStringColumn(table, "Name", 200); + PrimarySiteId = AddIntegerColumn(table, "PrimarySiteId"); + Synchronization = AddBooleanColumn(table, "Synchronization"); + Synchronize = AddBooleanColumn(table, "Synchronize"); + Localization = AddBooleanColumn(table, "Localization"); + + AddAuditableColumns(table); + + return this; + } + + public OperationBuilder SiteGroupDefinitionId { get; set; } + + public OperationBuilder Name { get; set; } + + public OperationBuilder PrimarySiteId { get; set; } + + public OperationBuilder Synchronization { get; set; } + + public OperationBuilder Synchronize { get; set; } + + public OperationBuilder Localization { get; set; } + } +} diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs new file mode 100644 index 00000000..ed063737 --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class SiteGroupEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "SiteGroup"; + private readonly PrimaryKey _primaryKey = new("PK_SiteGroup", x => x.SiteGroupId); + private readonly ForeignKey _groupForeignKey = new("FK_SiteGroup_SiteGroupDefinition", x => x.SiteGroupDefinitionId, "SiteGroupDefinition", "SiteGroupDefinitionId", ReferentialAction.Cascade); + private readonly ForeignKey _siteForeignKey = new("FK_SiteGroup_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); + + public SiteGroupEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_groupForeignKey); + ForeignKeys.Add(_siteForeignKey); + } + + protected override SiteGroupEntityBuilder BuildTable(ColumnsBuilder table) + { + SiteGroupId = AddAutoIncrementColumn(table, "SiteGroupId"); + SiteGroupDefinitionId = AddIntegerColumn(table, "SiteGroupDefinitionId"); + SiteId = AddIntegerColumn(table, "SiteId"); + Synchronize = AddBooleanColumn(table, "Synchronize"); + NotifyRoleName = AddStringColumn(table, "NotifyRoleName", 256, true); + SynchronizedOn = AddDateTimeColumn(table, "SynchronizedOn", true); + + AddAuditableColumns(table); + + return this; + } + + public OperationBuilder SiteGroupId { get; set; } + + public OperationBuilder SiteGroupDefinitionId { get; set; } + + public OperationBuilder SiteId { get; set; } + + public OperationBuilder Synchronize { get; set; } + + public OperationBuilder NotifyRoleName { get; set; } + + public OperationBuilder SynchronizedOn { get; set; } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs b/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs new file mode 100644 index 00000000..42255ce8 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.10.01.00.01")] + public class AddSiteGroups : MultiDatabaseMigration + { + public AddSiteGroups(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteGroupDefinitionEntityBuilder = new SiteGroupDefinitionEntityBuilder(migrationBuilder, ActiveDatabase); + siteGroupDefinitionEntityBuilder.Create(); + + var siteGroupEntityBuilder = new SiteGroupEntityBuilder(migrationBuilder, ActiveDatabase); + siteGroupEntityBuilder.Create(); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs b/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs new file mode 100644 index 00000000..0b411227 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.10.01.00.02")] + public class AddCultureCode : MultiDatabaseMigration + { + public AddCultureCode(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.AddStringColumn("CultureCode", 10, true); + + var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase); + userEntityBuilder.AddStringColumn("CultureCode", 10, true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/10010003_RemoveHomePageId.cs b/Oqtane.Server/Migrations/Tenant/10010003_RemoveHomePageId.cs new file mode 100644 index 00000000..bbc79ac5 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10010003_RemoveHomePageId.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.10.01.00.03")] + public class RemoveHomePageId : MultiDatabaseMigration + { + public RemoveHomePageId(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.DropColumn("HomePageId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index 67f05805..da556510 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.Caching.Memory; namespace Oqtane.Modules.HtmlText.Manager { [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] - public class HtmlTextManager : MigratableModuleBase, IInstallable, IPortable, ISearchable + public class HtmlTextManager : MigratableModuleBase, IInstallable, IPortable, ISynchronizable, ISearchable { private readonly IHtmlTextRepository _htmlText; private readonly IDBContextDependencies _DBContextDependencies; @@ -41,7 +41,45 @@ namespace Oqtane.Modules.HtmlText.Manager _cache = cache; } + // IInstallable implementation + public bool Install(Tenant tenant, string version) + { + if (tenant.DBType == Constants.DefaultDBType && version == "1.0.1") + { + // version 1.0.0 used SQL scripts rather than migrations, so we need to seed the migration history table + _sqlRepository.ExecuteNonQuery(tenant, MigrationUtils.BuildInsertScript("HtmlText.01.00.00.00")); + } + return Migrate(new HtmlTextContext(_DBContextDependencies), tenant, MigrationType.Up); + } + + public bool Uninstall(Tenant tenant) + { + return Migrate(new HtmlTextContext(_DBContextDependencies), tenant, MigrationType.Down); + } + + // IPortable implementation public string ExportModule(Module module) + { + return GetModuleContent(module); + } + + public void ImportModule(Module module, string content, string version) + { + SaveModuleContent(module, content, version); + } + + // ISynchronizable implementation + public string ExtractModule(Module module) + { + return GetModuleContent(module); + } + + public void LoadModule(Module module, string content, string version) + { + SaveModuleContent(module, content, version); + } + + private string GetModuleContent(Module module) { string content = ""; var htmltexts = _htmlText.GetHtmlTexts(module.ModuleId); @@ -53,6 +91,23 @@ namespace Oqtane.Modules.HtmlText.Manager return content; } + private void SaveModuleContent(Module module, string content, string version) + { + content = WebUtility.HtmlDecode(content); + var htmlText = new Models.HtmlText(); + htmlText.ModuleId = module.ModuleId; + htmlText.Content = content; + _htmlText.AddHtmlText(htmlText); + + //clear the cache for the module + var alias = _tenantManager.GetAlias(); + if (alias != null) + { + _cache.Remove($"HtmlText:{alias.SiteKey}:{module.ModuleId}"); + } + } + + // ISearchable implementation public Task> GetSearchContentsAsync(PageModule pageModule, DateTime lastIndexedOn) { var searchContents = new List(); @@ -70,36 +125,5 @@ namespace Oqtane.Modules.HtmlText.Manager return Task.FromResult(searchContents); } - - public void ImportModule(Module module, string content, string version) - { - content = WebUtility.HtmlDecode(content); - var htmlText = new Models.HtmlText(); - htmlText.ModuleId = module.ModuleId; - htmlText.Content = content; - _htmlText.AddHtmlText(htmlText); - - //clear the cache for the module - var alias = _tenantManager.GetAlias(); - if(alias != null) - { - _cache.Remove($"HtmlText:{alias.SiteKey}:{module.ModuleId}"); - } - } - - public bool Install(Tenant tenant, string version) - { - if (tenant.DBType == Constants.DefaultDBType && version == "1.0.1") - { - // version 1.0.0 used SQL scripts rather than migrations, so we need to seed the migration history table - _sqlRepository.ExecuteNonQuery(tenant, MigrationUtils.BuildInsertScript("HtmlText.01.00.00.00")); - } - return Migrate(new HtmlTextContext(_DBContextDependencies), tenant, MigrationType.Up); - } - - public bool Uninstall(Tenant tenant) - { - return Migrate(new HtmlTextContext(_DBContextDependencies), tenant, MigrationType.Down); - } } } diff --git a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs index cfeb8eb3..4c320455 100644 --- a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs +++ b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs @@ -2,17 +2,29 @@ using System.Linq; using Oqtane.Documentation; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; +using Oqtane.Repository; namespace Oqtane.Modules.HtmlText.Repository { + [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] + public interface IHtmlTextRepository + { + IEnumerable GetHtmlTexts(int moduleId); + Models.HtmlText GetHtmlText(int htmlTextId); + Models.HtmlText AddHtmlText(Models.HtmlText htmlText); + void DeleteHtmlText(int htmlTextId); + } + [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] public class HtmlTextRepository : IHtmlTextRepository, ITransientService { private readonly IDbContextFactory _factory; + private readonly IModuleRepository _moduleRepository; - public HtmlTextRepository(IDbContextFactory factory) + public HtmlTextRepository(IDbContextFactory factory, IModuleRepository moduleRepository) { _factory = factory; + _moduleRepository = moduleRepository; } public IEnumerable GetHtmlTexts(int moduleId) @@ -32,6 +44,11 @@ namespace Oqtane.Modules.HtmlText.Repository using var db = _factory.CreateDbContext(); db.HtmlText.Add(htmlText); db.SaveChanges(); + + // update module ModifiedOn date + var module = _moduleRepository.GetModule(htmlText.ModuleId); + _moduleRepository.UpdateModule(module); + return htmlText; } @@ -39,8 +56,15 @@ namespace Oqtane.Modules.HtmlText.Repository { using var db = _factory.CreateDbContext(); Models.HtmlText htmlText = db.HtmlText.FirstOrDefault(item => item.HtmlTextId == htmlTextId); - if (htmlText != null) db.HtmlText.Remove(htmlText); - db.SaveChanges(); + if (htmlText != null) + { + db.HtmlText.Remove(htmlText); + db.SaveChanges(); + + // update module ModifiedOn date + var module = _moduleRepository.GetModule(htmlText.ModuleId); + _moduleRepository.UpdateModule(module); + } } } } diff --git a/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs deleted file mode 100644 index d586f0ef..00000000 --- a/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Oqtane.Documentation; - -namespace Oqtane.Modules.HtmlText.Repository -{ - [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] - public interface IHtmlTextRepository - { - IEnumerable GetHtmlTexts(int moduleId); - Models.HtmlText GetHtmlText(int htmlTextId); - Models.HtmlText AddHtmlText(Models.HtmlText htmlText); - void DeleteHtmlText(int htmlTextId); - } -} diff --git a/Oqtane.Server/Modules/ISynchronizable.cs b/Oqtane.Server/Modules/ISynchronizable.cs new file mode 100644 index 00000000..7dce3284 --- /dev/null +++ b/Oqtane.Server/Modules/ISynchronizable.cs @@ -0,0 +1,13 @@ +using Oqtane.Models; + +namespace Oqtane.Modules +{ + public interface ISynchronizable + { + // You Must Set The "ServerManagerType" In Your IModule Interface + + string ExtractModule(Module module); + + void LoadModule(Module module, string content, string version); + } +} diff --git a/Oqtane.Server/Repository/Context/TenantDBContext.cs b/Oqtane.Server/Repository/Context/TenantDBContext.cs index 47932d7d..0c970c3b 100644 --- a/Oqtane.Server/Repository/Context/TenantDBContext.cs +++ b/Oqtane.Server/Repository/Context/TenantDBContext.cs @@ -134,5 +134,7 @@ namespace Oqtane.Repository public virtual DbSet SearchContentWord { get; set; } public virtual DbSet SearchWord { get; set; } public virtual DbSet MigrationHistory { get; set; } + public virtual DbSet SiteGroupDefinition { get; set; } + public virtual DbSet SiteGroup { get; set; } } } diff --git a/Oqtane.Server/Repository/SiteGroupDefinitionRepository.cs b/Oqtane.Server/Repository/SiteGroupDefinitionRepository.cs new file mode 100644 index 00000000..1d1019be --- /dev/null +++ b/Oqtane.Server/Repository/SiteGroupDefinitionRepository.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface ISiteGroupDefinitionRepository + { + IEnumerable GetSiteGroupDefinitions(); + SiteGroupDefinition AddSiteGroupDefinition(SiteGroupDefinition siteGroupDefinition); + SiteGroupDefinition UpdateSiteGroupDefinition(SiteGroupDefinition siteGroupDefinition); + SiteGroupDefinition GetSiteGroupDefinition(int siteGroupDefinitionId); + SiteGroupDefinition GetSiteGroupDefinition(int siteGroupDefinitionId, bool tracking); + void DeleteSiteGroupDefinition(int siteGroupDefinitionId); + } + + public class SiteGroupDefinitionRepository : ISiteGroupDefinitionRepository + { + private readonly IDbContextFactory _dbContextFactory; + + public SiteGroupDefinitionRepository(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public IEnumerable GetSiteGroupDefinitions() + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteGroupDefinition.ToList(); + } + + public SiteGroupDefinition AddSiteGroupDefinition(SiteGroupDefinition siteGroupDefinition) + { + using var db = _dbContextFactory.CreateDbContext(); + db.SiteGroupDefinition.Add(siteGroupDefinition); + db.SaveChanges(); + return siteGroupDefinition; + } + + public SiteGroupDefinition UpdateSiteGroupDefinition(SiteGroupDefinition siteGroupDefinition) + { + using var db = _dbContextFactory.CreateDbContext(); + db.Entry(siteGroupDefinition).State = EntityState.Modified; + db.SaveChanges(); + return siteGroupDefinition; + } + + public SiteGroupDefinition GetSiteGroupDefinition(int siteGroupDefinitionId) + { + return GetSiteGroupDefinition(siteGroupDefinitionId, true); + } + + public SiteGroupDefinition GetSiteGroupDefinition(int siteGroupDefinitionId, bool tracking) + { + using var db = _dbContextFactory.CreateDbContext(); + if (tracking) + { + return db.SiteGroupDefinition.FirstOrDefault(item => item.SiteGroupDefinitionId == siteGroupDefinitionId); + } + else + { + return db.SiteGroupDefinition.AsNoTracking().FirstOrDefault(item => item.SiteGroupDefinitionId == siteGroupDefinitionId); + } + } + + public void DeleteSiteGroupDefinition(int siteGroupDefinitionId) + { + using var db = _dbContextFactory.CreateDbContext(); + SiteGroupDefinition group = db.SiteGroupDefinition.Find(siteGroupDefinitionId); + db.SiteGroupDefinition.Remove(group); + db.SaveChanges(); + } + } +} diff --git a/Oqtane.Server/Repository/SiteGroupRepository.cs b/Oqtane.Server/Repository/SiteGroupRepository.cs new file mode 100644 index 00000000..644cec0c --- /dev/null +++ b/Oqtane.Server/Repository/SiteGroupRepository.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface ISiteGroupRepository + { + IEnumerable GetSiteGroups(); + IEnumerable GetSiteGroups(int siteId, int siteGroupDefinitionId); + SiteGroup AddSiteGroup(SiteGroup siteGroup); + SiteGroup UpdateSiteGroup(SiteGroup siteGroup); + SiteGroup GetSiteGroup(int siteSiteGroupDefinitionId); + SiteGroup GetSiteGroup(int siteSiteGroupDefinitionId, bool tracking); + void DeleteSiteGroup(int siteSiteGroupDefinitionId); + } + + public class SiteGroupRepository : ISiteGroupRepository + { + private readonly IDbContextFactory _dbContextFactory; + + public SiteGroupRepository(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public IEnumerable GetSiteGroups() + { + return GetSiteGroups(-1, -1); + } + + public IEnumerable GetSiteGroups(int siteId, int siteGroupDefinitionId) + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteGroup + .Where(item => (siteId == -1 || item.SiteId == siteId) && (siteGroupDefinitionId == -1 || item.SiteGroupDefinitionId == siteGroupDefinitionId)) + .Include(item => item.SiteGroupDefinition) // eager load + .ToList(); + } + + public SiteGroup AddSiteGroup(SiteGroup SiteGroup) + { + using var db = _dbContextFactory.CreateDbContext(); + db.SiteGroup.Add(SiteGroup); + db.SaveChanges(); + return SiteGroup; + } + + public SiteGroup UpdateSiteGroup(SiteGroup SiteGroup) + { + using var db = _dbContextFactory.CreateDbContext(); + db.Entry(SiteGroup).State = EntityState.Modified; + db.SaveChanges(); + return SiteGroup; + } + + public SiteGroup GetSiteGroup(int SiteGroupDefinitionId) + { + return GetSiteGroup(SiteGroupDefinitionId, true); + } + + public SiteGroup GetSiteGroup(int SiteGroupDefinitionId, bool tracking) + { + using var db = _dbContextFactory.CreateDbContext(); + if (tracking) + { + return db.SiteGroup + .Include(item => item.SiteGroupDefinition) // eager load + .FirstOrDefault(item => item.SiteGroupDefinitionId == SiteGroupDefinitionId); + } + else + { + return db.SiteGroup.AsNoTracking() + .Include(item => item.SiteGroupDefinition) // eager load + .FirstOrDefault(item => item.SiteGroupDefinitionId == SiteGroupDefinitionId); + } + } + + public void DeleteSiteGroup(int SiteGroupDefinitionId) + { + using var db = _dbContextFactory.CreateDbContext(); + SiteGroup SiteGroup = db.SiteGroup.Find(SiteGroupDefinitionId); + db.SiteGroup.Remove(SiteGroup); + db.SaveChanges(); + } + } +} diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index ef7b32d6..ea232358 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -21,6 +21,8 @@ namespace Oqtane.Services public class ServerSiteService : ISiteService { private readonly ISiteRepository _sites; + private readonly ISiteGroupRepository _siteGroups; + private readonly IAliasRepository _aliases; private readonly IPageRepository _pages; private readonly IThemeRepository _themes; private readonly IPageModuleRepository _pageModules; @@ -37,9 +39,11 @@ namespace Oqtane.Services private readonly IHttpContextAccessor _accessor; private readonly string _private = "[PRIVATE]"; - public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserManager userManager, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) + public ServerSiteService(ISiteRepository sites, ISiteGroupRepository siteGroups, IAliasRepository aliases, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserManager userManager, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) { _sites = sites; + _siteGroups = siteGroups; + _aliases = aliases; _pages = pages; _themes = themes; _pageModules = pageModules; @@ -145,12 +149,7 @@ namespace Oqtane.Services site.Settings.Add(Constants.PageManagementModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.PageManagementModule).ModuleId.ToString()); // languages - site.Languages = _languages.GetLanguages(site.SiteId).ToList(); - var defaultCulture = CultureInfo.GetCultureInfo(Constants.DefaultCulture); - if (!site.Languages.Exists(item => item.Code == defaultCulture.Name)) - { - site.Languages.Add(new Language { Code = defaultCulture.Name, Name = "", Version = Constants.Version, IsDefault = !site.Languages.Any(l => l.IsDefault) }); - } + site.Languages = GetLanguages(site.SiteId, alias.TenantId); // themes site.Themes = _themes.FilterThemes(_themes.GetThemes(site.SiteId).ToList()); @@ -158,6 +157,7 @@ namespace Oqtane.Services // installation date used for fingerprinting static assets site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); + // set tenant site.TenantId = alias.TenantId; } else @@ -311,6 +311,39 @@ namespace Oqtane.Services return modules.OrderBy(item => item.PageId).ThenBy(item => item.Pane).ThenBy(item => item.Order).ToList(); } + private List GetLanguages(int siteId, int tenantId) + { + var languages = new List(); + + var siteGroups = _siteGroups.GetSiteGroups(); + if (siteGroups.Any(item => item.SiteId == siteId && item.SiteGroupDefinition.Localization)) + { + var sites = _sites.GetSites().ToList(); + var aliases = _aliases.GetAliases().ToList(); + + foreach (var siteGroupDefinitionId in siteGroups.Where(item => item.SiteId == siteId && item.SiteGroupDefinition.Localization).Select(item => item.SiteGroupDefinitionId).Distinct().ToList()) + { + foreach (var siteGroup in siteGroups.Where(item => item.SiteGroupDefinitionId == siteGroupDefinitionId)) + { + var site = sites.FirstOrDefault(item => item.SiteId == siteGroup.SiteId); + if (site != null && !string.IsNullOrEmpty(site.CultureCode)) + { + if (!languages.Any(item => item.Code == site.CultureCode)) + { + var alias = aliases.FirstOrDefault(item => item.SiteId == siteGroup.SiteId && item.TenantId == tenantId && item.IsDefault); + if (alias != null) + { + languages.Add(new Language { Code = site.CultureCode, Name = "", AliasName = alias.Name, IsDefault = true }); + } + } + } + } + } + } + + return languages; + } + [Obsolete("This method is deprecated.", false)] public void SetAlias(Alias alias) { diff --git a/Oqtane.Shared/Models/Language.cs b/Oqtane.Shared/Models/Language.cs index 750d8583..306c1eaf 100644 --- a/Oqtane.Shared/Models/Language.cs +++ b/Oqtane.Shared/Models/Language.cs @@ -41,6 +41,12 @@ namespace Oqtane.Models /// public string Version { get; set; } + [NotMapped] + /// + /// The primary alias name for the site with this language + /// + public string AliasName { get; set; } + public Language Clone() { return new Language @@ -50,7 +56,8 @@ namespace Oqtane.Models Name = Name, Code = Code, IsDefault = IsDefault, - Version = Version + Version = Version, + AliasName = AliasName }; } } diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index 4ba4ede5..98a12430 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -26,6 +26,11 @@ namespace Oqtane.Models /// public string TimeZoneId { get; set; } + /// + /// The default culture for the site (ie. en-US) + /// + public string CultureCode { get; set; } + /// /// Reference to a which has the Logo for this site. /// Should be an image. @@ -120,11 +125,6 @@ namespace Oqtane.Models /// public string Version { get; set; } - /// - /// The home page of the site - the "/" path will be used by default if no home page is specified - /// - public int? HomePageId { get; set; } - /// /// Content to be included in the head of the page /// @@ -217,6 +217,7 @@ namespace Oqtane.Models SiteId = SiteId, Name = Name, TimeZoneId = TimeZoneId, + CultureCode = CultureCode, LogoFileId = LogoFileId, FaviconFileId = FaviconFileId, DefaultThemeType = DefaultThemeType, @@ -235,7 +236,6 @@ namespace Oqtane.Models Hybrid = Hybrid, EnhancedNavigation = EnhancedNavigation, Version = Version, - HomePageId = HomePageId, HeadContent = HeadContent, BodyContent = BodyContent, IsDeleted = IsDeleted, @@ -262,6 +262,10 @@ namespace Oqtane.Models [NotMapped] [Obsolete("This property is deprecated.", false)] public string DefaultLayoutType { get; set; } + + [NotMapped] + [Obsolete("This property is deprecated.", false)] + public int? HomePageId { get; set; } #endregion } } diff --git a/Oqtane.Shared/Models/SiteGroup.cs b/Oqtane.Shared/Models/SiteGroup.cs new file mode 100644 index 00000000..dffc30b6 --- /dev/null +++ b/Oqtane.Shared/Models/SiteGroup.cs @@ -0,0 +1,49 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Oqtane.Models +{ + public class SiteGroup : ModelBase + { + /// + /// ID to identify the site group + /// + public int SiteGroupId { get; set; } + + /// + /// Reference to the . + /// + public int SiteGroupDefinitionId { get; set; } + + /// + /// Reference to the . + /// + public int SiteId { get; set; } + + /// + /// Indicates if content should be synchronized for the site (false = compare, true = update) + /// + public bool Synchronize { get; set; } + + /// + /// The role who should be notified of changes to the site (non-primary site only) + /// + public string NotifyRoleName { get; set; } + + /// + /// The last date/time the site was synchronized + /// + public DateTime? SynchronizedOn { get; set; } + + /// + /// The itself. + /// + public SiteGroupDefinition SiteGroupDefinition { get; set; } + + /// + /// The primary alias for the site + /// + [NotMapped] + public string AliasName { get; set; } + } +} diff --git a/Oqtane.Shared/Models/SiteGroupDefinition.cs b/Oqtane.Shared/Models/SiteGroupDefinition.cs new file mode 100644 index 00000000..8436ed68 --- /dev/null +++ b/Oqtane.Shared/Models/SiteGroupDefinition.cs @@ -0,0 +1,35 @@ +namespace Oqtane.Models +{ + public class SiteGroupDefinition : ModelBase + { + /// + /// ID to identify the group + /// + public int SiteGroupDefinitionId { get; set; } + + /// + /// Name of the group + /// + public string Name { get; set; } + + /// + /// SiteId of the primary site in the group + /// + public int PrimarySiteId { get; set; } + + /// + /// Indicates if the group supports synchronization + /// + public bool Synchronization { get; set; } + + /// + /// Specifies if the group needs to be synchronized + /// + public bool Synchronize { get; set; } + + /// + /// Indicates if the group supports localization + /// + public bool Localization { get; set; } + } +} diff --git a/Oqtane.Shared/Models/User.cs b/Oqtane.Shared/Models/User.cs index dd3fa320..e84bb723 100644 --- a/Oqtane.Shared/Models/User.cs +++ b/Oqtane.Shared/Models/User.cs @@ -36,6 +36,11 @@ namespace Oqtane.Models /// public string TimeZoneId { get; set; } + /// + /// The default culture for the user (ie. en-US) + /// + public string CultureCode { get; set; } + /// /// Reference to a containing the users photo. /// @@ -140,6 +145,7 @@ namespace Oqtane.Models DisplayName = DisplayName, Email = Email, TimeZoneId = TimeZoneId, + CultureCode = CultureCode, PhotoFileId = PhotoFileId, LastLoginOn = LastLoginOn, LastIPAddress = LastIPAddress, diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 8f52a8db..2df66252 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -4,8 +4,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "10.0.4"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1,10.0.2,10.0.3,10.0.4"; + public static readonly string Version = "10.1.0"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1,10.0.2,10.0.3,10.0.4,10.1.0"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; @@ -36,6 +36,7 @@ namespace Oqtane.Shared public const string AdminSiteTemplate = "Oqtane.Infrastructure.SiteTemplates.AdminSiteTemplate, Oqtane.Server"; public const string DefaultSiteTemplate = "Oqtane.Infrastructure.SiteTemplates.DefaultSiteTemplate, Oqtane.Server"; + public const string EmptySiteTemplate = "Oqtane.Infrastructure.SiteTemplates.EmptySiteTemplate, Oqtane.Server"; public static readonly string[] DefaultHostModuleTypes = new[] { "Upgrade", "Themes", "SystemInfo", "Sql", "Sites", "ModuleDefinitions", "Logs", "Jobs", "ModuleCreator" }; diff --git a/Oqtane.Shared/Shared/EntityNames.cs b/Oqtane.Shared/Shared/EntityNames.cs index 1f9e162d..f7030223 100644 --- a/Oqtane.Shared/Shared/EntityNames.cs +++ b/Oqtane.Shared/Shared/EntityNames.cs @@ -16,6 +16,8 @@ namespace Oqtane.Shared public const string Role = "Role"; public const string Setting = "Setting"; public const string Site = "Site"; + public const string SiteGroup = "SiteGroup"; + public const string SiteGroupDefinition = "SiteGroupDefinition"; public const string Tenant = "Tenant"; public const string Theme = "Theme"; public const string UrlMapping = "UrlMapping"; From c95725d444e67984b85cedd1c01895b45ddb739f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 27 Jan 2026 17:05:31 -0500 Subject: [PATCH 04/75] only host users can synchronize sites --- Oqtane.Client/Modules/Admin/Site/Index.razor | 4 ++-- Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 852062cd..7e45f3a2 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -577,9 +577,9 @@
- @if (_siteGroupDefinitions.Any(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _siteGroupDefinitions.Any(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) { - + }

diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 8325d1a1..8ca7f7a5 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -113,7 +113,7 @@ namespace Oqtane.Infrastructure if (string.IsNullOrEmpty(log)) { - log = "No Site Groups Require Replication
"; + log = "No Site Groups Require Synchronization
"; } return log; From d6bbc6b82f08342347e5b7fc127399e230ad1255 Mon Sep 17 00:00:00 2001 From: isaeed Date: Wed, 28 Jan 2026 12:40:17 +0500 Subject: [PATCH 05/75] admin panel, new entry --- Oqtane.Client/Resources/SharedResources.resx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index b4b3ccfa..e1441d7c 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -204,6 +204,9 @@ System Update + + Setting Management + Download From 912c01cdf8a95adc75c7150af4c888c4807ea384 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 28 Jan 2026 10:47:39 -0500 Subject: [PATCH 06/75] refactorimg Site Groups --- Oqtane.Client/Modules/Admin/Site/Index.razor | 264 ++++++++++-------- .../Resources/Modules/Admin/Site/Index.resx | 28 +- .../Controllers/SiteGroupController.cs | 6 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 93 +++--- .../SiteGroupDefinitionEntityBuilder.cs | 5 +- .../EntityBuilders/SiteGroupEntityBuilder.cs | 6 - .../Tenant/10010001_AddSiteGroups.cs | 1 + .../Repository/SiteGroupRepository.cs | 20 +- Oqtane.Shared/Models/SiteGroup.cs | 10 - Oqtane.Shared/Models/SiteGroupDefinition.cs | 9 +- 10 files changed, 242 insertions(+), 200 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 7e45f3a2..e85de2a2 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -399,7 +399,7 @@ @foreach (var alias in _aliases) { - + } @if (!_addAlias) @@ -473,69 +473,75 @@ } @if (_siteGroupDefinitionId != -1 || _addSiteGroupDefinition) { - @if (!_addSiteGroupDefinition) - { -
- -
- +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ } + @if (_siteGroupDefinitionId != -1) + { +
+ +
+
+
- } - @if (_member == "Primary") - { -
- -
- -
+
+ } + @if (_siteGroupDefinitionId != -1 && _siteId != -1) + { +
+ +
+
-
- -
- -
-
-
- -
- -
-
- } - @if (_member == "Secondary") - { -
- -
- -
-
-
- -
- -
-
- } +
}
- @if ((_siteGroupDefinitionId != -1 || _addSiteGroupDefinition) && _member != "False") + @if ((_siteGroupDefinitionId != -1 || _addSiteGroupDefinition)) { } @@ -577,7 +583,7 @@
- @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _siteGroupDefinitions.Any(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _siteGroupDefinitions.Any(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization != null)) { } @@ -645,26 +651,27 @@ private int _pwasplashiconfileid = -1; private FileManager _pwasplashiconfilemanager; - private List _aliases; - private int _aliasid = -1; - private string _aliasname; - private string _defaultalias; - private bool _addAlias = false; - private string _rendermode = RenderModes.Interactive; private string _enhancednavigation = "True"; private string _runtime = Runtimes.Server; private string _prerender = "True"; private string _hybrid = "False"; - private List _siteGroupDefinitions = new List(); + private List _aliases; + private int _aliasid = -1; + private string _aliasname; + private string _defaultalias; + private bool _addAlias = false; + + private List _siteGroupDefinitions = new List(); + private List _sites = new List(); private int _siteGroupDefinitionId = -1; + private int _siteId = -1; private string _groupName = string.Empty; - private string _member = "Primary"; - private string _synchronization = "True"; - private string _synchronize = "True"; - private string _notifyRoleName = RoleNames.Admin; + private string _synchronization = "Update"; + private string _notify = "True"; private string _localization = "False"; + private string _member = "Primary"; private bool _addSiteGroupDefinition = false; private string _tenant = string.Empty; @@ -1246,36 +1253,80 @@ if (group != null) { _groupName = group.Name; - _member = (group.PrimarySiteId == PageState.Site.SiteId) ? "Primary" : "Secondary"; - _synchronization = group.Synchronization.ToString(); + if (group.Synchronization == null) + { + _synchronization = "False"; + } + else + { + _synchronization = (group.Synchronization.Value) ? "Update" : "Compare"; + } + _notify = group.Notify.ToString(); _localization = group.Localization.ToString(); - } - var siteGroup = await SiteGroupService.GetSiteGroupAsync(PageState.Site.SiteId, _siteGroupDefinitionId); - if (siteGroup != null) - { - _synchronize = siteGroup.Synchronize.ToString(); - _notifyRoleName = siteGroup.NotifyRoleName; - } - else - { + if (_sites.Count == 0) + { + _sites = await SiteService.GetSitesAsync(); + } + _siteId = PageState.Site.SiteId; _member = "False"; - _synchronize = "True"; - _notifyRoleName = RoleNames.Admin; + + var siteGroups = await SiteGroupService.GetSiteGroupsAsync(-1, _siteGroupDefinitionId); + foreach (var site in _sites) + { + site.Fingerprint = ""; // used as temporary state + var siteGroup = siteGroups.FirstOrDefault(item => item.SiteId == site.SiteId); + if (siteGroup != null) + { + if (group.PrimarySiteId == site.SiteId) + { + site.Fingerprint = "Primary"; + } + else + { + site.Fingerprint = "Secondary"; + } + + if (siteGroup.SiteId == _siteId) + { + _member = site.Fingerprint; + } + } + } } } StateHasChanged(); } - private async void MemberChanged(ChangeEventArgs e) + private async void SiteChanged(ChangeEventArgs e) { - _member = e.Value.ToString(); + _siteId = int.Parse(e.Value.ToString()); + if (_siteId != -1) + { + var siteGroup = await SiteGroupService.GetSiteGroupAsync(_siteId, _siteGroupDefinitionId); + if (siteGroup != null) + { + if (siteGroup.SiteGroupDefinition.PrimarySiteId == _siteId) + { + _member = "Primary"; + } + else + { + _member = "Secondary"; + } + } + else + { + _member = "False"; + } + } StateHasChanged(); } private async Task AddSiteGroup() { _groupName = ""; + _siteId = PageState.Site.SiteId; _member = "Primary"; _addSiteGroupDefinition = true; } @@ -1284,6 +1335,10 @@ { SiteGroupDefinition siteGroupDefinition = null; + bool? synchronization = null; + if (_synchronization == "Compare") synchronization = false; + if (_synchronization == "Update") synchronization = true; + if (_siteGroupDefinitionId == -1) { if (!string.IsNullOrEmpty(_groupName)) @@ -1291,8 +1346,9 @@ siteGroupDefinition = new Models.SiteGroupDefinition { Name = _groupName, - PrimarySiteId = PageState.Site.SiteId, - Synchronization = bool.Parse(_synchronization), + PrimarySiteId = _siteId, + Synchronization = synchronization, + Notify = bool.Parse(_notify), Localization = bool.Parse(_localization), Synchronize = false }; @@ -1304,23 +1360,12 @@ siteGroupDefinition = _siteGroupDefinitions.FirstOrDefault(item => item.SiteGroupDefinitionId == _siteGroupDefinitionId); if (siteGroupDefinition != null && !string.IsNullOrEmpty(_groupName)) { - if (_member == "False") - { - var siteGroup = await SiteGroupService.GetSiteGroupAsync(PageState.Site.SiteId, siteGroupDefinition.SiteGroupDefinitionId); - if (siteGroup != null) - { - await SiteGroupService.DeleteSiteGroupAsync(siteGroup.SiteGroupId); - } - siteGroupDefinition = null; - } - else - { - siteGroupDefinition.Name = _groupName; - siteGroupDefinition.PrimarySiteId = (_member == "Primary") ? PageState.Site.SiteId : siteGroupDefinition.PrimarySiteId; - siteGroupDefinition.Synchronization = bool.Parse(_synchronization); - siteGroupDefinition.Localization = bool.Parse(_localization); - siteGroupDefinition = await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(siteGroupDefinition); - } + siteGroupDefinition.Name = _groupName; + siteGroupDefinition.PrimarySiteId = (_member == "Primary") ? _siteId : siteGroupDefinition.PrimarySiteId; + siteGroupDefinition.Synchronization = synchronization; + siteGroupDefinition.Notify = bool.Parse(_notify); + siteGroupDefinition.Localization = bool.Parse(_localization); + siteGroupDefinition = await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(siteGroupDefinition); } else { @@ -1330,23 +1375,22 @@ if (siteGroupDefinition != null) { - var siteGroup = await SiteGroupService.GetSiteGroupAsync(PageState.Site.SiteId, siteGroupDefinition.SiteGroupDefinitionId); + var siteGroup = await SiteGroupService.GetSiteGroupAsync(_siteId, siteGroupDefinition.SiteGroupDefinitionId); if (siteGroup == null) { siteGroup = new SiteGroup { SiteGroupDefinitionId = siteGroupDefinition.SiteGroupDefinitionId, - SiteId = PageState.Site.SiteId, - Synchronize = bool.Parse(_synchronize), - NotifyRoleName= _notifyRoleName + SiteId = _siteId }; await SiteGroupService.AddSiteGroupAsync(siteGroup); } else { - siteGroup.Synchronize = bool.Parse(_synchronize); - siteGroup.NotifyRoleName = _notifyRoleName; - await SiteGroupService.UpdateSiteGroupAsync(siteGroup); + if (_member == "False") + { + await SiteGroupService.DeleteSiteGroupAsync(siteGroup.SiteGroupId); + } } await LoadSiteGroups(); @@ -1396,7 +1440,7 @@ } // mark secondary sites for synchronization - foreach (var group in _siteGroupDefinitions.Where(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) + foreach (var group in _siteGroupDefinitions.Where(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization != null)) { group.Synchronize = true; await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(group); diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 380e951a..e7efa0bb 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -327,8 +327,8 @@ Url: - - Default? + + Default Are You Sure You Wish To Delete {0}? @@ -540,18 +540,6 @@ Update - - Synchronization: - - - Specifies the synchronization approach from the primary site to the current site - - - Notify Role: - - - Optionally specifies a role in the current site whose users should be notified of content changes in the primary site - Delete Site Group @@ -567,4 +555,16 @@ Synchronize + + Notify? + + + Specifies if the site administrator of secondary sites should be notified of any synchronization activity + + + Site: + + + he sites in this tenant (database) + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/SiteGroupController.cs b/Oqtane.Server/Controllers/SiteGroupController.cs index e59f57d8..62441495 100644 --- a/Oqtane.Server/Controllers/SiteGroupController.cs +++ b/Oqtane.Server/Controllers/SiteGroupController.cs @@ -110,12 +110,12 @@ namespace Oqtane.Controllers if (siteGroup != null) { _siteGroupRepository.DeleteSiteGroup(id); - _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupDefinitionId, SyncEventActions.Delete); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Deleted {SiteGroupDefinitionId}", id); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Delete); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Deleted {SiteGroupId}", id); } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Delete Attempt {SiteGroupDefinitionId}", id); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Delete Attempt {SiteGroupId}", id); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 8ca7f7a5..7c3a0d8c 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -53,7 +53,7 @@ namespace Oqtane.Infrastructure var groups = siteGroupDefinitionRepository.GetSiteGroupDefinitions(); // iterate through groups which need to be synchronized - foreach (var group in groups.Where(item => item.Synchronize)) + foreach (var group in groups.Where(item => item.Synchronization != null && item.Synchronize)) { // get data if (siteGroups == null) @@ -64,7 +64,7 @@ namespace Oqtane.Infrastructure } var aliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == group.PrimarySiteId && item.IsDefault).Name; - log += $"Processing Primary Site: {sites.First(item => item.SiteId == group.PrimarySiteId).Name} - {aliasName}
"; + log += $"Processing Primary Site: {sites.First(item => item.SiteId == group.PrimarySiteId).Name} - {CreateLink(aliasName)}
"; // get primary site var primarySite = sites.FirstOrDefault(item => item.SiteId == group.PrimarySiteId); @@ -97,17 +97,17 @@ namespace Oqtane.Infrastructure siteGroup.SynchronizedOn = DateTime.UtcNow; siteGroupRepository.UpdateSiteGroup(siteGroup); - log += $"Processed Target Site: {secondarySite.Name} - {siteGroup.AliasName}
" + siteLog; + log += $"Processed Secondary Site: {secondarySite.Name} - {CreateLink(siteGroup.AliasName)}
" + siteLog; } else { - log += $"Site Group Has A SiteId {siteGroup.SiteId} Which Does Not Exist
"; + log += $"Site Group {group.Name} Has A SiteId {siteGroup.SiteId} Which Does Not Exist
"; } } } else { - log += $"Site Group Has A PrimarySiteId {group.PrimarySiteId} Which Does Not Exist
"; + log += $"Site Group {group.Name} Has A PrimarySiteId {group.PrimarySiteId} Which Does Not Exist
"; } } @@ -177,7 +177,7 @@ namespace Oqtane.Infrastructure secondarySite.DeletedOn = primarySite.DeletedOn; var siteRepository = provider.GetRequiredService(); - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { siteRepository.UpdateSite(secondarySite); } @@ -195,10 +195,10 @@ namespace Oqtane.Infrastructure syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySite.SiteId, SyncEventActions.Refresh); } - if (!string.IsNullOrEmpty(log) && !string.IsNullOrEmpty(siteGroup.NotifyRoleName)) + if (!string.IsNullOrEmpty(log) && siteGroup.SiteGroupDefinition.Notify) { - // send change log to users in role - SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, siteGroup.NotifyRoleName, log); + // send change log to administrators + SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, log); } return log; @@ -244,7 +244,7 @@ namespace Oqtane.Infrastructure if (role == null) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { roleRepository.AddRole(secondaryRole); } @@ -252,7 +252,7 @@ namespace Oqtane.Infrastructure } else { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { roleRepository.UpdateRole(secondaryRole); } @@ -265,7 +265,7 @@ namespace Oqtane.Infrastructure // remove roles in the secondary site which do not exist in the primary site foreach (var secondaryRole in secondaryRoles.Where(item => !primaryRoles.Select(item => item.Name).Contains(item.Name))) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { roleRepository.DeleteRole(secondaryRole.RoleId); } @@ -323,7 +323,7 @@ namespace Oqtane.Infrastructure if (folder == null) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { folderRepository.AddFolder(secondaryFolder); } @@ -331,7 +331,7 @@ namespace Oqtane.Infrastructure } else { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { folderRepository.UpdateFolder(secondaryFolder); } @@ -370,21 +370,21 @@ namespace Oqtane.Infrastructure if (file == null) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { fileRepository.AddFile(secondaryFile); ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); } - log += Log(siteGroup, $"File Added: {siteGroup.AliasName}{secondaryFolder.Path}{secondaryFile.Name}"); + log += Log(siteGroup, $"File Added: {CreateLink(siteGroup.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); } else { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { fileRepository.UpdateFile(secondaryFile); ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); } - log += Log(siteGroup, $"File Updated: {siteGroup.AliasName}{secondaryFolder.Path}{secondaryFile.Name}"); + log += Log(siteGroup, $"File Updated: {CreateLink(siteGroup.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); secondaryFiles.Remove(file); } } @@ -393,20 +393,20 @@ namespace Oqtane.Infrastructure // remove files in the secondary site which do not exist in the primary site foreach (var secondaryFile in secondaryFiles.Where(item => !primaryFiles.Select(item => item.Name).Contains(item.Name))) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { fileRepository.DeleteFile(secondaryFile.FileId); var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); System.IO.File.Delete(secondaryPath); } - log += Log(siteGroup, $"File Deleted: {siteGroup.AliasName}{secondaryFolder.Path}{secondaryFile.Name}"); + log += Log(siteGroup, $"File Deleted: {CreateLink(siteGroup.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); } } // remove folders in the secondary site which do not exist in the primary site foreach (var secondaryFolder in secondaryFolders.Where(item => !primaryFolders.Select(item => item.Path).Contains(item.Path))) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { folderRepository.DeleteFolder(secondaryFolder.FolderId); } @@ -490,19 +490,19 @@ namespace Oqtane.Infrastructure if (page == null) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { secondaryPage = pageRepository.AddPage(secondaryPage); } - log += Log(siteGroup, $"Page Added: {siteGroup.AliasName}{secondaryPage.Path}"); + log += Log(siteGroup, $"Page Added: {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); } else { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { secondaryPage = pageRepository.UpdatePage(secondaryPage); } - log += Log(siteGroup, $"Page Updated: {siteGroup.AliasName}{secondaryPage.Path}"); + log += Log(siteGroup, $"Page Updated: {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); secondaryPages.Remove(page); } } @@ -558,22 +558,22 @@ namespace Oqtane.Infrastructure if (module == null) { // add new module - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { module = moduleRepository.AddModule(secondaryPageModule.Module); updateContent = true; } - log += Log(siteGroup, $"Module Added: {module.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + log += Log(siteGroup, $"Module Added: {module.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); } if (module != null) { secondaryPageModule.ModuleId = module.ModuleId; secondaryPageModule.Module = null; // remove tracking - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); } - log += Log(siteGroup, $"Page Module Added: {module.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + log += Log(siteGroup, $"Page Module Added: {module.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); secondaryPageModule.Module = module; } } @@ -582,20 +582,20 @@ namespace Oqtane.Infrastructure // update existing module if (primaryPageModule.Module.ModifiedOn > siteGroup.SynchronizedOn) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { moduleRepository.UpdateModule(secondaryPageModule.Module); updateContent = true; } - log += Log(siteGroup, $"Module Updated: {secondaryPageModule.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + log += Log(siteGroup, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); } if (primaryPageModule.ModifiedOn > siteGroup.SynchronizedOn) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); } - log += Log(siteGroup, $"Page Module Updated: {secondaryPageModule.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + log += Log(siteGroup, $"Page Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); secondaryPageModules.Remove(pageModule); } } @@ -613,11 +613,11 @@ namespace Oqtane.Infrastructure var secondaryModuleContent = ((ISynchronizable)moduleObject).ExtractModule(secondaryPageModule.Module); if (primaryModuleContent != secondaryModuleContent) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, primaryModuleContent, primaryPageModule.Module.ModuleDefinition.Version); } - log += Log(siteGroup, $"Module Content Updated: {secondaryPageModule.Title} - {siteGroup.AliasName}{secondaryPage.Path}"); + log += Log(siteGroup, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); } } catch @@ -648,22 +648,22 @@ namespace Oqtane.Infrastructure } if (!primaryPageModules.Any(item => item.PageId == primaryPageId && item.Module.ModuleDefinitionName == secondaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == secondaryPageModule.Title.ToLower())) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); } - log += Log(siteGroup, $"Page Module Deleted: {secondaryPageModule.Title} - {siteGroup.AliasName}{secondaryPageModule.Page.Path}"); + log += Log(siteGroup, $"Page Module Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPageModule.Page.Path)}"); } } // remove pages in the secondary site which do not exist in the primary site foreach (var secondaryPage in secondaryPages.Where(item => !primaryPages.Select(item => item.Path).Contains(item.Path))) { - if (siteGroup.Synchronize) + if (siteGroup.SiteGroupDefinition.Synchronization.Value) { pageRepository.DeletePage(secondaryPage.PageId); } - log += Log(siteGroup, $"Page Deleted: {siteGroup.AliasName}{secondaryPage.Path}"); + log += Log(siteGroup, $"Page Deleted: {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); } if (siteGroup.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) @@ -710,7 +710,7 @@ namespace Oqtane.Infrastructure secondarySetting.SettingName = primarySetting.SettingName; secondarySetting.SettingValue = primarySetting.SettingValue; secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (siteGroup.SiteGroupDefinition.Synchronization.Value && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.AddSetting(secondarySetting); updated = true; @@ -722,7 +722,7 @@ namespace Oqtane.Infrastructure { secondarySetting.SettingValue = primarySetting.SettingValue; secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (siteGroup.SiteGroupDefinition.Synchronization.Value && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.UpdateSetting(secondarySetting); updated = true; @@ -735,7 +735,7 @@ namespace Oqtane.Infrastructure // any remaining secondary settings need to be deleted foreach (var secondarySetting in secondarySettings) { - if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (siteGroup.SiteGroupDefinition.Synchronization.Value && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.DeleteSetting(secondarySetting.EntityName, secondarySetting.SettingId); updated = true; @@ -750,12 +750,12 @@ namespace Oqtane.Infrastructure return log; } - private void SendNotifications(IServiceProvider provider, int siteId, string siteName, string roleName, string log) + private void SendNotifications(IServiceProvider provider, int siteId, string siteName, string log) { var userRoleRepository = provider.GetRequiredService(); var notificationRepository = provider.GetRequiredService(); - foreach (var userRole in userRoleRepository.GetUserRoles(roleName, siteId)) + foreach (var userRole in userRoleRepository.GetUserRoles(RoleNames.Admin, siteId)) { var notification = new Notification(siteId, userRole.User, $"{siteName} Change Log", log); notificationRepository.AddNotification(notification); @@ -774,5 +774,10 @@ namespace Oqtane.Infrastructure return ""; } } + + private string CreateLink(string url) + { + return "" + url + ""; + } } } diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs index 72974ecb..180e896d 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs @@ -24,7 +24,8 @@ namespace Oqtane.Migrations.EntityBuilders SiteGroupDefinitionId = AddAutoIncrementColumn(table, "SiteGroupDefinitionId"); Name = AddStringColumn(table, "Name", 200); PrimarySiteId = AddIntegerColumn(table, "PrimarySiteId"); - Synchronization = AddBooleanColumn(table, "Synchronization"); + Synchronization = AddBooleanColumn(table, "Synchronization", true); + Notify = AddBooleanColumn(table, "Notify"); Synchronize = AddBooleanColumn(table, "Synchronize"); Localization = AddBooleanColumn(table, "Localization"); @@ -41,6 +42,8 @@ namespace Oqtane.Migrations.EntityBuilders public OperationBuilder Synchronization { get; set; } + public OperationBuilder Notify { get; set; } + public OperationBuilder Synchronize { get; set; } public OperationBuilder Localization { get; set; } diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs index ed063737..e9e385ce 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs @@ -28,8 +28,6 @@ namespace Oqtane.Migrations.EntityBuilders SiteGroupId = AddAutoIncrementColumn(table, "SiteGroupId"); SiteGroupDefinitionId = AddIntegerColumn(table, "SiteGroupDefinitionId"); SiteId = AddIntegerColumn(table, "SiteId"); - Synchronize = AddBooleanColumn(table, "Synchronize"); - NotifyRoleName = AddStringColumn(table, "NotifyRoleName", 256, true); SynchronizedOn = AddDateTimeColumn(table, "SynchronizedOn", true); AddAuditableColumns(table); @@ -43,10 +41,6 @@ namespace Oqtane.Migrations.EntityBuilders public OperationBuilder SiteId { get; set; } - public OperationBuilder Synchronize { get; set; } - - public OperationBuilder NotifyRoleName { get; set; } - public OperationBuilder SynchronizedOn { get; set; } } } diff --git a/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs b/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs index 42255ce8..a8e767c2 100644 --- a/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs +++ b/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs @@ -21,6 +21,7 @@ namespace Oqtane.Migrations.Tenant var siteGroupEntityBuilder = new SiteGroupEntityBuilder(migrationBuilder, ActiveDatabase); siteGroupEntityBuilder.Create(); + siteGroupEntityBuilder.AddIndex("IX_SiteGroup", new[] { "SiteId", "SiteGroupDefinitionId" }, true); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Oqtane.Server/Repository/SiteGroupRepository.cs b/Oqtane.Server/Repository/SiteGroupRepository.cs index 644cec0c..392f83e7 100644 --- a/Oqtane.Server/Repository/SiteGroupRepository.cs +++ b/Oqtane.Server/Repository/SiteGroupRepository.cs @@ -11,9 +11,9 @@ namespace Oqtane.Repository IEnumerable GetSiteGroups(int siteId, int siteGroupDefinitionId); SiteGroup AddSiteGroup(SiteGroup siteGroup); SiteGroup UpdateSiteGroup(SiteGroup siteGroup); - SiteGroup GetSiteGroup(int siteSiteGroupDefinitionId); - SiteGroup GetSiteGroup(int siteSiteGroupDefinitionId, bool tracking); - void DeleteSiteGroup(int siteSiteGroupDefinitionId); + SiteGroup GetSiteGroup(int siteSiteGroupId); + SiteGroup GetSiteGroup(int siteSiteGroupId, bool tracking); + void DeleteSiteGroup(int siteSiteGroupId); } public class SiteGroupRepository : ISiteGroupRepository @@ -55,32 +55,32 @@ namespace Oqtane.Repository return SiteGroup; } - public SiteGroup GetSiteGroup(int SiteGroupDefinitionId) + public SiteGroup GetSiteGroup(int SiteGroupId) { - return GetSiteGroup(SiteGroupDefinitionId, true); + return GetSiteGroup(SiteGroupId, true); } - public SiteGroup GetSiteGroup(int SiteGroupDefinitionId, bool tracking) + public SiteGroup GetSiteGroup(int SiteGroupId, bool tracking) { using var db = _dbContextFactory.CreateDbContext(); if (tracking) { return db.SiteGroup .Include(item => item.SiteGroupDefinition) // eager load - .FirstOrDefault(item => item.SiteGroupDefinitionId == SiteGroupDefinitionId); + .FirstOrDefault(item => item.SiteGroupId == SiteGroupId); } else { return db.SiteGroup.AsNoTracking() .Include(item => item.SiteGroupDefinition) // eager load - .FirstOrDefault(item => item.SiteGroupDefinitionId == SiteGroupDefinitionId); + .FirstOrDefault(item => item.SiteGroupId == SiteGroupId); } } - public void DeleteSiteGroup(int SiteGroupDefinitionId) + public void DeleteSiteGroup(int SiteGroupId) { using var db = _dbContextFactory.CreateDbContext(); - SiteGroup SiteGroup = db.SiteGroup.Find(SiteGroupDefinitionId); + SiteGroup SiteGroup = db.SiteGroup.Find(SiteGroupId); db.SiteGroup.Remove(SiteGroup); db.SaveChanges(); } diff --git a/Oqtane.Shared/Models/SiteGroup.cs b/Oqtane.Shared/Models/SiteGroup.cs index dffc30b6..2668f55e 100644 --- a/Oqtane.Shared/Models/SiteGroup.cs +++ b/Oqtane.Shared/Models/SiteGroup.cs @@ -20,16 +20,6 @@ namespace Oqtane.Models /// public int SiteId { get; set; } - /// - /// Indicates if content should be synchronized for the site (false = compare, true = update) - /// - public bool Synchronize { get; set; } - - /// - /// The role who should be notified of changes to the site (non-primary site only) - /// - public string NotifyRoleName { get; set; } - /// /// The last date/time the site was synchronized /// diff --git a/Oqtane.Shared/Models/SiteGroupDefinition.cs b/Oqtane.Shared/Models/SiteGroupDefinition.cs index 8436ed68..a5e91a7b 100644 --- a/Oqtane.Shared/Models/SiteGroupDefinition.cs +++ b/Oqtane.Shared/Models/SiteGroupDefinition.cs @@ -18,9 +18,14 @@ namespace Oqtane.Models public int PrimarySiteId { get; set; } /// - /// Indicates if the group supports synchronization + /// Indicates if the group supports synchronization (null = no, false = compare, true = update) /// - public bool Synchronization { get; set; } + public bool? Synchronization { get; set; } + + /// + /// Indicates if the site administrator should be notified of any synchronization activity + /// + public bool Notify { get; set; } /// /// Specifies if the group needs to be synchronized From 8a4275c24050b69fcaa994f120bde086888e9e4a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 28 Jan 2026 13:59:35 -0500 Subject: [PATCH 07/75] add last synchronization date --- Oqtane.Client/Modules/Admin/Site/Index.razor | 60 +++++++++++-------- .../Resources/Modules/Admin/Site/Index.resx | 12 +++- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index e85de2a2..ae9d8f50 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -489,17 +489,20 @@
-
- -
- + @if (_synchronization != "False") + { +
+ +
+ +
-
+ }
- +
- @foreach (var site in _sites) { @@ -525,7 +527,7 @@
} - @if (_siteGroupDefinitionId != -1 && _siteId != -1) + @if (_siteGroupDefinitionId != -1) {
@@ -537,6 +539,15 @@
+ @if (_member == "Secondary") + { +
+ +
+ +
+
+ } }
@@ -666,12 +677,13 @@ private List _siteGroupDefinitions = new List(); private List _sites = new List(); private int _siteGroupDefinitionId = -1; - private int _siteId = -1; + private int _siteId; private string _groupName = string.Empty; private string _synchronization = "Update"; private string _notify = "True"; private string _localization = "False"; private string _member = "Primary"; + private string _synchronized = string.Empty; private bool _addSiteGroupDefinition = false; private string _tenant = string.Empty; @@ -1290,6 +1302,7 @@ if (siteGroup.SiteId == _siteId) { _member = site.Fingerprint; + _synchronized = UtcToLocal(siteGroup.SynchronizedOn).ToString(); } } } @@ -1301,24 +1314,22 @@ private async void SiteChanged(ChangeEventArgs e) { _siteId = int.Parse(e.Value.ToString()); - if (_siteId != -1) + var siteGroup = await SiteGroupService.GetSiteGroupAsync(_siteId, _siteGroupDefinitionId); + if (siteGroup != null) { - var siteGroup = await SiteGroupService.GetSiteGroupAsync(_siteId, _siteGroupDefinitionId); - if (siteGroup != null) + if (siteGroup.SiteGroupDefinition.PrimarySiteId == _siteId) { - if (siteGroup.SiteGroupDefinition.PrimarySiteId == _siteId) - { - _member = "Primary"; - } - else - { - _member = "Secondary"; - } + _member = "Primary"; } else { - _member = "False"; + _member = "Secondary"; } + _synchronized = UtcToLocal(siteGroup.SynchronizedOn).ToString(); + } + else + { + _member = "False"; } StateHasChanged(); } @@ -1328,6 +1339,7 @@ _groupName = ""; _siteId = PageState.Site.SiteId; _member = "Primary"; + _synchronized = ""; _addSiteGroupDefinition = true; } diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index e7efa0bb..f0f8f414 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -526,7 +526,7 @@ Localization? - Specifies if each site that is part of the group contains content which is localized in a different language + Specifies if the content of the sites in the group are localized Primary @@ -559,12 +559,18 @@ Notify? - Specifies if the site administrator of secondary sites should be notified of any synchronization activity + Specifies if the administrators of secondary sites should be notified of any synchronization activity Site: - he sites in this tenant (database) + The sites in this tenant (database) + + + Synchronized: + + + The date/time of the last synchronization for the site \ No newline at end of file From ce905499b019a51a23a8483f910c1e30db93baa8 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 28 Jan 2026 19:06:18 -0500 Subject: [PATCH 08/75] refactor Site Groups --- Oqtane.Client/Modules/Admin/Site/Index.razor | 83 ++++++++++--------- .../Resources/Modules/Admin/Site/Index.resx | 8 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 48 +++++------ .../SiteGroupDefinitionEntityBuilder.cs | 5 +- .../EntityBuilders/SiteGroupEntityBuilder.cs | 6 ++ Oqtane.Shared/Models/SiteGroup.cs | 10 +++ Oqtane.Shared/Models/SiteGroupDefinition.cs | 9 +- 7 files changed, 94 insertions(+), 75 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index ae9d8f50..63b59984 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -484,23 +484,10 @@
- @if (_synchronization != "False") - { -
- -
- -
-
- }
@@ -539,14 +526,35 @@
- @if (_member == "Secondary") + @if (_member == "Secondary" && _synchronization == "True") {
- +
- +
+
+ +
+ +
+
+ @if (!string.IsNullOrEmpty(_synchronized)) + { +
+ +
+ +
+
+ } } }
@@ -594,7 +602,7 @@
- @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _siteGroupDefinitions.Any(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization != null)) + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _siteGroupDefinitions.Any(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) { } @@ -679,10 +687,11 @@ private int _siteGroupDefinitionId = -1; private int _siteId; private string _groupName = string.Empty; - private string _synchronization = "Update"; - private string _notify = "True"; + private string _synchronization = "True"; private string _localization = "False"; private string _member = "Primary"; + private string _synchronize = "True"; + private string _notify = "True"; private string _synchronized = string.Empty; private bool _addSiteGroupDefinition = false; @@ -1265,15 +1274,7 @@ if (group != null) { _groupName = group.Name; - if (group.Synchronization == null) - { - _synchronization = "False"; - } - else - { - _synchronization = (group.Synchronization.Value) ? "Update" : "Compare"; - } - _notify = group.Notify.ToString(); + _synchronization = group.Synchronization.ToString(); _localization = group.Localization.ToString(); if (_sites.Count == 0) @@ -1302,6 +1303,8 @@ if (siteGroup.SiteId == _siteId) { _member = site.Fingerprint; + _synchronize = siteGroup.Synchronize.ToString(); + _notify = siteGroup.Notify.ToString(); _synchronized = UtcToLocal(siteGroup.SynchronizedOn).ToString(); } } @@ -1347,10 +1350,6 @@ { SiteGroupDefinition siteGroupDefinition = null; - bool? synchronization = null; - if (_synchronization == "Compare") synchronization = false; - if (_synchronization == "Update") synchronization = true; - if (_siteGroupDefinitionId == -1) { if (!string.IsNullOrEmpty(_groupName)) @@ -1359,8 +1358,7 @@ { Name = _groupName, PrimarySiteId = _siteId, - Synchronization = synchronization, - Notify = bool.Parse(_notify), + Synchronization = bool.Parse(_synchronization), Localization = bool.Parse(_localization), Synchronize = false }; @@ -1374,8 +1372,7 @@ { siteGroupDefinition.Name = _groupName; siteGroupDefinition.PrimarySiteId = (_member == "Primary") ? _siteId : siteGroupDefinition.PrimarySiteId; - siteGroupDefinition.Synchronization = synchronization; - siteGroupDefinition.Notify = bool.Parse(_notify); + siteGroupDefinition.Synchronization = bool.Parse(_synchronization); siteGroupDefinition.Localization = bool.Parse(_localization); siteGroupDefinition = await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(siteGroupDefinition); } @@ -1393,7 +1390,9 @@ siteGroup = new SiteGroup { SiteGroupDefinitionId = siteGroupDefinition.SiteGroupDefinitionId, - SiteId = _siteId + SiteId = _siteId, + Synchronize = bool.Parse(_synchronize), + Notify = bool.Parse(_notify) }; await SiteGroupService.AddSiteGroupAsync(siteGroup); } @@ -1403,6 +1402,12 @@ { await SiteGroupService.DeleteSiteGroupAsync(siteGroup.SiteGroupId); } + else + { + siteGroup.Synchronize = bool.Parse(_synchronize); + siteGroup.Notify = bool.Parse(_notify); + await SiteGroupService.UpdateSiteGroupAsync(siteGroup); + } } await LoadSiteGroups(); @@ -1452,7 +1457,7 @@ } // mark secondary sites for synchronization - foreach (var group in _siteGroupDefinitions.Where(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization != null)) + foreach (var group in _siteGroupDefinitions.Where(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) { group.Synchronize = true; await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(group); diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index f0f8f414..b653285a 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -559,7 +559,7 @@ Notify? - Specifies if the administrators of secondary sites should be notified of any synchronization activity + Specifies if site administrators should be notified of any synchronization activity Site: @@ -573,4 +573,10 @@ The date/time of the last synchronization for the site + + Synchronize? + + + Specifies the synchronization approach between the primary site and the selected site + \ No newline at end of file diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 7c3a0d8c..ef90905a 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -53,7 +53,7 @@ namespace Oqtane.Infrastructure var groups = siteGroupDefinitionRepository.GetSiteGroupDefinitions(); // iterate through groups which need to be synchronized - foreach (var group in groups.Where(item => item.Synchronization != null && item.Synchronize)) + foreach (var group in groups.Where(item => item.Synchronization && item.Synchronize)) { // get data if (siteGroups == null) @@ -177,7 +177,7 @@ namespace Oqtane.Infrastructure secondarySite.DeletedOn = primarySite.DeletedOn; var siteRepository = provider.GetRequiredService(); - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { siteRepository.UpdateSite(secondarySite); } @@ -195,7 +195,7 @@ namespace Oqtane.Infrastructure syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySite.SiteId, SyncEventActions.Refresh); } - if (!string.IsNullOrEmpty(log) && siteGroup.SiteGroupDefinition.Notify) + if (!string.IsNullOrEmpty(log) && siteGroup.Notify) { // send change log to administrators SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, log); @@ -244,7 +244,7 @@ namespace Oqtane.Infrastructure if (role == null) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { roleRepository.AddRole(secondaryRole); } @@ -252,7 +252,7 @@ namespace Oqtane.Infrastructure } else { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { roleRepository.UpdateRole(secondaryRole); } @@ -265,7 +265,7 @@ namespace Oqtane.Infrastructure // remove roles in the secondary site which do not exist in the primary site foreach (var secondaryRole in secondaryRoles.Where(item => !primaryRoles.Select(item => item.Name).Contains(item.Name))) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { roleRepository.DeleteRole(secondaryRole.RoleId); } @@ -323,7 +323,7 @@ namespace Oqtane.Infrastructure if (folder == null) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { folderRepository.AddFolder(secondaryFolder); } @@ -331,7 +331,7 @@ namespace Oqtane.Infrastructure } else { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { folderRepository.UpdateFolder(secondaryFolder); } @@ -370,7 +370,7 @@ namespace Oqtane.Infrastructure if (file == null) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { fileRepository.AddFile(secondaryFile); ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); @@ -379,7 +379,7 @@ namespace Oqtane.Infrastructure } else { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { fileRepository.UpdateFile(secondaryFile); ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); @@ -393,7 +393,7 @@ namespace Oqtane.Infrastructure // remove files in the secondary site which do not exist in the primary site foreach (var secondaryFile in secondaryFiles.Where(item => !primaryFiles.Select(item => item.Name).Contains(item.Name))) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { fileRepository.DeleteFile(secondaryFile.FileId); var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); @@ -406,7 +406,7 @@ namespace Oqtane.Infrastructure // remove folders in the secondary site which do not exist in the primary site foreach (var secondaryFolder in secondaryFolders.Where(item => !primaryFolders.Select(item => item.Path).Contains(item.Path))) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { folderRepository.DeleteFolder(secondaryFolder.FolderId); } @@ -490,7 +490,7 @@ namespace Oqtane.Infrastructure if (page == null) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { secondaryPage = pageRepository.AddPage(secondaryPage); } @@ -498,7 +498,7 @@ namespace Oqtane.Infrastructure } else { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { secondaryPage = pageRepository.UpdatePage(secondaryPage); } @@ -558,7 +558,7 @@ namespace Oqtane.Infrastructure if (module == null) { // add new module - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { module = moduleRepository.AddModule(secondaryPageModule.Module); updateContent = true; @@ -569,7 +569,7 @@ namespace Oqtane.Infrastructure { secondaryPageModule.ModuleId = module.ModuleId; secondaryPageModule.Module = null; // remove tracking - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); } @@ -582,7 +582,7 @@ namespace Oqtane.Infrastructure // update existing module if (primaryPageModule.Module.ModifiedOn > siteGroup.SynchronizedOn) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { moduleRepository.UpdateModule(secondaryPageModule.Module); updateContent = true; @@ -591,7 +591,7 @@ namespace Oqtane.Infrastructure } if (primaryPageModule.ModifiedOn > siteGroup.SynchronizedOn) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); } @@ -613,7 +613,7 @@ namespace Oqtane.Infrastructure var secondaryModuleContent = ((ISynchronizable)moduleObject).ExtractModule(secondaryPageModule.Module); if (primaryModuleContent != secondaryModuleContent) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, primaryModuleContent, primaryPageModule.Module.ModuleDefinition.Version); } @@ -648,7 +648,7 @@ namespace Oqtane.Infrastructure } if (!primaryPageModules.Any(item => item.PageId == primaryPageId && item.Module.ModuleDefinitionName == secondaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == secondaryPageModule.Title.ToLower())) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); } @@ -659,7 +659,7 @@ namespace Oqtane.Infrastructure // remove pages in the secondary site which do not exist in the primary site foreach (var secondaryPage in secondaryPages.Where(item => !primaryPages.Select(item => item.Path).Contains(item.Path))) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value) + if (siteGroup.Synchronize) { pageRepository.DeletePage(secondaryPage.PageId); } @@ -710,7 +710,7 @@ namespace Oqtane.Infrastructure secondarySetting.SettingName = primarySetting.SettingName; secondarySetting.SettingValue = primarySetting.SettingValue; secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (siteGroup.SiteGroupDefinition.Synchronization.Value && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.AddSetting(secondarySetting); updated = true; @@ -722,7 +722,7 @@ namespace Oqtane.Infrastructure { secondarySetting.SettingValue = primarySetting.SettingValue; secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (siteGroup.SiteGroupDefinition.Synchronization.Value && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.UpdateSetting(secondarySetting); updated = true; @@ -735,7 +735,7 @@ namespace Oqtane.Infrastructure // any remaining secondary settings need to be deleted foreach (var secondarySetting in secondarySettings) { - if (siteGroup.SiteGroupDefinition.Synchronization.Value && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.DeleteSetting(secondarySetting.EntityName, secondarySetting.SettingId); updated = true; diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs index 180e896d..72974ecb 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs @@ -24,8 +24,7 @@ namespace Oqtane.Migrations.EntityBuilders SiteGroupDefinitionId = AddAutoIncrementColumn(table, "SiteGroupDefinitionId"); Name = AddStringColumn(table, "Name", 200); PrimarySiteId = AddIntegerColumn(table, "PrimarySiteId"); - Synchronization = AddBooleanColumn(table, "Synchronization", true); - Notify = AddBooleanColumn(table, "Notify"); + Synchronization = AddBooleanColumn(table, "Synchronization"); Synchronize = AddBooleanColumn(table, "Synchronize"); Localization = AddBooleanColumn(table, "Localization"); @@ -42,8 +41,6 @@ namespace Oqtane.Migrations.EntityBuilders public OperationBuilder Synchronization { get; set; } - public OperationBuilder Notify { get; set; } - public OperationBuilder Synchronize { get; set; } public OperationBuilder Localization { get; set; } diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs index e9e385ce..a6663315 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs @@ -28,6 +28,8 @@ namespace Oqtane.Migrations.EntityBuilders SiteGroupId = AddAutoIncrementColumn(table, "SiteGroupId"); SiteGroupDefinitionId = AddIntegerColumn(table, "SiteGroupDefinitionId"); SiteId = AddIntegerColumn(table, "SiteId"); + Synchronize = AddBooleanColumn(table, "Synchronize"); + Notify = AddBooleanColumn(table, "Notify"); SynchronizedOn = AddDateTimeColumn(table, "SynchronizedOn", true); AddAuditableColumns(table); @@ -41,6 +43,10 @@ namespace Oqtane.Migrations.EntityBuilders public OperationBuilder SiteId { get; set; } + public OperationBuilder Synchronize { get; set; } + + public OperationBuilder Notify { get; set; } + public OperationBuilder SynchronizedOn { get; set; } } } diff --git a/Oqtane.Shared/Models/SiteGroup.cs b/Oqtane.Shared/Models/SiteGroup.cs index 2668f55e..e6edc2e5 100644 --- a/Oqtane.Shared/Models/SiteGroup.cs +++ b/Oqtane.Shared/Models/SiteGroup.cs @@ -20,6 +20,16 @@ namespace Oqtane.Models /// public int SiteId { get; set; } + /// + /// Specifies the site synchronization approach (false = compare, true = update) + /// + public bool Synchronize { get; set; } + + /// + /// Indicates if the site administrator should be notified of any synchronization activity + /// + public bool Notify { get; set; } + /// /// The last date/time the site was synchronized /// diff --git a/Oqtane.Shared/Models/SiteGroupDefinition.cs b/Oqtane.Shared/Models/SiteGroupDefinition.cs index a5e91a7b..4db114e4 100644 --- a/Oqtane.Shared/Models/SiteGroupDefinition.cs +++ b/Oqtane.Shared/Models/SiteGroupDefinition.cs @@ -18,14 +18,9 @@ namespace Oqtane.Models public int PrimarySiteId { get; set; } /// - /// Indicates if the group supports synchronization (null = no, false = compare, true = update) + /// Indicates if the group supports synchronization /// - public bool? Synchronization { get; set; } - - /// - /// Indicates if the site administrator should be notified of any synchronization activity - /// - public bool Notify { get; set; } + public bool Synchronization { get; set; } /// /// Specifies if the group needs to be synchronized From 738ad9bbfa02b05f4c660863a7b96b88333d9c4f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 29 Jan 2026 10:59:31 -0500 Subject: [PATCH 09/75] resolve issue replicating files --- Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index ef90905a..c032da7b 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -420,6 +420,10 @@ namespace Oqtane.Infrastructure { var primaryPath = Path.Combine(folderRepository.GetFolderPath(primaryFolder), primaryFile.Name); var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); + if (!Directory.Exists(Path.GetDirectoryName(secondaryPath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(secondaryPath)); + } System.IO.File.Copy(primaryPath, secondaryPath, true); } From d6458eeaf6f0848e226a54f48b19a4241ef95664 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 29 Jan 2026 11:12:45 -0500 Subject: [PATCH 10/75] add defensive logic --- .../Infrastructure/Jobs/SynchronizationJob.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index c032da7b..5ef48827 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -419,12 +419,15 @@ namespace Oqtane.Infrastructure private void ReplicateFile(IFolderRepository folderRepository, Folder primaryFolder, Models.File primaryFile, Folder secondaryFolder, Models.File secondaryFile) { var primaryPath = Path.Combine(folderRepository.GetFolderPath(primaryFolder), primaryFile.Name); - var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); - if (!Directory.Exists(Path.GetDirectoryName(secondaryPath))) + if (System.IO.File.Exists(primaryPath)) { - Directory.CreateDirectory(Path.GetDirectoryName(secondaryPath)); + var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); + if (!Directory.Exists(Path.GetDirectoryName(secondaryPath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(secondaryPath)); + } + System.IO.File.Copy(primaryPath, secondaryPath, true); } - System.IO.File.Copy(primaryPath, secondaryPath, true); } private string ReplicatePages(IServiceProvider provider, ISettingRepository settingRepository, ITenantManager tenantManager, SiteGroup siteGroup, int primarySiteId, int secondarySiteId) @@ -577,7 +580,7 @@ namespace Oqtane.Infrastructure { secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); } - log += Log(siteGroup, $"Page Module Added: {module.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + log += Log(siteGroup, $"Module Instance Added: {module.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); secondaryPageModule.Module = module; } } @@ -599,7 +602,7 @@ namespace Oqtane.Infrastructure { secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); } - log += Log(siteGroup, $"Page Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + log += Log(siteGroup, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); secondaryPageModules.Remove(pageModule); } } @@ -656,7 +659,7 @@ namespace Oqtane.Infrastructure { pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); } - log += Log(siteGroup, $"Page Module Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPageModule.Page.Path)}"); + log += Log(siteGroup, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPageModule.Page.Path)}"); } } From 81f4f87493f3d0a9b69b4a36c3aaaba2e5abb624 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 29 Jan 2026 16:38:45 -0500 Subject: [PATCH 11/75] fix #5951 change order of operations when saving module settings --- .../Modules/Admin/Modules/Settings.razor | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Settings.razor b/Oqtane.Client/Modules/Admin/Modules/Settings.razor index 297a365a..82ca6a69 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Settings.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Settings.razor @@ -276,15 +276,39 @@ var interop = new Interop(JSRuntime); if (await interop.FormValid(form)) { - if (!string.IsNullOrEmpty(_title)) { if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate)) { AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning); return; - } + } + + // update module settings first + if (_moduleSettingsType != null) + { + if (_moduleSettings is ISettingsControl moduleSettingsControl) + { + // module settings updated using explicit interface + await moduleSettingsControl.UpdateSettings(); + } + else + { + // legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component ) + _moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null); + } + } + + // update container settings + if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl) + { + await containerSettingsControl.UpdateSettings(); + } + + // update page module var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId); + var pageId = pagemodule.PageId; // preserve + var pane = pagemodule.Pane; // preserve pagemodule.PageId = int.Parse(_pageId); pagemodule.Title = _title; pagemodule.Pane = _pane; @@ -302,33 +326,21 @@ pagemodule.Header = _header; pagemodule.Footer = _footer; await PageModuleService.UpdatePageModuleAsync(pagemodule); - await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); + // update page module order if page or pane changed + if (pageId != pagemodule.PageId || pane != pagemodule.Pane) + { + await PageModuleService.UpdatePageModuleOrderAsync(pageId, pane); // old page/pane + await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); // new page/pane + } + + // update module var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId); module.AllPages = bool.Parse(_allPages); module.PageModuleId = ModuleState.PageModuleId; module.PermissionList = _permissionGrid.GetPermissionList(); await ModuleService.UpdateModuleAsync(module); - if (_moduleSettingsType != null) - { - if (_moduleSettings is ISettingsControl moduleSettingsControl) - { - // module settings updated using explicit interface - await moduleSettingsControl.UpdateSettings(); - } - else - { - // legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component ) - _moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null); - } - } - - if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl) - { - await containerSettingsControl.UpdateSettings(); - } - NavigationManager.NavigateTo(PageState.ReturnUrl); } else From d638f9492d6452501ed8a7d801b562830142c51d Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 30 Jan 2026 14:03:21 +0100 Subject: [PATCH 12/75] Move I[Module]Service to Shared Relocate the I[Module]Service interface from the client Services file to a new Shared/Interfaces/I[Module]Service.cs so the interface becomes a shared contract. Remove the duplicate interface from the client-side [Module]Service implementation. It was move in error, release 6.1.5 PR add a new Visual Studio Project Template #5493 --- Oqtane.Server/Oqtane.Server.csproj | 5 +++++ .../Client/Services/[Module]Service.cs | 12 ------------ .../Shared/Interfaces/I[Module]Service.cs | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 Oqtane.Server/wwwroot/Modules/Templates/External/Shared/Interfaces/I[Module]Service.cs diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 4ba5c66c..446622cf 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -20,6 +20,7 @@ + @@ -78,4 +79,8 @@ + + + + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs index d433d698..24046476 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs @@ -7,18 +7,6 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public interface I[Module]Service - { - Task> Get[Module]sAsync(int ModuleId); - - Task Get[Module]Async(int [Module]Id, int ModuleId); - - Task Add[Module]Async(Models.[Module] [Module]); - - Task Update[Module]Async(Models.[Module] [Module]); - - Task Delete[Module]Async(int [Module]Id, int ModuleId); - } public class [Module]Service : ServiceBase, I[Module]Service { diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/Interfaces/I[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/Interfaces/I[Module]Service.cs new file mode 100644 index 00000000..8c75bf44 --- /dev/null +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/Interfaces/I[Module]Service.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace [Owner].Module.[Module].Services +{ + public interface I[Module]Service + { + Task> Get[Module]sAsync(int ModuleId); + + Task Get[Module]Async(int [Module]Id, int ModuleId); + + Task Add[Module]Async(Models.[Module] [Module]); + + Task Update[Module]Async(Models.[Module] [Module]); + + Task Delete[Module]Async(int [Module]Id, int ModuleId); + } +} From 7ab1184a04aef7b4f0a92b66bf0df6c7c10f27df Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 30 Jan 2026 09:00:58 -0500 Subject: [PATCH 13/75] fix #5988 - add inline script support to static rendering --- Oqtane.Client/UI/SiteRouter.razor | 4 +-- Oqtane.Client/UI/ThemeBuilder.razor | 34 ------------------- Oqtane.Server/Components/App.razor | 30 ++++++++++------ .../Repository/ModuleDefinitionRepository.cs | 2 +- Oqtane.Server/Repository/ThemeRepository.cs | 2 +- Oqtane.Shared/Models/Resource.cs | 2 +- Oqtane.Shared/Shared/Utilities.cs | 4 +-- 7 files changed, 27 insertions(+), 51 deletions(-) diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 64e2f59d..275a8829 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -625,11 +625,11 @@ { if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site) { - if (resource.Url.StartsWith("~")) + if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~")) { resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); } - if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) + if (!string.IsNullOrEmpty(resource.Url) && !resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) { resource.Url = alias.BaseUrl + resource.Url; } diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index d22b21b4..3797dd05 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -227,38 +227,4 @@ } return stylesheets; } - - private string ManageScripts(List resources, Alias alias) - { - var scripts = ""; - if (resources != null) - { - foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Script && item.Location == ResourceLocation.Head)) - { - var script = CreateScript(resource, alias); - if (!scripts.Contains(script, StringComparison.OrdinalIgnoreCase)) - { - scripts += script + Environment.NewLine; - } - } - } - return scripts; - } - - private string CreateScript(Resource resource, Alias alias) - { - if (!string.IsNullOrEmpty(resource.Url)) - { - var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; - return ""; - } - else - { - // inline script - return ""; - } - } } diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 666dcef1..c717172d 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -543,8 +543,6 @@ } else { - var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; - var dataAttributes = ""; if (!resource.DataAttributes.ContainsKey("data-reload")) { @@ -566,12 +564,24 @@ } } - return ""; + if (!string.IsNullOrEmpty(resource.Url)) + { + var url = (string.IsNullOrEmpty(resource.Url) || resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; + + return ""; + } + else + { + return "" + resource.Content + ""; + } } } @@ -744,11 +754,11 @@ { if (rendermode == RenderModes.Static || resource.ResourceType == ResourceType.Stylesheet || resource.Level == ResourceLevel.Site) { - if (resource.Url.StartsWith("~")) + if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~")) { resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); } - if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) + if (!string.IsNullOrEmpty(resource.Url) && !resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) { resource.Url = alias.BaseUrl + resource.Url; } diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index d8bb7f4e..c2821a8b 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -364,7 +364,7 @@ namespace Oqtane.Repository { foreach (var resource in moduledefinition.Resources) { - if (resource.Url.StartsWith("~")) + if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~")) { resource.Url = resource.Url.Replace("~", "/Modules/" + Utilities.GetTypeName(moduledefinition.ModuleDefinitionName) + "/").Replace("//", "/"); } diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index ba901dd9..8962402c 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -329,7 +329,7 @@ namespace Oqtane.Repository { foreach (var resource in theme.Resources) { - if (resource.Url.StartsWith("~")) + if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~")) { resource.Url = resource.Url.Replace("~", "/Themes/" + Utilities.GetTypeName(theme.ThemeName) + "/").Replace("//", "/"); } diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index 42ecb5f9..ec0e4477 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -24,7 +24,7 @@ namespace Oqtane.Models get => _url; set { - _url = (value.Contains("://")) ? value : (!value.StartsWith("/") && !value.StartsWith("~") ? "/" : "") + value; + _url = (string.IsNullOrEmpty(value) || value.Contains("://")) ? value : (!value.StartsWith("/") && !value.StartsWith("~") ? "/" : "") + value; } } diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 45888f0c..ca710771 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -501,11 +501,11 @@ namespace Oqtane.Shared public static string GetUrlPath(string url) { - if (url.Contains("?")) + if (!string.IsNullOrEmpty(url) && url.Contains("?")) { url = url.Substring(0, url.IndexOf("?")); } - return url; + return url ?? ""; } public static string LogMessage(object @class, string message) From 8ec4922cd33325949aedf83fcb05eb9e02705ee8 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 30 Jan 2026 09:17:13 -0500 Subject: [PATCH 14/75] change order of operations when saving in edit page --- Oqtane.Client/Modules/Admin/Pages/Edit.razor | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index acf99d1e..029680ce 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -576,6 +576,7 @@ await ScrollToPageTop(); return; } + if (!string.IsNullOrEmpty(_themetype) && _containertype != "-") { string currentPath = _page.Path; @@ -635,6 +636,13 @@ return; } + // update theme settings + if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl) + { + await themeSettingsControl.UpdateSettings(); + } + + // default page properties if (_insert != "=") { Page child; @@ -688,7 +696,10 @@ _page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions); } + // update page _page = await PageService.UpdatePageAsync(_page); + + // update page order await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, _page.ParentId); if (_currentparentid == string.Empty) { @@ -699,11 +710,6 @@ await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, int.Parse(_currentparentid)); } - if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl) - { - await themeSettingsControl.UpdateSettings(); - } - await logger.LogInformation("Page Saved {Page}", _page); if (!string.IsNullOrEmpty(PageState.ReturnUrl)) { From 563696c7a53031a01cb9c298ea5986b009ba05d3 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 30 Jan 2026 17:37:35 +0100 Subject: [PATCH 15/75] Removed the Reference to the External Client in the External Server project --- .../External/Server/[Owner].Module.[Module].Server.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj index 45389595..8fabc8ca 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -27,7 +27,6 @@ - From 0dc9382215212c19540594b0b0d795bb57ba8522 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 30 Jan 2026 17:59:29 +0100 Subject: [PATCH 16/75] Move I[Module]Service to Shared/Interfaces Extract the I[Module]Service interface out of Client/Services/[Module]Service.cs and add it as a new shared interface file at Internal/Shared/Interfaces/I[Module]Service.cs. The inline interface definition was removed from [Module]Service.cs so the service class now implements the shared contract, centralizing the interface for reuse across components. --- .../Client/Services/[Module]Service.cs | 12 ------------ .../Shared/Interfaces/I[Module]Service.cs | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Interfaces/I[Module]Service.cs diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs index d433d698..24046476 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs @@ -7,18 +7,6 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public interface I[Module]Service - { - Task> Get[Module]sAsync(int ModuleId); - - Task Get[Module]Async(int [Module]Id, int ModuleId); - - Task Add[Module]Async(Models.[Module] [Module]); - - Task Update[Module]Async(Models.[Module] [Module]); - - Task Delete[Module]Async(int [Module]Id, int ModuleId); - } public class [Module]Service : ServiceBase, I[Module]Service { diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Interfaces/I[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Interfaces/I[Module]Service.cs new file mode 100644 index 00000000..8c75bf44 --- /dev/null +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Interfaces/I[Module]Service.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace [Owner].Module.[Module].Services +{ + public interface I[Module]Service + { + Task> Get[Module]sAsync(int ModuleId); + + Task Get[Module]Async(int [Module]Id, int ModuleId); + + Task Add[Module]Async(Models.[Module] [Module]); + + Task Update[Module]Async(Models.[Module] [Module]); + + Task Delete[Module]Async(int [Module]Id, int ModuleId); + } +} From d313c2f3a51ae6616fb2f1607781cfd073406e5f Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Tue, 3 Feb 2026 19:14:51 +0100 Subject: [PATCH 17/75] Enable CopyLocalLockFileAssemblies in project file --- .../External/Shared/[Owner].Module.[Module].Shared.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/[Owner].Module.[Module].Shared.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/[Owner].Module.[Module].Shared.csproj index 63175d61..c27c1e59 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/[Owner].Module.[Module].Shared.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/[Owner].Module.[Module].Shared.csproj @@ -8,6 +8,7 @@ [Owner] [Description] [Owner] + true [Owner].Module.[Module].Shared.Oqtane From 8817af42bdc7eec9f0bb93bbe662477f3e94b25f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 5 Feb 2026 13:23:14 -0500 Subject: [PATCH 18/75] add new method for getting neutral cultures --- Oqtane.Client/Modules/Admin/Site/Index.razor | 2 +- Oqtane.Client/Services/LocalizationService.cs | 14 ++++++- .../Controllers/LocalizationController.cs | 41 ++++++++++++------- .../Infrastructure/LocalizationManager.cs | 11 ++++- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 63b59984..a87f6cf2 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -721,7 +721,7 @@ if (site != null) { _timezones = TimeZoneService.GetTimeZones(); - _cultures = await LocalizationService.GetCulturesAsync(false); + _cultures = await LocalizationService.GetNeutralCulturesAsync(); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); diff --git a/Oqtane.Client/Services/LocalizationService.cs b/Oqtane.Client/Services/LocalizationService.cs index 97948b40..53eb3abf 100644 --- a/Oqtane.Client/Services/LocalizationService.cs +++ b/Oqtane.Client/Services/LocalizationService.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Oqtane.Documentation; @@ -14,10 +13,16 @@ namespace Oqtane.Services public interface ILocalizationService { /// - /// Returns a collection of supported cultures + /// Returns a collection of supported or installed cultures /// /// Task> GetCulturesAsync(bool installed); + + /// + /// Returns a collection of neutral cultures + /// + /// + Task> GetNeutralCulturesAsync(); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -31,5 +36,10 @@ namespace Oqtane.Services { return await GetJsonAsync>($"{Apiurl}?installed={installed}"); } + + public async Task> GetNeutralCulturesAsync() + { + return await GetJsonAsync>($"{Apiurl}/neutral"); + } } } diff --git a/Oqtane.Server/Controllers/LocalizationController.cs b/Oqtane.Server/Controllers/LocalizationController.cs index 7dd1452d..9f52c714 100644 --- a/Oqtane.Server/Controllers/LocalizationController.cs +++ b/Oqtane.Server/Controllers/LocalizationController.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Mvc; -using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Models; -using Oqtane.Repository; using Oqtane.Shared; namespace Oqtane.Controllers @@ -15,33 +13,27 @@ namespace Oqtane.Controllers public class LocalizationController : Controller { private readonly ILocalizationManager _localizationManager; - private readonly ISiteRepository _siteRepository; - private readonly ISiteGroupRepository _siteGroupRepository; - private readonly IAliasRepository _aliasRepository; - public LocalizationController(ILocalizationManager localizationManager, ISiteRepository siteRepository, ISiteGroupRepository siteGroupRepository, IAliasRepository aliasRepository) + public LocalizationController(ILocalizationManager localizationManager) { _localizationManager = localizationManager; - _siteRepository = siteRepository; - _siteGroupRepository = siteGroupRepository; - _aliasRepository = aliasRepository; } - // GET: api/localization + // GET: api/localization?installed=true/false [HttpGet()] public IEnumerable Get(bool installed) { - string[] culturecodes; + string[] cultureCodes; if (installed) { - culturecodes = _localizationManager.GetInstalledCultures(); + cultureCodes = _localizationManager.GetInstalledCultures(); } else { - culturecodes = _localizationManager.GetSupportedCultures(); + cultureCodes = _localizationManager.GetSupportedCultures(); } - var cultures = culturecodes.Select(c => new Culture + var cultures = cultureCodes.Select(c => new Culture { Name = CultureInfo.GetCultureInfo(c).Name, DisplayName = CultureInfo.GetCultureInfo(c).DisplayName, @@ -56,5 +48,26 @@ namespace Oqtane.Controllers return cultures.OrderBy(item => item.DisplayName); } + + // GET: api/localization/neutral + [HttpGet("neutral")] + public IEnumerable Get() + { + var cultureCodes = _localizationManager.GetNeutralCultures(); + + var cultures = cultureCodes.Select(c => new Culture + { + Name = CultureInfo.GetCultureInfo(c).Name, + DisplayName = CultureInfo.GetCultureInfo(c).DisplayName, + IsDefault = false + }).ToList(); + + if (cultures.Count == 0) + { + cultures.Add(new Culture { Name = "en", DisplayName = "English", IsDefault = false }); + } + + return cultures.OrderBy(item => item.DisplayName); + } } } diff --git a/Oqtane.Server/Infrastructure/LocalizationManager.cs b/Oqtane.Server/Infrastructure/LocalizationManager.cs index 50bda160..9ffd2708 100644 --- a/Oqtane.Server/Infrastructure/LocalizationManager.cs +++ b/Oqtane.Server/Infrastructure/LocalizationManager.cs @@ -14,6 +14,7 @@ namespace Oqtane.Infrastructure string GetDefaultCulture(); string[] GetSupportedCultures(); string[] GetInstalledCultures(); + string[] GetNeutralCultures(); } public class LocalizationManager : ILocalizationManager @@ -42,7 +43,6 @@ namespace Oqtane.Infrastructure public string[] GetSupportedCultures() { return CultureInfo.GetCultures(CultureTypes.AllCultures) - .Where(item => item.Name.Length == 2) // major languages only (this could be configurable) .Select(item => item.Name).OrderBy(c => c).ToArray(); } @@ -51,7 +51,14 @@ namespace Oqtane.Infrastructure return GetSatelliteAssemblyCultures(); } - // method is static as it is called during startup + public string[] GetNeutralCultures() + { + return CultureInfo.GetCultures(CultureTypes.AllCultures) + .Where(item => item.IsNeutralCulture) + .Select(item => item.Name).OrderBy(c => c).ToArray(); + } + + // note: method is public and static as it is called during startup public static string[] GetSatelliteAssemblyCultures() { var cultures = new List(); From b2ef3cc5744fb8cd4942cb422f1ea256d97012b1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 5 Feb 2026 14:30:06 -0500 Subject: [PATCH 19/75] additional validation --- Oqtane.Client/Modules/Admin/Site/Index.razor | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index a87f6cf2..473ce664 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -533,7 +533,10 @@
@@ -1328,6 +1331,8 @@ { _member = "Secondary"; } + _synchronize = siteGroup.Synchronize.ToString(); + _notify = siteGroup.Notify.ToString(); _synchronized = UtcToLocal(siteGroup.SynchronizedOn).ToString(); } else From dff2261994984dc37f5693ebe83fb2cfae3189e9 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 5 Feb 2026 15:33:08 -0500 Subject: [PATCH 20/75] improve support for BodyContent --- Oqtane.Server/Components/App.razor | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index c717172d..093782c6 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -79,6 +79,7 @@ @((MarkupString)_scripts) @((MarkupString)_bodyResources) + @((MarkupString)_bodyContent) @if (_renderMode == RenderModes.Static) { @@ -107,6 +108,7 @@ private string _language = "en"; private string _headResources = ""; private string _bodyResources = ""; + private string _bodyContent = ""; private string _styleSheets = ""; private string _scripts = ""; private string _message = ""; @@ -190,6 +192,8 @@ var resources = await GetPageResources(alias, site, page, modules, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action); ManageStyleSheets(resources); ManageScripts(resources, alias); + AddBodyContent(site.BodyContent); + AddBodyContent(page.BodyContent); // generate scripts if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null) @@ -806,4 +810,23 @@ } } } + + private void AddBodyContent(string content) + { + if (!string.IsNullOrEmpty(content)) + { + var elements = content.Split('<', StringSplitOptions.RemoveEmptyEntries); + foreach (var element in elements) + { + if (_renderMode == RenderModes.Static || (!element.ToLower().StartsWith("script") && !element.ToLower().StartsWith("/script"))) + { + if (!_bodyContent.Contains("<" + element) || element.StartsWith("/")) + { + _bodyContent += "<" + element; + } + } + } + _bodyContent += "\n"; + } + } } From 57deeb6acf77ce9889b683181b8f472792baa2e6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 6 Feb 2026 11:53:10 -0500 Subject: [PATCH 21/75] improvements for site groups --- Oqtane.Client/Modules/Admin/Site/Index.razor | 55 +++++++++---------- .../Resources/Modules/Admin/Site/Index.resx | 3 - .../Controls/ControlPanelInteractive.resx | 5 +- .../Services/SiteGroupDefinitionService.cs | 13 ++++- .../Themes/Controls/Theme/ControlPanel.razor | 7 ++- .../Theme/ControlPanelInteractive.razor | 17 ++++++ .../Controls/Theme/LanguageSelector.razor | 37 ------------- .../Controls/Theme/LanguageSwitcher.razor | 24 +++++--- .../Themes/OqtaneTheme/Themes/Default.razor | 1 - .../SiteGroupDefinitionController.cs | 49 ++++++++++++----- .../Infrastructure/Jobs/SynchronizationJob.cs | 7 ++- Oqtane.Server/Services/SiteService.cs | 11 ++++ 12 files changed, 131 insertions(+), 98 deletions(-) delete mode 100644 Oqtane.Client/Themes/Controls/Theme/LanguageSelector.razor diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 473ce664..fce700f7 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -549,15 +549,18 @@
- @if (!string.IsNullOrEmpty(_synchronized)) - { -
- -
+
+ +
+
-
+ @if (!string.IsNullOrEmpty(_synchronized)) + { + + }
- } +
+
} }
@@ -605,10 +608,6 @@
- @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _siteGroupDefinitions.Any(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) - { - - }

@@ -1411,10 +1410,23 @@ { siteGroup.Synchronize = bool.Parse(_synchronize); siteGroup.Notify = bool.Parse(_notify); + siteGroup.SynchronizedOn = string.IsNullOrEmpty(_synchronized) ? null : siteGroup.SynchronizedOn; await SiteGroupService.UpdateSiteGroupAsync(siteGroup); } } + if (siteGroupDefinition.Synchronization) + { + // enable synchronization job if it is not enabled already + var jobs = await JobService.GetJobsAsync(); + var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server"); + if (job != null && !job.IsEnabled) + { + job.IsEnabled = true; + await JobService.UpdateJobAsync(job); + } + } + await LoadSiteGroups(); } else @@ -1450,25 +1462,8 @@ } } - private async Task SynchronizeSite() + private async Task ResetSiteGroup() { - // enable synchronization job if it is not enabled already - var jobs = await JobService.GetJobsAsync(); - var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server"); - if (job != null && !job.IsEnabled) - { - job.IsEnabled = true; - await JobService.UpdateJobAsync(job); - } - - // mark secondary sites for synchronization - foreach (var group in _siteGroupDefinitions.Where(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization)) - { - group.Synchronize = true; - await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(group); - } - - AddModuleMessage(Localizer["Message.Site.Synchronize"], MessageType.Success); - await ScrollToPageTop(); + _synchronized = ""; } } diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index b653285a..ce84a1f7 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -552,9 +552,6 @@ Site Submitted For Synchronization - - Synchronize - Notify? diff --git a/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx b/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx index 15c567fc..a4981464 100644 --- a/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx +++ b/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx @@ -200,5 +200,8 @@ Copy Existing Module - + + + Synchronize + \ No newline at end of file diff --git a/Oqtane.Client/Services/SiteGroupDefinitionService.cs b/Oqtane.Client/Services/SiteGroupDefinitionService.cs index 4f9728e6..51148846 100644 --- a/Oqtane.Client/Services/SiteGroupDefinitionService.cs +++ b/Oqtane.Client/Services/SiteGroupDefinitionService.cs @@ -19,6 +19,12 @@ namespace Oqtane.Services /// Task> GetSiteGroupDefinitionsAsync(); + /// + /// Get all s + /// + /// + Task> GetSiteGroupDefinitionsAsync(int primarySiteId); + /// /// Get one specific /// @@ -57,7 +63,12 @@ namespace Oqtane.Services public async Task> GetSiteGroupDefinitionsAsync() { - return await GetJsonAsync>($"{Apiurl}", Enumerable.Empty().ToList()); + return await GetSiteGroupDefinitionsAsync(-1); + } + + public async Task> GetSiteGroupDefinitionsAsync(int primarySiteId) + { + return await GetJsonAsync>($"{Apiurl}?siteid={primarySiteId}", Enumerable.Empty().ToList()); } public async Task GetSiteGroupDefinitionAsync(int siteGroupDefinitionId) diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor index 78fb7a40..762e457a 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor @@ -4,6 +4,11 @@ @inject IPageService PageService @inject ISettingService SettingService +@if (ShowLanguageSwitcher) +{ + +} + @if (ShowEditMode && (_showEditMode || (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered)))) { @@ -48,11 +53,9 @@ [Parameter] public string BodyClass { get; set; } = "offcanvas-body overflow-auto"; - // deprecated in 10.1.0 - UI culture is set in user's profile [Parameter] public bool ShowLanguageSwitcher { get; set; } = true; - // deprecated in 10.1.0 - UI culture is set in user's profile [Parameter] public string LanguageDropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index b702ad8d..3b3b2eda 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -11,6 +11,7 @@ @inject ILogService logger @inject ISettingService SettingService @inject IJSRuntime jsRuntime +@inject ISiteGroupDefinitionService SiteGroupDefinitionService @inject IServiceProvider ServiceProvider @inject ILogService LoggingService @inject IStringLocalizer Localizer @@ -34,6 +35,10 @@
+ @if (_siteGroupDefinitions.Any(item => item.Synchronization)) + { + + }
} @if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList)) @@ -257,6 +262,7 @@ private List _pages = new List(); private List _modules = new List(); private List _containers = new List(); + private List _siteGroupDefinitions = new List(); private string _category = "Common"; private string _pane = ""; @@ -287,6 +293,7 @@ _allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Page.SiteId); _moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList(); _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList(); + _siteGroupDefinitions = await SiteGroupDefinitionService.GetSiteGroupDefinitionsAsync(PageState.Site.SiteId); } } @@ -631,4 +638,14 @@ { _message = ""; } + + private async Task SynchronizeSite() + { + foreach (var group in _siteGroupDefinitions.Where(item => item.Synchronization)) + { + group.Synchronize = true; + await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(group); + } + NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, ""), true); + } } diff --git a/Oqtane.Client/Themes/Controls/Theme/LanguageSelector.razor b/Oqtane.Client/Themes/Controls/Theme/LanguageSelector.razor deleted file mode 100644 index 4a8e1277..00000000 --- a/Oqtane.Client/Themes/Controls/Theme/LanguageSelector.razor +++ /dev/null @@ -1,37 +0,0 @@ -@using System.Globalization -@using Oqtane.Models -@using System.Linq -@namespace Oqtane.Themes.Controls -@inherits ThemeControlBase -@inject ILocalizationService LocalizationService -@inject NavigationManager NavigationManager - -@if (PageState.Site.Languages.Count() > 1) -{ -
- - -
-} - -@code{ - private string MenuAlignment = string.Empty; - - [Parameter] - public string DropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right - - [Parameter] - public string ButtonClass { get; set; } = "btn-outline-secondary"; - - protected override async Task OnParametersSetAsync() - { - MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty; - } -} diff --git a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor index 03cdcbaa..2211b7be 100644 --- a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor +++ b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor @@ -6,22 +6,29 @@ @inject ILocalizationCookieService LocalizationCookieService @inject NavigationManager NavigationManager -@if (_supportedCultures?.Count() > 1) +@if (PageState.Site.Languages.Count() > 1) {
@@ -29,7 +36,7 @@ } @code{ - private IEnumerable _supportedCultures; + private bool _contentLocalization; private string MenuAlignment = string.Empty; [Parameter] @@ -41,12 +48,13 @@ { MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty; - _supportedCultures = PageState.Languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name }); + // if AliasName is populated it means the site is using content localization + _contentLocalization = PageState.Languages.Any(item => !string.IsNullOrEmpty(item.AliasName)); if (PageState.QueryString.ContainsKey("culture")) { var culture = PageState.QueryString["culture"]; - if (_supportedCultures.Any(item => item.Name == culture)) + if (PageState.Site.Languages.Any(item => item.Code == culture)) { await LocalizationCookieService.SetLocalizationCookieAsync(culture); } diff --git a/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor b/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor index b54570ed..f4fe08d6 100644 --- a/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor +++ b/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor @@ -11,7 +11,6 @@ -
diff --git a/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs b/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs index ed419755..da2e4ceb 100644 --- a/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs +++ b/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Oqtane.Enums; -using Oqtane.Models; -using Oqtane.Shared; -using Oqtane.Infrastructure; -using Oqtane.Repository; -using System.Net; using System.Linq; +using System.Net; +using System.Security.Policy; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; namespace Oqtane.Controllers { @@ -27,12 +28,26 @@ namespace Oqtane.Controllers _alias = tenantManager.GetAlias(); } - // GET: api/ + // GET: api/?siteid=x [HttpGet] - [Authorize(Roles = RoleNames.Host)] - public IEnumerable Get() + [Authorize(Roles = RoleNames.Admin)] + public IEnumerable Get(string siteid) { - return _siteGroupDefinitionRepository.GetSiteGroupDefinitions().ToList(); + if (User.IsInRole(RoleNames.Host) || (int.TryParse(siteid, out int SiteId) && SiteId == _alias.SiteId)) + { + var siteGroupDefinitions = _siteGroupDefinitionRepository.GetSiteGroupDefinitions(); + if (!User.IsInRole(RoleNames.Host)) + { + siteGroupDefinitions = siteGroupDefinitions.Where(item => item.PrimarySiteId == _alias.SiteId); + } + return siteGroupDefinitions.ToList(); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Definition Get Attempt {SiteId}", siteid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } } // GET api//5 @@ -74,11 +89,17 @@ namespace Oqtane.Controllers // PUT api//5 [HttpPut("{id}")] - [Authorize(Roles = RoleNames.Host)] + [Authorize(Roles = RoleNames.Admin)] public SiteGroupDefinition Put(int id, [FromBody] SiteGroupDefinition siteGroupDefinition) { - if (ModelState.IsValid && siteGroupDefinition.SiteGroupDefinitionId == id && _siteGroupDefinitionRepository.GetSiteGroupDefinition(siteGroupDefinition.SiteGroupDefinitionId, false) != null) + if (ModelState.IsValid && siteGroupDefinition.SiteGroupDefinitionId == id) { + if (!User.IsInRole(RoleNames.Host) && siteGroupDefinition.Synchronize) + { + // admins can only update the synchronize field + siteGroupDefinition = _siteGroupDefinitionRepository.GetSiteGroupDefinition(siteGroupDefinition.SiteGroupDefinitionId, false); + siteGroupDefinition.Synchronize = true; + } siteGroupDefinition = _siteGroupDefinitionRepository.UpdateSiteGroupDefinition(siteGroupDefinition); _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupDefinition, siteGroupDefinition.SiteGroupDefinitionId, SyncEventActions.Update); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Definition Updated {Group}", siteGroupDefinition); diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 5ef48827..78fb5026 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -89,6 +89,10 @@ namespace Oqtane.Infrastructure { siteGroup.SynchronizedOn = DateTime.MinValue; } + if (siteGroup.SiteGroupDefinition.Localization) + { + siteGroup.Synchronize = false; // when using localization, do not overwrite content + } // replicate site var siteLog = ReplicateSite(provider, tenantManager, settingRepository, siteGroup, primarySite, secondarySite); @@ -136,6 +140,7 @@ namespace Oqtane.Infrastructure if (primarySite.ModifiedOn > siteGroup.SynchronizedOn) { secondarySite.TimeZoneId = primarySite.TimeZoneId; + secondarySite.CultureCode = primarySite.CultureCode; if (secondarySite.LogoFileId != primarySite.LogoFileId) { secondarySite.LogoFileId = ResolveFileId(provider, primarySite.LogoFileId, secondarySite.SiteId); @@ -181,7 +186,7 @@ namespace Oqtane.Infrastructure { siteRepository.UpdateSite(secondarySite); } - log += Log(siteGroup, $"Secondary Site Updated: {secondarySite.Name}"); + log += Log(siteGroup, $"Site Updated: {secondarySite.Name}"); } // site settings diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index ea232358..f8a50351 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -318,6 +318,7 @@ namespace Oqtane.Services var siteGroups = _siteGroups.GetSiteGroups(); if (siteGroups.Any(item => item.SiteId == siteId && item.SiteGroupDefinition.Localization)) { + // site is part of a localized site group - get all languages from the site group var sites = _sites.GetSites().ToList(); var aliases = _aliases.GetAliases().ToList(); @@ -340,6 +341,16 @@ namespace Oqtane.Services } } } + else + { + // use site languages + languages = _languages.GetLanguages(siteId).ToList(); + var defaultCulture = CultureInfo.GetCultureInfo(Constants.DefaultCulture); + if (!languages.Exists(item => item.Code == defaultCulture.Name)) + { + languages.Add(new Language { Code = defaultCulture.Name, Name = "", Version = Constants.Version, IsDefault = !languages.Any(l => l.IsDefault) }); + } + } return languages; } From 60b2e50511952f6b8ca900a4d623b3367b2336b6 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 6 Feb 2026 18:25:21 +0100 Subject: [PATCH 22/75] Rename services to Client/Server and update refs Rename the client service class/file from [Module]Service to Client[Module]Service and the server service to Server[Module]Service. Update Edit.razor and Index.razor to inject Client[Module]Service and adjust all calls accordingly. Update DI registration in ClientStartup to register Client[Module]Service. Also remove the System.Net.Http.Json package reference from the client .csproj. File renames and reference updates keep class names and registrations consistent between client and server templates. --- .../Client/Modules/[Owner].Module.[Module]/Edit.razor | 10 +++++----- .../Client/Modules/[Owner].Module.[Module]/Index.razor | 8 ++++---- .../{[Module]Service.cs => Client[Module]Service.cs} | 4 ++-- .../Templates/External/Client/Startup/ClientStartup.cs | 2 +- .../Client/[Owner].Module.[Module].Client.csproj | 1 - .../{[Module]Service.cs => Server[Module]Service.cs} | 0 6 files changed, 12 insertions(+), 13 deletions(-) rename Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/{[Module]Service.cs => Client[Module]Service.cs} (90%) rename Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/{[Module]Service.cs => Server[Module]Service.cs} (100%) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor index a3d25ab5..886b0c07 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor @@ -4,7 +4,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -55,7 +55,7 @@ if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); if ([Module] != null) { _name = [Module].Name; @@ -86,14 +86,14 @@ [Module] [Module] = new [Module](); [Module].ModuleId = ModuleState.ModuleId; [Module].Name = _name; - [Module] = await [Module]Service.Add[Module]Async([Module]); + [Module] = await Client[Module]Service.Add[Module]Async([Module]); await logger.LogInformation("[Module] Added {[Module]}", [Module]); } else { - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module].Name = _name; - await [Module]Service.Update[Module]Async([Module]); + await Client[Module]Service.Update[Module]Async([Module]); await logger.LogInformation("[Module] Updated {[Module]}", [Module]); } NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor index 6f43dcb3..e5d5d4e3 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor @@ -3,7 +3,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -52,7 +52,7 @@ else { try { - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) { @@ -65,9 +65,9 @@ else { try { - await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); + await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); StateHasChanged(); } catch (Exception ex) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs similarity index 90% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs rename to Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs index 24046476..ec451c42 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs @@ -8,9 +8,9 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public class [Module]Service : ServiceBase, I[Module]Service + public class Client[Module]Service : ServiceBase, I[Module]Service { - public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } + public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } private string Apiurl => CreateApiUrl("[Module]"); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs index 95ed096c..14170a14 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs @@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup { if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) { - services.AddScoped(); + services.AddScoped(); } } } diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index 9197b9b0..2a6abe7e 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -17,7 +17,6 @@ - diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/Server[Module]Service.cs similarity index 100% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/[Module]Service.cs rename to Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/Server[Module]Service.cs From 783d01bf9fbed8b790f2f65959e5a5876ea0013c Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 6 Feb 2026 18:31:58 +0100 Subject: [PATCH 23/75] Rename client and server module services Rename the client service class to Client[Module]Service and the server service file to Server[Module]Service, updating all references accordingly. Updated DI registration in ClientStartup to register Client[Module]Service, and updated Edit.razor and Index.razor to inject and call Client[Module]Service instead of [Module]Service. This clarifies client vs server implementations and avoids naming collisions. --- .../Client/Modules/[Owner].Module.[Module]/Edit.razor | 10 +++++----- .../Client/Modules/[Owner].Module.[Module]/Index.razor | 8 ++++---- .../{[Module]Service.cs => Client[Module]Service.cs} | 4 ++-- .../Templates/Internal/Client/Startup/ClientStartup.cs | 2 +- .../{[Module]Service.cs => Server[Module]Service.cs} | 0 5 files changed, 12 insertions(+), 12 deletions(-) rename Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/{[Module]Service.cs => Client[Module]Service.cs} (90%) rename Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/{[Module]Service.cs => Server[Module]Service.cs} (100%) diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor index 59badca4..a9406963 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor @@ -4,7 +4,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -50,7 +50,7 @@ if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); if ([Module] != null) { _name = [Module].Name; @@ -81,14 +81,14 @@ [Module] [Module] = new [Module](); [Module].ModuleId = ModuleState.ModuleId; [Module].Name = _name; - [Module] = await [Module]Service.Add[Module]Async([Module]); + [Module] = await Client[Module]Service.Add[Module]Async([Module]); await logger.LogInformation("[Module] Added {[Module]}", [Module]); } else { - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module].Name = _name; - await [Module]Service.Update[Module]Async([Module]); + await Client[Module]Service.Update[Module]Async([Module]); await logger.LogInformation("[Module] Updated {[Module]}", [Module]); } NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor index af8a4839..b53e5d23 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor @@ -3,7 +3,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -52,7 +52,7 @@ else { try { - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) { @@ -65,9 +65,9 @@ else { try { - await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); + await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); StateHasChanged(); } catch (Exception ex) diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs similarity index 90% rename from Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs rename to Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs index 24046476..ec451c42 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs @@ -8,9 +8,9 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public class [Module]Service : ServiceBase, I[Module]Service + public class Client[Module]Service : ServiceBase, I[Module]Service { - public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } + public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } private string Apiurl => CreateApiUrl("[Module]"); diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs index 95ed096c..14170a14 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs @@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup { if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) { - services.AddScoped(); + services.AddScoped(); } } } diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/Server[Module]Service.cs similarity index 100% rename from Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs rename to Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/Server[Module]Service.cs From aab0dd96ddafbfd4469345b7730330db36542fbd Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 6 Feb 2026 15:37:24 -0500 Subject: [PATCH 24/75] fix issue when loading languages for content localization --- .../Infrastructure/Jobs/SynchronizationJob.cs | 26 +++++++++---------- Oqtane.Server/Services/SiteService.cs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 78fb5026..356f9640 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -49,11 +49,11 @@ namespace Oqtane.Infrastructure List sites = null; List aliases = null; - // get groups - var groups = siteGroupDefinitionRepository.GetSiteGroupDefinitions(); + // get site groups + var siteGroupDefinitions = siteGroupDefinitionRepository.GetSiteGroupDefinitions(); - // iterate through groups which need to be synchronized - foreach (var group in groups.Where(item => item.Synchronization && item.Synchronize)) + // iterate through site groups which need to be synchronized + foreach (var siteGroupDefinition in siteGroupDefinitions.Where(item => item.Synchronization && item.Synchronize)) { // get data if (siteGroups == null) @@ -63,19 +63,19 @@ namespace Oqtane.Infrastructure aliases = aliasRepository.GetAliases().ToList(); } - var aliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == group.PrimarySiteId && item.IsDefault).Name; - log += $"Processing Primary Site: {sites.First(item => item.SiteId == group.PrimarySiteId).Name} - {CreateLink(aliasName)}
"; + var aliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroupDefinition.PrimarySiteId && item.IsDefault).Name; + log += $"Processing Primary Site: {sites.First(item => item.SiteId == siteGroupDefinition.PrimarySiteId).Name} - {CreateLink(aliasName)}
"; // get primary site - var primarySite = sites.FirstOrDefault(item => item.SiteId == group.PrimarySiteId); + var primarySite = sites.FirstOrDefault(item => item.SiteId == siteGroupDefinition.PrimarySiteId); if (primarySite != null) { // update flag to prevent job from processing group again - group.Synchronize = false; - siteGroupDefinitionRepository.UpdateSiteGroupDefinition(group); + siteGroupDefinition.Synchronize = false; + siteGroupDefinitionRepository.UpdateSiteGroupDefinition(siteGroupDefinition); - // iterate through sites in group - foreach (var siteGroup in siteGroups.Where(item => item.SiteGroupDefinitionId == group.SiteGroupDefinitionId && item.SiteId != group.PrimarySiteId)) + // iterate through sites in site group + foreach (var siteGroup in siteGroups.Where(item => item.SiteGroupDefinitionId == siteGroupDefinition.SiteGroupDefinitionId && item.SiteId != siteGroupDefinition.PrimarySiteId)) { // get secondary site var secondarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.SiteId); @@ -105,13 +105,13 @@ namespace Oqtane.Infrastructure } else { - log += $"Site Group {group.Name} Has A SiteId {siteGroup.SiteId} Which Does Not Exist
"; + log += $"Site Group {siteGroupDefinition.Name} Has A SiteId {siteGroup.SiteId} Which Does Not Exist
"; } } } else { - log += $"Site Group {group.Name} Has A PrimarySiteId {group.PrimarySiteId} Which Does Not Exist
"; + log += $"Site Group {siteGroupDefinition.Name} Has A PrimarySiteId {siteGroupDefinition.PrimarySiteId} Which Does Not Exist
"; } } diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index f8a50351..1cab31b9 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -334,7 +334,7 @@ namespace Oqtane.Services var alias = aliases.FirstOrDefault(item => item.SiteId == siteGroup.SiteId && item.TenantId == tenantId && item.IsDefault); if (alias != null) { - languages.Add(new Language { Code = site.CultureCode, Name = "", AliasName = alias.Name, IsDefault = true }); + languages.Add(new Language { Code = site.CultureCode, Name = "", AliasName = alias.Name, IsDefault = false }); } } } From c0e191537f4d69434e99371ea86b42f5c586bcba Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 6 Feb 2026 16:07:07 -0500 Subject: [PATCH 25/75] handle cache invalidation for site groups --- Oqtane.Server/Controllers/SiteGroupController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Oqtane.Server/Controllers/SiteGroupController.cs b/Oqtane.Server/Controllers/SiteGroupController.cs index 62441495..cbf41841 100644 --- a/Oqtane.Server/Controllers/SiteGroupController.cs +++ b/Oqtane.Server/Controllers/SiteGroupController.cs @@ -70,6 +70,7 @@ namespace Oqtane.Controllers { siteGroup = _siteGroupRepository.AddSiteGroup(siteGroup); _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupDefinitionId, SyncEventActions.Create); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroup.SiteId, SyncEventActions.Refresh); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Added {SiteGroup}", siteGroup); } else @@ -90,6 +91,7 @@ namespace Oqtane.Controllers { siteGroup = _siteGroupRepository.UpdateSiteGroup(siteGroup); _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupDefinitionId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroup.SiteId, SyncEventActions.Refresh); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Updated {SiteGroup}", siteGroup); } else @@ -111,6 +113,7 @@ namespace Oqtane.Controllers { _siteGroupRepository.DeleteSiteGroup(id); _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Delete); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroup.SiteId, SyncEventActions.Refresh); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Deleted {SiteGroupId}", id); } else From ddd6dfc4758d4a1c92fcb80b0392db3e6a29502c Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 9 Feb 2026 13:58:38 -0500 Subject: [PATCH 26/75] refactoring of site groups --- .../OqtaneServiceCollectionExtensions.cs | 2 +- Oqtane.Client/Modules/Admin/Site/Index.razor | 300 ++++++++---------- .../Resources/Modules/Admin/Site/Index.resx | 50 ++- .../Services/SiteGroupDefinitionService.cs | 94 ------ .../Services/SiteGroupMemberService.cs | 104 ++++++ Oqtane.Client/Services/SiteGroupService.cs | 54 ++-- .../Theme/ControlPanelInteractive.razor | 12 +- .../Controllers/SiteGroupController.cs | 53 ++-- .../SiteGroupDefinitionController.cs | 135 -------- .../Controllers/SiteGroupMemberController.cs | 126 ++++++++ .../OqtaneServiceCollectionExtensions.cs | 4 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 253 ++++++--------- .../SiteGroupDefinitionEntityBuilder.cs | 48 --- .../EntityBuilders/SiteGroupEntityBuilder.cs | 21 +- .../SiteGroupMemberEntityBuilder.cs | 49 +++ .../Tenant/10010001_AddSiteGroups.cs | 8 +- .../Repository/Context/TenantDBContext.cs | 2 +- .../SiteGroupDefinitionRepository.cs | 75 ----- .../Repository/SiteGroupMemberRepository.cs | 88 +++++ .../Repository/SiteGroupRepository.cs | 53 ++-- Oqtane.Server/Services/SiteService.cs | 18 +- Oqtane.Shared/Models/SiteGroup.cs | 37 +-- Oqtane.Shared/Models/SiteGroupDefinition.cs | 35 -- Oqtane.Shared/Models/SiteGroupMember.cs | 44 +++ Oqtane.Shared/Shared/EntityNames.cs | 2 +- Oqtane.Shared/Shared/SiteGroupTypes.cs | 8 + 26 files changed, 789 insertions(+), 886 deletions(-) delete mode 100644 Oqtane.Client/Services/SiteGroupDefinitionService.cs create mode 100644 Oqtane.Client/Services/SiteGroupMemberService.cs delete mode 100644 Oqtane.Server/Controllers/SiteGroupDefinitionController.cs create mode 100644 Oqtane.Server/Controllers/SiteGroupMemberController.cs delete mode 100644 Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs create mode 100644 Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs delete mode 100644 Oqtane.Server/Repository/SiteGroupDefinitionRepository.cs create mode 100644 Oqtane.Server/Repository/SiteGroupMemberRepository.cs delete mode 100644 Oqtane.Shared/Models/SiteGroupDefinition.cs create mode 100644 Oqtane.Shared/Models/SiteGroupMember.cs create mode 100644 Oqtane.Shared/Shared/SiteGroupTypes.cs diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 70188b2d..4cb6c220 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -57,8 +57,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); // providers diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index fce700f7..b89cb778 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -18,8 +18,8 @@ @inject IJobService JobService @inject IStringLocalizer SharedLocalizer @inject IOutputCacheService CacheService -@inject ISiteGroupDefinitionService SiteGroupDefinitionService @inject ISiteGroupService SiteGroupService +@inject ISiteGroupMemberService SiteGroupMemberService @if (_initialized) { @@ -448,22 +448,22 @@ -
+
- @if (!_addSiteGroupDefinition) + @if (!_addSiteGroup) {
- +
- - @foreach (var siteGroupDefinition in _siteGroupDefinitions) + @foreach (var siteGroup in _siteGroups) { - + } - @if (!_addSiteGroupDefinition) + @if (!_addSiteGroup) { } @@ -471,75 +471,62 @@
} - @if (_siteGroupDefinitionId != -1 || _addSiteGroupDefinition) + @if (_siteGroupId != -1 || _addSiteGroup) {
- +
- +
- -
-
-
- -
- + +
} - @if (_siteGroupDefinitionId != -1) + @if (_siteGroupId != -1) {
- +
+ @if (!_addSiteGroupMember) + { + + } + else + { + + }
} - @if (_siteGroupDefinitionId != -1) + @if (_siteGroupId != -1 && _siteId != -1) {
- +
- - - +
- @if (_member == "Secondary" && _synchronization == "True") + @if (_primary == "False" && _groupType == SiteGroupTypes.Synchronization) { -
- -
- -
-
@@ -556,7 +543,7 @@ @if (!string.IsNullOrEmpty(_synchronized)) { - + }
@@ -566,17 +553,17 @@
- @if ((_siteGroupDefinitionId != -1 || _addSiteGroupDefinition)) + @if ((_siteGroupId != -1 || _addSiteGroup)) { - + } - @if ((_siteGroupDefinitionId != -1 && !_addSiteGroupDefinition) && _member != "False") + @if (_siteGroupId != -1 && !_addSiteGroup && _siteId != -1 && !_addSiteGroupMember) { - + } - @if (_addSiteGroupDefinition) + @if (_addSiteGroup) { - + }
@@ -684,18 +671,17 @@ private string _defaultalias; private bool _addAlias = false; - private List _siteGroupDefinitions = new List(); + private List _siteGroups = new List(); private List _sites = new List(); - private int _siteGroupDefinitionId = -1; + private int _siteGroupId = -1; private int _siteId; private string _groupName = string.Empty; - private string _synchronization = "True"; - private string _localization = "False"; - private string _member = "Primary"; - private string _synchronize = "True"; + private string _groupType = SiteGroupTypes.Synchronization; + private string _primary = "True"; private string _notify = "True"; private string _synchronized = string.Empty; - private bool _addSiteGroupDefinition = false; + private bool _addSiteGroup = false; + private bool _addSiteGroupMember = false; private string _tenant = string.Empty; private string _database = string.Empty; @@ -1261,82 +1247,78 @@ private async Task LoadSiteGroups() { - _siteGroupDefinitions = await SiteGroupDefinitionService.GetSiteGroupDefinitionsAsync(); - _siteGroupDefinitionId = -1; - _addSiteGroupDefinition = false; + _siteGroups = await SiteGroupService.GetSiteGroupsAsync(); + _siteGroupId = -1; + _addSiteGroup = false; StateHasChanged(); } - private async void SiteGroupChanged(ChangeEventArgs e) + private async Task SiteGroupChanged(ChangeEventArgs e) { - _siteGroupDefinitionId = int.Parse(e.Value.ToString()); - if (_siteGroupDefinitionId != -1) + _siteGroupId = int.Parse(e.Value.ToString()); + if (_siteGroupId != -1) { - var group = _siteGroupDefinitions.FirstOrDefault(item => item.SiteGroupDefinitionId == _siteGroupDefinitionId); + var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId); if (group != null) { _groupName = group.Name; - _synchronization = group.Synchronization.ToString(); - _localization = group.Localization.ToString(); + _groupType = group.Type; + _siteId = -1; + _primary = "False"; + _addSiteGroupMember = false; - if (_sites.Count == 0) - { - _sites = await SiteService.GetSitesAsync(); - } - _siteId = PageState.Site.SiteId; - _member = "False"; - - var siteGroups = await SiteGroupService.GetSiteGroupsAsync(-1, _siteGroupDefinitionId); - foreach (var site in _sites) - { - site.Fingerprint = ""; // used as temporary state - var siteGroup = siteGroups.FirstOrDefault(item => item.SiteId == site.SiteId); - if (siteGroup != null) - { - if (group.PrimarySiteId == site.SiteId) - { - site.Fingerprint = "Primary"; - } - else - { - site.Fingerprint = "Secondary"; - } - - if (siteGroup.SiteId == _siteId) - { - _member = site.Fingerprint; - _synchronize = siteGroup.Synchronize.ToString(); - _notify = siteGroup.Notify.ToString(); - _synchronized = UtcToLocal(siteGroup.SynchronizedOn).ToString(); - } - } - } + await LoadSites(); } } StateHasChanged(); } - private async void SiteChanged(ChangeEventArgs e) + private async Task LoadSites() { - _siteId = int.Parse(e.Value.ToString()); - var siteGroup = await SiteGroupService.GetSiteGroupAsync(_siteId, _siteGroupDefinitionId); - if (siteGroup != null) + var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId); + + _sites = await SiteService.GetSitesAsync(); + if (_addSiteGroupMember) { - if (siteGroup.SiteGroupDefinition.PrimarySiteId == _siteId) - { - _member = "Primary"; - } - else - { - _member = "Secondary"; - } - _synchronize = siteGroup.Synchronize.ToString(); - _notify = siteGroup.Notify.ToString(); - _synchronized = UtcToLocal(siteGroup.SynchronizedOn).ToString(); + // include sites which are not members + _sites = _sites.ExceptBy(siteGroupMembers.Select(item => item.SiteId), item => item.SiteId).ToList(); } else { - _member = "False"; + // include sites which are members + _sites = _sites.Where(item => siteGroupMembers.Any(item2 => item2.SiteId == item.SiteId)).ToList(); + var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId); + foreach (var site in _sites) + { + if (group.PrimarySiteId == site.SiteId) + { + site.Name += $" ({Localizer["Primary"]})"; + } + else + { + site.Name += $" ({Localizer["Secondary"]})"; + } + } + + var siteGroupMember = siteGroupMembers.FirstOrDefault(item => item.SiteId == _siteId); + if (siteGroupMember != null) + { + _primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False"; + _notify = siteGroupMember.Notify.ToString(); + _synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString(); + } + } + } + + private async Task SiteChanged(ChangeEventArgs e) + { + _siteId = int.Parse(e.Value.ToString()); + var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, _siteGroupId); + if (siteGroupMember != null) + { + _primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False"; + _notify = siteGroupMember.Notify.ToString(); + _synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString(); } StateHasChanged(); } @@ -1345,77 +1327,73 @@ { _groupName = ""; _siteId = PageState.Site.SiteId; - _member = "Primary"; + _primary = "True"; _synchronized = ""; - _addSiteGroupDefinition = true; + _addSiteGroup = true; } - private async Task SaveSiteGroup() + private async Task AddSiteGroupMember() { - SiteGroupDefinition siteGroupDefinition = null; + _addSiteGroupMember = !_addSiteGroupMember; + _siteId = -1; + await LoadSites(); + } + + private async Task SaveSiteGroupMember() + { + SiteGroup siteGroup = null; - if (_siteGroupDefinitionId == -1) + if (_siteGroupId == -1) { if (!string.IsNullOrEmpty(_groupName)) { - siteGroupDefinition = new Models.SiteGroupDefinition + siteGroup = new SiteGroup { Name = _groupName, + Type = _groupType, PrimarySiteId = _siteId, - Synchronization = bool.Parse(_synchronization), - Localization = bool.Parse(_localization), Synchronize = false }; - siteGroupDefinition = await SiteGroupDefinitionService.AddSiteGroupDefinitionAsync(siteGroupDefinition); + siteGroup = await SiteGroupService.AddSiteGroupAsync(siteGroup); } } else { - siteGroupDefinition = _siteGroupDefinitions.FirstOrDefault(item => item.SiteGroupDefinitionId == _siteGroupDefinitionId); - if (siteGroupDefinition != null && !string.IsNullOrEmpty(_groupName)) + siteGroup = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId); + if (siteGroup != null && !string.IsNullOrEmpty(_groupName)) { - siteGroupDefinition.Name = _groupName; - siteGroupDefinition.PrimarySiteId = (_member == "Primary") ? _siteId : siteGroupDefinition.PrimarySiteId; - siteGroupDefinition.Synchronization = bool.Parse(_synchronization); - siteGroupDefinition.Localization = bool.Parse(_localization); - siteGroupDefinition = await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(siteGroupDefinition); + siteGroup.Name = _groupName; + siteGroup.Type = _groupType; + siteGroup.PrimarySiteId = (_primary == "True") ? _siteId : siteGroup.PrimarySiteId; + siteGroup = await SiteGroupService.UpdateSiteGroupAsync(siteGroup); } else { - siteGroupDefinition = null; + siteGroup = null; } } - if (siteGroupDefinition != null) + if (siteGroup != null) { - var siteGroup = await SiteGroupService.GetSiteGroupAsync(_siteId, siteGroupDefinition.SiteGroupDefinitionId); - if (siteGroup == null) + var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, siteGroup.SiteGroupId); + if (siteGroupMember == null) { - siteGroup = new SiteGroup + siteGroupMember = new SiteGroupMember { - SiteGroupDefinitionId = siteGroupDefinition.SiteGroupDefinitionId, + SiteGroupId = siteGroup.SiteGroupId, SiteId = _siteId, - Synchronize = bool.Parse(_synchronize), Notify = bool.Parse(_notify) }; - await SiteGroupService.AddSiteGroupAsync(siteGroup); + await SiteGroupMemberService.AddSiteGroupMemberAsync(siteGroupMember); } else { - if (_member == "False") - { - await SiteGroupService.DeleteSiteGroupAsync(siteGroup.SiteGroupId); - } - else - { - siteGroup.Synchronize = bool.Parse(_synchronize); - siteGroup.Notify = bool.Parse(_notify); - siteGroup.SynchronizedOn = string.IsNullOrEmpty(_synchronized) ? null : siteGroup.SynchronizedOn; - await SiteGroupService.UpdateSiteGroupAsync(siteGroup); - } + siteGroupMember.Notify = bool.Parse(_notify); + siteGroupMember.SynchronizedOn = string.IsNullOrEmpty(_synchronized) ? null : siteGroupMember.SynchronizedOn; + await SiteGroupMemberService.UpdateSiteGroupMemberAsync(siteGroupMember); } - if (siteGroupDefinition.Synchronization) + if (siteGroup.Type == SiteGroupTypes.Synchronization) { // enable synchronization job if it is not enabled already var jobs = await JobService.GetJobsAsync(); @@ -1436,33 +1414,33 @@ } } - private async Task CancelSiteGroup() + private async Task CancelSiteGroupMember() { _groupName = ""; await LoadSiteGroups(); } - private async Task DeleteSiteGroup() + private async Task DeleteSiteGroupMember() { - if (_siteGroupDefinitionId != -1) + if (_siteGroupId != -1) { - var siteGroup = await SiteGroupService.GetSiteGroupAsync(PageState.Site.SiteId, _siteGroupDefinitionId); - if (siteGroup != null) + var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(PageState.Site.SiteId, _siteGroupId); + if (siteGroupMember != null) { - await SiteGroupService.DeleteSiteGroupAsync(siteGroup.SiteGroupDefinitionId); + await SiteGroupMemberService.DeleteSiteGroupMemberAsync(siteGroupMember.SiteGroupId); } - var siteGroups = await SiteGroupService.GetSiteGroupsAsync(-1, _siteGroupDefinitionId); - if (!siteGroups.Any()) + var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId); + if (!siteGroupMembers.Any()) { - await SiteGroupDefinitionService.DeleteSiteGroupDefinitionAsync(_siteGroupDefinitionId); + await SiteGroupService.DeleteSiteGroupAsync(_siteGroupId); } await LoadSiteGroups(); } } - private async Task ResetSiteGroup() + private async Task ResetSiteGroupMember() { _synchronized = ""; } diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index ce84a1f7..3ea74b8e 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -498,17 +498,17 @@ A url for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder). - + Group: - + The site groups in this tenant (database) - - Member? + + Primary? - - Indicates if the current site is a member of the selected group + + Indicates if the selected site is the primary member of the site group Name: @@ -516,18 +516,6 @@ Name of the site group - - Synchronization? - - - Specifies if the group supports content synchronization between the primary site and other sites in the group - - - Localization? - - - Specifies if the content of the sites in the group are localized - Primary @@ -540,11 +528,11 @@ Update - - Delete Site Group + + Delete Site Group Member - - Are You Sure You Wish To Delete {0}? + + Are You Sure You Wish To Delete This Member From The Site Group? Group Name Is Required @@ -559,21 +547,27 @@ Specifies if site administrators should be notified of any synchronization activity - Site: + Members: - The sites in this tenant (database) + The sites which are members of this site group Synchronized: + + Type: + + + The site group type (ie. synchronization, localization) + The date/time of the last synchronization for the site - - Synchronize? + + Synchronization - - Specifies the synchronization approach between the primary site and the selected site + + Localization \ No newline at end of file diff --git a/Oqtane.Client/Services/SiteGroupDefinitionService.cs b/Oqtane.Client/Services/SiteGroupDefinitionService.cs deleted file mode 100644 index 51148846..00000000 --- a/Oqtane.Client/Services/SiteGroupDefinitionService.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Oqtane.Models; -using System.Threading.Tasks; -using System.Net.Http; -using System.Collections.Generic; -using Oqtane.Documentation; -using Oqtane.Shared; -using System.Linq; - -namespace Oqtane.Services -{ - /// - /// Service to manage s on a - /// - public interface ISiteGroupDefinitionService - { - /// - /// Get all s - /// - /// - Task> GetSiteGroupDefinitionsAsync(); - - /// - /// Get all s - /// - /// - Task> GetSiteGroupDefinitionsAsync(int primarySiteId); - - /// - /// Get one specific - /// - /// ID-reference of a - /// - Task GetSiteGroupDefinitionAsync(int siteGroupDefinitionId); - - /// - /// Add / save a new to the database. - /// - /// - /// - Task AddSiteGroupDefinitionAsync(SiteGroupDefinition siteGroupDefinition); - - /// - /// Update a in the database. - /// - /// - /// - Task UpdateSiteGroupDefinitionAsync(SiteGroupDefinition siteGroupDefinition); - - /// - /// Delete a in the database. - /// - /// ID-reference of a - /// - Task DeleteSiteGroupDefinitionAsync(int siteGroupDefinitionId); - } - - [PrivateApi("Don't show in the documentation, as everything should use the Interface")] - public class SiteGroupDefinitionService : ServiceBase, ISiteGroupDefinitionService - { - public SiteGroupDefinitionService(HttpClient http, SiteState siteState) : base(http, siteState) { } - - private string Apiurl => CreateApiUrl("SiteGroupDefinition"); - - public async Task> GetSiteGroupDefinitionsAsync() - { - return await GetSiteGroupDefinitionsAsync(-1); - } - - public async Task> GetSiteGroupDefinitionsAsync(int primarySiteId) - { - return await GetJsonAsync>($"{Apiurl}?siteid={primarySiteId}", Enumerable.Empty().ToList()); - } - - public async Task GetSiteGroupDefinitionAsync(int siteGroupDefinitionId) - { - return await GetJsonAsync($"{Apiurl}/{siteGroupDefinitionId}"); - } - - public async Task AddSiteGroupDefinitionAsync(SiteGroupDefinition siteGroupDefinition) - { - return await PostJsonAsync(Apiurl, siteGroupDefinition); - } - - public async Task UpdateSiteGroupDefinitionAsync(SiteGroupDefinition siteGroupDefinition) - { - return await PutJsonAsync($"{Apiurl}/{siteGroupDefinition.SiteGroupDefinitionId}", siteGroupDefinition); - } - - public async Task DeleteSiteGroupDefinitionAsync(int siteGroupDefinitionId) - { - await DeleteAsync($"{Apiurl}/{siteGroupDefinitionId}"); - } - } -} diff --git a/Oqtane.Client/Services/SiteGroupMemberService.cs b/Oqtane.Client/Services/SiteGroupMemberService.cs new file mode 100644 index 00000000..22bee97f --- /dev/null +++ b/Oqtane.Client/Services/SiteGroupMemberService.cs @@ -0,0 +1,104 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Collections.Generic; +using Oqtane.Documentation; +using Oqtane.Shared; +using System.Linq; + +namespace Oqtane.Services +{ + /// + /// Service to manage s on a + /// + public interface ISiteGroupMemberService + { + /// + /// Get all s + /// + /// + Task> GetSiteGroupMembersAsync(int siteId, int siteGroupId); + + /// + /// Get one specific + /// + /// ID-reference of a + /// + Task GetSiteGroupMemberAsync(int siteGroupMemberId); + + /// + /// Get one specific + /// + /// ID-reference of a + /// ID-reference of a + /// + Task GetSiteGroupMemberAsync(int siteId, int siteGroupId); + + /// + /// Add / save a new to the database. + /// + /// + /// + Task AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember); + + /// + /// Update a in the database. + /// + /// + /// + Task UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember); + + /// + /// Delete a in the database. + /// + /// ID-reference of a + /// + Task DeleteSiteGroupMemberAsync(int siteGroupMemberId); + } + + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class SiteGroupMemberService : ServiceBase, ISiteGroupMemberService + { + public SiteGroupMemberService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("SiteGroupMember"); + + public async Task> GetSiteGroupMembersAsync(int siteId, int siteGroupId) + { + return await GetJsonAsync>($"{Apiurl}?siteid={siteId}&groupid={siteGroupId}", Enumerable.Empty().ToList()); + } + + public async Task GetSiteGroupMemberAsync(int siteGroupMemberId) + { + return await GetJsonAsync($"{Apiurl}/{siteGroupMemberId}"); + } + + public async Task GetSiteGroupMemberAsync(int siteId, int siteGroupId) + { + var siteGroupMembers = await GetSiteGroupMembersAsync(siteId, siteGroupId); + if (siteGroupMembers != null && siteGroupMembers.Count > 0) + { + return siteGroupMembers[0]; + } + else + { + return null; + } + } + + public async Task AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember) + { + return await PostJsonAsync(Apiurl, siteGroupMember); + } + + public async Task UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember) + { + return await PutJsonAsync($"{Apiurl}/{siteGroupMember.SiteGroupId}", siteGroupMember); + } + + public async Task DeleteSiteGroupMemberAsync(int siteGroupMemberId) + { + await DeleteAsync($"{Apiurl}/{siteGroupMemberId}"); + } + } +} diff --git a/Oqtane.Client/Services/SiteGroupService.cs b/Oqtane.Client/Services/SiteGroupService.cs index 8cae99f7..83a63242 100644 --- a/Oqtane.Client/Services/SiteGroupService.cs +++ b/Oqtane.Client/Services/SiteGroupService.cs @@ -17,43 +17,41 @@ namespace Oqtane.Services /// Get all s /// /// - Task> GetSiteGroupsAsync(int siteId, int siteGroupDefinitionId); + Task> GetSiteGroupsAsync(); + + /// + /// Get all s + /// + /// + Task> GetSiteGroupsAsync(int primarySiteId); /// /// Get one specific /// - /// ID-reference of a + /// ID-reference of a /// - Task GetSiteGroupAsync(int siteSiteGroupDefinitionId); - - /// - /// Get one specific - /// - /// ID-reference of a - /// ID-reference of a - /// - Task GetSiteGroupAsync(int siteId, int siteGroupDefinitionId); + Task GetSiteGroupAsync(int siteGroupId); /// /// Add / save a new to the database. /// - /// + /// /// Task AddSiteGroupAsync(SiteGroup siteGroup); /// /// Update a in the database. /// - /// + /// /// Task UpdateSiteGroupAsync(SiteGroup siteGroup); /// /// Delete a in the database. /// - /// ID-reference of a + /// ID-reference of a /// - Task DeleteSiteGroupAsync(int siteSiteGroupDefinitionId); + Task DeleteSiteGroupAsync(int siteGroupId); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -63,27 +61,19 @@ namespace Oqtane.Services private string Apiurl => CreateApiUrl("SiteGroup"); - public async Task> GetSiteGroupsAsync(int siteId, int siteGroupDefinitionId) + public async Task> GetSiteGroupsAsync() { - return await GetJsonAsync>($"{Apiurl}?siteid={siteId}&groupid={siteGroupDefinitionId}", Enumerable.Empty().ToList()); + return await GetSiteGroupsAsync(-1); } - public async Task GetSiteGroupAsync(int siteSiteGroupDefinitionId) + public async Task> GetSiteGroupsAsync(int primarySiteId) { - return await GetJsonAsync($"{Apiurl}/{siteSiteGroupDefinitionId}"); + return await GetJsonAsync>($"{Apiurl}?siteid={primarySiteId}", Enumerable.Empty().ToList()); } - public async Task GetSiteGroupAsync(int siteId, int siteGroupDefinitionId) + public async Task GetSiteGroupAsync(int siteGroupId) { - var siteGroups = await GetSiteGroupsAsync(siteId, siteGroupDefinitionId); - if (siteGroups != null && siteGroups.Count > 0) - { - return siteGroups[0]; - } - else - { - return null; - } + return await GetJsonAsync($"{Apiurl}/{siteGroupId}"); } public async Task AddSiteGroupAsync(SiteGroup siteGroup) @@ -93,12 +83,12 @@ namespace Oqtane.Services public async Task UpdateSiteGroupAsync(SiteGroup siteGroup) { - return await PutJsonAsync($"{Apiurl}/{siteGroup.SiteGroupDefinitionId}", siteGroup); + return await PutJsonAsync($"{Apiurl}/{siteGroup.SiteGroupId}", siteGroup); } - public async Task DeleteSiteGroupAsync(int siteSiteGroupDefinitionId) + public async Task DeleteSiteGroupAsync(int siteGroupId) { - await DeleteAsync($"{Apiurl}/{siteSiteGroupDefinitionId}"); + await DeleteAsync($"{Apiurl}/{siteGroupId}"); } } } diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index 3b3b2eda..91860090 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -11,7 +11,7 @@ @inject ILogService logger @inject ISettingService SettingService @inject IJSRuntime jsRuntime -@inject ISiteGroupDefinitionService SiteGroupDefinitionService +@inject ISiteGroupService SiteGroupService @inject IServiceProvider ServiceProvider @inject ILogService LoggingService @inject IStringLocalizer Localizer @@ -35,7 +35,7 @@
- @if (_siteGroupDefinitions.Any(item => item.Synchronization)) + @if (_siteGroups.Any(item => item.Type == SiteGroupTypes.Synchronization)) { } @@ -262,7 +262,7 @@ private List _pages = new List(); private List _modules = new List(); private List _containers = new List(); - private List _siteGroupDefinitions = new List(); + private List _siteGroups = new List(); private string _category = "Common"; private string _pane = ""; @@ -293,7 +293,7 @@ _allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Page.SiteId); _moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList(); _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList(); - _siteGroupDefinitions = await SiteGroupDefinitionService.GetSiteGroupDefinitionsAsync(PageState.Site.SiteId); + _siteGroups = await SiteGroupService.GetSiteGroupsAsync(PageState.Site.SiteId); } } @@ -641,10 +641,10 @@ private async Task SynchronizeSite() { - foreach (var group in _siteGroupDefinitions.Where(item => item.Synchronization)) + foreach (var group in _siteGroups.Where(item => item.Type == SiteGroupTypes.Synchronization)) { group.Synchronize = true; - await SiteGroupDefinitionService.UpdateSiteGroupDefinitionAsync(group); + await SiteGroupService.UpdateSiteGroupAsync(group); } NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, ""), true); } diff --git a/Oqtane.Server/Controllers/SiteGroupController.cs b/Oqtane.Server/Controllers/SiteGroupController.cs index cbf41841..61e2a174 100644 --- a/Oqtane.Server/Controllers/SiteGroupController.cs +++ b/Oqtane.Server/Controllers/SiteGroupController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Security.Policy; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Oqtane.Enums; @@ -27,18 +28,23 @@ namespace Oqtane.Controllers _alias = tenantManager.GetAlias(); } - // GET: api/?siteid=x&groupid=y + // GET: api/?siteid=x [HttpGet] - [Authorize(Roles = RoleNames.Host)] - public IEnumerable Get(string siteid, string groupid) + [Authorize(Roles = RoleNames.Admin)] + public IEnumerable Get(string siteid) { - if (int.TryParse(siteid, out int SiteId) && int.TryParse(groupid, out int SiteGroupDefinitionId)) + if (User.IsInRole(RoleNames.Host) || (int.TryParse(siteid, out int SiteId) && SiteId == _alias.SiteId)) { - return _siteGroupRepository.GetSiteGroups(SiteId, SiteGroupDefinitionId).ToList(); + var siteGroups = _siteGroupRepository.GetSiteGroups(); + if (!User.IsInRole(RoleNames.Host)) + { + siteGroups = siteGroups.Where(item => item.PrimarySiteId == _alias.SiteId); + } + return siteGroups.ToList(); } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Get Attempt for SiteId {SiteId} And SiteGroupDefinitionId {SiteGroupDefinitionId}", siteid, groupid); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Get Attempt {SiteId}", siteid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return null; } @@ -49,10 +55,10 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Host)] public SiteGroup Get(int id) { - var siteGroup = _siteGroupRepository.GetSiteGroup(id); - if (siteGroup != null) + var group = _siteGroupRepository.GetSiteGroup(id); + if (group != null) { - return siteGroup; + return group; } else { @@ -69,13 +75,12 @@ namespace Oqtane.Controllers if (ModelState.IsValid) { siteGroup = _siteGroupRepository.AddSiteGroup(siteGroup); - _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupDefinitionId, SyncEventActions.Create); - _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroup.SiteId, SyncEventActions.Refresh); - _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Added {SiteGroup}", siteGroup); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Create); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Added {Group}", siteGroup); } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Post Attempt {SiteGroup}", siteGroup); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Post Attempt {Group}", siteGroup); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; siteGroup = null; } @@ -84,19 +89,24 @@ namespace Oqtane.Controllers // PUT api//5 [HttpPut("{id}")] - [Authorize(Roles = RoleNames.Host)] + [Authorize(Roles = RoleNames.Admin)] public SiteGroup Put(int id, [FromBody] SiteGroup siteGroup) { - if (ModelState.IsValid && siteGroup.SiteGroupDefinitionId == id && _siteGroupRepository.GetSiteGroup(siteGroup.SiteGroupDefinitionId, false) != null) + if (ModelState.IsValid && siteGroup.SiteGroupId == id) { + if (!User.IsInRole(RoleNames.Host) && siteGroup.Synchronize) + { + // admins can only update the synchronize field + siteGroup = _siteGroupRepository.GetSiteGroup(siteGroup.SiteGroupId, false); + siteGroup.Synchronize = true; + } siteGroup = _siteGroupRepository.UpdateSiteGroup(siteGroup); - _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupDefinitionId, SyncEventActions.Update); - _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroup.SiteId, SyncEventActions.Refresh); - _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Updated {SiteGroup}", siteGroup); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Update); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Updated {Group}", siteGroup); } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Put Attempt {SiteGroup}", siteGroup); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Put Attempt {Group}", siteGroup); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; siteGroup = null; } @@ -113,12 +123,11 @@ namespace Oqtane.Controllers { _siteGroupRepository.DeleteSiteGroup(id); _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Delete); - _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroup.SiteId, SyncEventActions.Refresh); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Deleted {SiteGroupId}", id); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Deleted {siteGroupId}", id); } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Delete Attempt {SiteGroupId}", id); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Delete Attempt {siteGroupId}", id); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } diff --git a/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs b/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs deleted file mode 100644 index da2e4ceb..00000000 --- a/Oqtane.Server/Controllers/SiteGroupDefinitionController.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Security.Policy; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Oqtane.Enums; -using Oqtane.Infrastructure; -using Oqtane.Models; -using Oqtane.Repository; -using Oqtane.Shared; - -namespace Oqtane.Controllers -{ - [Route(ControllerRoutes.ApiRoute)] - public class SiteGroupDefinitionController : Controller - { - private readonly ISiteGroupDefinitionRepository _siteGroupDefinitionRepository; - private readonly ISyncManager _syncManager; - private readonly ILogManager _logger; - private readonly Alias _alias; - - public SiteGroupDefinitionController(ISiteGroupDefinitionRepository siteGroupDefinitionRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) - { - _siteGroupDefinitionRepository = siteGroupDefinitionRepository; - _syncManager = syncManager; - _logger = logger; - _alias = tenantManager.GetAlias(); - } - - // GET: api/?siteid=x - [HttpGet] - [Authorize(Roles = RoleNames.Admin)] - public IEnumerable Get(string siteid) - { - if (User.IsInRole(RoleNames.Host) || (int.TryParse(siteid, out int SiteId) && SiteId == _alias.SiteId)) - { - var siteGroupDefinitions = _siteGroupDefinitionRepository.GetSiteGroupDefinitions(); - if (!User.IsInRole(RoleNames.Host)) - { - siteGroupDefinitions = siteGroupDefinitions.Where(item => item.PrimarySiteId == _alias.SiteId); - } - return siteGroupDefinitions.ToList(); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Definition Get Attempt {SiteId}", siteid); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return null; - } - } - - // GET api//5 - [HttpGet("{id}")] - [Authorize(Roles = RoleNames.Host)] - public SiteGroupDefinition Get(int id) - { - var group = _siteGroupDefinitionRepository.GetSiteGroupDefinition(id); - if (group != null) - { - return group; - } - else - { - HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; - return null; - } - } - - // POST api/ - [HttpPost] - [Authorize(Roles = RoleNames.Host)] - public SiteGroupDefinition Post([FromBody] SiteGroupDefinition siteGroupDefinition) - { - if (ModelState.IsValid) - { - siteGroupDefinition = _siteGroupDefinitionRepository.AddSiteGroupDefinition(siteGroupDefinition); - _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupDefinition, siteGroupDefinition.SiteGroupDefinitionId, SyncEventActions.Create); - _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Definition Added {Group}", siteGroupDefinition); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Definition Post Attempt {Group}", siteGroupDefinition); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - siteGroupDefinition = null; - } - return siteGroupDefinition; - } - - // PUT api//5 - [HttpPut("{id}")] - [Authorize(Roles = RoleNames.Admin)] - public SiteGroupDefinition Put(int id, [FromBody] SiteGroupDefinition siteGroupDefinition) - { - if (ModelState.IsValid && siteGroupDefinition.SiteGroupDefinitionId == id) - { - if (!User.IsInRole(RoleNames.Host) && siteGroupDefinition.Synchronize) - { - // admins can only update the synchronize field - siteGroupDefinition = _siteGroupDefinitionRepository.GetSiteGroupDefinition(siteGroupDefinition.SiteGroupDefinitionId, false); - siteGroupDefinition.Synchronize = true; - } - siteGroupDefinition = _siteGroupDefinitionRepository.UpdateSiteGroupDefinition(siteGroupDefinition); - _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupDefinition, siteGroupDefinition.SiteGroupDefinitionId, SyncEventActions.Update); - _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Definition Updated {Group}", siteGroupDefinition); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Definition Put Attempt {Group}", siteGroupDefinition); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - siteGroupDefinition = null; - } - return siteGroupDefinition; - } - - // DELETE api//5 - [HttpDelete("{id}")] - [Authorize(Roles = RoleNames.Host)] - public void Delete(int id) - { - var siteGroupDefinition = _siteGroupDefinitionRepository.GetSiteGroupDefinition(id); - if (siteGroupDefinition != null) - { - _siteGroupDefinitionRepository.DeleteSiteGroupDefinition(id); - _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupDefinition, siteGroupDefinition.SiteGroupDefinitionId, SyncEventActions.Delete); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Definition Deleted {siteGroupDefinitionId}", id); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Definition Delete Attempt {siteGroupDefinitionId}", id); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - } - } - } -} diff --git a/Oqtane.Server/Controllers/SiteGroupMemberController.cs b/Oqtane.Server/Controllers/SiteGroupMemberController.cs new file mode 100644 index 00000000..2cb8442c --- /dev/null +++ b/Oqtane.Server/Controllers/SiteGroupMemberController.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class SiteGroupMemberController : Controller + { + private readonly ISiteGroupMemberRepository _siteGroupMemberRepository; + private readonly ISyncManager _syncManager; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public SiteGroupMemberController(ISiteGroupMemberRepository siteGroupMemberRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + { + _siteGroupMemberRepository = siteGroupMemberRepository; + _syncManager = syncManager; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET: api/?siteid=x&groupid=y + [HttpGet] + [Authorize(Roles = RoleNames.Host)] + public IEnumerable Get(string siteid, string groupid) + { + if (int.TryParse(siteid, out int SiteId) && int.TryParse(groupid, out int SiteGroupId)) + { + return _siteGroupMemberRepository.GetSiteGroupMembers(SiteId, SiteGroupId).ToList(); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Get Attempt for SiteId {SiteId} And SiteGroupId {SiteGroupId}", siteid, groupid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // GET api//5 + [HttpGet("{id}")] + [Authorize(Roles = RoleNames.Host)] + public SiteGroupMember Get(int id) + { + var siteGroupMember = _siteGroupMemberRepository.GetSiteGroupMember(id); + if (siteGroupMember != null) + { + return siteGroupMember; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + return null; + } + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Host)] + public SiteGroupMember Post([FromBody] SiteGroupMember siteGroupMember) + { + if (ModelState.IsValid) + { + siteGroupMember = _siteGroupMemberRepository.AddSiteGroupMember(siteGroupMember); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupId, SyncEventActions.Create); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Member Added {SiteGroupMember}", siteGroupMember); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Post Attempt {SiteGroupMember}", siteGroupMember); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroupMember = null; + } + return siteGroupMember; + } + + // PUT api//5 + [HttpPut("{id}")] + [Authorize(Roles = RoleNames.Host)] + public SiteGroupMember Put(int id, [FromBody] SiteGroupMember siteGroupMember) + { + if (ModelState.IsValid && siteGroupMember.SiteGroupId == id && _siteGroupMemberRepository.GetSiteGroupMember(siteGroupMember.SiteGroupId, false) != null) + { + siteGroupMember = _siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Member Updated {SiteGroupMember}", siteGroupMember); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Put Attempt {SiteGroupMember}", siteGroupMember); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroupMember = null; + } + return siteGroupMember; + } + + // DELETE api//5 + [HttpDelete("{id}")] + [Authorize(Roles = RoleNames.Host)] + public void Delete(int id) + { + var siteGroupMember = _siteGroupMemberRepository.GetSiteGroupMember(id); + if (siteGroupMember != null) + { + _siteGroupMemberRepository.DeleteSiteGroupMember(id); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupMemberId, SyncEventActions.Delete); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Member Deleted {SiteGroupMemberId}", id); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Delete Attempt {SiteGroupMemberId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index a541ebfd..8249560a 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -233,8 +233,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); @@ -284,8 +284,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); + services.AddTransient(); // managers services.AddTransient(); diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 356f9640..6b8c3897 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -38,80 +38,76 @@ namespace Oqtane.Infrastructure { string log = ""; - var siteGroupDefinitionRepository = provider.GetRequiredService(); var siteGroupRepository = provider.GetRequiredService(); + var siteGroupMemberRepository = provider.GetRequiredService(); var siteRepository = provider.GetRequiredService(); var aliasRepository = provider.GetRequiredService(); var tenantManager = provider.GetRequiredService(); var settingRepository = provider.GetRequiredService(); - List siteGroups = null; + List siteGroupMembers = null; List sites = null; List aliases = null; // get site groups - var siteGroupDefinitions = siteGroupDefinitionRepository.GetSiteGroupDefinitions(); + var siteGroups = siteGroupRepository.GetSiteGroups(); // iterate through site groups which need to be synchronized - foreach (var siteGroupDefinition in siteGroupDefinitions.Where(item => item.Synchronization && item.Synchronize)) + foreach (var siteGroup in siteGroups.Where(item => item.Type == SiteGroupTypes.Synchronization && item.Synchronize)) { // get data - if (siteGroups == null) + if (siteGroupMembers == null) { - siteGroups = siteGroupRepository.GetSiteGroups().ToList(); + siteGroupMembers = siteGroupMemberRepository.GetSiteGroupMembers().ToList(); sites = siteRepository.GetSites().ToList(); aliases = aliasRepository.GetAliases().ToList(); } - var aliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroupDefinition.PrimarySiteId && item.IsDefault).Name; - log += $"Processing Primary Site: {sites.First(item => item.SiteId == siteGroupDefinition.PrimarySiteId).Name} - {CreateLink(aliasName)}
"; + var aliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroup.PrimarySiteId && item.IsDefault).Name; + log += $"Processing Primary Site: {sites.First(item => item.SiteId == siteGroup.PrimarySiteId).Name} - {CreateLink(aliasName)}
"; // get primary site - var primarySite = sites.FirstOrDefault(item => item.SiteId == siteGroupDefinition.PrimarySiteId); + var primarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.PrimarySiteId); if (primarySite != null) { // update flag to prevent job from processing group again - siteGroupDefinition.Synchronize = false; - siteGroupDefinitionRepository.UpdateSiteGroupDefinition(siteGroupDefinition); + siteGroup.Synchronize = false; + siteGroupRepository.UpdateSiteGroup(siteGroup); // iterate through sites in site group - foreach (var siteGroup in siteGroups.Where(item => item.SiteGroupDefinitionId == siteGroupDefinition.SiteGroupDefinitionId && item.SiteId != siteGroupDefinition.PrimarySiteId)) + foreach (var siteGroupMember in siteGroupMembers.Where(item => item.SiteGroupId == siteGroup.SiteGroupId && item.SiteId != siteGroup.PrimarySiteId)) { // get secondary site - var secondarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.SiteId); + var secondarySite = sites.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId); if (secondarySite != null) { // get default alias for site - siteGroup.AliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroup.SiteId && item.IsDefault).Name; + siteGroupMember.AliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroupMember.SiteId && item.IsDefault).Name; // initialize SynchronizedOn - if (siteGroup.SynchronizedOn == null) + if (siteGroupMember.SynchronizedOn == null) { - siteGroup.SynchronizedOn = DateTime.MinValue; - } - if (siteGroup.SiteGroupDefinition.Localization) - { - siteGroup.Synchronize = false; // when using localization, do not overwrite content + siteGroupMember.SynchronizedOn = DateTime.MinValue; } // replicate site - var siteLog = ReplicateSite(provider, tenantManager, settingRepository, siteGroup, primarySite, secondarySite); + var siteLog = ReplicateSite(provider, tenantManager, settingRepository, siteGroupMember, primarySite, secondarySite); // set synchronized on date/time - siteGroup.SynchronizedOn = DateTime.UtcNow; - siteGroupRepository.UpdateSiteGroup(siteGroup); + siteGroupMember.SynchronizedOn = DateTime.UtcNow; + siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember); - log += $"Processed Secondary Site: {secondarySite.Name} - {CreateLink(siteGroup.AliasName)}
" + siteLog; + log += $"Processed Secondary Site: {secondarySite.Name} - {CreateLink(siteGroupMember.AliasName)}
" + siteLog; } else { - log += $"Site Group {siteGroupDefinition.Name} Has A SiteId {siteGroup.SiteId} Which Does Not Exist
"; + log += $"Site Group {siteGroup.Name} Has A SiteId {siteGroupMember.SiteId} Which Does Not Exist
"; } } } else { - log += $"Site Group {siteGroupDefinition.Name} Has A PrimarySiteId {siteGroupDefinition.PrimarySiteId} Which Does Not Exist
"; + log += $"Site Group {siteGroup.Name} Has A PrimarySiteId {siteGroup.PrimarySiteId} Which Does Not Exist
"; } } @@ -123,21 +119,21 @@ namespace Oqtane.Infrastructure return log; } - private string ReplicateSite(IServiceProvider provider, ITenantManager tenantManager, ISettingRepository settingRepository, SiteGroup siteGroup, Site primarySite, Site secondarySite) + private string ReplicateSite(IServiceProvider provider, ITenantManager tenantManager, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, Site primarySite, Site secondarySite) { var log = ""; // replicate roles/users - log += ReplicateRoles(provider, settingRepository, siteGroup, primarySite.SiteId, secondarySite.SiteId); + log += ReplicateRoles(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); // replicate folders/files - log += ReplicateFolders(provider, settingRepository, siteGroup, primarySite.SiteId, secondarySite.SiteId); + log += ReplicateFolders(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); // replicate pages/modules - log += ReplicatePages(provider, settingRepository, tenantManager, siteGroup, primarySite.SiteId, secondarySite.SiteId); + log += ReplicatePages(provider, settingRepository, tenantManager, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); // replicate site - if (primarySite.ModifiedOn > siteGroup.SynchronizedOn) + if (primarySite.ModifiedOn > siteGroupMember.SynchronizedOn) { secondarySite.TimeZoneId = primarySite.TimeZoneId; secondarySite.CultureCode = primarySite.CultureCode; @@ -182,17 +178,14 @@ namespace Oqtane.Infrastructure secondarySite.DeletedOn = primarySite.DeletedOn; var siteRepository = provider.GetRequiredService(); - if (siteGroup.Synchronize) - { - siteRepository.UpdateSite(secondarySite); - } - log += Log(siteGroup, $"Site Updated: {secondarySite.Name}"); + siteRepository.UpdateSite(secondarySite); + log += Log(siteGroupMember, $"Site Updated: {secondarySite.Name}"); } // site settings - log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Site, primarySite.SiteId, secondarySite.SiteId); + log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Site, primarySite.SiteId, secondarySite.SiteId); - if (siteGroup.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) + if (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) { // clear cache for secondary site if any content was replicated var syncManager = provider.GetRequiredService(); @@ -200,7 +193,7 @@ namespace Oqtane.Infrastructure syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySite.SiteId, SyncEventActions.Refresh); } - if (!string.IsNullOrEmpty(log) && siteGroup.Notify) + if (!string.IsNullOrEmpty(log) && siteGroupMember.Notify) { // send change log to administrators SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, log); @@ -220,7 +213,7 @@ namespace Oqtane.Infrastructure return fileId; } - private string ReplicateRoles(IServiceProvider provider, ISettingRepository settingRepository, SiteGroup siteGroup, int primarySiteId, int secondarySiteId) + private string ReplicateRoles(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) { // get roles var roleRepository = provider.GetRequiredService(); @@ -239,7 +232,7 @@ namespace Oqtane.Infrastructure secondaryRole.SiteId = secondarySiteId; } - if (role == null || primaryRole.ModifiedOn > siteGroup.SynchronizedOn) + if (role == null || primaryRole.ModifiedOn > siteGroupMember.SynchronizedOn) { // set all properties secondaryRole.Name = primaryRole.Name; @@ -249,19 +242,13 @@ namespace Oqtane.Infrastructure if (role == null) { - if (siteGroup.Synchronize) - { - roleRepository.AddRole(secondaryRole); - } - log += Log(siteGroup, $"Role Added: {secondaryRole.Name}"); + roleRepository.AddRole(secondaryRole); + log += Log(siteGroupMember, $"Role Added: {secondaryRole.Name}"); } else { - if (siteGroup.Synchronize) - { - roleRepository.UpdateRole(secondaryRole); - } - log += Log(siteGroup, $"Role Updated: {secondaryRole.Name}"); + roleRepository.UpdateRole(secondaryRole); + log += Log(siteGroupMember, $"Role Updated: {secondaryRole.Name}"); secondaryRoles.Remove(role); } } @@ -270,20 +257,17 @@ namespace Oqtane.Infrastructure // remove roles in the secondary site which do not exist in the primary site foreach (var secondaryRole in secondaryRoles.Where(item => !primaryRoles.Select(item => item.Name).Contains(item.Name))) { - if (siteGroup.Synchronize) - { - roleRepository.DeleteRole(secondaryRole.RoleId); - } - log += Log(siteGroup, $"Role Deleted: {secondaryRole.Name}"); + roleRepository.DeleteRole(secondaryRole.RoleId); + log += Log(siteGroupMember, $"Role Deleted: {secondaryRole.Name}"); } // settings - log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Role, primarySiteId, secondarySiteId); + log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Role, primarySiteId, secondarySiteId); return log; } - private string ReplicateFolders(IServiceProvider provider, ISettingRepository settingRepository, SiteGroup siteGroup, int primarySiteId, int secondarySiteId) + private string ReplicateFolders(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) { var folderRepository = provider.GetRequiredService(); var fileRepository = provider.GetRequiredService(); @@ -305,7 +289,7 @@ namespace Oqtane.Infrastructure secondaryFolder.SiteId = secondarySiteId; } - if (folder == null || primaryFolder.ModifiedOn > siteGroup.SynchronizedOn) + if (folder == null || primaryFolder.ModifiedOn > siteGroupMember.SynchronizedOn) { // set all properties secondaryFolder.ParentId = null; @@ -328,25 +312,19 @@ namespace Oqtane.Infrastructure if (folder == null) { - if (siteGroup.Synchronize) - { - folderRepository.AddFolder(secondaryFolder); - } - log += Log(siteGroup, $"Folder Added: {secondaryFolder.Path}"); + folderRepository.AddFolder(secondaryFolder); + log += Log(siteGroupMember, $"Folder Added: {secondaryFolder.Path}"); } else { - if (siteGroup.Synchronize) - { - folderRepository.UpdateFolder(secondaryFolder); - } - log += Log(siteGroup, $"Folder Updated: {secondaryFolder.Path}"); + folderRepository.UpdateFolder(secondaryFolder); + log += Log(siteGroupMember, $"Folder Updated: {secondaryFolder.Path}"); secondaryFolders.Remove(folder); } } // folder settings - log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId); + log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId); // get files for folder var primaryFiles = fileRepository.GetFiles(primaryFolder.FolderId); @@ -364,7 +342,7 @@ namespace Oqtane.Infrastructure secondaryFile.Name = primaryFile.Name; } - if (file == null || primaryFile.ModifiedOn > siteGroup.SynchronizedOn) + if (file == null || primaryFile.ModifiedOn > siteGroupMember.SynchronizedOn) { // set all properties secondaryFile.Extension = primaryFile.Extension; @@ -375,21 +353,15 @@ namespace Oqtane.Infrastructure if (file == null) { - if (siteGroup.Synchronize) - { - fileRepository.AddFile(secondaryFile); - ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); - } - log += Log(siteGroup, $"File Added: {CreateLink(siteGroup.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); + fileRepository.AddFile(secondaryFile); + ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + log += Log(siteGroupMember, $"File Added: {CreateLink(siteGroupMember.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); } else { - if (siteGroup.Synchronize) - { - fileRepository.UpdateFile(secondaryFile); - ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); - } - log += Log(siteGroup, $"File Updated: {CreateLink(siteGroup.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); + fileRepository.UpdateFile(secondaryFile); + ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); secondaryFiles.Remove(file); } } @@ -398,24 +370,18 @@ namespace Oqtane.Infrastructure // remove files in the secondary site which do not exist in the primary site foreach (var secondaryFile in secondaryFiles.Where(item => !primaryFiles.Select(item => item.Name).Contains(item.Name))) { - if (siteGroup.Synchronize) - { - fileRepository.DeleteFile(secondaryFile.FileId); - var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); - System.IO.File.Delete(secondaryPath); - } - log += Log(siteGroup, $"File Deleted: {CreateLink(siteGroup.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); + fileRepository.DeleteFile(secondaryFile.FileId); + var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); + System.IO.File.Delete(secondaryPath); + log += Log(siteGroupMember, $"File Deleted: {CreateLink(siteGroupMember.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); } } // remove folders in the secondary site which do not exist in the primary site foreach (var secondaryFolder in secondaryFolders.Where(item => !primaryFolders.Select(item => item.Path).Contains(item.Path))) { - if (siteGroup.Synchronize) - { - folderRepository.DeleteFolder(secondaryFolder.FolderId); - } - log += Log(siteGroup, $"Folder Deleted: {secondaryFolder.Path}"); + folderRepository.DeleteFolder(secondaryFolder.FolderId); + log += Log(siteGroupMember, $"Folder Deleted: {secondaryFolder.Path}"); } return log; @@ -435,7 +401,7 @@ namespace Oqtane.Infrastructure } } - private string ReplicatePages(IServiceProvider provider, ISettingRepository settingRepository, ITenantManager tenantManager, SiteGroup siteGroup, int primarySiteId, int secondarySiteId) + private string ReplicatePages(IServiceProvider provider, ISettingRepository settingRepository, ITenantManager tenantManager, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) { var pageRepository = provider.GetRequiredService(); var pageModuleRepository = provider.GetRequiredService(); @@ -463,7 +429,7 @@ namespace Oqtane.Infrastructure secondaryPage.SiteId = secondarySiteId; } - if (page == null || primaryPage.ModifiedOn > siteGroup.SynchronizedOn) + if (page == null || primaryPage.ModifiedOn > siteGroupMember.SynchronizedOn) { // set all properties secondaryPage.Path = primaryPage.Path; @@ -502,25 +468,19 @@ namespace Oqtane.Infrastructure if (page == null) { - if (siteGroup.Synchronize) - { - secondaryPage = pageRepository.AddPage(secondaryPage); - } - log += Log(siteGroup, $"Page Added: {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + secondaryPage = pageRepository.AddPage(secondaryPage); + log += Log(siteGroupMember, $"Page Added: {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } else { - if (siteGroup.Synchronize) - { - secondaryPage = pageRepository.UpdatePage(secondaryPage); - } - log += Log(siteGroup, $"Page Updated: {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + secondaryPage = pageRepository.UpdatePage(secondaryPage); + log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); secondaryPages.Remove(page); } } // page settings - log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId); + log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId); // modules if (primaryPageModules == null) @@ -547,7 +507,7 @@ namespace Oqtane.Infrastructure secondaryPageModule.Module.ModuleDefinitionName = primaryPageModule.Module.ModuleDefinitionName; } - if (pageModule == null || primaryPageModule.ModifiedOn > siteGroup.SynchronizedOn || primaryPageModule.Module.ModifiedOn > siteGroup.SynchronizedOn) + if (pageModule == null || primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn || primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn) { // set all properties secondaryPageModule.Title = primaryPageModule.Title; @@ -570,44 +530,32 @@ namespace Oqtane.Infrastructure if (module == null) { // add new module - if (siteGroup.Synchronize) - { - module = moduleRepository.AddModule(secondaryPageModule.Module); - updateContent = true; - } - log += Log(siteGroup, $"Module Added: {module.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + module = moduleRepository.AddModule(secondaryPageModule.Module); + updateContent = true; + log += Log(siteGroupMember, $"Module Added: {module.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } if (module != null) { secondaryPageModule.ModuleId = module.ModuleId; secondaryPageModule.Module = null; // remove tracking - if (siteGroup.Synchronize) - { - secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); - } - log += Log(siteGroup, $"Module Instance Added: {module.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); + log += Log(siteGroupMember, $"Module Instance Added: {module.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); secondaryPageModule.Module = module; } } else { // update existing module - if (primaryPageModule.Module.ModifiedOn > siteGroup.SynchronizedOn) + if (primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn) { - if (siteGroup.Synchronize) - { - moduleRepository.UpdateModule(secondaryPageModule.Module); - updateContent = true; - } - log += Log(siteGroup, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + moduleRepository.UpdateModule(secondaryPageModule.Module); + updateContent = true; + log += Log(siteGroupMember, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } - if (primaryPageModule.ModifiedOn > siteGroup.SynchronizedOn) + if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn) { - if (siteGroup.Synchronize) - { - secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); - } - log += Log(siteGroup, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); + log += Log(siteGroupMember, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); secondaryPageModules.Remove(pageModule); } } @@ -625,11 +573,8 @@ namespace Oqtane.Infrastructure var secondaryModuleContent = ((ISynchronizable)moduleObject).ExtractModule(secondaryPageModule.Module); if (primaryModuleContent != secondaryModuleContent) { - if (siteGroup.Synchronize) - { - ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, primaryModuleContent, primaryPageModule.Module.ModuleDefinition.Version); - } - log += Log(siteGroup, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, primaryModuleContent, primaryPageModule.Module.ModuleDefinition.Version); + log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } } catch @@ -641,7 +586,7 @@ namespace Oqtane.Infrastructure } // module settings - log += ReplicateSettings(settingRepository, siteGroup, EntityNames.Module, primaryPageModule.ModuleId, secondaryPageModule.ModuleId); + log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, secondaryPageModule.ModuleId); } } @@ -660,25 +605,19 @@ namespace Oqtane.Infrastructure } if (!primaryPageModules.Any(item => item.PageId == primaryPageId && item.Module.ModuleDefinitionName == secondaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == secondaryPageModule.Title.ToLower())) { - if (siteGroup.Synchronize) - { - pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); - } - log += Log(siteGroup, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroup.AliasName + secondaryPageModule.Page.Path)}"); + pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); + log += Log(siteGroupMember, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPageModule.Page.Path)}"); } } // remove pages in the secondary site which do not exist in the primary site foreach (var secondaryPage in secondaryPages.Where(item => !primaryPages.Select(item => item.Path).Contains(item.Path))) { - if (siteGroup.Synchronize) - { - pageRepository.DeletePage(secondaryPage.PageId); - } - log += Log(siteGroup, $"Page Deleted: {CreateLink(siteGroup.AliasName + secondaryPage.Path)}"); + pageRepository.DeletePage(secondaryPage.PageId); + log += Log(siteGroupMember, $"Page Deleted: {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } - if (siteGroup.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) + if (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) { // clear cache for secondary site if any content was replicated var syncManager = provider.GetRequiredService(); @@ -705,7 +644,7 @@ namespace Oqtane.Infrastructure }).ToList(); } - private string ReplicateSettings(ISettingRepository settingRepository, SiteGroup siteGroup, string entityName, int primaryEntityId, int secondaryEntityId) + private string ReplicateSettings(ISettingRepository settingRepository, SiteGroupMember siteGroupMember, string entityName, int primaryEntityId, int secondaryEntityId) { var log = ""; var updated = false; @@ -722,7 +661,7 @@ namespace Oqtane.Infrastructure secondarySetting.SettingName = primarySetting.SettingName; secondarySetting.SettingValue = primarySetting.SettingValue; secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.AddSetting(secondarySetting); updated = true; @@ -734,7 +673,7 @@ namespace Oqtane.Infrastructure { secondarySetting.SettingValue = primarySetting.SettingValue; secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.UpdateSetting(secondarySetting); updated = true; @@ -747,7 +686,7 @@ namespace Oqtane.Infrastructure // any remaining secondary settings need to be deleted foreach (var secondarySetting in secondarySettings) { - if (siteGroup.Synchronize && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.DeleteSetting(secondarySetting.EntityName, secondarySetting.SettingId); updated = true; @@ -756,7 +695,7 @@ namespace Oqtane.Infrastructure if (updated) { - log += Log(siteGroup, $"{entityName} Settings Updated"); + log += Log(siteGroupMember, $"{entityName} Settings Updated"); } return log; @@ -774,10 +713,10 @@ namespace Oqtane.Infrastructure } } - private string Log(SiteGroup siteGroup, string content) + private string Log(SiteGroupMember siteGroupMember, string content) { // not necessary to log initial replication - if (siteGroup.SynchronizedOn != DateTime.MinValue) + if (siteGroupMember.SynchronizedOn != DateTime.MinValue) { return content + "
"; } diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs deleted file mode 100644 index 72974ecb..00000000 --- a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupDefinitionEntityBuilder.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Migrations.Operations; -using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; -using Oqtane.Databases.Interfaces; - -// ReSharper disable MemberCanBePrivate.Global -// ReSharper disable UnusedAutoPropertyAccessor.Global - -namespace Oqtane.Migrations.EntityBuilders -{ - public class SiteGroupDefinitionEntityBuilder : AuditableBaseEntityBuilder - { - private const string _entityTableName = "SiteGroupDefinition"; - private readonly PrimaryKey _primaryKey = new("PK_SiteGroupDefinition", x => x.SiteGroupDefinitionId); - - public SiteGroupDefinitionEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) - { - EntityTableName = _entityTableName; - PrimaryKey = _primaryKey; - } - - protected override SiteGroupDefinitionEntityBuilder BuildTable(ColumnsBuilder table) - { - SiteGroupDefinitionId = AddAutoIncrementColumn(table, "SiteGroupDefinitionId"); - Name = AddStringColumn(table, "Name", 200); - PrimarySiteId = AddIntegerColumn(table, "PrimarySiteId"); - Synchronization = AddBooleanColumn(table, "Synchronization"); - Synchronize = AddBooleanColumn(table, "Synchronize"); - Localization = AddBooleanColumn(table, "Localization"); - - AddAuditableColumns(table); - - return this; - } - - public OperationBuilder SiteGroupDefinitionId { get; set; } - - public OperationBuilder Name { get; set; } - - public OperationBuilder PrimarySiteId { get; set; } - - public OperationBuilder Synchronization { get; set; } - - public OperationBuilder Synchronize { get; set; } - - public OperationBuilder Localization { get; set; } - } -} diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs index a6663315..3af17760 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs @@ -12,25 +12,20 @@ namespace Oqtane.Migrations.EntityBuilders { private const string _entityTableName = "SiteGroup"; private readonly PrimaryKey _primaryKey = new("PK_SiteGroup", x => x.SiteGroupId); - private readonly ForeignKey _groupForeignKey = new("FK_SiteGroup_SiteGroupDefinition", x => x.SiteGroupDefinitionId, "SiteGroupDefinition", "SiteGroupDefinitionId", ReferentialAction.Cascade); - private readonly ForeignKey _siteForeignKey = new("FK_SiteGroup_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); public SiteGroupEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) { EntityTableName = _entityTableName; PrimaryKey = _primaryKey; - ForeignKeys.Add(_groupForeignKey); - ForeignKeys.Add(_siteForeignKey); } protected override SiteGroupEntityBuilder BuildTable(ColumnsBuilder table) { SiteGroupId = AddAutoIncrementColumn(table, "SiteGroupId"); - SiteGroupDefinitionId = AddIntegerColumn(table, "SiteGroupDefinitionId"); - SiteId = AddIntegerColumn(table, "SiteId"); + Name = AddStringColumn(table, "Name", 200); + Type = AddStringColumn(table, "Type", 50); + PrimarySiteId = AddIntegerColumn(table, "PrimarySiteId"); Synchronize = AddBooleanColumn(table, "Synchronize"); - Notify = AddBooleanColumn(table, "Notify"); - SynchronizedOn = AddDateTimeColumn(table, "SynchronizedOn", true); AddAuditableColumns(table); @@ -39,14 +34,12 @@ namespace Oqtane.Migrations.EntityBuilders public OperationBuilder SiteGroupId { get; set; } - public OperationBuilder SiteGroupDefinitionId { get; set; } + public OperationBuilder Name { get; set; } - public OperationBuilder SiteId { get; set; } + public OperationBuilder Type { get; set; } + + public OperationBuilder PrimarySiteId { get; set; } public OperationBuilder Synchronize { get; set; } - - public OperationBuilder Notify { get; set; } - - public OperationBuilder SynchronizedOn { get; set; } } } diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs new file mode 100644 index 00000000..92b6cfd2 --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class SiteGroupMemberEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "SiteGroupMember"; + private readonly PrimaryKey _primaryKey = new("PK_SiteGroupMember", x => x.SiteGroupMemberId); + private readonly ForeignKey _groupForeignKey = new("FK_SiteGroupMember_SiteGroup", x => x.SiteGroupId, "SiteGroup", "SiteGroupId", ReferentialAction.Cascade); + private readonly ForeignKey _siteForeignKey = new("FK_SiteGroupMember_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); + + public SiteGroupMemberEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_groupForeignKey); + ForeignKeys.Add(_siteForeignKey); + } + + protected override SiteGroupMemberEntityBuilder BuildTable(ColumnsBuilder table) + { + SiteGroupMemberId = AddAutoIncrementColumn(table, "SiteGroupMemberId"); + SiteGroupId = AddIntegerColumn(table, "SiteGroupId"); + SiteId = AddIntegerColumn(table, "SiteId"); + Notify = AddBooleanColumn(table, "Notify"); + SynchronizedOn = AddDateTimeColumn(table, "SynchronizedOn", true); + + AddAuditableColumns(table); + + return this; + } + + public OperationBuilder SiteGroupMemberId { get; set; } + + public OperationBuilder SiteGroupId { get; set; } + + public OperationBuilder SiteId { get; set; } + + public OperationBuilder Notify { get; set; } + + public OperationBuilder SynchronizedOn { get; set; } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs b/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs index a8e767c2..f504e86d 100644 --- a/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs +++ b/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs @@ -16,12 +16,12 @@ namespace Oqtane.Migrations.Tenant protected override void Up(MigrationBuilder migrationBuilder) { - var siteGroupDefinitionEntityBuilder = new SiteGroupDefinitionEntityBuilder(migrationBuilder, ActiveDatabase); - siteGroupDefinitionEntityBuilder.Create(); - var siteGroupEntityBuilder = new SiteGroupEntityBuilder(migrationBuilder, ActiveDatabase); siteGroupEntityBuilder.Create(); - siteGroupEntityBuilder.AddIndex("IX_SiteGroup", new[] { "SiteId", "SiteGroupDefinitionId" }, true); + + var siteGroupMemberEntityBuilder = new SiteGroupMemberEntityBuilder(migrationBuilder, ActiveDatabase); + siteGroupMemberEntityBuilder.Create(); + siteGroupMemberEntityBuilder.AddIndex("IX_SiteGroupMember", new[] { "SiteId", "SiteGroupId" }, true); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Oqtane.Server/Repository/Context/TenantDBContext.cs b/Oqtane.Server/Repository/Context/TenantDBContext.cs index 0c970c3b..f6325fd9 100644 --- a/Oqtane.Server/Repository/Context/TenantDBContext.cs +++ b/Oqtane.Server/Repository/Context/TenantDBContext.cs @@ -134,7 +134,7 @@ namespace Oqtane.Repository public virtual DbSet SearchContentWord { get; set; } public virtual DbSet SearchWord { get; set; } public virtual DbSet MigrationHistory { get; set; } - public virtual DbSet SiteGroupDefinition { get; set; } public virtual DbSet SiteGroup { get; set; } + public virtual DbSet SiteGroupMember { get; set; } } } diff --git a/Oqtane.Server/Repository/SiteGroupDefinitionRepository.cs b/Oqtane.Server/Repository/SiteGroupDefinitionRepository.cs deleted file mode 100644 index 1d1019be..00000000 --- a/Oqtane.Server/Repository/SiteGroupDefinitionRepository.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using Oqtane.Models; - -namespace Oqtane.Repository -{ - public interface ISiteGroupDefinitionRepository - { - IEnumerable GetSiteGroupDefinitions(); - SiteGroupDefinition AddSiteGroupDefinition(SiteGroupDefinition siteGroupDefinition); - SiteGroupDefinition UpdateSiteGroupDefinition(SiteGroupDefinition siteGroupDefinition); - SiteGroupDefinition GetSiteGroupDefinition(int siteGroupDefinitionId); - SiteGroupDefinition GetSiteGroupDefinition(int siteGroupDefinitionId, bool tracking); - void DeleteSiteGroupDefinition(int siteGroupDefinitionId); - } - - public class SiteGroupDefinitionRepository : ISiteGroupDefinitionRepository - { - private readonly IDbContextFactory _dbContextFactory; - - public SiteGroupDefinitionRepository(IDbContextFactory dbContextFactory) - { - _dbContextFactory = dbContextFactory; - } - - public IEnumerable GetSiteGroupDefinitions() - { - using var db = _dbContextFactory.CreateDbContext(); - return db.SiteGroupDefinition.ToList(); - } - - public SiteGroupDefinition AddSiteGroupDefinition(SiteGroupDefinition siteGroupDefinition) - { - using var db = _dbContextFactory.CreateDbContext(); - db.SiteGroupDefinition.Add(siteGroupDefinition); - db.SaveChanges(); - return siteGroupDefinition; - } - - public SiteGroupDefinition UpdateSiteGroupDefinition(SiteGroupDefinition siteGroupDefinition) - { - using var db = _dbContextFactory.CreateDbContext(); - db.Entry(siteGroupDefinition).State = EntityState.Modified; - db.SaveChanges(); - return siteGroupDefinition; - } - - public SiteGroupDefinition GetSiteGroupDefinition(int siteGroupDefinitionId) - { - return GetSiteGroupDefinition(siteGroupDefinitionId, true); - } - - public SiteGroupDefinition GetSiteGroupDefinition(int siteGroupDefinitionId, bool tracking) - { - using var db = _dbContextFactory.CreateDbContext(); - if (tracking) - { - return db.SiteGroupDefinition.FirstOrDefault(item => item.SiteGroupDefinitionId == siteGroupDefinitionId); - } - else - { - return db.SiteGroupDefinition.AsNoTracking().FirstOrDefault(item => item.SiteGroupDefinitionId == siteGroupDefinitionId); - } - } - - public void DeleteSiteGroupDefinition(int siteGroupDefinitionId) - { - using var db = _dbContextFactory.CreateDbContext(); - SiteGroupDefinition group = db.SiteGroupDefinition.Find(siteGroupDefinitionId); - db.SiteGroupDefinition.Remove(group); - db.SaveChanges(); - } - } -} diff --git a/Oqtane.Server/Repository/SiteGroupMemberRepository.cs b/Oqtane.Server/Repository/SiteGroupMemberRepository.cs new file mode 100644 index 00000000..82efb37a --- /dev/null +++ b/Oqtane.Server/Repository/SiteGroupMemberRepository.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface ISiteGroupMemberRepository + { + IEnumerable GetSiteGroupMembers(); + IEnumerable GetSiteGroupMembers(int siteId, int siteGroupId); + SiteGroupMember AddSiteGroupMember(SiteGroupMember siteGroupMember); + SiteGroupMember UpdateSiteGroupMember(SiteGroupMember siteGroupMember); + SiteGroupMember GetSiteGroupMember(int siteGroupMemberId); + SiteGroupMember GetSiteGroupMember(int siteGroupMemberId, bool tracking); + void DeleteSiteGroupMember(int siteGroupMemberId); + } + + public class SiteGroupMemberRepository : ISiteGroupMemberRepository + { + private readonly IDbContextFactory _dbContextFactory; + + public SiteGroupMemberRepository(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public IEnumerable GetSiteGroupMembers() + { + return GetSiteGroupMembers(-1, -1); + } + + public IEnumerable GetSiteGroupMembers(int siteId, int siteGroupId) + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteGroupMember + .Where(item => (siteId == -1 || item.SiteId == siteId) && (siteGroupId == -1 || item.SiteGroupId == siteGroupId)) + .Include(item => item.SiteGroup) // eager load + .ToList(); + } + + public SiteGroupMember AddSiteGroupMember(SiteGroupMember siteGroupMember) + { + using var db = _dbContextFactory.CreateDbContext(); + db.SiteGroupMember.Add(siteGroupMember); + db.SaveChanges(); + return siteGroupMember; + } + + public SiteGroupMember UpdateSiteGroupMember(SiteGroupMember siteGroupMember) + { + using var db = _dbContextFactory.CreateDbContext(); + db.Entry(siteGroupMember).State = EntityState.Modified; + db.SaveChanges(); + return siteGroupMember; + } + + public SiteGroupMember GetSiteGroupMember(int siteGroupMemberId) + { + return GetSiteGroupMember(siteGroupMemberId, true); + } + + public SiteGroupMember GetSiteGroupMember(int siteGroupMemberId, bool tracking) + { + using var db = _dbContextFactory.CreateDbContext(); + if (tracking) + { + return db.SiteGroupMember + .Include(item => item.SiteGroup) // eager load + .FirstOrDefault(item => item.SiteGroupMemberId == siteGroupMemberId); + } + else + { + return db.SiteGroupMember.AsNoTracking() + .Include(item => item.SiteGroup) // eager load + .FirstOrDefault(item => item.SiteGroupMemberId == siteGroupMemberId); + } + } + + public void DeleteSiteGroupMember(int siteGroupMemberId) + { + using var db = _dbContextFactory.CreateDbContext(); + SiteGroupMember SiteGroupMember = db.SiteGroupMember.Find(siteGroupMemberId); + db.SiteGroupMember.Remove(SiteGroupMember); + db.SaveChanges(); + } + } +} diff --git a/Oqtane.Server/Repository/SiteGroupRepository.cs b/Oqtane.Server/Repository/SiteGroupRepository.cs index 392f83e7..883974c3 100644 --- a/Oqtane.Server/Repository/SiteGroupRepository.cs +++ b/Oqtane.Server/Repository/SiteGroupRepository.cs @@ -8,12 +8,11 @@ namespace Oqtane.Repository public interface ISiteGroupRepository { IEnumerable GetSiteGroups(); - IEnumerable GetSiteGroups(int siteId, int siteGroupDefinitionId); SiteGroup AddSiteGroup(SiteGroup siteGroup); SiteGroup UpdateSiteGroup(SiteGroup siteGroup); - SiteGroup GetSiteGroup(int siteSiteGroupId); - SiteGroup GetSiteGroup(int siteSiteGroupId, bool tracking); - void DeleteSiteGroup(int siteSiteGroupId); + SiteGroup GetSiteGroup(int siteGroupId); + SiteGroup GetSiteGroup(int siteGroupId, bool tracking); + void DeleteSiteGroup(int siteGroupId); } public class SiteGroupRepository : ISiteGroupRepository @@ -24,64 +23,52 @@ namespace Oqtane.Repository { _dbContextFactory = dbContextFactory; } - + public IEnumerable GetSiteGroups() { - return GetSiteGroups(-1, -1); + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteGroup.ToList(); } - public IEnumerable GetSiteGroups(int siteId, int siteGroupDefinitionId) + public SiteGroup AddSiteGroup(SiteGroup siteGroup) { using var db = _dbContextFactory.CreateDbContext(); - return db.SiteGroup - .Where(item => (siteId == -1 || item.SiteId == siteId) && (siteGroupDefinitionId == -1 || item.SiteGroupDefinitionId == siteGroupDefinitionId)) - .Include(item => item.SiteGroupDefinition) // eager load - .ToList(); - } - - public SiteGroup AddSiteGroup(SiteGroup SiteGroup) - { - using var db = _dbContextFactory.CreateDbContext(); - db.SiteGroup.Add(SiteGroup); + db.SiteGroup.Add(siteGroup); db.SaveChanges(); - return SiteGroup; + return siteGroup; } - public SiteGroup UpdateSiteGroup(SiteGroup SiteGroup) + public SiteGroup UpdateSiteGroup(SiteGroup siteGroup) { using var db = _dbContextFactory.CreateDbContext(); - db.Entry(SiteGroup).State = EntityState.Modified; + db.Entry(siteGroup).State = EntityState.Modified; db.SaveChanges(); - return SiteGroup; + return siteGroup; } - public SiteGroup GetSiteGroup(int SiteGroupId) + public SiteGroup GetSiteGroup(int siteGroupId) { - return GetSiteGroup(SiteGroupId, true); + return GetSiteGroup(siteGroupId, true); } - public SiteGroup GetSiteGroup(int SiteGroupId, bool tracking) + public SiteGroup GetSiteGroup(int siteGroupId, bool tracking) { using var db = _dbContextFactory.CreateDbContext(); if (tracking) { - return db.SiteGroup - .Include(item => item.SiteGroupDefinition) // eager load - .FirstOrDefault(item => item.SiteGroupId == SiteGroupId); + return db.SiteGroup.FirstOrDefault(item => item.SiteGroupId == siteGroupId); } else { - return db.SiteGroup.AsNoTracking() - .Include(item => item.SiteGroupDefinition) // eager load - .FirstOrDefault(item => item.SiteGroupId == SiteGroupId); + return db.SiteGroup.AsNoTracking().FirstOrDefault(item => item.SiteGroupId == siteGroupId); } } - public void DeleteSiteGroup(int SiteGroupId) + public void DeleteSiteGroup(int siteGroupId) { using var db = _dbContextFactory.CreateDbContext(); - SiteGroup SiteGroup = db.SiteGroup.Find(SiteGroupId); - db.SiteGroup.Remove(SiteGroup); + SiteGroup group = db.SiteGroup.Find(siteGroupId); + db.SiteGroup.Remove(group); db.SaveChanges(); } } diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index 1cab31b9..e197faf0 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -21,7 +21,7 @@ namespace Oqtane.Services public class ServerSiteService : ISiteService { private readonly ISiteRepository _sites; - private readonly ISiteGroupRepository _siteGroups; + private readonly ISiteGroupMemberRepository _siteGroupMembers; private readonly IAliasRepository _aliases; private readonly IPageRepository _pages; private readonly IThemeRepository _themes; @@ -39,10 +39,10 @@ namespace Oqtane.Services private readonly IHttpContextAccessor _accessor; private readonly string _private = "[PRIVATE]"; - public ServerSiteService(ISiteRepository sites, ISiteGroupRepository siteGroups, IAliasRepository aliases, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserManager userManager, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) + public ServerSiteService(ISiteRepository sites, ISiteGroupMemberRepository siteGroupMembers, IAliasRepository aliases, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserManager userManager, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) { _sites = sites; - _siteGroups = siteGroups; + _siteGroupMembers = siteGroupMembers; _aliases = aliases; _pages = pages; _themes = themes; @@ -315,23 +315,23 @@ namespace Oqtane.Services { var languages = new List(); - var siteGroups = _siteGroups.GetSiteGroups(); - if (siteGroups.Any(item => item.SiteId == siteId && item.SiteGroupDefinition.Localization)) + var siteGroupMembers = _siteGroupMembers.GetSiteGroupMembers(); + if (siteGroupMembers.Any(item => item.SiteId == siteId && item.SiteGroup.Type == SiteGroupTypes.Localization)) { // site is part of a localized site group - get all languages from the site group var sites = _sites.GetSites().ToList(); var aliases = _aliases.GetAliases().ToList(); - foreach (var siteGroupDefinitionId in siteGroups.Where(item => item.SiteId == siteId && item.SiteGroupDefinition.Localization).Select(item => item.SiteGroupDefinitionId).Distinct().ToList()) + foreach (var siteGroupId in siteGroupMembers.Where(item => item.SiteId == siteId && item.SiteGroup.Type == SiteGroupTypes.Localization).Select(item => item.SiteGroupId).Distinct().ToList()) { - foreach (var siteGroup in siteGroups.Where(item => item.SiteGroupDefinitionId == siteGroupDefinitionId)) + foreach (var siteGroupMember in siteGroupMembers.Where(item => item.SiteGroupId == siteGroupId)) { - var site = sites.FirstOrDefault(item => item.SiteId == siteGroup.SiteId); + var site = sites.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId); if (site != null && !string.IsNullOrEmpty(site.CultureCode)) { if (!languages.Any(item => item.Code == site.CultureCode)) { - var alias = aliases.FirstOrDefault(item => item.SiteId == siteGroup.SiteId && item.TenantId == tenantId && item.IsDefault); + var alias = aliases.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId && item.TenantId == tenantId && item.IsDefault); if (alias != null) { languages.Add(new Language { Code = site.CultureCode, Name = "", AliasName = alias.Name, IsDefault = false }); diff --git a/Oqtane.Shared/Models/SiteGroup.cs b/Oqtane.Shared/Models/SiteGroup.cs index e6edc2e5..5d71709b 100644 --- a/Oqtane.Shared/Models/SiteGroup.cs +++ b/Oqtane.Shared/Models/SiteGroup.cs @@ -1,49 +1,30 @@ -using System; -using System.ComponentModel.DataAnnotations.Schema; - namespace Oqtane.Models { public class SiteGroup : ModelBase { /// - /// ID to identify the site group + /// ID to identify the group /// public int SiteGroupId { get; set; } /// - /// Reference to the . + /// Name of the group /// - public int SiteGroupDefinitionId { get; set; } + public string Name { get; set; } /// - /// Reference to the . + /// Group type ie. Synchronization, Localization /// - public int SiteId { get; set; } + public string Type { get; set; } /// - /// Specifies the site synchronization approach (false = compare, true = update) + /// SiteId of the primary site in the group /// - public bool Synchronize { get; set; } + public int PrimarySiteId { get; set; } /// - /// Indicates if the site administrator should be notified of any synchronization activity + /// Specifies if the group should be synchronized /// - public bool Notify { get; set; } - - /// - /// The last date/time the site was synchronized - /// - public DateTime? SynchronizedOn { get; set; } - - /// - /// The itself. - /// - public SiteGroupDefinition SiteGroupDefinition { get; set; } - - /// - /// The primary alias for the site - /// - [NotMapped] - public string AliasName { get; set; } + public bool Synchronize{ get; set; } } } diff --git a/Oqtane.Shared/Models/SiteGroupDefinition.cs b/Oqtane.Shared/Models/SiteGroupDefinition.cs deleted file mode 100644 index 4db114e4..00000000 --- a/Oqtane.Shared/Models/SiteGroupDefinition.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Oqtane.Models -{ - public class SiteGroupDefinition : ModelBase - { - /// - /// ID to identify the group - /// - public int SiteGroupDefinitionId { get; set; } - - /// - /// Name of the group - /// - public string Name { get; set; } - - /// - /// SiteId of the primary site in the group - /// - public int PrimarySiteId { get; set; } - - /// - /// Indicates if the group supports synchronization - /// - public bool Synchronization { get; set; } - - /// - /// Specifies if the group needs to be synchronized - /// - public bool Synchronize { get; set; } - - /// - /// Indicates if the group supports localization - /// - public bool Localization { get; set; } - } -} diff --git a/Oqtane.Shared/Models/SiteGroupMember.cs b/Oqtane.Shared/Models/SiteGroupMember.cs new file mode 100644 index 00000000..0101a4eb --- /dev/null +++ b/Oqtane.Shared/Models/SiteGroupMember.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Oqtane.Models +{ + public class SiteGroupMember : ModelBase + { + /// + /// ID to identify the site group + /// + public int SiteGroupMemberId { get; set; } + + /// + /// Reference to the . + /// + public int SiteGroupId { get; set; } + + /// + /// Reference to the . + /// + public int SiteId { get; set; } + + /// + /// Indicates if the site administrator should be notified of any synchronization activity + /// + public bool Notify { get; set; } + + /// + /// The last date/time the site was synchronized + /// + public DateTime? SynchronizedOn { get; set; } + + /// + /// The itself. + /// + public SiteGroup SiteGroup { get; set; } + + /// + /// The primary alias for the site + /// + [NotMapped] + public string AliasName { get; set; } + } +} diff --git a/Oqtane.Shared/Shared/EntityNames.cs b/Oqtane.Shared/Shared/EntityNames.cs index f7030223..72571e82 100644 --- a/Oqtane.Shared/Shared/EntityNames.cs +++ b/Oqtane.Shared/Shared/EntityNames.cs @@ -17,7 +17,7 @@ namespace Oqtane.Shared public const string Setting = "Setting"; public const string Site = "Site"; public const string SiteGroup = "SiteGroup"; - public const string SiteGroupDefinition = "SiteGroupDefinition"; + public const string SiteGroupMember = "SiteGroupMember"; public const string Tenant = "Tenant"; public const string Theme = "Theme"; public const string UrlMapping = "UrlMapping"; diff --git a/Oqtane.Shared/Shared/SiteGroupTypes.cs b/Oqtane.Shared/Shared/SiteGroupTypes.cs new file mode 100644 index 00000000..c1b028a5 --- /dev/null +++ b/Oqtane.Shared/Shared/SiteGroupTypes.cs @@ -0,0 +1,8 @@ +namespace Oqtane.Shared +{ + public class SiteGroupTypes + { + public const string Synchronization = "Synchronization"; + public const string Localization = "Localization"; + } +} From 6f2e676c0028397f31fef76b641e6957385b1b4c Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 10 Feb 2026 08:55:11 -0500 Subject: [PATCH 27/75] improvements to site groups --- Oqtane.Client/Modules/Admin/Site/Index.razor | 83 ++++----- .../Resources/Modules/Admin/Site/Index.resx | 17 +- .../Theme/ControlPanelInteractive.razor | 7 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 176 ++++++++++++------ .../SiteGroupMemberEntityBuilder.cs | 3 - Oqtane.Shared/Models/SiteGroupMember.cs | 5 - Oqtane.Shared/Shared/SiteGroupTypes.cs | 1 + 7 files changed, 165 insertions(+), 127 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index b89cb778..9aa6090f 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -480,10 +480,11 @@
- +
@@ -517,7 +518,7 @@ @if (_siteGroupId != -1 && _siteId != -1) {
- +
- @if (_primary == "False" && _groupType == SiteGroupTypes.Synchronization) + @if (_primary == "False" && (_groupType == SiteGroupTypes.Synchronization || _groupType == SiteGroupTypes.Comparison)) {
- -
- -
-
-
- +
@@ -678,7 +670,6 @@ private string _groupName = string.Empty; private string _groupType = SiteGroupTypes.Synchronization; private string _primary = "True"; - private string _notify = "True"; private string _synchronized = string.Empty; private bool _addSiteGroup = false; private bool _addSiteGroupMember = false; @@ -1304,7 +1295,6 @@ if (siteGroupMember != null) { _primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False"; - _notify = siteGroupMember.Notify.ToString(); _synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString(); } } @@ -1317,7 +1307,6 @@ if (siteGroupMember != null) { _primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False"; - _notify = siteGroupMember.Notify.ToString(); _synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString(); } StateHasChanged(); @@ -1338,29 +1327,33 @@ _siteId = -1; await LoadSites(); } - + private async Task SaveSiteGroupMember() { + if (string.IsNullOrEmpty(_groupName)) + { + AddModuleMessage(Localizer["Message.Required.GroupName"], MessageType.Warning); + await ScrollToPageTop(); + return; + } + SiteGroup siteGroup = null; if (_siteGroupId == -1) { - if (!string.IsNullOrEmpty(_groupName)) + siteGroup = new SiteGroup { - siteGroup = new SiteGroup - { - Name = _groupName, - Type = _groupType, - PrimarySiteId = _siteId, - Synchronize = false - }; - siteGroup = await SiteGroupService.AddSiteGroupAsync(siteGroup); - } + Name = _groupName, + Type = _groupType, + PrimarySiteId = _siteId, + Synchronize = false + }; + siteGroup = await SiteGroupService.AddSiteGroupAsync(siteGroup); } else { siteGroup = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId); - if (siteGroup != null && !string.IsNullOrEmpty(_groupName)) + if (siteGroup != null) { siteGroup.Name = _groupName; siteGroup.Type = _groupType; @@ -1375,22 +1368,23 @@ if (siteGroup != null) { - var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, siteGroup.SiteGroupId); - if (siteGroupMember == null) + if (_siteId != -1) { - siteGroupMember = new SiteGroupMember + var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, siteGroup.SiteGroupId); + if (siteGroupMember == null) { - SiteGroupId = siteGroup.SiteGroupId, - SiteId = _siteId, - Notify = bool.Parse(_notify) - }; - await SiteGroupMemberService.AddSiteGroupMemberAsync(siteGroupMember); - } - else - { - siteGroupMember.Notify = bool.Parse(_notify); - siteGroupMember.SynchronizedOn = string.IsNullOrEmpty(_synchronized) ? null : siteGroupMember.SynchronizedOn; - await SiteGroupMemberService.UpdateSiteGroupMemberAsync(siteGroupMember); + siteGroupMember = new SiteGroupMember + { + SiteGroupId = siteGroup.SiteGroupId, + SiteId = _siteId + }; + await SiteGroupMemberService.AddSiteGroupMemberAsync(siteGroupMember); + } + else + { + siteGroupMember.SynchronizedOn = string.IsNullOrEmpty(_synchronized) ? null : siteGroupMember.SynchronizedOn; + await SiteGroupMemberService.UpdateSiteGroupMemberAsync(siteGroupMember); + } } if (siteGroup.Type == SiteGroupTypes.Synchronization) @@ -1407,11 +1401,6 @@ await LoadSiteGroups(); } - else - { - AddModuleMessage(Localizer["Message.Required.GroupName"], MessageType.Warning); - await ScrollToPageTop(); - } } private async Task CancelSiteGroupMember() diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 3ea74b8e..b12fb46d 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -508,7 +508,7 @@ Primary? - Indicates if the selected site is the primary member of the site group + Indicates if the selected member is the primary site of the site group Name: @@ -535,17 +535,11 @@ Are You Sure You Wish To Delete This Member From The Site Group? - Group Name Is Required + Site Group Name Is Required Site Submitted For Synchronization - - Notify? - - - Specifies if site administrators should be notified of any synchronization activity - Members: @@ -559,10 +553,10 @@ Type: - The site group type (ie. synchronization, localization) + The site group type (ie. synchronization, comparison, localization) - The date/time of the last synchronization for the site + The date/time when the site was last synchronized Synchronization @@ -570,4 +564,7 @@ Localization + + Comparison + \ No newline at end of file diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index 91860090..15fe5b3a 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -35,9 +35,10 @@
- @if (_siteGroups.Any(item => item.Type == SiteGroupTypes.Synchronization)) + @if (_siteGroups.Any(item => item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison)) { - +
+ }
} @@ -641,7 +642,7 @@ private async Task SynchronizeSite() { - foreach (var group in _siteGroups.Where(item => item.Type == SiteGroupTypes.Synchronization)) + foreach (var group in _siteGroups.Where(item => item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison)) { group.Synchronize = true; await SiteGroupService.UpdateSiteGroupAsync(group); diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 6b8c3897..3fb1513b 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -53,7 +53,7 @@ namespace Oqtane.Infrastructure var siteGroups = siteGroupRepository.GetSiteGroups(); // iterate through site groups which need to be synchronized - foreach (var siteGroup in siteGroups.Where(item => item.Type == SiteGroupTypes.Synchronization && item.Synchronize)) + foreach (var siteGroup in siteGroups.Where(item => item.Synchronize && (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison))) { // get data if (siteGroupMembers == null) @@ -64,7 +64,8 @@ namespace Oqtane.Infrastructure } var aliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroup.PrimarySiteId && item.IsDefault).Name; - log += $"Processing Primary Site: {sites.First(item => item.SiteId == siteGroup.PrimarySiteId).Name} - {CreateLink(aliasName)}
"; + log += (siteGroup.Type == SiteGroupTypes.Synchronization) ? "Synchronizing " : "Comparing "; + log += $"Primary Site: {sites.First(item => item.SiteId == siteGroup.PrimarySiteId).Name} - {CreateLink(aliasName)}
"; // get primary site var primarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.PrimarySiteId); @@ -90,14 +91,14 @@ namespace Oqtane.Infrastructure siteGroupMember.SynchronizedOn = DateTime.MinValue; } - // replicate site - var siteLog = ReplicateSite(provider, tenantManager, settingRepository, siteGroupMember, primarySite, secondarySite); + // synchronize site + var siteLog = SynchronizeSite(provider, tenantManager, settingRepository, siteGroupMember, primarySite, secondarySite); // set synchronized on date/time siteGroupMember.SynchronizedOn = DateTime.UtcNow; siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember); - log += $"Processed Secondary Site: {secondarySite.Name} - {CreateLink(siteGroupMember.AliasName)}
" + siteLog; + log += $"With Secondary Site: {secondarySite.Name} - {CreateLink(siteGroupMember.AliasName)}
" + siteLog; } else { @@ -119,20 +120,20 @@ namespace Oqtane.Infrastructure return log; } - private string ReplicateSite(IServiceProvider provider, ITenantManager tenantManager, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, Site primarySite, Site secondarySite) + private string SynchronizeSite(IServiceProvider provider, ITenantManager tenantManager, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, Site primarySite, Site secondarySite) { var log = ""; - // replicate roles/users - log += ReplicateRoles(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); + // synchronize roles/users + log += SynchronizeRoles(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); - // replicate folders/files - log += ReplicateFolders(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); + // synchronize folders/files + log += SynchronizeFolders(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); - // replicate pages/modules - log += ReplicatePages(provider, settingRepository, tenantManager, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); + // synchronize pages/modules + log += SynchronizePages(provider, settingRepository, tenantManager, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); - // replicate site + // synchronize site if (primarySite.ModifiedOn > siteGroupMember.SynchronizedOn) { secondarySite.TimeZoneId = primarySite.TimeZoneId; @@ -177,23 +178,26 @@ namespace Oqtane.Infrastructure secondarySite.DeletedBy = primarySite.DeletedBy; secondarySite.DeletedOn = primarySite.DeletedOn; - var siteRepository = provider.GetRequiredService(); - siteRepository.UpdateSite(secondarySite); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + var siteRepository = provider.GetRequiredService(); + siteRepository.UpdateSite(secondarySite); + } log += Log(siteGroupMember, $"Site Updated: {secondarySite.Name}"); } // site settings - log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Site, primarySite.SiteId, secondarySite.SiteId); + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Site, primarySite.SiteId, secondarySite.SiteId); if (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) { - // clear cache for secondary site if any content was replicated + // clear cache for secondary site if any content was Synchronized var syncManager = provider.GetRequiredService(); var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySite.SiteId }; syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySite.SiteId, SyncEventActions.Refresh); } - if (!string.IsNullOrEmpty(log) && siteGroupMember.Notify) + if (!string.IsNullOrEmpty(log) && siteGroupMember.SiteGroup.Type == SiteGroupTypes.Comparison) { // send change log to administrators SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, log); @@ -213,7 +217,7 @@ namespace Oqtane.Infrastructure return fileId; } - private string ReplicateRoles(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) + private string SynchronizeRoles(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) { // get roles var roleRepository = provider.GetRequiredService(); @@ -242,12 +246,18 @@ namespace Oqtane.Infrastructure if (role == null) { - roleRepository.AddRole(secondaryRole); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + roleRepository.AddRole(secondaryRole); + } log += Log(siteGroupMember, $"Role Added: {secondaryRole.Name}"); } else { - roleRepository.UpdateRole(secondaryRole); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + roleRepository.UpdateRole(secondaryRole); + } log += Log(siteGroupMember, $"Role Updated: {secondaryRole.Name}"); secondaryRoles.Remove(role); } @@ -257,17 +267,20 @@ namespace Oqtane.Infrastructure // remove roles in the secondary site which do not exist in the primary site foreach (var secondaryRole in secondaryRoles.Where(item => !primaryRoles.Select(item => item.Name).Contains(item.Name))) { - roleRepository.DeleteRole(secondaryRole.RoleId); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + roleRepository.DeleteRole(secondaryRole.RoleId); + } log += Log(siteGroupMember, $"Role Deleted: {secondaryRole.Name}"); } // settings - log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Role, primarySiteId, secondarySiteId); + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Role, primarySiteId, secondarySiteId); return log; } - private string ReplicateFolders(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) + private string SynchronizeFolders(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) { var folderRepository = provider.GetRequiredService(); var fileRepository = provider.GetRequiredService(); @@ -308,23 +321,29 @@ namespace Oqtane.Infrastructure secondaryFolder.Capacity = primaryFolder.Capacity; secondaryFolder.ImageSizes = primaryFolder.ImageSizes; secondaryFolder.IsSystem = primaryFolder.IsSystem; - secondaryFolder.PermissionList = ReplicatePermissions(primaryFolder.PermissionList, secondarySiteId); + secondaryFolder.PermissionList = SynchronizePermissions(primaryFolder.PermissionList, secondarySiteId); if (folder == null) { - folderRepository.AddFolder(secondaryFolder); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + folderRepository.AddFolder(secondaryFolder); + } log += Log(siteGroupMember, $"Folder Added: {secondaryFolder.Path}"); } else { - folderRepository.UpdateFolder(secondaryFolder); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + folderRepository.UpdateFolder(secondaryFolder); + } log += Log(siteGroupMember, $"Folder Updated: {secondaryFolder.Path}"); secondaryFolders.Remove(folder); } } // folder settings - log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId); + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId); // get files for folder var primaryFiles = fileRepository.GetFiles(primaryFolder.FolderId); @@ -353,14 +372,20 @@ namespace Oqtane.Infrastructure if (file == null) { - fileRepository.AddFile(secondaryFile); - ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + fileRepository.AddFile(secondaryFile); + SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + } log += Log(siteGroupMember, $"File Added: {CreateLink(siteGroupMember.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); } else { - fileRepository.UpdateFile(secondaryFile); - ReplicateFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + fileRepository.UpdateFile(secondaryFile); + SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + } log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); secondaryFiles.Remove(file); } @@ -370,9 +395,12 @@ namespace Oqtane.Infrastructure // remove files in the secondary site which do not exist in the primary site foreach (var secondaryFile in secondaryFiles.Where(item => !primaryFiles.Select(item => item.Name).Contains(item.Name))) { - fileRepository.DeleteFile(secondaryFile.FileId); - var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); - System.IO.File.Delete(secondaryPath); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + fileRepository.DeleteFile(secondaryFile.FileId); + var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); + System.IO.File.Delete(secondaryPath); + } log += Log(siteGroupMember, $"File Deleted: {CreateLink(siteGroupMember.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); } } @@ -380,14 +408,17 @@ namespace Oqtane.Infrastructure // remove folders in the secondary site which do not exist in the primary site foreach (var secondaryFolder in secondaryFolders.Where(item => !primaryFolders.Select(item => item.Path).Contains(item.Path))) { - folderRepository.DeleteFolder(secondaryFolder.FolderId); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + folderRepository.DeleteFolder(secondaryFolder.FolderId); + } log += Log(siteGroupMember, $"Folder Deleted: {secondaryFolder.Path}"); } return log; } - private void ReplicateFile(IFolderRepository folderRepository, Folder primaryFolder, Models.File primaryFile, Folder secondaryFolder, Models.File secondaryFile) + private void SynchronizeFile(IFolderRepository folderRepository, Folder primaryFolder, Models.File primaryFile, Folder secondaryFolder, Models.File secondaryFile) { var primaryPath = Path.Combine(folderRepository.GetFolderPath(primaryFolder), primaryFile.Name); if (System.IO.File.Exists(primaryPath)) @@ -401,7 +432,7 @@ namespace Oqtane.Infrastructure } } - private string ReplicatePages(IServiceProvider provider, ISettingRepository settingRepository, ITenantManager tenantManager, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) + private string SynchronizePages(IServiceProvider provider, ISettingRepository settingRepository, ITenantManager tenantManager, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) { var pageRepository = provider.GetRequiredService(); var pageModuleRepository = provider.GetRequiredService(); @@ -464,23 +495,29 @@ namespace Oqtane.Infrastructure secondaryPage.DeletedBy = primaryPage.DeletedBy; secondaryPage.DeletedOn = primaryPage.DeletedOn; secondaryPage.IsDeleted = primaryPage.IsDeleted; - secondaryPage.PermissionList = ReplicatePermissions(primaryPage.PermissionList, secondarySiteId); + secondaryPage.PermissionList = SynchronizePermissions(primaryPage.PermissionList, secondarySiteId); if (page == null) { - secondaryPage = pageRepository.AddPage(secondaryPage); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + secondaryPage = pageRepository.AddPage(secondaryPage); + } log += Log(siteGroupMember, $"Page Added: {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } else { - secondaryPage = pageRepository.UpdatePage(secondaryPage); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + secondaryPage = pageRepository.UpdatePage(secondaryPage); + } log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); secondaryPages.Remove(page); } } // page settings - log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId); + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId); // modules if (primaryPageModules == null) @@ -517,7 +554,7 @@ namespace Oqtane.Infrastructure secondaryPageModule.Header = primaryPageModule.Header; secondaryPageModule.Footer = primaryPageModule.Footer; secondaryPageModule.IsDeleted = primaryPageModule.IsDeleted; - secondaryPageModule.Module.PermissionList = ReplicatePermissions(primaryPageModule.Module.PermissionList, secondarySiteId); + secondaryPageModule.Module.PermissionList = SynchronizePermissions(primaryPageModule.Module.PermissionList, secondarySiteId); secondaryPageModule.Module.AllPages = false; secondaryPageModule.Module.IsDeleted = false; @@ -530,15 +567,21 @@ namespace Oqtane.Infrastructure if (module == null) { // add new module - module = moduleRepository.AddModule(secondaryPageModule.Module); - updateContent = true; + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + module = moduleRepository.AddModule(secondaryPageModule.Module); + updateContent = true; + } log += Log(siteGroupMember, $"Module Added: {module.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } if (module != null) { secondaryPageModule.ModuleId = module.ModuleId; secondaryPageModule.Module = null; // remove tracking - secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); + } log += Log(siteGroupMember, $"Module Instance Added: {module.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); secondaryPageModule.Module = module; } @@ -548,13 +591,19 @@ namespace Oqtane.Infrastructure // update existing module if (primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn) { - moduleRepository.UpdateModule(secondaryPageModule.Module); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + moduleRepository.UpdateModule(secondaryPageModule.Module); + } updateContent = true; log += Log(siteGroupMember, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn) { - secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); + } log += Log(siteGroupMember, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); secondaryPageModules.Remove(pageModule); } @@ -573,7 +622,10 @@ namespace Oqtane.Infrastructure var secondaryModuleContent = ((ISynchronizable)moduleObject).ExtractModule(secondaryPageModule.Module); if (primaryModuleContent != secondaryModuleContent) { - ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, primaryModuleContent, primaryPageModule.Module.ModuleDefinition.Version); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, primaryModuleContent, primaryPageModule.Module.ModuleDefinition.Version); + } log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } } @@ -586,7 +638,7 @@ namespace Oqtane.Infrastructure } // module settings - log += ReplicateSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, secondaryPageModule.ModuleId); + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, secondaryPageModule.ModuleId); } } @@ -605,7 +657,10 @@ namespace Oqtane.Infrastructure } if (!primaryPageModules.Any(item => item.PageId == primaryPageId && item.Module.ModuleDefinitionName == secondaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == secondaryPageModule.Title.ToLower())) { - pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); + } log += Log(siteGroupMember, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPageModule.Page.Path)}"); } } @@ -613,13 +668,16 @@ namespace Oqtane.Infrastructure // remove pages in the secondary site which do not exist in the primary site foreach (var secondaryPage in secondaryPages.Where(item => !primaryPages.Select(item => item.Path).Contains(item.Path))) { - pageRepository.DeletePage(secondaryPage.PageId); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + pageRepository.DeletePage(secondaryPage.PageId); + } log += Log(siteGroupMember, $"Page Deleted: {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); } if (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) { - // clear cache for secondary site if any content was replicated + // clear cache for secondary site if any content was Synchronized var syncManager = provider.GetRequiredService(); var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySiteId }; syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySiteId, SyncEventActions.Refresh); @@ -628,7 +686,7 @@ namespace Oqtane.Infrastructure return log; } - private List ReplicatePermissions(List permissionList, int siteId) + private List SynchronizePermissions(List permissionList, int siteId) { return permissionList.Select(item => new Permission { @@ -644,7 +702,7 @@ namespace Oqtane.Infrastructure }).ToList(); } - private string ReplicateSettings(ISettingRepository settingRepository, SiteGroupMember siteGroupMember, string entityName, int primaryEntityId, int secondaryEntityId) + private string SynchronizeSettings(ISettingRepository settingRepository, SiteGroupMember siteGroupMember, string entityName, int primaryEntityId, int secondaryEntityId) { var log = ""; var updated = false; @@ -661,7 +719,7 @@ namespace Oqtane.Infrastructure secondarySetting.SettingName = primarySetting.SettingName; secondarySetting.SettingValue = primarySetting.SettingValue; secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.AddSetting(secondarySetting); updated = true; @@ -673,7 +731,7 @@ namespace Oqtane.Infrastructure { secondarySetting.SettingValue = primarySetting.SettingValue; secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.UpdateSetting(secondarySetting); updated = true; @@ -686,7 +744,7 @@ namespace Oqtane.Infrastructure // any remaining secondary settings need to be deleted foreach (var secondarySetting in secondarySettings) { - if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { settingRepository.DeleteSetting(secondarySetting.EntityName, secondarySetting.SettingId); updated = true; @@ -715,7 +773,7 @@ namespace Oqtane.Infrastructure private string Log(SiteGroupMember siteGroupMember, string content) { - // not necessary to log initial replication + // not necessary to log initial synchronization if (siteGroupMember.SynchronizedOn != DateTime.MinValue) { return content + "
"; diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs index 92b6cfd2..d5ff717b 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs @@ -28,7 +28,6 @@ namespace Oqtane.Migrations.EntityBuilders SiteGroupMemberId = AddAutoIncrementColumn(table, "SiteGroupMemberId"); SiteGroupId = AddIntegerColumn(table, "SiteGroupId"); SiteId = AddIntegerColumn(table, "SiteId"); - Notify = AddBooleanColumn(table, "Notify"); SynchronizedOn = AddDateTimeColumn(table, "SynchronizedOn", true); AddAuditableColumns(table); @@ -42,8 +41,6 @@ namespace Oqtane.Migrations.EntityBuilders public OperationBuilder SiteId { get; set; } - public OperationBuilder Notify { get; set; } - public OperationBuilder SynchronizedOn { get; set; } } } diff --git a/Oqtane.Shared/Models/SiteGroupMember.cs b/Oqtane.Shared/Models/SiteGroupMember.cs index 0101a4eb..cb572509 100644 --- a/Oqtane.Shared/Models/SiteGroupMember.cs +++ b/Oqtane.Shared/Models/SiteGroupMember.cs @@ -20,11 +20,6 @@ namespace Oqtane.Models /// public int SiteId { get; set; } - /// - /// Indicates if the site administrator should be notified of any synchronization activity - /// - public bool Notify { get; set; } - /// /// The last date/time the site was synchronized /// diff --git a/Oqtane.Shared/Shared/SiteGroupTypes.cs b/Oqtane.Shared/Shared/SiteGroupTypes.cs index c1b028a5..55e0f7ff 100644 --- a/Oqtane.Shared/Shared/SiteGroupTypes.cs +++ b/Oqtane.Shared/Shared/SiteGroupTypes.cs @@ -3,6 +3,7 @@ namespace Oqtane.Shared public class SiteGroupTypes { public const string Synchronization = "Synchronization"; + public const string Comparison = "Comparison"; public const string Localization = "Localization"; } } From 3dcb391a1479840945e924797f4de40db7986f8f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 10 Feb 2026 09:13:39 -0500 Subject: [PATCH 28/75] improve log messages --- .../Infrastructure/Jobs/SynchronizationJob.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 3fb1513b..b26a7874 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -83,7 +83,14 @@ namespace Oqtane.Infrastructure if (secondarySite != null) { // get default alias for site - siteGroupMember.AliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroupMember.SiteId && item.IsDefault).Name; + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + siteGroupMember.AliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroupMember.SiteId && item.IsDefault).Name; + } + else + { + siteGroupMember.AliasName = aliasName; + } // initialize SynchronizedOn if (siteGroupMember.SynchronizedOn == null) @@ -183,7 +190,7 @@ namespace Oqtane.Infrastructure var siteRepository = provider.GetRequiredService(); siteRepository.UpdateSite(secondarySite); } - log += Log(siteGroupMember, $"Site Updated: {secondarySite.Name}"); + log += Log(siteGroupMember, $"Site Updated: {secondarySite.Name} - {CreateLink(siteGroupMember.AliasName)}"); } // site settings From ad300a58c19e4dd17b8bff6d4b38a161e91579b4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 10 Feb 2026 12:32:09 -0500 Subject: [PATCH 29/75] improvements to site synchronization --- .../Controls/ControlPanelInteractive.resx | 5 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 62 ++++++++++++------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx b/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx index a4981464..5247824e 100644 --- a/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx +++ b/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx @@ -174,9 +174,6 @@ Title: - - Check For System Updates - Visibility: @@ -202,6 +199,6 @@ Copy Existing Module - Synchronize + Synchronize Site \ No newline at end of file diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index b26a7874..89591704 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -266,13 +266,17 @@ namespace Oqtane.Infrastructure roleRepository.UpdateRole(secondaryRole); } log += Log(siteGroupMember, $"Role Updated: {secondaryRole.Name}"); - secondaryRoles.Remove(role); } } + + if (role != null) + { + secondaryRoles.Remove(role); + } } // remove roles in the secondary site which do not exist in the primary site - foreach (var secondaryRole in secondaryRoles.Where(item => !primaryRoles.Select(item => item.Name).Contains(item.Name))) + foreach (var secondaryRole in secondaryRoles) { if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { @@ -345,10 +349,14 @@ namespace Oqtane.Infrastructure folderRepository.UpdateFolder(secondaryFolder); } log += Log(siteGroupMember, $"Folder Updated: {secondaryFolder.Path}"); - secondaryFolders.Remove(folder); } } + if (folder != null) + { + secondaryFolders.Remove(folder); + } + // folder settings log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId); @@ -384,7 +392,7 @@ namespace Oqtane.Infrastructure fileRepository.AddFile(secondaryFile); SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); } - log += Log(siteGroupMember, $"File Added: {CreateLink(siteGroupMember.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); + log += Log(siteGroupMember, $"File Added: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); } else { @@ -393,14 +401,18 @@ namespace Oqtane.Infrastructure fileRepository.UpdateFile(secondaryFile); SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); } - log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); - secondaryFiles.Remove(file); + log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); } } + + if (file != null) + { + secondaryFiles.Remove(file); + } } // remove files in the secondary site which do not exist in the primary site - foreach (var secondaryFile in secondaryFiles.Where(item => !primaryFiles.Select(item => item.Name).Contains(item.Name))) + foreach (var secondaryFile in secondaryFiles) { if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { @@ -408,12 +420,12 @@ namespace Oqtane.Infrastructure var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); System.IO.File.Delete(secondaryPath); } - log += Log(siteGroupMember, $"File Deleted: {CreateLink(siteGroupMember.AliasName + secondaryFolder.Path + secondaryFile.Name)}"); + log += Log(siteGroupMember, $"File Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); } } // remove folders in the secondary site which do not exist in the primary site - foreach (var secondaryFolder in secondaryFolders.Where(item => !primaryFolders.Select(item => item.Path).Contains(item.Path))) + foreach (var secondaryFolder in secondaryFolders) { if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { @@ -510,7 +522,7 @@ namespace Oqtane.Infrastructure { secondaryPage = pageRepository.AddPage(secondaryPage); } - log += Log(siteGroupMember, $"Page Added: {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); + log += Log(siteGroupMember, $"Page Added: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } else { @@ -518,11 +530,15 @@ namespace Oqtane.Infrastructure { secondaryPage = pageRepository.UpdatePage(secondaryPage); } - log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); - secondaryPages.Remove(page); + log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } } + if (page != null) + { + secondaryPages.Remove(page); + } + // page settings log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId); @@ -579,7 +595,7 @@ namespace Oqtane.Infrastructure module = moduleRepository.AddModule(secondaryPageModule.Module); updateContent = true; } - log += Log(siteGroupMember, $"Module Added: {module.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); + log += Log(siteGroupMember, $"Module Added: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } if (module != null) { @@ -589,7 +605,7 @@ namespace Oqtane.Infrastructure { secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); } - log += Log(siteGroupMember, $"Module Instance Added: {module.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); + log += Log(siteGroupMember, $"Module Instance Added: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); secondaryPageModule.Module = module; } } @@ -603,7 +619,7 @@ namespace Oqtane.Infrastructure moduleRepository.UpdateModule(secondaryPageModule.Module); } updateContent = true; - log += Log(siteGroupMember, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); + log += Log(siteGroupMember, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn) { @@ -611,8 +627,7 @@ namespace Oqtane.Infrastructure { secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); } - log += Log(siteGroupMember, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); - secondaryPageModules.Remove(pageModule); + log += Log(siteGroupMember, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } } @@ -633,7 +648,7 @@ namespace Oqtane.Infrastructure { ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, primaryModuleContent, primaryPageModule.Module.ModuleDefinition.Version); } - log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); + log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } } catch @@ -644,6 +659,11 @@ namespace Oqtane.Infrastructure } } + if (pageModule != null) + { + secondaryPageModules.Remove(pageModule); + } + // module settings log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, secondaryPageModule.ModuleId); } @@ -668,18 +688,18 @@ namespace Oqtane.Infrastructure { pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); } - log += Log(siteGroupMember, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + secondaryPageModule.Page.Path)}"); + log += Log(siteGroupMember, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPageModule.Page.Path)}"); } } // remove pages in the secondary site which do not exist in the primary site - foreach (var secondaryPage in secondaryPages.Where(item => !primaryPages.Select(item => item.Path).Contains(item.Path))) + foreach (var secondaryPage in secondaryPages) { if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { pageRepository.DeletePage(secondaryPage.PageId); } - log += Log(siteGroupMember, $"Page Deleted: {CreateLink(siteGroupMember.AliasName + secondaryPage.Path)}"); + log += Log(siteGroupMember, $"Page Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } if (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) From e95a6b774ed7b8d26adf96a632ab7d137e0ed98e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 10 Feb 2026 13:16:12 -0500 Subject: [PATCH 30/75] site group improvements --- .../Themes/Controls/Theme/ControlPanel.razor | 1 + .../Theme/ControlPanelInteractive.razor | 4 ++-- .../Controls/Theme/LanguageSwitcher.razor | 2 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 21 +++++++++---------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor index 762e457a..7fcf2542 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor @@ -97,6 +97,7 @@ Alias = PageState.Alias, Site = new Site { + SiteId = PageState.Site.SiteId, DefaultContainerType = PageState.Site.DefaultContainerType, Settings = PageState.Site.Settings, Themes = PageState.Site.Themes diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index 15fe5b3a..2b126f25 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -35,7 +35,7 @@
- @if (_siteGroups.Any(item => item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison)) + @if (_siteGroups.Any(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison) && item.PrimarySiteId == PageState.Site.SiteId)) {
@@ -642,7 +642,7 @@ private async Task SynchronizeSite() { - foreach (var group in _siteGroups.Where(item => item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison)) + foreach (var group in _siteGroups.Where(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison) && item.PrimarySiteId == PageState.Site.SiteId)) { group.Synchronize = true; await SiteGroupService.UpdateSiteGroupAsync(group); diff --git a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor index 2211b7be..d049e436 100644 --- a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor +++ b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor @@ -17,7 +17,7 @@ { @if (_contentLocalization) { - @language.Name + @language.Name } else { diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 89591704..3d079633 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -63,9 +63,9 @@ namespace Oqtane.Infrastructure aliases = aliasRepository.GetAliases().ToList(); } - var aliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroup.PrimarySiteId && item.IsDefault).Name; + var primaryAliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroup.PrimarySiteId && item.IsDefault).Name; log += (siteGroup.Type == SiteGroupTypes.Synchronization) ? "Synchronizing " : "Comparing "; - log += $"Primary Site: {sites.First(item => item.SiteId == siteGroup.PrimarySiteId).Name} - {CreateLink(aliasName)}
"; + log += $"Primary Site: {sites.First(item => item.SiteId == siteGroup.PrimarySiteId).Name} - {CreateLink(primaryAliasName)}
"; // get primary site var primarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.PrimarySiteId); @@ -83,14 +83,8 @@ namespace Oqtane.Infrastructure if (secondarySite != null) { // get default alias for site - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - siteGroupMember.AliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroupMember.SiteId && item.IsDefault).Name; - } - else - { - siteGroupMember.AliasName = aliasName; - } + var secondaryAliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroupMember.SiteId && item.IsDefault).Name; + siteGroupMember.AliasName = (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) ? secondaryAliasName : primaryAliasName; // initialize SynchronizedOn if (siteGroupMember.SynchronizedOn == null) @@ -100,12 +94,16 @@ namespace Oqtane.Infrastructure // synchronize site var siteLog = SynchronizeSite(provider, tenantManager, settingRepository, siteGroupMember, primarySite, secondarySite); + if (string.IsNullOrEmpty(siteLog)) + { + siteLog = (siteGroupMember.SynchronizedOn != DateTime.MinValue) ? "No Changes Identified
" : "Initialization Complete
"; + } // set synchronized on date/time siteGroupMember.SynchronizedOn = DateTime.UtcNow; siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember); - log += $"With Secondary Site: {secondarySite.Name} - {CreateLink(siteGroupMember.AliasName)}
" + siteLog; + log += $"With Secondary Site: {secondarySite.Name} - {CreateLink(secondaryAliasName)}
" + siteLog; } else { @@ -208,6 +206,7 @@ namespace Oqtane.Infrastructure { // send change log to administrators SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, log); + log += Log(siteGroupMember, $"Change Log Sent To Administrators"); } return log; From d13e6fcdadbddef4422008a53497cd81f99b3fb0 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 11 Feb 2026 08:59:10 -0500 Subject: [PATCH 31/75] improvements to ISynchronizable --- .../Infrastructure/Jobs/SynchronizationJob.cs | 44 ++++++------- .../HtmlText/Manager/HtmlTextManager.cs | 61 +++++++++++-------- .../HtmlText/Repository/HtmlTextRepository.cs | 14 +---- Oqtane.Server/Modules/ISynchronizable.cs | 5 +- 4 files changed, 59 insertions(+), 65 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 3d079633..32ac379c 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -16,9 +16,8 @@ namespace Oqtane.Infrastructure // JobType = "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server" // synchronization only supports sites in the same tenant (database) - // module title is used as a key to identify module instances on a page (ie. using "-" as a module title is problematic ie. content as configuration) - // relies on Module.ModifiedOn to be set if the module content changes (for efficiency) - // modules must implement ISynchronizable interface (new interface as IPortable was generally only implemented in an additive manner) + // module title is used as a key to identify module instances on a page + // modules must implement ISynchronizable interface // define settings that should not be synchronized (should be extensible in the future) List excludedSettings = new List() { @@ -580,8 +579,6 @@ namespace Oqtane.Infrastructure secondaryPageModule.Module.AllPages = false; secondaryPageModule.Module.IsDeleted = false; - var updateContent = false; - if (pageModule == null) { // check if module exists @@ -592,7 +589,6 @@ namespace Oqtane.Infrastructure if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { module = moduleRepository.AddModule(secondaryPageModule.Module); - updateContent = true; } log += Log(siteGroupMember, $"Module Added: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } @@ -617,7 +613,6 @@ namespace Oqtane.Infrastructure { moduleRepository.UpdateModule(secondaryPageModule.Module); } - updateContent = true; log += Log(siteGroupMember, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn) @@ -629,31 +624,30 @@ namespace Oqtane.Infrastructure log += Log(siteGroupMember, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } } + } - // module content - if (updateContent && primaryPageModule.Module.ModuleDefinition.ServerManagerType != "") + // module content + if (primaryPageModule.Module.ModuleDefinition.ServerManagerType != "") + { + Type moduleType = Type.GetType(primaryPageModule.Module.ModuleDefinition.ServerManagerType); + if (moduleType != null && moduleType.GetInterface(nameof(ISynchronizable)) != null) { - Type moduleType = Type.GetType(primaryPageModule.Module.ModuleDefinition.ServerManagerType); - if (moduleType != null && moduleType.GetInterface(nameof(ISynchronizable)) != null) + try { - try + var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); + var moduleContent = ((ISynchronizable)moduleObject).ExtractModule(primaryPageModule.Module, siteGroupMember.SynchronizedOn.Value); + if (!string.IsNullOrEmpty(moduleContent)) { - var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); - var primaryModuleContent = ((ISynchronizable)moduleObject).ExtractModule(primaryPageModule.Module); - var secondaryModuleContent = ((ISynchronizable)moduleObject).ExtractModule(secondaryPageModule.Module); - if (primaryModuleContent != secondaryModuleContent) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, primaryModuleContent, primaryPageModule.Module.ModuleDefinition.Version); - } - log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, moduleContent); } + log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } - catch - { - // error exporting/importing - } + } + catch + { + // error exporting/importing } } } diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index da556510..93647fe7 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -59,39 +59,50 @@ namespace Oqtane.Modules.HtmlText.Manager // IPortable implementation public string ExportModule(Module module) - { - return GetModuleContent(module); - } - - public void ImportModule(Module module, string content, string version) - { - SaveModuleContent(module, content, version); - } - - // ISynchronizable implementation - public string ExtractModule(Module module) - { - return GetModuleContent(module); - } - - public void LoadModule(Module module, string content, string version) - { - SaveModuleContent(module, content, version); - } - - private string GetModuleContent(Module module) { string content = ""; - var htmltexts = _htmlText.GetHtmlTexts(module.ModuleId); - if (htmltexts != null && htmltexts.Any()) + var htmltext = GetModuleContent(module.ModuleId); + if (htmltext != null) { - var htmltext = htmltexts.OrderByDescending(item => item.CreatedOn).First(); content = WebUtility.HtmlEncode(htmltext.Content); } return content; } - private void SaveModuleContent(Module module, string content, string version) + public void ImportModule(Module module, string content, string version) + { + SaveModuleContent(module, content); + } + + // ISynchronizable implementation + public string ExtractModule(Module module, DateTime lastSynchronizedOn) + { + string content = ""; + var htmltext = GetModuleContent(module.ModuleId); + if (htmltext != null && htmltext.CreatedOn > lastSynchronizedOn) + { + content = WebUtility.HtmlEncode(htmltext.Content); + } + return content; + } + + public void LoadModule(Module module, string content) + { + SaveModuleContent(module, content); + } + + private Models.HtmlText GetModuleContent(int moduleId) + { + // get the most recent htmltext record for the module + var htmltexts = _htmlText.GetHtmlTexts(moduleId); + if (htmltexts != null && htmltexts.Any()) + { + return htmltexts.OrderByDescending(item => item.CreatedOn).First(); + } + return null; + } + + private void SaveModuleContent(Module module, string content) { content = WebUtility.HtmlDecode(content); var htmlText = new Models.HtmlText(); diff --git a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs index 4c320455..53688d4c 100644 --- a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs +++ b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs @@ -2,7 +2,6 @@ using System.Linq; using Oqtane.Documentation; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; -using Oqtane.Repository; namespace Oqtane.Modules.HtmlText.Repository { @@ -19,12 +18,10 @@ namespace Oqtane.Modules.HtmlText.Repository public class HtmlTextRepository : IHtmlTextRepository, ITransientService { private readonly IDbContextFactory _factory; - private readonly IModuleRepository _moduleRepository; - public HtmlTextRepository(IDbContextFactory factory, IModuleRepository moduleRepository) + public HtmlTextRepository(IDbContextFactory factory) { _factory = factory; - _moduleRepository = moduleRepository; } public IEnumerable GetHtmlTexts(int moduleId) @@ -44,11 +41,6 @@ namespace Oqtane.Modules.HtmlText.Repository using var db = _factory.CreateDbContext(); db.HtmlText.Add(htmlText); db.SaveChanges(); - - // update module ModifiedOn date - var module = _moduleRepository.GetModule(htmlText.ModuleId); - _moduleRepository.UpdateModule(module); - return htmlText; } @@ -60,10 +52,6 @@ namespace Oqtane.Modules.HtmlText.Repository { db.HtmlText.Remove(htmlText); db.SaveChanges(); - - // update module ModifiedOn date - var module = _moduleRepository.GetModule(htmlText.ModuleId); - _moduleRepository.UpdateModule(module); } } } diff --git a/Oqtane.Server/Modules/ISynchronizable.cs b/Oqtane.Server/Modules/ISynchronizable.cs index 7dce3284..456d992f 100644 --- a/Oqtane.Server/Modules/ISynchronizable.cs +++ b/Oqtane.Server/Modules/ISynchronizable.cs @@ -1,3 +1,4 @@ +using System; using Oqtane.Models; namespace Oqtane.Modules @@ -6,8 +7,8 @@ namespace Oqtane.Modules { // You Must Set The "ServerManagerType" In Your IModule Interface - string ExtractModule(Module module); + string ExtractModule(Module module, DateTime lastSynchronizedOn); - void LoadModule(Module module, string content, string version); + void LoadModule(Module module, string content); } } From 10c1779f84e9c547f6f24fbb0eea1f5fd237efa6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 11 Feb 2026 09:23:23 -0500 Subject: [PATCH 32/75] update to .NET SDK 10.0.3 (and latest dependencies) --- .../Client/Oqtane.Application.Client.csproj | 8 +++---- .../Server/Oqtane.Application.Server.csproj | 6 ++--- Oqtane.Client/Oqtane.Client.csproj | 10 ++++----- Oqtane.Maui/Oqtane.Maui.csproj | 10 ++++----- Oqtane.Package/Oqtane.Client.nuspec | 10 ++++----- Oqtane.Package/Oqtane.Server.nuspec | 22 +++++++++---------- Oqtane.Package/Oqtane.Shared.nuspec | 4 ++-- Oqtane.Server/Oqtane.Server.csproj | 22 +++++++++---------- .../[Owner].Module.[Module].Client.csproj | 10 ++++----- .../[Owner].Module.[Module].Server.csproj | 8 +++---- .../[Owner].Theme.[Theme].Client.csproj | 6 ++--- Oqtane.Shared/Oqtane.Shared.csproj | 4 ++-- 12 files changed, 60 insertions(+), 60 deletions(-) diff --git a/Oqtane.Application/Client/Oqtane.Application.Client.csproj b/Oqtane.Application/Client/Oqtane.Application.Client.csproj index 69df010a..1aa1a35e 100644 --- a/Oqtane.Application/Client/Oqtane.Application.Client.csproj +++ b/Oqtane.Application/Client/Oqtane.Application.Client.csproj @@ -12,10 +12,10 @@ - - - - + + + + diff --git a/Oqtane.Application/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj index 9aed8cfc..671e148f 100644 --- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj +++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj @@ -22,9 +22,9 @@ - - - + + + diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index e4249072..c1db2ae1 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 6c64032d..2184fe35 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -54,11 +54,11 @@ - - - - - + + + + + diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 708e1d4d..7ed919e1 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -19,11 +19,11 @@ - - - - - + + + + + diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index f03cd1d9..e776097a 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -20,21 +20,21 @@ - - - - + + + + - + - - - + + + - - - + + + diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 6338ba42..e8e1fbcd 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -18,8 +18,8 @@ oqtane - - + + diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 446622cf..b5ba8453 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -28,28 +28,28 @@ - - - - + + + + - + - - + + - + - - + + - + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index 9197b9b0..deaadec5 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -13,11 +13,11 @@ - - - - - + + + + + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj index 8fabc8ca..c69fd9cf 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -20,10 +20,10 @@ - - - - + + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj index db9ce2c7..3378693d 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj @@ -14,9 +14,9 @@ - - - + + + diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 1ed1fc4a..7b8432a7 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -5,8 +5,8 @@ - - + + From 971ba796eff1ba60e5b9337b5712d57cc0385f3d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 11 Feb 2026 10:05:46 -0500 Subject: [PATCH 33/75] increment version to 10.1.0 --- Directory.Build.props | 4 ++-- .../Client/Oqtane.Application.Client.csproj | 2 +- Oqtane.Application/Oqtane.Application.Template.nuspec | 2 +- .../Server/Oqtane.Application.Server.csproj | 2 +- .../Shared/Oqtane.Application.Shared.csproj | 2 +- Oqtane.Maui/Oqtane.Maui.csproj | 2 +- Oqtane.Package/Oqtane.Client.nuspec | 6 +++--- Oqtane.Package/Oqtane.Framework.nuspec | 6 +++--- Oqtane.Package/Oqtane.Server.nuspec | 8 ++++---- Oqtane.Package/Oqtane.Shared.nuspec | 4 ++-- Oqtane.Package/Oqtane.Updater.nuspec | 4 ++-- Oqtane.Package/install.ps1 | 2 +- Oqtane.Package/upgrade.ps1 | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 72d3c1d5..e1f15325 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ net10.0 Debug;Release - 10.0.4 + 10.1.0 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 https://github.com/oqtane/oqtane.framework Git diff --git a/Oqtane.Application/Client/Oqtane.Application.Client.csproj b/Oqtane.Application/Client/Oqtane.Application.Client.csproj index 1aa1a35e..8874d9db 100644 --- a/Oqtane.Application/Client/Oqtane.Application.Client.csproj +++ b/Oqtane.Application/Client/Oqtane.Application.Client.csproj @@ -23,7 +23,7 @@ - + diff --git a/Oqtane.Application/Oqtane.Application.Template.nuspec b/Oqtane.Application/Oqtane.Application.Template.nuspec index e6c2bdb6..9ba1ed74 100644 --- a/Oqtane.Application/Oqtane.Application.Template.nuspec +++ b/Oqtane.Application/Oqtane.Application.Template.nuspec @@ -2,7 +2,7 @@ Oqtane.Application.Template - 10.0.4 + 10.1.0 Oqtane Application Template For Blazor Shaun Walker false diff --git a/Oqtane.Application/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj index 671e148f..3526407a 100644 --- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj +++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj index fb8f617b..7689e461 100644 --- a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj +++ b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj @@ -11,7 +11,7 @@ - + diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 2184fe35..18f449d8 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -18,7 +18,7 @@ com.oqtane.maui - 10.0.4 + 10.1.0 1 diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 7ed919e1..42696e16 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 10.0.4 + 10.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,13 +12,13 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane - + diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index 83edf33a..25f116ad 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 10.0.4 + 10.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v10.0.4/Oqtane.Framework.10.0.4.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/download/v10.1.0/Oqtane.Framework.10.1.0.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index e776097a..4caf53a3 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 10.0.4 + 10.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,14 +12,14 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane - - + + diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index e8e1fbcd..b1374aea 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 10.0.4 + 10.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index 5cdb39ae..840f3de0 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 10.0.4 + 10.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index 39124413..1849738e 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.4.Install.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.0.Install.zip" -Force diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index eb0e0a31..dba48f9b 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.4.Upgrade.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.0.Upgrade.zip" -Force From 6d35d408291d5b9b055e0df0d3b7cca5545f295d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 11 Feb 2026 10:20:19 -0500 Subject: [PATCH 34/75] improve user delete experience --- Oqtane.Client/Modules/Admin/Users/Edit.razor | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index d629c8bb..eed0111b 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -224,7 +224,7 @@ { } - @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True") + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _candelete) { } @@ -251,6 +251,7 @@ private string _lastlogin; private string _lastipaddress; private bool _ishost = false; + private bool _candelete = false; private string _passwordrequirements; private string _password = string.Empty; @@ -296,6 +297,7 @@ _timezoneid = PageState.User.TimeZoneId; _culturecode = PageState.User.CultureCode; _isdeleted = user.IsDeleted.ToString(); + _candelete = user.IsDeleted; _lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn)); _lastipaddress = user.LastIPAddress; _ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); From 2d19f21b2ef647d8ed0b75f7f2ef47f1a2ded620 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 11 Feb 2026 11:21:05 -0500 Subject: [PATCH 35/75] improve content localization --- Oqtane.Server/Services/SiteService.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index e197faf0..3c356fbd 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -329,13 +329,10 @@ namespace Oqtane.Services var site = sites.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId); if (site != null && !string.IsNullOrEmpty(site.CultureCode)) { - if (!languages.Any(item => item.Code == site.CultureCode)) + var alias = aliases.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId && item.TenantId == tenantId && item.IsDefault); + if (alias != null) { - var alias = aliases.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId && item.TenantId == tenantId && item.IsDefault); - if (alias != null) - { - languages.Add(new Language { Code = site.CultureCode, Name = "", AliasName = alias.Name, IsDefault = false }); - } + languages.Add(new Language { Code = site.CultureCode, Name = "", AliasName = alias.Name, IsDefault = false }); } } } From 69fc1c9895983a007ebf9ae80b5f699e1274415e Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 11 Feb 2026 13:52:35 -0500 Subject: [PATCH 36/75] Revert "Rename services to Client/Server and update refs" --- .../Client/Modules/[Owner].Module.[Module]/Edit.razor | 10 +++++----- .../Client/Modules/[Owner].Module.[Module]/Index.razor | 8 ++++---- .../{Client[Module]Service.cs => [Module]Service.cs} | 4 ++-- .../Templates/External/Client/Startup/ClientStartup.cs | 2 +- .../Client/[Owner].Module.[Module].Client.csproj | 1 + .../{Server[Module]Service.cs => [Module]Service.cs} | 0 6 files changed, 13 insertions(+), 12 deletions(-) rename Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/{Client[Module]Service.cs => [Module]Service.cs} (90%) rename Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/{Server[Module]Service.cs => [Module]Service.cs} (100%) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor index 886b0c07..a3d25ab5 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor @@ -4,7 +4,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service Client[Module]Service +@inject I[Module]Service [Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -55,7 +55,7 @@ if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); - [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); if ([Module] != null) { _name = [Module].Name; @@ -86,14 +86,14 @@ [Module] [Module] = new [Module](); [Module].ModuleId = ModuleState.ModuleId; [Module].Name = _name; - [Module] = await Client[Module]Service.Add[Module]Async([Module]); + [Module] = await [Module]Service.Add[Module]Async([Module]); await logger.LogInformation("[Module] Added {[Module]}", [Module]); } else { - [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module].Name = _name; - await Client[Module]Service.Update[Module]Async([Module]); + await [Module]Service.Update[Module]Async([Module]); await logger.LogInformation("[Module] Updated {[Module]}", [Module]); } NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor index e5d5d4e3..6f43dcb3 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor @@ -3,7 +3,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service Client[Module]Service +@inject I[Module]Service [Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -52,7 +52,7 @@ else { try { - _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) { @@ -65,9 +65,9 @@ else { try { - await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); + await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); - _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); StateHasChanged(); } catch (Exception ex) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs similarity index 90% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs rename to Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs index ec451c42..24046476 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs @@ -8,9 +8,9 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public class Client[Module]Service : ServiceBase, I[Module]Service + public class [Module]Service : ServiceBase, I[Module]Service { - public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } + public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } private string Apiurl => CreateApiUrl("[Module]"); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs index 14170a14..95ed096c 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs @@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup { if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) { - services.AddScoped(); + services.AddScoped(); } } } diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index fce77d74..deaadec5 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -17,6 +17,7 @@ + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/Server[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/[Module]Service.cs similarity index 100% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/Server[Module]Service.cs rename to Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/[Module]Service.cs From 171735fac4888f6fbb1e3ec20a45ea73cd420dac Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 11 Feb 2026 13:54:10 -0500 Subject: [PATCH 37/75] Revert "Rename client and server module services" --- .../Client/Modules/[Owner].Module.[Module]/Edit.razor | 10 +++++----- .../Client/Modules/[Owner].Module.[Module]/Index.razor | 8 ++++---- .../{Client[Module]Service.cs => [Module]Service.cs} | 4 ++-- .../Templates/Internal/Client/Startup/ClientStartup.cs | 2 +- .../{Server[Module]Service.cs => [Module]Service.cs} | 0 5 files changed, 12 insertions(+), 12 deletions(-) rename Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/{Client[Module]Service.cs => [Module]Service.cs} (90%) rename Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/{Server[Module]Service.cs => [Module]Service.cs} (100%) diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor index a9406963..59badca4 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor @@ -4,7 +4,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service Client[Module]Service +@inject I[Module]Service [Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -50,7 +50,7 @@ if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); - [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); if ([Module] != null) { _name = [Module].Name; @@ -81,14 +81,14 @@ [Module] [Module] = new [Module](); [Module].ModuleId = ModuleState.ModuleId; [Module].Name = _name; - [Module] = await Client[Module]Service.Add[Module]Async([Module]); + [Module] = await [Module]Service.Add[Module]Async([Module]); await logger.LogInformation("[Module] Added {[Module]}", [Module]); } else { - [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module].Name = _name; - await Client[Module]Service.Update[Module]Async([Module]); + await [Module]Service.Update[Module]Async([Module]); await logger.LogInformation("[Module] Updated {[Module]}", [Module]); } NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor index b53e5d23..af8a4839 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor @@ -3,7 +3,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service Client[Module]Service +@inject I[Module]Service [Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -52,7 +52,7 @@ else { try { - _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) { @@ -65,9 +65,9 @@ else { try { - await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); + await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); - _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); StateHasChanged(); } catch (Exception ex) diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs similarity index 90% rename from Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs rename to Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs index ec451c42..24046476 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs @@ -8,9 +8,9 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public class Client[Module]Service : ServiceBase, I[Module]Service + public class [Module]Service : ServiceBase, I[Module]Service { - public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } + public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } private string Apiurl => CreateApiUrl("[Module]"); diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs index 14170a14..95ed096c 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs @@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup { if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) { - services.AddScoped(); + services.AddScoped(); } } } diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/Server[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs similarity index 100% rename from Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/Server[Module]Service.cs rename to Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs From 486132f91887ee87298caddcf5b1b795611e9848 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 11 Feb 2026 14:03:46 -0500 Subject: [PATCH 38/75] update default module/theme template solution files to avoid building the linked Oqtane.Server project --- .../Modules/Templates/External/[Owner].Module.[Module].slnx | 4 +++- .../Themes/Templates/External/[Owner].Theme.[Theme].slnx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].slnx b/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].slnx index 74f9bd8a..813952c3 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].slnx +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].slnx @@ -1,5 +1,7 @@ - + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].slnx b/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].slnx index 1b1f9c26..9a634bc9 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].slnx +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].slnx @@ -1,5 +1,7 @@ - + + + From 4cc93487699b901ca20a20381e5a98b6a86556d1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 11 Feb 2026 14:07:57 -0500 Subject: [PATCH 39/75] remove System.Net.Http.Json from default moduile template --- .../External/Client/[Owner].Module.[Module].Client.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index deaadec5..fce77d74 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -17,7 +17,6 @@ - From 84e8edb159aee5fe4b0e1b4b2a8d5ae934966df7 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 11 Feb 2026 14:11:22 -0500 Subject: [PATCH 40/75] Revert "Revert "Rename services to Client/Server and update refs"" --- .../Client/Modules/[Owner].Module.[Module]/Edit.razor | 10 +++++----- .../Client/Modules/[Owner].Module.[Module]/Index.razor | 8 ++++---- .../{[Module]Service.cs => Client[Module]Service.cs} | 4 ++-- .../Templates/External/Client/Startup/ClientStartup.cs | 2 +- .../{[Module]Service.cs => Server[Module]Service.cs} | 0 5 files changed, 12 insertions(+), 12 deletions(-) rename Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/{[Module]Service.cs => Client[Module]Service.cs} (90%) rename Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/{[Module]Service.cs => Server[Module]Service.cs} (100%) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor index a3d25ab5..886b0c07 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor @@ -4,7 +4,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -55,7 +55,7 @@ if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); if ([Module] != null) { _name = [Module].Name; @@ -86,14 +86,14 @@ [Module] [Module] = new [Module](); [Module].ModuleId = ModuleState.ModuleId; [Module].Name = _name; - [Module] = await [Module]Service.Add[Module]Async([Module]); + [Module] = await Client[Module]Service.Add[Module]Async([Module]); await logger.LogInformation("[Module] Added {[Module]}", [Module]); } else { - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module].Name = _name; - await [Module]Service.Update[Module]Async([Module]); + await Client[Module]Service.Update[Module]Async([Module]); await logger.LogInformation("[Module] Updated {[Module]}", [Module]); } NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor index 6f43dcb3..e5d5d4e3 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor @@ -3,7 +3,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -52,7 +52,7 @@ else { try { - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) { @@ -65,9 +65,9 @@ else { try { - await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); + await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); StateHasChanged(); } catch (Exception ex) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs similarity index 90% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs rename to Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs index 24046476..ec451c42 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs @@ -8,9 +8,9 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public class [Module]Service : ServiceBase, I[Module]Service + public class Client[Module]Service : ServiceBase, I[Module]Service { - public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } + public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } private string Apiurl => CreateApiUrl("[Module]"); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs index 95ed096c..14170a14 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs @@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup { if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) { - services.AddScoped(); + services.AddScoped(); } } } diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/Server[Module]Service.cs similarity index 100% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/[Module]Service.cs rename to Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/Server[Module]Service.cs From 0d75e0f555e0dbbbd2417f3dc11faa26223a50a7 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 11 Feb 2026 14:12:15 -0500 Subject: [PATCH 41/75] Revert "Revert "Rename client and server module services"" --- .../Client/Modules/[Owner].Module.[Module]/Edit.razor | 10 +++++----- .../Client/Modules/[Owner].Module.[Module]/Index.razor | 8 ++++---- .../{[Module]Service.cs => Client[Module]Service.cs} | 4 ++-- .../Templates/Internal/Client/Startup/ClientStartup.cs | 2 +- .../{[Module]Service.cs => Server[Module]Service.cs} | 0 5 files changed, 12 insertions(+), 12 deletions(-) rename Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/{[Module]Service.cs => Client[Module]Service.cs} (90%) rename Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/{[Module]Service.cs => Server[Module]Service.cs} (100%) diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor index 59badca4..a9406963 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor @@ -4,7 +4,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -50,7 +50,7 @@ if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); if ([Module] != null) { _name = [Module].Name; @@ -81,14 +81,14 @@ [Module] [Module] = new [Module](); [Module].ModuleId = ModuleState.ModuleId; [Module].Name = _name; - [Module] = await [Module]Service.Add[Module]Async([Module]); + [Module] = await Client[Module]Service.Add[Module]Async([Module]); await logger.LogInformation("[Module] Added {[Module]}", [Module]); } else { - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module].Name = _name; - await [Module]Service.Update[Module]Async([Module]); + await Client[Module]Service.Update[Module]Async([Module]); await logger.LogInformation("[Module] Updated {[Module]}", [Module]); } NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor index af8a4839..b53e5d23 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor @@ -3,7 +3,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -52,7 +52,7 @@ else { try { - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) { @@ -65,9 +65,9 @@ else { try { - await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); + await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); StateHasChanged(); } catch (Exception ex) diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs similarity index 90% rename from Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs rename to Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs index 24046476..ec451c42 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs @@ -8,9 +8,9 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public class [Module]Service : ServiceBase, I[Module]Service + public class Client[Module]Service : ServiceBase, I[Module]Service { - public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } + public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } private string Apiurl => CreateApiUrl("[Module]"); diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs index 95ed096c..14170a14 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs @@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup { if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) { - services.AddScoped(); + services.AddScoped(); } } } diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/Server[Module]Service.cs similarity index 100% rename from Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs rename to Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/Server[Module]Service.cs From d9db41ac996ce2812e325ee349e7d23b6cc46e55 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 13 Feb 2026 10:50:03 -0500 Subject: [PATCH 42/75] provide support for @page route --- .../Repository/ModuleDefinitionRepository.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index c2821a8b..f8dfc4ce 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -4,6 +4,8 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Oqtane.Infrastructure; @@ -420,6 +422,7 @@ namespace Oqtane.Repository } moduledefinition = moduledefinitions[index]; + // actions var modulecontrolobject = Activator.CreateInstance(modulecontroltype) as IModuleControl; string actions = modulecontrolobject.Actions; @@ -431,6 +434,73 @@ namespace Oqtane.Repository } } + // check for Page attribute + var routeAttributes = modulecontroltype.GetCustomAttributes(typeof(RouteAttribute), true).Cast(); + if (routeAttributes != null && routeAttributes.Any()) + { + var route = routeAttributes.First().Template; + if (!string.IsNullOrEmpty(route)) + { + // @page "/route" (note that nested routes are not permitted) + var pageTemplate = new PageTemplate(); + pageTemplate.AliasName = "*"; + pageTemplate.Version = "*"; + pageTemplate.Name = route.Substring(1); + pageTemplate.Update = false; + pageTemplate.PageTemplateModules = new List() + { + new PageTemplateModule { Title = route.Substring(1), Order = 1 } + }; + pageTemplate.PermissionList = new List(); + + // check for Authorize attributes + var authorizeAttributes = modulecontroltype.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast(); + if (authorizeAttributes != null && authorizeAttributes.Any()) + { + foreach (var authorizeAttribute in authorizeAttributes) + { + if (string.IsNullOrEmpty(authorizeAttribute.Roles)) + { + // [Authorize] + pageTemplate.PermissionList.Add(new Permission(PermissionNames.View, RoleNames.Registered, true)); + } + else + { + // [Authorize(Roles = "role1, permission:role2")] + foreach (var role in authorizeAttribute.Roles.Split(',')) + { + var permissionName = PermissionNames.View; + var roleName = role.Trim(); + if (roleName.Contains(":")) + { + permissionName = roleName.Substring(0, roleName.IndexOf(":") - 1); + roleName = roleName.Substring(roleName.IndexOf(":") + 1); + } + pageTemplate.PermissionList.Add(new Permission(permissionName, roleName, true)); + } + } + } + } + else + { + // default view permission + pageTemplate.PermissionList.Add(new Permission(PermissionNames.View, RoleNames.Everyone, true)); + } + if (!pageTemplate.PermissionList.Any(item => item.PermissionName == PermissionNames.Edit && item.RoleName == RoleNames.Admin)) + { + // default edit permission + pageTemplate.PermissionList.Add(new Permission(PermissionNames.Edit, RoleNames.Admin, true)); + } + + if (moduledefinition.PageTemplates == null) + { + // use pagetemplate if not already defined in IModule + moduledefinition.PageTemplates = new List(); + moduledefinition.PageTemplates.Add(pageTemplate); + } + } + } + moduledefinitions[index] = moduledefinition; } From 657c6620d51eb625dc33db960aa7fb35db2fd5c4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 13 Feb 2026 16:56:54 -0500 Subject: [PATCH 43/75] modifications for page attributes --- .../Repository/ModuleDefinitionRepository.cs | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index f8dfc4ce..b845ce88 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -445,15 +445,11 @@ namespace Oqtane.Repository var pageTemplate = new PageTemplate(); pageTemplate.AliasName = "*"; pageTemplate.Version = "*"; - pageTemplate.Name = route.Substring(1); + pageTemplate.Path = route.Substring(1); pageTemplate.Update = false; - pageTemplate.PageTemplateModules = new List() - { - new PageTemplateModule { Title = route.Substring(1), Order = 1 } - }; - pageTemplate.PermissionList = new List(); // check for Authorize attributes + var permissionList = new List(); var authorizeAttributes = modulecontroltype.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast(); if (authorizeAttributes != null && authorizeAttributes.Any()) { @@ -462,7 +458,7 @@ namespace Oqtane.Repository if (string.IsNullOrEmpty(authorizeAttribute.Roles)) { // [Authorize] - pageTemplate.PermissionList.Add(new Permission(PermissionNames.View, RoleNames.Registered, true)); + permissionList.Add(new Permission(PermissionNames.View, RoleNames.Registered, true)); } else { @@ -476,25 +472,37 @@ namespace Oqtane.Repository permissionName = roleName.Substring(0, roleName.IndexOf(":") - 1); roleName = roleName.Substring(roleName.IndexOf(":") + 1); } - pageTemplate.PermissionList.Add(new Permission(permissionName, roleName, true)); + permissionList.Add(new Permission(permissionName, roleName, true)); } } } } else { - // default view permission - pageTemplate.PermissionList.Add(new Permission(PermissionNames.View, RoleNames.Everyone, true)); + // view permission + permissionList.Add(new Permission(PermissionNames.View, RoleNames.Everyone, true)); } - if (!pageTemplate.PermissionList.Any(item => item.PermissionName == PermissionNames.Edit && item.RoleName == RoleNames.Admin)) + // default permissions + if (!permissionList.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Admin)) { - // default edit permission - pageTemplate.PermissionList.Add(new Permission(PermissionNames.Edit, RoleNames.Admin, true)); + permissionList.Add(new Permission(PermissionNames.View, RoleNames.Admin, true)); } + if (!permissionList.Any(item => item.PermissionName == PermissionNames.Edit && item.RoleName == RoleNames.Admin)) + { + permissionList.Add(new Permission(PermissionNames.Edit, RoleNames.Admin, true)); + } + pageTemplate.PermissionList = permissionList; + // add module instance + var pageTemplateModule = new PageTemplateModule(); + pageTemplateModule.Title = route.Substring(1); + pageTemplateModule.PermissionList = permissionList; + pageTemplate.PageTemplateModules = new List(); + pageTemplate.PageTemplateModules.Add(pageTemplateModule); + + // use pagetemplate if not already defined in IModule if (moduledefinition.PageTemplates == null) { - // use pagetemplate if not already defined in IModule moduledefinition.PageTemplates = new List(); moduledefinition.PageTemplates.Add(pageTemplate); } From d3c4e78baa5a8156a9f4eb38d71fbc7f1fc2a919 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sun, 15 Feb 2026 13:45:45 -0500 Subject: [PATCH 44/75] resolve permission issue --- .../Repository/ModuleDefinitionRepository.cs | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index b845ce88..a43174e9 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -447,6 +447,7 @@ namespace Oqtane.Repository pageTemplate.Version = "*"; pageTemplate.Path = route.Substring(1); pageTemplate.Update = false; + pageTemplate.PageTemplateModules = new List(); // check for Authorize attributes var permissionList = new List(); @@ -482,29 +483,33 @@ namespace Oqtane.Repository // view permission permissionList.Add(new Permission(PermissionNames.View, RoleNames.Everyone, true)); } - // default permissions - if (!permissionList.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Admin)) + + // assign page permissions + foreach (var permission in permissionList) { - permissionList.Add(new Permission(PermissionNames.View, RoleNames.Admin, true)); + if (!pageTemplate.PermissionList.Any(item => item.PermissionName == permission.PermissionName && item.RoleName == permission.RoleName)) + { + pageTemplate.PermissionList.Add(permission); + } } - if (!permissionList.Any(item => item.PermissionName == PermissionNames.Edit && item.RoleName == RoleNames.Admin)) - { - permissionList.Add(new Permission(PermissionNames.Edit, RoleNames.Admin, true)); - } - pageTemplate.PermissionList = permissionList; // add module instance var pageTemplateModule = new PageTemplateModule(); pageTemplateModule.Title = route.Substring(1); - pageTemplateModule.PermissionList = permissionList; - pageTemplate.PageTemplateModules = new List(); + // assign module permissions + foreach (var permission in permissionList) + { + if (!pageTemplateModule.PermissionList.Any(item => item.PermissionName == permission.PermissionName && item.RoleName == permission.RoleName)) + { + pageTemplateModule.PermissionList.Add(permission.Clone()); + } + } pageTemplate.PageTemplateModules.Add(pageTemplateModule); - // use pagetemplate if not already defined in IModule + // if PageTemplates was not already defined in IModule if (moduledefinition.PageTemplates == null) { - moduledefinition.PageTemplates = new List(); - moduledefinition.PageTemplates.Add(pageTemplate); + moduledefinition.PageTemplates = new List { pageTemplate }; } } } From a4f7d1f745c0a03191c68eb39c7d0175590b0dac Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 16 Feb 2026 07:59:55 -0500 Subject: [PATCH 45/75] add comment for clarification --- Oqtane.Server/Repository/SiteRepository.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index 7a94737c..4f917a3c 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -359,6 +359,7 @@ namespace Oqtane.Repository } pageTemplate.Path = (parent != null) ? parent.Path + "/" + pageTemplate.Name : pageTemplate.Name; } + // home page path can be specified with "home" or "/" pageTemplate.Path = (pageTemplate.Path.ToLower() == "home") ? "" : pageTemplate.Path; pageTemplate.Path = (pageTemplate.Path == "/") ? "" : pageTemplate.Path; var page = pages.FirstOrDefault(item => item.Path.ToLower() == pageTemplate.Path.ToLower()); From dd816f7c4457ea0c7d2cd42b2149a52eff197280 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 16 Feb 2026 08:08:20 -0500 Subject: [PATCH 46/75] clean up unused resource keys --- Oqtane.Client/Modules/Admin/Site/Index.razor | 2 +- .../Resources/Modules/Admin/Site/Index.resx | 2 +- .../Resources/Modules/Admin/Sites/Add.resx | 30 ------------------- Oqtane.Shared/Models/Site.cs | 2 +- 4 files changed, 3 insertions(+), 33 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 9aa6090f..06890701 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -358,7 +358,7 @@ }
- +
+
+
+

@@ -231,6 +238,7 @@ private string _url = ""; private string _contact = ""; private string _license = ""; + private string _fingerprint = ""; private List _permissions = null; private string _createdby; private DateTime _createdon; @@ -266,6 +274,7 @@ _url = moduleDefinition.Url; _contact = moduleDefinition.Contact; _license = moduleDefinition.License; + _fingerprint = moduleDefinition.Fingerprint; _permissions = moduleDefinition.PermissionList; _createdby = moduleDefinition.CreatedBy; _createdon = moduleDefinition.CreatedOn; diff --git a/Oqtane.Client/Modules/Admin/Themes/Edit.razor b/Oqtane.Client/Modules/Admin/Themes/Edit.razor index 4e7560e5..21e21a3c 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Edit.razor @@ -30,6 +30,7 @@ +
@@ -81,6 +82,12 @@ }
+
+ +
+ +
+

@@ -117,6 +124,7 @@ private string _url = ""; private string _contact = ""; private string _license = ""; + private string _fingerprint = ""; private List _permissions = null; private string _createdby; private DateTime _createdon; @@ -143,6 +151,7 @@ _url = theme.Url; _contact = theme.Contact; _license = theme.License; + _fingerprint = theme.Fingerprint; _permissions = theme.PermissionList; _createdby = theme.CreatedBy; _createdon = theme.CreatedOn; diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx index eb32861b..a3c86886 100644 --- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx @@ -234,4 +234,10 @@ Pages + + Fingerprint: + + + A unique identifier for the module's static resources. This value can be changed by clicking the Save option below (ie. cache busting). + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx index 27b70ddd..bf155a07 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx @@ -186,4 +186,10 @@ Permissions + + Fingerprint: + + + A unique identifier for the theme's static resources. This value can be changed by clicking the Save option below (ie. cache busting). + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index 139da8e2..229b419f 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -164,6 +164,7 @@ namespace Oqtane.Controllers { _moduleDefinitions.UpdateModuleDefinition(moduleDefinition); _syncManager.AddSyncEvent(_alias, EntityNames.ModuleDefinition, moduleDefinition.ModuleDefinitionId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh); // fingerprint changed _logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Definition Updated {ModuleDefinition}", moduleDefinition); } else diff --git a/Oqtane.Server/Controllers/ThemeController.cs b/Oqtane.Server/Controllers/ThemeController.cs index 3f025b40..20547e1b 100644 --- a/Oqtane.Server/Controllers/ThemeController.cs +++ b/Oqtane.Server/Controllers/ThemeController.cs @@ -117,6 +117,7 @@ namespace Oqtane.Controllers { _themes.UpdateTheme(theme); _syncManager.AddSyncEvent(_alias, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh); // fingerprint changed _logger.Log(LogLevel.Information, this, LogFunction.Update, "Theme Updated {Theme}", theme); } else diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index a43174e9..aa48d232 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -113,7 +113,7 @@ namespace Oqtane.Repository ModuleDefinition.Resources = moduleDefinition.Resources; ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled; ModuleDefinition.PackageName = moduleDefinition.PackageName; - ModuleDefinition.Fingerprint = Utilities.GenerateSimpleHash(moduleDefinition.ModifiedOn.ToString("yyyyMMddHHmm")); + ModuleDefinition.Fingerprint = moduleDefinition.Fingerprint; } return ModuleDefinition; @@ -186,6 +186,7 @@ namespace Oqtane.Repository ModuleDefinition.CreatedOn = moduledefinition.CreatedOn; ModuleDefinition.ModifiedBy = moduledefinition.ModifiedBy; ModuleDefinition.ModifiedOn = moduledefinition.ModifiedOn; + ModuleDefinition.Fingerprint = Utilities.GenerateSimpleHash(moduledefinition.ModifiedOn.ToString("yyyyMMddHHmm")); } // any remaining module definitions are orphans diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index 8962402c..57d7b7db 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -99,7 +99,7 @@ namespace Oqtane.Repository Theme.ContainerSettingsType = theme.ContainerSettingsType; Theme.PackageName = theme.PackageName; Theme.PermissionList = theme.PermissionList; - Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); + Theme.Fingerprint = theme.Fingerprint; Themes.Add(Theme); } @@ -165,6 +165,7 @@ namespace Oqtane.Repository Theme.CreatedOn = theme.CreatedOn; Theme.ModifiedBy = theme.ModifiedBy; Theme.ModifiedOn = theme.ModifiedOn; + Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); } // any remaining themes are orphans From 3e0b5bfa090f03050a211008e54d6b1538f3a3e5 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 17 Feb 2026 09:49:24 -0500 Subject: [PATCH 50/75] changed terminology from Comparison to Change Detection for Site Group Type --- Oqtane.Client/Modules/Admin/Site/Index.razor | 6 +++--- Oqtane.Client/Resources/Modules/Admin/Site/Index.resx | 6 +++--- .../Themes/Controls/Theme/ControlPanelInteractive.razor | 4 ++-- Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs | 4 ++-- Oqtane.Shared/Shared/SiteGroupTypes.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 06890701..49a3273e 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -480,11 +480,11 @@
- +
@@ -526,7 +526,7 @@
- @if (_primary == "False" && (_groupType == SiteGroupTypes.Synchronization || _groupType == SiteGroupTypes.Comparison)) + @if (_primary == "False" && (_groupType == SiteGroupTypes.Synchronization || _groupType == SiteGroupTypes.ChangeDetection)) {
diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 87f3939e..b7c269f3 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -553,7 +553,7 @@ Type: - The site group type (ie. synchronization, comparison, localization) + Defines the specific behavior of the site group The date/time when the site was last synchronized @@ -564,7 +564,7 @@ Localization - - Comparison + + Change Detection \ No newline at end of file diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index 2b126f25..028b33ec 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -35,7 +35,7 @@
- @if (_siteGroups.Any(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison) && item.PrimarySiteId == PageState.Site.SiteId)) + @if (_siteGroups.Any(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId)) {
@@ -642,7 +642,7 @@ private async Task SynchronizeSite() { - foreach (var group in _siteGroups.Where(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison) && item.PrimarySiteId == PageState.Site.SiteId)) + foreach (var group in _siteGroups.Where(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId)) { group.Synchronize = true; await SiteGroupService.UpdateSiteGroupAsync(group); diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 32ac379c..71e83467 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -52,7 +52,7 @@ namespace Oqtane.Infrastructure var siteGroups = siteGroupRepository.GetSiteGroups(); // iterate through site groups which need to be synchronized - foreach (var siteGroup in siteGroups.Where(item => item.Synchronize && (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.Comparison))) + foreach (var siteGroup in siteGroups.Where(item => item.Synchronize && (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection))) { // get data if (siteGroupMembers == null) @@ -201,7 +201,7 @@ namespace Oqtane.Infrastructure syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySite.SiteId, SyncEventActions.Refresh); } - if (!string.IsNullOrEmpty(log) && siteGroupMember.SiteGroup.Type == SiteGroupTypes.Comparison) + if (!string.IsNullOrEmpty(log) && siteGroupMember.SiteGroup.Type == SiteGroupTypes.ChangeDetection) { // send change log to administrators SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, log); diff --git a/Oqtane.Shared/Shared/SiteGroupTypes.cs b/Oqtane.Shared/Shared/SiteGroupTypes.cs index 55e0f7ff..e1a56052 100644 --- a/Oqtane.Shared/Shared/SiteGroupTypes.cs +++ b/Oqtane.Shared/Shared/SiteGroupTypes.cs @@ -3,7 +3,7 @@ namespace Oqtane.Shared public class SiteGroupTypes { public const string Synchronization = "Synchronization"; - public const string Comparison = "Comparison"; + public const string ChangeDetection = "ChangeDetection"; public const string Localization = "Localization"; } } From 6f6870b16d00639e6f37752ecc1b83eb0e6a3469 Mon Sep 17 00:00:00 2001 From: "Kuyck, Pieter" Date: Tue, 17 Feb 2026 15:59:20 +0100 Subject: [PATCH 51/75] #6046: FileRepository.GetFile: compare both filenames in the same cast. --- .../Client/Properties/launchSettings.json | 12 ++++++++++++ Oqtane.Server/Repository/FileRepository.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 Oqtane.Application/Client/Properties/launchSettings.json diff --git a/Oqtane.Application/Client/Properties/launchSettings.json b/Oqtane.Application/Client/Properties/launchSettings.json new file mode 100644 index 00000000..e8883ef9 --- /dev/null +++ b/Oqtane.Application/Client/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Oqtane.Application.Client": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:62381;http://localhost:62382" + } + } +} \ No newline at end of file diff --git a/Oqtane.Server/Repository/FileRepository.cs b/Oqtane.Server/Repository/FileRepository.cs index 3cd3e2bf..873626d2 100644 --- a/Oqtane.Server/Repository/FileRepository.cs +++ b/Oqtane.Server/Repository/FileRepository.cs @@ -121,7 +121,7 @@ namespace Oqtane.Repository var file = db.File.AsNoTracking() .Include(item => item.Folder) .FirstOrDefault(item => item.FolderId == folderId && - item.Name.ToLower() == fileName); + item.Name.ToLower() == fileName.ToLower()); if (file != null) { From 912ed66547b4cfa2b06c21d6cfe70da728fe2b5a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 17 Feb 2026 12:16:58 -0500 Subject: [PATCH 52/75] performance optimization to limit the number of HtmlText content versions --- Oqtane.Client/Modules/HtmlText/Edit.razor | 4 ++-- Oqtane.Client/Modules/HtmlText/Settings.razor | 12 +++++++++++ .../Resources/Modules/HtmlText/Settings.resx | 6 ++++++ .../HtmlText/Repository/HtmlTextRepository.cs | 21 ++++++++++++++++--- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Oqtane.Client/Modules/HtmlText/Edit.razor b/Oqtane.Client/Modules/HtmlText/Edit.razor index 9ad9628b..384a27b4 100644 --- a/Oqtane.Client/Modules/HtmlText/Edit.razor +++ b/Oqtane.Client/Modules/HtmlText/Edit.razor @@ -31,8 +31,8 @@       - @SharedLocalizer["CreatedOn"] - @SharedLocalizer["CreatedBy"] + @Localizer["CreatedOn"] + @Localizer["CreatedBy"] diff --git a/Oqtane.Client/Modules/HtmlText/Settings.razor b/Oqtane.Client/Modules/HtmlText/Settings.razor index 783e1180..85413e55 100644 --- a/Oqtane.Client/Modules/HtmlText/Settings.razor +++ b/Oqtane.Client/Modules/HtmlText/Settings.razor @@ -16,6 +16,12 @@ +
+ +
+ +
+
@@ -26,12 +32,14 @@ private bool validated = false; private string _dynamictokens; + private string _versions = "5"; protected override void OnInitialized() { try { _dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false"); + _versions = SettingService.GetSetting(ModuleState.Settings, "Versions", "3"); } catch (Exception ex) { @@ -45,6 +53,10 @@ { var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens); + if (int.TryParse(_versions, out int versions) && versions >= 0 && versions <= 9) + { + settings = SettingService.SetSetting(settings, "Versions", versions.ToString()); + } await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); } catch (Exception ex) diff --git a/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx b/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx index 68a539c0..f72cd586 100644 --- a/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx +++ b/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx @@ -123,4 +123,10 @@ Dynamic Tokens? + + The number of content versions to preserve (note that zero means unlimited) + + + Versions: + \ No newline at end of file diff --git a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs index 53688d4c..2f999602 100644 --- a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs +++ b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs @@ -1,7 +1,9 @@ -using System.Linq; -using Oqtane.Documentation; using System.Collections.Generic; +using System.Linq; using Microsoft.EntityFrameworkCore; +using Oqtane.Documentation; +using Oqtane.Repository; +using Oqtane.Shared; namespace Oqtane.Modules.HtmlText.Repository { @@ -18,10 +20,12 @@ namespace Oqtane.Modules.HtmlText.Repository public class HtmlTextRepository : IHtmlTextRepository, ITransientService { private readonly IDbContextFactory _factory; + private readonly ISettingRepository _settingRepository; - public HtmlTextRepository(IDbContextFactory factory) + public HtmlTextRepository(IDbContextFactory factory, ISettingRepository settingRepository) { _factory = factory; + _settingRepository = settingRepository; } public IEnumerable GetHtmlTexts(int moduleId) @@ -39,6 +43,17 @@ namespace Oqtane.Modules.HtmlText.Repository public Models.HtmlText AddHtmlText(Models.HtmlText htmlText) { using var db = _factory.CreateDbContext(); + + var versions = int.Parse(_settingRepository.GetSettingValue(EntityNames.Module, htmlText.ModuleId, "Versions", "3")); + if (versions > 0) + { + var htmlTexts = db.HtmlText.Where(item => item.ModuleId == htmlText.ModuleId).OrderByDescending(item => item.CreatedOn).ToList(); + for (int i = versions - 1; i < htmlTexts.Count; i++) + { + db.HtmlText.Remove(htmlTexts[i]); + } + } + db.HtmlText.Add(htmlText); db.SaveChanges(); return htmlText; From b25279cdcff7944d7ae0c1a08fae41dbffb488de Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Tue, 17 Feb 2026 12:19:11 -0500 Subject: [PATCH 53/75] Delete Oqtane.Application/Client/Properties/launchSettings.json --- .../Client/Properties/launchSettings.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Oqtane.Application/Client/Properties/launchSettings.json diff --git a/Oqtane.Application/Client/Properties/launchSettings.json b/Oqtane.Application/Client/Properties/launchSettings.json deleted file mode 100644 index e8883ef9..00000000 --- a/Oqtane.Application/Client/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "Oqtane.Application.Client": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:62381;http://localhost:62382" - } - } -} \ No newline at end of file From df7f3f7bba8752e290b1c049c872cf94fc8b8232 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 17 Feb 2026 16:25:10 -0500 Subject: [PATCH 54/75] performance and scalability improvement - get the most recent HtmlText record directly from database and cache only a single object rather than the entire collection --- Oqtane.Client/Modules/HtmlText/Edit.razor | 46 +++++++----------- .../HtmlText/Services/HtmlTextService.cs | 7 --- Oqtane.Client/Modules/HtmlText/Settings.razor | 2 +- .../Controllers/HtmlTextController.cs | 17 ------- .../HtmlText/Manager/HtmlTextManager.cs | 48 +++++++------------ .../HtmlText/Repository/HtmlTextRepository.cs | 9 ++-- .../HtmlText/Services/HtmlTextService.cs | 40 +++++----------- 7 files changed, 50 insertions(+), 119 deletions(-) diff --git a/Oqtane.Client/Modules/HtmlText/Edit.razor b/Oqtane.Client/Modules/HtmlText/Edit.razor index 384a27b4..7f8ed55a 100644 --- a/Oqtane.Client/Modules/HtmlText/Edit.razor +++ b/Oqtane.Client/Modules/HtmlText/Edit.razor @@ -122,13 +122,9 @@ { try { - htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId); - if (htmltext != null) - { - _view = htmltext.Content; - _view = Utilities.FormatContent(_view, PageState.Alias, "render"); - StateHasChanged(); - } + _view = htmltext.Content; + _view = Utilities.FormatContent(_view, PageState.Alias, "render"); + StateHasChanged(); } catch (Exception ex) { @@ -141,19 +137,15 @@ { try { - htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId); - if (htmltext != null) - { - var content = htmltext.Content; - htmltext = new HtmlText(); - htmltext.ModuleId = ModuleState.ModuleId; - htmltext.Content = content; - await HtmlTextService.AddHtmlTextAsync(htmltext); - await logger.LogInformation("Content Restored {HtmlText}", htmltext); - AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success); - await LoadContent(); - StateHasChanged(); - } + var content = htmltext.Content; + htmltext = new HtmlText(); + htmltext.ModuleId = ModuleState.ModuleId; + htmltext.Content = content; + await HtmlTextService.AddHtmlTextAsync(htmltext); + await logger.LogInformation("Content Restored {HtmlText}", htmltext); + AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success); + await LoadContent(); + StateHasChanged(); } catch (Exception ex) { @@ -166,15 +158,11 @@ { try { - htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId); - if (htmltext != null) - { - await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId); - await logger.LogInformation("Content Deleted {HtmlText}", htmltext); - AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success); - await LoadContent(); - StateHasChanged(); - } + await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId); + await logger.LogInformation("Content Deleted {HtmlText}", htmltext); + AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success); + await LoadContent(); + StateHasChanged(); } catch (Exception ex) { diff --git a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs index 600374cf..324cfedb 100644 --- a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs +++ b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs @@ -14,8 +14,6 @@ namespace Oqtane.Modules.HtmlText.Services Task GetHtmlTextAsync(int moduleId); - Task GetHtmlTextAsync(int htmlTextId, int moduleId); - Task AddHtmlTextAsync(Models.HtmlText htmltext); Task DeleteHtmlTextAsync(int htmlTextId, int moduleId); @@ -38,11 +36,6 @@ namespace Oqtane.Modules.HtmlText.Services return await GetJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId)); } - public async Task GetHtmlTextAsync(int htmlTextId, int moduleId) - { - return await GetJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId)); - } - public async Task AddHtmlTextAsync(Models.HtmlText htmlText) { return await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText); diff --git a/Oqtane.Client/Modules/HtmlText/Settings.razor b/Oqtane.Client/Modules/HtmlText/Settings.razor index 85413e55..eaf30bc0 100644 --- a/Oqtane.Client/Modules/HtmlText/Settings.razor +++ b/Oqtane.Client/Modules/HtmlText/Settings.razor @@ -39,7 +39,7 @@ try { _dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false"); - _versions = SettingService.GetSetting(ModuleState.Settings, "Versions", "3"); + _versions = SettingService.GetSetting(ModuleState.Settings, "Versions", "5"); } catch (Exception ex) { diff --git a/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs b/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs index fb8ef512..c5b6d851 100644 --- a/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs +++ b/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs @@ -58,23 +58,6 @@ namespace Oqtane.Modules.HtmlText.Controllers } } - // GET api//5/6 - [HttpGet("{id}/{moduleId}")] - [Authorize(Policy = PolicyNames.ViewModule)] - public async Task Get(int id, int moduleId) - { - if (IsAuthorizedEntityId(EntityNames.Module, moduleId)) - { - return await _htmlTextService.GetHtmlTextAsync(id, moduleId); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Get Attempt {HtmlTextId}", id); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return null; - } - } - // POST api/ [HttpPost] [Authorize(Policy = PolicyNames.EditModule)] diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index 93647fe7..cbef97fb 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -1,18 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; -using Oqtane.Infrastructure; -using Oqtane.Models; -using Oqtane.Modules.HtmlText.Repository; using System.Net; -using Oqtane.Enums; -using Oqtane.Repository; -using Oqtane.Shared; -using Oqtane.Migrations.Framework; -using Oqtane.Documentation; -using Oqtane.Interfaces; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; +using Oqtane.Documentation; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Interfaces; +using Oqtane.Migrations.Framework; +using Oqtane.Models; +using Oqtane.Modules.HtmlText.Repository; +using Oqtane.Repository; +using Oqtane.Shared; // ReSharper disable ConvertToUsingDeclaration @@ -21,20 +21,15 @@ namespace Oqtane.Modules.HtmlText.Manager [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] public class HtmlTextManager : MigratableModuleBase, IInstallable, IPortable, ISynchronizable, ISearchable { - private readonly IHtmlTextRepository _htmlText; + private readonly IHtmlTextRepository _htmlTextRepository; private readonly IDBContextDependencies _DBContextDependencies; private readonly ISqlRepository _sqlRepository; private readonly ITenantManager _tenantManager; private readonly IMemoryCache _cache; - public HtmlTextManager( - IHtmlTextRepository htmlText, - IDBContextDependencies DBContextDependencies, - ISqlRepository sqlRepository, - ITenantManager tenantManager, - IMemoryCache cache) + public HtmlTextManager(IHtmlTextRepository htmlTextRepository, IDBContextDependencies DBContextDependencies, ISqlRepository sqlRepository, ITenantManager tenantManager, IMemoryCache cache) { - _htmlText = htmlText; + _htmlTextRepository = htmlTextRepository; _DBContextDependencies = DBContextDependencies; _sqlRepository = sqlRepository; _tenantManager = tenantManager; @@ -61,7 +56,7 @@ namespace Oqtane.Modules.HtmlText.Manager public string ExportModule(Module module) { string content = ""; - var htmltext = GetModuleContent(module.ModuleId); + var htmltext = _htmlTextRepository.GetHtmlText(module.ModuleId); if (htmltext != null) { content = WebUtility.HtmlEncode(htmltext.Content); @@ -78,7 +73,7 @@ namespace Oqtane.Modules.HtmlText.Manager public string ExtractModule(Module module, DateTime lastSynchronizedOn) { string content = ""; - var htmltext = GetModuleContent(module.ModuleId); + var htmltext = _htmlTextRepository.GetHtmlText(module.ModuleId); if (htmltext != null && htmltext.CreatedOn > lastSynchronizedOn) { content = WebUtility.HtmlEncode(htmltext.Content); @@ -91,24 +86,13 @@ namespace Oqtane.Modules.HtmlText.Manager SaveModuleContent(module, content); } - private Models.HtmlText GetModuleContent(int moduleId) - { - // get the most recent htmltext record for the module - var htmltexts = _htmlText.GetHtmlTexts(moduleId); - if (htmltexts != null && htmltexts.Any()) - { - return htmltexts.OrderByDescending(item => item.CreatedOn).First(); - } - return null; - } - private void SaveModuleContent(Module module, string content) { content = WebUtility.HtmlDecode(content); var htmlText = new Models.HtmlText(); htmlText.ModuleId = module.ModuleId; htmlText.Content = content; - _htmlText.AddHtmlText(htmlText); + _htmlTextRepository.AddHtmlText(htmlText); //clear the cache for the module var alias = _tenantManager.GetAlias(); @@ -123,7 +107,7 @@ namespace Oqtane.Modules.HtmlText.Manager { var searchContents = new List(); - var htmltext = _htmlText.GetHtmlTexts(pageModule.ModuleId)?.OrderByDescending(item => item.CreatedOn).FirstOrDefault(); + var htmltext = _htmlTextRepository.GetHtmlText(pageModule.ModuleId); if (htmltext != null && htmltext.CreatedOn >= lastIndexedOn) { searchContents.Add(new SearchContent diff --git a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs index 2f999602..490242a5 100644 --- a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs +++ b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs @@ -11,7 +11,7 @@ namespace Oqtane.Modules.HtmlText.Repository public interface IHtmlTextRepository { IEnumerable GetHtmlTexts(int moduleId); - Models.HtmlText GetHtmlText(int htmlTextId); + Models.HtmlText GetHtmlText(int moduleId); Models.HtmlText AddHtmlText(Models.HtmlText htmlText); void DeleteHtmlText(int htmlTextId); } @@ -34,17 +34,18 @@ namespace Oqtane.Modules.HtmlText.Repository return db.HtmlText.Where(item => item.ModuleId == moduleId).ToList(); } - public Models.HtmlText GetHtmlText(int htmlTextId) + public Models.HtmlText GetHtmlText(int moduleId) { using var db = _factory.CreateDbContext(); - return db.HtmlText.Find(htmlTextId); + return db.HtmlText.Where(item => item.ModuleId == moduleId)? + .OrderByDescending(item => item.CreatedOn).FirstOrDefault(); } public Models.HtmlText AddHtmlText(Models.HtmlText htmlText) { using var db = _factory.CreateDbContext(); - var versions = int.Parse(_settingRepository.GetSettingValue(EntityNames.Module, htmlText.ModuleId, "Versions", "3")); + var versions = int.Parse(_settingRepository.GetSettingValue(EntityNames.Module, htmlText.ModuleId, "Versions", "5")); if (versions > 0) { var htmlTexts = db.HtmlText.Where(item => item.ModuleId == htmlText.ModuleId).OrderByDescending(item => item.CreatedOn).ToList(); diff --git a/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs index 49055c4e..8e9150b3 100644 --- a/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs +++ b/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs @@ -17,16 +17,16 @@ namespace Oqtane.Modules.HtmlText.Services [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] public class ServerHtmlTextService : IHtmlTextService, ITransientService { - private readonly IHtmlTextRepository _htmlText; + private readonly IHtmlTextRepository _htmlTextRepository; private readonly IUserPermissions _userPermissions; private readonly IMemoryCache _cache; private readonly ILogManager _logger; private readonly IHttpContextAccessor _accessor; private readonly Alias _alias; - public ServerHtmlTextService(IHtmlTextRepository htmlText, IUserPermissions userPermissions, IMemoryCache cache, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor) + public ServerHtmlTextService(IHtmlTextRepository htmlTextRepository, IUserPermissions userPermissions, IMemoryCache cache, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor) { - _htmlText = htmlText; + _htmlTextRepository = htmlTextRepository; _userPermissions = userPermissions; _cache = cache; _logger = logger; @@ -38,7 +38,7 @@ namespace Oqtane.Modules.HtmlText.Services { if (_accessor.HttpContext.User.IsInRole(RoleNames.Registered)) { - return Task.FromResult(GetCachedHtmlTexts(moduleId)); + return Task.FromResult(_htmlTextRepository.GetHtmlTexts(moduleId).ToList()); } else { @@ -51,7 +51,11 @@ namespace Oqtane.Modules.HtmlText.Services { if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, moduleId, PermissionNames.View)) { - return Task.FromResult(GetCachedHtmlTexts(moduleId)?.OrderByDescending(item => item.CreatedOn).FirstOrDefault()); + return Task.FromResult(_cache.GetOrCreate($"HtmlText:{_alias.SiteKey}:{moduleId}", entry => + { + entry.SlidingExpiration = TimeSpan.FromMinutes(30); + return _htmlTextRepository.GetHtmlText(moduleId); + })); } else { @@ -60,24 +64,11 @@ namespace Oqtane.Modules.HtmlText.Services } } - public Task GetHtmlTextAsync(int htmlTextId, int moduleId) - { - if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, moduleId, PermissionNames.View)) - { - return Task.FromResult(GetCachedHtmlTexts(moduleId)?.FirstOrDefault(item => item.HtmlTextId == htmlTextId)); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Get Attempt {HtmlTextId} {ModuleId}", htmlTextId, moduleId); - return null; - } - } - public Task AddHtmlTextAsync(Models.HtmlText htmlText) { if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, htmlText.ModuleId, PermissionNames.Edit)) { - htmlText = _htmlText.AddHtmlText(htmlText); + htmlText = _htmlTextRepository.AddHtmlText(htmlText); ClearCache(htmlText.ModuleId); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Html/Text Added {HtmlText}", htmlText); } @@ -93,7 +84,7 @@ namespace Oqtane.Modules.HtmlText.Services { if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, moduleId, PermissionNames.Edit)) { - _htmlText.DeleteHtmlText(htmlTextId); + _htmlTextRepository.DeleteHtmlText(htmlTextId); ClearCache(moduleId); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Html/Text Deleted {HtmlTextId}", htmlTextId); } @@ -104,15 +95,6 @@ namespace Oqtane.Modules.HtmlText.Services return Task.CompletedTask; } - private List GetCachedHtmlTexts(int moduleId) - { - return _cache.GetOrCreate($"HtmlText:{_alias.SiteKey}:{moduleId}", entry => - { - entry.SlidingExpiration = TimeSpan.FromMinutes(30); - return _htmlText.GetHtmlTexts(moduleId).ToList(); - }); - } - private void ClearCache(int moduleId) { _cache.Remove($"HtmlText:{_alias.SiteKey}:{moduleId}"); From 4db58c2866f7a0a61ee2782475dd962cfd556453 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 17 Feb 2026 16:35:15 -0500 Subject: [PATCH 55/75] remove unnecessary using --- Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index cbef97fb..14fa9057 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; From 13a58ed0997cd7b03a7861e2f4990714104e2999 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 18 Feb 2026 13:59:25 -0500 Subject: [PATCH 56/75] add new global replace service for bulk updating content --- .../Modules/Admin/GlobalReplace/Index.razor | 117 ++++++++++++ .../Modules/Admin/GlobalReplace/Index.resx | 174 +++++++++++++++++ Oqtane.Client/Resources/SharedResources.resx | 3 + .../Infrastructure/Jobs/GlobalReplaceJob.cs | 178 ++++++++++++++++++ .../Infrastructure/Jobs/NotificationJob.cs | 6 +- Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs | 2 +- .../Infrastructure/Jobs/SearchIndexJob.cs | 2 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 4 +- .../SiteTemplates/AdminSiteTemplate.cs | 28 +++ .../Infrastructure/UpgradeManager.cs | 42 +++++ .../Repository/ModuleDefinitionRepository.cs | 8 +- Oqtane.Shared/Models/GlobalReplace.cs | 43 +++++ 12 files changed, 599 insertions(+), 8 deletions(-) create mode 100644 Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor create mode 100644 Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx create mode 100644 Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs create mode 100644 Oqtane.Shared/Models/GlobalReplace.cs diff --git a/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor new file mode 100644 index 00000000..7fad443d --- /dev/null +++ b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor @@ -0,0 +1,117 @@ +@namespace Oqtane.Modules.Admin.GlobalReplace +@using System.Text.Json +@inherits ModuleBase +@inject ISettingService SettingService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+

+ +

+ +@code { + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + public override string Title => "Global Replace"; + + private string _find; + private string _replace; + private string _caseSensitive = "True"; + private string _site = "True"; + private string _pages = "True"; + private string _modules = "True"; + private string _content = "True"; + + private async Task Save() + { + try + { + if (!string.IsNullOrEmpty(_find) && !string.IsNullOrEmpty(_replace)) + { + var replace = new GlobalReplace + { + Find = _find, + Replace = _replace, + CaseSensitive = bool.Parse(_caseSensitive), + Site = bool.Parse(_site), + Pages = bool.Parse(_pages), + Modules = bool.Parse(_modules), + Content = bool.Parse(_content) + }; + + var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); + settings = SettingService.SetSetting(settings, "GlobalReplace_" + DateTime.UtcNow.ToString("yyyyMMddHHmmss"), JsonSerializer.Serialize(replace)); + await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); + AddModuleMessage(Localizer["Success.Save"], MessageType.Success); + } + else + { + AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Saving Global Replace Settings {Error}", ex.Message); + AddModuleMessage(Localizer["Error.Save"], MessageType.Error); + } + } + +} \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx b/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx new file mode 100644 index 00000000..08909f15 --- /dev/null +++ b/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Match Case? + + + Specify if the replacement operation should be case sensitive + + + Module Content? + + + Specify if module content should be updated + + + Page Properties? + + + Specify if page properties should be updated (ie. name, title, headcontent, bodycontent) + + + Site Properties? + + + Specify if site properties should be updated (ie. name, headcontent, bodycontent) + + + Replace With: + + + Specify the replacement content + + + Module Properties? + + + Specify if module properties should be updated (ie. title, header, footer) + + + Your Global Replace Request Has Been Submitted And Will Be Executed Shortly + + + Error Saving Global Replace + + + Specify the content which needs to be replaced + + + Find What: + + + Global Replace + + + This Operation is Permanent. Are You Sure You Wish To Proceed? + + \ No newline at end of file diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index e1441d7c..06ea6782 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -483,4 +483,7 @@ Installed + + Global Replace + \ No newline at end of file diff --git a/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs b/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs new file mode 100644 index 00000000..008230af --- /dev/null +++ b/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs @@ -0,0 +1,178 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Modules; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public class GlobalReplaceJob : HostedServiceBase + { + public GlobalReplaceJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + Name = "Global Replace Job"; + Frequency = "m"; // run every minute. + Interval = 1; + IsEnabled = true; + } + + public override async Task ExecuteJobAsync(IServiceProvider provider) + { + string log = ""; + + // get services + var siteRepository = provider.GetRequiredService(); + var settingRepository = provider.GetRequiredService(); + var tenantManager = provider.GetRequiredService(); + var pageRepository = provider.GetRequiredService(); + var pageModuleRepository = provider.GetRequiredService(); + + var sites = siteRepository.GetSites().ToList(); + foreach (var site in sites.Where(item => !item.IsDeleted)) + { + log += $"Processing Site: {site.Name}
"; + + // get global replace items in order by date/time submitted + var globalReplaceSettings = settingRepository.GetSettings(EntityNames.Site, site.SiteId) + .Where(item => item.SettingName.StartsWith("GlobalReplace_")) + .OrderBy(item => item.SettingName); + + if (globalReplaceSettings != null && globalReplaceSettings.Any()) + { + // get first item + var globalReplace = JsonSerializer.Deserialize(globalReplaceSettings.First().SettingValue); + + var find = globalReplace.Find; + var replace = globalReplace.Replace; + var comparisonType = (globalReplace.CaseSensitive) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + + log += $"Replacing: '{find}' With: '{replace}' Case Sensitive: {globalReplace.CaseSensitive}
"; + + var tenantId = tenantManager.GetTenant().TenantId; + tenantManager.SetAlias(tenantId, site.SiteId); + + var changed = false; + if (site.Name != null && site.Name.Contains(find, comparisonType)) + { + site.Name = site.Name.Replace(find, replace, comparisonType); + changed = true; + } + if (site.HeadContent != null && site.HeadContent.Contains(find, comparisonType)) + { + site.HeadContent = site.HeadContent.Replace(find, replace, comparisonType); + changed = true; + } + if (site.BodyContent != null && site.BodyContent.Contains(find, comparisonType)) + { + site.BodyContent = site.BodyContent.Replace(find, replace, comparisonType); + changed = true; + } + if (changed && globalReplace.Site) + { + siteRepository.UpdateSite(site); + log += $"Site Updated
"; + } + + var pages = pageRepository.GetPages(site.SiteId); + var pageModules = pageModuleRepository.GetPageModules(site.SiteId); + + // iterate pages + foreach (var page in pages) + { + // page properties + changed = false; + if (page.Name != null && page.Name.Contains(find, comparisonType)) + { + page.Name = page.Name.Replace(find, replace, comparisonType); + changed = true; + } + if (page.Title != null && page.Title.Contains(find, comparisonType)) + { + page.Title = page.Title.Replace(find, replace, comparisonType); + changed = true; + } + if (page.HeadContent != null && page.HeadContent.Contains(find, comparisonType)) + { + page.HeadContent = page.HeadContent.Replace(find, replace, comparisonType); + changed = true; + } + if (page.BodyContent != null && page.BodyContent.Contains(find, comparisonType)) + { + page.BodyContent = page.BodyContent.Replace(find, replace, comparisonType); + changed = true; + } + if (changed && globalReplace.Pages) + { + pageRepository.UpdatePage(page); + log += $"Page Updated: /{page.Path}
"; + } + + foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId)) + { + // pagemodule properties + changed = false; + if (pageModule.Title != null && pageModule.Title.Contains(find, comparisonType)) + { + pageModule.Title = pageModule.Title.Replace(find, replace, comparisonType); + changed = true; + } + if (pageModule.Header != null && pageModule.Header.Contains(find, comparisonType)) + { + pageModule.Header = pageModule.Header.Replace(find, replace, comparisonType); + changed = true; + } + if (pageModule.Footer != null && pageModule.Footer.Contains(find, comparisonType)) + { + pageModule.Footer = pageModule.Footer.Replace(find, replace, comparisonType); + changed = true; + } + if (changed && globalReplace.Modules) + { + pageModuleRepository.UpdatePageModule(pageModule); + log += $"Module Updated: {pageModule.Title} - /{page.Path}
"; + } + + // module content + if (pageModule.Module.ModuleDefinition != null && pageModule.Module.ModuleDefinition.ServerManagerType != "") + { + Type moduleType = Type.GetType(pageModule.Module.ModuleDefinition.ServerManagerType); + if (moduleType != null && moduleType.GetInterface(nameof(IPortable)) != null) + { + try + { + // module content + var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); + var moduleContent = ((IPortable)moduleObject).ExportModule(pageModule.Module); + if (!string.IsNullOrEmpty(moduleContent) && moduleContent.Contains(find, comparisonType) && globalReplace.Content) + { + moduleContent = moduleContent.Replace(find, replace, comparisonType); + ((IPortable)moduleObject).ImportModule(pageModule.Module, moduleContent, pageModule.Module.ModuleDefinition.Version); + log += $"Module Content Updated: {pageModule.Title} - /{page.Path}
"; + } + } + catch (Exception ex) + { + log += $"Error Processing Module {pageModule.Module.ModuleDefinition.Name} - {ex.Message}
"; + } + } + } + } + } + + // remove global replace setting to prevent reprocessing + settingRepository.DeleteSetting(EntityNames.Site, globalReplaceSettings.First().SettingId); + } + else + { + log += $"No Criteria Provided
"; + } + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index 48589187..4f5fbd74 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -38,7 +38,7 @@ namespace Oqtane.Infrastructure // iterate through sites for current tenant List sites = siteRepository.GetSites().ToList(); - foreach (Site site in sites) + foreach (Site site in sites.Where(item => !item.IsDeleted)) { log += "Processing Notifications For Site: " + site.Name + "
"; @@ -48,7 +48,7 @@ namespace Oqtane.Infrastructure // get site settings var settings = settingRepository.GetSettings(EntityNames.Site, site.SiteId, EntityNames.Host, -1); - if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True") + if (settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True") { bool valid = true; if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic") @@ -290,7 +290,7 @@ namespace Oqtane.Infrastructure } else { - log += "Site Deleted Or SMTP Disabled In Site Settings
"; + log += "SMTP Disabled In Site Settings
"; } } else diff --git a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs index 3bcff41d..06c88ab5 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -36,7 +36,7 @@ namespace Oqtane.Infrastructure // iterate through sites for current tenant List sites = siteRepository.GetSites().ToList(); - foreach (Site site in sites) + foreach (Site site in sites.Where(item => !item.IsDeleted)) { log += "
Processing Site: " + site.Name + "
"; int count; diff --git a/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs b/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs index 9b2d2f83..6549c14d 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs @@ -40,7 +40,7 @@ namespace Oqtane.Infrastructure var searchService = provider.GetRequiredService(); var sites = siteRepository.GetSites().ToList(); - foreach (var site in sites) + foreach (var site in sites.Where(item => !item.IsDeleted)) { log += $"Indexing Site: {site.Name}
"; diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 71e83467..b59611e7 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -68,7 +68,7 @@ namespace Oqtane.Infrastructure // get primary site var primarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.PrimarySiteId); - if (primarySite != null) + if (primarySite != null && !primarySite.IsDeleted) { // update flag to prevent job from processing group again siteGroup.Synchronize = false; @@ -112,7 +112,7 @@ namespace Oqtane.Infrastructure } else { - log += $"Site Group {siteGroup.Name} Has A PrimarySiteId {siteGroup.PrimarySiteId} Which Does Not Exist
"; + log += $"Site Group {siteGroup.Name} Has A PrimarySiteId {siteGroup.PrimarySiteId} Which Does Not Exist Or Is Deleted
"; } } diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs index 7229c86d..ab26f5ed 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs @@ -607,6 +607,34 @@ namespace Oqtane.Infrastructure.SiteTemplates } } }); + pageTemplates.Add(new PageTemplate + { + Name = "Global Replace", + Parent = "Admin", + Order = 23, + Path = "admin/replace", + Icon = Icons.LoopSquare, + IsNavigation = false, + IsPersonalizable = false, + PermissionList = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + PageTemplateModules = new List + { + new PageTemplateModule + { + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.GlobalReplace.Index).ToModuleDefinitionName(), Title = "Global Replace", Pane = PaneNames.Default, + PermissionList = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + Content = "" + } + } + }); // host pages (order starts at 51) pageTemplates.Add(new PageTemplate diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 64e02f03..6b4d15fa 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -93,6 +93,9 @@ namespace Oqtane.Infrastructure case "10.0.4": Upgrade_10_0_4(tenant, scope); break; + case "10.1.0": + Upgrade_10_1_0(tenant, scope); + break; } } } @@ -602,6 +605,45 @@ namespace Oqtane.Infrastructure RemoveAssemblies(tenant, assemblies, "10.0.4"); } + private void Upgrade_10_1_0(Tenant tenant, IServiceScope scope) + { + var pageTemplates = new List + { + new PageTemplate + { + Update = false, + Name = "Global Replace", + Parent = "Admin", + Order = 23, + Path = "admin/replace", + Icon = Icons.LoopSquare, + IsNavigation = false, + IsPersonalizable = false, + PermissionList = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + PageTemplateModules = new List + { + new PageTemplateModule + { + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.GlobalReplace.Index).ToModuleDefinitionName(), Title = "Global Replace", Pane = PaneNames.Default, + PermissionList = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + Content = "" + } + } + } + }; + + AddPagesToSites(scope, tenant, pageTemplates); + } + + private void AddPagesToSites(IServiceScope scope, Tenant tenant, List pageTemplates) { var tenants = scope.ServiceProvider.GetRequiredService(); diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index aa48d232..fd1e11e7 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -354,7 +354,7 @@ namespace Oqtane.Repository moduledefinition = new ModuleDefinition { Name = Utilities.GetTypeNameLastSegment(modulecontroltype.Namespace, 0), - Description = "Manage " + Utilities.GetTypeNameLastSegment(modulecontroltype.Namespace, 0), + Description = Utilities.GetTypeNameLastSegment(modulecontroltype.Namespace, 0), Categories = ((qualifiedModuleType.StartsWith("Oqtane.Modules.Admin.")) ? "Admin" : "") }; } @@ -434,6 +434,12 @@ namespace Oqtane.Repository moduledefinition.ControlTypeRoutes += (action + "=" + modulecontroltype.FullName + ", " + modulecontroltype.Assembly.GetName().Name + ";"); } } + // module title + if (modulecontroltype.Name == Constants.DefaultAction && !string.IsNullOrEmpty(modulecontrolobject.Title)) + { + moduledefinition.Name = modulecontrolobject.Title; + moduledefinition.Description = "Manage " + moduledefinition.Name; + } // check for Page attribute var routeAttributes = modulecontroltype.GetCustomAttributes(typeof(RouteAttribute), true).Cast(); diff --git a/Oqtane.Shared/Models/GlobalReplace.cs b/Oqtane.Shared/Models/GlobalReplace.cs new file mode 100644 index 00000000..c2a63ed3 --- /dev/null +++ b/Oqtane.Shared/Models/GlobalReplace.cs @@ -0,0 +1,43 @@ +namespace Oqtane.Models +{ + /// + /// Describes a global replace operation + /// + public class GlobalReplace + { + /// + /// text to replace + /// + public string Find { get; set; } + + /// + /// replacement text + /// + public string Replace { get; set; } + + /// + /// case sensitive + /// + public bool CaseSensitive { get; set; } + + /// + /// include site properties + /// + public bool Site { get; set; } + + /// + /// include page properties + /// + public bool Pages { get; set; } + + /// + /// include module properties + /// + public bool Modules { get; set; } + + /// + /// include content + /// + public bool Content { get; set; } + } +} From 0fd97d34d951e3d9a1ebb88f745bbeb05ca80576 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 19 Feb 2026 08:23:11 -0500 Subject: [PATCH 57/75] add Job Tasks to enable the execution of adhoc asynchronous site-based workloads --- .../OqtaneServiceCollectionExtensions.cs | 1 + .../Modules/Admin/GlobalReplace/Index.razor | 8 +- Oqtane.Client/Modules/Admin/Users/Users.razor | 16 +- .../Modules/Admin/GlobalReplace/Index.resx | 2 +- .../Resources/Modules/Admin/Users/Users.resx | 5 +- Oqtane.Client/Services/JobTaskService.cs | 46 +++++ Oqtane.Client/Services/UserService.cs | 14 -- .../Controllers/JobTaskController.cs | 63 +++++++ Oqtane.Server/Controllers/UserController.cs | 36 ---- .../OqtaneServiceCollectionExtensions.cs | 2 + .../Infrastructure/Interfaces/IJobTask.cs | 13 ++ .../Infrastructure/Jobs/GlobalReplaceJob.cs | 178 ------------------ Oqtane.Server/Infrastructure/Jobs/TaskJob.cs | 83 ++++++++ .../Infrastructure/Tasks/GlobalReplaceTask.cs | 150 +++++++++++++++ .../Infrastructure/Tasks/ImportUsersTask.cs | 48 +++++ .../Infrastructure/Tasks/JobTaskBase.cs | 19 ++ .../EntityBuilders/JobTaskEntityBuilder.cs | 54 ++++++ .../Migrations/Tenant/10010004_AddJobTasks.cs | 28 +++ .../Repository/Context/TenantDBContext.cs | 1 + Oqtane.Server/Repository/JobLogRepository.cs | 4 +- Oqtane.Server/Repository/JobTaskRepository.cs | 56 ++++++ Oqtane.Shared/Models/Job.cs | 2 +- Oqtane.Shared/Models/JobTask.cs | 56 ++++++ 23 files changed, 633 insertions(+), 252 deletions(-) create mode 100644 Oqtane.Client/Services/JobTaskService.cs create mode 100644 Oqtane.Server/Controllers/JobTaskController.cs create mode 100644 Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs delete mode 100644 Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs create mode 100644 Oqtane.Server/Infrastructure/Jobs/TaskJob.cs create mode 100644 Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs create mode 100644 Oqtane.Server/Infrastructure/Tasks/ImportUsersTask.cs create mode 100644 Oqtane.Server/Infrastructure/Tasks/JobTaskBase.cs create mode 100644 Oqtane.Server/Migrations/EntityBuilders/JobTaskEntityBuilder.cs create mode 100644 Oqtane.Server/Migrations/Tenant/10010004_AddJobTasks.cs create mode 100644 Oqtane.Server/Repository/JobTaskRepository.cs create mode 100644 Oqtane.Shared/Models/JobTask.cs diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 4cb6c220..f699d7fe 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -41,6 +41,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor index 7fad443d..7ac71a70 100644 --- a/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor +++ b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor @@ -1,7 +1,7 @@ @namespace Oqtane.Modules.Admin.GlobalReplace @using System.Text.Json @inherits ModuleBase -@inject ISettingService SettingService +@inject IJobTaskService JobTaskService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -97,9 +97,9 @@ Content = bool.Parse(_content) }; - var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); - settings = SettingService.SetSetting(settings, "GlobalReplace_" + DateTime.UtcNow.ToString("yyyyMMddHHmmss"), JsonSerializer.Serialize(replace)); - await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); + var jobTask = new JobTask(PageState.Site.SiteId, "Global Replace", "Oqtane.Infrastructure.GlobalReplaceTask, Oqtane.Server", JsonSerializer.Serialize(replace)); + await JobTaskService.AddJobTaskAsync(jobTask); + AddModuleMessage(Localizer["Success.Save"], MessageType.Success); } else diff --git a/Oqtane.Client/Modules/Admin/Users/Users.razor b/Oqtane.Client/Modules/Admin/Users/Users.razor index a3ac65d3..0a357a57 100644 --- a/Oqtane.Client/Modules/Admin/Users/Users.razor +++ b/Oqtane.Client/Modules/Admin/Users/Users.razor @@ -1,7 +1,7 @@ @namespace Oqtane.Modules.Admin.Users @inherits ModuleBase @inject NavigationManager NavigationManager -@inject IUserService UserService +@inject IJobTaskService JobTaskService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -43,17 +43,9 @@ var fileid = _filemanager.GetFileId(); if (fileid != -1) { - ShowProgressIndicator(); - var results = await UserService.ImportUsersAsync(PageState.Site.SiteId, fileid, bool.Parse(_notify)); - if (bool.Parse(results["Success"])) - { - AddModuleMessage(string.Format(Localizer["Message.Import.Success"], results["Users"]), MessageType.Success); - } - else - { - AddModuleMessage(Localizer["Message.Import.Failure"], MessageType.Error); - } - HideProgressIndicator(); + var jobTask = new JobTask(PageState.Site.SiteId, "Import Users", "Oqtane.Infrastructure.ImportUsersTask, Oqtane.Server", $"{fileid}:{_notify}"); + await JobTaskService.AddJobTaskAsync(jobTask); + AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success); } else { diff --git a/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx b/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx index 08909f15..e5339e4d 100644 --- a/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx @@ -154,7 +154,7 @@ Specify if module properties should be updated (ie. title, header, footer) - Your Global Replace Request Has Been Submitted And Will Be Executed Shortly + Your Global Replace Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient. Error Saving Global Replace diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx index 02e73f23..1b293eaa 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx @@ -129,11 +129,8 @@ Import - - User Import Failed. Please Review Your Event Log For More Detailed Information. - - User Import Successful. {0} Users Imported. + Your User Import Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient. You Must Specify A User File For Import diff --git a/Oqtane.Client/Services/JobTaskService.cs b/Oqtane.Client/Services/JobTaskService.cs new file mode 100644 index 00000000..4e596a52 --- /dev/null +++ b/Oqtane.Client/Services/JobTaskService.cs @@ -0,0 +1,46 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using Oqtane.Documentation; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + /// + /// Service to manage tasks () + /// + public interface IJobTaskService + { + /// + /// Return a specific task + /// + /// + /// + Task GetJobTaskAsync(int jobTaskId); + + /// + /// Adds a new task + /// + /// + /// + Task AddJobTaskAsync(JobTask jobTask); + } + + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class JobTaskService : ServiceBase, IJobTaskService + { + public JobTaskService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("JobTask"); + + public async Task GetJobTaskAsync(int jobTaskId) + { + return await GetJsonAsync($"{Apiurl}/{jobTaskId}"); + } + + public async Task AddJobTaskAsync(JobTask jobTask) + { + return await PostJsonAsync(Apiurl, jobTask); + } + } +} diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 049a93d5..bc20f9b2 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -161,15 +161,6 @@ namespace Oqtane.Services /// Task GetPasswordRequirementsAsync(int siteId); - /// - /// Bulk import of users - /// - /// ID of a - /// ID of a - /// Indicates if new users should be notified by email - /// - Task> ImportUsersAsync(int siteId, int fileId, bool notify); - /// /// Get passkeys for a user /// @@ -351,11 +342,6 @@ namespace Oqtane.Services return string.Format(passwordValidationCriteriaTemplate, minimumlength, uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement); } - public async Task> ImportUsersAsync(int siteId, int fileId, bool notify) - { - return await PostJsonAsync>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}¬ify={notify}", null); - } - public async Task> GetPasskeysAsync(int userId) { return await GetJsonAsync>($"{Apiurl}/passkey?id={userId}"); diff --git a/Oqtane.Server/Controllers/JobTaskController.cs b/Oqtane.Server/Controllers/JobTaskController.cs new file mode 100644 index 00000000..5e02f229 --- /dev/null +++ b/Oqtane.Server/Controllers/JobTaskController.cs @@ -0,0 +1,63 @@ +using System.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class JobTaskController : Controller + { + private readonly IJobTaskRepository _jobTasks; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public JobTaskController(IJobTaskRepository jobTasks, ILogManager logger, ITenantManager tenantManager) + { + _jobTasks = jobTasks; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET api//5 + [HttpGet("{id}")] + [Authorize(Roles = RoleNames.Admin)] + public JobTask Get(int id) + { + var jobTask = _jobTasks.GetJobTask(id); + if (jobTask.SiteId == _alias.SiteId) + { + return jobTask; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Admin)] + public JobTask Post([FromBody] JobTask jobTask) + { + if (ModelState.IsValid && jobTask.SiteId == _alias.SiteId) + { + jobTask.IsCompleted = false; + jobTask = _jobTasks.AddJobTask(jobTask); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Job Task Added {JobTask}", jobTask); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Job Task Post Attempt {JobTask}", jobTask); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + jobTask = null; + } + return jobTask; + } + } +} diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index aa6aa909..8d466356 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -418,42 +418,6 @@ namespace Oqtane.Controllers return requirements; } - // POST api//import?siteid=x&fileid=y¬ify=z - [HttpPost("import")] - [Authorize(Roles = RoleNames.Admin)] - public async Task> Import(string siteid, string fileid, string notify) - { - if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId && int.TryParse(fileid, out int FileId) && bool.TryParse(notify, out bool Notify)) - { - var file = _files.GetFile(FileId); - if (file != null) - { - if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList)) - { - return await _userManager.ImportUsers(SiteId, _files.GetFilePath(file), Notify); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return null; - } - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Import File Does Not Exist {SiteId} {FileId}", siteid, fileid); - HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; - return null; - } - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return null; - } - } - // GET: api//passkey?id=x [HttpGet("passkey")] [Authorize] diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 8249560a..a26768ee 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -214,6 +214,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -274,6 +275,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs b/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs new file mode 100644 index 00000000..c2941553 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using Oqtane.Models; + +namespace Oqtane.Infrastructure +{ + public interface IJobTask + { + string ExecuteTask(IServiceProvider provider, Site site, string parameters); + + Task ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters); + } +} diff --git a/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs b/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs deleted file mode 100644 index 008230af..00000000 --- a/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Oqtane.Models; -using Oqtane.Modules; -using Oqtane.Repository; -using Oqtane.Shared; - -namespace Oqtane.Infrastructure -{ - public class GlobalReplaceJob : HostedServiceBase - { - public GlobalReplaceJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) - { - Name = "Global Replace Job"; - Frequency = "m"; // run every minute. - Interval = 1; - IsEnabled = true; - } - - public override async Task ExecuteJobAsync(IServiceProvider provider) - { - string log = ""; - - // get services - var siteRepository = provider.GetRequiredService(); - var settingRepository = provider.GetRequiredService(); - var tenantManager = provider.GetRequiredService(); - var pageRepository = provider.GetRequiredService(); - var pageModuleRepository = provider.GetRequiredService(); - - var sites = siteRepository.GetSites().ToList(); - foreach (var site in sites.Where(item => !item.IsDeleted)) - { - log += $"Processing Site: {site.Name}
"; - - // get global replace items in order by date/time submitted - var globalReplaceSettings = settingRepository.GetSettings(EntityNames.Site, site.SiteId) - .Where(item => item.SettingName.StartsWith("GlobalReplace_")) - .OrderBy(item => item.SettingName); - - if (globalReplaceSettings != null && globalReplaceSettings.Any()) - { - // get first item - var globalReplace = JsonSerializer.Deserialize(globalReplaceSettings.First().SettingValue); - - var find = globalReplace.Find; - var replace = globalReplace.Replace; - var comparisonType = (globalReplace.CaseSensitive) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - - log += $"Replacing: '{find}' With: '{replace}' Case Sensitive: {globalReplace.CaseSensitive}
"; - - var tenantId = tenantManager.GetTenant().TenantId; - tenantManager.SetAlias(tenantId, site.SiteId); - - var changed = false; - if (site.Name != null && site.Name.Contains(find, comparisonType)) - { - site.Name = site.Name.Replace(find, replace, comparisonType); - changed = true; - } - if (site.HeadContent != null && site.HeadContent.Contains(find, comparisonType)) - { - site.HeadContent = site.HeadContent.Replace(find, replace, comparisonType); - changed = true; - } - if (site.BodyContent != null && site.BodyContent.Contains(find, comparisonType)) - { - site.BodyContent = site.BodyContent.Replace(find, replace, comparisonType); - changed = true; - } - if (changed && globalReplace.Site) - { - siteRepository.UpdateSite(site); - log += $"Site Updated
"; - } - - var pages = pageRepository.GetPages(site.SiteId); - var pageModules = pageModuleRepository.GetPageModules(site.SiteId); - - // iterate pages - foreach (var page in pages) - { - // page properties - changed = false; - if (page.Name != null && page.Name.Contains(find, comparisonType)) - { - page.Name = page.Name.Replace(find, replace, comparisonType); - changed = true; - } - if (page.Title != null && page.Title.Contains(find, comparisonType)) - { - page.Title = page.Title.Replace(find, replace, comparisonType); - changed = true; - } - if (page.HeadContent != null && page.HeadContent.Contains(find, comparisonType)) - { - page.HeadContent = page.HeadContent.Replace(find, replace, comparisonType); - changed = true; - } - if (page.BodyContent != null && page.BodyContent.Contains(find, comparisonType)) - { - page.BodyContent = page.BodyContent.Replace(find, replace, comparisonType); - changed = true; - } - if (changed && globalReplace.Pages) - { - pageRepository.UpdatePage(page); - log += $"Page Updated: /{page.Path}
"; - } - - foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId)) - { - // pagemodule properties - changed = false; - if (pageModule.Title != null && pageModule.Title.Contains(find, comparisonType)) - { - pageModule.Title = pageModule.Title.Replace(find, replace, comparisonType); - changed = true; - } - if (pageModule.Header != null && pageModule.Header.Contains(find, comparisonType)) - { - pageModule.Header = pageModule.Header.Replace(find, replace, comparisonType); - changed = true; - } - if (pageModule.Footer != null && pageModule.Footer.Contains(find, comparisonType)) - { - pageModule.Footer = pageModule.Footer.Replace(find, replace, comparisonType); - changed = true; - } - if (changed && globalReplace.Modules) - { - pageModuleRepository.UpdatePageModule(pageModule); - log += $"Module Updated: {pageModule.Title} - /{page.Path}
"; - } - - // module content - if (pageModule.Module.ModuleDefinition != null && pageModule.Module.ModuleDefinition.ServerManagerType != "") - { - Type moduleType = Type.GetType(pageModule.Module.ModuleDefinition.ServerManagerType); - if (moduleType != null && moduleType.GetInterface(nameof(IPortable)) != null) - { - try - { - // module content - var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); - var moduleContent = ((IPortable)moduleObject).ExportModule(pageModule.Module); - if (!string.IsNullOrEmpty(moduleContent) && moduleContent.Contains(find, comparisonType) && globalReplace.Content) - { - moduleContent = moduleContent.Replace(find, replace, comparisonType); - ((IPortable)moduleObject).ImportModule(pageModule.Module, moduleContent, pageModule.Module.ModuleDefinition.Version); - log += $"Module Content Updated: {pageModule.Title} - /{page.Path}
"; - } - } - catch (Exception ex) - { - log += $"Error Processing Module {pageModule.Module.ModuleDefinition.Name} - {ex.Message}
"; - } - } - } - } - } - - // remove global replace setting to prevent reprocessing - settingRepository.DeleteSetting(EntityNames.Site, globalReplaceSettings.First().SettingId); - } - else - { - log += $"No Criteria Provided
"; - } - } - - return log; - } - } -} diff --git a/Oqtane.Server/Infrastructure/Jobs/TaskJob.cs b/Oqtane.Server/Infrastructure/Jobs/TaskJob.cs new file mode 100644 index 00000000..bcbf7125 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Jobs/TaskJob.cs @@ -0,0 +1,83 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Repository; + +namespace Oqtane.Infrastructure +{ + public class TaskJob : HostedServiceBase + { + public TaskJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + Name = "Task Job"; + Frequency = "m"; // run every minute + Interval = 1; + IsEnabled = true; + } + + // job is executed for each tenant in installation + public override async Task ExecuteJobAsync(IServiceProvider provider) + { + var log = ""; + + var tenantManager = provider.GetRequiredService(); + var tenant = tenantManager.GetTenant(); + + // iterate through sites for current tenant + var siteRepository = provider.GetRequiredService(); + var sites = siteRepository.GetSites().ToList(); + foreach (var site in sites.Where(item => !item.IsDeleted)) + { + log += $"Processing Site: {site.Name}
"; + + // get incomplete tasks for site + var jobTaskRepository = provider.GetRequiredService(); + var tasks = jobTaskRepository.GetJobTasks(site.SiteId).ToList(); + if (tasks != null && tasks.Any()) + { + foreach (var task in tasks) + { + log += $"Executing Task: {task.Name}
"; + + Type taskType = Type.GetType(task.Type); + if (taskType != null && taskType.GetInterface(nameof(IJobTask)) != null) + { + try + { + tenantManager.SetAlias(tenant.TenantId, site.SiteId); + + var taskObject = ActivatorUtilities.CreateInstance(provider, taskType); + var taskLog = ((IJobTask)taskObject).ExecuteTask(provider, site, task.Parameters); + taskLog += await ((IJobTask)taskObject).ExecuteTaskAsync(provider, site, task.Parameters); + + task.Status = taskLog; + } + catch (Exception ex) + { + task.Status = "Error: " + ex.Message; + } + } + else + { + task.Status = $"Error: Task {task.Name} Has An Invalid Type {task.Type}
"; + } + + // update task + task.IsCompleted = true; + jobTaskRepository.UpdateJobTask(task); + + log += task.Status + "
"; + } + } + else + { + log += "No Tasks To Execute
"; + } + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs b/Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs new file mode 100644 index 00000000..6f8ae05b --- /dev/null +++ b/Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs @@ -0,0 +1,150 @@ +using System; +using System.Linq; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Modules; +using Oqtane.Repository; + +namespace Oqtane.Infrastructure +{ + public class GlobalReplaceTask : JobTaskBase + { + public override string ExecuteTask(IServiceProvider provider, Site site, string parameters) + { + string log = ""; + + // get services + var siteRepository = provider.GetRequiredService(); + var pageRepository = provider.GetRequiredService(); + var pageModuleRepository = provider.GetRequiredService(); + + if (!string.IsNullOrEmpty(parameters)) + { + // get parameters + var globalReplace = JsonSerializer.Deserialize(parameters); + + var find = globalReplace.Find; + var replace = globalReplace.Replace; + var comparisonType = (globalReplace.CaseSensitive) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + + log += $"Replacing: '{find}' With: '{replace}' Case Sensitive: {globalReplace.CaseSensitive}
"; + + // site properties + site = siteRepository.GetSite(site.SiteId); + var changed = false; + if (site.Name != null && site.Name.Contains(find, comparisonType)) + { + site.Name = site.Name.Replace(find, replace, comparisonType); + changed = true; + } + if (site.HeadContent != null && site.HeadContent.Contains(find, comparisonType)) + { + site.HeadContent = site.HeadContent.Replace(find, replace, comparisonType); + changed = true; + } + if (site.BodyContent != null && site.BodyContent.Contains(find, comparisonType)) + { + site.BodyContent = site.BodyContent.Replace(find, replace, comparisonType); + changed = true; + } + if (changed && globalReplace.Site) + { + siteRepository.UpdateSite(site); + log += $"Site Updated
"; + } + + var pages = pageRepository.GetPages(site.SiteId).ToList(); + var pageModules = pageModuleRepository.GetPageModules(site.SiteId).ToList(); + + // iterate pages + foreach (var page in pages) + { + // page properties + changed = false; + if (page.Name != null && page.Name.Contains(find, comparisonType)) + { + page.Name = page.Name.Replace(find, replace, comparisonType); + changed = true; + } + if (page.Title != null && page.Title.Contains(find, comparisonType)) + { + page.Title = page.Title.Replace(find, replace, comparisonType); + changed = true; + } + if (page.HeadContent != null && page.HeadContent.Contains(find, comparisonType)) + { + page.HeadContent = page.HeadContent.Replace(find, replace, comparisonType); + changed = true; + } + if (page.BodyContent != null && page.BodyContent.Contains(find, comparisonType)) + { + page.BodyContent = page.BodyContent.Replace(find, replace, comparisonType); + changed = true; + } + if (changed && globalReplace.Pages) + { + pageRepository.UpdatePage(page); + log += $"Page Updated: /{page.Path}
"; + } + + foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId)) + { + // module properties + changed = false; + if (pageModule.Title != null && pageModule.Title.Contains(find, comparisonType)) + { + pageModule.Title = pageModule.Title.Replace(find, replace, comparisonType); + changed = true; + } + if (pageModule.Header != null && pageModule.Header.Contains(find, comparisonType)) + { + pageModule.Header = pageModule.Header.Replace(find, replace, comparisonType); + changed = true; + } + if (pageModule.Footer != null && pageModule.Footer.Contains(find, comparisonType)) + { + pageModule.Footer = pageModule.Footer.Replace(find, replace, comparisonType); + changed = true; + } + if (changed && globalReplace.Modules) + { + pageModuleRepository.UpdatePageModule(pageModule); + log += $"Module Updated: {pageModule.Title} - /{page.Path}
"; + } + + // module content + if (pageModule.Module.ModuleDefinition != null && pageModule.Module.ModuleDefinition.ServerManagerType != "") + { + Type moduleType = Type.GetType(pageModule.Module.ModuleDefinition.ServerManagerType); + if (moduleType != null && moduleType.GetInterface(nameof(IPortable)) != null) + { + try + { + var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); + var moduleContent = ((IPortable)moduleObject).ExportModule(pageModule.Module); + if (!string.IsNullOrEmpty(moduleContent) && moduleContent.Contains(find, comparisonType) && globalReplace.Content) + { + moduleContent = moduleContent.Replace(find, replace, comparisonType); + ((IPortable)moduleObject).ImportModule(pageModule.Module, moduleContent, pageModule.Module.ModuleDefinition.Version); + log += $"Module Content Updated: {pageModule.Title} - /{page.Path}
"; + } + } + catch (Exception ex) + { + log += $"Error Processing Module {pageModule.Module.ModuleDefinition.Name} - {ex.Message}
"; + } + } + } + } + } + } + else + { + log += $"No Criteria Provided
"; + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Tasks/ImportUsersTask.cs b/Oqtane.Server/Infrastructure/Tasks/ImportUsersTask.cs new file mode 100644 index 00000000..28dd843d --- /dev/null +++ b/Oqtane.Server/Infrastructure/Tasks/ImportUsersTask.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Managers; +using Oqtane.Models; +using Oqtane.Repository; + +namespace Oqtane.Infrastructure +{ + public class ImportUsersTask : JobTaskBase + { + public override async Task ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters) + { + string log = ""; + + if (!string.IsNullOrEmpty(parameters) && parameters.Contains(":")) + { + var fileId = int.Parse(parameters.Split(':')[0]); + var notify = bool.Parse(parameters.Split(':')[1]); + + var fileRepository = provider.GetRequiredService(); + var userManager = provider.GetRequiredService(); + + var file = fileRepository.GetFile(fileId); + if (file != null) + { + var filePath = fileRepository.GetFilePath(file); + log += $"Importing Users From {filePath}
"; + var result = await userManager.ImportUsers(site.SiteId, filePath, notify); + if (result["Success"] == "True") + { + log += $"{result["Users"]} Users Imported
"; + } + else + { + log += $"User Import Failed
"; + } + } + else + { + log += $"Import Users FileId {fileId} Does Not Exist
"; + } + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Tasks/JobTaskBase.cs b/Oqtane.Server/Infrastructure/Tasks/JobTaskBase.cs new file mode 100644 index 00000000..af359c7a --- /dev/null +++ b/Oqtane.Server/Infrastructure/Tasks/JobTaskBase.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using Oqtane.Models; + +namespace Oqtane.Infrastructure +{ + public class JobTaskBase : IJobTask + { + public virtual string ExecuteTask(IServiceProvider provider, Site site, string parameters) + { + return ""; + } + + public virtual Task ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters) + { + return Task.FromResult(string.Empty); + } + } +} diff --git a/Oqtane.Server/Migrations/EntityBuilders/JobTaskEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/JobTaskEntityBuilder.cs new file mode 100644 index 00000000..393e6df9 --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/JobTaskEntityBuilder.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; +using Oqtane.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class JobTaskEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "JobTask"; + private readonly PrimaryKey _primaryKey = new("PK_JobTask", x => x.JobTaskId); + private readonly ForeignKey _siteForeignKey = new("FK_JobTask_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); + + public JobTaskEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_siteForeignKey); + } + + protected override JobTaskEntityBuilder BuildTable(ColumnsBuilder table) + { + JobTaskId = AddAutoIncrementColumn(table,"JobTaskId"); + SiteId = AddIntegerColumn(table,"SiteId"); + Name = AddStringColumn(table, "Name", 200); + Type = AddStringColumn(table, "Type", 200); + Parameters = AddMaxStringColumn(table, "Parameters", true); + IsCompleted = AddBooleanColumn(table, "IsCompleted", true); + Status = AddMaxStringColumn(table, "Status", true); + + AddAuditableColumns(table); + + return this; + } + + public OperationBuilder JobTaskId { get; private set; } + + public OperationBuilder SiteId { get; private set; } + + public OperationBuilder Name { get; private set; } + + public OperationBuilder Type { get; private set; } + + public OperationBuilder Parameters { get; private set; } + + public OperationBuilder IsCompleted { get; private set; } + + public OperationBuilder Status { get; private set; } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/10010004_AddJobTasks.cs b/Oqtane.Server/Migrations/Tenant/10010004_AddJobTasks.cs new file mode 100644 index 00000000..ef0ab492 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10010004_AddJobTasks.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.10.01.00.04")] + public class AddJobTasks : MultiDatabaseMigration + { + public AddJobTasks(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var jobTaskEntityBuilder = new JobTaskEntityBuilder(migrationBuilder, ActiveDatabase); + jobTaskEntityBuilder.Create(); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Repository/Context/TenantDBContext.cs b/Oqtane.Server/Repository/Context/TenantDBContext.cs index f6325fd9..57fffa14 100644 --- a/Oqtane.Server/Repository/Context/TenantDBContext.cs +++ b/Oqtane.Server/Repository/Context/TenantDBContext.cs @@ -136,5 +136,6 @@ namespace Oqtane.Repository public virtual DbSet MigrationHistory { get; set; } public virtual DbSet SiteGroup { get; set; } public virtual DbSet SiteGroupMember { get; set; } + public virtual DbSet JobTask { get; set; } } } diff --git a/Oqtane.Server/Repository/JobLogRepository.cs b/Oqtane.Server/Repository/JobLogRepository.cs index 523ac356..a3994e11 100644 --- a/Oqtane.Server/Repository/JobLogRepository.cs +++ b/Oqtane.Server/Repository/JobLogRepository.cs @@ -61,8 +61,8 @@ namespace Oqtane.Repository public void DeleteJobLog(int jobLogId) { - JobLog joblog = _db.JobLog.Find(jobLogId); - _db.JobLog.Remove(joblog); + JobLog jobLog = _db.JobLog.Find(jobLogId); + _db.JobLog.Remove(jobLog); _db.SaveChanges(); } } diff --git a/Oqtane.Server/Repository/JobTaskRepository.cs b/Oqtane.Server/Repository/JobTaskRepository.cs new file mode 100644 index 00000000..125afa03 --- /dev/null +++ b/Oqtane.Server/Repository/JobTaskRepository.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface IJobTaskRepository + { + IEnumerable GetJobTasks(int siteId); + JobTask GetJobTask(int jobTaskId); + JobTask AddJobTask(JobTask jobTask); + JobTask UpdateJobTask(JobTask jobTask); + void DeleteJobTask(int jobTaskId); + } + + public class JobTaskRepository : IJobTaskRepository + { + private TenantDBContext _db; + + public JobTaskRepository(TenantDBContext context) + { + _db = context; + } + + public IEnumerable GetJobTasks(int siteId) + { + return _db.JobTask.Where(item => item.SiteId == siteId && !item.IsCompleted).OrderBy(item => item.CreatedOn); + } + + public JobTask GetJobTask(int jobTaskId) + { + return _db.JobTask.SingleOrDefault(item => item.JobTaskId == jobTaskId); + } + + public JobTask AddJobTask(JobTask jobTask) + { + _db.JobTask.Add(jobTask); + _db.SaveChanges(); + return jobTask; + } + public JobTask UpdateJobTask(JobTask jobTask) + { + _db.Entry(jobTask).State = EntityState.Modified; + _db.SaveChanges(); + return jobTask; + } + + public void DeleteJobTask(int jobTaskId) + { + JobTask jobTask = _db.JobTask.Find(jobTaskId); + _db.JobTask.Remove(jobTask); + _db.SaveChanges(); + } + } +} diff --git a/Oqtane.Shared/Models/Job.cs b/Oqtane.Shared/Models/Job.cs index e54e9df4..f505fa0e 100644 --- a/Oqtane.Shared/Models/Job.cs +++ b/Oqtane.Shared/Models/Job.cs @@ -3,7 +3,7 @@ using System; namespace Oqtane.Models { /// - /// Definition of a Job / Task which is run on the server. + /// Definition of a Job which is run on the server /// When Jobs run, they create a /// public class Job : ModelBase diff --git a/Oqtane.Shared/Models/JobTask.cs b/Oqtane.Shared/Models/JobTask.cs new file mode 100644 index 00000000..abca61b0 --- /dev/null +++ b/Oqtane.Shared/Models/JobTask.cs @@ -0,0 +1,56 @@ +using System; + +namespace Oqtane.Models +{ + /// + /// An instance of a Task which is executed by the TaskJob + /// + public class JobTask : ModelBase + { + /// + /// Internal ID + /// + public int JobTaskId { get; set; } + + /// + /// Site where the Task should execute + /// + public int SiteId { get; set; } + + /// + /// Name for simple identification + /// + public string Name { get; set; } + + /// + /// Fully qualified type name of the Task + /// + public string Type { get; set; } + + /// + /// Any parameters related to the Task + /// + public string Parameters { get; set; } + + /// + /// Indicates if the Task is completed + /// + public bool IsCompleted { get; set; } + + /// + /// Any status information provided by the Task + /// + public string Status { get; set; } + + // constructors + public JobTask() { } + + public JobTask(int siteId, string name, string type, string parameters) + { + SiteId = siteId; + Name = name; + Type = type; + Parameters = parameters; + } + } +} From 060eaa7aff5e9c0889b587df1bd417b96520f59f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 19 Feb 2026 10:47:30 -0500 Subject: [PATCH 58/75] change JobTask to SiteTask --- .../OqtaneServiceCollectionExtensions.cs | 2 +- .../Modules/Admin/GlobalReplace/Index.razor | 6 +- Oqtane.Client/Modules/Admin/Users/Users.razor | 6 +- Oqtane.Client/Services/JobTaskService.cs | 46 ----------- Oqtane.Client/Services/SiteTaskService.cs | 46 +++++++++++ ...askController.cs => SiteTaskController.cs} | 32 ++++---- .../OqtaneServiceCollectionExtensions.cs | 4 +- .../Infrastructure/Interfaces/IJobTask.cs | 2 +- Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs | 13 +++ .../Jobs/{TaskJob.cs => SiteTaskJob.cs} | 18 ++-- .../{Tasks => SiteTasks}/GlobalReplaceTask.cs | 2 +- .../{Tasks => SiteTasks}/ImportUsersTask.cs | 2 +- .../SiteTaskBase.cs} | 2 +- ...ityBuilder.cs => SiteTaskEntityBuilder.cs} | 16 ++-- ...ddJobTasks.cs => 10010004_AddSiteTasks.cs} | 8 +- .../Repository/Context/TenantDBContext.cs | 2 +- Oqtane.Server/Repository/JobTaskRepository.cs | 56 ------------- .../Repository/SiteTaskRepository.cs | 82 +++++++++++++++++++ .../Models/{JobTask.cs => SiteTask.cs} | 12 ++- 19 files changed, 197 insertions(+), 160 deletions(-) delete mode 100644 Oqtane.Client/Services/JobTaskService.cs create mode 100644 Oqtane.Client/Services/SiteTaskService.cs rename Oqtane.Server/Controllers/{JobTaskController.cs => SiteTaskController.cs} (58%) rename Oqtane.Server/Infrastructure/Jobs/{TaskJob.cs => SiteTaskJob.cs} (78%) rename Oqtane.Server/Infrastructure/{Tasks => SiteTasks}/GlobalReplaceTask.cs (99%) rename Oqtane.Server/Infrastructure/{Tasks => SiteTasks}/ImportUsersTask.cs (97%) rename Oqtane.Server/Infrastructure/{Tasks/JobTaskBase.cs => SiteTasks/SiteTaskBase.cs} (91%) rename Oqtane.Server/Migrations/EntityBuilders/{JobTaskEntityBuilder.cs => SiteTaskEntityBuilder.cs} (65%) rename Oqtane.Server/Migrations/Tenant/{10010004_AddJobTasks.cs => 10010004_AddSiteTasks.cs} (68%) delete mode 100644 Oqtane.Server/Repository/JobTaskRepository.cs create mode 100644 Oqtane.Server/Repository/SiteTaskRepository.cs rename Oqtane.Shared/Models/{JobTask.cs => SiteTask.cs} (80%) diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index f699d7fe..dca72f70 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -41,7 +41,6 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -60,6 +59,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); // providers diff --git a/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor index 7ac71a70..dff7e341 100644 --- a/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor +++ b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor @@ -1,7 +1,7 @@ @namespace Oqtane.Modules.Admin.GlobalReplace @using System.Text.Json @inherits ModuleBase -@inject IJobTaskService JobTaskService +@inject ISiteTaskService SiteTaskService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -97,8 +97,8 @@ Content = bool.Parse(_content) }; - var jobTask = new JobTask(PageState.Site.SiteId, "Global Replace", "Oqtane.Infrastructure.GlobalReplaceTask, Oqtane.Server", JsonSerializer.Serialize(replace)); - await JobTaskService.AddJobTaskAsync(jobTask); + var siteTask = new SiteTask(PageState.Site.SiteId, "Global Replace", "Oqtane.Infrastructure.GlobalReplaceTask, Oqtane.Server", JsonSerializer.Serialize(replace)); + await SiteTaskService.AddSiteTaskAsync(siteTask); AddModuleMessage(Localizer["Success.Save"], MessageType.Success); } diff --git a/Oqtane.Client/Modules/Admin/Users/Users.razor b/Oqtane.Client/Modules/Admin/Users/Users.razor index 0a357a57..41e0912f 100644 --- a/Oqtane.Client/Modules/Admin/Users/Users.razor +++ b/Oqtane.Client/Modules/Admin/Users/Users.razor @@ -1,7 +1,7 @@ @namespace Oqtane.Modules.Admin.Users @inherits ModuleBase @inject NavigationManager NavigationManager -@inject IJobTaskService JobTaskService +@inject ISiteTaskService SiteTaskService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -43,8 +43,8 @@ var fileid = _filemanager.GetFileId(); if (fileid != -1) { - var jobTask = new JobTask(PageState.Site.SiteId, "Import Users", "Oqtane.Infrastructure.ImportUsersTask, Oqtane.Server", $"{fileid}:{_notify}"); - await JobTaskService.AddJobTaskAsync(jobTask); + var siteTask = new SiteTask(PageState.Site.SiteId, "Import Users", "Oqtane.Infrastructure.ImportUsersTask, Oqtane.Server", $"{fileid}:{_notify}"); + await SiteTaskService.AddSiteTaskAsync(siteTask); AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success); } else diff --git a/Oqtane.Client/Services/JobTaskService.cs b/Oqtane.Client/Services/JobTaskService.cs deleted file mode 100644 index 4e596a52..00000000 --- a/Oqtane.Client/Services/JobTaskService.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Oqtane.Models; -using System.Threading.Tasks; -using System.Net.Http; -using Oqtane.Documentation; -using Oqtane.Shared; - -namespace Oqtane.Services -{ - /// - /// Service to manage tasks () - /// - public interface IJobTaskService - { - /// - /// Return a specific task - /// - /// - /// - Task GetJobTaskAsync(int jobTaskId); - - /// - /// Adds a new task - /// - /// - /// - Task AddJobTaskAsync(JobTask jobTask); - } - - [PrivateApi("Don't show in the documentation, as everything should use the Interface")] - public class JobTaskService : ServiceBase, IJobTaskService - { - public JobTaskService(HttpClient http, SiteState siteState) : base(http, siteState) { } - - private string Apiurl => CreateApiUrl("JobTask"); - - public async Task GetJobTaskAsync(int jobTaskId) - { - return await GetJsonAsync($"{Apiurl}/{jobTaskId}"); - } - - public async Task AddJobTaskAsync(JobTask jobTask) - { - return await PostJsonAsync(Apiurl, jobTask); - } - } -} diff --git a/Oqtane.Client/Services/SiteTaskService.cs b/Oqtane.Client/Services/SiteTaskService.cs new file mode 100644 index 00000000..b0c68859 --- /dev/null +++ b/Oqtane.Client/Services/SiteTaskService.cs @@ -0,0 +1,46 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using Oqtane.Documentation; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + /// + /// Service to manage tasks () + /// + public interface ISiteTaskService + { + /// + /// Return a specific task + /// + /// + /// + Task GetSiteTaskAsync(int siteTaskId); + + /// + /// Adds a new task + /// + /// + /// + Task AddSiteTaskAsync(SiteTask siteTask); + } + + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class SiteTaskService : ServiceBase, ISiteTaskService + { + public SiteTaskService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("SiteTask"); + + public async Task GetSiteTaskAsync(int siteTaskId) + { + return await GetJsonAsync($"{Apiurl}/{siteTaskId}"); + } + + public async Task AddSiteTaskAsync(SiteTask siteTask) + { + return await PostJsonAsync(Apiurl, siteTask); + } + } +} diff --git a/Oqtane.Server/Controllers/JobTaskController.cs b/Oqtane.Server/Controllers/SiteTaskController.cs similarity index 58% rename from Oqtane.Server/Controllers/JobTaskController.cs rename to Oqtane.Server/Controllers/SiteTaskController.cs index 5e02f229..61842f84 100644 --- a/Oqtane.Server/Controllers/JobTaskController.cs +++ b/Oqtane.Server/Controllers/SiteTaskController.cs @@ -10,15 +10,15 @@ using Oqtane.Shared; namespace Oqtane.Controllers { [Route(ControllerRoutes.ApiRoute)] - public class JobTaskController : Controller + public class SiteTaskController : Controller { - private readonly IJobTaskRepository _jobTasks; + private readonly ISiteTaskRepository _siteTasks; private readonly ILogManager _logger; private readonly Alias _alias; - public JobTaskController(IJobTaskRepository jobTasks, ILogManager logger, ITenantManager tenantManager) + public SiteTaskController(ISiteTaskRepository siteTasks, ILogManager logger, ITenantManager tenantManager) { - _jobTasks = jobTasks; + _siteTasks = siteTasks; _logger = logger; _alias = tenantManager.GetAlias(); } @@ -26,12 +26,12 @@ namespace Oqtane.Controllers // GET api//5 [HttpGet("{id}")] [Authorize(Roles = RoleNames.Admin)] - public JobTask Get(int id) + public SiteTask Get(int id) { - var jobTask = _jobTasks.GetJobTask(id); - if (jobTask.SiteId == _alias.SiteId) + var siteTask = _siteTasks.GetSiteTask(id); + if (siteTask.SiteId == _alias.SiteId) { - return jobTask; + return siteTask; } else { @@ -43,21 +43,21 @@ namespace Oqtane.Controllers // POST api/ [HttpPost] [Authorize(Roles = RoleNames.Admin)] - public JobTask Post([FromBody] JobTask jobTask) + public SiteTask Post([FromBody] SiteTask siteTask) { - if (ModelState.IsValid && jobTask.SiteId == _alias.SiteId) + if (ModelState.IsValid && siteTask.SiteId == _alias.SiteId) { - jobTask.IsCompleted = false; - jobTask = _jobTasks.AddJobTask(jobTask); - _logger.Log(LogLevel.Information, this, LogFunction.Create, "Job Task Added {JobTask}", jobTask); + siteTask.IsCompleted = false; + siteTask = _siteTasks.AddSiteTask(siteTask); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Task Added {SiteTask}", siteTask); } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Job Task Post Attempt {JobTask}", jobTask); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Task Post Attempt {SiteTask}", siteTask); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - jobTask = null; + siteTask = null; } - return jobTask; + return siteTask; } } } diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index a26768ee..fceb8f8f 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -214,7 +214,6 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -236,6 +235,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); @@ -275,7 +275,6 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -288,6 +287,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // managers services.AddTransient(); diff --git a/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs b/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs index c2941553..ab26d820 100644 --- a/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs +++ b/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs @@ -4,7 +4,7 @@ using Oqtane.Models; namespace Oqtane.Infrastructure { - public interface IJobTask + public interface ISiteTask { string ExecuteTask(IServiceProvider provider, Site site, string parameters); diff --git a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs index 06c88ab5..0b9d3288 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -33,6 +33,7 @@ namespace Oqtane.Infrastructure var visitorRepository = provider.GetRequiredService(); var notificationRepository = provider.GetRequiredService(); var urlMappingRepository = provider.GetRequiredService(); + var siteTaskRepository = provider.GetRequiredService(); // iterate through sites for current tenant List sites = siteRepository.GetSites().ToList(); @@ -94,6 +95,18 @@ namespace Oqtane.Infrastructure { log += $"Error Purging Broken Urls - {ex.Message}
"; } + + // purge completed site tasks + retention = 30; // 30 day default + try + { + count = siteTaskRepository.DeleteSiteTasks(site.SiteId, retention); + log += count.ToString() + " Completed Tasks Purged
"; + } + catch (Exception ex) + { + log += $"Error Purging Completed Site Tasks - {ex.Message}
"; + } } return log; diff --git a/Oqtane.Server/Infrastructure/Jobs/TaskJob.cs b/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs similarity index 78% rename from Oqtane.Server/Infrastructure/Jobs/TaskJob.cs rename to Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs index bcbf7125..93cfc96a 100644 --- a/Oqtane.Server/Infrastructure/Jobs/TaskJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs @@ -7,11 +7,11 @@ using Oqtane.Repository; namespace Oqtane.Infrastructure { - public class TaskJob : HostedServiceBase + public class SiteTaskJob : HostedServiceBase { - public TaskJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + public SiteTaskJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) { - Name = "Task Job"; + Name = "Site Task Job"; Frequency = "m"; // run every minute Interval = 1; IsEnabled = true; @@ -33,8 +33,8 @@ namespace Oqtane.Infrastructure log += $"Processing Site: {site.Name}
"; // get incomplete tasks for site - var jobTaskRepository = provider.GetRequiredService(); - var tasks = jobTaskRepository.GetJobTasks(site.SiteId).ToList(); + var siteTaskRepository = provider.GetRequiredService(); + var tasks = siteTaskRepository.GetSiteTasks(site.SiteId).ToList(); if (tasks != null && tasks.Any()) { foreach (var task in tasks) @@ -42,15 +42,15 @@ namespace Oqtane.Infrastructure log += $"Executing Task: {task.Name}
"; Type taskType = Type.GetType(task.Type); - if (taskType != null && taskType.GetInterface(nameof(IJobTask)) != null) + if (taskType != null && taskType.GetInterface(nameof(ISiteTask)) != null) { try { tenantManager.SetAlias(tenant.TenantId, site.SiteId); var taskObject = ActivatorUtilities.CreateInstance(provider, taskType); - var taskLog = ((IJobTask)taskObject).ExecuteTask(provider, site, task.Parameters); - taskLog += await ((IJobTask)taskObject).ExecuteTaskAsync(provider, site, task.Parameters); + var taskLog = ((ISiteTask)taskObject).ExecuteTask(provider, site, task.Parameters); + taskLog += await ((ISiteTask)taskObject).ExecuteTaskAsync(provider, site, task.Parameters); task.Status = taskLog; } @@ -66,7 +66,7 @@ namespace Oqtane.Infrastructure // update task task.IsCompleted = true; - jobTaskRepository.UpdateJobTask(task); + siteTaskRepository.UpdateSiteTask(task); log += task.Status + "
"; } diff --git a/Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs similarity index 99% rename from Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs rename to Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs index 6f8ae05b..008fd9a8 100644 --- a/Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs +++ b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs @@ -8,7 +8,7 @@ using Oqtane.Repository; namespace Oqtane.Infrastructure { - public class GlobalReplaceTask : JobTaskBase + public class GlobalReplaceTask : SiteTaskBase { public override string ExecuteTask(IServiceProvider provider, Site site, string parameters) { diff --git a/Oqtane.Server/Infrastructure/Tasks/ImportUsersTask.cs b/Oqtane.Server/Infrastructure/SiteTasks/ImportUsersTask.cs similarity index 97% rename from Oqtane.Server/Infrastructure/Tasks/ImportUsersTask.cs rename to Oqtane.Server/Infrastructure/SiteTasks/ImportUsersTask.cs index 28dd843d..8da990c9 100644 --- a/Oqtane.Server/Infrastructure/Tasks/ImportUsersTask.cs +++ b/Oqtane.Server/Infrastructure/SiteTasks/ImportUsersTask.cs @@ -7,7 +7,7 @@ using Oqtane.Repository; namespace Oqtane.Infrastructure { - public class ImportUsersTask : JobTaskBase + public class ImportUsersTask : SiteTaskBase { public override async Task ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters) { diff --git a/Oqtane.Server/Infrastructure/Tasks/JobTaskBase.cs b/Oqtane.Server/Infrastructure/SiteTasks/SiteTaskBase.cs similarity index 91% rename from Oqtane.Server/Infrastructure/Tasks/JobTaskBase.cs rename to Oqtane.Server/Infrastructure/SiteTasks/SiteTaskBase.cs index af359c7a..81cdc80d 100644 --- a/Oqtane.Server/Infrastructure/Tasks/JobTaskBase.cs +++ b/Oqtane.Server/Infrastructure/SiteTasks/SiteTaskBase.cs @@ -4,7 +4,7 @@ using Oqtane.Models; namespace Oqtane.Infrastructure { - public class JobTaskBase : IJobTask + public class SiteTaskBase : ISiteTask { public virtual string ExecuteTask(IServiceProvider provider, Site site, string parameters) { diff --git a/Oqtane.Server/Migrations/EntityBuilders/JobTaskEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs similarity index 65% rename from Oqtane.Server/Migrations/EntityBuilders/JobTaskEntityBuilder.cs rename to Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs index 393e6df9..5872d67f 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/JobTaskEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs @@ -9,22 +9,22 @@ using Oqtane.Interfaces; namespace Oqtane.Migrations.EntityBuilders { - public class JobTaskEntityBuilder : AuditableBaseEntityBuilder + public class SiteTaskEntityBuilder : AuditableBaseEntityBuilder { - private const string _entityTableName = "JobTask"; - private readonly PrimaryKey _primaryKey = new("PK_JobTask", x => x.JobTaskId); - private readonly ForeignKey _siteForeignKey = new("FK_JobTask_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); + private const string _entityTableName = "SiteTask"; + private readonly PrimaryKey _primaryKey = new("PK_SiteTask", x => x.SiteTaskId); + private readonly ForeignKey _siteForeignKey = new("FK_SiteTask_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); - public JobTaskEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + public SiteTaskEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) { EntityTableName = _entityTableName; PrimaryKey = _primaryKey; ForeignKeys.Add(_siteForeignKey); } - protected override JobTaskEntityBuilder BuildTable(ColumnsBuilder table) + protected override SiteTaskEntityBuilder BuildTable(ColumnsBuilder table) { - JobTaskId = AddAutoIncrementColumn(table,"JobTaskId"); + SiteTaskId = AddAutoIncrementColumn(table,"SiteTaskId"); SiteId = AddIntegerColumn(table,"SiteId"); Name = AddStringColumn(table, "Name", 200); Type = AddStringColumn(table, "Type", 200); @@ -37,7 +37,7 @@ namespace Oqtane.Migrations.EntityBuilders return this; } - public OperationBuilder JobTaskId { get; private set; } + public OperationBuilder SiteTaskId { get; private set; } public OperationBuilder SiteId { get; private set; } diff --git a/Oqtane.Server/Migrations/Tenant/10010004_AddJobTasks.cs b/Oqtane.Server/Migrations/Tenant/10010004_AddSiteTasks.cs similarity index 68% rename from Oqtane.Server/Migrations/Tenant/10010004_AddJobTasks.cs rename to Oqtane.Server/Migrations/Tenant/10010004_AddSiteTasks.cs index ef0ab492..ece41554 100644 --- a/Oqtane.Server/Migrations/Tenant/10010004_AddJobTasks.cs +++ b/Oqtane.Server/Migrations/Tenant/10010004_AddSiteTasks.cs @@ -8,16 +8,16 @@ namespace Oqtane.Migrations.Tenant { [DbContext(typeof(TenantDBContext))] [Migration("Tenant.10.01.00.04")] - public class AddJobTasks : MultiDatabaseMigration + public class AddSiteTasks : MultiDatabaseMigration { - public AddJobTasks(IDatabase database) : base(database) + public AddSiteTasks(IDatabase database) : base(database) { } protected override void Up(MigrationBuilder migrationBuilder) { - var jobTaskEntityBuilder = new JobTaskEntityBuilder(migrationBuilder, ActiveDatabase); - jobTaskEntityBuilder.Create(); + var siteTaskEntityBuilder = new SiteTaskEntityBuilder(migrationBuilder, ActiveDatabase); + siteTaskEntityBuilder.Create(); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Oqtane.Server/Repository/Context/TenantDBContext.cs b/Oqtane.Server/Repository/Context/TenantDBContext.cs index 57fffa14..8a5a7751 100644 --- a/Oqtane.Server/Repository/Context/TenantDBContext.cs +++ b/Oqtane.Server/Repository/Context/TenantDBContext.cs @@ -136,6 +136,6 @@ namespace Oqtane.Repository public virtual DbSet MigrationHistory { get; set; } public virtual DbSet SiteGroup { get; set; } public virtual DbSet SiteGroupMember { get; set; } - public virtual DbSet JobTask { get; set; } + public virtual DbSet SiteTask { get; set; } } } diff --git a/Oqtane.Server/Repository/JobTaskRepository.cs b/Oqtane.Server/Repository/JobTaskRepository.cs deleted file mode 100644 index 125afa03..00000000 --- a/Oqtane.Server/Repository/JobTaskRepository.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using Oqtane.Models; - -namespace Oqtane.Repository -{ - public interface IJobTaskRepository - { - IEnumerable GetJobTasks(int siteId); - JobTask GetJobTask(int jobTaskId); - JobTask AddJobTask(JobTask jobTask); - JobTask UpdateJobTask(JobTask jobTask); - void DeleteJobTask(int jobTaskId); - } - - public class JobTaskRepository : IJobTaskRepository - { - private TenantDBContext _db; - - public JobTaskRepository(TenantDBContext context) - { - _db = context; - } - - public IEnumerable GetJobTasks(int siteId) - { - return _db.JobTask.Where(item => item.SiteId == siteId && !item.IsCompleted).OrderBy(item => item.CreatedOn); - } - - public JobTask GetJobTask(int jobTaskId) - { - return _db.JobTask.SingleOrDefault(item => item.JobTaskId == jobTaskId); - } - - public JobTask AddJobTask(JobTask jobTask) - { - _db.JobTask.Add(jobTask); - _db.SaveChanges(); - return jobTask; - } - public JobTask UpdateJobTask(JobTask jobTask) - { - _db.Entry(jobTask).State = EntityState.Modified; - _db.SaveChanges(); - return jobTask; - } - - public void DeleteJobTask(int jobTaskId) - { - JobTask jobTask = _db.JobTask.Find(jobTaskId); - _db.JobTask.Remove(jobTask); - _db.SaveChanges(); - } - } -} diff --git a/Oqtane.Server/Repository/SiteTaskRepository.cs b/Oqtane.Server/Repository/SiteTaskRepository.cs new file mode 100644 index 00000000..4d927c32 --- /dev/null +++ b/Oqtane.Server/Repository/SiteTaskRepository.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface ISiteTaskRepository + { + IEnumerable GetSiteTasks(int siteId); + SiteTask GetSiteTask(int siteTaskId); + SiteTask AddSiteTask(SiteTask siteTask); + SiteTask UpdateSiteTask(SiteTask siteTask); + void DeleteSiteTask(int siteTaskId); + int DeleteSiteTasks(int siteId, int age); + } + + public class SiteTaskRepository : ISiteTaskRepository + { + private readonly IDbContextFactory _dbContextFactory; + + public SiteTaskRepository(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public IEnumerable GetSiteTasks(int siteId) + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteTask.Where(item => item.SiteId == siteId && !item.IsCompleted).OrderBy(item => item.CreatedOn); + } + + public SiteTask GetSiteTask(int siteTaskId) + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteTask.SingleOrDefault(item => item.SiteTaskId == siteTaskId); + } + + public SiteTask AddSiteTask(SiteTask siteTask) + { + using var db = _dbContextFactory.CreateDbContext(); + db.SiteTask.Add(siteTask); + db.SaveChanges(); + return siteTask; + } + public SiteTask UpdateSiteTask(SiteTask siteTask) + { + using var db = _dbContextFactory.CreateDbContext(); + db.Entry(siteTask).State = EntityState.Modified; + db.SaveChanges(); + return siteTask; + } + + public void DeleteSiteTask(int siteTaskId) + { + using var db = _dbContextFactory.CreateDbContext(); + SiteTask siteTask = db.SiteTask.Find(siteTaskId); + db.SiteTask.Remove(siteTask); + db.SaveChanges(); + } + + public int DeleteSiteTasks(int siteId, int age) + { + using var db = _dbContextFactory.CreateDbContext(); + // delete completed tasks in batches of 100 records + var count = 0; + var purgedate = DateTime.UtcNow.AddDays(-age); + var tasks = db.SiteTask.Where(item => item.SiteId == siteId && item.IsCompleted && item.CreatedOn < purgedate) + .OrderBy(item => item.CreatedOn).Take(100).ToList(); + while (tasks.Count > 0) + { + count += tasks.Count; + db.SiteTask.RemoveRange(tasks); + db.SaveChanges(); + tasks = db.SiteTask.Where(item => item.SiteId == siteId && item.IsCompleted && item.CreatedOn < purgedate) + .OrderBy(item => item.CreatedOn).Take(100).ToList(); + } + return count; + } + } +} diff --git a/Oqtane.Shared/Models/JobTask.cs b/Oqtane.Shared/Models/SiteTask.cs similarity index 80% rename from Oqtane.Shared/Models/JobTask.cs rename to Oqtane.Shared/Models/SiteTask.cs index abca61b0..63b6d973 100644 --- a/Oqtane.Shared/Models/JobTask.cs +++ b/Oqtane.Shared/Models/SiteTask.cs @@ -1,16 +1,14 @@ -using System; - namespace Oqtane.Models { /// - /// An instance of a Task which is executed by the TaskJob + /// An instance of a SiteTask which is executed by the SiteTaskJob /// - public class JobTask : ModelBase + public class SiteTask : ModelBase { /// /// Internal ID /// - public int JobTaskId { get; set; } + public int SiteTaskId { get; set; } /// /// Site where the Task should execute @@ -43,9 +41,9 @@ namespace Oqtane.Models public string Status { get; set; } // constructors - public JobTask() { } + public SiteTask() { } - public JobTask(int siteId, string name, string type, string parameters) + public SiteTask(int siteId, string name, string type, string parameters) { SiteId = siteId; Name = name; From 5c2bd8093acff5bec0405f4069900d764e0d3524 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 19 Feb 2026 12:42:04 -0500 Subject: [PATCH 59/75] remove unecessary using statements --- Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs | 1 - Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs b/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs index 93cfc96a..5e5c7207 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.Eventing.Reader; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs index 5872d67f..b0b9df1c 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs @@ -2,7 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; using Oqtane.Databases.Interfaces; -using Oqtane.Interfaces; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedAutoPropertyAccessor.Global From a6006ce1fef6f2649a76290e72498a77315d9905 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 19 Feb 2026 13:27:19 -0500 Subject: [PATCH 60/75] resolve DbContext issue --- Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs | 6 ++++-- Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs | 4 ++-- Oqtane.Server/Repository/SiteTaskRepository.cs | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs b/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs index 5e5c7207..e82ba007 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs @@ -21,18 +21,20 @@ namespace Oqtane.Infrastructure { var log = ""; + // resolve services var tenantManager = provider.GetRequiredService(); + var siteRepository = provider.GetRequiredService(); + var siteTaskRepository = provider.GetRequiredService(); + var tenant = tenantManager.GetTenant(); // iterate through sites for current tenant - var siteRepository = provider.GetRequiredService(); var sites = siteRepository.GetSites().ToList(); foreach (var site in sites.Where(item => !item.IsDeleted)) { log += $"Processing Site: {site.Name}
"; // get incomplete tasks for site - var siteTaskRepository = provider.GetRequiredService(); var tasks = siteTaskRepository.GetSiteTasks(site.SiteId).ToList(); if (tasks != null && tasks.Any()) { diff --git a/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs index 008fd9a8..14b11ae4 100644 --- a/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs +++ b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs @@ -110,7 +110,7 @@ namespace Oqtane.Infrastructure if (changed && globalReplace.Modules) { pageModuleRepository.UpdatePageModule(pageModule); - log += $"Module Updated: {pageModule.Title} - /{page.Path}
"; + log += $"Module Updated: {pageModule.Title} Page: /{page.Path}
"; } // module content @@ -127,7 +127,7 @@ namespace Oqtane.Infrastructure { moduleContent = moduleContent.Replace(find, replace, comparisonType); ((IPortable)moduleObject).ImportModule(pageModule.Module, moduleContent, pageModule.Module.ModuleDefinition.Version); - log += $"Module Content Updated: {pageModule.Title} - /{page.Path}
"; + log += $"Module Content Updated: {pageModule.Title} Page: /{page.Path}
"; } } catch (Exception ex) diff --git a/Oqtane.Server/Repository/SiteTaskRepository.cs b/Oqtane.Server/Repository/SiteTaskRepository.cs index 4d927c32..87ea15dc 100644 --- a/Oqtane.Server/Repository/SiteTaskRepository.cs +++ b/Oqtane.Server/Repository/SiteTaskRepository.cs @@ -28,7 +28,8 @@ namespace Oqtane.Repository public IEnumerable GetSiteTasks(int siteId) { using var db = _dbContextFactory.CreateDbContext(); - return db.SiteTask.Where(item => item.SiteId == siteId && !item.IsCompleted).OrderBy(item => item.CreatedOn); + return db.SiteTask.Where(item => item.SiteId == siteId && !item.IsCompleted) + .OrderBy(item => item.CreatedOn).ToList(); } public SiteTask GetSiteTask(int siteTaskId) From 2736fa451cfd2c2984546fa08b400e75fbbff2b7 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 19 Feb 2026 13:42:32 -0500 Subject: [PATCH 61/75] support html encoded content --- Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs index 14b11ae4..364032a9 100644 --- a/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs +++ b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Oqtane.Models; @@ -123,9 +124,9 @@ namespace Oqtane.Infrastructure { var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); var moduleContent = ((IPortable)moduleObject).ExportModule(pageModule.Module); - if (!string.IsNullOrEmpty(moduleContent) && moduleContent.Contains(find, comparisonType) && globalReplace.Content) + if (!string.IsNullOrEmpty(moduleContent) && moduleContent.Contains(WebUtility.HtmlEncode(find), comparisonType) && globalReplace.Content) { - moduleContent = moduleContent.Replace(find, replace, comparisonType); + moduleContent = moduleContent.Replace(WebUtility.HtmlEncode(find), WebUtility.HtmlEncode(replace), comparisonType); ((IPortable)moduleObject).ImportModule(pageModule.Module, moduleContent, pageModule.Module.ModuleDefinition.Version); log += $"Module Content Updated: {pageModule.Title} Page: /{page.Path}
"; } From 8752b247233f2530a9ff7ce8ddda4910ea33d65b Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 19 Feb 2026 14:25:42 -0500 Subject: [PATCH 62/75] fix renaming issue --- .../Infrastructure/Interfaces/{IJobTask.cs => ISiteTask.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Oqtane.Server/Infrastructure/Interfaces/{IJobTask.cs => ISiteTask.cs} (100%) diff --git a/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs b/Oqtane.Server/Infrastructure/Interfaces/ISiteTask.cs similarity index 100% rename from Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs rename to Oqtane.Server/Infrastructure/Interfaces/ISiteTask.cs From ae0c4c1099885a9e97d61c82ad8c6e086be6584a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 20 Feb 2026 08:42:41 -0500 Subject: [PATCH 63/75] handle caching in Global Replace --- .../Infrastructure/SiteTasks/GlobalReplaceTask.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs index 364032a9..c9b1efa6 100644 --- a/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs +++ b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Oqtane.Models; using Oqtane.Modules; using Oqtane.Repository; +using Oqtane.Shared; namespace Oqtane.Infrastructure { @@ -19,6 +20,8 @@ namespace Oqtane.Infrastructure var siteRepository = provider.GetRequiredService(); var pageRepository = provider.GetRequiredService(); var pageModuleRepository = provider.GetRequiredService(); + var TenantManager = provider.GetRequiredService(); + var syncManager = provider.GetRequiredService(); if (!string.IsNullOrEmpty(parameters)) { @@ -28,6 +31,7 @@ namespace Oqtane.Infrastructure var find = globalReplace.Find; var replace = globalReplace.Replace; var comparisonType = (globalReplace.CaseSensitive) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + var refresh = false; log += $"Replacing: '{find}' With: '{replace}' Case Sensitive: {globalReplace.CaseSensitive}
"; @@ -53,6 +57,7 @@ namespace Oqtane.Infrastructure { siteRepository.UpdateSite(site); log += $"Site Updated
"; + refresh = true; } var pages = pageRepository.GetPages(site.SiteId).ToList(); @@ -87,6 +92,7 @@ namespace Oqtane.Infrastructure { pageRepository.UpdatePage(page); log += $"Page Updated: /{page.Path}
"; + refresh = true; } foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId)) @@ -112,6 +118,7 @@ namespace Oqtane.Infrastructure { pageModuleRepository.UpdatePageModule(pageModule); log += $"Module Updated: {pageModule.Title} Page: /{page.Path}
"; + refresh = true; } // module content @@ -139,6 +146,12 @@ namespace Oqtane.Infrastructure } } } + + if (refresh) + { + // clear cache + syncManager.AddSyncEvent(TenantManager.GetAlias(), EntityNames.Site, site.SiteId, SyncEventActions.Refresh); + } } else { From 12f06a766231060f813635a4b803ac2b8cc8c30d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 20 Feb 2026 14:53:41 -0500 Subject: [PATCH 64/75] fix culture cookie so that it supports culture and ui culture --- Oqtane.Server/Components/App.razor | 79 +++++++++++++++++++----------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 093782c6..a55b28a1 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -188,45 +188,20 @@ await GetJwtToken(alias); } - // includes resources + // include resources var resources = await GetPageResources(alias, site, page, modules, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action); ManageStyleSheets(resources); ManageScripts(resources, alias); AddBodyContent(site.BodyContent); AddBodyContent(page.BodyContent); + ManageLocalization(site); - // generate scripts + // PWA script if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null) { _scripts += CreatePWAScript(alias, site, route); } - // set culture if not specified - string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName]; - if (cultureCookie == null) - { - // get default language for site - if (site.Languages.Any()) - { - // use default language if specified otherwise use first language in collection - cultureCookie = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code; - } - else - { - // fallback language - cultureCookie = LocalizationManager.GetDefaultCulture(); - } - // convert language code to culture cookie format (ie. "c=en|uic=en") - cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(cultureCookie)); - SetLocalizationCookie(cultureCookie); - } - - // set language for page - if (!string.IsNullOrEmpty(cultureCookie)) - { - _language = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie).Culture.Name; - } - // create initial PageState _pageState = new PageState { @@ -829,4 +804,52 @@ _bodyContent += "\n"; } } + + private void ManageLocalization(Site site) + { + // get user culture (ui localization) + var uiCulture = site?.User?.CultureCode; + if (string.IsNullOrEmpty(uiCulture)) + { + // get default language for site + if (site.Languages.Any()) + { + // use default language if specified otherwise use first language in collection + uiCulture = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code; + } + else + { + // fallback language + uiCulture = LocalizationManager.GetDefaultCulture(); + } + } + + // get site culture (content localization) + var culture = site?.CultureCode; + if (string.IsNullOrEmpty(culture)) + { + culture = uiCulture; + } + _language = culture; // html element language attribute + + // get culture cookie + string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName]; + if (cultureCookie != null) + { + // verify culture cookie (which could be inaccurate when using subfolders ie. domain.com/fr as cookies are based on domain) + var requestCulture = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie); + if (culture != requestCulture.Culture.Name || uiCulture != requestCulture.UICulture.Name) + { + // convert to culture cookie format (ie. "c=en|uic=en") and save cookie + cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(culture, uiCulture)); + SetLocalizationCookie(cultureCookie); + } + } + else + { + // convert to culture cookie format (ie. "c=en|uic=en") and save cookie + cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(culture, uiCulture)); + SetLocalizationCookie(cultureCookie); + } + } } From 0d4d51448eda5f42475a6cfca6fc12708794553d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 20 Feb 2026 15:11:54 -0500 Subject: [PATCH 65/75] allow LanguageSwitcher to support culture and ui culture --- Oqtane.Client/Services/LocalizationCookieService.cs | 5 +++-- Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor | 4 ++-- Oqtane.Server/Infrastructure/LocalizationManager.cs | 1 - Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs | 2 ++ Oqtane.Server/Services/LocalizationCookieService.cs | 4 ++-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Oqtane.Client/Services/LocalizationCookieService.cs b/Oqtane.Client/Services/LocalizationCookieService.cs index 6ebbdc76..2d6b258f 100644 --- a/Oqtane.Client/Services/LocalizationCookieService.cs +++ b/Oqtane.Client/Services/LocalizationCookieService.cs @@ -14,8 +14,9 @@ namespace Oqtane.Services /// Set the localization cookie ///
/// + /// /// - Task SetLocalizationCookieAsync(string culture); + Task SetLocalizationCookieAsync(string culture, string uiCulture); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -23,7 +24,7 @@ namespace Oqtane.Services { public LocalizationCookieService(HttpClient http, SiteState siteState) : base(http, siteState) { } - public Task SetLocalizationCookieAsync(string culture) + public Task SetLocalizationCookieAsync(string culture, string uiCulture) { return Task.CompletedTask; // only used in server side rendering } diff --git a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor index d049e436..98420061 100644 --- a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor +++ b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor @@ -56,7 +56,7 @@ var culture = PageState.QueryString["culture"]; if (PageState.Site.Languages.Any(item => item.Code == culture)) { - await LocalizationCookieService.SetLocalizationCookieAsync(culture); + await LocalizationCookieService.SetLocalizationCookieAsync(PageState.Site.CultureCode, culture); } NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", "")); } @@ -66,7 +66,7 @@ { if (culture != CultureInfo.CurrentUICulture.Name) { - var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); + var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(PageState.Site.CultureCode, culture)); var interop = new Interop(JSRuntime); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax"); NavigationManager.NavigateTo(NavigationManager.Uri, true); diff --git a/Oqtane.Server/Infrastructure/LocalizationManager.cs b/Oqtane.Server/Infrastructure/LocalizationManager.cs index 9ffd2708..58c48bd5 100644 --- a/Oqtane.Server/Infrastructure/LocalizationManager.cs +++ b/Oqtane.Server/Infrastructure/LocalizationManager.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Reflection; using Microsoft.Extensions.Options; -using Oqtane.Models; using Oqtane.Shared; namespace Oqtane.Infrastructure diff --git a/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs b/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs index 0b411227..81bb9cbf 100644 --- a/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs +++ b/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs @@ -18,9 +18,11 @@ namespace Oqtane.Migrations.Tenant { var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); siteEntityBuilder.AddStringColumn("CultureCode", 10, true); + siteEntityBuilder.UpdateData("CultureCode", $"'{Shared.Constants.DefaultCulture}'"); var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase); userEntityBuilder.AddStringColumn("CultureCode", 10, true); + userEntityBuilder.UpdateData("CultureCode", $"'{Shared.Constants.DefaultCulture}'"); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Oqtane.Server/Services/LocalizationCookieService.cs b/Oqtane.Server/Services/LocalizationCookieService.cs index 1bedfc6f..13a3205b 100644 --- a/Oqtane.Server/Services/LocalizationCookieService.cs +++ b/Oqtane.Server/Services/LocalizationCookieService.cs @@ -16,9 +16,9 @@ namespace Oqtane.Services _accessor = accessor; } - public Task SetLocalizationCookieAsync(string culture) + public Task SetLocalizationCookieAsync(string culture, string uiCulture) { - var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); + var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture, uiCulture)); _accessor.HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, new CookieOptions { From 36789495dff920cde43308d82253d289348944de Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 23 Feb 2026 08:11:29 -0500 Subject: [PATCH 66/75] refactor synchronization job --- .../Infrastructure/Jobs/SynchronizationJob.cs | 724 ++++++++++-------- 1 file changed, 397 insertions(+), 327 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index b59611e7..43047ff7 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -18,6 +18,7 @@ namespace Oqtane.Infrastructure // synchronization only supports sites in the same tenant (database) // module title is used as a key to identify module instances on a page // modules must implement ISynchronizable interface + // change detection does not support deleted items as key values will usually be different due to localization // define settings that should not be synchronized (should be extensible in the future) List excludedSettings = new List() { @@ -98,7 +99,7 @@ namespace Oqtane.Infrastructure siteLog = (siteGroupMember.SynchronizedOn != DateTime.MinValue) ? "No Changes Identified
" : "Initialization Complete
"; } - // set synchronized on date/time + // set synchronized date/time siteGroupMember.SynchronizedOn = DateTime.UtcNow; siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember); @@ -128,7 +129,7 @@ namespace Oqtane.Infrastructure { var log = ""; - // synchronize roles/users + // synchronize roles log += SynchronizeRoles(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); // synchronize folders/files @@ -140,60 +141,64 @@ namespace Oqtane.Infrastructure // synchronize site if (primarySite.ModifiedOn > siteGroupMember.SynchronizedOn) { - secondarySite.TimeZoneId = primarySite.TimeZoneId; - secondarySite.CultureCode = primarySite.CultureCode; - if (secondarySite.LogoFileId != primarySite.LogoFileId) - { - secondarySite.LogoFileId = ResolveFileId(provider, primarySite.LogoFileId, secondarySite.SiteId); - } - if (secondarySite.FaviconFileId != primarySite.FaviconFileId) - { - secondarySite.FaviconFileId = ResolveFileId(provider, primarySite.FaviconFileId, secondarySite.SiteId); ; - } - secondarySite.DefaultThemeType = primarySite.DefaultThemeType; - secondarySite.DefaultContainerType = primarySite.DefaultContainerType; - secondarySite.AdminContainerType = primarySite.AdminContainerType; - secondarySite.PwaIsEnabled = primarySite.PwaIsEnabled; - if (secondarySite.PwaAppIconFileId != primarySite.PwaAppIconFileId) - { - secondarySite.PwaAppIconFileId = ResolveFileId(provider, primarySite.PwaAppIconFileId, secondarySite.SiteId); ; - } - if (secondarySite.PwaSplashIconFileId != primarySite.PwaSplashIconFileId) - { - secondarySite.PwaSplashIconFileId = ResolveFileId(provider, primarySite.PwaSplashIconFileId, secondarySite.SiteId); ; - } - secondarySite.AllowRegistration = primarySite.AllowRegistration; - secondarySite.VisitorTracking = primarySite.VisitorTracking; - secondarySite.CaptureBrokenUrls = primarySite.CaptureBrokenUrls; - secondarySite.SiteGuid = primarySite.SiteGuid; - secondarySite.RenderMode = primarySite.RenderMode; - secondarySite.Runtime = primarySite.Runtime; - secondarySite.Prerender = primarySite.Prerender; - secondarySite.Hybrid = primarySite.Hybrid; - secondarySite.EnhancedNavigation = primarySite.EnhancedNavigation; - secondarySite.Version = primarySite.Version; - secondarySite.HeadContent = primarySite.HeadContent; - secondarySite.BodyContent = primarySite.BodyContent; - secondarySite.CreatedBy = primarySite.CreatedBy; - secondarySite.CreatedOn = primarySite.CreatedOn; - secondarySite.ModifiedBy = primarySite.ModifiedBy; - secondarySite.ModifiedOn = primarySite.ModifiedOn; - secondarySite.IsDeleted = primarySite.IsDeleted; - secondarySite.DeletedBy = primarySite.DeletedBy; - secondarySite.DeletedOn = primarySite.DeletedOn; - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { + secondarySite.TimeZoneId = primarySite.TimeZoneId; + secondarySite.CultureCode = primarySite.CultureCode; + if (secondarySite.LogoFileId != primarySite.LogoFileId) + { + secondarySite.LogoFileId = ResolveFileId(provider, primarySite.LogoFileId, secondarySite.SiteId); + } + if (secondarySite.FaviconFileId != primarySite.FaviconFileId) + { + secondarySite.FaviconFileId = ResolveFileId(provider, primarySite.FaviconFileId, secondarySite.SiteId); ; + } + secondarySite.DefaultThemeType = primarySite.DefaultThemeType; + secondarySite.DefaultContainerType = primarySite.DefaultContainerType; + secondarySite.AdminContainerType = primarySite.AdminContainerType; + secondarySite.PwaIsEnabled = primarySite.PwaIsEnabled; + if (secondarySite.PwaAppIconFileId != primarySite.PwaAppIconFileId) + { + secondarySite.PwaAppIconFileId = ResolveFileId(provider, primarySite.PwaAppIconFileId, secondarySite.SiteId); ; + } + if (secondarySite.PwaSplashIconFileId != primarySite.PwaSplashIconFileId) + { + secondarySite.PwaSplashIconFileId = ResolveFileId(provider, primarySite.PwaSplashIconFileId, secondarySite.SiteId); ; + } + secondarySite.AllowRegistration = primarySite.AllowRegistration; + secondarySite.VisitorTracking = primarySite.VisitorTracking; + secondarySite.CaptureBrokenUrls = primarySite.CaptureBrokenUrls; + secondarySite.SiteGuid = primarySite.SiteGuid; + secondarySite.RenderMode = primarySite.RenderMode; + secondarySite.Runtime = primarySite.Runtime; + secondarySite.Prerender = primarySite.Prerender; + secondarySite.Hybrid = primarySite.Hybrid; + secondarySite.EnhancedNavigation = primarySite.EnhancedNavigation; + secondarySite.Version = primarySite.Version; + secondarySite.HeadContent = primarySite.HeadContent; + secondarySite.BodyContent = primarySite.BodyContent; + secondarySite.CreatedBy = primarySite.CreatedBy; + secondarySite.CreatedOn = primarySite.CreatedOn; + secondarySite.ModifiedBy = primarySite.ModifiedBy; + secondarySite.ModifiedOn = primarySite.ModifiedOn; + secondarySite.IsDeleted = primarySite.IsDeleted; + secondarySite.DeletedBy = primarySite.DeletedBy; + secondarySite.DeletedOn = primarySite.DeletedOn; + var siteRepository = provider.GetRequiredService(); siteRepository.UpdateSite(secondarySite); + log += Log(siteGroupMember, $"Site Updated: {secondarySite.Name} - {CreateLink(siteGroupMember.AliasName)}"); + } + else // change detection + { + log += Log(siteGroupMember, $"Site Updated: {primarySite.Name} - {CreateLink(siteGroupMember.AliasName)}"); } - log += Log(siteGroupMember, $"Site Updated: {secondarySite.Name} - {CreateLink(siteGroupMember.AliasName)}"); } // site settings log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Site, primarySite.SiteId, secondarySite.SiteId); - if (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log))) { // clear cache for secondary site if any content was Synchronized var syncManager = provider.GetRequiredService(); @@ -201,11 +206,11 @@ namespace Oqtane.Infrastructure syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySite.SiteId, SyncEventActions.Refresh); } - if (!string.IsNullOrEmpty(log) && siteGroupMember.SiteGroup.Type == SiteGroupTypes.ChangeDetection) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.ChangeDetection && !string.IsNullOrEmpty(log)) { // send change log to administrators SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, log); - log += Log(siteGroupMember, $"Change Log Sent To Administrators"); + log += Log(siteGroupMember, $"Change Log Sent To Administrators For Secondary Site: {secondarySite.Name}"); } return log; @@ -231,56 +236,61 @@ namespace Oqtane.Infrastructure var log = ""; foreach (var primaryRole in primaryRoles) - { - var role = secondaryRoles.FirstOrDefault(item => item.Name == primaryRole.Name); - - var secondaryRole = role; - if (secondaryRole == null) - { - secondaryRole = new Role(); - secondaryRole.SiteId = secondarySiteId; - } - - if (role == null || primaryRole.ModifiedOn > siteGroupMember.SynchronizedOn) - { - // set all properties - secondaryRole.Name = primaryRole.Name; - secondaryRole.Description = primaryRole.Description; - secondaryRole.IsAutoAssigned = primaryRole.IsAutoAssigned; - secondaryRole.IsSystem = primaryRole.IsSystem; - - if (role == null) - { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - roleRepository.AddRole(secondaryRole); - } - log += Log(siteGroupMember, $"Role Added: {secondaryRole.Name}"); - } - else - { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - roleRepository.UpdateRole(secondaryRole); - } - log += Log(siteGroupMember, $"Role Updated: {secondaryRole.Name}"); - } - } - - if (role != null) - { - secondaryRoles.Remove(role); - } - } - - // remove roles in the secondary site which do not exist in the primary site - foreach (var secondaryRole in secondaryRoles) { if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { - roleRepository.DeleteRole(secondaryRole.RoleId); + var role = secondaryRoles.FirstOrDefault(item => item.Name == primaryRole.Name); + + var secondaryRole = role; + if (secondaryRole == null) + { + secondaryRole = new Role(); + secondaryRole.SiteId = secondarySiteId; + } + + if (role == null || primaryRole.ModifiedOn > siteGroupMember.SynchronizedOn) + { + // set all properties + secondaryRole.Name = primaryRole.Name; + secondaryRole.Description = primaryRole.Description; + secondaryRole.IsAutoAssigned = primaryRole.IsAutoAssigned; + secondaryRole.IsSystem = primaryRole.IsSystem; + + if (role == null) + { + roleRepository.AddRole(secondaryRole); + log += Log(siteGroupMember, $"Role Added: {secondaryRole.Name}"); + } + else + { + roleRepository.UpdateRole(secondaryRole); + log += Log(siteGroupMember, $"Role Updated: {secondaryRole.Name}"); + } + } + + if (role != null) + { + secondaryRoles.Remove(role); + } + } + else // change detection + { + if (primaryRole.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Role Updated: {primaryRole.Name}"); + } + } + + } + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + // remove roles in the secondary site which do not exist in the primary site + foreach (var secondaryRole in secondaryRoles) + { + roleRepository.DeleteRole(secondaryRole.RoleId); + log += Log(siteGroupMember, $"Role Deleted: {secondaryRole.Name}"); } - log += Log(siteGroupMember, $"Role Deleted: {secondaryRole.Name}"); } // settings @@ -302,67 +312,104 @@ namespace Oqtane.Infrastructure // iterate through folders foreach (var primaryFolder in primaryFolders) { - var folder = secondaryFolders.FirstOrDefault(item => item.Path == primaryFolder.Path); - - var secondaryFolder = folder; - if (secondaryFolder == null) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { - secondaryFolder = new Folder(); - secondaryFolder.SiteId = secondarySiteId; - } + var folder = secondaryFolders.FirstOrDefault(item => item.Path == primaryFolder.Path); - if (folder == null || primaryFolder.ModifiedOn > siteGroupMember.SynchronizedOn) - { - // set all properties - secondaryFolder.ParentId = null; - if (primaryFolder.ParentId != null) + var secondaryFolder = folder; + if (secondaryFolder == null) { - var parentFolder = folderRepository.GetFolder(secondarySiteId, primaryFolders.First(item => item.FolderId == primaryFolder.ParentId).Path); - if (parentFolder != null) - { - secondaryFolder.ParentId = parentFolder.FolderId; - } + secondaryFolder = new Folder(); + secondaryFolder.SiteId = secondarySiteId; } - secondaryFolder.Type = primaryFolder.Type; - secondaryFolder.Name = primaryFolder.Name; - secondaryFolder.Order = primaryFolder.Order; - secondaryFolder.ImageSizes = primaryFolder.ImageSizes; - secondaryFolder.Capacity = primaryFolder.Capacity; - secondaryFolder.ImageSizes = primaryFolder.ImageSizes; - secondaryFolder.IsSystem = primaryFolder.IsSystem; - secondaryFolder.PermissionList = SynchronizePermissions(primaryFolder.PermissionList, secondarySiteId); - if (folder == null) + if (folder == null || primaryFolder.ModifiedOn > siteGroupMember.SynchronizedOn) { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + // set all properties + secondaryFolder.ParentId = null; + if (primaryFolder.ParentId != null) + { + var parentFolder = folderRepository.GetFolder(secondarySiteId, primaryFolders.First(item => item.FolderId == primaryFolder.ParentId).Path); + if (parentFolder != null) + { + secondaryFolder.ParentId = parentFolder.FolderId; + } + } + secondaryFolder.Type = primaryFolder.Type; + secondaryFolder.Name = primaryFolder.Name; + secondaryFolder.Order = primaryFolder.Order; + secondaryFolder.ImageSizes = primaryFolder.ImageSizes; + secondaryFolder.Capacity = primaryFolder.Capacity; + secondaryFolder.ImageSizes = primaryFolder.ImageSizes; + secondaryFolder.IsSystem = primaryFolder.IsSystem; + secondaryFolder.PermissionList = SynchronizePermissions(primaryFolder.PermissionList, secondarySiteId); + + if (folder == null) { folderRepository.AddFolder(secondaryFolder); + log += Log(siteGroupMember, $"Folder Added: {secondaryFolder.Path}"); } - log += Log(siteGroupMember, $"Folder Added: {secondaryFolder.Path}"); - } - else - { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + else { folderRepository.UpdateFolder(secondaryFolder); + log += Log(siteGroupMember, $"Folder Updated: {secondaryFolder.Path}"); } - log += Log(siteGroupMember, $"Folder Updated: {secondaryFolder.Path}"); + } + + if (folder != null) + { + secondaryFolders.Remove(folder); + } + + // folder settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId); + + // files + log += SynchronizeFiles(provider, folderRepository, fileRepository, siteGroupMember, primaryFolder, secondaryFolder); + } + else // change detection + { + if (primaryFolder.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Folder Updated: {primaryFolder.Path}"); + + // folder settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, -1); + + // files + log += SynchronizeFiles(provider, folderRepository, fileRepository, siteGroupMember, primaryFolder, null); } } + } - if (folder != null) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + // remove folders in the secondary site which do not exist in the primary site + foreach (var secondaryFolder in secondaryFolders) { - secondaryFolders.Remove(folder); + folderRepository.DeleteFolder(secondaryFolder.FolderId); + log += Log(siteGroupMember, $"Folder Deleted: {secondaryFolder.Path}"); } + } - // folder settings - log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId); + return log; + } - // get files for folder - var primaryFiles = fileRepository.GetFiles(primaryFolder.FolderId); - var secondaryFiles = fileRepository.GetFiles(secondaryFolder.FolderId).ToList(); + private string SynchronizeFiles(IServiceProvider provider, IFolderRepository folderRepository, IFileRepository fileRepository, SiteGroupMember siteGroupMember, Folder primaryFolder, Folder secondaryFolder) + { + var log = ""; - foreach (var primaryFile in primaryFiles) + // get files for folder + var primaryFiles = fileRepository.GetFiles(primaryFolder.FolderId); + var secondaryFiles = new List(); + if (secondaryFolder != null) + { + secondaryFiles = fileRepository.GetFiles(secondaryFolder.FolderId).ToList(); + } + + foreach (var primaryFile in primaryFiles) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { var file = secondaryFiles.FirstOrDefault(item => item.Name == primaryFile.Name); @@ -385,20 +432,14 @@ namespace Oqtane.Infrastructure if (file == null) { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - fileRepository.AddFile(secondaryFile); - SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); - } + fileRepository.AddFile(secondaryFile); + SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); log += Log(siteGroupMember, $"File Added: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); } else { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - fileRepository.UpdateFile(secondaryFile); - SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); - } + fileRepository.UpdateFile(secondaryFile); + SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); } } @@ -408,28 +449,25 @@ namespace Oqtane.Infrastructure secondaryFiles.Remove(file); } } - - // remove files in the secondary site which do not exist in the primary site - foreach (var secondaryFile in secondaryFiles) + else // change detection { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + if (primaryFile.ModifiedOn > siteGroupMember.SynchronizedOn) { - fileRepository.DeleteFile(secondaryFile.FileId); - var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); - System.IO.File.Delete(secondaryPath); + log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + "/" + primaryFolder.Path + primaryFile.Name)}"); } - log += Log(siteGroupMember, $"File Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); } } - // remove folders in the secondary site which do not exist in the primary site - foreach (var secondaryFolder in secondaryFolders) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + // remove files in the secondary site which do not exist in the primary site + foreach (var secondaryFile in secondaryFiles) { - folderRepository.DeleteFolder(secondaryFolder.FolderId); + fileRepository.DeleteFile(secondaryFile.FileId); + var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); + System.IO.File.Delete(secondaryPath); + log += Log(siteGroupMember, $"File Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); } - log += Log(siteGroupMember, $"Folder Deleted: {secondaryFolder.Path}"); } return log; @@ -456,10 +494,11 @@ namespace Oqtane.Infrastructure var moduleRepository = provider.GetRequiredService(); var log = ""; - List primaryPageModules = null; - List secondaryPageModules = null; - int tenantId = tenantManager.GetTenant().TenantId; + tenantManager.SetAlias(tenantId, primarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions() + var primaryPageModules = pageModuleRepository.GetPageModules(primarySiteId).ToList(); + tenantManager.SetAlias(tenantId, secondarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions() + var secondaryPageModules = pageModuleRepository.GetPageModules(secondarySiteId).ToList(); // get pages (ignore personalized) var primaryPages = pageRepository.GetPages(primarySiteId).Where(item => item.UserId == null); @@ -468,90 +507,122 @@ namespace Oqtane.Infrastructure // iterate through primary pages foreach (var primaryPage in primaryPages) { - var page = secondaryPages.FirstOrDefault(item => item.Path == primaryPage.Path); - - var secondaryPage = page; - if (secondaryPage == null) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { - secondaryPage = new Page(); - secondaryPage.SiteId = secondarySiteId; - } + var page = secondaryPages.FirstOrDefault(item => item.Path == primaryPage.Path); - if (page == null || primaryPage.ModifiedOn > siteGroupMember.SynchronizedOn) - { - // set all properties - secondaryPage.Path = primaryPage.Path; - secondaryPage.Name = primaryPage.Name; - secondaryPage.ParentId = null; - if (primaryPage.ParentId != null) + var secondaryPage = page; + if (secondaryPage == null) { - var parentPage = pageRepository.GetPage(primaryPages.First(item => item.PageId == primaryPage.ParentId).Path, secondarySiteId); - if (parentPage != null) - { - secondaryPage.ParentId = parentPage.PageId; - } + secondaryPage = new Page(); + secondaryPage.SiteId = secondarySiteId; } - secondaryPage.Title = primaryPage.Title; - secondaryPage.Order = primaryPage.Order; - secondaryPage.Url = primaryPage.Url; - secondaryPage.ThemeType = primaryPage.ThemeType; - secondaryPage.DefaultContainerType = primaryPage.DefaultContainerType; - secondaryPage.HeadContent = primaryPage.HeadContent; - secondaryPage.BodyContent = primaryPage.BodyContent; - secondaryPage.Icon = primaryPage.Icon; - secondaryPage.IsNavigation = primaryPage.IsNavigation; - secondaryPage.IsClickable = primaryPage.IsClickable; - secondaryPage.UserId = null; - secondaryPage.IsPersonalizable = primaryPage.IsPersonalizable; - secondaryPage.EffectiveDate = primaryPage.EffectiveDate; - secondaryPage.ExpiryDate = primaryPage.ExpiryDate; - secondaryPage.CreatedBy = primaryPage.CreatedBy; - secondaryPage.CreatedOn = primaryPage.CreatedOn; - secondaryPage.ModifiedBy = primaryPage.ModifiedBy; - secondaryPage.ModifiedOn = primaryPage.ModifiedOn; - secondaryPage.DeletedBy = primaryPage.DeletedBy; - secondaryPage.DeletedOn = primaryPage.DeletedOn; - secondaryPage.IsDeleted = primaryPage.IsDeleted; - secondaryPage.PermissionList = SynchronizePermissions(primaryPage.PermissionList, secondarySiteId); - if (page == null) + if (page == null || primaryPage.ModifiedOn > siteGroupMember.SynchronizedOn) { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + // set all properties + secondaryPage.Path = primaryPage.Path; + secondaryPage.Name = primaryPage.Name; + secondaryPage.ParentId = null; + if (primaryPage.ParentId != null) + { + var parentPage = pageRepository.GetPage(primaryPages.First(item => item.PageId == primaryPage.ParentId).Path, secondarySiteId); + if (parentPage != null) + { + secondaryPage.ParentId = parentPage.PageId; + } + } + secondaryPage.Title = primaryPage.Title; + secondaryPage.Order = primaryPage.Order; + secondaryPage.Url = primaryPage.Url; + secondaryPage.ThemeType = primaryPage.ThemeType; + secondaryPage.DefaultContainerType = primaryPage.DefaultContainerType; + secondaryPage.HeadContent = primaryPage.HeadContent; + secondaryPage.BodyContent = primaryPage.BodyContent; + secondaryPage.Icon = primaryPage.Icon; + secondaryPage.IsNavigation = primaryPage.IsNavigation; + secondaryPage.IsClickable = primaryPage.IsClickable; + secondaryPage.UserId = null; + secondaryPage.IsPersonalizable = primaryPage.IsPersonalizable; + secondaryPage.EffectiveDate = primaryPage.EffectiveDate; + secondaryPage.ExpiryDate = primaryPage.ExpiryDate; + secondaryPage.CreatedBy = primaryPage.CreatedBy; + secondaryPage.CreatedOn = primaryPage.CreatedOn; + secondaryPage.ModifiedBy = primaryPage.ModifiedBy; + secondaryPage.ModifiedOn = primaryPage.ModifiedOn; + secondaryPage.DeletedBy = primaryPage.DeletedBy; + secondaryPage.DeletedOn = primaryPage.DeletedOn; + secondaryPage.IsDeleted = primaryPage.IsDeleted; + secondaryPage.PermissionList = SynchronizePermissions(primaryPage.PermissionList, secondarySiteId); + + if (page == null) { secondaryPage = pageRepository.AddPage(secondaryPage); + log += Log(siteGroupMember, $"Page Added: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } - log += Log(siteGroupMember, $"Page Added: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); - } - else - { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + else { secondaryPage = pageRepository.UpdatePage(secondaryPage); + log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } - log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } + + if (page != null) + { + secondaryPages.Remove(page); + } + + // page settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId); + + // modules + log += SynchronizeModules(provider, settingRepository, pageModuleRepository, moduleRepository, siteGroupMember, primaryPageModules, secondaryPageModules, primaryPage, secondaryPage, secondarySiteId); + } + else // change detection + { + if (primaryPage.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}"); + } + + // page settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, -1); + + // modules + log += SynchronizeModules(provider, settingRepository, pageModuleRepository, moduleRepository, siteGroupMember, primaryPageModules, secondaryPageModules, primaryPage, null, secondarySiteId); } - if (page != null) - { - secondaryPages.Remove(page); - } + } - // page settings - log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId); + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + // remove pages in the secondary site which do not exist in the primary site + foreach (var secondaryPage in secondaryPages) + { + pageRepository.DeletePage(secondaryPage.PageId); + log += Log(siteGroupMember, $"Page Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + } + } - // modules - if (primaryPageModules == null) - { - tenantManager.SetAlias(tenantId, primarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions() - primaryPageModules = pageModuleRepository.GetPageModules(primarySiteId).ToList(); - } - if (secondaryPageModules == null) - { - tenantManager.SetAlias(tenantId, secondarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions() - secondaryPageModules = pageModuleRepository.GetPageModules(secondarySiteId).ToList(); - } - foreach (var primaryPageModule in primaryPageModules.Where(item => item.PageId == primaryPage.PageId)) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log))) + { + // clear cache for secondary site if any content was Synchronized + var syncManager = provider.GetRequiredService(); + var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySiteId }; + syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySiteId, SyncEventActions.Refresh); + } + + return log; + } + + private string SynchronizeModules(IServiceProvider provider, ISettingRepository settingRepository, IPageModuleRepository pageModuleRepository, IModuleRepository moduleRepository, SiteGroupMember siteGroupMember, List primaryPageModules, List secondaryPageModules, Page primaryPage, Page secondaryPage, int secondarySiteId) + { + var log = ""; + + // iterate through primary modules on primary page + foreach (var primaryPageModule in primaryPageModules.Where(item => item.PageId == primaryPage.PageId)) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { var pageModule = secondaryPageModules.FirstOrDefault(item => item.PageId == secondaryPage.PageId && item.Module.ModuleDefinitionName == primaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == primaryPageModule.Title.ToLower()); @@ -585,21 +656,14 @@ namespace Oqtane.Infrastructure var module = secondaryPageModules.FirstOrDefault(item => item.Module.ModuleDefinitionName == primaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == primaryPageModule.Title.ToLower())?.Module; if (module == null) { - // add new module - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - module = moduleRepository.AddModule(secondaryPageModule.Module); - } + module = moduleRepository.AddModule(secondaryPageModule.Module); log += Log(siteGroupMember, $"Module Added: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } if (module != null) { secondaryPageModule.ModuleId = module.ModuleId; secondaryPageModule.Module = null; // remove tracking - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); - } + secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); log += Log(siteGroupMember, $"Module Instance Added: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); secondaryPageModule.Module = module; } @@ -609,49 +673,17 @@ namespace Oqtane.Infrastructure // update existing module if (primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn) { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - moduleRepository.UpdateModule(secondaryPageModule.Module); - } + moduleRepository.UpdateModule(secondaryPageModule.Module); log += Log(siteGroupMember, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn) { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); - } + secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); log += Log(siteGroupMember, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); } } } - // module content - if (primaryPageModule.Module.ModuleDefinition.ServerManagerType != "") - { - Type moduleType = Type.GetType(primaryPageModule.Module.ModuleDefinition.ServerManagerType); - if (moduleType != null && moduleType.GetInterface(nameof(ISynchronizable)) != null) - { - try - { - var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); - var moduleContent = ((ISynchronizable)moduleObject).ExtractModule(primaryPageModule.Module, siteGroupMember.SynchronizedOn.Value); - if (!string.IsNullOrEmpty(moduleContent)) - { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, moduleContent); - } - log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); - } - } - catch - { - // error exporting/importing - } - } - } - if (pageModule != null) { secondaryPageModules.Remove(pageModule); @@ -659,48 +691,73 @@ namespace Oqtane.Infrastructure // module settings log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, secondaryPageModule.ModuleId); + + // module content + log += SynchronizeModuleContent(provider, siteGroupMember, primaryPageModule, secondaryPageModule, primaryPage, secondaryPage); + } + else // change detection + { + if (primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Module Updated: {primaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}"); + } + if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Module Instance Updated: {primaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}"); + } + + // module settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, -1); + + // module content + log += SynchronizeModuleContent(provider, siteGroupMember, primaryPageModule, null, primaryPage, secondaryPage); } } - // remove modules in the secondary site which do not exist in the primary site - foreach (var secondaryPageModule in secondaryPageModules) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { - var primaryPageId = -1; - var secondaryPage = secondaryPages.FirstOrDefault(item => item.PageId == secondaryPageModule.PageId); - if (secondaryPage != null) + // remove modules on the secondary page which do not exist on the primary page + foreach (var secondaryPageModule in secondaryPageModules.Where(item => item.PageId == secondaryPage.PageId)) { - var primaryPage = primaryPages.FirstOrDefault(item => item.Path == secondaryPage.Path); - if (primaryPage != null) - { - primaryPageId = primaryPage.PageId; - } - } - if (!primaryPageModules.Any(item => item.PageId == primaryPageId && item.Module.ModuleDefinitionName == secondaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == secondaryPageModule.Title.ToLower())) - { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); - } + pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); log += Log(siteGroupMember, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPageModule.Page.Path)}"); } } - // remove pages in the secondary site which do not exist in the primary site - foreach (var secondaryPage in secondaryPages) - { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) - { - pageRepository.DeletePage(secondaryPage.PageId); - } - log += Log(siteGroupMember, $"Page Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); - } + return log; + } - if (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)) + private string SynchronizeModuleContent(IServiceProvider provider, SiteGroupMember siteGroupMember, PageModule primaryPageModule, PageModule secondaryPageModule, Page primaryPage, Page secondaryPage) + { + var log = ""; + + if (primaryPageModule.Module.ModuleDefinition.ServerManagerType != "") { - // clear cache for secondary site if any content was Synchronized - var syncManager = provider.GetRequiredService(); - var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySiteId }; - syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySiteId, SyncEventActions.Refresh); + Type moduleType = Type.GetType(primaryPageModule.Module.ModuleDefinition.ServerManagerType); + if (moduleType != null && moduleType.GetInterface(nameof(ISynchronizable)) != null) + { + try + { + var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); + var moduleContent = ((ISynchronizable)moduleObject).ExtractModule(primaryPageModule.Module, siteGroupMember.SynchronizedOn.Value); + if (!string.IsNullOrEmpty(moduleContent)) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, moduleContent); + log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + } + else // change detection + { + log += Log(siteGroupMember, $"Module Content Updated: {primaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}"); + } + } + } + catch + { + // error exporting/importing + } + } } return log; @@ -730,44 +787,57 @@ namespace Oqtane.Infrastructure var secondarySettings = settingRepository.GetSettings(entityName, secondaryEntityId).ToList(); foreach (var primarySetting in settingRepository.GetSettings(entityName, primaryEntityId)) { - var secondarySetting = secondarySettings.FirstOrDefault(item => item.SettingName == primarySetting.SettingName); - if (secondarySetting == null) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { - secondarySetting = new Setting(); - secondarySetting.EntityName = primarySetting.EntityName; - secondarySetting.EntityId = secondaryEntityId; - secondarySetting.SettingName = primarySetting.SettingName; - secondarySetting.SettingValue = primarySetting.SettingValue; - secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) - { - settingRepository.AddSetting(secondarySetting); - updated = true; - } - } - else - { - if (secondarySetting.SettingValue != primarySetting.SettingValue || secondarySetting.IsPrivate != primarySetting.IsPrivate) + var secondarySetting = secondarySettings.FirstOrDefault(item => item.SettingName == primarySetting.SettingName); + if (secondarySetting == null) { + secondarySetting = new Setting(); + secondarySetting.EntityName = primarySetting.EntityName; + secondarySetting.EntityId = secondaryEntityId; + secondarySetting.SettingName = primarySetting.SettingName; secondarySetting.SettingValue = primarySetting.SettingValue; secondarySetting.IsPrivate = primarySetting.IsPrivate; - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) { - settingRepository.UpdateSetting(secondarySetting); + settingRepository.AddSetting(secondarySetting); updated = true; } } - secondarySettings.Remove(secondarySetting); + else + { + if (secondarySetting.SettingValue != primarySetting.SettingValue || secondarySetting.IsPrivate != primarySetting.IsPrivate) + { + secondarySetting.SettingValue = primarySetting.SettingValue; + secondarySetting.IsPrivate = primarySetting.IsPrivate; + if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + { + settingRepository.UpdateSetting(secondarySetting); + updated = true; + } + } + secondarySettings.Remove(secondarySetting); + } + } + else // change detection + { + if (primarySetting.ModifiedOn > siteGroupMember.SynchronizedOn && !excludedSettings.Any(item => item.EntityName == primarySetting.EntityName && item.SettingName == primarySetting.SettingName)) + { + updated = true; + } } } - // any remaining secondary settings need to be deleted - foreach (var secondarySetting in secondarySettings) + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { - if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && !excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + // any remaining secondary settings need to be deleted + foreach (var secondarySetting in secondarySettings) { - settingRepository.DeleteSetting(secondarySetting.EntityName, secondarySetting.SettingId); - updated = true; + if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + { + settingRepository.DeleteSetting(secondarySetting.EntityName, secondarySetting.SettingId); + updated = true; + } } } From 458c8534c7f7ed0ed725ed9998218ea6e902f888 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 24 Feb 2026 08:59:40 -0500 Subject: [PATCH 67/75] improve change detection notification logic --- .../Infrastructure/Jobs/SynchronizationJob.cs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 43047ff7..f699fe20 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -209,8 +209,7 @@ namespace Oqtane.Infrastructure if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.ChangeDetection && !string.IsNullOrEmpty(log)) { // send change log to administrators - SendNotifications(provider, secondarySite.SiteId, secondarySite.Name, log); - log += Log(siteGroupMember, $"Change Log Sent To Administrators For Secondary Site: {secondarySite.Name}"); + log += SendNotifications(provider, siteGroupMember, secondarySite.SiteId, secondarySite.Name, log); } return log; @@ -849,16 +848,29 @@ namespace Oqtane.Infrastructure return log; } - private void SendNotifications(IServiceProvider provider, int siteId, string siteName, string log) + private string SendNotifications(IServiceProvider provider, SiteGroupMember siteGroupMember, int siteId, string siteName, string changeLog) { var userRoleRepository = provider.GetRequiredService(); var notificationRepository = provider.GetRequiredService(); + var log = ""; - foreach (var userRole in userRoleRepository.GetUserRoles(RoleNames.Admin, siteId)) + // get administrators for site + var userRoles = userRoleRepository.GetUserRoles(RoleNames.Admin, siteId); + if (userRoles != null && userRoles.Any()) { - var notification = new Notification(siteId, userRole.User, $"{siteName} Change Log", log); - notificationRepository.AddNotification(notification); + foreach (var userRole in userRoles) + { + var notification = new Notification(siteId, userRole.User, $"{siteName} Change Log", changeLog); + notificationRepository.AddNotification(notification); + } + log += Log(siteGroupMember, $"Change Log Sent To Administrators For Secondary Site: {siteName}"); } + else + { + log += Log(siteGroupMember, $"Error Sending Change Log - Secondary Site {siteName} Does Not Have Any Administrators Defined"); + } + + return log; } private string Log(SiteGroupMember siteGroupMember, string content) From 9aad4000380d4272ea41dc2ffe899249432154a3 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 24 Feb 2026 15:35:45 -0500 Subject: [PATCH 68/75] add copy page functionality to control panel --- Oqtane.Client/Modules/Admin/Pages/Edit.razor | 104 +++++++++++++----- .../Controls/ControlPanelInteractive.resx | 3 + Oqtane.Client/Services/PageService.cs | 14 +++ .../Theme/ControlPanelInteractive.razor | 40 +++++-- Oqtane.Server/Controllers/PageController.cs | 88 +++++++++++++-- .../Infrastructure/Jobs/SynchronizationJob.cs | 2 +- 6 files changed, 204 insertions(+), 47 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 029680ce..4fe412c5 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -46,7 +46,7 @@
- +
- - -
+ @if (!_copy) + { + + +
    @Localizer["ModuleTitle"] @Localizer["ModuleDefinition"] -
- - - - @context.Title - @context.ModuleDefinition?.Name - -
-
- @if (_themeSettingsType != null) - { - - @_themeSettingsComponent -
- - +
+ + + + @context.Title + @context.ModuleDefinition?.Name + +
+ @if (_themeSettingsType != null) + { + + @_themeSettingsComponent +
+ + +
+ } + } } @@ -349,6 +353,7 @@ private List _containers = new List(); private List _pages; private int _pageId; + private bool _copy = false; private string _name; private string _currentparentid; private string _parentid = "-1"; @@ -394,6 +399,10 @@ { _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pageId = Int32.Parse(PageState.QueryString["id"]); + if (PageState.QueryString.ContainsKey("copy")) + { + _copy = bool.Parse(PageState.QueryString["copy"]); + } _page = await PageService.GetPageAsync(_pageId); _icons = await SystemService.GetIconsAsync(); _iconresources = Utilities.GetFullTypeName(typeof(IconResources).AssemblyQualifiedName); @@ -413,7 +422,7 @@ _children = new List(); foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid, CultureInfo.InvariantCulture)))) { - if (p.PageId != _pageId && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList)) + if ((p.PageId != _pageId || _copy) && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList)) { _children.Add(p); } @@ -440,6 +449,12 @@ _expirydate = Utilities.UtcAsLocalDate(_page.ExpiryDate); _ispersonalizable = _page.IsPersonalizable.ToString(); + if (_copy) + { + _insert = ">"; + _childid = _page.PageId; + } + // appearance _title = _page.Title; _themetype = _page.ThemeType; @@ -470,6 +485,19 @@ // permissions _permissions = _page.PermissionList; _updatemodulepermissions = "True"; + if (_copy) + { + _permissions = _page.PermissionList.Select(item => new Permission + { + SiteId = item.SiteId, + EntityName = item.EntityName, + EntityId = -1, + PermissionName = item.PermissionName, + RoleName = item.RoleName, + UserId = item.UserId, + IsAuthorized = item.IsAuthorized, + }).ToList(); + } // page modules var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId); @@ -484,6 +512,13 @@ _deletedon = _page.DeletedOn; ThemeSettings(); + + if (_copy) + { + _name = ""; + _path = ""; + } + _initialized = true; } else @@ -554,7 +589,7 @@ builder.OpenComponent(0, _themeSettingsType); builder.AddAttribute(1, "RenderModeBoundary", RenderModeBoundary); builder.AddComponentReferenceCapture(2, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); - + builder.CloseComponent(); }; } @@ -581,6 +616,13 @@ { string currentPath = _page.Path; + if (_copy) + { + _page = new Page(); + _page.SiteId = PageState.Site.SiteId; + currentPath = ""; + } + _page.Name = _name; if (_parentid == "-1") @@ -696,8 +738,19 @@ _page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions); } - // update page - _page = await PageService.UpdatePageAsync(_page); + if (_copy) + { + // create page + _page = await PageService.AddPageAsync(_page); + await PageService.CopyPageAsync(_pageId, _page.PageId, bool.Parse(_updatemodulepermissions)); + await logger.LogInformation("Page Added {Page}", _page); + } + else + { + // update page + _page = await PageService.UpdatePageAsync(_page); + await logger.LogInformation("Page Saved {Page}", _page); + } // update page order await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, _page.ParentId); @@ -710,7 +763,6 @@ await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, int.Parse(_currentparentid)); } - await logger.LogInformation("Page Saved {Page}", _page); if (!string.IsNullOrEmpty(PageState.ReturnUrl)) { NavigationManager.NavigateTo(PageState.ReturnUrl, true); // redirect to page being edited and reload diff --git a/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx b/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx index 5247824e..1ef062f4 100644 --- a/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx +++ b/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx @@ -201,4 +201,7 @@ Synchronize Site + + Copy Page + \ No newline at end of file diff --git a/Oqtane.Client/Services/PageService.cs b/Oqtane.Client/Services/PageService.cs index e3f940c6..4beefcd0 100644 --- a/Oqtane.Client/Services/PageService.cs +++ b/Oqtane.Client/Services/PageService.cs @@ -71,6 +71,15 @@ namespace Oqtane.Services /// /// Task DeletePageAsync(int pageId); + + /// + /// Copies the modules from one page to another + /// + /// + /// + /// + /// + Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -129,5 +138,10 @@ namespace Oqtane.Services { await DeleteAsync($"{Apiurl}/{pageId}"); } + + public async Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions) + { + await PostAsync($"{Apiurl}/{fromPageId}/{toPageId}/{usePagePermissions}"); + } } } diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index 028b33ec..377bde6b 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -59,18 +59,29 @@ -
-
- @if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone)) - { - - } - else - { - - } + @if (PageState.Page.UserId == null) + { +
+
+ +
-
+ } + @if (!PageState.Page.Path.StartsWith("admin/")) + { +
+
+ @if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone)) + { + + } + else + { + + } +
+
+ }
@if (_deleteConfirmation) @@ -496,7 +507,12 @@ case "Edit": // get page management moduleid moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]); - NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}")); + NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, "Edit", $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}")); + break; + case "Copy": + // get page management moduleid + moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]); + NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, "Edit", $"id={PageState.Page.PageId}©=true&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}")); break; } } diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index 69bed0bc..3a367bb2 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -1,16 +1,15 @@ using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Oqtane.Models; -using Oqtane.Shared; using System.Linq; -using Oqtane.Security; using System.Net; +using System.Security; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Oqtane.Enums; using Oqtane.Infrastructure; +using Oqtane.Models; using Oqtane.Repository; -using System.Xml.Linq; -using Microsoft.AspNetCore.Diagnostics; +using Oqtane.Security; +using Oqtane.Shared; namespace Oqtane.Controllers { @@ -498,6 +497,79 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } - } + // POST api//5/6 + [HttpPost("{fromPageId}/{toPageId}/{usePagePermissions}")] + [Authorize(Roles = RoleNames.Registered)] + public void Post(int fromPageId, int toPageId, bool usePagePermissions) + { + var fromPage = _pages.GetPage(fromPageId); + if (fromPage != null && fromPage.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, fromPage.PermissionList)) + { + var toPage = _pages.GetPage(toPageId); + if (toPage != null && toPage.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, toPage.PermissionList)) + { + // copy modules + List pageModules = _pageModules.GetPageModules(fromPage.SiteId).ToList(); + foreach (PageModule pm in pageModules.Where(item => item.PageId == fromPage.PageId && !item.Module.AllPages && !item.IsDeleted)) + { + Module module = new Module(); + module.SiteId = fromPage.SiteId; + module.PageId = toPageId; + module.ModuleDefinitionName = pm.Module.ModuleDefinitionName; + module.AllPages = false; + if (usePagePermissions) + { + module.PermissionList = toPage.PermissionList; + } + else + { + module.PermissionList = pm.Module.PermissionList; + } + module.PermissionList = module.PermissionList.Select(item => new Permission + { + SiteId = item.SiteId, + EntityName = EntityNames.Module, + EntityId = -1, + PermissionName = item.PermissionName, + RoleName = item.RoleName, + UserId = item.UserId, + IsAuthorized = item.IsAuthorized, + }).ToList(); + module = _modules.AddModule(module); + + string content = _modules.ExportModule(pm.ModuleId); + if (content != "") + { + _modules.ImportModule(module.ModuleId, content); + } + + PageModule pageModule = new PageModule(); + pageModule.PageId = toPageId; + pageModule.ModuleId = module.ModuleId; + pageModule.Title = pm.Title; + pageModule.Pane = pm.Pane; + pageModule.Order = pm.Order; + pageModule.ContainerType = pm.ContainerType; + pageModule.EffectiveDate = pm.EffectiveDate; + pageModule.ExpiryDate = pm.ExpiryDate; + pageModule.Header = pm.Header; + pageModule.Footer = pm.Footer; + + _pageModules.AddPageModule(pageModule); + } + + _syncManager.AddSyncEvent(_alias, EntityNames.Site, fromPage.SiteId, SyncEventActions.Refresh); + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + } } diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index f699fe20..2b574833 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -17,7 +17,7 @@ namespace Oqtane.Infrastructure // synchronization only supports sites in the same tenant (database) // module title is used as a key to identify module instances on a page - // modules must implement ISynchronizable interface + // modules must implement ISynchronizable interface for content synchronization // change detection does not support deleted items as key values will usually be different due to localization // define settings that should not be synchronized (should be extensible in the future) From 09f6a1d531cebd7d0eac2c4681ae7e2d1353fda1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 24 Feb 2026 16:04:55 -0500 Subject: [PATCH 69/75] explicitly set module order in Default Site Template --- .../SiteTemplates/DefaultSiteTemplate.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index 65bf8c74..a1e1691a 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -49,7 +49,10 @@ namespace Oqtane.Infrastructure.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Welcome To Oqtane...", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "Welcome To Oqtane...", + Pane = PaneNames.Default, + Order = 1, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -60,7 +63,10 @@ namespace Oqtane.Infrastructure.SiteTemplates "

Blazor is a modern front-end web framework based on HTML, CSS, and C# that helps you build web applications faster. Blazor provides a component-based architecture with server-side rendering and full client-side interactivity in a single solution, where you can switch between server-side and client-side rendering modes and even mix them in the same web page. For desktop or mobile scenarios, Blazor Hybrid is part of .NET MAUI and uses a Web View to render components natively on all modern devices.

" + "

Blazor is a feature of ASP.NET, the popular cross platform development framework from Microsoft that provides powerful tools and libraries for building modern software applications.

" }, - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "MIT License", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "MIT License", + Pane = PaneNames.Default, + Order = 3, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -71,7 +77,10 @@ namespace Oqtane.Infrastructure.SiteTemplates "

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

" + "

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

" }, - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "Secure Content", + Pane = PaneNames.Default, + Order = 5, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Registered, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -96,7 +105,10 @@ namespace Oqtane.Infrastructure.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "Secure Content", + Pane = PaneNames.Default, + Order = 1, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Registered, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -121,7 +133,10 @@ namespace Oqtane.Infrastructure.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "My Page", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "My Page", + Pane = PaneNames.Default, + Order = 1, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), From 06e555ddd16627285a7dd1b38666c7f52ea3393e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 25 Feb 2026 08:12:31 -0500 Subject: [PATCH 70/75] relocate folder hierarchy logic to folder repository (consistent with page approach) --- Oqtane.Server/Controllers/FolderController.cs | 44 +----------------- Oqtane.Server/Repository/FolderRepository.cs | 45 ++++++++++++++++++- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/Oqtane.Server/Controllers/FolderController.cs b/Oqtane.Server/Controllers/FolderController.cs index 12a9c3fa..e456c61f 100644 --- a/Oqtane.Server/Controllers/FolderController.cs +++ b/Oqtane.Server/Controllers/FolderController.cs @@ -43,7 +43,7 @@ namespace Oqtane.Controllers int SiteId; if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) { - var hierarchy = GetFoldersHierarchy(_folders.GetFolders(SiteId).ToList()); + var hierarchy = _folders.GetFolders(SiteId).ToList(); foreach (Folder folder in hierarchy) { // note that Browse permission is used for this method @@ -281,47 +281,5 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } - - private static List GetFoldersHierarchy(List folders) - { - List hierarchy = new List(); - Action, Folder> getPath = null; - getPath = (folderList, folder) => - { - IEnumerable children; - int level; - if (folder == null) - { - level = -1; - children = folders.Where(item => item.ParentId == null); - } - else - { - level = folder.Level; - children = folders.Where(item => item.ParentId == folder.FolderId); - } - - foreach (Folder child in children) - { - child.Level = level + 1; - child.HasChildren = folders.Any(item => item.ParentId == child.FolderId); - hierarchy.Add(child); - getPath(folderList, child); - } - }; - folders = folders.OrderBy(item => item.Name).ToList(); - getPath(folders, null); - - // add any non-hierarchical items to the end of the list - foreach (Folder folder in folders) - { - if (hierarchy.Find(item => item.FolderId == folder.FolderId) == null) - { - hierarchy.Add(folder); - } - } - - return hierarchy; - } } } diff --git a/Oqtane.Server/Repository/FolderRepository.cs b/Oqtane.Server/Repository/FolderRepository.cs index fb68033f..d7f1c43e 100644 --- a/Oqtane.Server/Repository/FolderRepository.cs +++ b/Oqtane.Server/Repository/FolderRepository.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Hosting; @@ -45,7 +46,49 @@ namespace Oqtane.Repository { folder.PermissionList = permissions.Where(item => item.EntityId == folder.FolderId).ToList(); } - return folders; + return GetFoldersHierarchy(folders); + } + + private static List GetFoldersHierarchy(List folders) + { + List hierarchy = new List(); + Action, Folder> getPath = null; + getPath = (folderList, folder) => + { + IEnumerable children; + int level; + if (folder == null) + { + level = -1; + children = folders.Where(item => item.ParentId == null); + } + else + { + level = folder.Level; + children = folders.Where(item => item.ParentId == folder.FolderId); + } + + foreach (Folder child in children) + { + child.Level = level + 1; + child.HasChildren = folders.Any(item => item.ParentId == child.FolderId); + hierarchy.Add(child); + getPath(folderList, child); + } + }; + folders = folders.OrderBy(item => item.Name).ToList(); + getPath(folders, null); + + // add any non-hierarchical items to the end of the list + foreach (Folder folder in folders) + { + if (hierarchy.Find(item => item.FolderId == folder.FolderId) == null) + { + hierarchy.Add(folder); + } + } + + return hierarchy; } public Folder AddFolder(Folder folder) From 00d14552b161062886002400b34c1a02c57b308d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 25 Feb 2026 09:46:05 -0500 Subject: [PATCH 71/75] do not display audit info when copying pages --- Oqtane.Client/Modules/Admin/Pages/Edit.razor | 5 +- Oqtane.Server/Controllers/PageController.cs | 62 ++++++++++++-------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 4fe412c5..96ec6395 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -225,7 +225,10 @@

- + @if (!_copy) + { + + }
diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index 3a367bb2..a23ba365 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -513,35 +513,47 @@ namespace Oqtane.Controllers List pageModules = _pageModules.GetPageModules(fromPage.SiteId).ToList(); foreach (PageModule pm in pageModules.Where(item => item.PageId == fromPage.PageId && !item.Module.AllPages && !item.IsDeleted)) { - Module module = new Module(); - module.SiteId = fromPage.SiteId; - module.PageId = toPageId; - module.ModuleDefinitionName = pm.Module.ModuleDefinitionName; - module.AllPages = false; - if (usePagePermissions) + Module module; + + // determine if module is a shared instance (ie. exists on other pages) + if (!pageModules.Any(item => item.ModuleId == pm.ModuleId && item.PageId != fromPage.PageId)) { - module.PermissionList = toPage.PermissionList; + // create new module + module = new Module(); + module.SiteId = fromPage.SiteId; + module.PageId = toPageId; + module.ModuleDefinitionName = pm.Module.ModuleDefinitionName; + module.AllPages = false; + if (usePagePermissions) + { + module.PermissionList = toPage.PermissionList; + } + else + { + module.PermissionList = pm.Module.PermissionList; + } + module.PermissionList = module.PermissionList.Select(item => new Permission + { + SiteId = item.SiteId, + EntityName = EntityNames.Module, + EntityId = -1, + PermissionName = item.PermissionName, + RoleName = item.RoleName, + UserId = item.UserId, + IsAuthorized = item.IsAuthorized, + }).ToList(); + + module = _modules.AddModule(module); + string content = _modules.ExportModule(pm.ModuleId); + if (content != "") + { + _modules.ImportModule(module.ModuleId, content); + } } else { - module.PermissionList = pm.Module.PermissionList; - } - module.PermissionList = module.PermissionList.Select(item => new Permission - { - SiteId = item.SiteId, - EntityName = EntityNames.Module, - EntityId = -1, - PermissionName = item.PermissionName, - RoleName = item.RoleName, - UserId = item.UserId, - IsAuthorized = item.IsAuthorized, - }).ToList(); - module = _modules.AddModule(module); - - string content = _modules.ExportModule(pm.ModuleId); - if (content != "") - { - _modules.ImportModule(module.ModuleId, content); + // use existing module + module = pm.Module; } PageModule pageModule = new PageModule(); From 0cc1b5a3e95d53c69640e940c4064eedeea78540 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 25 Feb 2026 10:38:59 -0500 Subject: [PATCH 72/75] set default module title in control panel when adding or copying existing module --- .../Controls/Theme/ControlPanelInteractive.razor | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index 377bde6b..ffd54afa 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -166,7 +166,7 @@ } - @foreach (Module module in _modules) { @@ -358,6 +358,20 @@ StateHasChanged(); } + private async Task PageModuleChanged(ChangeEventArgs e) + { + _moduleId = (string)e.Value; + if (_moduleId != "-") + { + _title = _modules.First(item => item.ModuleId == int.Parse(_moduleId)).Title; + } + else + { + _title = ""; + } + StateHasChanged(); + } + private async Task AddModule() { if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList)) From 573a914699f8423be8de9c9e1657fc1ddedc1fff Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 25 Feb 2026 11:44:15 -0500 Subject: [PATCH 73/75] provide an indicator in Module Settings when a module is shared across multiple pages --- .../Modules/Admin/Modules/Settings.razor | 30 +++++++++++-------- Oqtane.Client/Modules/Admin/Site/Index.razor | 2 +- .../Modules/Admin/Modules/Settings.resx | 2 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 7 +++-- .../Repository/PageModuleRepository.cs | 2 ++ Oqtane.Shared/Models/Module.cs | 5 ++++ 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Settings.razor b/Oqtane.Client/Modules/Admin/Modules/Settings.razor index 82ca6a69..bde62f80 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Settings.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Settings.razor @@ -73,27 +73,29 @@
- +
- - } - else + + } + else + { + + + }
@@ -161,6 +163,7 @@ private string _pane; private string _containerType; private string _allPages = "false"; + private bool _isShared = false; private string _header = ""; private string _footer = ""; private string _permissionNames = ""; @@ -207,6 +210,7 @@ _expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate); _allPages = pagemodule.Module.AllPages.ToString(); + _isShared = pagemodule.Module.IsShared; createdby = pagemodule.Module.CreatedBy; createdon = pagemodule.Module.CreatedOn; modifiedby = pagemodule.Module.ModifiedBy; diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 49a3273e..9b94e0c0 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -526,7 +526,7 @@ - @if (_primary == "False" && (_groupType == SiteGroupTypes.Synchronization || _groupType == SiteGroupTypes.ChangeDetection)) + @if (_primary == "False" && !string.IsNullOrEmpty(_synchronized) && (_groupType == SiteGroupTypes.Synchronization || _groupType == SiteGroupTypes.ChangeDetection)) {
diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx index 4f8f895c..0cf90297 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx @@ -130,7 +130,7 @@ Indicate if this module should be displayed on all pages - The page that the module is located on + The page that the module is located on. Please note that shared modules cannot be moved to other pages. Title: diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 2b574833..24e3ed19 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -617,6 +617,7 @@ namespace Oqtane.Infrastructure private string SynchronizeModules(IServiceProvider provider, ISettingRepository settingRepository, IPageModuleRepository pageModuleRepository, IModuleRepository moduleRepository, SiteGroupMember siteGroupMember, List primaryPageModules, List secondaryPageModules, Page primaryPage, Page secondaryPage, int secondarySiteId) { var log = ""; + var removePageModules = secondaryPageModules.Where(item => item.PageId == secondaryPage.PageId).ToList(); // iterate through primary modules on primary page foreach (var primaryPageModule in primaryPageModules.Where(item => item.PageId == primaryPage.PageId)) @@ -651,7 +652,7 @@ namespace Oqtane.Infrastructure if (pageModule == null) { - // check if module exists + // check if module exists in site (ie. a shared instance) var module = secondaryPageModules.FirstOrDefault(item => item.Module.ModuleDefinitionName == primaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == primaryPageModule.Title.ToLower())?.Module; if (module == null) { @@ -685,7 +686,7 @@ namespace Oqtane.Infrastructure if (pageModule != null) { - secondaryPageModules.Remove(pageModule); + removePageModules.Remove(pageModule); } // module settings @@ -716,7 +717,7 @@ namespace Oqtane.Infrastructure if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) { // remove modules on the secondary page which do not exist on the primary page - foreach (var secondaryPageModule in secondaryPageModules.Where(item => item.PageId == secondaryPage.PageId)) + foreach (var secondaryPageModule in removePageModules) { pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); log += Log(siteGroupMember, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPageModule.Page.Path)}"); diff --git a/Oqtane.Server/Repository/PageModuleRepository.cs b/Oqtane.Server/Repository/PageModuleRepository.cs index 239a9a67..8913f7fc 100644 --- a/Oqtane.Server/Repository/PageModuleRepository.cs +++ b/Oqtane.Server/Repository/PageModuleRepository.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Security.Policy; using Microsoft.EntityFrameworkCore; using Oqtane.Models; using Oqtane.Shared; @@ -98,6 +99,7 @@ namespace Oqtane.Repository var permissions = _permissions.GetPermissions(pagemodule.Module.SiteId, EntityNames.Module).ToList(); pagemodule = GetPageModule(pagemodule, moduledefinitions, permissions); } + pagemodule.Module.IsShared = db.PageModule.Count(item => item.ModuleId == pagemodule.ModuleId) > 1; return pagemodule; } diff --git a/Oqtane.Shared/Models/Module.cs b/Oqtane.Shared/Models/Module.cs index 7775fcf5..1c2a23cb 100644 --- a/Oqtane.Shared/Models/Module.cs +++ b/Oqtane.Shared/Models/Module.cs @@ -33,6 +33,11 @@ namespace Oqtane.Models /// public bool AllPages { get; set; } + /// + /// indicates if the module is shared across multiple pages (only set in specific scenarios to ensure performance) + /// + [NotMapped] + public bool IsShared { get; set; } /// /// Reference to the used for this module. From 1cb0a45715dc70e907bec7372e7678b0d80a993b Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 25 Feb 2026 14:17:14 -0500 Subject: [PATCH 74/75] update azuredeploy to 10.1.0 --- azuredeploy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azuredeploy.json b/azuredeploy.json index 72478428..2f2ee83c 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -220,7 +220,7 @@ "apiVersion": "2024-04-01", "name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]", "properties": { - "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.0.4/Oqtane.Framework.10.0.4.Install.zip" + "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.1.0/Oqtane.Framework.10.1.0.Install.zip" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" From c3a0a9662389e8cad542f623bc2152b6af39ea6e Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 25 Feb 2026 14:21:58 -0500 Subject: [PATCH 75/75] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 96492eeb..3071a708 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline # Latest Release -[10.0.4](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4) was released on January 20, 2026 and is a maintenance release including 29 pull requests by 3 different contributors, pushing the total number of project commits all-time over 7500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[10.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0) was released on February 25, 2026 and is a maintenance release including 72 pull requests by 6 different contributors, pushing the total number of project commits all-time over 7600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. # Try It Now! @@ -111,6 +111,12 @@ Connect with other developers, get support, and share ideas by joining the Oqtan # Roadmap This project is open source, and therefore is a work in progress... +[10.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0) (Feb 25, 2026) +- [x] Site Groups, Content Synchronization, Content Localization +- [x] Global Replace +- [x] Copy Page +- [x] Site Tasks + [10.0.4](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4) (Jan 20, 2026) - [x] Stabilization improvements