Merge remote-tracking branch 'oqtane/dev' into dev

This commit is contained in:
Mark Davis
2024-07-05 21:53:14 -07:00
183 changed files with 5353 additions and 2178 deletions

26
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,26 @@
---
name: Bug Report
about: Create a bug report to help us improve the product
title: "[BUG] "
labels: ''
assignees: ''
---
### Oqtane Info
Version - #.#.#
Render Mode - Static
Interactivity - Server
Database - SQL Server
### Describe the bug
### Expected Behavior
### Steps To Reproduce
### Anything else?

View File

@ -0,0 +1,20 @@
---
name: Enhancement Request
about: 'Suggest a product enhancement '
title: "[ENH] "
labels: ''
assignees: ''
---
### Oqtane Info
Version - #.#.#
Render Mode - Static
Interactivity - Server
Database - SQL Server
### Describe the enhancement
### Anything else?

View File

@ -170,6 +170,7 @@
try try
{ {
Folder folder; Folder folder;
if (_folderId != -1) if (_folderId != -1)
{ {
folder = await FolderService.GetFolderAsync(_folderId); folder = await FolderService.GetFolderAsync(_folderId);
@ -179,8 +180,6 @@
folder = new Folder(); folder = new Folder();
} }
folder.SiteId = PageState.Site.SiteId;
if (_parentId == -1) if (_parentId == -1)
{ {
folder.ParentId = null; folder.ParentId = null;
@ -190,6 +189,14 @@
folder.ParentId = _parentId; folder.ParentId = _parentId;
} }
// check for duplicate folder names
if (_folders.Any(item => item.ParentId == folder.ParentId && item.Name == _name && item.FolderId != _folderId))
{
AddModuleMessage(Localizer["Message.Folder.Duplicate"], MessageType.Warning);
return;
}
folder.SiteId = PageState.Site.SiteId;
folder.Name = _name; folder.Name = _name;
folder.Type = _type; folder.Type = _type;
folder.ImageSizes = _imagesizes; folder.ImageSizes = _imagesizes;

View File

@ -26,8 +26,8 @@ else
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.JobId.ToString())" ResourceKey="EditJob" /></td> <td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.JobId.ToString())" ResourceKey="EditJob" /></td>
<td><ActionLink Action="Log" Class="btn btn-secondary" Parameters="@($"id=" + context.JobId.ToString())" ResourceKey="JobLog" /></td> <td><ActionLink Action="Log" Text="Log" Class="btn btn-secondary" Parameters="@($"id=" + context.JobId.ToString())" ResourceKey="JobLog" /></td>
<td>@context.Name</td> <td>@context.Name</td>
<td>@DisplayStatus(context.IsEnabled, context.IsExecuting)</td> <td>@DisplayStatus(context.IsEnabled, context.IsExecuting)</td>
<td>@DisplayFrequency(context.Interval, context.Frequency)</td> <td>@DisplayFrequency(context.Interval, context.Frequency)</td>

View File

@ -63,7 +63,7 @@ else
<th>@Localizer["Function"]</th> <th>@Localizer["Function"]</th>
</Header> </Header>
<Row> <Row>
<td class="@GetClass(context.Function)"><ActionLink Action="Detail" Parameters="@($"/{context.LogId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_level, _function, _rows, _page)))" ResourceKey="LogDetails" /></td> <td class="@GetClass(context.Function)"><ActionLink Action="Detail" Text="Details" Parameters="@($"/{context.LogId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_level, _function, _rows, _page)))" ResourceKey="LogDetails" /></td>
<td class="@GetClass(context.Function)">@context.LogDate</td> <td class="@GetClass(context.Function)">@context.LogDate</td>
<td class="@GetClass(context.Function)">@context.Level</td> <td class="@GetClass(context.Function)">@context.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td> <td class="@GetClass(context.Function)">@context.Feature</td>
@ -86,47 +86,48 @@ else
</div> </div>
</div> </div>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
<ActionDialog Header="Clear Events" Message="Are You Sure You Wish To Remove All Log Events?" Action="DeleteLogs" Class="btn btn-danger" OnClick="@(async () => await DeleteLogs())" ResourceKey="DeleteLogs" />
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
} }
@code { @code {
private string _level = "-"; private string _level = "-";
private string _function = "-"; private string _function = "-";
private string _rows = "10"; private string _rows = "10";
private int _page = 1; private int _page = 1;
private List<Log> _logs; private List<Log> _logs;
private int _retention = 30; private int _retention = 30;
public override string UrlParametersTemplate => "/{level}/{function}/{rows}/{page}"; public override string UrlParametersTemplate => "/{level}/{function}/{rows}/{page}";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
try try
{ {
if (UrlParameters.ContainsKey("level")) if (UrlParameters.ContainsKey("level"))
{ {
_level = UrlParameters["level"]; _level = UrlParameters["level"];
} }
if (UrlParameters.ContainsKey("function")) if (UrlParameters.ContainsKey("function"))
{ {
_function = UrlParameters["function"]; _function = UrlParameters["function"];
} }
if (UrlParameters.ContainsKey("rows")) if (UrlParameters.ContainsKey("rows"))
{ {
_rows = UrlParameters["rows"]; _rows = UrlParameters["rows"];
} }
if (UrlParameters.ContainsKey("page") && int.TryParse(UrlParameters["page"], out int page)) if (UrlParameters.ContainsKey("page") && int.TryParse(UrlParameters["page"], out int page))
{ {
_page = page; _page = page;
} }
await GetLogs(); await GetLogs();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = int.Parse( SettingService.GetSetting(settings, "LogRetention", "30")); _retention = int.Parse( SettingService.GetSetting(settings, "LogRetention", "30"));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -213,22 +214,37 @@ else
return classname; return classname;
} }
private async Task SaveSiteSettings() private async Task SaveSiteSettings()
{ {
try try
{ {
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "LogRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "LogRetention", _retention.ToString(), true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message); await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error); AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
} }
} }
private async Task DeleteLogs()
{
try
{
await LogService.DeleteLogsAsync(PageState.Site.SiteId);
await GetLogs();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Logs {Error}", ex.Message);
AddModuleMessage(Localizer["Error.DeleteLogs"], MessageType.Error);
}
}
private void OnPageChange(int page) private void OnPageChange(int page)
{ {

View File

@ -32,7 +32,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="categories" HelpText="Comma delimited list of module categories" ResourceKey="Categories">Categories: </Label> <Label Class="col-sm-3" For="categories" HelpText="Comma delimited list of module categories" ResourceKey="Categories">Categories: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="categories" class="form-control" @bind="@_categories" maxlength="200" required /> <input id="categories" class="form-control" @bind="@_categories" maxlength="200" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -306,10 +306,10 @@
_languages = _languages.OrderBy(item => item.Name).ToList(); _languages = _languages.OrderBy(item => item.Name).ToList();
} }
// Group modules by PageId // get distinct pages where module exists
// Get distinct PageIds where modules are present var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
var distinctPageIds = PageState.Modules var distinctPageIds = modules
.Where(md => md.ModuleDefinition.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false) .Where(md => md.ModuleDefinition?.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false)
.Select(md => md.PageId) .Select(md => md.PageId)
.Distinct(); .Distinct();

View File

@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.ModuleDefinitions @namespace Oqtane.Modules.Admin.ModuleDefinitions
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IModuleService ModuleService
@inject IModuleDefinitionService ModuleDefinitionService @inject IModuleDefinitionService ModuleDefinitionService
@inject IPackageService PackageService @inject IPackageService PackageService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@ -50,7 +51,7 @@ else
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.ModuleDefinitionId.ToString())" ResourceKey="EditModule" /></td> <td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.ModuleDefinitionId.ToString())" ResourceKey="EditModule" /></td>
<td> <td>
@if (context.AssemblyName != Constants.ClientId) @if (context.AssemblyName != Constants.ClientId)
{ {
@ -70,7 +71,7 @@ else
} }
</td> </td>
<td> <td>
@if (context.AssemblyName == Constants.ClientId || PageState.Modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null) @if (context.AssemblyName == Constants.ClientId || _modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null)
{ {
<span>@SharedLocalizer["Yes"]</span> <span>@SharedLocalizer["Yes"]</span>
} }
@ -99,6 +100,7 @@ else
} }
@code { @code {
private List<Module> _modules;
private List<ModuleDefinition> _allModuleDefinitions; private List<ModuleDefinition> _allModuleDefinitions;
private List<ModuleDefinition> _moduleDefinitions; private List<ModuleDefinition> _moduleDefinitions;
private List<Package> _packages; private List<Package> _packages;
@ -111,6 +113,7 @@ else
{ {
try try
{ {
_modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
_allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId); _allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',')).Distinct().ToList(); _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',')).Distinct().ToList();
await LoadModuleDefinitions(); await LoadModuleDefinitions();

View File

@ -94,7 +94,7 @@
</div> </div>
} }
</TabPanel> </TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions"> <TabPanel Name="Permissions" Heading="Permissions" ResourceKey="Permissions">
@if (_permissions != null) @if (_permissions != null)
{ {
<div class="container"> <div class="container">
@ -126,9 +126,8 @@
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo> <AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form> </form>
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Module Settings";
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
@ -144,7 +143,7 @@
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
private Type _moduleSettingsType; private Type _moduleSettingsType;
private object _moduleSettings; private object _moduleSettings;
private string _moduleSettingsTitle = "Module Settings"; private string _moduleSettingsTitle;
private RenderFragment ModuleSettingsComponent { get; set; } private RenderFragment ModuleSettingsComponent { get; set; }
private Type _containerSettingsType; private Type _containerSettingsType;
private object _containerSettings; private object _containerSettings;
@ -158,8 +157,10 @@
protected override void OnInitialized() protected override void OnInitialized()
{ {
SetModuleTitle(Localizer["ModuleSettings.Title"]);
_module = ModuleState.ModuleDefinition.Name; _module = ModuleState.ModuleDefinition.Name;
_title = ModuleState.Title; _title = ModuleState.Title;
_moduleSettingsTitle = Localizer["ModuleSettings.Heading"];
_pane = ModuleState.Pane; _pane = ModuleState.Pane;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType); _containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType; _containerType = ModuleState.ContainerType;
@ -173,6 +174,7 @@
_effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate); _effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate); _expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate);
if (ModuleState.ModuleDefinition != null) if (ModuleState.ModuleDefinition != null)
{ {
_permissionNames = ModuleState.ModuleDefinition?.PermissionNames; _permissionNames = ModuleState.ModuleDefinition?.PermissionNames;

View File

@ -198,12 +198,6 @@
</div> </div>
</div> </div>
</TabPanel> </TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading=@Localizer["Theme.Heading"] ResourceKey="ThemeSettings">
@_themeSettingsComponent
</TabPanel>
}
</TabStrip> </TabStrip>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
@ -238,9 +232,6 @@
private string _bodycontent; private string _bodycontent;
private string _permissions = null; private string _permissions = null;
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
private Type _themeSettingsType;
private object _themeSettings;
private RenderFragment _themeSettingsComponent { get; set; }
private bool _refresh = false; private bool _refresh = false;
protected Page _parent = null; protected Page _parent = null;
protected Dictionary<string, string> _icons; protected Dictionary<string, string> _icons;
@ -281,7 +272,6 @@
} }
_effectivedate = Utilities.UtcAsLocalDate(PageState.Page.EffectiveDate); _effectivedate = Utilities.UtcAsLocalDate(PageState.Page.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(PageState.Page.ExpiryDate); _expirydate = Utilities.UtcAsLocalDate(PageState.Page.ExpiryDate);
ThemeSettings();
_initialized = true; _initialized = true;
} }
else else
@ -324,7 +314,6 @@
_themetype = (string)e.Value; _themetype = (string)e.Value;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = _containers.First().TypeName; _containertype = _containers.First().TypeName;
ThemeSettings();
StateHasChanged(); StateHasChanged();
// if theme chosen is different than default site theme, display warning message to user // if theme chosen is different than default site theme, display warning message to user
@ -334,28 +323,6 @@
} }
} }
private void ThemeSettings()
{
_themeSettingsType = null;
_themeSettingsComponent = null;
var theme = PageState.Site.Themes.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype)));
if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
{
_themeSettingsType = Type.GetType(theme.ThemeSettingsType);
if (_themeSettingsType != null)
{
_themeSettingsComponent = builder =>
{
builder.OpenComponent(0, _themeSettingsType);
builder.AddAttribute(1, "RenderModeBoundary", RenderModeBoundary);
builder.AddComponentReferenceCapture(2, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); });
builder.CloseComponent();
};
}
_refresh = true;
}
}
private async Task SavePage() private async Task SavePage()
{ {
validated = true; validated = true;
@ -385,45 +352,34 @@
page.ParentId = Int32.Parse(_parentid); page.ParentId = Int32.Parse(_parentid);
} }
// path can be a link to an external url if (string.IsNullOrEmpty(_path))
if (!_path.Contains("://"))
{ {
if (string.IsNullOrEmpty(_path)) _path = _name;
{ }
_path = _name;
}
(_path, string parameters) = Utilities.ParsePath(_path); if (_path.Contains("/"))
{
if (_path.Contains("/")) if (_path.EndsWith("/") && _path != "/")
{ {
if (_path.EndsWith("/") && _path != "/") _path = _path.Substring(0, _path.Length - 1);
{
_path = _path.Substring(0, _path.Length - 1);
}
_path = _path.Substring(_path.LastIndexOf("/") + 1);
} }
if (_parentid == "-1") _path = _path.Substring(_path.LastIndexOf("/") + 1);
{ }
page.Path = Utilities.GetFriendlyUrl(_path); if (_parentid == "-1")
} {
else page.Path = Utilities.GetFriendlyUrl(_path);
{
Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == page.ParentId);
if (parent.Path == string.Empty)
{
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
}
else
{
page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
}
}
page.Path += parameters;
} }
else else
{ {
page.Path = _path; Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == page.ParentId);
if (parent.Path == string.Empty)
{
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
}
else
{
page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
}
} }
var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
@ -493,18 +449,11 @@
await logger.LogInformation("Page Added {Page}", page); await logger.LogInformation("Page Added {Page}", page);
if (!string.IsNullOrEmpty(PageState.ReturnUrl)) if (!string.IsNullOrEmpty(PageState.ReturnUrl))
{ {
NavigationManager.NavigateTo(PageState.ReturnUrl, true); NavigationManager.NavigateTo(NavigateUrl(page.Path), true); // redirect to page added and reload
} }
else else
{ {
if (!page.Path.Contains("://")) NavigationManager.NavigateTo(NavigateUrl()); // redirect to page management
{
NavigationManager.NavigateTo(page.Path); // redirect to new page created
}
else
{
NavigationManager.NavigateTo(NavigateUrl("admin/pages"));
}
} }
} }
else else

View File

@ -1,5 +1,6 @@
@namespace Oqtane.Modules.Admin.Pages @namespace Oqtane.Modules.Admin.Pages
@using Oqtane.Interfaces @using Oqtane.Interfaces
@using System.Globalization
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IPageService PageService @inject IPageService PageService
@ -362,7 +363,7 @@
_parent = PageState.Pages.FirstOrDefault(item => item.PageId == _page.ParentId); _parent = PageState.Pages.FirstOrDefault(item => item.PageId == _page.ParentId);
} }
_children = new List<Page>(); _children = new List<Page>();
foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) foreach (Page p in PageState.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 && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
@ -380,7 +381,7 @@
} }
else else
{ {
if (_path.Contains("/") & !_path.Contains("://")) if (_path.Contains("/"))
{ {
_path = _path.Substring(_path.LastIndexOf("/") + 1); _path = _path.Substring(_path.LastIndexOf("/") + 1);
} }
@ -414,7 +415,7 @@
_permissions = _page.PermissionList; _permissions = _page.PermissionList;
// page modules // page modules
_pageModules = PageState.Modules.Where(m => m.PageId == _page.PageId).ToList(); _pageModules = PageState.Modules;
// audit // audit
_createdby = _page.CreatedBy; _createdby = _page.CreatedBy;
@ -529,45 +530,34 @@
_page.ParentId = Int32.Parse(_parentid); _page.ParentId = Int32.Parse(_parentid);
} }
// path can be a link to an external url if (string.IsNullOrEmpty(_path))
if (!_path.Contains("://"))
{ {
if (string.IsNullOrEmpty(_path)) _path = _name;
{ }
_path = _name;
}
(_path, string parameters) = Utilities.ParsePath(_path); if (_path.Contains("/"))
{
if (_path.Contains("/")) if (_path.EndsWith("/") && _path != "/")
{ {
if (_path.EndsWith("/") && _path != "/") _path = _path.Substring(0, _path.Length - 1);
{
_path = _path.Substring(0, _path.Length - 1);
}
_path = _path.Substring(_path.LastIndexOf("/") + 1);
} }
if (_parentid == "-1") _path = _path.Substring(_path.LastIndexOf("/") + 1);
{ }
_page.Path = Utilities.GetFriendlyUrl(_path); if (_parentid == "-1")
} {
else _page.Path = Utilities.GetFriendlyUrl(_path);
{
Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == _page.ParentId);
if (parent.Path == string.Empty)
{
_page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
}
else
{
_page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
}
}
_page.Path += parameters;
} }
else else
{ {
_page.Path = _path; Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == _page.ParentId);
if (parent.Path == string.Empty)
{
_page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
}
else
{
_page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
}
} }
var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
@ -654,18 +644,11 @@
await logger.LogInformation("Page Saved {Page}", _page); await logger.LogInformation("Page Saved {Page}", _page);
if (!string.IsNullOrEmpty(PageState.ReturnUrl)) if (!string.IsNullOrEmpty(PageState.ReturnUrl))
{ {
NavigationManager.NavigateTo(PageState.ReturnUrl, true); NavigationManager.NavigateTo(PageState.ReturnUrl, true); // redirect to page being edited and reload
} }
else else
{ {
if (!_page.Path.Contains("://")) NavigationManager.NavigateTo(NavigateUrl()); // redirect to page management
{
NavigationManager.NavigateTo(NavigateUrl(), true); // redirect to page being edited
}
else
{
NavigationManager.NavigateTo(NavigateUrl("admin/pages"));
}
} }
} }
else else

View File

@ -17,7 +17,7 @@
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.PageId.ToString())" ResourceKey="EditPage" /></td> <td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.PageId.ToString())" ResourceKey="EditPage" /></td>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td> <td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => NavigationManager.NavigateTo(Browse(context)))">@Localizer["Browse"]</button></td> <td><button type="button" class="btn btn-secondary" @onclick="@(async () => NavigationManager.NavigateTo(Browse(context)))">@Localizer["Browse"]</button></td>
<td>@(new string('-', context.Level * 2))@(context.Name)</td> <td>@(new string('-', context.Level * 2))@(context.Name)</td>

View File

@ -22,7 +22,7 @@ else
<th>@Localizer["Order"]</th> <th>@Localizer["Order"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.ProfileId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditProfile" /></td> <td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.ProfileId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditProfile" /></td>
<td><ActionDialog Header="Delete Profile" Message="@string.Format(Localizer["Confirm.Profile.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteProfile(context.ProfileId))" ResourceKey="DeleteProfile" /></td> <td><ActionDialog Header="Delete Profile" Message="@string.Format(Localizer["Confirm.Profile.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteProfile(context.ProfileId))" ResourceKey="DeleteProfile" /></td>
<td>@context.Name</td> <td>@context.Name</td>
<td>@context.Title</td> <td>@context.Title</td>

View File

@ -20,9 +20,9 @@ else
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" Disabled="@(context.IsSystem)" ResourceKey="Edit" /></td> <td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" Disabled="@(context.IsSystem)" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete Role" Message="@string.Format(Localizer["Confirm.DeleteUser"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td> <td><ActionDialog Header="Delete Role" Message="@string.Format(Localizer["Confirm.DeleteUser"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td>
<td><ActionLink Action="Users" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Users" /></td> <td><ActionLink Action="Users" Text="Users" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Users" /></td>
<td>@context.Name</td> <td>@context.Name</td>
</Row> </Row>
</Pager> </Pager>

View File

@ -0,0 +1,116 @@
@using Oqtane.Services
@using System.Net
@namespace Oqtane.Modules.Admin.SearchResults
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ISearchResultsService SearchResultsService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="search-result-container">
<div class="row">
<div class="col">
<form method="post" @formname="SearchInputForm" @onsubmit="Search" data-enhance>
<div class="input-group mb-3">
<span class="input-group-text">@Localizer["SearchLabel"]</span>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="text" name="keywords" class="form-control shadow-none" maxlength="50"
aria-label="Keywords"
placeholder="@Localizer["SearchPlaceholder"]"
@bind="@_keywords">
<button class="btn btn-primary" type="submit">@SharedLocalizer["Search"]</button>
<a class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Reset"]</a>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-3">
@if (_loading)
{
<div class="app-progress-indicator"></div>
}
else
{
@if (_searchResults != null && _searchResults.Results != null)
{
if (_searchResults.Results.Any())
{
<Pager Items="@_searchResults?.Results"
Format="Grid"
Columns="1"
Toolbar="Bottom"
Parameters="@($"q={_keywords}")">
<Row>
<div class="search-item">
<h4 class="mb-1"><a href="@context.Url">@context.Title</a></h4>
<p class="mb-0 text-muted">@((MarkupString)context.Snippet)</p>
</div>
</Row>
</Pager>
}
else
{
<div class="alert alert-info show mt-3" role="alert">
@Localizer["NoResult"]
</div>
}
}
<div class="clearfix"></div>
}
</div>
</div>
</div>
@code {
public override string RenderMode => RenderModes.Static;
private SearchSortDirections _searchSortDirection = SearchSortDirections.Descending; //default sort by
private SearchSortFields _searchSortField = SearchSortFields.Relevance;
private string _keywords;
private bool _loading;
private SearchResults _searchResults;
[SupplyParameterFromForm(FormName = "SearchInputForm")]
public string KeyWords { get => ""; set => _keywords = value; }
protected override async Task OnInitializedAsync()
{
if (PageState.QueryString.ContainsKey("q"))
{
_keywords = WebUtility.UrlDecode(PageState.QueryString["q"]);
}
if (!string.IsNullOrEmpty(_keywords))
{
await PerformSearch();
}
}
private void Search()
{
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, $"page=1&q={_keywords}"));
}
private async Task PerformSearch()
{
_loading = true;
StateHasChanged();
var searchQuery = new SearchQuery
{
SiteId = PageState.Site.SiteId,
Alias = PageState.Alias,
User = PageState.User,
Keywords = _keywords,
SortDirection = _searchSortDirection,
SortField = _searchSortField,
PageIndex = 0,
PageSize = int.MaxValue
};
_searchResults = await SearchResultsService.SearchAsync(ModuleState.ModuleId, searchQuery);
_loading = false;
StateHasChanged();
}
}

View File

@ -0,0 +1,23 @@
using System.Collections.Generic;
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.SearchResults
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Search Results",
Description = "Display Search Results",
Version = Constants.Version,
Categories = "Admin",
Resources = new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" }
}
};
}
}

View File

@ -319,7 +319,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="rendermode" HelpText="The default render mode for the site" ResourceKey="Rendermode">Render Mode: </Label> <Label Class="col-sm-3" For="rendermode" HelpText="The default render mode for the site" ResourceKey="Rendermode">Render Mode: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="rendermode" class="form-select" @bind="@_rendermode" required> <select id="rendermode" class="form-select" value="@_rendermode" @onchange="(e => RenderModeChanged(e))" required>
<option value="@RenderModes.Interactive">@(SharedLocalizer["RenderMode" + @RenderModes.Interactive])</option> <option value="@RenderModes.Interactive">@(SharedLocalizer["RenderMode" + @RenderModes.Interactive])</option>
<option value="@RenderModes.Static">@(SharedLocalizer["RenderMode" + @RenderModes.Static])</option> <option value="@RenderModes.Static">@(SharedLocalizer["RenderMode" + @RenderModes.Static])</option>
<option value="@RenderModes.Headless">@(SharedLocalizer["RenderMode" + @RenderModes.Headless])</option> <option value="@RenderModes.Headless">@(SharedLocalizer["RenderMode" + @RenderModes.Headless])</option>
@ -337,7 +337,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="prerender" HelpText="Specifies if interactive components should prerender their output" ResourceKey="Prerender">Prerender? </Label> <Label Class="col-sm-3" For="prerender" HelpText="Specifies if interactive components should prerender their output on the server" ResourceKey="Prerender">Prerender: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="prerender" class="form-select" @bind="@_prerender" required> <select id="prerender" class="form-select" @bind="@_prerender" required>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
@ -572,6 +572,23 @@
} }
} }
private void RenderModeChanged(ChangeEventArgs e)
{
_rendermode = (string)e.Value;
switch (_rendermode)
{
case RenderModes.Interactive:
_prerender = "True";
break;
case RenderModes.Static:
_prerender = "False";
break;
case RenderModes.Headless:
_prerender = "False";
break;
}
}
private async Task SaveSite() private async Task SaveSite()
{ {
validated = true; validated = true;

View File

@ -26,6 +26,12 @@
<input id="osversion" class="form-control" @bind="@_osversion" readonly /> <input id="osversion" class="form-control" @bind="@_osversion" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="process" HelpText="Indicates if the current process is 32 bit or 64 bit" ResourceKey="Process">Process: </Label>
<div class="col-sm-9">
<input id="process" class="form-control" @bind="@_process" readonly />
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="machinename" HelpText="Machine Name" ResourceKey="MachineName">Machine Name: </Label> <Label Class="col-sm-3" For="machinename" HelpText="Machine Name" ResourceKey="MachineName">Machine Name: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -62,12 +68,6 @@
<input id="servertime" class="form-control" @bind="@_servertime" readonly /> <input id="servertime" class="form-control" @bind="@_servertime" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tickcount" HelpText="Amount Of Time The Service Has Been Available And Operational" ResourceKey="TickCount">Service Uptime: </Label>
<div class="col-sm-9">
<input id="tickcount" class="form-control" @bind="@_tickcount" readonly />
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="workingset" HelpText="Memory Allocation Of Service (in MB)" ResourceKey="WorkingSet">Memory Allocation: </Label> <Label Class="col-sm-3" For="workingset" HelpText="Memory Allocation Of Service (in MB)" ResourceKey="WorkingSet">Memory Allocation: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -165,13 +165,13 @@
private string _version = string.Empty; private string _version = string.Empty;
private string _clrversion = string.Empty; private string _clrversion = string.Empty;
private string _osversion = string.Empty; private string _osversion = string.Empty;
private string _machinename = string.Empty; private string _process = string.Empty;
private string _machinename = string.Empty;
private string _ipaddress = string.Empty; private string _ipaddress = string.Empty;
private string _environment = string.Empty; private string _environment = string.Empty;
private string _contentrootpath = string.Empty; private string _contentrootpath = string.Empty;
private string _webrootpath = string.Empty; private string _webrootpath = string.Empty;
private string _servertime = string.Empty; private string _servertime = string.Empty;
private string _tickcount = string.Empty;
private string _workingset = string.Empty; private string _workingset = string.Empty;
private string _installationid = string.Empty; private string _installationid = string.Empty;
@ -192,13 +192,13 @@
{ {
_clrversion = systeminfo["CLRVersion"].ToString(); _clrversion = systeminfo["CLRVersion"].ToString();
_osversion = systeminfo["OSVersion"].ToString(); _osversion = systeminfo["OSVersion"].ToString();
_machinename = systeminfo["MachineName"].ToString(); _process = systeminfo["Process"].ToString();
_machinename = systeminfo["MachineName"].ToString();
_ipaddress = systeminfo["IPAddress"].ToString(); _ipaddress = systeminfo["IPAddress"].ToString();
_environment = systeminfo["Environment"].ToString(); _environment = systeminfo["Environment"].ToString();
_contentrootpath = systeminfo["ContentRootPath"].ToString(); _contentrootpath = systeminfo["ContentRootPath"].ToString();
_webrootpath = systeminfo["WebRootPath"].ToString(); _webrootpath = systeminfo["WebRootPath"].ToString();
_servertime = systeminfo["ServerTime"].ToString() + " UTC"; _servertime = systeminfo["ServerTime"].ToString() + " UTC";
_tickcount = TimeSpan.FromMilliseconds(Convert.ToInt64(systeminfo["TickCount"].ToString())).ToString();
_workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB"; _workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB";
} }

View File

@ -29,7 +29,7 @@ else
<th>&nbsp;</th> <th>&nbsp;</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.ThemeId.ToString())" ResourceKey="EditTheme" /></td> <td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.ThemeId.ToString())" ResourceKey="EditTheme" /></td>
<td> <td>
@if (context.AssemblyName != Constants.ClientId) @if (context.AssemblyName != Constants.ClientId)
{ {
@ -63,7 +63,6 @@ else
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadTheme(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button> <button type="button" class="btn btn-success" @onclick=@(async () => await DownloadTheme(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button>
} }
</td> </td>
<td></td>
</Row> </Row>
</Pager> </Pager>
} }

View File

@ -37,7 +37,7 @@ else
<th>@Localizer["Requested"]</th> <th>@Localizer["Requested"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.UrlMappingId.ToString())" ResourceKey="Edit" /></td> <td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UrlMappingId.ToString())" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete Url Mapping" Message="@string.Format(Localizer["Confirm.DeleteUrlMapping"], context.Url)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUrlMapping(context))" ResourceKey="DeleteUrlMapping" /></td> <td><ActionDialog Header="Delete Url Mapping" Message="@string.Format(Localizer["Confirm.DeleteUrlMapping"], context.Url)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUrlMapping(context))" ResourceKey="DeleteUrlMapping" /></td>
<td> <td>
<a href="@Utilities.TenantUrl(PageState.Alias, context.Url)">@context.Url</a> <a href="@Utilities.TenantUrl(PageState.Alias, context.Url)">@context.Url</a>

View File

@ -226,11 +226,6 @@
user.Password = _password; user.Password = _password;
user.Email = email; user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.PhotoFileId = null;
if (user.PhotoFileId == -1)
{
user.PhotoFileId = null;
}
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));

View File

@ -32,13 +32,13 @@ else
</Header> </Header>
<Row> <Row>
<td> <td>
<ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" /> <ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" />
</td> </td>
<td> <td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" /> <ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" />
</td> </td>
<td> <td>
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" /> <ActionLink Action="Roles" Text="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" />
</td> </td>
<td>@context.User.Username</td> <td>@context.User.Username</td>
<td>@context.User.DisplayName</td> <td>@context.User.DisplayName</td>

View File

@ -43,7 +43,7 @@ else
<th>@Localizer["Created"]</th> <th>@Localizer["Created"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Detail" Parameters="@($"id={context.VisitorId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, $"type={_type}&days={_days}&page={_page}"))" ResourceKey="Details" /></td> <td><ActionLink Action="Detail" Text="Detail" Parameters="@($"id={context.VisitorId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, $"type={_type}&days={_days}&page={_page}"))" ResourceKey="Details" /></td>
<td>@context.IPAddress</td> <td>@context.IPAddress</td>
<td> <td>
@if (context.UserId != null) @if (context.UserId != null)
@ -69,14 +69,20 @@ else
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="duration" HelpText="The duration of a browsing session considered to be a distinct visit (in minutes)" ResourceKey="Duration">Session Duration: </Label>
<div class="col-sm-9">
<input id="duration" class="form-control" type="number" min="0" step="1" @bind="@_duration" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="filter" HelpText="Comma delimited list of terms which may exist in IP addresses, user agents, or languages identifying visitors which should not be tracked" ResourceKey="Filter">Filter: </Label> <Label Class="col-sm-3" For="filter" HelpText="Comma delimited list of terms which may exist in IP addresses, user agents, or languages identifying visitors which should not be tracked" ResourceKey="Filter">Filter: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="filter" class="form-control" @bind="@_filter" rows="3"></textarea> <textarea id="filter" class="form-control" @bind="@_filter" rows="3"></textarea>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of visitor activity to retain" ResourceKey="Retention">Retention (Days): </Label> <Label Class="col-sm-3" For="retention" HelpText="Number of days of visitor activity to retain" ResourceKey="Retention">Retention: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" /> <input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
</div> </div>
@ -103,7 +109,8 @@ else
private int _page = 1; private int _page = 1;
private List<Visitor> _visitors; private List<Visitor> _visitors;
private string _tracking; private string _tracking;
private string _filter = ""; private int _duration = 5;
private string _filter = "";
private int _retention = 30; private int _retention = 30;
private string _correlation = "true"; private string _correlation = "true";
@ -128,7 +135,8 @@ else
_tracking = PageState.Site.VisitorTracking.ToString(); _tracking = PageState.Site.VisitorTracking.ToString();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter); _duration = int.Parse(SettingService.GetSetting(settings, "VisitorDuration", "5"));
_filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter);
_retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30")); _retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30"));
_correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true"); _correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true");
} }
@ -179,7 +187,8 @@ else
await SiteService.UpdateSiteAsync(site); await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true); settings = SettingService.SetSetting(settings, "VisitorDuration", _duration.ToString(), true);
settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true);
settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true);
settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true); settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);

View File

@ -35,11 +35,11 @@
{ {
if (Disabled) if (Disabled)
{ {
<button type="button" class="@Class" disabled>@((MarkupString)_iconSpan) @Text</button> <button type="button" class="@Class" disabled>@((MarkupString)_openIconSpan) @_openText</button>
} }
else else
{ {
<button type="button" class="@Class" @onclick="DisplayModal">@((MarkupString)_iconSpan) @Text</button> <button type="button" class="@Class" @onclick="DisplayModal">@((MarkupString)_openIconSpan) @_openText</button>
} }
} }
} }
@ -54,7 +54,7 @@ else
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@Header</h5> <h5 class="modal-title">@Header</h5>
<form method="post" @formname="@($"ActionDialogCloseForm{Id}")" @onsubmit="DisplayModal" data-enhance> <form method="post" @formname="@($"ActionDialogCloseForm{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="btn-close" aria-label="Close"></button> <button type="submit" class="btn-close" aria-label="Close"></button>
</form> </form>
</div> </div>
@ -65,12 +65,12 @@ else
@if (!string.IsNullOrEmpty(Action)) @if (!string.IsNullOrEmpty(Action))
{ {
<form method="post" @formname="@($"ActionDialogConfirmForm{Id}")" @onsubmit="Confirm" data-enhance> <form method="post" @formname="@($"ActionDialogConfirmForm{Id}")" @onsubmit="Confirm" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_iconSpan) @Text</button> <button type="submit" class="@Class">@((MarkupString)_iconSpan) @Text</button>
</form> </form>
} }
<form method="post" @formname="@($"ActionDialogCancelForm{Id}")" @onsubmit="DisplayModal" data-enhance> <form method="post" @formname="@($"ActionDialogCancelForm{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="btn btn-secondary">@SharedLocalizer["Cancel"]</button> <button type="submit" class="btn btn-secondary">@SharedLocalizer["Cancel"]</button>
</form> </form>
</div> </div>
@ -83,13 +83,13 @@ else
{ {
if (Disabled) if (Disabled)
{ {
<button type="button" class="@Class" disabled>@((MarkupString)_iconSpan) @Text</button> <button type="button" class="@Class" disabled>@((MarkupString)_openIconSpan) @_openText</button>
} }
else else
{ {
<form method="post" @formname="@($"ActionDialogActionForm{Id}")" @onsubmit="DisplayModal" data-enhance> <form method="post" @formname="@($"ActionDialogActionForm{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_iconSpan) @Text</button> <button type="submit" class="@Class">@((MarkupString)_openIconSpan) @_openText</button>
</form> </form>
} }
} }
@ -101,6 +101,8 @@ else
private bool _editmode = false; private bool _editmode = false;
private bool _authorized = false; private bool _authorized = false;
private string _iconSpan = string.Empty; private string _iconSpan = string.Empty;
private string _openIconSpan = string.Empty;
private string _openText = string.Empty;
[Parameter] [Parameter]
public string Header { get; set; } // required public string Header { get; set; } // required
@ -138,6 +140,9 @@ else
[Parameter] [Parameter]
public string IconName { get; set; } // optional - specifies an icon for the link - default is no icon public string IconName { get; set; } // optional - specifies an icon for the link - default is no icon
[Parameter]
public bool IconOnly { get; set; } // optional - specifies only icon in opening link
[Parameter] [Parameter]
public string Id { get; set; } // optional - specifies a unique id for the compoment - required when there are multiple component instances on a page in static rendering public string Id { get; set; } // optional - specifies a unique id for the compoment - required when there are multiple component instances on a page in static rendering
@ -157,6 +162,8 @@ else
{ {
Text = Action; Text = Action;
} }
_openText = Text;
if (string.IsNullOrEmpty(Class)) if (string.IsNullOrEmpty(Class))
{ {
Class = "btn btn-success"; Class = "btn btn-success";
@ -169,11 +176,20 @@ else
if (!string.IsNullOrEmpty(IconName)) if (!string.IsNullOrEmpty(IconName))
{ {
if (!IconName.Contains(" ")) if (IconOnly)
{
_openText = string.Empty;
}
// Check if IconName starts with "oi oi-"
bool startsWithOiOi = IconName.StartsWith("oi oi-");
if (!startsWithOiOi && !IconName.Contains(" "))
{ {
IconName = "oi oi-" + IconName; IconName = "oi oi-" + IconName;
} }
_iconSpan = $"<span class=\"{IconName}\"></span>&nbsp;"; _openIconSpan = $"<span class=\"{IconName}\"></span>{(IconOnly ? "" : "&nbsp")}";
_iconSpan = $"<span class=\"{IconName}\"></span>&nbsp";
} }
Text = Localize(nameof(Text), Text); Text = Localize(nameof(Text), Text);

View File

@ -145,7 +145,10 @@
if (!string.IsNullOrEmpty(IconName)) if (!string.IsNullOrEmpty(IconName))
{ {
if (!IconName.Contains(" ")) // Check if IconName starts with "oi oi-"
bool startsWithOiOi = IconName.StartsWith("oi oi-");
if (!startsWithOiOi && !IconName.Contains(" "))
{ {
IconName = "oi oi-" + IconName; IconName = "oi oi-" + IconName;
} }

View File

@ -6,23 +6,24 @@
{ {
<div class="@_classname alert-dismissible fade show mb-3" role="alert"> <div class="@_classname alert-dismissible fade show mb-3" role="alert">
@((MarkupString)Message) @((MarkupString)Message)
@if (PageState != null) @if (Type == MessageType.Error && PageState != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
@if (Type == MessageType.Error && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) <NavLink class="ms-2" href="@NavigateUrl("admin/log")">View Details</NavLink>
{ }
<NavLink class="ms-2" href="@NavigateUrl("admin/log")">View Details</NavLink> @if (ModuleState.RenderMode == RenderModes.Static)
} {
<form method="post" @onsubmit="DismissModal" @formname="@_formname" data-enhance> <a href="@NavigationManager.Uri" class="btn-close" data-dismiss="alert" aria-label="close"></a>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> }
<button type="submit" class="btn-close" aria-label="Close"></button> else
</form> {
<button type="button" class="btn-close" data-dismiss="alert" aria-label="close" @onclick="CloseMessage"></button>
} }
</div> </div>
} }
@code { @code {
private string _message = string.Empty;
private string _classname = string.Empty; private string _classname = string.Empty;
private string _formname = "ModuleMessageForm";
[Parameter] [Parameter]
public string Message { get; set; } public string Message { get; set; }
@ -30,32 +31,13 @@
[Parameter] [Parameter]
public MessageType Type { get; set; } public MessageType Type { get; set; }
public void RefreshMessage(string message, MessageType type) [Parameter]
{ public RenderModeBoundary Parent { get; set; }
Message = message;
Type = type;
UpdateClassName();
StateHasChanged();
}
protected override void OnInitialized()
{
if (ModuleState != null)
{
_formname += ModuleState.PageModuleId.ToString();
}
}
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
UpdateClassName(); _message = Message;
} if (!string.IsNullOrEmpty(_message))
private void UpdateClassName()
{
if (!string.IsNullOrEmpty(Message))
{ {
_classname = GetMessageType(Type); _classname = GetMessageType(Type);
} }
@ -82,9 +64,15 @@
return classname; return classname;
} }
private void CloseMessage(MouseEventArgs e)
private void DismissModal()
{ {
Message = ""; if(Parent != null)
{
Parent.DismissMessage();
}
else
{
NavigationManager.NavigateTo(NavigationManager.Uri);
}
} }
} }

View File

@ -11,7 +11,7 @@
@if (!string.IsNullOrEmpty(SearchProperties)) @if (!string.IsNullOrEmpty(SearchProperties))
{ {
<form autocomplete="off"> <form autocomplete="off">
<div class="input-group my-3"> <div class="input-group my-3 @SearchBoxClass">
<input type="text" id="pagersearch" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @bind="@_search" /> <input type="text" id="pagersearch" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @bind="@_search" />
<button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button> <button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button> <button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button>
@ -23,16 +23,16 @@
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li> </li>
@for (int i = _startPage; i <= _endPage; i++) @for (int i = _startPage; i <= _endPage; i++)
{ {
@ -40,30 +40,30 @@
if (pager == _page) if (pager == _page)
{ {
<li class="page-item app-pager-pointer active"> <li class="page-item app-pager-pointer active">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link shadow-none" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
else else
{ {
<li class="page-item app-pager-pointer"> <li class="page-item app-pager-pointer">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link shadow-none" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
} }
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a> <a class="page-link shadow-none" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li> </li>
</ul> </ul>
} }
@ -73,8 +73,8 @@
@if (!string.IsNullOrEmpty(SearchProperties)) @if (!string.IsNullOrEmpty(SearchProperties))
{ {
<form method="post" autocomplete="off" @formname="PagerForm" @onsubmit="Search" data-enhance> <form method="post" autocomplete="off" @formname="PagerForm" @onsubmit="Search" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<div class="input-group my-3"> <div class="input-group my-3 @SearchBoxClass">
<input type="text" id="pagersearch" name="_search" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @bind="@_search" /> <input type="text" id="pagersearch" name="_search" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @bind="@_search" />
<button type="submit" class="btn btn-primary">@SharedLocalizer["Search"]</button> <button type="submit" class="btn btn-primary">@SharedLocalizer["Search"]</button>
<a class="btn btn-secondary" href="@PageUrl(1, "")">@SharedLocalizer["Reset"]</a> <a class="btn btn-secondary" href="@PageUrl(1, "")">@SharedLocalizer["Reset"]</a>
@ -86,16 +86,16 @@
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(1, _search)"><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(1, _search)"><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_startPage - 1, _search)"><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(_startPage - 1, _search)"><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(((_page > 1) ? _page - 1 : _page), _search)"><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(((_page > 1) ? _page - 1 : _page), _search)"><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li> </li>
@for (int i = _startPage; i <= _endPage; i++) @for (int i = _startPage; i <= _endPage; i++)
{ {
@ -103,30 +103,30 @@
if (pager == _page) if (pager == _page)
{ {
<li class="page-item app-pager-pointer active"> <li class="page-item app-pager-pointer active">
<a class="page-link" href="@PageUrl(pager, _search)">@pager</a> <a class="page-link shadow-none" href="@PageUrl(pager, _search)">@pager</a>
</li> </li>
} }
else else
{ {
<li class="page-item app-pager-pointer"> <li class="page-item app-pager-pointer">
<a class="page-link" href="@PageUrl(pager, _search)">@pager</a> <a class="page-link shadow-none" href="@PageUrl(pager, _search)">@pager</a>
</li> </li>
} }
} }
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(((_page < _pages) ? _page + 1 : _page), _search)"><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(((_page < _pages) ? _page + 1 : _page), _search)"><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_endPage + 1, _search)"><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(_endPage + 1, _search)"><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_pages, _search)"><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(_pages, _search)"><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a> <a class="page-link shadow-none" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li> </li>
</ul> </ul>
} }
@ -202,16 +202,16 @@
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li> </li>
@for (int i = _startPage; i <= _endPage; i++) @for (int i = _startPage; i <= _endPage; i++)
{ {
@ -219,30 +219,30 @@
if (pager == _page) if (pager == _page)
{ {
<li class="page-item app-pager-pointer active"> <li class="page-item app-pager-pointer active">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link shadow-none" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
else else
{ {
<li class="page-item app-pager-pointer"> <li class="page-item app-pager-pointer">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link shadow-none" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
} }
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link shadow-none" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a> <a class="page-link shadow-none" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li> </li>
</ul> </ul>
} }
@ -250,16 +250,16 @@
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(1, _search)"><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(1, _search)"><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_startPage - 1, _search)"><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(_startPage - 1, _search)"><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(((_page > 1) ? _page - 1 : _page), _search)"><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(((_page > 1) ? _page - 1 : _page), _search)"><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li> </li>
@for (int i = _startPage; i <= _endPage; i++) @for (int i = _startPage; i <= _endPage; i++)
{ {
@ -267,30 +267,30 @@
if (pager == _page) if (pager == _page)
{ {
<li class="page-item app-pager-pointer active"> <li class="page-item app-pager-pointer active">
<a class="page-link" href="@PageUrl(pager, _search)">@pager</a> <a class="page-link shadow-none" href="@PageUrl(pager, _search)">@pager</a>
</li> </li>
} }
else else
{ {
<li class="page-item app-pager-pointer"> <li class="page-item app-pager-pointer">
<a class="page-link" href="@PageUrl(pager, _search)">@pager</a> <a class="page-link shadow-none" href="@PageUrl(pager, _search)">@pager</a>
</li> </li>
} }
} }
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(((_page < _pages) ? _page + 1 : _page), _search)"><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(((_page < _pages) ? _page + 1 : _page), _search)"><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_endPage + 1, _search)"><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(_endPage + 1, _search)"><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_pages, _search)"><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link shadow-none" href="@PageUrl(_pages, _search)"><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a> <a class="page-link shadow-none" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li> </li>
</ul> </ul>
} }
@ -359,6 +359,9 @@
[Parameter] [Parameter]
public string SearchProperties { get; set; } // comma delimited list of property names to include in search public string SearchProperties { get; set; } // comma delimited list of property names to include in search
[Parameter]
public string SearchBoxClass { get; set; } // class for Search box
[Parameter] [Parameter]
public string Parameters { get; set; } // optional - querystring parameters in the form of "id=x&name=y" used in static render mode public string Parameters { get; set; } // optional - querystring parameters in the form of "id=x&name=y" used in static render mode

View File

@ -4,11 +4,11 @@ using System.Threading.Tasks;
namespace Oqtane.Modules.Controls namespace Oqtane.Modules.Controls
{ {
public class RichTextEditorInterop public class QuillEditorInterop
{ {
private readonly IJSRuntime _jsRuntime; private readonly IJSRuntime _jsRuntime;
public RichTextEditorInterop(IJSRuntime jsRuntime) public QuillEditorInterop(IJSRuntime jsRuntime)
{ {
_jsRuntime = jsRuntime; _jsRuntime = jsRuntime;
} }
@ -105,13 +105,25 @@ namespace Oqtane.Modules.Controls
} }
} }
public Task InsertImage(ElementReference quillElement, string imageUrl, string altText) public ValueTask<int> GetCurrentCursor(ElementReference quillElement)
{
try
{
return _jsRuntime.InvokeAsync<int>("Oqtane.RichTextEditor.getCurrentCursor", quillElement);
}
catch
{
return new ValueTask<int>(Task.FromResult(0));
}
}
public Task InsertImage(ElementReference quillElement, string imageUrl, string altText, int editorIndex)
{ {
try try
{ {
_jsRuntime.InvokeAsync<object>( _jsRuntime.InvokeAsync<object>(
"Oqtane.RichTextEditor.insertQuillImage", "Oqtane.RichTextEditor.insertQuillImage",
quillElement, imageUrl, altText); quillElement, imageUrl, altText, editorIndex);
return Task.CompletedTask; return Task.CompletedTask;
} }
catch catch

View File

@ -0,0 +1,365 @@
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@implements ITextEditor
@inject IStringLocalizer<QuillTextEditor> Localizer
<div class="quill-text-editor">
<TabStrip ActiveTab="@_activetab">
@if (AllowRichText)
{
<TabPanel Name="Rich" Heading="Rich Text Editor" ResourceKey="RichTextEditor">
@if (_richfilemanager)
{
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button>
}
@if (_richfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRichFileManager">@Localizer["Close"]</button>
}
</div>
<div class="row">
<div class="col">
<div @ref="@_toolBar">
@if (ToolbarContent != null)
{
@ToolbarContent
}
else
{
<select class="ql-header">
<option selected=""></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
</select>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
</span>
}
</div>
<div @ref="@_editorElement"></div>
</div>
</div>
</TabPanel>
}
@if (AllowRawHtml)
{
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
@if (_rawfilemanager)
{
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRawImage">@Localizer["InsertImage"]</button>
}
@if (_rawfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRawFileManager">@Localizer["Close"]</button>
}
</div>
@if (ReadOnly)
{
<textarea id="@_rawhtmlid" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10" readonly></textarea>
}
else
{
<textarea id="@_rawhtmlid" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10"></textarea>
}
</TabPanel>
}
</TabStrip>
</div>
@code {
private bool _initialized = false;
private QuillEditorInterop interop;
private FileManager _fileManager;
private string _activetab = "Rich";
private ElementReference _editorElement;
private ElementReference _toolBar;
private bool _richfilemanager = false;
private string _richhtml = string.Empty;
private string _originalrichhtml = string.Empty;
private bool _rawfilemanager = false;
private string _rawhtmlid = "RawHtmlEditor_" + Guid.NewGuid().ToString("N");
private string _rawhtml = string.Empty;
private string _originalrawhtml = string.Empty;
private string _message = string.Empty;
private bool _contentchanged = false;
private int _editorIndex;
[Parameter]
public bool AllowFileManagement{ get; set; }
[Parameter]
public bool AllowRichText { get; set; } = true;
[Parameter]
public bool AllowRawHtml { get; set; } = true;
[Parameter]
public bool ReadOnly { get; set; }
[Parameter]
public string Placeholder { get; set; }
[Parameter]
public string Theme { get; set; } = "snow";
[Parameter]
public string DebugLevel { get; set; } = "info";
[Parameter]
public RenderFragment ToolbarContent { get; set; }
public override List<Resource> Resources { get; set; } = new List<Resource>()
{
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js", Location = ResourceLocation.Body }
};
protected override void OnInitialized()
{
interop = new QuillEditorInterop(JSRuntime);
if (string.IsNullOrEmpty(Placeholder))
{
Placeholder = Localizer["Placeholder"];
}
}
protected override void OnParametersSet()
{
if (!AllowRichText)
{
_activetab = "Raw";
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (AllowRichText)
{
if (firstRender)
{
await interop.CreateEditor(
_editorElement,
_toolBar,
ReadOnly,
Placeholder,
Theme,
DebugLevel);
await interop.LoadEditorContent(_editorElement, _richhtml);
// preserve a copy of the content (Quill sanitizes content so we need to retrieve it from the editor as it may have been modified)
_originalrichhtml = await interop.GetHtml(_editorElement);
_initialized = true;
}
else
{
if (_initialized)
{
if (_contentchanged)
{
// reload editor if Content passed to component has changed
await interop.LoadEditorContent(_editorElement, _richhtml);
_originalrichhtml = await interop.GetHtml(_editorElement);
}
else
{
// preserve changed content on re-render event
var richhtml = await interop.GetHtml(_editorElement);
if (richhtml != _richhtml)
{
_richhtml = richhtml;
await interop.LoadEditorContent(_editorElement, _richhtml);
}
}
}
}
_contentchanged = false;
}
}
public void Initialize(string content)
{
_richhtml = content;
_rawhtml = content;
_originalrawhtml = _rawhtml; // preserve for comparison later
_originalrichhtml = "";
_richhtml = content;
_contentchanged = content != _originalrawhtml;
StateHasChanged();
}
public async Task<string> GetContent()
{
// evaluate raw html content as first priority
if (_rawhtml != _originalrawhtml)
{
return _rawhtml;
}
else
{
var richhtml = "";
if (AllowRichText)
{
richhtml = await interop.GetHtml(_editorElement);
}
if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml))
{
// convert Quill's empty content to empty string
if (richhtml == "<p><br></p>")
{
richhtml = string.Empty;
}
return richhtml;
}
else
{
// return original raw html content
return _originalrawhtml;
}
}
}
public void CloseRichFileManager()
{
_richfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRawFileManager()
{
_rawfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public async Task<string> GetHtml()
{
// evaluate raw html content as first priority
if (_rawhtml != _originalrawhtml)
{
return _rawhtml;
}
else
{
var richhtml = "";
if (AllowRichText)
{
richhtml = await interop.GetHtml(_editorElement);
}
if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml))
{
// convert Quill's empty content to empty string
if (richhtml == "<p><br></p>")
{
richhtml = string.Empty;
}
return richhtml;
}
else
{
// return original raw html content
return _originalrawhtml;
}
}
}
public async Task InsertRichImage()
{
_message = string.Empty;
if (_richfilemanager)
{
var file = _fileManager.GetFile();
if (file != null)
{
await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name), _editorIndex);
_richhtml = await interop.GetHtml(_editorElement);
_richfilemanager = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_editorIndex = await interop.GetCurrentCursor(_editorElement);
_richfilemanager = true;
}
StateHasChanged();
}
public async Task InsertRawImage()
{
_message = string.Empty;
if (_rawfilemanager)
{
var file = _fileManager.GetFile();
if (file != null)
{
var interop = new Interop(JSRuntime);
int pos = await interop.GetCaretPosition(_rawhtmlid);
var image = "<img src=\"" + file.Url + "\" alt=\"" + ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name) + "\" class=\"img-fluid\">";
_rawhtml = _rawhtml.Substring(0, pos) + image + _rawhtml.Substring(pos);
_rawfilemanager = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_rawfilemanager = true;
}
StateHasChanged();
}
}

View File

@ -1,120 +1,22 @@
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.Extensions.DependencyInjection
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@inherits ModuleControlBase @inherits ModuleControlBase
@inject IServiceProvider ServiceProvider
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IStringLocalizer<RichTextEditor> Localizer @inject IStringLocalizer<RichTextEditor> Localizer
<div class="row" style="margin-bottom: 50px;"> <div class="row" style="margin-bottom: 50px;">
<div class="col"> <div class="col">
<TabStrip ActiveTab="@_activetab"> @_textEditorComponent
@if (AllowRichText)
{
<TabPanel Name="Rich" Heading="Rich Text Editor" ResourceKey="RichTextEditor">
@if (_richfilemanager)
{
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button>
}
@if (_richfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRichFileManager">@Localizer["Close"]</button>
}
</div>
<div class="row">
<div class="col">
<div @ref="@_toolBar">
@if (ToolbarContent != null)
{
@ToolbarContent
}
else
{
<select class="ql-header">
<option selected=""></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
</select>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
</span>
}
</div>
<div @ref="@_editorElement">
</div>
</div>
</div>
</TabPanel>
}
@if (AllowRawHtml)
{
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
@if (_rawfilemanager)
{
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRawImage">@Localizer["InsertImage"]</button>
}
@if (_rawfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRawFileManager">@Localizer["Close"]</button>
}
</div>
@if (ReadOnly)
{
<textarea id="rawhtmleditor" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10" readonly></textarea>
}
else
{
<textarea id="rawhtmleditor" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10"></textarea>
}
</TabPanel>
}
</TabStrip>
</div> </div>
</div> </div>
@code { @code {
private ElementReference _editorElement; private ITextEditorProvider _textEditorProvider;
private ElementReference _toolBar; private RenderFragment _textEditorComponent;
private bool _richfilemanager = false; private ITextEditor _textEditor;
private FileManager _fileManager;
private string _richhtml = string.Empty;
private string _originalrichhtml = string.Empty;
private bool _rawfilemanager = false;
private string _rawhtml = string.Empty;
private string _originalrawhtml = string.Empty;
private string _message = string.Empty;
private string _activetab = "Rich";
[Parameter] [Parameter]
public string Content { get; set; } public string Content { get; set; }
@ -123,7 +25,7 @@
public bool ReadOnly { get; set; } = false; public bool ReadOnly { get; set; } = false;
[Parameter] [Parameter]
public string Placeholder { get; set; } = "Enter Your Content..."; public string Placeholder { get; set; }
[Parameter] [Parameter]
public bool AllowFileManagement { get; set; } = true; public bool AllowFileManagement { get; set; } = true;
@ -139,152 +41,114 @@
public RenderFragment ToolbarContent { get; set; } public RenderFragment ToolbarContent { get; set; }
[Parameter] [Parameter]
public string Theme { get; set; } = "snow"; public string Theme { get; set; }
[Parameter] [Parameter]
public string DebugLevel { get; set; } = "info"; public string DebugLevel { get; set; }
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources { get; set; } = new List<Resource>();
protected override async Task OnInitializedAsync()
{ {
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js" }, _textEditorProvider = await GetTextEditorProvider();
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js" }, }
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js" }
};
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
_richhtml = Content; _textEditorComponent = (builder) =>
_rawhtml = Content;
_originalrawhtml = _rawhtml; // preserve for comparison later
_originalrichhtml = "";
// Quill wraps content in <p> tags which can be used as a signal to set the active tab
if (!string.IsNullOrEmpty(Content) && !Content.StartsWith("<p>") && AllowRawHtml)
{ {
_activetab = "Raw"; CreateTextEditor(builder);
} };
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
await base.OnAfterRenderAsync(firstRender); if(_textEditor != null)
if (AllowRichText)
{ {
var interop = new RichTextEditorInterop(JSRuntime); _textEditor.Initialize(Content);
if (firstRender)
{
await interop.CreateEditor(
_editorElement,
_toolBar,
ReadOnly,
Placeholder,
Theme,
DebugLevel);
}
await interop.LoadEditorContent(_editorElement, _richhtml);
if (string.IsNullOrEmpty(_originalrichhtml))
{
// preserve a copy of the rich text content (Quill sanitizes content so we need to retrieve it from the editor)
_originalrichhtml = await interop.GetHtml(_editorElement);
}
} }
}
public void CloseRichFileManager() await base.OnAfterRenderAsync(firstRender);
{
_richfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRawFileManager()
{
_rawfilemanager = false;
_message = string.Empty;
StateHasChanged();
} }
public async Task<string> GetHtml() public async Task<string> GetHtml()
{ {
// evaluate raw html content as first priority return await _textEditor.GetContent();
if (_rawhtml != _originalrawhtml) }
private void CreateTextEditor(RenderTreeBuilder builder)
{
if(_textEditorProvider != null)
{ {
return _rawhtml; var editorType = Type.GetType(_textEditorProvider.EditorType);
} if (editorType != null)
else
{
var richhtml = "";
if (AllowRichText)
{ {
// return rich text content if it has changed builder.OpenComponent(0, editorType);
var interop = new RichTextEditorInterop(JSRuntime);
richhtml = await interop.GetHtml(_editorElement); //set editor parameters if available.
var attributes = new Dictionary<string, object>
{
{ "AllowFileManagement", AllowFileManagement },
{ "AllowRichText", AllowRichText },
{ "AllowRawHtml", AllowRawHtml },
{ "ReadOnly", ReadOnly }
};
if(!string.IsNullOrEmpty(Theme))
{
attributes.Add("Theme", Theme);
}
if (!string.IsNullOrEmpty(DebugLevel))
{
attributes.Add("DebugLevel", DebugLevel);
}
if (!string.IsNullOrEmpty(Placeholder))
{
attributes.Add("Placeholder", Placeholder);
}
if(ToolbarContent != null)
{
attributes.Add("ToolbarContent", ToolbarContent);
}
var index = 1;
foreach(var name in attributes.Keys)
{
if (editorType.GetProperty(name) != null)
{
builder.AddAttribute(index++, name, attributes[name]);
}
}
builder.AddComponentReferenceCapture(index, (c) =>
{
_textEditor = (ITextEditor)c;
});
builder.CloseComponent();
} }
// rich text value will only be blank if AllowRichText is disabled or the JS Interop method failed }
if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml) && !string.IsNullOrEmpty(_originalrichhtml)) }
{
return richhtml;
}
else
{
// return original raw html content
return _originalrawhtml;
}
}
}
public async Task InsertRichImage() private async Task<ITextEditorProvider> GetTextEditorProvider()
{ {
_message = string.Empty; const string DefaultEditorName = "Quill";
if (_richfilemanager)
{
var file = _fileManager.GetFile();
if (file != null)
{
var interop = new RichTextEditorInterop(JSRuntime);
await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name));
_richhtml = await interop.GetHtml(_editorElement);
_richfilemanager = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_richfilemanager = true;
}
StateHasChanged();
}
public async Task InsertRawImage() var editorName = await GetTextEditorName(DefaultEditorName);
{ var editorProviders = ServiceProvider.GetServices<ITextEditorProvider>();
_message = string.Empty; var editorProvider = editorProviders.FirstOrDefault(i => i.Name == editorName);
if (_rawfilemanager) if(editorProvider == null)
{ {
var file = _fileManager.GetFile(); editorProvider = editorProviders.FirstOrDefault(i => i.Name == DefaultEditorName);
if (file != null) }
{
var interop = new Interop(JSRuntime); return editorProvider;
int pos = await interop.GetCaretPosition("rawhtmleditor"); }
var image = "<img src=\"" + file.Url + "\" alt=\"" + ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name) + "\" class=\"img-fluid\">";
_rawhtml = _rawhtml.Substring(0, pos) + image + _rawhtml.Substring(pos); private async Task<string> GetTextEditorName(string defaultName)
_rawfilemanager = false; {
} const string EditorSettingName = "TextEditor";
else
{ var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_message = Localizer["Message.Require.Image"]; return SettingService.GetSetting(settings, EditorSettingName, defaultName);
} }
}
else
{
_rawfilemanager = true;
}
StateHasChanged();
}
} }

View File

@ -23,7 +23,7 @@
</li> </li>
} }
</ul> </ul>
<div class="tab-content"> <div class="tab-content @TabContentClass">
<br /> <br />
@ChildContent @ChildContent
</div> </div>
@ -47,6 +47,9 @@
[Parameter] [Parameter]
public string Id { get; set; } // optional - used to uniquely identify an instance of a tab strip component (will be set automatically if no value provided) public string Id { get; set; } // optional - used to uniquely identify an instance of a tab strip component (will be set automatically if no value provided)
[Parameter]
public string TabContentClass { get; set; } // optional - to extend the TabContent div.
protected override void OnInitialized() protected override void OnInitialized()
{ {
if (string.IsNullOrEmpty(Id)) if (string.IsNullOrEmpty(Id))

View File

@ -37,6 +37,10 @@
content = htmltext.Content; content = htmltext.Content;
content = Utilities.FormatContent(content, PageState.Alias, "render"); content = Utilities.FormatContent(content, PageState.Alias, "render");
} }
else
{
content = "";
}
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@ -52,6 +52,8 @@ namespace Oqtane.Modules
public virtual string RenderMode { get { return RenderModes.Interactive; } } // interactive by default public virtual string RenderMode { get { return RenderModes.Interactive; } } // interactive by default
public virtual bool? Prerender { get { return null; } } // allows the Site Prerender property to be overridden
// url parameters // url parameters
public virtual string UrlParametersTemplate { get; set; } public virtual string UrlParametersTemplate { get; set; }
@ -276,7 +278,6 @@ namespace Oqtane.Modules
public void AddModuleMessage(string message, MessageType type, string position) public void AddModuleMessage(string message, MessageType type, string position)
{ {
ClearModuleMessage();
RenderModeBoundary.AddModuleMessage(message, type, position); RenderModeBoundary.AddModuleMessage(message, type, position);
} }

View File

@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>5.1.0</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -12,7 +12,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>
@ -22,9 +22,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.3" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,11 @@
using Oqtane.Interfaces;
namespace Oqtane.Providers
{
public class QuillTextEditorProvider : ITextEditorProvider
{
public string Name => "Quill";
public string EditorType => "Oqtane.Modules.Controls.QuillTextEditor, Oqtane.Client";
}
}

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -153,7 +153,7 @@
<data name="Integrated" xml:space="preserve"> <data name="Integrated" xml:space="preserve">
<value>Integrated</value> <value>Integrated</value>
</data> </data>
<data name="Encryption,Text" xml:space="preserve"> <data name="Encryption.Text" xml:space="preserve">
<value>Encryption:</value> <value>Encryption:</value>
</data> </data>
<data name="Encryption.HelpText" xml:space="preserve"> <data name="Encryption.HelpText" xml:space="preserve">

View File

@ -195,4 +195,7 @@
<data name="Folder Management" xml:space="preserve"> <data name="Folder Management" xml:space="preserve">
<value>Folder Management</value> <value>Folder Management</value>
</data> </data>
<data name="Message.Folder.Duplicate" xml:space="preserve">
<value>Folder Name Specified Already Exists In Parent</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -210,4 +210,16 @@
<data name="Success.SaveSiteSettings" xml:space="preserve"> <data name="Success.SaveSiteSettings" xml:space="preserve">
<value>Settings Saved Successfully</value> <value>Settings Saved Successfully</value>
</data> </data>
<data name="DeleteLogs.Header" xml:space="preserve">
<value>Clear Events</value>
</data>
<data name="DeleteLogs.Message" xml:space="preserve">
<value>Are You Sure You Wish To Remove All Log Events?</value>
</data>
<data name="DeleteLogs.Text" xml:space="preserve">
<value>Clear Events</value>
</data>
<data name="Error.DeleteLogs" xml:space="preserve">
<value>Error Deleting Log Events</value>
</data>
</root> </root>

View File

@ -156,7 +156,7 @@
<data name="Module.Text" xml:space="preserve"> <data name="Module.Text" xml:space="preserve">
<value>Module:</value> <value>Module:</value>
</data> </data>
<data name="Module Settings" xml:space="preserve"> <data name="ModuleSettings.Heading" xml:space="preserve">
<value>Module Settings</value> <value>Module Settings</value>
</data> </data>
<data name="Pane.HelpText" xml:space="preserve"> <data name="Pane.HelpText" xml:space="preserve">
@ -177,4 +177,16 @@
<data name="ExpiryDate.Text" xml:space="preserve"> <data name="ExpiryDate.Text" xml:space="preserve">
<value>Expiry Date: </value> <value>Expiry Date: </value>
</data> </data>
<data name="Permissions.Text" xml:space="preserve">
<value>Permissions</value>
</data>
<data name="Permissions.Heading" xml:space="preserve">
<value>Permissions</value>
</data>
<data name="ContainerSettings.Heading" xml:space="preserve">
<value>Container Settings</value>
</data>
<data name="ModuleSettings.Title" xml:space="preserve">
<value>Module Settings</value>
</data>
</root> </root>

View File

@ -147,4 +147,7 @@
<data name="Title" xml:space="preserve"> <data name="Title" xml:space="preserve">
<value>Title</value> <value>Title</value>
</data> </data>
<data name="Detail.Text" xml:space="preserve">
<value>Detail</value>
</data>
</root> </root>

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NoResult" xml:space="preserve">
<value>No Content Matches The Criteria Provided</value>
</data>
<data name="SearchLabel" xml:space="preserve">
<value>Search:</value>
</data>
<data name="SearchPlaceholder" xml:space="preserve">
<value>Search</value>
</data>
</root>

View File

@ -277,10 +277,10 @@
<value>UI Component Settings</value> <value>UI Component Settings</value>
</data> </data>
<data name="Prerender.HelpText" xml:space="preserve"> <data name="Prerender.HelpText" xml:space="preserve">
<value>Specifies if interactive components should prerender their output</value> <value>Specifies if interactive components should prerender their output on the server</value>
</data> </data>
<data name="Prerender.Text" xml:space="preserve"> <data name="Prerender.Text" xml:space="preserve">
<value>Prerender? </value> <value>Prerender: </value>
</data> </data>
<data name="RenderMode.HelpText" xml:space="preserve"> <data name="RenderMode.HelpText" xml:space="preserve">
<value>The default render mode for the site</value> <value>The default render mode for the site</value>

View File

@ -255,12 +255,6 @@
<data name="MachineName.Text" xml:space="preserve"> <data name="MachineName.Text" xml:space="preserve">
<value>Machine Name:</value> <value>Machine Name:</value>
</data> </data>
<data name="TickCount.HelpText" xml:space="preserve">
<value>Amount Of Time The Service Has Been Available And Operational</value>
</data>
<data name="TickCount.Text" xml:space="preserve">
<value>Service Uptime:</value>
</data>
<data name="WebRootPath.HelpText" xml:space="preserve"> <data name="WebRootPath.HelpText" xml:space="preserve">
<value>Server Web Root Path</value> <value>Server Web Root Path</value>
</data> </data>
@ -294,4 +288,10 @@
<data name="Error.ClearLog" xml:space="preserve"> <data name="Error.ClearLog" xml:space="preserve">
<value>Ann Error Occurred Clearing The System Log</value> <value>Ann Error Occurred Clearing The System Log</value>
</data> </data>
<data name="Process.HelpText" xml:space="preserve">
<value>Indicates if the current process is 32 bit or 64 bit</value>
</data>
<data name="Process.Text" xml:space="preserve">
<value>Process: </value>
</data>
</root> </root>

View File

@ -159,4 +159,7 @@
<data name="Url" xml:space="preserve"> <data name="Url" xml:space="preserve">
<value>Url</value> <value>Url</value>
</data> </data>
<data name="Edit.Text" xml:space="preserve">
<value>Edit</value>
</data>
</root> </root>

View File

@ -184,7 +184,7 @@
<value>Number of days of visitor activity to retain</value> <value>Number of days of visitor activity to retain</value>
</data> </data>
<data name="Retention.Text" xml:space="preserve"> <data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value> <value>Retention:</value>
</data> </data>
<data name="Correlation.HelpText" xml:space="preserve"> <data name="Correlation.HelpText" xml:space="preserve">
<value>Indicate if new visitors to this site should be correlated based on their IP Address</value> <value>Indicate if new visitors to this site should be correlated based on their IP Address</value>
@ -192,4 +192,10 @@
<data name="Correlation.Text" xml:space="preserve"> <data name="Correlation.Text" xml:space="preserve">
<value>Correlate Visitors?</value> <value>Correlate Visitors?</value>
</data> </data>
<data name="Duration.HelpText" xml:space="preserve">
<value>The duration of a browsing session considered to be a distinct visit (in minutes)</value>
</data>
<data name="Duration.Text" xml:space="preserve">
<value>Session Duration:</value>
</data>
</root> </root>

View File

@ -117,13 +117,16 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="InsertImage" xml:space="preserve">
<value>Insert Image</value>
</data>
<data name="Close" xml:space="preserve"> <data name="Close" xml:space="preserve">
<value>Close</value> <value>Close</value>
</data> </data>
<data name="InsertImage" xml:space="preserve">
<value>Insert Image</value>
</data>
<data name="Message.Require.Image" xml:space="preserve"> <data name="Message.Require.Image" xml:space="preserve">
<value>You Must Select An Image To Insert</value> <value>You Must Select An Image To Insert</value>
</data> </data>
<data name="Placeholder" xml:space="preserve">
<value>Enter Your Content...</value>
</data>
</root> </root>

View File

@ -453,4 +453,10 @@
<data name="RenderModeStatic" xml:space="preserve"> <data name="RenderModeStatic" xml:space="preserve">
<value>Static</value> <value>Static</value>
</data> </data>
<data name="Disabled" xml:space="preserve">
<value>Disabled</value>
</data>
<data name="Enabled" xml:space="preserve">
<value>Enabled</value>
</data>
</root> </root>

View File

@ -198,4 +198,7 @@
<data name="LocationTop" xml:space="preserve"> <data name="LocationTop" xml:space="preserve">
<value>Top</value> <value>Top</value>
</data> </data>
<data name="Module.CopyExisting" xml:space="preserve">
<value>Copy Existing Module</value>
</data>
</root> </root>

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DeleteModule" xml:space="preserve">
<value>Delete Module</value>
</data>
<data name="ExportContent" xml:space="preserve">
<value>Export Content</value>
</data>
<data name="ImportContent" xml:space="preserve">
<value>Import Content</value>
</data>
<data name="ManageSettings" xml:space="preserve">
<value>Manage Settings</value>
</data>
<data name="MoveDown" xml:space="preserve">
<value>Move Down</value>
</data>
<data name="MoveToBottom" xml:space="preserve">
<value>Move To Bottom</value>
</data>
<data name="MoveToTop" xml:space="preserve">
<value>Move To Top</value>
</data>
<data name="MoveUp" xml:space="preserve">
<value>MoveUp</value>
</data>
<data name="PublishModule" xml:space="preserve">
<value>Publish Module</value>
</data>
<data name="UnpublishModule" xml:space="preserve">
<value>Unpublish Module</value>
</data>
</root>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Search" xml:space="preserve">
<value>Search</value>
</data>
<data name="SearchPlaceHolder" xml:space="preserve">
<value>Search</value>
</data>
</root>

View File

@ -29,6 +29,13 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<Log> GetLogAsync(int logId); Task<Log> GetLogAsync(int logId);
/// <summary>
/// Clear the entire logs of the given site.
/// </summary>
/// <param name="siteId"></param>
/// <returns></returns>
Task DeleteLogsAsync(int siteId);
/// <summary> /// <summary>
/// Creates a new log entry /// Creates a new log entry
/// </summary> /// </summary>

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Models;
namespace Oqtane.Services
{
[PrivateApi("Mark SearchResults classes as private, since it's not very useful in the public docs")]
public interface ISearchResultsService
{
Task<SearchResults> SearchAsync(int moduleId, SearchQuery searchQuery);
}
}

View File

@ -46,6 +46,14 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task DeleteSiteAsync(int siteId); Task DeleteSiteAsync(int siteId);
/// <summary>
/// Returns a list of modules
/// </summary>
/// <param name="siteId"></param>
/// <param name="pageId"></param>
/// <returns></returns>
Task<List<Module>> GetModulesAsync(int siteId, int pageId);
[PrivateApi] [PrivateApi]
[Obsolete("This method is deprecated.", false)] [Obsolete("This method is deprecated.", false)]
void SetAlias(Alias alias); void SetAlias(Alias alias);

View File

@ -35,6 +35,11 @@ namespace Oqtane.Services
return await GetJsonAsync<Log>($"{Apiurl}/{logId}"); return await GetJsonAsync<Log>($"{Apiurl}/{logId}");
} }
public async Task DeleteLogsAsync(int siteId)
{
await DeleteAsync($"{Apiurl}?siteid={siteId}");
}
public async Task Log(int? pageId, int? moduleId, int? userId, string category, string feature, LogFunction function, LogLevel level, Exception exception, string message, params object[] args) public async Task Log(int? pageId, int? moduleId, int? userId, string category, string feature, LogFunction function, LogLevel level, Exception exception, string message, params object[] args)
{ {
await Log(null, pageId, moduleId, userId, category, feature, function, level, exception, message, args); await Log(null, pageId, moduleId, userId, category, feature, function, level, exception, message, args);

View File

@ -33,7 +33,7 @@ namespace Oqtane.Services
public async Task<Profile> UpdateProfileAsync(Profile profile) public async Task<Profile> UpdateProfileAsync(Profile profile)
{ {
return await PutJsonAsync<Profile>($"{Apiurl}/{profile.SiteId}", profile); return await PutJsonAsync<Profile>($"{Apiurl}/{profile.ProfileId}", profile);
} }
public async Task DeleteProfileAsync(int profileId) public async Task DeleteProfileAsync(int profileId)
{ {

View File

@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Shared;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SearchResultsService : ServiceBase, ISearchResultsService, IClientService
{
public SearchResultsService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string ApiUrl => CreateApiUrl("SearchResults");
public async Task<SearchResults> SearchAsync(int moduleId, SearchQuery searchQuery)
{
return await PostJsonAsync<SearchQuery, SearchResults>(CreateAuthorizationPolicyUrl(ApiUrl, EntityNames.Module, moduleId), searchQuery);
}
}
}

View File

@ -41,6 +41,11 @@ namespace Oqtane.Services
await DeleteAsync($"{Apiurl}/{siteId}"); await DeleteAsync($"{Apiurl}/{siteId}");
} }
public async Task<List<Module>> GetModulesAsync(int siteId, int pageId)
{
return await GetJsonAsync<List<Module>>($"{Apiurl}/modules/{siteId}/{pageId}");
}
[Obsolete("This method is deprecated.", false)] [Obsolete("This method is deprecated.", false)]
public void SetAlias(Alias alias) public void SetAlias(Alias alias)
{ {

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Shared; using Oqtane.Shared;
using System; using System;
using System.Globalization;
namespace Oqtane.Services namespace Oqtane.Services
{ {
@ -18,7 +19,7 @@ namespace Oqtane.Services
public async Task<List<Visitor>> GetVisitorsAsync(int siteId, DateTime fromDate) public async Task<List<Visitor>> GetVisitorsAsync(int siteId, DateTime fromDate)
{ {
List<Visitor> visitors = await GetJsonAsync<List<Visitor>>($"{Apiurl}?siteid={siteId}&fromdate={fromDate.ToString("dd-MMM-yyyy")}"); List<Visitor> visitors = await GetJsonAsync<List<Visitor>>($"{Apiurl}?siteid={siteId}&fromdate={fromDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}");
return visitors.OrderByDescending(item => item.VisitedOn).ToList(); return visitors.OrderByDescending(item => item.VisitedOn).ToList();
} }

View File

@ -1,6 +1,5 @@
@namespace Oqtane.Themes @namespace Oqtane.Themes
@inherits ContainerBase @inherits ContainerBase
@inject NavigationManager NavigationManager
<div class="app-admin-modal"> <div class="app-admin-modal">
<div class="modal" tabindex="-1" role="dialog"> <div class="modal" tabindex="-1" role="dialog">
@ -8,10 +7,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><ModuleTitle /></h5> <h5 class="modal-title"><ModuleTitle /></h5>
<form method="post" class="app-form-inline" @formname="AdminContainerForm" @onsubmit="@CloseModal" data-enhance> <a href="@_url" class="btn-close" aria-label="Close"></a>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="btn-close" aria-label="Close"></button>
</form>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<ModuleInstance /> <ModuleInstance />
@ -22,9 +18,11 @@
</div> </div>
@code { @code {
private void CloseModal() private string _url;
{
NavigationManager.NavigateTo((!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : NavigateUrl()); protected override void OnParametersSet()
{
_url = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : NavigateUrl();
} }
} }

View File

@ -9,13 +9,20 @@
<div class="row flex-xl-nowrap gx-0"> <div class="row flex-xl-nowrap gx-0">
<div class="sidebar"> <div class="sidebar">
<nav class="navbar"> <nav class="navbar">
<Logo /><Menu Orientation="Vertical" /> <Logo />
<Menu Orientation="Vertical" />
<Search CssClass="px-3 d-none d-md-block" />
</nav> </nav>
</div> </div>
<div class="main g-0"> <div class="main g-0">
<div class="top-row px-4"> <div class="top-row px-4">
<div class="ms-auto"><UserProfile /> <Login /> <ControlPanel LanguageDropdownAlignment="right" /></div> <div class="ms-auto">
<Search CssClass="me-3 text-center bg-primary d-inline-block d-md-none" Position="Top" />
<UserProfile />
<Login />
<ControlPanel LanguageDropdownAlignment="right" />
</div>
</div> </div>
<div class="container"> <div class="container">
<div class="row px-4"> <div class="row px-4">
@ -31,9 +38,13 @@
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
// obtained from https://cdnjs.com/libraries // obtained from https://cdnjs.com/libraries
new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css", Integrity = "sha512-b2QcS5SsA8tZodcDtGRELiGv5SaKSk1vDHDaQRda0htPYWZ6046lr3kJ5bAAQdpV2mmA/4v0wQF9MyU6/pDIAg==", CrossOrigin = "anonymous" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css",
Integrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==",
CrossOrigin = "anonymous" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" },
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js", Integrity = "sha512-X/YkDZyjTf4wyc2Vy16YGCPHwAY8rZJY+POgokZjQB2mhIRFJCckEGc6YyX9eNsPfn0PzThEuNs+uaomE5CO6A==", CrossOrigin = "anonymous", Location = ResourceLocation.Body } new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js",
Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==",
CrossOrigin = "anonymous", Location = ResourceLocation.Body },
}; };
} }

View File

@ -10,6 +10,6 @@
} }
else else
{ {
<ModuleActionsInteractive PageState="@PageState" ModuleState="@ModuleState" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, PageState.Site.Prerender)" /> <ModuleActionsInteractive PageState="@PageState" ModuleState="@ModuleState" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, false)" />
} }
} }

View File

@ -9,6 +9,8 @@ using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
using Oqtane.UI; using Oqtane.UI;
using System.Net; using System.Net;
using static System.Runtime.InteropServices.JavaScript.JSType;
using Microsoft.Extensions.Localization;
// ReSharper disable UnassignedGetOnlyAutoProperty // ReSharper disable UnassignedGetOnlyAutoProperty
// ReSharper disable MemberCanBePrivate.Global // ReSharper disable MemberCanBePrivate.Global
@ -20,6 +22,7 @@ namespace Oqtane.Themes.Controls
[Inject] public NavigationManager NavigationManager { get; set; } [Inject] public NavigationManager NavigationManager { get; set; }
[Inject] public IPageModuleService PageModuleService { get; set; } [Inject] public IPageModuleService PageModuleService { get; set; }
[Inject] public IModuleService ModuleService { get; set; } [Inject] public IModuleService ModuleService { get; set; }
[Inject] public IStringLocalizer<ModuleActionsBase> Localizer { get; set; }
[Parameter] public PageState PageState { get; set; } [Parameter] public PageState PageState { get; set; }
[Parameter] public Module ModuleState { get; set; } [Parameter] public Module ModuleState { get; set; }
@ -37,30 +40,30 @@ namespace Oqtane.Themes.Controls
if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList)) if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{ {
actionList.Add(new ActionViewModel { Icon = Icons.Cog, Name = "Manage Settings", Action = async (u, m) => await Settings(u, m) }); actionList.Add(new ActionViewModel { Icon = Icons.Cog, Name = Localizer["ManageSettings"], Action = async (u, m) => await Settings(u, m) });
if (UserSecurity.ContainsRole(ModuleState.PermissionList, PermissionNames.View, RoleNames.Everyone)) if (UserSecurity.ContainsRole(ModuleState.PermissionList, PermissionNames.View, RoleNames.Everyone))
{ {
actionList.Add(new ActionViewModel { Icon = Icons.CircleX, Name = "Unpublish Module", Action = async (s, m) => await Unpublish(s, m) }); actionList.Add(new ActionViewModel { Icon = Icons.CircleX, Name = Localizer["UnpublishModule"], Action = async (s, m) => await Unpublish(s, m) });
} }
else else
{ {
actionList.Add(new ActionViewModel { Icon = Icons.CircleCheck, Name = "Publish Module", Action = async (s, m) => await Publish(s, m) }); actionList.Add(new ActionViewModel { Icon = Icons.CircleCheck, Name = Localizer["PublishModule"], Action = async (s, m) => await Publish(s, m) });
} }
actionList.Add(new ActionViewModel { Icon = Icons.Trash, Name = "Delete Module", Action = async (u, m) => await DeleteModule(u, m) }); actionList.Add(new ActionViewModel { Icon = Icons.Trash, Name = Localizer["DeleteModule"], Action = async (u, m) => await DeleteModule(u, m) });
if (ModuleState.ModuleDefinition != null && ModuleState.ModuleDefinition.IsPortable) if (ModuleState.ModuleDefinition != null && ModuleState.ModuleDefinition.IsPortable)
{ {
actionList.Add(new ActionViewModel { Name = "" }); actionList.Add(new ActionViewModel { Name = "" });
actionList.Add(new ActionViewModel { Icon = Icons.CloudUpload, Name = "Import Content", Action = async (u, m) => await EditUrlAsync(u, m.ModuleId, "Import") }); actionList.Add(new ActionViewModel { Icon = Icons.CloudUpload, Name = Localizer["ImportContent"], Action = async (u, m) => await EditUrlAsync(u, m.ModuleId, "Import") });
actionList.Add(new ActionViewModel { Icon = Icons.CloudDownload, Name = "Export Content", Action = async (u, m) => await EditUrlAsync(u, m.ModuleId, "Export") }); actionList.Add(new ActionViewModel { Icon = Icons.CloudDownload, Name = Localizer["ExportContent"], Action = async (u, m) => await EditUrlAsync(u, m.ModuleId, "Export") });
} }
actionList.Add(new ActionViewModel { Name = "" }); actionList.Add(new ActionViewModel { Name = "" });
if (ModuleState.PaneModuleIndex > 0) if (ModuleState.PaneModuleIndex > 0)
{ {
actionList.Add(new ActionViewModel { Icon = Icons.DataTransferUpload, Name = "Move To Top", Action = async (s, m) => await MoveTop(s, m) }); actionList.Add(new ActionViewModel { Icon = Icons.DataTransferUpload, Name = Localizer["MoveToTop"], Action = async (s, m) => await MoveTop(s, m) });
} }
if (ModuleState.PaneModuleIndex > 0) if (ModuleState.PaneModuleIndex > 0)

View File

@ -6,13 +6,13 @@
@if (ShowLanguageSwitcher) @if (ShowLanguageSwitcher)
{ {
<LanguageSwitcher DropdownAlignment="@LanguageDropdownAlignment" /> <LanguageSwitcher ButtonClass="@ButtonClass" DropdownAlignment="@LanguageDropdownAlignment" />
} }
@if (_showEditMode || (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))) @if (_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> <form method="post" class="app-form-inline" @formname="EditModeForm" @onsubmit="@(async () => await ToggleEditMode(PageState.EditMode))" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
@if (PageState.EditMode) @if (PageState.EditMode)
{ {
<button type="submit" class="btn @ButtonClass active" aria-pressed="true" autocomplete="off"> <button type="submit" class="btn @ButtonClass active" aria-pressed="true" autocomplete="off">
@ -36,7 +36,7 @@
} }
else else
{ {
<ControlPanelInteractive PageState="@PageState" SiteState="@SiteState" ButtonClass="@ButtonClass" ContainerClass="@ContainerClass" HeaderClass="@HeaderClass" BodyClass="@BodyClass" ShowLanguageSwitcher="@ShowLanguageSwitcher" LanguageDropdownAlignment="@LanguageDropdownAlignment" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, PageState.Site.Prerender)" /> <ControlPanelInteractive PageState="@PageState" SiteState="@SiteState" ButtonClass="@ButtonClass" ContainerClass="@ContainerClass" HeaderClass="@HeaderClass" BodyClass="@BodyClass" ShowLanguageSwitcher="@ShowLanguageSwitcher" LanguageDropdownAlignment="@LanguageDropdownAlignment" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, false)" />
} }
} }
@ -73,7 +73,7 @@
} }
else else
{ {
foreach (var module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId)) foreach (var module in PageState.Modules)
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, module.PermissionList)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, module.PermissionList))
{ {

View File

@ -15,6 +15,7 @@
@inject ILogService LoggingService @inject ILogService LoggingService
@inject IStringLocalizer<ControlPanelInteractive> Localizer @inject IStringLocalizer<ControlPanelInteractive> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IServiceProvider ServiceProvider
<button type="button" class="btn @ButtonClass ms-1" data-bs-toggle="offcanvas" data-bs-target="#offcanvasControlPanel" aria-controls="offcanvasControlPanel" @onclick="ClearMessage"> <button type="button" class="btn @ButtonClass ms-1" data-bs-toggle="offcanvas" data-bs-target="#offcanvasControlPanel" aria-controls="offcanvasControlPanel" @onclick="ClearMessage">
<span class="oi oi-cog"></span> <span class="oi oi-cog"></span>
@ -93,9 +94,13 @@
<div class="row"> <div class="row">
<div class="col text-center"> <div class="col text-center">
<label for="Module" class="control-label">@Localizer["Module.Manage"]</label> <label for="Module" class="control-label">@Localizer["Module.Manage"]</label>
<select class="form-select" @bind="@_moduleType"> <select class="form-select" @onchange="(e => ModuleTypeChanged(e))">
<option value="new">@Localizer["Module.AddNew"]</option> <option value="new">@Localizer["Module.AddNew"]</option>
<option value="existing">@Localizer["Module.AddExisting"]</option> @if (PageState.Page.UserId == null)
{
<option value="add">@Localizer["Module.AddExisting"]</option>
<option value="copy">@Localizer["Module.CopyExisting"]</option>
}
</select> </select>
@if (_moduleType == "new") @if (_moduleType == "new")
{ {
@ -138,7 +143,7 @@
} }
else else
{ {
<select class="form-select mt-1" @onchange="(e => PageChanged(e))"> <select class="form-select mt-1" value="@_pageId" @onchange="(e => PageChanged(e))">
<option value="-">&lt;@Localizer["Page.Select"]&gt;</option> <option value="-">&lt;@Localizer["Page.Select"]&gt;</option>
@foreach (Page p in _pages) @foreach (Page p in _pages)
{ {
@ -211,7 +216,7 @@
<div class="row d-flex"> <div class="row d-flex">
<div class="col"> <div class="col">
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-secondary col-12" @onclick=@(async () => await LogoutUser())>@Localizer["Logout"]</button> <button type="button" data-bs-dismiss="offcanvas" class="btn btn-secondary col-12 mt-2" @onclick=@(async () => await LogoutUser())>@Localizer["Logout"]</button>
</div> </div>
</div> </div>
</div> </div>
@ -291,7 +296,7 @@
_containerType = PageState.Site.DefaultContainerType; _containerType = PageState.Site.DefaultContainerType;
_allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId); _allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList(); _moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
_categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',')).Distinct().ToList(); _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList();
} }
} }
@ -334,14 +339,22 @@
StateHasChanged(); StateHasChanged();
} }
private void PageChanged(ChangeEventArgs e) private void ModuleTypeChanged(ChangeEventArgs e)
{
_moduleType = (string)e.Value;
_pageId = "-";
_moduleId = "-";
}
private async Task PageChanged(ChangeEventArgs e)
{ {
_pageId = (string)e.Value; _pageId = (string)e.Value;
if (_pageId != "-") if (_pageId != "-")
{ {
_modules = PageState.Modules _modules = await ModuleService.GetModulesAsync(PageState.Page.SiteId);
.Where(module => module.PageId == int.Parse(_pageId) && _modules = _modules.Where(module => module.PageId == int.Parse(_pageId) &&
UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList)) UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList) &&
(_moduleType == "add" || module.ModuleDefinition.IsPortable))
.ToList(); .ToList();
} }
_moduleId = "-"; _moduleId = "-";
@ -354,6 +367,7 @@
{ {
if ((_moduleType == "new" && _moduleDefinitionName != "-") || (_moduleType != "new" && _moduleId != "-")) if ((_moduleType == "new" && _moduleDefinitionName != "-") || (_moduleType != "new" && _moduleId != "-"))
{ {
var newModuleId = _moduleId != "-" ? int.Parse(_moduleId) : 0;
if (_moduleType == "new") if (_moduleType == "new")
{ {
Module module = new Module(); Module module = new Module();
@ -361,33 +375,37 @@
module.PageId = PageState.Page.PageId; module.PageId = PageState.Page.PageId;
module.ModuleDefinitionName = _moduleDefinitionName; module.ModuleDefinitionName = _moduleDefinitionName;
module.AllPages = false; module.AllPages = false;
module.PermissionList = GenerateDefaultPermissions(module.SiteId);
var permissions = new List<Permission>();
if (_visibility == "view")
{
// set module view permissions to page view permissions
permissions = SetPermissions(permissions, module.SiteId, PermissionNames.View, PermissionNames.View);
}
else
{
// set module view permissions to page edit permissions
permissions = SetPermissions(permissions, module.SiteId, PermissionNames.View, PermissionNames.Edit);
}
// set module edit permissions to page edit permissions
permissions = SetPermissions(permissions, module.SiteId, PermissionNames.Edit, PermissionNames.Edit);
module.PermissionList = permissions;
module = await ModuleService.AddModuleAsync(module); module = await ModuleService.AddModuleAsync(module);
_moduleId = module.ModuleId.ToString(); newModuleId = module.ModuleId;
}
else if (_moduleType == "copy")
{
var module = await ModuleService.GetModuleAsync(int.Parse(_moduleId));
module.ModuleId = 0;
module.SiteId = PageState.Site.SiteId;
module.PageId = PageState.Page.PageId;
module.AllPages = false;
module.PermissionList = GenerateDefaultPermissions(module.SiteId);
module = await ModuleService.AddModuleAsync(module);
var moduleContent = await ModuleService.ExportModuleAsync(int.Parse(_moduleId), PageState.Page.PageId);
if (!string.IsNullOrEmpty(moduleContent))
{
await ModuleService.ImportModuleAsync(module.ModuleId, PageState.Page.PageId, moduleContent);
}
newModuleId = module.ModuleId;
} }
var pageModule = new PageModule var pageModule = new PageModule
{ {
PageId = PageState.Page.PageId, PageId = PageState.Page.PageId,
ModuleId = int.Parse(_moduleId), ModuleId = newModuleId,
Title = _title Title = _title
}; };
if (pageModule.Title == "") if (string.IsNullOrEmpty(pageModule.Title))
{ {
if (_moduleType == "new") if (_moduleType == "new")
{ {
@ -412,9 +430,16 @@
await PageModuleService.UpdatePageModuleOrderAsync(pageModule.PageId, pageModule.Pane); await PageModuleService.UpdatePageModuleOrderAsync(pageModule.PageId, pageModule.Pane);
await UpdateSettingsAsync(); await UpdateSettingsAsync();
_message = $"<div class=\"alert alert-success mt-2 text-center\" role=\"alert\">{Localizer["Success.Page.ModuleAdd"]}</div>"; if (PageState.RenderMode == RenderModes.Interactive)
_title = ""; {
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, "")); _message = $"<div class=\"alert alert-success mt-2 text-center\" role=\"alert\">{Localizer["Success.Page.ModuleAdd"]}</div>";
_title = "";
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, ""));
}
else // reload page in static rendering
{
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, ""), true);
}
} }
else else
{ {
@ -427,6 +452,25 @@
} }
} }
private List<Permission> GenerateDefaultPermissions(int siteId)
{
var permissions = new List<Permission>();
if (_visibility == "view")
{
// set module view permissions to page view permissions
permissions = SetPermissions(permissions, siteId, PermissionNames.View, PermissionNames.View);
}
else
{
// set module view permissions to page edit permissions
permissions = SetPermissions(permissions, siteId, PermissionNames.View, PermissionNames.Edit);
}
// set module edit permissions to page edit permissions
permissions = SetPermissions(permissions, siteId, PermissionNames.Edit, PermissionNames.Edit);
return permissions;
}
private List<Permission> SetPermissions(List<Permission> permissions, int siteId, string modulePermission, string pagePermission) private List<Permission> SetPermissions(List<Permission> permissions, int siteId, string modulePermission, string pagePermission)
{ {
foreach (var permission in PageState.Page.PermissionList.Where(item => item.PermissionName == pagePermission)) foreach (var permission in PageState.Page.PermissionList.Where(item => item.PermissionName == pagePermission))
@ -438,27 +482,19 @@
private void Navigate(string location) private void Navigate(string location)
{ {
Module module; int moduleId;
switch (location) switch (location)
{ {
case "Admin": case "Admin":
// get admin dashboard moduleid // get admin dashboard moduleid
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule); moduleId = int.Parse(PageState.Site.Settings[Constants.AdminDashboardModule]);
if (module != null) NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin", moduleId, "Index", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
{
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin", module.ModuleId, "Index", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
}
break; break;
case "Add": case "Add":
case "Edit": case "Edit":
string url = "";
// get page management moduleid // get page management moduleid
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.PageManagementModule); moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]);
if (module != null) NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
{
url = Utilities.EditUrl(PageState.Alias.Path, "admin/pages", module.ModuleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}");
NavigationManager.NavigateTo(url);
}
break; break;
} }
} }

View File

@ -1,21 +1,29 @@
@namespace Oqtane.Themes.Controls
@inherits ThemeControlBase
@using System.Globalization @using System.Globalization
@using Microsoft.AspNetCore.Localization @using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Http
@using Oqtane.Models @using Oqtane.Models
@namespace Oqtane.Themes.Controls
@inherits ThemeControlBase
@inject ILanguageService LanguageService @inject ILanguageService LanguageService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@if (_supportedCultures?.Count() > 1) @if (_supportedCultures?.Count() > 1)
{ {
<div class="btn-group pe-1" role="group"> <div class="btn-group pe-1" role="group">
<button id="btnCultures" type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <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> <span class="oi oi-globe"></span>
</button> </button>
<div class="dropdown-menu @MenuAlignment" aria-labelledby="btnCultures"> <div class="dropdown-menu @MenuAlignment" aria-labelledby="btnCultures">
@foreach (var culture in _supportedCultures) @foreach (var culture in _supportedCultures)
{ {
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(culture.Name))">@culture.DisplayName</a> @if (PageState.RenderMode == RenderModes.Interactive)
{
<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>
}
else
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + culture.Name)">@culture.DisplayName</a>
}
} }
</div> </div>
</div> </div>
@ -23,9 +31,15 @@
@code{ @code{
private IEnumerable<Culture> _supportedCultures; private IEnumerable<Culture> _supportedCultures;
private string MenuAlignment = string.Empty;
[Parameter] [Parameter]
public string DropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right public string DropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right
private string MenuAlignment = string.Empty; [Parameter]
public string ButtonClass { get; set; } = "btn-outline-secondary";
[CascadingParameter]
HttpContext HttpContext { get; set; }
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
@ -33,16 +47,26 @@
var languages = PageState.Languages; var languages = PageState.Languages;
_supportedCultures = languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name }); _supportedCultures = languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name });
if (PageState.QueryString.ContainsKey("culture"))
{
var culture = PageState.QueryString["culture"];
if (_supportedCultures.Any(item => item.Name == culture))
{
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, new CookieOptions { Path = "/", Expires = DateTimeOffset.UtcNow.AddYears(365) });
}
NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", ""), forceLoad: true);
}
} }
private async Task SetCultureAsync(string culture) private async Task SetCultureAsync(string culture)
{ {
if (culture != CultureInfo.CurrentUICulture.Name) if (culture != CultureInfo.CurrentUICulture.Name)
{ {
var interop = new Interop(JSRuntime);
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
var interop = new Interop(JSRuntime);
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360);
NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
} }
} }

View File

@ -16,7 +16,7 @@
else else
{ {
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm"> <form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" /> <input type="hidden" name="returnurl" value="@returnurl" />
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button> <button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
</form> </form>

View File

@ -0,0 +1,64 @@
@namespace Oqtane.Themes.Controls
@using System.Net
@using Microsoft.AspNetCore.Http
@inherits ThemeControlBase
@inject IStringLocalizer<Search> Localizer
@inject NavigationManager NavigationManager
@inject IHttpContextAccessor HttpContext
@if (_searchResultsPage != null)
{
<span class="app-search @CssClass">
<form method="post" class="app-form-inline" @formname="@($"{Position}SearchForm")" @onsubmit="@PerformSearch" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="text" name="keywords" maxlength="50"
class="form-control d-inline-block pe-5 shadow-none"
@bind-value="_keywords"
placeholder="@Localizer["SearchPlaceHolder"]"
aria-label="Search" />
<button type="submit" class="btn btn-search">
<span class="oi oi-magnifying-glass align-middle"></span>
</button>
</form>
</span>
}
@code {
private Page _searchResultsPage;
private string _keywords = "";
[Parameter]
public string CssClass { get; set; }
[Parameter]
public string Position { get; set; } = "Main";
[Parameter]
public string SearchResultPagePath { get; set; } = "search";
protected override void OnInitialized()
{
if(!string.IsNullOrEmpty(SearchResultPagePath))
{
_searchResultsPage = PageState.Pages.FirstOrDefault(i => i.Path == SearchResultPagePath);
}
}
protected override void OnParametersSet()
{
}
private void PerformSearch()
{
var keywords = HttpContext.HttpContext.Request.Form["keywords"];
if (!string.IsNullOrEmpty(keywords) && _searchResultsPage != null)
{
var url = NavigateUrl(_searchResultsPage.Path, $"q={keywords}");
NavigationManager.NavigateTo(url);
}
}
}

View File

@ -17,9 +17,13 @@ namespace Oqtane.Themes.OqtaneTheme
Resources = new List<Resource>() Resources = new List<Resource>()
{ {
// obtained from https://cdnjs.com/libraries // obtained from https://cdnjs.com/libraries
new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cyborg/bootstrap.min.css", Integrity = "sha512-RfNxVfFNFgqk9MXO4TCKXYXn9hgc+keHCg3xFFGbnp2q7Cifda+YYzMTDHwsQtNx4DuqIMgfvZead7XOtB9CDQ==", CrossOrigin = "anonymous" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/cyborg/bootstrap.min.css",
Integrity = "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==",
CrossOrigin = "anonymous" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" },
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js", Integrity = "sha512-X/YkDZyjTf4wyc2Vy16YGCPHwAY8rZJY+POgokZjQB2mhIRFJCckEGc6YyX9eNsPfn0PzThEuNs+uaomE5CO6A==", CrossOrigin = "anonymous", Location = ResourceLocation.Body } new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js",
Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==",
CrossOrigin = "anonymous", Location = ResourceLocation.Body },
} }
}; };
} }

View File

@ -6,7 +6,12 @@
<nav class="navbar navbar-dark bg-primary fixed-top"> <nav class="navbar navbar-dark bg-primary fixed-top">
<Logo /><Menu Orientation="Horizontal" /> <Logo /><Menu Orientation="Horizontal" />
<div class="controls ms-auto"> <div class="controls ms-auto">
<div class="controls-group"><UserProfile ShowRegister="@_register" /> <Login ShowLogin="@_login" /> <ControlPanel LanguageDropdownAlignment="right" /></div> <div class="controls-group">
<Search CssClass="me-3 text-center bg-primary" />
<UserProfile ShowRegister="@_register" />
<Login ShowLogin="@_login" />
<ControlPanel LanguageDropdownAlignment="right" />
</div>
</div> </div>
</nav> </nav>
<div class="content"> <div class="content">

View File

@ -121,15 +121,15 @@
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
if (_login != "-") if (_login != "-")
{ {
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login, true); settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login);
} }
if (_register != "-") if (_register != "-")
{ {
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register, true); settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register);
} }
if (_footer != "-") if (_footer != "-")
{ {
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Footer", _footer, true); settings = SettingService.SetSetting(settings, GetType().Namespace + ":Footer", _footer);
} }
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
} }

View File

@ -3,8 +3,6 @@
@using Oqtane.Shared @using Oqtane.Shared
@inject SiteState SiteState @inject SiteState SiteState
@implements IDisposable @implements IDisposable
@* the following StreamRendering attribute is required - if it is removed the framework will not render the content in static rendering *@
@attribute [StreamRendering]
@if (!string.IsNullOrEmpty(_title)) @if (!string.IsNullOrEmpty(_title))
{ {

View File

@ -0,0 +1,13 @@
// This is just a placeholder file
// It is necessary for the documentation to successfully build this project.
// Reason is that docfx will run the .net compiler and find references
// to this class in the project.
// But since the real class is just a .razor file, ATM docfx will fail.
//
// Note added 2024-06-27 by @iJungleboy.
// We hope that as .net and docfx improve, the razor-compiler will work in that scenario
// as well, and this file can be removed.
namespace Oqtane.UI;
public partial class ModuleInstance;

View File

@ -1,13 +1,17 @@
@namespace Oqtane.UI @namespace Oqtane.UI
@inject SiteState SiteState @inject SiteState SiteState
@if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Interactive) @if (_comment != null)
{ {
<StreamRenderingDisabled ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" /> @((MarkupString)_comment)
} @if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static)
else {
{ <RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" />
<StreamRenderingEnabled ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" /> }
else
{
<RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" @rendermode="InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, _prerender)" />
}
} }
@code { @code {
@ -20,6 +24,24 @@ else
[CascadingParameter] [CascadingParameter]
private Module ModuleState { get; set; } private Module ModuleState { get; set; }
private bool _prerender;
private string _comment;
protected override void OnParametersSet()
{
_prerender = ModuleState.Prerender ?? PageState.Site.Prerender;
_comment = "<!-- rendermode: ";
if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Static)
{
_comment += RenderModes.Static;
}
else
{
_comment += $"{RenderModes.Interactive}:{PageState.Runtime} - prerender: {_prerender}";
}
_comment += " -->";
}
[Obsolete("AddModuleMessage is deprecated. Use AddModuleMessage in ModuleBase instead.", false)] [Obsolete("AddModuleMessage is deprecated. Use AddModuleMessage in ModuleBase instead.", false)]
public void AddModuleMessage(string message, MessageType type) public void AddModuleMessage(string message, MessageType type)

View File

@ -9,6 +9,7 @@ namespace Oqtane.UI
public Alias Alias { get; set; } public Alias Alias { get; set; }
public Site Site { get; set; } public Site Site { get; set; }
public Page Page { get; set; } public Page Page { get; set; }
public List<Module> Modules { get; set; }
public User User { get; set; } public User User { get; set; }
public Uri Uri { get; set; } public Uri Uri { get; set; }
public Route Route { get; set; } public Route Route { get; set; }
@ -31,10 +32,6 @@ namespace Oqtane.UI
{ {
get { return Site.Pages; } get { return Site.Pages; }
} }
public List<Module> Modules
{
get { return Site.Modules; }
}
public List<Language> Languages public List<Language> Languages
{ {
get { return Site.Languages; } get { return Site.Languages; }

View File

@ -43,7 +43,7 @@ else
DynamicComponent = builder => DynamicComponent = builder =>
{ {
foreach (Module module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId)) foreach (Module module in PageState.Modules)
{ {
// set renderid - this allows the framework to determine which components should be rendered when PageState changes // set renderid - this allows the framework to determine which components should be rendered when PageState changes
if (module.RenderId != PageState.RenderId) if (module.RenderId != PageState.RenderId)

View File

@ -0,0 +1,13 @@
// This is just a placeholder file
// It is necessary for the documentation to successfully build this project.
// Reason is that docfx will run the .net compiler and find references
// to this class in the project.
// But since the real class is just a .razor file, ATM docfx will fail.
//
// Note added 2024-06-27 by @iJungleboy.
// We hope that as .net and docfx improve, the razor-compiler will work in that scenario
// as well, and this file can be removed.
namespace Oqtane.UI;
public partial class RenderModeBoundary;

View File

@ -10,14 +10,19 @@
{ {
@if (ModuleType != null) @if (ModuleType != null)
{ {
@((MarkupString)$"<!-- rendermode: {ModuleState.RenderMode} -->") @if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "top")
<ModuleMessage @ref="moduleMessageTop" Message="@_messageContent" Type="@_messageType" /> {
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" />
}
@DynamicComponent @DynamicComponent
@if (_progressIndicator) @if (_progressIndicator)
{ {
<div class="app-progress-indicator"></div> <div class="app-progress-indicator"></div>
} }
<ModuleMessage @ref="moduleMessageBottom" Message="@_messageContent" Type="@_messageType" /> @if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "bottom")
{
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" />
}
} }
} }
else else
@ -42,8 +47,6 @@
private string _messagePosition; private string _messagePosition;
private bool _progressIndicator = false; private bool _progressIndicator = false;
private string _error; private string _error;
private ModuleMessage moduleMessageTop;
private ModuleMessage moduleMessageBottom;
[Parameter] [Parameter]
public SiteState SiteState { get; set; } public SiteState SiteState { get; set; }
@ -104,12 +107,17 @@
public void AddModuleMessage(string message, MessageType type, string position) public void AddModuleMessage(string message, MessageType type, string position)
{ {
_messageContent = message; if(message != _messageContent
_messageType = type; || type != _messageType
_messagePosition = position; || position != _messagePosition)
_progressIndicator = false; {
_messageContent = message;
_messageType = type;
_messagePosition = position;
_progressIndicator = false;
Refresh(); StateHasChanged();
}
} }
public void ShowProgressIndicator() public void ShowProgressIndicator()
@ -124,25 +132,10 @@
StateHasChanged(); StateHasChanged();
} }
private void DismissMessage() public void DismissMessage()
{ {
_messageContent = ""; _messageContent = "";
} StateHasChanged();
private void Refresh()
{
var updateTop = string.IsNullOrEmpty(_messageContent) || _messagePosition == "top";
var updateBottom = string.IsNullOrEmpty(_messageContent) || _messagePosition == "bottom";
if (updateTop && moduleMessageTop != null)
{
moduleMessageTop.RefreshMessage(_messageContent, _messageType);
}
if (updateBottom && moduleMessageBottom != null)
{
moduleMessageBottom.RefreshMessage(_messageContent, _messageType);
}
} }
protected override async Task OnErrorAsync(Exception exception) protected override async Task OnErrorAsync(Exception exception)

View File

@ -3,8 +3,6 @@
@inject IInstallationService InstallationService @inject IInstallationService InstallationService
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject SiteState SiteState @inject SiteState SiteState
@* the following StreamRendering attribute is required - if it is removed the framework will not render the content in static rendering *@
@attribute [StreamRendering]
@if (_initialized) @if (_initialized)
{ {

View File

@ -1,6 +1,7 @@
@using System.Diagnostics.CodeAnalysis @using System.Diagnostics.CodeAnalysis
@using System.Net @using System.Net
@using Microsoft.AspNetCore.Http @using Microsoft.AspNetCore.Http
@using System.Globalization
@namespace Oqtane.UI @namespace Oqtane.UI
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject SiteState SiteState @inject SiteState SiteState
@ -95,6 +96,7 @@
{ {
Site site = null; Site site = null;
Page page = null; Page page = null;
List<Module> modules = null;
User user = null; User user = null;
var editmode = false; var editmode = false;
var refresh = false; var refresh = false;
@ -103,7 +105,7 @@
_error = ""; _error = "";
Route route = new Route(_absoluteUri, SiteState.Alias.Path); Route route = new Route(_absoluteUri, SiteState.Alias.Path);
int moduleid = int.Parse(route.ModuleId); int moduleid = int.Parse(route.ModuleId, CultureInfo.InvariantCulture);
var action = route.Action; var action = route.Action;
var querystring = Utilities.ParseQueryString(route.Query); var querystring = Utilities.ParseQueryString(route.Query);
@ -171,9 +173,9 @@
visitorId = PageState.VisitorId; visitorId = PageState.VisitorId;
} }
if (PageState.RenderMode == RenderModes.Interactive) if (PageState != null && PageState.RenderMode == RenderModes.Interactive)
{ {
// process any sync events (for synchrozing the client application with the server) // process any sync events (for synchronizing the client application with the server)
var sync = await SyncService.GetSyncEventsAsync(lastsyncdate); var sync = await SyncService.GetSyncEventsAsync(lastsyncdate);
lastsyncdate = sync.SyncDate; lastsyncdate = sync.SyncDate;
if (sync.SyncEvents.Any()) if (sync.SyncEvents.Any())
@ -211,7 +213,7 @@
return; return;
} }
if (PageState == null || refresh || PageState.Page.Path != route.PagePath) if (refresh || PageState == null || PageState.Page.Path != route.PagePath)
{ {
page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase)); page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
} }
@ -251,105 +253,108 @@
} }
} }
if (page != null) // check if user is authorized to view page
if (page != null && UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList) && (Utilities.IsPageModuleVisible(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList)))
{ {
// check if user is authorized to view page // edit mode
if (UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList) && (Utilities.IsPageModuleVisible(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList))) if (user != null)
{ {
// edit mode if (querystring.ContainsKey("editmode") && querystring["edit"] == "true")
if (user != null)
{ {
if (querystring.ContainsKey("editmode") && querystring["edit"] == "true") editmode = true;
}
else
{
editmode = (page.PageId == ((user.Settings.ContainsKey("CP-editmode")) ? int.Parse(user.Settings["CP-editmode"], CultureInfo.InvariantCulture) : -1));
if (!editmode)
{ {
editmode = true; var userSettings = new Dictionary<string, string> { { "CP-editmode", "-1" } };
} await SettingService.UpdateUserSettingsAsync(userSettings, user.UserId);
else
{
editmode = (page.PageId == ((user.Settings.ContainsKey("CP-editmode")) ? int.Parse(user.Settings["CP-editmode"]) : -1));
if (!editmode)
{
var userSettings = new Dictionary<string, string> { { "CP-editmode", "-1" } };
await SettingService.UpdateUserSettingsAsync(userSettings, user.UserId);
}
} }
} }
}
// load additional metadata for current page // get modules for current page
page = ProcessPage(page, site, user, SiteState.Alias); if (refresh || PageState.Modules == null || !PageState.Modules.Any() || PageState.Modules.First().PageId != page.PageId)
{
// load additional metadata for modules modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
(page, site.Modules) = ProcessModules(page, site.Modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);
// populate page state (which acts as a client-side cache for subsequent requests)
_pagestate = new PageState
{
Alias = SiteState.Alias,
Site = site,
Page = page,
User = user,
Uri = new Uri(_absoluteUri, UriKind.Absolute),
Route = route,
QueryString = querystring,
UrlParameters = route.UrlParameters,
ModuleId = moduleid,
Action = action,
EditMode = editmode,
LastSyncDate = lastsyncdate,
RenderMode = RenderMode,
Runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime),
VisitorId = visitorId,
RemoteIPAddress = SiteState.RemoteIPAddress,
ReturnUrl = returnurl,
IsInternalNavigation = _isInternalNavigation,
RenderId = Guid.NewGuid(),
Refresh = false
};
OnStateChange?.Invoke(_pagestate);
if (PageState.RenderMode == RenderModes.Interactive)
{
await ScrollToFragment(_pagestate.Uri);
}
} }
else else
{ {
// Need to redirect 404 as page doesnot exist in a Permission or Timeframe modules = PageState.Modules;
if (route.PagePath != "404") }
{
// redirect to 404 page // load additional metadata for current page
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", "")); page = ProcessPage(page, site, user, SiteState.Alias);
}
// load additional metadata for modules
(page, modules) = ProcessModules(page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);
// populate page state (which acts as a client-side cache for subsequent requests)
_pagestate = new PageState
{
Alias = SiteState.Alias,
Site = site,
Page = page,
Modules = modules,
User = user,
Uri = new Uri(_absoluteUri, UriKind.Absolute),
Route = route,
QueryString = querystring,
UrlParameters = route.UrlParameters,
ModuleId = moduleid,
Action = action,
EditMode = editmode,
LastSyncDate = lastsyncdate,
RenderMode = RenderMode,
Runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime),
VisitorId = visitorId,
RemoteIPAddress = SiteState.RemoteIPAddress,
ReturnUrl = returnurl,
IsInternalNavigation = _isInternalNavigation,
RenderId = Guid.NewGuid(),
Refresh = false
};
OnStateChange?.Invoke(_pagestate);
if (PageState.RenderMode == RenderModes.Interactive)
{
await ScrollToFragment(_pagestate.Uri);
} }
} }
else // page not found else
{ {
// look for url mapping if (page == null)
var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{ {
var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl + route.Query; // check for url mapping
NavigationManager.NavigateTo(url, false); var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{
var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl + route.Query;
NavigationManager.NavigateTo(url, false);
return;
}
} }
else // not mapped else
{ {
if (user == null) if (user == null)
{ {
// redirect to login page if user not logged in as they may need to be authenticated // redirect to login page if user not logged in as they may need to be authenticated
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "login", "?returnurl=" + WebUtility.UrlEncode(route.PathAndQuery))); NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "login", "?returnurl=" + WebUtility.UrlEncode(route.PathAndQuery)));
return;
} }
else }
{
if (route.PagePath != "404") // page not found or user does not have sufficient access
{ if (route.PagePath != "404")
// redirect to 404 page {
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", "")); // redirect to 404 page
} NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", ""));
else }
{ else
// redirect to home page as a fallback {
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", "")); // redirect to home page as a fallback
} NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", ""));
}
} }
} }
} }
@ -484,6 +489,7 @@
// retrieve module component resources // retrieve module component resources
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
module.RenderMode = moduleobject.RenderMode; module.RenderMode = moduleobject.RenderMode;
module.Prerender = moduleobject.Prerender;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -557,7 +563,7 @@
{ {
foreach (var resource in resources) foreach (var resource in resources)
{ {
if (resource.Level != ResourceLevel.Site) if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site)
{ {
if (resource.Url.StartsWith("~")) if (resource.Url.StartsWith("~"))
{ {

View File

@ -1,21 +0,0 @@
@attribute [StreamRendering(false)]
@if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static)
{
<RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" />
}
else
{
<RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, PageState.Site.Prerender)" />
}
@code {
[Parameter]
public SiteState SiteState { get; set; }
[Parameter]
public PageState PageState { get; set; }
[Parameter]
public Module ModuleState { get; set; }
}

View File

@ -1,21 +0,0 @@
@attribute [StreamRendering(true)]
@if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static)
{
<RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" />
}
else
{
<RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, PageState.Site.Prerender)" />
}
@code {
[Parameter]
public SiteState SiteState { get; set; }
[Parameter]
public PageState PageState { get; set; }
[Parameter]
public Module ModuleState { get; set; }
}

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>5.1.0</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,8 +33,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.2" />
<PackageReference Include="MySql.Data" Version="8.3.0" /> <PackageReference Include="MySql.Data" Version="8.4.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>5.1.0</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -34,8 +34,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" /> <PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>5.1.0</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>5.1.0</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> --> <!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> --> <!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Version>5.1.0</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace> <RootNamespace>Oqtane.Maui</RootNamespace>
@ -31,7 +31,7 @@
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid> <ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>5.1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>5.2.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
@ -65,15 +65,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.3" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.6" />
<PackageReference Include="System.Net.Http.Json" Version="8.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.10" /> <PackageReference Include="Microsoft.Maui.Controls" Version="8.0.40" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.10" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.40" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="8.0.10" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="8.0.40" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Client</id> <id>Oqtane.Client</id>
<version>5.1.0</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Framework</id> <id>Oqtane.Framework</id>
<version>5.1.0</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright> <copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v5.1.0/Oqtane.Framework.5.1.0.Upgrade.zip</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v5.2.0/Oqtane.Framework.5.2.0.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane framework</tags> <tags>oqtane framework</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Server</id> <id>Oqtane.Server</id>
<version>5.1.0</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Shared</id> <id>Oqtane.Shared</id>
<version>5.1.0</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Updater</id> <id>Oqtane.Updater</id>
<version>5.1.0</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.1.0.Install.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.0.Install.zip" -Force

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.1.0.Upgrade.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.0.Upgrade.zip" -Force

View File

@ -16,6 +16,7 @@
@using Oqtane.Shared @using Oqtane.Shared
@using Oqtane.Themes @using Oqtane.Themes
@using Oqtane.Extensions @using Oqtane.Extensions
@using System.Globalization
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IAntiforgery Antiforgery @inject IAntiforgery Antiforgery
@inject IConfigManager ConfigManager @inject IConfigManager ConfigManager
@ -39,7 +40,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/" /> <base href="/" />
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css" />
@if (!string.IsNullOrEmpty(_PWAScript)) @if (_scripts.Contains("PWA Manifest"))
{ {
<link id="app-manifest" rel="manifest" /> <link id="app-manifest" rel="manifest" />
} }
@ -68,20 +69,13 @@
<Routes PageState="@_pageState" RenderMode="@_renderMode" Runtime="@_runtime" AntiForgeryToken="@_antiForgeryToken" AuthorizationToken="@_authorizationToken" Platform="@_platform" @rendermode="InteractiveRenderMode.GetInteractiveRenderMode(_runtime, _prerender)" /> <Routes PageState="@_pageState" RenderMode="@_renderMode" Runtime="@_runtime" AntiForgeryToken="@_antiForgeryToken" AuthorizationToken="@_authorizationToken" Platform="@_platform" @rendermode="InteractiveRenderMode.GetInteractiveRenderMode(_runtime, _prerender)" />
} }
@if (!string.IsNullOrEmpty(_reconnectScript)) <script src="_framework/blazor.web.js"></script>
{
@((MarkupString)_reconnectScript)
}
@if (!string.IsNullOrEmpty(_PWAScript))
{
@((MarkupString)_PWAScript)
}
@((MarkupString)_bodyResources)
<script src="js/app.js"></script> <script src="js/app.js"></script>
<script src="js/loadjs.min.js"></script> <script src="js/loadjs.min.js"></script>
<script src="js/interop.js"></script> <script src="js/interop.js"></script>
<script src="_framework/blazor.web.js"></script>
@((MarkupString)_scripts)
@((MarkupString)_bodyResources)
} }
else else
{ {
@ -105,8 +99,7 @@
private string _headResources = ""; private string _headResources = "";
private string _bodyResources = ""; private string _bodyResources = "";
private string _styleSheets = ""; private string _styleSheets = "";
private string _PWAScript = ""; private string _scripts = "";
private string _reconnectScript = "";
private string _message = ""; private string _message = "";
private PageState _pageState; private PageState _pageState;
@ -139,6 +132,7 @@
_renderMode = site.RenderMode; _renderMode = site.RenderMode;
_runtime = site.Runtime; _runtime = site.Runtime;
_prerender = site.Prerender; _prerender = site.Prerender;
var modules = new List<Module>();
Route route = new Route(url, alias.Path); Route route = new Route(url, alias.Path);
var page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase)); var page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
@ -163,6 +157,10 @@
{ {
HandlePageNotFound(site, page, route); HandlePageNotFound(site, page, route);
} }
else
{
modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
}
if (site.VisitorTracking) if (site.VisitorTracking)
{ {
@ -175,23 +173,25 @@
CreateJwtToken(alias); CreateJwtToken(alias);
} }
// include stylesheets to prevent FOUC // includes resources
var resources = GetPageResources(alias, site, page, int.Parse(route.ModuleId), route.Action); var resources = await GetPageResources(alias, site, page, modules, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action);
ManageStyleSheets(resources); ManageStyleSheets(resources);
ManageScripts(resources, alias);
// scripts // generate scripts
if (_renderMode == RenderModes.Static)
{
ManageScripts(resources, alias);
}
if (_renderMode == RenderModes.Interactive && _runtime == Runtimes.Server) if (_renderMode == RenderModes.Interactive && _runtime == Runtimes.Server)
{ {
_reconnectScript = CreateReconnectScript(); _scripts += CreateReconnectScript();
} }
if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null) if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null)
{ {
_PWAScript = CreatePWAScript(alias, site, route); _scripts += CreatePWAScript(alias, site, route);
} }
@if (_renderMode == RenderModes.Static)
{
_scripts += CreateScrollPositionScript();
}
_headResources += ParseScripts(site.HeadContent); _headResources += ParseScripts(site.HeadContent);
_bodyResources += ParseScripts(site.BodyContent); _bodyResources += ParseScripts(site.BodyContent);
@ -226,6 +226,7 @@
Alias = alias, Alias = alias,
Site = site, Site = site,
Page = page, Page = page,
Modules = modules,
User = null, User = null,
Uri = new Uri(url, UriKind.Absolute), Uri = new Uri(url, UriKind.Absolute),
Route = route, Route = route,
@ -329,14 +330,26 @@
int? userid = Context.User.UserId(); int? userid = Context.User.UserId();
userid = (userid == -1) ? null : userid; userid = (userid == -1) ? null : userid;
// check if cookie already exists // get cookie value
var visitorCookieName = Constants.VisitorCookiePrefix + SiteId.ToString();
var visitorCookieValue = Context.Request.Cookies[visitorCookieName];
DateTime expiry = DateTime.MinValue;
if (visitorCookieValue != null && visitorCookieValue.Contains("|"))
{
var values = visitorCookieValue.Split('|');
int.TryParse(values[0], out _visitorId);
DateTime.TryParseExact(values[1], "M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out expiry);
}
else // legacy cookie format
{
int.TryParse(visitorCookieValue, out _visitorId);
}
bool setcookie = false;
Visitor visitor = null; Visitor visitor = null;
bool addcookie = false;
var VisitorCookie = Constants.VisitorCookiePrefix + SiteId.ToString(); if (_visitorId <= 0)
if (!int.TryParse(Context.Request.Cookies[VisitorCookie], out _visitorId))
{ {
// if enabled use IP Address correlation // if enabled use IP Address correlation
_visitorId = -1;
var correlate = bool.Parse(settings.GetValue("VisitorCorrelation", "true")); var correlate = bool.Parse(settings.GetValue("VisitorCorrelation", "true"));
if (correlate) if (correlate)
{ {
@ -344,12 +357,12 @@
if (visitor != null) if (visitor != null)
{ {
_visitorId = visitor.VisitorId; _visitorId = visitor.VisitorId;
addcookie = true; setcookie = true;
} }
} }
} }
if (_visitorId == -1) if (_visitorId <= 0)
{ {
// create new visitor // create new visitor
visitor = new Visitor(); visitor = new Visitor();
@ -365,52 +378,59 @@
visitor.VisitedOn = DateTime.UtcNow; visitor.VisitedOn = DateTime.UtcNow;
visitor = VisitorRepository.AddVisitor(visitor); visitor = VisitorRepository.AddVisitor(visitor);
_visitorId = visitor.VisitorId; _visitorId = visitor.VisitorId;
addcookie = true; setcookie = true;
} }
else else
{ {
if (visitor == null) // check expiry
if (DateTime.UtcNow > expiry)
{ {
// get visitor if it was not previously loaded if (visitor == null)
visitor = VisitorRepository.GetVisitor(_visitorId);
}
if (visitor != null)
{
// update visitor
visitor.IPAddress = _remoteIPAddress;
visitor.UserAgent = useragent;
visitor.Language = language;
visitor.Url = url;
if (!string.IsNullOrEmpty(referrer))
{ {
visitor.Referrer = referrer; // get visitor if not previously loaded
visitor = VisitorRepository.GetVisitor(_visitorId);
} }
if (userid != null) if (visitor != null)
{ {
visitor.UserId = userid; // update visitor
visitor.IPAddress = _remoteIPAddress;
visitor.UserAgent = useragent;
visitor.Language = language;
visitor.Url = url;
if (!string.IsNullOrEmpty(referrer))
{
visitor.Referrer = referrer;
}
if (userid != null)
{
visitor.UserId = userid;
}
visitor.Visits += 1;
visitor.VisitedOn = DateTime.UtcNow;
VisitorRepository.UpdateVisitor(visitor);
setcookie = true;
}
else
{
// remove cookie if visitor does not exist
Context.Response.Cookies.Delete(visitorCookieName);
} }
visitor.Visits += 1;
visitor.VisitedOn = DateTime.UtcNow;
VisitorRepository.UpdateVisitor(visitor);
}
else
{
// remove cookie if VisitorId does not exist
Context.Response.Cookies.Delete(VisitorCookie);
} }
} }
// append cookie // set cookie
if (addcookie) if (setcookie)
{ {
expiry = DateTime.UtcNow.AddMinutes(int.Parse(settings.GetValue("VisitorDuration", "5")));
Context.Response.Cookies.Append( Context.Response.Cookies.Append(
VisitorCookie, visitorCookieName,
_visitorId.ToString(), $"{_visitorId}|{expiry.ToString("M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture)}",
new CookieOptions() new CookieOptions()
{ {
Expires = DateTimeOffset.UtcNow.AddYears(1), Expires = DateTimeOffset.UtcNow.AddYears(10),
IsEssential = true IsEssential = true
} }
); );
} }
} }
@ -432,7 +452,7 @@
private string CreatePWAScript(Alias alias, Site site, Route route) private string CreatePWAScript(Alias alias, Site site, Route route)
{ {
return return Environment.NewLine +
"<script>" + Environment.NewLine + "<script>" + Environment.NewLine +
" // PWA Manifest" + Environment.NewLine + " // PWA Manifest" + Environment.NewLine +
" setTimeout(() => {" + Environment.NewLine + " setTimeout(() => {" + Environment.NewLine +
@ -468,14 +488,14 @@
" console.log('ServiceWorker Registration Failed ', err);" + Environment.NewLine + " console.log('ServiceWorker Registration Failed ', err);" + Environment.NewLine +
" });" + Environment.NewLine + " });" + Environment.NewLine +
" };" + Environment.NewLine + " };" + Environment.NewLine +
"</script>"; "</script>" + Environment.NewLine;
} }
private string CreateReconnectScript() private string CreateReconnectScript()
{ {
return return Environment.NewLine +
"<script>" + Environment.NewLine + "<script>" + Environment.NewLine +
" // Blazor Server Reconnect" + Environment.NewLine + " // Interactive Blazor Server Reconnect" + Environment.NewLine +
" new MutationObserver((mutations, observer) => {" + Environment.NewLine + " new MutationObserver((mutations, observer) => {" + Environment.NewLine +
" if (document.querySelector('#components-reconnect-modal h5 a')) {" + Environment.NewLine + " if (document.querySelector('#components-reconnect-modal h5 a')) {" + Environment.NewLine +
" async function attemptReload() {" + Environment.NewLine + " async function attemptReload() {" + Environment.NewLine +
@ -487,7 +507,26 @@
" setInterval(attemptReload, 5000);" + Environment.NewLine + " setInterval(attemptReload, 5000);" + Environment.NewLine +
" }" + Environment.NewLine + " }" + Environment.NewLine +
" }).observe(document.body, { childList: true, subtree: true });" + Environment.NewLine + " }).observe(document.body, { childList: true, subtree: true });" + Environment.NewLine +
"</script>"; "</script>" + Environment.NewLine;
}
private string CreateScrollPositionScript()
{
return Environment.NewLine +
"<script>" + Environment.NewLine +
" // Blazor Static Rendering Scroll Position" + Environment.NewLine +
" window.interceptNavigation = () => {" + Environment.NewLine +
" let currentUrl = window.location.href;" + Environment.NewLine +
" Blazor.addEventListener('enhancedload', () => {" + Environment.NewLine +
" let newUrl = window.location.href;" + Environment.NewLine +
" if (currentUrl != newUrl) {" + Environment.NewLine +
" window.scrollTo({ top: 0, left: 0, behavior: 'instant' });" + Environment.NewLine +
" }" + Environment.NewLine +
" currentUrl = newUrl;" + Environment.NewLine +
" });" + Environment.NewLine +
" };" + Environment.NewLine +
" document.onload += window.interceptNavigation();" + Environment.NewLine +
"</script>" + Environment.NewLine;
} }
private string ParseScripts(string content) private string ParseScripts(string content)
@ -536,7 +575,7 @@
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") + ((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") + ((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
((resource.ES6Module) ? " type=\"module\"" : "") + ((resource.ES6Module) ? " type=\"module\"" : "") +
" src =\"" + url + "\"></script>"; // src at end of element due to enhanced navigation patch algorithm " src=\"" + url + "\"></script>"; // src at end of element due to enhanced navigation patch algorithm
} }
else else
{ {
@ -558,7 +597,7 @@
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture))); CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)));
} }
private List<Resource> GetPageResources(Alias alias, Site site, Page page, int moduleid, string action) private async Task<List<Resource>> GetPageResources(Alias alias, Site site, Page page, List<Module> modules, int moduleid, string action)
{ {
var resources = new List<Resource>(); var resources = new List<Resource>();
@ -566,13 +605,13 @@
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType)); var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType));
if (theme != null) if (theme != null)
{ {
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName)); resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode);
} }
else else
{ {
// fallback to default Oqtane theme // fallback to default Oqtane theme
theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme)); theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme));
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName)); resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode);
} }
var type = Type.GetType(themeType); var type = Type.GetType(themeType);
if (type != null) if (type != null)
@ -580,16 +619,16 @@
var obj = Activator.CreateInstance(type) as IThemeControl; var obj = Activator.CreateInstance(type) as IThemeControl;
if (obj != null) if (obj != null)
{ {
resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace); resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode);
} }
} }
foreach (Module module in site.Modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid)) foreach (Module module in modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid))
{ {
var typename = ""; var typename = "";
if (module.ModuleDefinition != null) if (module.ModuleDefinition != null)
{ {
resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName)); resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode);
// handle default action // handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -635,7 +674,7 @@
var obj = Activator.CreateInstance(moduletype) as IModuleControl; var obj = Activator.CreateInstance(moduletype) as IModuleControl;
if (obj != null) if (obj != null)
{ {
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
{ {
// settings components are embedded within a framework settings module // settings components are embedded within a framework settings module
@ -643,45 +682,51 @@
if (moduletype != null) if (moduletype != null)
{ {
obj = Activator.CreateInstance(moduletype) as IModuleControl; obj = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
} }
} }
} }
} }
} }
// site level resources for modules in site if (site.RenderMode == RenderModes.Interactive)
var modules = site.Modules.GroupBy(item => item.ModuleDefinition?.ModuleDefinitionName).Select(group => group.First()).ToList();
foreach (var module in modules)
{ {
if (module.ModuleDefinition?.Resources != null) // site level resources for modules in site
var sitemodules = await SiteService.GetModulesAsync(site.SiteId, -1);
foreach (var module in sitemodules.GroupBy(item => item.ModuleDefinition?.ModuleDefinitionName).Select(group => group.First()).ToList())
{ {
resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName)); if (module.ModuleDefinition?.Resources != null)
{
resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode);
}
} }
} }
return resources; return resources;
} }
private List<Resource> AddResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name) private List<Resource> AddResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string rendermode)
{ {
if (resources != null) if (resources != null)
{ {
foreach (var resource in resources) foreach (var resource in resources)
{ {
if (resource.Url.StartsWith("~")) if (rendermode == RenderModes.Static || resource.ResourceType == ResourceType.Stylesheet || resource.Level == ResourceLevel.Site)
{ {
resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); if (resource.Url.StartsWith("~"))
} {
if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
{ }
resource.Url = alias.BaseUrl + resource.Url; if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
} {
resource.Url = alias.BaseUrl + resource.Url;
}
// ensure resource does not exist already // ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower()))
{ {
pageresources.Add(resource.Clone(level, name)); pageresources.Add(resource.Clone(level, name));
}
} }
} }
} }
@ -692,6 +737,7 @@
{ {
if (resources != null) if (resources != null)
{ {
// include stylesheets to prevent FOUC
string batch = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff"); string batch = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff");
int count = 0; int count = 0;
foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Stylesheet)) foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Stylesheet))

View File

@ -0,0 +1,16 @@
// This is just a placeholder file
// It is necessary for the documentation to successfully build this project.
// Reason is that docfx will run the .net compiler and find references
// to this class in the project.
// But since the real class is just a .razor file, ATM docfx will fail.
//
// Note added 2024-06-27 by @iJungleboy.
// We hope that as .net and docfx improve, the razor-compiler will work in that scenario
// as well, and this file can be removed.
using Oqtane.Documentation;
namespace Oqtane.Components;
[PrivateApi]
public class _Placeholder;

View File

@ -760,7 +760,7 @@ namespace Oqtane.Controllers
{ {
if (!Directory.Exists(folderpath)) if (!Directory.Exists(folderpath))
{ {
string path = ""; string path = folderpath.StartsWith(Path.DirectorySeparatorChar) ? Path.DirectorySeparatorChar.ToString() : string.Empty;
var separators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; var separators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
string[] folders = folderpath.Split(separators, StringSplitOptions.RemoveEmptyEntries); string[] folders = folderpath.Split(separators, StringSplitOptions.RemoveEmptyEntries);
foreach (string folder in folders) foreach (string folder in folders)

View File

@ -76,5 +76,20 @@ namespace Oqtane.Controllers
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
} }
[HttpDelete]
[Authorize(Roles = RoleNames.Admin)]
public void Delete(string siteId)
{
if (int.TryParse(siteId, out int parsedSiteId) && parsedSiteId == _alias.SiteId)
{
_logs.DeleteLogs(parsedSiteId, 0); // specifying zero for age results in all logs being deleted
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Log Delete Attempt {SiteId}", siteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
} }
} }

View File

@ -9,7 +9,6 @@ using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security; using Oqtane.Security;
using System.Net; using System.Net;
using System.Security.Policy;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {

View File

@ -365,8 +365,8 @@ namespace Oqtane.Controllers
{ {
{ "FrameworkVersion", moduleDefinition.Version }, { "FrameworkVersion", moduleDefinition.Version },
{ "ClientReference", $"<PackageReference Include=\"Oqtane.Client\" Version=\"{moduleDefinition.Version}\" />" }, { "ClientReference", $"<PackageReference Include=\"Oqtane.Client\" Version=\"{moduleDefinition.Version}\" />" },
{ "ServerReference", $"<PackageReference Include=\"Oqtane.Client\" Version=\"{moduleDefinition.Version}\" />" }, { "ServerReference", $"<PackageReference Include=\"Oqtane.Server\" Version=\"{moduleDefinition.Version}\" />" },
{ "SharedReference", $"<PackageReference Include=\"Oqtane.Client\" Version=\"{moduleDefinition.Version}\" />" }, { "SharedReference", $"<PackageReference Include=\"Oqtane.Shared\" Version=\"{moduleDefinition.Version}\" />" },
}; };
}); });
} }

View File

@ -0,0 +1,41 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Documentation;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Services;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class SearchResultsController : ModuleControllerBase
{
private readonly ISearchService _searchService;
public SearchResultsController(ISearchService searchService, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor)
{
_searchService = searchService;
}
[HttpPost]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<Models.SearchResults> Post([FromBody] Models.SearchQuery searchQuery)
{
try
{
return await _searchService.SearchAsync(searchQuery);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Other, ex, "Fetch search results failed.", searchQuery);
HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return null;
}
}
}
}

View File

@ -81,5 +81,12 @@ namespace Oqtane.Controllers
{ {
await _siteService.DeleteSiteAsync(id); await _siteService.DeleteSiteAsync(id);
} }
// GET api/<controller>/modules/5/6
[HttpGet("modules/{siteId}/{pageId}")]
public async Task<IEnumerable<Module>> GetModules(int siteId, int pageId)
{
return await _siteService.GetModulesAsync(siteId, pageId);
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More