Merge branch 'oqtane:dev' into dev

This commit is contained in:
Amir Jahangard
2023-07-12 12:43:40 +03:30
committed by GitHub
70 changed files with 823 additions and 207 deletions

View File

@ -14,10 +14,16 @@
@if (_containers != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="module" HelpText="The name of the module" ResourceKey="Module">Module: </Label>
<div class="col-sm-9">
<input id="module" type="text" class="form-control" @bind="@_module" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Enter the title of the module" ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" type="text" name="Title" class="form-control" @bind="@_title" required />
<input id="title" type="text" class="form-control" @bind="@_title" required />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -104,6 +110,7 @@
private ElementReference form;
private bool validated = false;
private List<ThemeControl> _containers = new List<ThemeControl>();
private string _module;
private string _title;
private string _containerType;
private string _allPages = "false";
@ -125,6 +132,7 @@
protected override void OnInitialized()
{
_module = ModuleState.ModuleDefinition.Name;
_title = ModuleState.Title;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType;

View File

@ -144,6 +144,11 @@ else
<TabPanel Name="Notifications" ResourceKey="Notifications">
@if (notifications != null)
{
<select class="form-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option>
<option value="from">@Localizer["Items.Sent"]</option>
</select>
<br />
<ActionLink Action="Add" Text="Send Notification" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="SendNotification" />
<br /><br />
@if (filter == "to")
@ -159,22 +164,41 @@ else
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.FromDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
@if (context.IsRead)
{
<td>@context.FromDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
}
else
{
<td><b>@context.FromDisplayName</b></td>
<td><b>@context.Subject</b></td>
<td><b>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</b></td>
}
</Row>
<Detail>
<td colspan="2"></td>
<td colspan="3">
@{
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@(context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body)
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
}
else
{
<b>@notificationSummary</b>
}
</td>
</Detail>
</Pager>
@ -192,22 +216,42 @@ else
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.ToDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
@if (context.IsRead)
{
<td>@context.ToDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
}
else
{
<td><b>@context.ToDisplayName</b></td>
<td><b>@context.Subject</b></td>
<td><b>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</b></td>
}
</Row>
<Detail>
<td colspan="2"></td>
<td colspan="3">
@{
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@(context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body)
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
}
else
{
<b>@notificationSummary</b>
}
</td>
</Detail>
</Pager>
@ -217,11 +261,6 @@ else
<br />
<ActionDialog Header="Clear Notifications" Message="Are You Sure You Wish To Permanently Delete All Notifications ?" Action="Delete All Notifications" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllNotifications())" ResourceKey="DeleteAllNotifications" />
}
<br /><hr />
<select class="form-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option>
<option value="from">@Localizer["Items.Sent"]</option>
</select>
}
</TabPanel>
</TabStrip>
@ -246,6 +285,7 @@ else
private string category = string.Empty;
private string filter = "to";
private List<Notification> notifications;
private string notificationSummary = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;

View File

@ -118,6 +118,9 @@
Notification notification = await NotificationService.GetNotificationAsync(notificationid);
if (notification != null)
{
notification.IsRead = true;
notification = await NotificationService.UpdateNotificationAsync(notification);
int userid = -1;
if (notification.ToUserId == PageState.User.UserId)
{

View File

@ -219,7 +219,14 @@
if (folder != null)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.PermissionList);
_files = await FileService.GetFilesAsync(FolderId);
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Browse, folder.PermissionList))
{
_files = await FileService.GetFilesAsync(FolderId);
}
else
{
_files = new List<File>();
}
}
else
{
@ -322,27 +329,29 @@
var folder = (Folder == Constants.PackagesFolder) ? Folder : FolderId.ToString();
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken);
// uploading is asynchronous so we need to wait for the uploads to complete
// note that this will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
bool success = false;
int attempts = 0;
while (attempts < 5 && !success)
// uploading is asynchronous so we need to poll to determine if uploads are completed
var success = true;
int upload = 0;
while (upload < uploads.Length && success)
{
attempts += 1;
Thread.Sleep(1000 * attempts); // progressive retry
success = true;
List<File> files = await FileService.GetFilesAsync(folder);
if (files.Count > 0)
success = false;
// note that progressive retry will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
int attempts = 0;
while (attempts < 5 && !success)
{
foreach (string upload in uploads)
attempts += 1;
Thread.Sleep(1000 * attempts); // progressive retry
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[upload]);
if (file != null)
{
if (!files.Exists(item => item.Name == upload))
{
success = false;
}
success = true;
}
}
if (success)
{
upload++;
}
}
// reset progress indicators
@ -372,14 +381,14 @@
else
{
// set FileId to first file in upload collection
await GetFiles();
var file = _files.Where(item => item.Name == uploads[0]).FirstOrDefault();
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[0]);
if (file != null)
{
FileId = file.FileId;
await SetImage();
await OnUpload.InvokeAsync(FileId);
}
await GetFiles();
StateHasChanged();
}
}

View File

@ -17,7 +17,10 @@
<hr class="app-rule" />
</div>
<div class="collapse @_show" id="@Name">
@ChildContent
@if (ChildContent != null)
{
@ChildContent
}
</div>
@code {
@ -26,7 +29,7 @@
private string _show = string.Empty;
[Parameter]
public RenderFragment ChildContent { get; set; }
public RenderFragment ChildContent { get; set; } = null;
[Parameter]
public string Name { get; set; } // required - the name of the section

View File

@ -9,7 +9,6 @@ using Oqtane.UI;
using System.Collections.Generic;
using Microsoft.JSInterop;
using System.Linq;
using Oqtane.Themes;
namespace Oqtane.Modules
{

View File

@ -4,7 +4,7 @@
<TargetFramework>net7.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>4.0.0</Version>
<Version>4.0.1</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/v4.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -150,4 +150,10 @@
<data name="Error.Module.Load" xml:space="preserve">
<value>A Problem Was Encountered Loading Module {0}. The Module Is Either Invalid Or Does Not Exist.</value>
</data>
<data name="Module.HelpText" xml:space="preserve">
<value>The name of the module</value>
</data>
<data name="Module.Text" xml:space="preserve">
<value>Module:</value>
</data>
</root>

View File

@ -46,6 +46,11 @@ namespace Oqtane.Services
return await GetJsonAsync<File>($"{Apiurl}/{fileId}");
}
public async Task<File> GetFileAsync(int folderId, string name)
{
return await GetJsonAsync<File>($"{Apiurl}/name/{name}/{folderId}");
}
public async Task<File> AddFileAsync(File file)
{
return await PostJsonAsync<File>(Apiurl, file);

View File

@ -1,5 +1,6 @@
using Oqtane.Models;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace Oqtane.Services
@ -33,6 +34,15 @@ namespace Oqtane.Services
/// <returns></returns>
Task<File> GetFileAsync(int fileId);
/// <summary>
/// Get a <see cref="File"/> based on the <see cref="Folder"/> and file name.
/// </summary>
/// <param name="folderId">Reference to the <see cref="Folder"/></param>
/// <param name="name">name of the file
/// </param>
/// <returns></returns>
Task<File> GetFileAsync(int folderId, string name);
/// <summary>
/// Add / store a <see cref="File"/> record.
/// This does not contain the file contents.

View File

@ -18,6 +18,27 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId);
/// <summary>
///
/// </summary>
/// <param name="siteId"></param>
/// <param name="direction"></param>
/// <param name="userId"></param>
/// <param name="count"></param>
/// <param name="isRead"></param>
/// <returns></returns>
Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId, int count, bool isRead);
/// <summary>
///
/// </summary>
/// <param name="siteId"></param>
/// <param name="direction"></param>
/// <param name="userId"></param>
/// <param name="isRead"></param>
/// <returns></returns>
Task<int> GetNotificationCountAsync(int siteId, string direction, int userId, bool isRead);
/// <summary>
/// Returns a specific notifications
/// </summary>

View File

@ -27,6 +27,17 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package);
/// <summary>
/// Returns a list of packages matching the given parameters
/// </summary>
/// <param name="type"></param>
/// <param name="search"></param>
/// <param name="price"></param>
/// <param name="package"></param>
/// <param name="sort"></param>
/// <returns></returns>
Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package, string sort);
/// <summary>
/// Returns a specific package
/// </summary>

View File

@ -22,6 +22,20 @@ namespace Oqtane.Services
return notifications.OrderByDescending(item => item.CreatedOn).ToList();
}
public async Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId, int count, bool isRead)
{
var notifications = await GetJsonAsync<List<Notification>>($"{Apiurl}/read?siteid={siteId}&direction={direction.ToLower()}&userid={userId}&count={count}&isread={isRead}");
return notifications.OrderByDescending(item => item.CreatedOn).ToList();
}
public async Task<int> GetNotificationCountAsync(int siteId, string direction, int userId, bool isRead)
{
var notificationCount = await GetJsonAsync<int>($"{Apiurl}/read-count?siteid={siteId}&direction={direction.ToLower()}&userid={userId}&isread={isRead}");
return notificationCount;
}
public async Task<Notification> GetNotificationAsync(int notificationId)
{
return await GetJsonAsync<Notification>($"{Apiurl}/{notificationId}");

View File

@ -23,7 +23,12 @@ namespace Oqtane.Services
public async Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package)
{
return await GetJsonAsync<List<Package>>($"{Apiurl}?type={type}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}");
return await GetPackagesAsync(type, search, price, package, "");
}
public async Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package, string sort)
{
return await GetJsonAsync<List<Package>>($"{Apiurl}?type={type}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}&sort={sort}");
}
public async Task<Package> GetPackageAsync(string packageId, string version)

View File

@ -21,7 +21,7 @@ namespace Oqtane.Services
_siteState = siteState;
}
private HttpClient GetHttpClient()
public HttpClient GetHttpClient()
{
if (!_httpClient.DefaultRequestHeaders.Contains(Constants.AntiForgeryTokenHeaderName) && _siteState != null && !string.IsNullOrEmpty(_siteState.AntiForgeryToken))
{
@ -206,7 +206,6 @@ namespace Oqtane.Services
Console.WriteLine($"Request: {response.RequestMessage.RequestUri}");
Console.WriteLine($"Response status: {response.StatusCode} {response.ReasonPhrase}");
}
return false;
}

View File

@ -36,7 +36,7 @@
@if (_canViewAdminDashboard || UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{
<button type="button" class="btn @ButtonClass" data-bs-toggle="offcanvas" data-bs-target="#offcanvasControlPanel" aria-controls="offcanvasControlPanel">
<button type="button" class="btn @ButtonClass ms-1" data-bs-toggle="offcanvas" data-bs-target="#offcanvasControlPanel" aria-controls="offcanvasControlPanel">
<span class="oi oi-cog"></span>
</button>
@ -471,6 +471,12 @@
private async Task ToggleEditMode(bool EditMode)
{
Page page = null;
if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))
{
page = await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId);
}
if (_showEditMode)
{
if (EditMode)
@ -490,9 +496,8 @@
}
else
{
if (PageState.Page.IsPersonalizable && PageState.User != null)
if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))
{
var page = await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId);
PageState.EditMode = true;
NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + ((PageState.EditMode) ? "true" : "false")));
}

View File

@ -19,7 +19,6 @@ namespace Oqtane.Themes.Controls
[Inject] public IUserService UserService { get; set; }
[Inject] public IJSRuntime jsRuntime { get; set; }
[Inject] public IServiceProvider ServiceProvider { get; set; }
[Inject] public ILogService LoggingService { get; set; }
protected void LoginUser()
{

View File

@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
using System;
@ -13,6 +16,9 @@ namespace Oqtane.Themes
{
public abstract class ThemeBase : ComponentBase, IThemeControl
{
[Inject]
protected ILogService LoggingService { get; set; }
[Inject]
protected IJSRuntime JSRuntime { get; set; }
@ -186,6 +192,148 @@ namespace Oqtane.Themes
await interop.ScrollTo(0, 0, "smooth");
}
// logging methods
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
{
LogFunction logFunction;
if (string.IsNullOrEmpty(function))
{
// try to infer from page action
function = PageState.Action;
}
if (!Enum.TryParse(function, out logFunction))
{
switch (function.ToLower())
{
case "add":
logFunction = LogFunction.Create;
break;
case "edit":
logFunction = LogFunction.Update;
break;
case "delete":
logFunction = LogFunction.Delete;
break;
case "":
logFunction = LogFunction.Read;
break;
default:
logFunction = LogFunction.Other;
break;
}
}
await Log(alias, level, logFunction, exception, message, args);
}
public async Task Log(Alias alias, LogLevel level, LogFunction function, Exception exception, string message, params object[] args)
{
int pageId = PageState.Page.PageId;
string category = GetType().AssemblyQualifiedName;
string feature = Utilities.GetTypeNameLastSegment(category, 1);
await LoggingService.Log(alias, pageId, null, PageState.User?.UserId, category, feature, function, level, exception, message, args);
}
public class Logger
{
private readonly ModuleBase _moduleBase;
public Logger(ModuleBase moduleBase)
{
_moduleBase = moduleBase;
}
public async Task LogTrace(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Trace, "", null, message, args);
}
public async Task LogTrace(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Trace, function, null, message, args);
}
public async Task LogTrace(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Trace, "", exception, message, args);
}
public async Task LogDebug(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Debug, "", null, message, args);
}
public async Task LogDebug(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Debug, function, null, message, args);
}
public async Task LogDebug(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Debug, "", exception, message, args);
}
public async Task LogInformation(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Information, "", null, message, args);
}
public async Task LogInformation(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Information, function, null, message, args);
}
public async Task LogInformation(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Information, "", exception, message, args);
}
public async Task LogWarning(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Warning, "", null, message, args);
}
public async Task LogWarning(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Warning, function, null, message, args);
}
public async Task LogWarning(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Warning, "", exception, message, args);
}
public async Task LogError(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Error, "", null, message, args);
}
public async Task LogError(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Error, function, null, message, args);
}
public async Task LogError(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Error, "", exception, message, args);
}
public async Task LogCritical(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Critical, "", null, message, args);
}
public async Task LogCritical(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Critical, function, null, message, args);
}
public async Task LogCritical(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Critical, "", exception, message, args);
}
}
[Obsolete("ContentUrl(int fileId) is deprecated. Use FileUrl(int fileId) instead.", false)]
public string ContentUrl(int fileid)
{

View File

@ -223,12 +223,12 @@
}
if (page == null)
{
// look for personalized page
page = await PageService.GetPageAsync(route.PagePath, site.SiteId);
}
else
{
if (user != null && page.IsPersonalizable)
// look for personalized page
if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList))
{
var personalized = await PageService.GetPageAsync(route.PagePath + "/" + user.Username, site.SiteId);
if (personalized != null)