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

This commit is contained in:
Mark Davis 2024-07-05 21:53:14 -07:00
commit 4b17847ea5
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
{
Folder folder;
if (_folderId != -1)
{
folder = await FolderService.GetFolderAsync(_folderId);
@ -179,8 +180,6 @@
folder = new Folder();
}
folder.SiteId = PageState.Site.SiteId;
if (_parentId == -1)
{
folder.ParentId = null;
@ -189,7 +188,15 @@
{
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.Type = _type;
folder.ImageSizes = _imagesizes;

View File

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

View File

@ -63,7 +63,7 @@ else
<th>@Localizer["Function"]</th>
</Header>
<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.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td>
@ -86,47 +86,48 @@ else
</div>
</div>
<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>
</TabStrip>
}
@code {
private string _level = "-";
private string _function = "-";
private string _rows = "10";
private int _page = 1;
private List<Log> _logs;
private int _retention = 30;
private string _level = "-";
private string _function = "-";
private string _rows = "10";
private int _page = 1;
private List<Log> _logs;
private int _retention = 30;
public override string UrlParametersTemplate => "/{level}/{function}/{rows}/{page}";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override string UrlParametersTemplate => "/{level}/{function}/{rows}/{page}";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync()
{
try
{
if (UrlParameters.ContainsKey("level"))
{
_level = UrlParameters["level"];
}
if (UrlParameters.ContainsKey("function"))
{
_function = UrlParameters["function"];
}
if (UrlParameters.ContainsKey("rows"))
{
_rows = UrlParameters["rows"];
}
if (UrlParameters.ContainsKey("page") && int.TryParse(UrlParameters["page"], out int page))
{
_page = page;
}
protected override async Task OnParametersSetAsync()
{
try
{
if (UrlParameters.ContainsKey("level"))
{
_level = UrlParameters["level"];
}
if (UrlParameters.ContainsKey("function"))
{
_function = UrlParameters["function"];
}
if (UrlParameters.ContainsKey("rows"))
{
_rows = UrlParameters["rows"];
}
if (UrlParameters.ContainsKey("page") && int.TryParse(UrlParameters["page"], out int page))
{
_page = page;
}
await GetLogs();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = int.Parse( SettingService.GetSetting(settings, "LogRetention", "30"));
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = int.Parse( SettingService.GetSetting(settings, "LogRetention", "30"));
}
catch (Exception ex)
{
@ -213,22 +214,37 @@ else
return classname;
}
private async Task SaveSiteSettings()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "LogRetention", _retention.ToString(), true);
private async Task SaveSiteSettings()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "LogRetention", _retention.ToString(), true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
}
}
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
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)
{

View File

@ -32,7 +32,7 @@
<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>
<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 class="row mb-1 align-items-center">
@ -306,10 +306,10 @@
_languages = _languages.OrderBy(item => item.Name).ToList();
}
// Group modules by PageId
// Get distinct PageIds where modules are present
var distinctPageIds = PageState.Modules
.Where(md => md.ModuleDefinition.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false)
// get distinct pages where module exists
var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
var distinctPageIds = modules
.Where(md => md.ModuleDefinition?.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false)
.Select(md => md.PageId)
.Distinct();

View File

@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.ModuleDefinitions
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IModuleService ModuleService
@inject IModuleDefinitionService ModuleDefinitionService
@inject IPackageService PackageService
@inject IStringLocalizer<Index> Localizer
@ -50,7 +51,7 @@ else
<th style="width: 1px;">&nbsp;</th>
</Header>
<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>
@if (context.AssemblyName != Constants.ClientId)
{
@ -70,7 +71,7 @@ else
}
</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>
}
@ -99,6 +100,7 @@ else
}
@code {
private List<Module> _modules;
private List<ModuleDefinition> _allModuleDefinitions;
private List<ModuleDefinition> _moduleDefinitions;
private List<Package> _packages;
@ -111,6 +113,7 @@ else
{
try
{
_modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
_allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',')).Distinct().ToList();
await LoadModuleDefinitions();

View File

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

View File

@ -198,12 +198,6 @@
</div>
</div>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading=@Localizer["Theme.Heading"] ResourceKey="ThemeSettings">
@_themeSettingsComponent
</TabPanel>
}
</TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
@ -238,9 +232,6 @@
private string _bodycontent;
private string _permissions = null;
private PermissionGrid _permissionGrid;
private Type _themeSettingsType;
private object _themeSettings;
private RenderFragment _themeSettingsComponent { get; set; }
private bool _refresh = false;
protected Page _parent = null;
protected Dictionary<string, string> _icons;
@ -281,7 +272,6 @@
}
_effectivedate = Utilities.UtcAsLocalDate(PageState.Page.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(PageState.Page.ExpiryDate);
ThemeSettings();
_initialized = true;
}
else
@ -324,7 +314,6 @@
_themetype = (string)e.Value;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = _containers.First().TypeName;
ThemeSettings();
StateHasChanged();
// 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()
{
validated = true;
@ -385,45 +352,34 @@
page.ParentId = Int32.Parse(_parentid);
}
// path can be a link to an external url
if (!_path.Contains("://"))
if (string.IsNullOrEmpty(_path))
{
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(_path.LastIndexOf("/") + 1);
_path = _path.Substring(0, _path.Length - 1);
}
if (_parentid == "-1")
{
page.Path = Utilities.GetFriendlyUrl(_path);
}
else
{
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;
_path = _path.Substring(_path.LastIndexOf("/") + 1);
}
if (_parentid == "-1")
{
page.Path = Utilities.GetFriendlyUrl(_path);
}
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);
@ -493,18 +449,11 @@
await logger.LogInformation("Page Added {Page}", page);
if (!string.IsNullOrEmpty(PageState.ReturnUrl))
{
NavigationManager.NavigateTo(PageState.ReturnUrl, true);
NavigationManager.NavigateTo(NavigateUrl(page.Path), true); // redirect to page added and reload
}
else
{
if (!page.Path.Contains("://"))
{
NavigationManager.NavigateTo(page.Path); // redirect to new page created
}
else
{
NavigationManager.NavigateTo(NavigateUrl("admin/pages"));
}
NavigationManager.NavigateTo(NavigateUrl()); // redirect to page management
}
}
else

View File

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

View File

@ -17,7 +17,7 @@
<th>@SharedLocalizer["Name"]</th>
</Header>
<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><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>

View File

@ -22,7 +22,7 @@ else
<th>@Localizer["Order"]</th>
</Header>
<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>@context.Name</td>
<td>@context.Title</td>

View File

@ -20,9 +20,9 @@ else
<th>@SharedLocalizer["Name"]</th>
</Header>
<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><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>
</Row>
</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">
<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">
<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.Static">@(SharedLocalizer["RenderMode" + @RenderModes.Static])</option>
<option value="@RenderModes.Headless">@(SharedLocalizer["RenderMode" + @RenderModes.Headless])</option>
@ -337,7 +337,7 @@
</div>
</div>
<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">
<select id="prerender" class="form-select" @bind="@_prerender" required>
<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()
{
validated = true;

View File

@ -26,6 +26,12 @@
<input id="osversion" class="form-control" @bind="@_osversion" readonly />
</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">
<Label Class="col-sm-3" For="machinename" HelpText="Machine Name" ResourceKey="MachineName">Machine Name: </Label>
<div class="col-sm-9">
@ -62,12 +68,6 @@
<input id="servertime" class="form-control" @bind="@_servertime" readonly />
</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">
<Label Class="col-sm-3" For="workingset" HelpText="Memory Allocation Of Service (in MB)" ResourceKey="WorkingSet">Memory Allocation: </Label>
<div class="col-sm-9">
@ -165,13 +165,13 @@
private string _version = string.Empty;
private string _clrversion = 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 _environment = string.Empty;
private string _contentrootpath = string.Empty;
private string _webrootpath = string.Empty;
private string _servertime = string.Empty;
private string _tickcount = string.Empty;
private string _workingset = string.Empty;
private string _installationid = string.Empty;
@ -192,13 +192,13 @@
{
_clrversion = systeminfo["CLRVersion"].ToString();
_osversion = systeminfo["OSVersion"].ToString();
_machinename = systeminfo["MachineName"].ToString();
_process = systeminfo["Process"].ToString();
_machinename = systeminfo["MachineName"].ToString();
_ipaddress = systeminfo["IPAddress"].ToString();
_environment = systeminfo["Environment"].ToString();
_contentrootpath = systeminfo["ContentRootPath"].ToString();
_webrootpath = systeminfo["WebRootPath"].ToString();
_servertime = systeminfo["ServerTime"].ToString() + " UTC";
_tickcount = TimeSpan.FromMilliseconds(Convert.ToInt64(systeminfo["TickCount"].ToString())).ToString();
_workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB";
}

View File

@ -29,7 +29,7 @@ else
<th>&nbsp;</th>
</Header>
<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>
@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>
}
</td>
<td></td>
</Row>
</Pager>
}

View File

@ -37,7 +37,7 @@ else
<th>@Localizer["Requested"]</th>
</Header>
<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>
<a href="@Utilities.TenantUrl(PageState.Alias, context.Url)">@context.Url</a>

View File

@ -226,11 +226,6 @@
user.Password = _password;
user.Email = email;
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));

View File

@ -32,13 +32,13 @@ else
</Header>
<Row>
<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>
<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>
<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>@context.User.Username</td>
<td>@context.User.DisplayName</td>

View File

@ -43,7 +43,7 @@ else
<th>@Localizer["Created"]</th>
</Header>
<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>
@if (context.UserId != null)
@ -69,14 +69,20 @@ else
</select>
</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>
<div class="col-sm-9">
<textarea id="filter" class="form-control" @bind="@_filter" rows="3"></textarea>
</div>
</div>
<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">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
</div>
@ -103,7 +109,8 @@ else
private int _page = 1;
private List<Visitor> _visitors;
private string _tracking;
private string _filter = "";
private int _duration = 5;
private string _filter = "";
private int _retention = 30;
private string _correlation = "true";
@ -128,7 +135,8 @@ else
_tracking = PageState.Site.VisitorTracking.ToString();
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"));
_correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true");
}
@ -179,7 +187,8 @@ else
await SiteService.UpdateSiteAsync(site);
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, "VisitorCorrelation", _correlation, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);

View File

@ -35,11 +35,11 @@
{
if (Disabled)
{
<button type="button" class="@Class" disabled>@((MarkupString)_iconSpan) @Text</button>
<button type="button" class="@Class" disabled>@((MarkupString)_openIconSpan) @_openText</button>
}
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">
<h5 class="modal-title">@Header</h5>
<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>
</form>
</div>
@ -65,12 +65,12 @@ else
@if (!string.IsNullOrEmpty(Action))
{
<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>
</form>
}
<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>
</form>
</div>
@ -83,13 +83,13 @@ else
{
if (Disabled)
{
<button type="button" class="@Class" disabled>@((MarkupString)_iconSpan) @Text</button>
<button type="button" class="@Class" disabled>@((MarkupString)_openIconSpan) @_openText</button>
}
else
{
<form method="post" @formname="@($"ActionDialogActionForm{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_iconSpan) @Text</button>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_openIconSpan) @_openText</button>
</form>
}
}
@ -101,6 +101,8 @@ else
private bool _editmode = false;
private bool _authorized = false;
private string _iconSpan = string.Empty;
private string _openIconSpan = string.Empty;
private string _openText = string.Empty;
[Parameter]
public string Header { get; set; } // required
@ -138,6 +140,9 @@ else
[Parameter]
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]
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;
}
_openText = Text;
if (string.IsNullOrEmpty(Class))
{
Class = "btn btn-success";
@ -169,11 +176,20 @@ else
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;
}
_iconSpan = $"<span class=\"{IconName}\"></span>&nbsp;";
_openIconSpan = $"<span class=\"{IconName}\"></span>{(IconOnly ? "" : "&nbsp")}";
_iconSpan = $"<span class=\"{IconName}\"></span>&nbsp";
}
Text = Localize(nameof(Text), Text);

View File

@ -145,7 +145,10 @@
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;
}

View File

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

View File

@ -11,7 +11,7 @@
@if (!string.IsNullOrEmpty(SearchProperties))
{
<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" />
<button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</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">
<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>
@if (_pages > _displayPages && _displayPages > 1)
{
<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 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>
@for (int i = _startPage; i <= _endPage; i++)
{
@ -40,30 +40,30 @@
if (pager == _page)
{
<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>
}
else
{
<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 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>
@if (_pages > _displayPages && _displayPages > 1)
{
<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 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 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>
</ul>
}
@ -73,8 +73,8 @@
@if (!string.IsNullOrEmpty(SearchProperties))
{
<form method="post" autocomplete="off" @formname="PagerForm" @onsubmit="Search" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<div class="input-group my-3">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<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" />
<button type="submit" class="btn btn-primary">@SharedLocalizer["Search"]</button>
<a class="btn btn-secondary" href="@PageUrl(1, "")">@SharedLocalizer["Reset"]</a>
@ -86,16 +86,16 @@
{
<ul class="pagination justify-content-center my-2">
<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>
@if (_pages > _displayPages && _displayPages > 1)
{
<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 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>
@for (int i = _startPage; i <= _endPage; i++)
{
@ -103,30 +103,30 @@
if (pager == _page)
{
<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>
}
else
{
<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 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>
@if (_pages > _displayPages && _displayPages > 1)
{
<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 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 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>
</ul>
}
@ -202,16 +202,16 @@
{
<ul class="pagination justify-content-center my-2">
<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>
@if (_pages > _displayPages && _displayPages > 1)
{
<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 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>
@for (int i = _startPage; i <= _endPage; i++)
{
@ -219,30 +219,30 @@
if (pager == _page)
{
<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>
}
else
{
<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 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>
@if (_pages > _displayPages && _displayPages > 1)
{
<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 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 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>
</ul>
}
@ -250,16 +250,16 @@
{
<ul class="pagination justify-content-center my-2">
<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>
@if (_pages > _displayPages && _displayPages > 1)
{
<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 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>
@for (int i = _startPage; i <= _endPage; i++)
{
@ -267,30 +267,30 @@
if (pager == _page)
{
<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>
}
else
{
<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 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>
@if (_pages > _displayPages && _displayPages > 1)
{
<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 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 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>
</ul>
}
@ -359,6 +359,9 @@
[Parameter]
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]
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
{
public class RichTextEditorInterop
public class QuillEditorInterop
{
private readonly IJSRuntime _jsRuntime;
public RichTextEditorInterop(IJSRuntime jsRuntime)
public QuillEditorInterop(IJSRuntime 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
{
_jsRuntime.InvokeAsync<object>(
"Oqtane.RichTextEditor.insertQuillImage",
quillElement, imageUrl, altText);
quillElement, imageUrl, altText, editorIndex);
return Task.CompletedTask;
}
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 Microsoft.AspNetCore.Components.Rendering
@using Microsoft.Extensions.DependencyInjection
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@inject IServiceProvider ServiceProvider
@inject ISettingService SettingService
@inject IStringLocalizer<RichTextEditor> Localizer
<div class="row" style="margin-bottom: 50px;">
<div class="col">
<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="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>
@_textEditorComponent
</div>
</div>
@code {
private ElementReference _editorElement;
private ElementReference _toolBar;
private bool _richfilemanager = false;
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";
private ITextEditorProvider _textEditorProvider;
private RenderFragment _textEditorComponent;
private ITextEditor _textEditor;
[Parameter]
public string Content { get; set; }
@ -123,7 +25,7 @@
public bool ReadOnly { get; set; } = false;
[Parameter]
public string Placeholder { get; set; } = "Enter Your Content...";
public string Placeholder { get; set; }
[Parameter]
public bool AllowFileManagement { get; set; } = true;
@ -139,152 +41,114 @@
public RenderFragment ToolbarContent { get; set; }
[Parameter]
public string Theme { get; set; } = "snow";
public string Theme { get; set; }
[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" },
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" }
};
_textEditorProvider = await GetTextEditorProvider();
}
protected override void OnParametersSet()
{
_richhtml = Content;
_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)
_textEditorComponent = (builder) =>
{
_activetab = "Raw";
}
CreateTextEditor(builder);
};
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (AllowRichText)
if(_textEditor != null)
{
var interop = new RichTextEditorInterop(JSRuntime);
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);
}
_textEditor.Initialize(Content);
}
}
public void CloseRichFileManager()
{
_richfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRawFileManager()
{
_rawfilemanager = false;
_message = string.Empty;
StateHasChanged();
await base.OnAfterRenderAsync(firstRender);
}
public async Task<string> GetHtml()
{
// evaluate raw html content as first priority
if (_rawhtml != _originalrawhtml)
return await _textEditor.GetContent();
}
private void CreateTextEditor(RenderTreeBuilder builder)
{
if(_textEditorProvider != null)
{
return _rawhtml;
}
else
{
var richhtml = "";
if (AllowRichText)
var editorType = Type.GetType(_textEditorProvider.EditorType);
if (editorType != null)
{
// return rich text content if it has changed
var interop = new RichTextEditorInterop(JSRuntime);
richhtml = await interop.GetHtml(_editorElement);
builder.OpenComponent(0, editorType);
//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()
{
_message = string.Empty;
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();
}
private async Task<ITextEditorProvider> GetTextEditorProvider()
{
const string DefaultEditorName = "Quill";
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("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);
_rawfilemanager = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_rawfilemanager = true;
}
StateHasChanged();
}
var editorName = await GetTextEditorName(DefaultEditorName);
var editorProviders = ServiceProvider.GetServices<ITextEditorProvider>();
var editorProvider = editorProviders.FirstOrDefault(i => i.Name == editorName);
if(editorProvider == null)
{
editorProvider = editorProviders.FirstOrDefault(i => i.Name == DefaultEditorName);
}
return editorProvider;
}
private async Task<string> GetTextEditorName(string defaultName)
{
const string EditorSettingName = "TextEditor";
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
return SettingService.GetSetting(settings, EditorSettingName, defaultName);
}
}

View File

@ -23,7 +23,7 @@
</li>
}
</ul>
<div class="tab-content">
<div class="tab-content @TabContentClass">
<br />
@ChildContent
</div>
@ -47,6 +47,9 @@
[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)
[Parameter]
public string TabContentClass { get; set; } // optional - to extend the TabContent div.
protected override void OnInitialized()
{
if (string.IsNullOrEmpty(Id))

View File

@ -37,6 +37,10 @@
content = htmltext.Content;
content = Utilities.FormatContent(content, PageState.Alias, "render");
}
else
{
content = "";
}
}
}
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 bool? Prerender { get { return null; } } // allows the Site Prerender property to be overridden
// url parameters
public virtual string UrlParametersTemplate { get; set; }
@ -276,7 +278,6 @@ namespace Oqtane.Modules
public void AddModuleMessage(string message, MessageType type, string position)
{
ClearModuleMessage();
RenderModeBoundary.AddModuleMessage(message, type, position);
}

View File

@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>5.1.0</Version>
<Version>5.2.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -12,7 +12,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<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>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -22,9 +22,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
</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>
<!--
Microsoft ResX Schema
@ -153,7 +153,7 @@
<data name="Integrated" xml:space="preserve">
<value>Integrated</value>
</data>
<data name="Encryption,Text" xml:space="preserve">
<data name="Encryption.Text" xml:space="preserve">
<value>Encryption:</value>
</data>
<data name="Encryption.HelpText" xml:space="preserve">

View File

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

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -210,4 +210,16 @@
<data name="Success.SaveSiteSettings" xml:space="preserve">
<value>Settings Saved Successfully</value>
</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>

View File

@ -156,7 +156,7 @@
<data name="Module.Text" xml:space="preserve">
<value>Module:</value>
</data>
<data name="Module Settings" xml:space="preserve">
<data name="ModuleSettings.Heading" xml:space="preserve">
<value>Module Settings</value>
</data>
<data name="Pane.HelpText" xml:space="preserve">
@ -177,4 +177,16 @@
<data name="ExpiryDate.Text" xml:space="preserve">
<value>Expiry Date: </value>
</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>

View File

@ -147,4 +147,7 @@
<data name="Title" xml:space="preserve">
<value>Title</value>
</data>
<data name="Detail.Text" xml:space="preserve">
<value>Detail</value>
</data>
</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>
</data>
<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 name="Prerender.Text" xml:space="preserve">
<value>Prerender? </value>
<value>Prerender: </value>
</data>
<data name="RenderMode.HelpText" xml:space="preserve">
<value>The default render mode for the site</value>

View File

@ -255,12 +255,6 @@
<data name="MachineName.Text" xml:space="preserve">
<value>Machine Name:</value>
</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">
<value>Server Web Root Path</value>
</data>
@ -294,4 +288,10 @@
<data name="Error.ClearLog" xml:space="preserve">
<value>Ann Error Occurred Clearing The System Log</value>
</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>

View File

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

View File

@ -184,7 +184,7 @@
<value>Number of days of visitor activity to retain</value>
</data>
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
<value>Retention:</value>
</data>
<data name="Correlation.HelpText" xml:space="preserve">
<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">
<value>Correlate Visitors?</value>
</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>

View File

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

View File

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

View File

@ -198,4 +198,7 @@
<data name="LocationTop" xml:space="preserve">
<value>Top</value>
</data>
<data name="Module.CopyExisting" xml:space="preserve">
<value>Copy Existing Module</value>
</data>
</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>
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>
/// Creates a new log entry
/// </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>
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]
[Obsolete("This method is deprecated.", false)]
void SetAlias(Alias alias);

View File

@ -35,6 +35,11 @@ namespace Oqtane.Services
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)
{
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)
{
return await PutJsonAsync<Profile>($"{Apiurl}/{profile.SiteId}", profile);
return await PutJsonAsync<Profile>($"{Apiurl}/{profile.ProfileId}", profile);
}
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}");
}
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)]
public void SetAlias(Alias alias)
{

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Oqtane.Documentation;
using Oqtane.Shared;
using System;
using System.Globalization;
namespace Oqtane.Services
{
@ -18,7 +19,7 @@ namespace Oqtane.Services
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();
}

View File

@ -1,6 +1,5 @@
@namespace Oqtane.Themes
@inherits ContainerBase
@inject NavigationManager NavigationManager
<div class="app-admin-modal">
<div class="modal" tabindex="-1" role="dialog">
@ -8,10 +7,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><ModuleTitle /></h5>
<form method="post" class="app-form-inline" @formname="AdminContainerForm" @onsubmit="@CloseModal" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="btn-close" aria-label="Close"></button>
</form>
<a href="@_url" class="btn-close" aria-label="Close"></a>
</div>
<div class="modal-body">
<ModuleInstance />
@ -22,9 +18,11 @@
</div>
@code {
private void CloseModal()
{
NavigationManager.NavigateTo((!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : NavigateUrl());
private string _url;
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="sidebar">
<nav class="navbar">
<Logo /><Menu Orientation="Vertical" />
<Logo />
<Menu Orientation="Vertical" />
<Search CssClass="px-3 d-none d-md-block" />
</nav>
</div>
<div class="main g-0">
<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 class="container">
<div class="row px-4">
@ -31,9 +38,13 @@
public override List<Resource> Resources => new List<Resource>()
{
// 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.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
{
<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.UI;
using System.Net;
using static System.Runtime.InteropServices.JavaScript.JSType;
using Microsoft.Extensions.Localization;
// ReSharper disable UnassignedGetOnlyAutoProperty
// ReSharper disable MemberCanBePrivate.Global
@ -20,6 +22,7 @@ namespace Oqtane.Themes.Controls
[Inject] public NavigationManager NavigationManager { get; set; }
[Inject] public IPageModuleService PageModuleService { get; set; }
[Inject] public IModuleService ModuleService { get; set; }
[Inject] public IStringLocalizer<ModuleActionsBase> Localizer { get; set; }
[Parameter] public PageState PageState { 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))
{
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))
{
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
{
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)
{
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.CloudDownload, Name = "Export Content", Action = async (u, m) => await EditUrlAsync(u, m.ModuleId, "Export") });
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 = Localizer["ExportContent"], Action = async (u, m) => await EditUrlAsync(u, m.ModuleId, "Export") });
}
actionList.Add(new ActionViewModel { Name = "" });
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)

View File

@ -6,13 +6,13 @@
@if (ShowLanguageSwitcher)
{
<LanguageSwitcher DropdownAlignment="@LanguageDropdownAlignment" />
<LanguageSwitcher ButtonClass="@ButtonClass" DropdownAlignment="@LanguageDropdownAlignment" />
}
@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>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
@if (PageState.EditMode)
{
<button type="submit" class="btn @ButtonClass active" aria-pressed="true" autocomplete="off">
@ -36,7 +36,7 @@
}
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
{
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))
{

View File

@ -15,6 +15,7 @@
@inject ILogService LoggingService
@inject IStringLocalizer<ControlPanelInteractive> Localizer
@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">
<span class="oi oi-cog"></span>
@ -93,9 +94,13 @@
<div class="row">
<div class="col text-center">
<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="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>
@if (_moduleType == "new")
{
@ -138,7 +143,7 @@
}
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>
@foreach (Page p in _pages)
{
@ -211,7 +216,7 @@
<div class="row d-flex">
<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>
@ -291,7 +296,7 @@
_containerType = PageState.Site.DefaultContainerType;
_allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_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();
}
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;
if (_pageId != "-")
{
_modules = PageState.Modules
.Where(module => module.PageId == int.Parse(_pageId) &&
UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList))
_modules = await ModuleService.GetModulesAsync(PageState.Page.SiteId);
_modules = _modules.Where(module => module.PageId == int.Parse(_pageId) &&
UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList) &&
(_moduleType == "add" || module.ModuleDefinition.IsPortable))
.ToList();
}
_moduleId = "-";
@ -354,6 +367,7 @@
{
if ((_moduleType == "new" && _moduleDefinitionName != "-") || (_moduleType != "new" && _moduleId != "-"))
{
var newModuleId = _moduleId != "-" ? int.Parse(_moduleId) : 0;
if (_moduleType == "new")
{
Module module = new Module();
@ -361,33 +375,37 @@
module.PageId = PageState.Page.PageId;
module.ModuleDefinitionName = _moduleDefinitionName;
module.AllPages = false;
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.PermissionList = GenerateDefaultPermissions(module.SiteId);
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
{
PageId = PageState.Page.PageId,
ModuleId = int.Parse(_moduleId),
ModuleId = newModuleId,
Title = _title
};
if (pageModule.Title == "")
if (string.IsNullOrEmpty(pageModule.Title))
{
if (_moduleType == "new")
{
@ -412,9 +430,16 @@
await PageModuleService.UpdatePageModuleOrderAsync(pageModule.PageId, pageModule.Pane);
await UpdateSettingsAsync();
_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, ""));
if (PageState.RenderMode == RenderModes.Interactive)
{
_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
{
@ -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)
{
foreach (var permission in PageState.Page.PermissionList.Where(item => item.PermissionName == pagePermission))
@ -438,27 +482,19 @@
private void Navigate(string location)
{
Module module;
int moduleId;
switch (location)
{
case "Admin":
// get admin dashboard moduleid
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule);
if (module != null)
{
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin", module.ModuleId, "Index", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
}
moduleId = int.Parse(PageState.Site.Settings[Constants.AdminDashboardModule]);
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin", moduleId, "Index", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
break;
case "Add":
case "Edit":
string url = "";
// get page management moduleid
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.PageManagementModule);
if (module != null)
{
url = Utilities.EditUrl(PageState.Alias.Path, "admin/pages", module.ModuleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}");
NavigationManager.NavigateTo(url);
}
moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]);
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
break;
}
}

View File

@ -1,21 +1,29 @@
@namespace Oqtane.Themes.Controls
@inherits ThemeControlBase
@using System.Globalization
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Http
@using Oqtane.Models
@namespace Oqtane.Themes.Controls
@inherits ThemeControlBase
@inject ILanguageService LanguageService
@inject NavigationManager NavigationManager
@if (_supportedCultures?.Count() > 1)
{
<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>
</button>
<div class="dropdown-menu @MenuAlignment" aria-labelledby="btnCultures">
@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>
@ -23,9 +31,15 @@
@code{
private IEnumerable<Culture> _supportedCultures;
private string MenuAlignment = string.Empty;
[Parameter]
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()
{
@ -33,16 +47,26 @@
var languages = PageState.Languages;
_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)
{
if (culture != CultureInfo.CurrentUICulture.Name)
{
var interop = new Interop(JSRuntime);
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
var interop = new Interop(JSRuntime);
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360);
NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
}
}

View File

@ -16,7 +16,7 @@
else
{
<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" />
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
</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>()
{
// 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.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">
<Logo /><Menu Orientation="Horizontal" />
<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>
</nav>
<div class="content">

View File

@ -121,15 +121,15 @@
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
if (_login != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login, true);
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login);
}
if (_register != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register, true);
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register);
}
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);
}

View File

@ -3,8 +3,6 @@
@using Oqtane.Shared
@inject SiteState SiteState
@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))
{

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
@inject SiteState SiteState
@if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Interactive)
@if (_comment != null)
{
<StreamRenderingDisabled ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" />
}
else
{
<StreamRenderingEnabled ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" />
@((MarkupString)_comment)
@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, _prerender)" />
}
}
@code {
@ -20,6 +24,24 @@ else
[CascadingParameter]
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)]
public void AddModuleMessage(string message, MessageType type)

View File

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

View File

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

View File

@ -3,8 +3,6 @@
@inject IInstallationService InstallationService
@inject IJSRuntime JSRuntime
@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)
{

View File

@ -1,6 +1,7 @@
@using System.Diagnostics.CodeAnalysis
@using System.Net
@using Microsoft.AspNetCore.Http
@using System.Globalization
@namespace Oqtane.UI
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject SiteState SiteState
@ -95,6 +96,7 @@
{
Site site = null;
Page page = null;
List<Module> modules = null;
User user = null;
var editmode = false;
var refresh = false;
@ -103,7 +105,7 @@
_error = "";
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 querystring = Utilities.ParseQueryString(route.Query);
@ -171,9 +173,9 @@
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);
lastsyncdate = sync.SyncDate;
if (sync.SyncEvents.Any())
@ -211,7 +213,7 @@
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));
}
@ -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
if (UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList) && (Utilities.IsPageModuleVisible(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList)))
// edit mode
if (user != null)
{
// edit mode
if (user != null)
if (querystring.ContainsKey("editmode") && querystring["edit"] == "true")
{
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;
}
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);
}
var userSettings = new Dictionary<string, string> { { "CP-editmode", "-1" } };
await SettingService.UpdateUserSettingsAsync(userSettings, user.UserId);
}
}
// load additional metadata for current page
page = ProcessPage(page, site, user, SiteState.Alias);
}
// load additional metadata for modules
(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);
}
// get modules for current page
if (refresh || PageState.Modules == null || !PageState.Modules.Any() || PageState.Modules.First().PageId != page.PageId)
{
modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
}
else
{
// Need to redirect 404 as page doesnot exist in a Permission or Timeframe
if (route.PagePath != "404")
{
// redirect to 404 page
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", ""));
}
modules = PageState.Modules;
}
// load additional metadata for current page
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
var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
if (page == null)
{
var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl + route.Query;
NavigationManager.NavigateTo(url, false);
// check for url mapping
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)
{
// 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)));
return;
}
else
{
if (route.PagePath != "404")
{
// redirect to 404 page
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", ""));
}
else
{
// redirect to home page as a fallback
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", ""));
}
}
}
// 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", ""));
}
else
{
// redirect to home page as a fallback
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", ""));
}
}
}
@ -484,6 +489,7 @@
// retrieve module component resources
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
module.RenderMode = moduleobject.RenderMode;
module.Prerender = moduleobject.Prerender;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -557,7 +563,7 @@
{
foreach (var resource in resources)
{
if (resource.Level != ResourceLevel.Site)
if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site)
{
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>
<TargetFramework>net8.0</TargetFramework>
<Version>5.1.0</Version>
<Version>5.2.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<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>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,8 +33,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="MySql.Data" Version="8.3.0" />
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.2" />
<PackageReference Include="MySql.Data" Version="8.4.0" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.1.0</Version>
<Version>5.2.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<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>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -34,8 +34,8 @@
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.1.0</Version>
<Version>5.2.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<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>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.1.0</Version>
<Version>5.2.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<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>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
</ItemGroup>
<ItemGroup>

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<Version>5.1.0</Version>
<Version>5.2.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<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>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace>
@ -31,7 +31,7 @@
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>5.1.0</ApplicationDisplayVersion>
<ApplicationDisplayVersion>5.2.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
@ -65,15 +65,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.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="Microsoft.Maui.Controls" Version="8.0.10" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="8.0.10" />
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.40" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.40" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="8.0.40" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Client</id>
<version>5.1.0</version>
<version>5.2.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<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>
<tags>oqtane</tags>
</metadata>
@ -21,4 +21,4 @@
<file src="..\Oqtane.Client\bin\Release\net8.0\Oqtane.Client.pdb" target="lib\net8.0" />
<file src="icon.png" target="" />
</files>
</package>
</package>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Framework</id>
<version>5.1.0</version>
<version>5.2.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -11,12 +11,12 @@
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<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>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0</releaseNotes>
<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.2.0</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane framework</tags>
</metadata>
<files>
<file src="icon.png" target="" />
</files>
</package>
</package>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Server</id>
<version>5.1.0</version>
<version>5.2.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<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>
<tags>oqtane</tags>
</metadata>
@ -21,4 +21,4 @@
<file src="..\Oqtane.Server\bin\Release\net8.0\Oqtane.Server.pdb" target="lib\net8.0" />
<file src="icon.png" target="" />
</files>
</package>
</package>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Shared</id>
<version>5.1.0</version>
<version>5.2.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<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>
<tags>oqtane</tags>
</metadata>
@ -21,4 +21,4 @@
<file src="..\Oqtane.Shared\bin\Release\net8.0\Oqtane.Shared.pdb" target="lib\net8.0" />
<file src="icon.png" target="" />
</files>
</package>
</package>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Updater</id>
<version>5.1.0</version>
<version>5.2.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<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>
<tags>oqtane</tags>
</metadata>
@ -20,4 +20,4 @@
<file src="..\Oqtane.Updater\bin\Release\net8.0\publish\*.*" target="lib\net8.0" />
<file src="icon.png" target="" />
</files>
</package>
</package>

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.Themes
@using Oqtane.Extensions
@using System.Globalization
@inject NavigationManager NavigationManager
@inject IAntiforgery Antiforgery
@inject IConfigManager ConfigManager
@ -39,7 +40,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
@if (!string.IsNullOrEmpty(_PWAScript))
@if (_scripts.Contains("PWA 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)" />
}
@if (!string.IsNullOrEmpty(_reconnectScript))
{
@((MarkupString)_reconnectScript)
}
@if (!string.IsNullOrEmpty(_PWAScript))
{
@((MarkupString)_PWAScript)
}
@((MarkupString)_bodyResources)
<script src="_framework/blazor.web.js"></script>
<script src="js/app.js"></script>
<script src="js/loadjs.min.js"></script>
<script src="js/interop.js"></script>
<script src="_framework/blazor.web.js"></script>
@((MarkupString)_scripts)
@((MarkupString)_bodyResources)
}
else
{
@ -105,8 +99,7 @@
private string _headResources = "";
private string _bodyResources = "";
private string _styleSheets = "";
private string _PWAScript = "";
private string _reconnectScript = "";
private string _scripts = "";
private string _message = "";
private PageState _pageState;
@ -139,6 +132,7 @@
_renderMode = site.RenderMode;
_runtime = site.Runtime;
_prerender = site.Prerender;
var modules = new List<Module>();
Route route = new Route(url, alias.Path);
var page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
@ -163,6 +157,10 @@
{
HandlePageNotFound(site, page, route);
}
else
{
modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
}
if (site.VisitorTracking)
{
@ -175,23 +173,25 @@
CreateJwtToken(alias);
}
// include stylesheets to prevent FOUC
var resources = GetPageResources(alias, site, page, int.Parse(route.ModuleId), route.Action);
// includes resources
var resources = await GetPageResources(alias, site, page, modules, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action);
ManageStyleSheets(resources);
ManageScripts(resources, alias);
// scripts
if (_renderMode == RenderModes.Static)
{
ManageScripts(resources, alias);
}
// generate scripts
if (_renderMode == RenderModes.Interactive && _runtime == Runtimes.Server)
{
_reconnectScript = CreateReconnectScript();
_scripts += CreateReconnectScript();
}
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);
_bodyResources += ParseScripts(site.BodyContent);
@ -220,12 +220,13 @@
_language = _language.Replace("c=", "");
}
// create initial PageState
// create initial PageState
_pageState = new PageState
{
Alias = alias,
Site = site,
Page = page,
Modules = modules,
User = null,
Uri = new Uri(url, UriKind.Absolute),
Route = route,
@ -329,14 +330,26 @@
int? userid = Context.User.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;
bool addcookie = false;
var VisitorCookie = Constants.VisitorCookiePrefix + SiteId.ToString();
if (!int.TryParse(Context.Request.Cookies[VisitorCookie], out _visitorId))
if (_visitorId <= 0)
{
// if enabled use IP Address correlation
_visitorId = -1;
var correlate = bool.Parse(settings.GetValue("VisitorCorrelation", "true"));
if (correlate)
{
@ -344,12 +357,12 @@
if (visitor != null)
{
_visitorId = visitor.VisitorId;
addcookie = true;
setcookie = true;
}
}
}
if (_visitorId == -1)
if (_visitorId <= 0)
{
// create new visitor
visitor = new Visitor();
@ -365,52 +378,59 @@
visitor.VisitedOn = DateTime.UtcNow;
visitor = VisitorRepository.AddVisitor(visitor);
_visitorId = visitor.VisitorId;
addcookie = true;
setcookie = true;
}
else
{
if (visitor == null)
// check expiry
if (DateTime.UtcNow > expiry)
{
// get visitor if it was not previously loaded
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))
if (visitor == null)
{
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
if (addcookie)
// set cookie
if (setcookie)
{
expiry = DateTime.UtcNow.AddMinutes(int.Parse(settings.GetValue("VisitorDuration", "5")));
Context.Response.Cookies.Append(
VisitorCookie,
_visitorId.ToString(),
visitorCookieName,
$"{_visitorId}|{expiry.ToString("M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture)}",
new CookieOptions()
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
IsEssential = true
}
{
Expires = DateTimeOffset.UtcNow.AddYears(10),
IsEssential = true
}
);
}
}
@ -432,7 +452,7 @@
private string CreatePWAScript(Alias alias, Site site, Route route)
{
return
return Environment.NewLine +
"<script>" + Environment.NewLine +
" // PWA Manifest" + Environment.NewLine +
" setTimeout(() => {" + Environment.NewLine +
@ -468,14 +488,14 @@
" console.log('ServiceWorker Registration Failed ', err);" + Environment.NewLine +
" });" + Environment.NewLine +
" };" + Environment.NewLine +
"</script>";
"</script>" + Environment.NewLine;
}
private string CreateReconnectScript()
{
return
return Environment.NewLine +
"<script>" + Environment.NewLine +
" // Blazor Server Reconnect" + Environment.NewLine +
" // Interactive Blazor Server Reconnect" + Environment.NewLine +
" new MutationObserver((mutations, observer) => {" + Environment.NewLine +
" if (document.querySelector('#components-reconnect-modal h5 a')) {" + Environment.NewLine +
" async function attemptReload() {" + Environment.NewLine +
@ -487,7 +507,26 @@
" setInterval(attemptReload, 5000);" + Environment.NewLine +
" }" + 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)
@ -536,7 +575,7 @@
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
((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
{
@ -558,7 +597,7 @@
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>();
@ -566,13 +605,13 @@
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType));
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
{
// fallback to default Oqtane theme
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);
if (type != null)
@ -580,16 +619,16 @@
var obj = Activator.CreateInstance(type) as IThemeControl;
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 = "";
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
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -635,7 +674,7 @@
var obj = Activator.CreateInstance(moduletype) as IModuleControl;
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)
{
// settings components are embedded within a framework settings module
@ -643,45 +682,51 @@
if (moduletype != null)
{
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
var modules = site.Modules.GroupBy(item => item.ModuleDefinition?.ModuleDefinitionName).Select(group => group.First()).ToList();
foreach (var module in modules)
if (site.RenderMode == RenderModes.Interactive)
{
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;
}
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)
{
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.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
{
resource.Url = alias.BaseUrl + resource.Url;
}
if (resource.Url.StartsWith("~"))
{
resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
}
if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
{
resource.Url = alias.BaseUrl + resource.Url;
}
// ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower()))
{
pageresources.Add(resource.Clone(level, name));
// ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower()))
{
pageresources.Add(resource.Clone(level, name));
}
}
}
}
@ -692,6 +737,7 @@
{
if (resources != null)
{
// include stylesheets to prevent FOUC
string batch = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff");
int count = 0;
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))
{
string path = "";
string path = folderpath.StartsWith(Path.DirectorySeparatorChar) ? Path.DirectorySeparatorChar.ToString() : string.Empty;
var separators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
string[] folders = folderpath.Split(separators, StringSplitOptions.RemoveEmptyEntries);
foreach (string folder in folders)

View File

@ -76,5 +76,20 @@ namespace Oqtane.Controllers
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.Security;
using System.Net;
using System.Security.Policy;
namespace Oqtane.Controllers
{

View File

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