improvements for site groups

This commit is contained in:
sbwalker
2026-02-06 11:53:10 -05:00
parent dff2261994
commit 57deeb6acf
12 changed files with 131 additions and 98 deletions

View File

@@ -549,15 +549,18 @@
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(_synchronized))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="synchronized" HelpText="The date/time of the last synchronization for the site" ResourceKey="Synchronized">Synchronized: </Label>
<div class="col-sm-9">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="synchronized" HelpText="The date/time of the last synchronization for the site" ResourceKey="Synchronized">Synchronized: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="synchronized" class="form-control" @bind="@_synchronized" disabled />
</div>
@if (!string.IsNullOrEmpty(_synchronized))
{
<button type="button" class="btn btn-primary" @onclick="ResetSiteGroup">@SharedLocalizer["Reset"]</button>
}
</div>
}
</div>
</div>
}
}
<div class="row mb-1 align-items-center">
@@ -605,10 +608,6 @@
<br />
<button type="button" class="btn btn-success" @onclick="SaveSite">@SharedLocalizer["Save"]</button>
<ActionDialog Header="Delete Site" Message="@Localizer["Confirm.DeleteSite"]" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteSite())" ResourceKey="DeleteSite" />
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _siteGroupDefinitions.Any(item => item.PrimarySiteId == PageState.Site.SiteId && item.Synchronization))
{
<button type="button" class="btn btn-primary ms-1" @onclick="SynchronizeSite">@Localizer["Synchronize"]</button>
}
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
@@ -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 = "";
}
}

View File

@@ -552,9 +552,6 @@
<data name="Message.Site.Synchronize" xml:space="preserve">
<value>Site Submitted For Synchronization</value>
</data>
<data name="Synchronize" xml:space="preserve">
<value>Synchronize</value>
</data>
<data name="Notify.Text" xml:space="preserve">
<value>Notify?</value>
</data>

View File

@@ -200,5 +200,8 @@
</data>
<data name="Module.CopyExisting" xml:space="preserve">
<value>Copy Existing Module</value>
</data>
</data>
<data name="Synchronize" xml:space="preserve">
<value>Synchronize</value>
</data>
</root>

View File

@@ -19,6 +19,12 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<SiteGroupDefinition>> GetSiteGroupDefinitionsAsync();
/// <summary>
/// Get all <see cref="SiteGroupDefinition"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroupDefinition>> GetSiteGroupDefinitionsAsync(int primarySiteId);
/// <summary>
/// Get one specific <see cref="SiteGroupDefinition"/>
/// </summary>
@@ -57,7 +63,12 @@ namespace Oqtane.Services
public async Task<List<SiteGroupDefinition>> GetSiteGroupDefinitionsAsync()
{
return await GetJsonAsync<List<SiteGroupDefinition>>($"{Apiurl}", Enumerable.Empty<SiteGroupDefinition>().ToList());
return await GetSiteGroupDefinitionsAsync(-1);
}
public async Task<List<SiteGroupDefinition>> GetSiteGroupDefinitionsAsync(int primarySiteId)
{
return await GetJsonAsync<List<SiteGroupDefinition>>($"{Apiurl}?siteid={primarySiteId}", Enumerable.Empty<SiteGroupDefinition>().ToList());
}
public async Task<SiteGroupDefinition> GetSiteGroupDefinitionAsync(int siteGroupDefinitionId)

View File

@@ -4,6 +4,11 @@
@inject IPageService PageService
@inject ISettingService SettingService
@if (ShowLanguageSwitcher)
{
<LanguageSwitcher ButtonClass="@ButtonClass" DropdownAlignment="@LanguageDropdownAlignment" />
}
@if (ShowEditMode && (_showEditMode || (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))))
{
<form method="post" class="app-form-inline" @formname="EditModeForm" @onsubmit="@(async () => await ToggleEditMode(PageState.EditMode))" data-enhance>
@@ -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

View File

@@ -11,6 +11,7 @@
@inject ILogService logger
@inject ISettingService SettingService
@inject IJSRuntime jsRuntime
@inject ISiteGroupDefinitionService SiteGroupDefinitionService
@inject IServiceProvider ServiceProvider
@inject ILogService LoggingService
@inject IStringLocalizer<ControlPanelInteractive> Localizer
@@ -34,6 +35,10 @@
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-primary col-12" @onclick=@(async () => Navigate("Admin"))>@Localizer["AdminDash"]</button>
</div>
</div>
@if (_siteGroupDefinitions.Any(item => item.Synchronization))
{
<button type="button" class="btn btn-success col-12 mt-1" @onclick="SynchronizeSite">@Localizer["Synchronize"]</button>
}
<hr class="app-rule" />
}
@if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
@@ -257,6 +262,7 @@
private List<Page> _pages = new List<Page>();
private List<Module> _modules = new List<Module>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteGroupDefinition> _siteGroupDefinitions = new List<SiteGroupDefinition>();
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);
}
}

View File

@@ -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)
{
<div class="app-languages btn-group pe-1" role="group">
<button id="btnCultures" type="button" class="btn @ButtonClass dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="oi oi-globe"></span>
</button>
<div class="dropdown-menu @MenuAlignment">
@foreach (var language in PageState.Site.Languages)
{
<a class="dropdown-item" href="@(PageState.Alias.Protocol + language.AliasName)">@language.Name</a>
}
</div>
</div>
}
@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;
}
}

View File

@@ -6,22 +6,29 @@
@inject ILocalizationCookieService LocalizationCookieService
@inject NavigationManager NavigationManager
@if (_supportedCultures?.Count() > 1)
@if (PageState.Site.Languages.Count() > 1)
{
<div class="app-languages btn-group pe-1" role="group">
<button id="btnCultures" type="button" class="btn @ButtonClass dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="oi oi-globe"></span>
</button>
<div class="dropdown-menu @MenuAlignment" aria-labelledby="btnCultures">
@foreach (var culture in _supportedCultures)
@foreach (var language in PageState.Site.Languages)
{
@if (PageState.RenderMode == RenderModes.Interactive)
@if (_contentLocalization)
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(culture.Name))" @onclick:preventDefault="true">@culture.DisplayName</a>
<a class="dropdown-item" href="@(PageState.Alias.Protocol + language.AliasName)">@language.Name</a>
}
else
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + culture.Name)" data-enhance-nav="false">@culture.DisplayName</a>
@if (PageState.RenderMode == RenderModes.Interactive)
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == language.Code ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(language.Code))" @onclick:preventDefault="true">@language.Name</a>
}
else
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == language.Code ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + language.Code)" data-enhance-nav="false">@language.Name</a>
}
}
}
</div>
@@ -29,7 +36,7 @@
}
@code{
private IEnumerable<Culture> _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);
}

View File

@@ -11,7 +11,6 @@
<Search CssClass="me-3 text-center bg-primary" />
<UserProfile ShowRegister="@_register" />
<Login ShowLogin="@_login" />
<LanguageSelector />
<ControlPanel LanguageDropdownAlignment="right" />
</div>
</div>

View File

@@ -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/<controller>
// GET: api/<controller>?siteid=x
[HttpGet]
[Authorize(Roles = RoleNames.Host)]
public IEnumerable<SiteGroupDefinition> Get()
[Authorize(Roles = RoleNames.Admin)]
public IEnumerable<SiteGroupDefinition> 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/<controller>/5
@@ -74,11 +89,17 @@ namespace Oqtane.Controllers
// PUT api/<controller>/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);

View File

@@ -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

View File

@@ -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;
}