From 4c9960b983ecc82d8172b3371215981a749668cc Mon Sep 17 00:00:00 2001 From: hishamco Date: Fri, 5 Mar 2021 16:41:53 +0300 Subject: [PATCH 01/31] Rows -> Maximum Rows --- Oqtane.Client/Modules/Admin/Logs/Index.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Admin/Logs/Index.razor b/Oqtane.Client/Modules/Admin/Logs/Index.razor index 3ace61fd..ca99da6d 100644 --- a/Oqtane.Client/Modules/Admin/Logs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Logs/Index.razor @@ -36,7 +36,7 @@ else - + - + +

+ - - @@ -36,8 +36,8 @@ else - - @@ -48,7 +48,7 @@ else @if (_logs.Any()) { - +
  @Localizer["Date"] @@ -141,10 +141,6 @@ else private async Task GetLogs() { _logs = await LogService.GetLogsAsync(PageState.Site.SiteId, ((_level == "-") ? string.Empty : _level), ((_function == "-") ? string.Empty : _function), int.Parse(_rows)); - await InvokeAsync(() => - { - base.StateHasChanged(); - }); } private string GetClass(string function) diff --git a/Oqtane.Client/Modules/Controls/Pager.razor b/Oqtane.Client/Modules/Controls/Pager.razor index 64f32728..c89b1301 100644 --- a/Oqtane.Client/Modules/Controls/Pager.razor +++ b/Oqtane.Client/Modules/Controls/Pager.razor @@ -1,4 +1,4 @@ -@namespace Oqtane.Modules.Controls +@namespace Oqtane.Modules.Controls @inherits ModuleControlBase @typeparam TableItem @@ -114,10 +114,10 @@ @code { private int _pages = 0; private int _page = 1; - private int _maxItems; - private int _maxPages; - private int _startPage; - private int _endPage; + private int _maxItems = 10; + private int _maxPages = 5; + private int _startPage = 0; + private int _endPage = 0; [Parameter] public string Format { get; set; } @@ -172,24 +172,20 @@ } } - if (string.IsNullOrEmpty(PageSize)) - { - _maxItems = 10; - } - else + if (!string.IsNullOrEmpty(PageSize)) { _maxItems = int.Parse(PageSize); } - if (string.IsNullOrEmpty(DisplayPages)) - { - _maxPages = 5; - } - else + if (!string.IsNullOrEmpty(DisplayPages)) { _maxPages = int.Parse(DisplayPages); } + _page = 1; + _startPage = 0; + _endPage = 0; + if (Items != null) { ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems); From 86ce8994d9be457c42cd2460a1fa1b6ce8f246dd Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 11 Mar 2021 20:21:15 -0500 Subject: [PATCH 08/31] fix #1156 add defensive coding for scenario where host name does not match any alias --- Oqtane.Server/Pages/_Host.cshtml | 7 ++++++- Oqtane.Server/Pages/_Host.cshtml.cs | 27 ++++++++++++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Oqtane.Server/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml index cf2d69f0..e1cc6c21 100644 --- a/Oqtane.Server/Pages/_Host.cshtml +++ b/Oqtane.Server/Pages/_Host.cshtml @@ -1,4 +1,4 @@ -@page "/" +@page "/" @namespace Oqtane.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.Extensions.Configuration @@ -35,6 +35,11 @@ 🗙 + @if (Model.Message != "") + { + @Model.Message + } + @if (Configuration.GetSection("Runtime").Value == "WebAssembly") diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index 4dc9cb7e..156b5fac 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -39,6 +39,7 @@ namespace Oqtane.Pages public string HeadResources = ""; public string BodyResources = ""; + public string Message = ""; public void OnGet() { @@ -54,20 +55,28 @@ namespace Oqtane.Pages if (HttpContext.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName] == null && !string.IsNullOrEmpty(_configuration.GetConnectionString("DefaultConnection"))) { var uri = new Uri(Request.GetDisplayUrl()); - var alias = _aliases.GetAlias(uri.Authority + "/" + uri.LocalPath.Substring(1)); - _state.Alias = alias; - - // set default language for site if the culture is not supported - var languages = _languages.GetLanguages(alias.SiteId); - if (languages.Any() && languages.All(l => l.Code != CultureInfo.CurrentUICulture.Name)) + var hostname = uri.Authority + "/" + uri.LocalPath.Substring(1); + var alias = _aliases.GetAlias(hostname); + if (alias != null) { - var defaultLanguage = languages.Where(l => l.IsDefault).SingleOrDefault() ?? languages.First(); + _state.Alias = alias; - SetLocalizationCookie(defaultLanguage.Code); + // set default language for site if the culture is not supported + var languages = _languages.GetLanguages(alias.SiteId); + if (languages.Any() && languages.All(l => l.Code != CultureInfo.CurrentUICulture.Name)) + { + var defaultLanguage = languages.Where(l => l.IsDefault).SingleOrDefault() ?? languages.First(); + + SetLocalizationCookie(defaultLanguage.Code); + } + else + { + SetLocalizationCookie(_localizationManager.GetDefaultCulture()); + } } else { - SetLocalizationCookie(_localizationManager.GetDefaultCulture()); + Message = $"No Matching Alias For Host Name {hostname}"; } } } From c5e3c9b35ee04d41ff6f5de5d899582a21da1846 Mon Sep 17 00:00:00 2001 From: hishamco Date: Sat, 13 Mar 2021 13:02:40 +0300 Subject: [PATCH 09/31] Delete profile should refresh profiles list --- Oqtane.Client/Modules/Admin/Profiles/Index.razor | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Admin/Profiles/Index.razor b/Oqtane.Client/Modules/Admin/Profiles/Index.razor index 0bdf8554..f90f367e 100644 --- a/Oqtane.Client/Modules/Admin/Profiles/Index.razor +++ b/Oqtane.Client/Modules/Admin/Profiles/Index.razor @@ -32,7 +32,7 @@ else protected override async Task OnInitializedAsync() { - _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); + await GetProfilesAsync(); } private async Task DeleteProfile(int profileId) @@ -41,7 +41,12 @@ else { await ProfileService.DeleteProfileAsync(profileId); await logger.LogInformation("Profile Deleted {ProfileId}", profileId); + AddModuleMessage(Localizer["Profile Deleted"], MessageType.Success); + + await GetProfilesAsync(); + + StateHasChanged(); } catch (Exception ex) { @@ -49,4 +54,7 @@ else AddModuleMessage(Localizer["Error Deleting Profile"], MessageType.Error); } } + + private async Task GetProfilesAsync() + => _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); } From 801615c981204769f127330d40a0bc5061821901 Mon Sep 17 00:00:00 2001 From: Pieter Kuyck Date: Sat, 13 Mar 2021 12:34:47 +0100 Subject: [PATCH 10/31] fix: user delete --- Oqtane.Server/Controllers/UserController.cs | 1 + Oqtane.Server/Repository/FolderRepository.cs | 10 +++++++++- .../Repository/Interfaces/IFolderRepository.cs | 3 ++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 9819994b..46189e57 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -271,6 +271,7 @@ namespace Oqtane.Controllers if (result != null) { + _folders.DeleteUserFolder(id); _users.DeleteUser(id); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Deleted {UserId}", id); } diff --git a/Oqtane.Server/Repository/FolderRepository.cs b/Oqtane.Server/Repository/FolderRepository.cs index dc7125bb..a7acd0b3 100644 --- a/Oqtane.Server/Repository/FolderRepository.cs +++ b/Oqtane.Server/Repository/FolderRepository.cs @@ -90,7 +90,15 @@ namespace Oqtane.Repository _db.Folder.Remove(folder); _db.SaveChanges(); } - + public void DeleteUserFolder(int userId) + { + string userFolderPath = Utilities.PathCombine("Users", userId.ToString(), System.IO.Path.DirectorySeparatorChar.ToString()); + List folderIdsToDelete = new List(_db.Folder.Where(a => a.Path == userFolderPath).Select(a => a.FolderId)); + foreach (int folderId in folderIdsToDelete) + { + DeleteFolder(folderId); + } + } public string GetFolderPath(int folderId) { Folder folder = _db.Folder.Find(folderId); diff --git a/Oqtane.Server/Repository/Interfaces/IFolderRepository.cs b/Oqtane.Server/Repository/Interfaces/IFolderRepository.cs index 5ce7467f..977d6a28 100644 --- a/Oqtane.Server/Repository/Interfaces/IFolderRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IFolderRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Oqtane.Models; namespace Oqtane.Repository @@ -12,6 +12,7 @@ namespace Oqtane.Repository Folder GetFolder(int folderId, bool tracking); Folder GetFolder(int siteId, string path); void DeleteFolder(int folderId); + void DeleteUserFolder(int userId); string GetFolderPath(int folderId); string GetFolderPath(Folder folder); } From c527f28a6d5d9d1ad173f3b1df5073f3d001b53c Mon Sep 17 00:00:00 2001 From: hishamco Date: Sat, 13 Mar 2021 17:02:24 +0300 Subject: [PATCH 11/31] Address feedback --- Oqtane.Client/Modules/Admin/Profiles/Index.razor | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Profiles/Index.razor b/Oqtane.Client/Modules/Admin/Profiles/Index.razor index f90f367e..141730f9 100644 --- a/Oqtane.Client/Modules/Admin/Profiles/Index.razor +++ b/Oqtane.Client/Modules/Admin/Profiles/Index.razor @@ -30,7 +30,7 @@ else public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; - protected override async Task OnInitializedAsync() + protected override async Task OnParametersSetAsync() { await GetProfilesAsync(); } @@ -56,5 +56,7 @@ else } private async Task GetProfilesAsync() - => _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); + { + _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); + } } From 63140bce817cccd43467a59d8c789e2d9fbb89c2 Mon Sep 17 00:00:00 2001 From: Erwin Yulianto Date: Wed, 17 Mar 2021 19:26:50 +0700 Subject: [PATCH 12/31] Remove admin border after edit After finish Edit, there's a class "container" which is conflict with Bootstrap that cause an issue on Full-width pane. --- Oqtane.Client/UI/Pane.razor | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/UI/Pane.razor b/Oqtane.Client/UI/Pane.razor index 0c04ff94..139135b9 100644 --- a/Oqtane.Client/UI/Pane.razor +++ b/Oqtane.Client/UI/Pane.razor @@ -18,7 +18,7 @@ else @code { private bool _useadminborder = false; - private string _paneadminborder = "container"; + private string _paneadminborder = "app-pane-admin-border"; private string _panetitle = ""; [CascadingParameter] @@ -34,12 +34,11 @@ else if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions) && Name != PaneNames.Admin) { _useadminborder = true; - _paneadminborder = "app-pane-admin-border"; _panetitle = "
" + Name + " Pane
"; } else { - _paneadminborder = "container"; + _useadminborder = false; _panetitle = ""; } @@ -130,4 +129,4 @@ else builder.SetKey(module.PageModuleId); builder.CloseComponent(); } -} \ No newline at end of file +} From d8bcc322399eaef73f09f8be7ba7efdf2ede297c Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 29 Mar 2021 12:58:40 -0400 Subject: [PATCH 13/31] refactor user deletion --- Oqtane.Client/Modules/Admin/Users/Index.razor | 2 +- .../Services/Interfaces/IUserService.cs | 4 +- Oqtane.Client/Services/UserService.cs | 6 +-- Oqtane.Server/Controllers/UserController.cs | 52 +++++++++++++++---- Oqtane.Server/Repository/FolderRepository.cs | 10 +--- .../Interfaces/IFolderRepository.cs | 1 - .../Repository/UserRoleRepository.cs | 4 +- 7 files changed, 50 insertions(+), 29 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 8ac788ee..e648083f 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -85,7 +85,7 @@ else var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); if (user != null) { - await UserService.DeleteUserAsync(user.UserId); + await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); await logger.LogInformation("User Deleted {User}", UserRole.User); StateHasChanged(); } diff --git a/Oqtane.Client/Services/Interfaces/IUserService.cs b/Oqtane.Client/Services/Interfaces/IUserService.cs index eca072c5..b47b3481 100644 --- a/Oqtane.Client/Services/Interfaces/IUserService.cs +++ b/Oqtane.Client/Services/Interfaces/IUserService.cs @@ -1,4 +1,4 @@ -using Oqtane.Models; +using Oqtane.Models; using System.Threading.Tasks; namespace Oqtane.Services @@ -13,7 +13,7 @@ namespace Oqtane.Services Task UpdateUserAsync(User user); - Task DeleteUserAsync(int userId); + Task DeleteUserAsync(int userId, int siteId); Task LoginUserAsync(User user, bool setCookie, bool isPersistent); diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 34aaaebb..3538ecb7 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -1,4 +1,4 @@ -using Oqtane.Shared; +using Oqtane.Shared; using Oqtane.Models; using System.Net.Http; using System.Threading.Tasks; @@ -36,9 +36,9 @@ namespace Oqtane.Services return await PutJsonAsync($"{Apiurl}/{user.UserId}", user); } - public async Task DeleteUserAsync(int userId) + public async Task DeleteUserAsync(int userId, int siteId) { - await DeleteAsync($"{Apiurl}/{userId}"); + await DeleteAsync($"{Apiurl}/{userId}?siteid={siteId}"); } public async Task LoginUserAsync(User user, bool setCookie, bool isPersistent) diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 46189e57..4d3f1abc 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -111,7 +111,6 @@ namespace Oqtane.Controllers return null; } - //TODO shoud be moved to another layer private async Task CreateUser(User user) { User newUser = null; @@ -261,19 +260,50 @@ namespace Oqtane.Controllers // DELETE api//5?siteid=x [HttpDelete("{id}")] [Authorize(Roles = RoleNames.Admin)] - public async Task Delete(int id) + public async Task Delete(int id, string siteid) { - IdentityUser identityuser = await _identityUserManager.FindByNameAsync(_users.GetUser(id).Username); - - if (identityuser != null) + User user = _users.GetUser(id); + if (user != null) { - var result = await _identityUserManager.DeleteAsync(identityuser); - - if (result != null) + // remove user roles for site + foreach (UserRole userrole in _userRoles.GetUserRoles(user.UserId, Int32.Parse(siteid)).ToList()) { - _folders.DeleteUserFolder(id); - _users.DeleteUser(id); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Deleted {UserId}", id); + _userRoles.DeleteUserRole(userrole.UserRoleId); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Role Deleted {UserRole}", userrole); + } + + // remove user folder for site + var folder = _folders.GetFolder(Int32.Parse(siteid), Utilities.PathCombine("Users", user.UserId.ToString(), Path.DirectorySeparatorChar.ToString())); + if (folder != null) + { + if (Directory.Exists(_folders.GetFolderPath(folder))) + { + Directory.Delete(_folders.GetFolderPath(folder), true); + } + _folders.DeleteFolder(folder.FolderId); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Folder Deleted {Folder}", folder); + } + + // delete user if they are not a member of any other sites + if (!_userRoles.GetUserRoles(user.UserId, -1).Any()) + { + // get identity user + IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); + if (identityuser != null) + { + // delete identity user + var result = await _identityUserManager.DeleteAsync(identityuser); + if (result != null) + { + // delete user + _users.DeleteUser(user.UserId); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Deleted {UserId}", user.UserId); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Delete, "Error Deleting User {UserId}", user.UserId, result.ToString()); + } + } } } } diff --git a/Oqtane.Server/Repository/FolderRepository.cs b/Oqtane.Server/Repository/FolderRepository.cs index a7acd0b3..dc7125bb 100644 --- a/Oqtane.Server/Repository/FolderRepository.cs +++ b/Oqtane.Server/Repository/FolderRepository.cs @@ -90,15 +90,7 @@ namespace Oqtane.Repository _db.Folder.Remove(folder); _db.SaveChanges(); } - public void DeleteUserFolder(int userId) - { - string userFolderPath = Utilities.PathCombine("Users", userId.ToString(), System.IO.Path.DirectorySeparatorChar.ToString()); - List folderIdsToDelete = new List(_db.Folder.Where(a => a.Path == userFolderPath).Select(a => a.FolderId)); - foreach (int folderId in folderIdsToDelete) - { - DeleteFolder(folderId); - } - } + public string GetFolderPath(int folderId) { Folder folder = _db.Folder.Find(folderId); diff --git a/Oqtane.Server/Repository/Interfaces/IFolderRepository.cs b/Oqtane.Server/Repository/Interfaces/IFolderRepository.cs index 977d6a28..dba914a7 100644 --- a/Oqtane.Server/Repository/Interfaces/IFolderRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IFolderRepository.cs @@ -12,7 +12,6 @@ namespace Oqtane.Repository Folder GetFolder(int folderId, bool tracking); Folder GetFolder(int siteId, string path); void DeleteFolder(int folderId); - void DeleteUserFolder(int userId); string GetFolderPath(int folderId); string GetFolderPath(Folder folder); } diff --git a/Oqtane.Server/Repository/UserRoleRepository.cs b/Oqtane.Server/Repository/UserRoleRepository.cs index 79f8a629..b47bd6f7 100644 --- a/Oqtane.Server/Repository/UserRoleRepository.cs +++ b/Oqtane.Server/Repository/UserRoleRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Oqtane.Models; @@ -27,7 +27,7 @@ namespace Oqtane.Repository return _db.UserRole.Where(item => item.UserId == userId) .Include(item => item.Role) // eager load roles .Include(item => item.User) // eager load users - .Where(item => item.Role.SiteId == siteId || item.Role.SiteId == null); + .Where(item => item.Role.SiteId == siteId || item.Role.SiteId == null || siteId == -1); } public UserRole AddUserRole(UserRole userRole) From 40f9437ea57732dcaccf18564b56341c5c0ee4ca Mon Sep 17 00:00:00 2001 From: hishamco Date: Mon, 29 Mar 2021 20:15:04 +0300 Subject: [PATCH 14/31] Add Resources folder to Oqtane.Client --- Oqtane.Client/Resources/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Oqtane.Client/Resources/.gitkeep diff --git a/Oqtane.Client/Resources/.gitkeep b/Oqtane.Client/Resources/.gitkeep new file mode 100644 index 00000000..e69de29b From 62362b91940fffb81086baed60ad5507ae782684 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Tue, 30 Mar 2021 10:06:25 -0400 Subject: [PATCH 15/31] make module creator templates extensible --- Oqtane.Client/Modules/Admin/Files/Edit.razor | 2 +- .../Modules/Admin/ModuleCreator/Index.razor | 29 +++-- .../Interfaces/IModuleDefinitionService.cs | 1 + .../Services/ModuleDefinitionService.cs | 6 ++ .../Controllers/InstallationController.cs | 18 +++- .../Controllers/ModuleDefinitionController.cs | 54 +++------- .../Infrastructure/UpgradeManager.cs | 14 ++- .../Modules/[Owner].[Module]/Edit.razor | 100 ------------------ .../Modules/[Owner].[Module]/Index.razor | 100 ------------------ .../Modules/[Owner].[Module]/Interop.cs | 15 --- .../Modules/[Owner].[Module]/ModuleInfo.cs | 17 --- .../Services/I[Module]Service.cs | 19 ---- .../Services/[Module]Service.cs | 49 --------- .../Modules/[Owner].[Module]/Settings.razor | 47 -------- .../Controllers/[Module]Controller.cs | 91 ---------------- .../Manager/[Module]Manager.cs | 61 ----------- .../Repository/I[Module]Repository.cs | 14 --- .../Repository/[Module]Context.cs | 18 ---- .../Repository/[Module]Repository.cs | 49 --------- .../Scripts/[Owner].[Module].1.0.0.sql | 26 ----- .../Scripts/[Owner].[Module].Uninstall.sql | 6 -- .../Modules/[Owner].[Module]/Module.css | 1 - .../Modules/[Owner].[Module]/Module.js | 5 - .../[Owner].[Module]/Models/[Module].cs | 19 ---- Oqtane.Shared/Shared/Constants.cs | 4 +- 25 files changed, 67 insertions(+), 698 deletions(-) delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Edit.razor delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Index.razor delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Interop.cs delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/ModuleInfo.cs delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Services/I[Module]Service.cs delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Services/[Module]Service.cs delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Settings.razor delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Controllers/[Module]Controller.cs delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Manager/[Module]Manager.cs delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/I[Module]Repository.cs delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/[Module]Context.cs delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/[Module]Repository.cs delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Scripts/[Owner].[Module].1.0.0.sql delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Scripts/[Owner].[Module].Uninstall.sql delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/wwwroot/Modules/[Owner].[Module]/Module.css delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/wwwroot/Modules/[Owner].[Module]/Module.js delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Shared/Modules/[Owner].[Module]/Models/[Module].cs diff --git a/Oqtane.Client/Modules/Admin/Files/Edit.razor b/Oqtane.Client/Modules/Admin/Files/Edit.razor index 892f0559..b2ac3b64 100644 --- a/Oqtane.Client/Modules/Admin/Files/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Files/Edit.razor @@ -36,7 +36,7 @@ - + diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Index.razor b/Oqtane.Client/Modules/Admin/ModuleCreator/Index.razor index d824e595..6baa0376 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Index.razor @@ -9,7 +9,7 @@ @using System.Text.RegularExpressions @using System.IO; -@if (string.IsNullOrEmpty(_moduledefinitionname)) +@if (string.IsNullOrEmpty(_moduledefinitionname) && _systeminfo != null && _templates != null) { @@ -38,13 +38,15 @@ @@ -90,9 +92,11 @@ else private string _module = string.Empty; private string _description = string.Empty; private string _template = "-"; - public string _reference = Constants.Version; + private string _reference = Constants.Version; private string _location = string.Empty; + private Dictionary _systeminfo; + private List _templates; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; @@ -102,6 +106,7 @@ else { _moduledefinitionname = SettingService.GetSetting(ModuleState.Settings, "ModuleDefinitionName", ""); _systeminfo = await SystemService.GetSystemInfoAsync(); + _templates = await ModuleDefinitionService.GetModuleDefinitionTemplatesAsync(); if (string.IsNullOrEmpty(_moduledefinitionname)) { @@ -185,18 +190,8 @@ else if (_template != "-" && _systeminfo != null && _systeminfo.ContainsKey("serverpath")) { string[] path = _systeminfo["serverpath"].Split(Path.DirectorySeparatorChar); - if (_template == "internal") - { - _location = string.Join(Path.DirectorySeparatorChar, path, 0, path.Length - 1) + - Path.DirectorySeparatorChar + "Oqtane.Client" + - Path.DirectorySeparatorChar + "Modules" + - Path.DirectorySeparatorChar + _owner + "." + _module; - } - else - { - _location = string.Join(Path.DirectorySeparatorChar, path, 0, path.Length - 2) + - Path.DirectorySeparatorChar + _owner + "." + _module; - } + _location = string.Join(Path.DirectorySeparatorChar, path, 0, path.Length - 2) + + Path.DirectorySeparatorChar + _owner + "." + _module; } StateHasChanged(); } diff --git a/Oqtane.Client/Services/Interfaces/IModuleDefinitionService.cs b/Oqtane.Client/Services/Interfaces/IModuleDefinitionService.cs index e4b8ce64..55f8ad91 100644 --- a/Oqtane.Client/Services/Interfaces/IModuleDefinitionService.cs +++ b/Oqtane.Client/Services/Interfaces/IModuleDefinitionService.cs @@ -13,5 +13,6 @@ namespace Oqtane.Services Task InstallModuleDefinitionsAsync(); Task DeleteModuleDefinitionAsync(int moduleDefinitionId, int siteId); Task CreateModuleDefinitionAsync(ModuleDefinition moduleDefinition); + Task> GetModuleDefinitionTemplatesAsync(); } } diff --git a/Oqtane.Client/Services/ModuleDefinitionService.cs b/Oqtane.Client/Services/ModuleDefinitionService.cs index 87ae7e09..9f8984c0 100644 --- a/Oqtane.Client/Services/ModuleDefinitionService.cs +++ b/Oqtane.Client/Services/ModuleDefinitionService.cs @@ -53,5 +53,11 @@ namespace Oqtane.Services { return await PostJsonAsync($"{Apiurl}", moduleDefinition); } + + public async Task> GetModuleDefinitionTemplatesAsync() + { + List templates = await GetJsonAsync>($"{Apiurl}/templates"); + return templates; + } } } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index cac15728..01de103b 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -130,7 +130,14 @@ namespace Oqtane.Controllers var instance = Activator.CreateInstance(type) as IModule; foreach (string name in instance.ModuleDefinition.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - if (!list.Contains(name)) list.Insert(0, name); + if (System.IO.File.Exists(Path.Combine(binFolder, name + ".dll"))) + { + if (!list.Contains(name)) list.Insert(0, name); + } + else + { + Console.WriteLine("Module " + instance.ModuleDefinition.ModuleDefinitionName + " dependency " + name + ".dll does not exist"); + } } } foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(ITheme)))) @@ -138,7 +145,14 @@ namespace Oqtane.Controllers var instance = Activator.CreateInstance(type) as ITheme; foreach (string name in instance.Theme.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - if (!list.Contains(name)) list.Insert(0, name); + if (System.IO.File.Exists(Path.Combine(binFolder, name + ".dll"))) + { + if (!list.Contains(name)) list.Insert(0, name); + } + else + { + Console.WriteLine("Theme " + instance.Theme.ThemeName + " dependency " + name + ".dll does not exist" ); + } } } } diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index f417ce33..8769c120 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -13,8 +13,6 @@ using Oqtane.Repository; using Oqtane.Security; using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using System.Xml.Linq; using System.Text.Json; namespace Oqtane.Controllers @@ -174,6 +172,20 @@ namespace Oqtane.Controllers } } + // GET: api//templates + [HttpGet("templates")] + [Authorize(Roles = RoleNames.Host)] + public List Get() + { + var templates = new List(); + string templatePath = Utilities.PathCombine(_environment.WebRootPath, "Modules", "Templates", Path.DirectorySeparatorChar.ToString()); + foreach (string directory in Directory.GetDirectories(templatePath)) + { + templates.Add(directory.Replace(templatePath, "")); + } + return templates; + } + // POST api/?moduleid=x [HttpPost] [Authorize(Roles = RoleNames.Host)] @@ -185,30 +197,12 @@ namespace Oqtane.Controllers DirectoryInfo rootFolder = Directory.GetParent(_environment.ContentRootPath); string templatePath = Utilities.PathCombine(_environment.WebRootPath, "Modules", "Templates", moduleDefinition.Template,Path.DirectorySeparatorChar.ToString()); - if (moduleDefinition.Template == "internal") - { - rootPath = Utilities.PathCombine(rootFolder.FullName,Path.DirectorySeparatorChar.ToString()); - moduleDefinition.ModuleDefinitionName = moduleDefinition.Owner + "." + moduleDefinition.Name + ", Oqtane.Client"; - moduleDefinition.ServerManagerType = moduleDefinition.Owner + "." + moduleDefinition.Name + ".Manager." + moduleDefinition.Name + "Manager, Oqtane.Server"; - } - else - { - rootPath = Utilities.PathCombine(rootFolder.Parent.FullName , moduleDefinition.Owner + "." + moduleDefinition.Name,Path.DirectorySeparatorChar.ToString()); - moduleDefinition.ModuleDefinitionName = moduleDefinition.Owner + "." + moduleDefinition.Name + ", " + moduleDefinition.Owner + "." + moduleDefinition.Name + ".Client.Oqtane"; - moduleDefinition.ServerManagerType = moduleDefinition.Owner + "." + moduleDefinition.Name + ".Manager." + moduleDefinition.Name + "Manager, " + moduleDefinition.Owner + "." + moduleDefinition.Name + ".Server.Oqtane"; - } + rootPath = Utilities.PathCombine(rootFolder.Parent.FullName , moduleDefinition.Owner + "." + moduleDefinition.Name,Path.DirectorySeparatorChar.ToString()); + moduleDefinition.ModuleDefinitionName = moduleDefinition.Owner + "." + moduleDefinition.Name + ", " + moduleDefinition.Owner + "." + moduleDefinition.Name + ".Client.Oqtane"; + moduleDefinition.ServerManagerType = moduleDefinition.Owner + "." + moduleDefinition.Name + ".Manager." + moduleDefinition.Name + "Manager, " + moduleDefinition.Owner + "." + moduleDefinition.Name + ".Server.Oqtane"; ProcessTemplatesRecursively(new DirectoryInfo(templatePath), rootPath, rootFolder.Name, templatePath, moduleDefinition); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Module Definition Created {ModuleDefinition}", moduleDefinition); - - if (moduleDefinition.Template == "internal") - { - // add embedded resources to project file - List resources = new List(); - resources.Add(Utilities.PathCombine("Modules", moduleDefinition.Owner + "." + moduleDefinition.Name, "Scripts", moduleDefinition.Owner + "." + moduleDefinition.Name + ".1.0.0.sql")); - resources.Add(Utilities.PathCombine("Modules", moduleDefinition.Owner + "." + moduleDefinition.Name, "Scripts", moduleDefinition.Owner + "." + moduleDefinition.Name + ".Uninstall.sql")); - EmbedResourceFiles(Utilities.PathCombine(rootPath, "Oqtane.Server", "Oqtane.Server.csproj"), resources); - } } return moduleDefinition; @@ -269,19 +263,5 @@ namespace Oqtane.Controllers } } } - - private void EmbedResourceFiles(string projectfile, List resources) - { - XDocument project = XDocument.Load(projectfile); - var itemGroup = project.Descendants("ItemGroup").Descendants("EmbeddedResource").FirstOrDefault().Parent; - if (itemGroup != null) - { - foreach (var resource in resources) - { - itemGroup.Add(new XElement("EmbeddedResource", new XAttribute("Include", resource))); - } - } - project.Save(projectfile); - } } } diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 0344de6e..cbfcc63b 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -1,9 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; using Oqtane.Extensions; using Oqtane.Models; using Oqtane.Repository; using Oqtane.Shared; using System.Collections.Generic; +using System.IO; using System.Linq; namespace Oqtane.Infrastructure @@ -12,11 +14,13 @@ namespace Oqtane.Infrastructure { private readonly IAliasRepository _aliases; private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IWebHostEnvironment _environment; - public UpgradeManager(IAliasRepository aliases, IServiceScopeFactory serviceScopeFactory) + public UpgradeManager(IAliasRepository aliases, IServiceScopeFactory serviceScopeFactory, IWebHostEnvironment environment) { _aliases = aliases; _serviceScopeFactory = serviceScopeFactory; + _environment = environment; } public void Upgrade(Tenant tenant, string version) @@ -61,6 +65,12 @@ namespace Oqtane.Infrastructure //}); CreateSitePages(tenant, pageTemplates); break; + case "2.0.2": + if (tenant.Name == TenantNames.Master) + { + Directory.Delete(Utilities.PathCombine(_environment.WebRootPath, "Modules", "Templates", "Internal", Path.DirectorySeparatorChar.ToString()), true); + } + break; } } diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Edit.razor b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Edit.razor deleted file mode 100644 index 339879f8..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Edit.razor +++ /dev/null @@ -1,100 +0,0 @@ -@using Oqtane.Modules.Controls -@using [Owner].[Module].Services -@using [Owner].[Module].Models - -@namespace [Owner].[Module] -@inherits ModuleBase -@inject I[Module]Service [Module]Service -@inject NavigationManager NavigationManager - -
- +
- - - - -
- - - -
- -Cancel -
-
-@if (PageState.Action == "Edit") -{ - -} - -@code { - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; - - public override string Actions => "Add,Edit"; - - public override string Title => "Manage [Module]"; - - public override List Resources => new List() - { - new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" } - }; - - int _id; - string _name; - string _createdby; - DateTime _createdon; - string _modifiedby; - DateTime _modifiedon; - - protected override async Task OnInitializedAsync() - { - try - { - if (PageState.Action == "Edit") - { - _id = Int32.Parse(PageState.QueryString["id"]); - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); - if ([Module] != null) - { - _name = [Module].Name; - _createdby = [Module].CreatedBy; - _createdon = [Module].CreatedOn; - _modifiedby = [Module].ModifiedBy; - _modifiedon = [Module].ModifiedOn; - } - } - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading [Module] {[Module]Id} {Error}", _id, ex.Message); - AddModuleMessage("Error Loading [Module]", MessageType.Error); - } - } - - private async Task Save() - { - try - { - if (PageState.Action == "Add") - { - [Module] [Module] = new [Module](); - [Module].ModuleId = ModuleState.ModuleId; - [Module].Name = _name; - [Module] = await [Module]Service.Add[Module]Async([Module]); - await logger.LogInformation("[Module] Added {[Module]}", [Module]); - } - else - { - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); - [Module].Name = _name; - await [Module]Service.Update[Module]Async([Module]); - await logger.LogInformation("[Module] Updated {[Module]}", [Module]); - } - NavigationManager.NavigateTo(NavigateUrl()); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Saving [Module] {Error}", ex.Message); - AddModuleMessage("Error Saving [Module]", MessageType.Error); - } - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Index.razor b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Index.razor deleted file mode 100644 index 37cfef2a..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Index.razor +++ /dev/null @@ -1,100 +0,0 @@ -@using [Owner].[Module].Services -@using [Owner].[Module].Models - -@namespace [Owner].[Module] -@inherits ModuleBase -@inject I[Module]Service [Module]Service -@inject NavigationManager NavigationManager - -@if (_[Module]s == null) -{ -

Loading...

-} -else -{ - -
-
- @if (@_[Module]s.Count != 0) - { - -
-   -   - Name -
- - - - @context.Name - -
- } - else - { -

No [Module]s To Display

- } -} - - - -
-[Module] Module Created Successfully. Use Edit Mode To Add A [Module]. You Can Access The Files At The Following Locations:

-[RootPath]Oqtane.Client\Modules\[Module]\
-- Edit.razor - component for adding or editing content
-- Index.razor - main component for your module **the content you are reading is in this file**
-- ModuleInfo.cs - implements IModule interface to provide configuration settings for your module
-- Settings.razor - component for managing module settings
-- Services\I[Module]Service.cs - interface for defining service API methods
-- Services\[Module]Service.cs - implements service API interface methods

-[RootPath]Oqtane.Server\Modules\[Module]\
-- Controllers\[Module]Controller.cs - API methods implemented using a REST pattern
-- Manager\[Module]Manager.cs - implements optional module interfaces for features such as import/export of content
-- Repository\I[Module]Repository.cs - interface for defining repository methods
-- Repository\[Module]Respository.cs - implements repository interface methods for data access using EF Core
-- Repository\[Module]Context.cs - provides a DB Context for data access
-- Scripts\[Owner].[Module]s.1.0.0.sql - database schema definition script
-- Scripts\[Owner].[Module]s.Uninstall.sql - database uninstall script

-[RootPath]Oqtane.Shared\Modules\[Module]\
-- Models\[Module].cs - model definition

- - - -@code { - public override List Resources => new List() - { - new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }, - new Resource { ResourceType = ResourceType.Script, Url = ModulePath() + "Module.js" } - }; - - List<[Module]> _[Module]s; - - protected override async Task OnInitializedAsync() - { - try - { - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading [Module] {Error}", ex.Message); - AddModuleMessage("Error Loading [Module]", MessageType.Error); - } - } - - private async Task Delete([Module] [Module]) - { - try - { - await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); - await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Deleting [Module] {[Module]} {Error}", [Module], ex.Message); - AddModuleMessage("Error Deleting [Module]", MessageType.Error); - } - } -} \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Interop.cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Interop.cs deleted file mode 100644 index 9d6f0a4e..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Interop.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.JSInterop; -using System.Threading.Tasks; - -namespace [Owner].[Module] -{ - public class Interop - { - private readonly IJSRuntime _jsRuntime; - - public Interop(IJSRuntime jsRuntime) - { - _jsRuntime = jsRuntime; - } - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/ModuleInfo.cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/ModuleInfo.cs deleted file mode 100644 index 6586d51d..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/ModuleInfo.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Oqtane.Models; -using Oqtane.Modules; - -namespace [Owner].[Module] -{ - public class ModuleInfo : IModule - { - public ModuleDefinition ModuleDefinition => new ModuleDefinition - { - Name = "[Module]", - Description = "[Module]", - Version = "1.0.0", - ServerManagerType = "[ServerManagerType]", - ReleaseVersions = "1.0.0" - }; - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Services/I[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Services/I[Module]Service.cs deleted file mode 100644 index 601eba6a..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Services/I[Module]Service.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using [Owner].[Module].Models; - -namespace [Owner].[Module].Services -{ - public interface I[Module]Service - { - Task> Get[Module]sAsync(int ModuleId); - - Task Get[Module]Async(int [Module]Id, int ModuleId); - - Task Add[Module]Async(Models.[Module] [Module]); - - Task Update[Module]Async(Models.[Module] [Module]); - - Task Delete[Module]Async(int [Module]Id, int ModuleId); - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Services/[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Services/[Module]Service.cs deleted file mode 100644 index 05a85ac7..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Services/[Module]Service.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Oqtane.Modules; -using Oqtane.Services; -using Oqtane.Shared; -using [Owner].[Module].Models; - -namespace [Owner].[Module].Services -{ - public class [Module]Service : ServiceBase, I[Module]Service, IService - { - private readonly SiteState _siteState; - - public [Module]Service(HttpClient http, SiteState siteState) : base(http) - { - _siteState = siteState; - } - - private string Apiurl => CreateApiUrl(_siteState.Alias, "[Module]"); - - public async Task> Get[Module]sAsync(int ModuleId) - { - List [Module]s = await GetJsonAsync>(CreateAuthorizationPolicyUrl($"{Apiurl}?moduleid={ModuleId}", ModuleId)); - return [Module]s.OrderBy(item => item.Name).ToList(); - } - - public async Task Get[Module]Async(int [Module]Id, int ModuleId) - { - return await GetJsonAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}", ModuleId)); - } - - public async Task Add[Module]Async(Models.[Module] [Module]) - { - return await PostJsonAsync(CreateAuthorizationPolicyUrl($"{Apiurl}", [Module].ModuleId), [Module]); - } - - public async Task Update[Module]Async(Models.[Module] [Module]) - { - return await PutJsonAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module].[Module]Id}", [Module].ModuleId), [Module]); - } - - public async Task Delete[Module]Async(int [Module]Id, int ModuleId) - { - await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}", ModuleId)); - } - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Settings.razor b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Settings.razor deleted file mode 100644 index 7989d7c9..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Client/Modules/[Owner].[Module]/Settings.razor +++ /dev/null @@ -1,47 +0,0 @@ -@namespace [Owner].[Module] -@inherits ModuleBase -@inject ISettingService SettingService - - - - - - -
- - - -
- -@code { - public override string Title => "[Module] Settings"; - - string _value; - - protected override async Task OnInitializedAsync() - { - try - { - Dictionary settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); - _value = SettingService.GetSetting(settings, "SettingName", ""); - } - catch (Exception ex) - { - ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error); - } - } - - public async Task UpdateSettings() - { - try - { - Dictionary settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); - SettingService.SetSetting(settings, "SettingName", _value); - await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); - } - catch (Exception ex) - { - ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error); - } - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Controllers/[Module]Controller.cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Controllers/[Module]Controller.cs deleted file mode 100644 index 3e2fe7b8..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Controllers/[Module]Controller.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; -using Oqtane.Shared; -using Oqtane.Enums; -using Oqtane.Infrastructure; -using [Owner].[Module].Models; -using [Owner].[Module].Repository; - -namespace [Owner].[Module].Controllers -{ - [Route(ControllerRoutes.Default)] - public class [Module]Controller : Controller - { - private readonly I[Module]Repository _[Module]Repository; - private readonly ILogManager _logger; - protected int _entityId = -1; - - public [Module]Controller(I[Module]Repository [Module]Repository, ILogManager logger, IHttpContextAccessor accessor) - { - _[Module]Repository = [Module]Repository; - _logger = logger; - - if (accessor.HttpContext.Request.Query.ContainsKey("entityid")) - { - _entityId = int.Parse(accessor.HttpContext.Request.Query["entityid"]); - } - } - - // GET: api/?moduleid=x - [HttpGet] - [Authorize(Policy = PolicyNames.ViewModule)] - public IEnumerable Get(string moduleid) - { - return _[Module]Repository.Get[Module]s(int.Parse(moduleid)); - } - - // GET api//5 - [HttpGet("{id}")] - [Authorize(Policy = PolicyNames.ViewModule)] - public Models.[Module] Get(int id) - { - Models.[Module] [Module] = _[Module]Repository.Get[Module](id); - if ([Module] != null && [Module].ModuleId != _entityId) - { - [Module] = null; - } - return [Module]; - } - - // POST api/ - [HttpPost] - [Authorize(Policy = PolicyNames.EditModule)] - public Models.[Module] Post([FromBody] Models.[Module] [Module]) - { - if (ModelState.IsValid && [Module].ModuleId == _entityId) - { - [Module] = _[Module]Repository.Add[Module]([Module]); - _logger.Log(LogLevel.Information, this, LogFunction.Create, "[Module] Added {[Module]}", [Module]); - } - return [Module]; - } - - // PUT api//5 - [HttpPut("{id}")] - [Authorize(Policy = PolicyNames.EditModule)] - public Models.[Module] Put(int id, [FromBody] Models.[Module] [Module]) - { - if (ModelState.IsValid && [Module].ModuleId == _entityId) - { - [Module] = _[Module]Repository.Update[Module]([Module]); - _logger.Log(LogLevel.Information, this, LogFunction.Update, "[Module] Updated {[Module]}", [Module]); - } - return [Module]; - } - - // DELETE api//5 - [HttpDelete("{id}")] - [Authorize(Policy = PolicyNames.EditModule)] - public void Delete(int id) - { - Models.[Module] [Module] = _[Module]Repository.Get[Module](id); - if ([Module] != null && [Module].ModuleId == _entityId) - { - _[Module]Repository.Delete[Module](id); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "[Module] Deleted {[Module]Id}", id); - } - } - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Manager/[Module]Manager.cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Manager/[Module]Manager.cs deleted file mode 100644 index 486c8ddf..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Manager/[Module]Manager.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Oqtane.Modules; -using Oqtane.Models; -using Oqtane.Infrastructure; -using Oqtane.Repository; -using [Owner].[Module].Models; -using [Owner].[Module].Repository; - -namespace [Owner].[Module].Manager -{ - public class [Module]Manager : IInstallable, IPortable - { - private I[Module]Repository _[Module]Repository; - private ISqlRepository _sql; - - public [Module]Manager(I[Module]Repository [Module]Repository, ISqlRepository sql) - { - _[Module]Repository = [Module]Repository; - _sql = sql; - } - - public bool Install(Tenant tenant, string version) - { - return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module]." + version + ".sql"); - } - - public bool Uninstall(Tenant tenant) - { - return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module].Uninstall.sql"); - } - - public string ExportModule(Module module) - { - string content = ""; - List [Module]s = _[Module]Repository.Get[Module]s(module.ModuleId).ToList(); - if ([Module]s != null) - { - content = JsonSerializer.Serialize([Module]s); - } - return content; - } - - public void ImportModule(Module module, string content, string version) - { - List [Module]s = null; - if (!string.IsNullOrEmpty(content)) - { - [Module]s = JsonSerializer.Deserialize>(content); - } - if ([Module]s != null) - { - foreach(var [Module] in [Module]s) - { - _[Module]Repository.Add[Module](new Models.[Module] { ModuleId = module.ModuleId, Name = [Module].Name }); - } - } - } - } -} \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/I[Module]Repository.cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/I[Module]Repository.cs deleted file mode 100644 index 52b7913a..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/I[Module]Repository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using [Owner].[Module].Models; - -namespace [Owner].[Module].Repository -{ - public interface I[Module]Repository - { - IEnumerable Get[Module]s(int ModuleId); - Models.[Module] Get[Module](int [Module]Id); - Models.[Module] Add[Module](Models.[Module] [Module]); - Models.[Module] Update[Module](Models.[Module] [Module]); - void Delete[Module](int [Module]Id); - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/[Module]Context.cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/[Module]Context.cs deleted file mode 100644 index a10d7219..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/[Module]Context.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Http; -using Oqtane.Modules; -using Oqtane.Repository; -using [Owner].[Module].Models; - -namespace [Owner].[Module].Repository -{ - public class [Module]Context : DBContextBase, IService - { - public virtual DbSet [Module] { get; set; } - - public [Module]Context(ITenantResolver tenantResolver, IHttpContextAccessor accessor) : base(tenantResolver, accessor) - { - // ContextBase handles multi-tenant database connections - } - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/[Module]Repository.cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/[Module]Repository.cs deleted file mode 100644 index 9c8c3628..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Repository/[Module]Repository.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System.Linq; -using System.Collections.Generic; -using Oqtane.Modules; -using [Owner].[Module].Models; - -namespace [Owner].[Module].Repository -{ - public class [Module]Repository : I[Module]Repository, IService - { - private readonly [Module]Context _db; - - public [Module]Repository([Module]Context context) - { - _db = context; - } - - public IEnumerable Get[Module]s(int ModuleId) - { - return _db.[Module].Where(item => item.ModuleId == ModuleId); - } - - public Models.[Module] Get[Module](int [Module]Id) - { - return _db.[Module].Find([Module]Id); - } - - public Models.[Module] Add[Module](Models.[Module] [Module]) - { - _db.[Module].Add([Module]); - _db.SaveChanges(); - return [Module]; - } - - public Models.[Module] Update[Module](Models.[Module] [Module]) - { - _db.Entry([Module]).State = EntityState.Modified; - _db.SaveChanges(); - return [Module]; - } - - public void Delete[Module](int [Module]Id) - { - Models.[Module] [Module] = _db.[Module].Find([Module]Id); - _db.[Module].Remove([Module]); - _db.SaveChanges(); - } - } -} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Scripts/[Owner].[Module].1.0.0.sql b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Scripts/[Owner].[Module].1.0.0.sql deleted file mode 100644 index 7a1b99ea..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Scripts/[Owner].[Module].1.0.0.sql +++ /dev/null @@ -1,26 +0,0 @@ -/* -Create [Owner][Module] table -*/ - -CREATE TABLE [dbo].[[Owner][Module]]( - [[Module]Id] [int] IDENTITY(1,1) NOT NULL, - [ModuleId] [int] NOT NULL, - [Name] [nvarchar](256) NOT NULL, - [CreatedBy] [nvarchar](256) NOT NULL, - [CreatedOn] [datetime] NOT NULL, - [ModifiedBy] [nvarchar](256) NOT NULL, - [ModifiedOn] [datetime] NOT NULL, - CONSTRAINT [PK_[Owner][Module]] PRIMARY KEY CLUSTERED - ( - [[Module]Id] ASC - ) -) -GO - -/* -Create foreign key relationships -*/ -ALTER TABLE [dbo].[[Owner][Module]] WITH CHECK ADD CONSTRAINT [FK_[Owner][Module]_Module] FOREIGN KEY([ModuleId]) -REFERENCES [dbo].Module ([ModuleId]) -ON DELETE CASCADE -GO \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Scripts/[Owner].[Module].Uninstall.sql b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Scripts/[Owner].[Module].Uninstall.sql deleted file mode 100644 index 47baecc9..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/Modules/[Owner].[Module]/Scripts/[Owner].[Module].Uninstall.sql +++ /dev/null @@ -1,6 +0,0 @@ -/* -Remove [Owner][Module] table -*/ - -DROP TABLE [dbo].[[Owner][Module]] -GO diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/wwwroot/Modules/[Owner].[Module]/Module.css b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/wwwroot/Modules/[Owner].[Module]/Module.css deleted file mode 100644 index 0856a263..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/wwwroot/Modules/[Owner].[Module]/Module.css +++ /dev/null @@ -1 +0,0 @@ -/* Module Custom Styles */ \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/wwwroot/Modules/[Owner].[Module]/Module.js b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/wwwroot/Modules/[Owner].[Module]/Module.js deleted file mode 100644 index 8f072470..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Server/wwwroot/Modules/[Owner].[Module]/Module.js +++ /dev/null @@ -1,5 +0,0 @@ -/* Module Script */ -var [Owner] = [Owner] || {}; - -[Owner].[Module] = { -}; \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Shared/Modules/[Owner].[Module]/Models/[Module].cs b/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Shared/Modules/[Owner].[Module]/Models/[Module].cs deleted file mode 100644 index 94f73ffa..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/Internal/Oqtane.Shared/Modules/[Owner].[Module]/Models/[Module].cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations.Schema; -using Oqtane.Models; - -namespace [Owner].[Module].Models -{ - [Table("[Owner][Module]")] - public class [Module] : IAuditable - { - public int [Module]Id { get; set; } - public int ModuleId { get; set; } - public string Name { get; set; } - - public string CreatedBy { get; set; } - public DateTime CreatedOn { get; set; } - public string ModifiedBy { get; set; } - public DateTime ModifiedOn { get; set; } - } -} diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 1b05a7e7..60106dd2 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -5,8 +5,8 @@ namespace Oqtane.Shared { public class Constants { public const string PackageId = "Oqtane.Framework"; - public const string Version = "2.0.1"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1"; + public const string Version = "2.0.2"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2"; public const string PageComponent = "Oqtane.UI.ThemeBuilder, Oqtane.Client"; public const string ContainerComponent = "Oqtane.UI.ContainerBuilder, Oqtane.Client"; From 367a23171d187a5b69a1789dcb8363fc5805e5a5 Mon Sep 17 00:00:00 2001 From: hishamco Date: Tue, 30 Mar 2021 17:26:03 +0300 Subject: [PATCH 16/31] thene -> theme --- Oqtane.Client/Modules/Admin/Themes/View.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Admin/Themes/View.razor b/Oqtane.Client/Modules/Admin/Themes/View.razor index 2510759d..d6d4b4de 100644 --- a/Oqtane.Client/Modules/Admin/Themes/View.razor +++ b/Oqtane.Client/Modules/Admin/Themes/View.razor @@ -24,7 +24,7 @@ - + From 09c040128a28a2ff07ab2ccbf39922604397d8b6 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Tue, 30 Mar 2021 17:48:49 -0400 Subject: [PATCH 17/31] Ensure Install Wizard will only be displayed if the Master database connection string in appsettings.json is not specified. This addresses a potential security issue where the Install Wizard could be displayed in an existing installation if the Master database connection failed during startup. --- Oqtane.Client/App.razor | 28 ++++++++++------- Oqtane.Client/Modules/Admin/Sites/Index.razor | 2 +- .../Controllers/InstallationController.cs | 3 +- .../Infrastructure/DatabaseManager.cs | 30 +++++++++++++------ .../Interfaces/IDatabaseManager.cs | 4 +-- .../Infrastructure/UpgradeManager.cs | 15 ++++++---- Oqtane.Server/Pages/_Host.cshtml | 4 ++- Oqtane.Server/wwwroot/css/app.css | 9 +++++- 8 files changed, 63 insertions(+), 32 deletions(-) diff --git a/Oqtane.Client/App.razor b/Oqtane.Client/App.razor index 3d13adff..ca4b25bd 100644 --- a/Oqtane.Client/App.razor +++ b/Oqtane.Client/App.razor @@ -1,31 +1,39 @@ -@inject IInstallationService InstallationService +@inject IInstallationService InstallationService @if (_initialized) { - @if (!_installed) + @if (!_installation.Success) { } else { - - - - - + @if (string.IsNullOrEmpty(_installation.Message)) + { + + + + + + } + else + { +
+ @_installation.Message +
+ } } } @code { + private Installation _installation; private bool _initialized; - private bool _installed; private PageState PageState { get; set; } protected override async Task OnParametersSetAsync() { - var installation = await InstallationService.IsInstalled(); - _installed = installation.Success; + _installation = await InstallationService.IsInstalled(); _initialized = true; } diff --git a/Oqtane.Client/Modules/Admin/Sites/Index.razor b/Oqtane.Client/Modules/Admin/Sites/Index.razor index 3a571cd3..9d028e1d 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Index.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Index.razor @@ -22,7 +22,7 @@ else - @context.Name + @context.Name } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 01de103b..2d8491d6 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -55,8 +55,7 @@ namespace Oqtane.Controllers [HttpGet("installed")] public Installation IsInstalled() { - bool isInstalled = _databaseManager.IsInstalled(); - return new Installation {Success = isInstalled, Message = string.Empty}; + return _databaseManager.IsInstalled(); } [HttpGet("upgrade")] diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 308b5cf0..9ee98e25 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -33,27 +33,30 @@ namespace Oqtane.Infrastructure _cache = cache; } - public bool IsInstalled() + public Installation IsInstalled() { - var defaultConnectionString = NormalizeConnectionString(_config.GetConnectionString(SettingKeys.ConnectionStringKey)); - var result = !string.IsNullOrEmpty(defaultConnectionString); - if (result) + var result = new Installation { Success = false, Message = string.Empty }; + if (!string.IsNullOrEmpty(_config.GetConnectionString(SettingKeys.ConnectionStringKey))) { + result.Success = true; using (var scope = _serviceScopeFactory.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); - result = db.Database.CanConnect(); - if (result) + if (db.Database.CanConnect()) { try { - result = db.Tenant.Any(); + var provisioned = db.Tenant.Any(); } catch { - result = false; + result.Message = "Master Database Not Installed Correctly"; } } + else + { + result.Message = "Cannot Connect To Master Database"; + } } } return result; @@ -74,7 +77,8 @@ namespace Oqtane.Infrastructure // startup or silent installation install = new InstallConfig { ConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey), TenantName = TenantNames.Master, IsNewTenant = false }; - if (!IsInstalled()) + var installation = IsInstalled(); + if (!installation.Success) { install.Aliases = GetInstallationConfig(SettingKeys.DefaultAliasKey, string.Empty); install.HostPassword = GetInstallationConfig(SettingKeys.HostPasswordKey, string.Empty); @@ -97,6 +101,14 @@ namespace Oqtane.Infrastructure install.ConnectionString = ""; } } + else + { + if (!string.IsNullOrEmpty(installation.Message)) + { + // problem with prior installation + install.ConnectionString = ""; + } + } } else { diff --git a/Oqtane.Server/Infrastructure/Interfaces/IDatabaseManager.cs b/Oqtane.Server/Infrastructure/Interfaces/IDatabaseManager.cs index ffb0ff9c..6257db16 100644 --- a/Oqtane.Server/Infrastructure/Interfaces/IDatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/Interfaces/IDatabaseManager.cs @@ -1,11 +1,11 @@ -using Oqtane.Models; +using Oqtane.Models; using Oqtane.Shared; namespace Oqtane.Infrastructure { public interface IDatabaseManager { - bool IsInstalled(); + Installation IsInstalled(); Installation Install(); Installation Install(InstallConfig install); } diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index cbfcc63b..b2385b2b 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -25,14 +25,12 @@ namespace Oqtane.Infrastructure public void Upgrade(Tenant tenant, string version) { - // core framework upgrade logic - note that you can check if current tenant is Master if you only want to execute logic once - var pageTemplates = new List(); - + // core framework upgrade logic - note that you can check if current tenant is Master if you only want to execute the logic once switch (version) { case "0.9.0": - // add a page to all existing sites on upgrade - + // this code is commented out on purpose - it provides an example of how to programmatically add a page to all existing sites on upgrade + var pageTemplates = new List(); //pageTemplates.Add(new PageTemplate //{ // Name = "Test", @@ -68,7 +66,12 @@ namespace Oqtane.Infrastructure case "2.0.2": if (tenant.Name == TenantNames.Master) { - Directory.Delete(Utilities.PathCombine(_environment.WebRootPath, "Modules", "Templates", "Internal", Path.DirectorySeparatorChar.ToString()), true); + // remove Internal module template files as they are no longer supported + var internalTemplatePath = Utilities.PathCombine(_environment.WebRootPath, "Modules", "Templates", "Internal", Path.DirectorySeparatorChar.ToString()); + if (Directory.Exists(internalTemplatePath)) + { + Directory.Delete(internalTemplatePath, true); + } } break; } diff --git a/Oqtane.Server/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml index e1cc6c21..1678969c 100644 --- a/Oqtane.Server/Pages/_Host.cshtml +++ b/Oqtane.Server/Pages/_Host.cshtml @@ -37,7 +37,9 @@ @if (Model.Message != "") { - @Model.Message +
+ @Model.Message +
} diff --git a/Oqtane.Server/wwwroot/css/app.css b/Oqtane.Server/wwwroot/css/app.css index ce87887d..eebf63a6 100644 --- a/Oqtane.Server/wwwroot/css/app.css +++ b/Oqtane.Server/wwwroot/css/app.css @@ -1,4 +1,4 @@ -@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; @@ -125,6 +125,13 @@ app { vertical-align: inherit; } +.app-alert { + padding: 20px; + background-color: #f44336; /* red */ + color: white; + margin-bottom: 15px; +} + /* Tooltips */ .app-tooltip { cursor: help; From ec0b317f80589b2afbf2157c12cd015145aed390 Mon Sep 17 00:00:00 2001 From: hishamco Date: Wed, 31 Mar 2021 00:50:19 +0300 Subject: [PATCH 18/31] Fix localizer in Admin pages --- Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor | 2 +- Oqtane.Client/Modules/Admin/Pages/Index.razor | 2 +- Oqtane.Client/Modules/Admin/Profiles/Index.razor | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor index 538bf700..3ebfed0b 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor @@ -26,7 +26,7 @@ else @if (context.AssemblyName != "Oqtane.Client") { - + } @context.Name diff --git a/Oqtane.Client/Modules/Admin/Pages/Index.razor b/Oqtane.Client/Modules/Admin/Pages/Index.razor index 4ff925fc..fef67427 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Index.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Index.razor @@ -16,7 +16,7 @@
- + @(new string('-', context.Level * 2))@(context.Name)
diff --git a/Oqtane.Client/Modules/Admin/Profiles/Index.razor b/Oqtane.Client/Modules/Admin/Profiles/Index.razor index 141730f9..4705b290 100644 --- a/Oqtane.Client/Modules/Admin/Profiles/Index.razor +++ b/Oqtane.Client/Modules/Admin/Profiles/Index.razor @@ -19,7 +19,7 @@ else - + @context.Name
From c92a81fcb6f1f644b0110bdb9558fc64092a3468 Mon Sep 17 00:00:00 2001 From: hishamco Date: Wed, 31 Mar 2021 14:15:36 +0300 Subject: [PATCH 19/31] Remove unnecessary localizer from RecycleBin page --- Oqtane.Client/Modules/Admin/RecycleBin/Index.razor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor index 7c30c7a1..47e2a47a 100644 --- a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor +++ b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor @@ -34,7 +34,7 @@ @if (_pages.Any()) {
- +
} } @@ -68,7 +68,7 @@ @if (_modules.Any()) {
- +
} From bd48e1d8f1d37c7285ea4f4e228a89f3b1efe2ef Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 31 Mar 2021 15:39:01 -0400 Subject: [PATCH 20/31] if running on WebAssembly reload the client application if the server application is restarted --- Oqtane.Client/UI/SiteRouter.razor | 9 +++++++-- Oqtane.Server/Infrastructure/SyncManager.cs | 4 ++-- Oqtane.Server/Startup.cs | 5 ++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 6b42f15b..ea3148dc 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -80,7 +80,7 @@ var urlparameters = string.Empty; var editmode = false; var reload = Reload.None; - var lastsyncdate = DateTime.UtcNow; + var lastsyncdate = DateTime.UtcNow.AddHours(-1); var runtime = GetRuntime(); Uri uri = new Uri(_absoluteUri); @@ -107,9 +107,14 @@ SiteState.Alias = alias; // set state for services lastsyncdate = alias.SyncDate; - // process any sync events for site + // process any sync events if (reload != Reload.Site && alias.SyncEvents.Any()) { + // if running on WebAssembly reload the client application if the server application was restarted + if (runtime == Shared.Runtime.WebAssembly && PageState != null && alias.SyncEvents.Exists(item => item.TenantId == -1)) + { + NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority + "?reload", true); + } if (alias.SyncEvents.Exists(item => item.EntityName == EntityNames.Site && item.EntityId == alias.SiteId)) { reload = Reload.Site; diff --git a/Oqtane.Server/Infrastructure/SyncManager.cs b/Oqtane.Server/Infrastructure/SyncManager.cs index f41c01b0..d70cc456 100644 --- a/Oqtane.Server/Infrastructure/SyncManager.cs +++ b/Oqtane.Server/Infrastructure/SyncManager.cs @@ -1,4 +1,4 @@ -using Oqtane.Models; +using Oqtane.Models; using System; using System.Collections.Generic; using System.Linq; @@ -17,7 +17,7 @@ namespace Oqtane.Infrastructure public List GetSyncEvents(int tenantId, DateTime lastSyncDate) { - return SyncEvents.Where(item => item.TenantId == tenantId && item.ModifiedOn >= lastSyncDate).ToList(); + return SyncEvents.Where(item => (item.TenantId == tenantId || item.TenantId == -1) && item.ModifiedOn >= lastSyncDate).ToList(); } public void AddSyncEvent(int tenantId, string entityName, int entityId) diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index ba2a7d89..1e744c01 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -226,7 +226,7 @@ namespace Oqtane } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISyncManager sync) { ServiceActivator.Configure(app.ApplicationServices); @@ -264,6 +264,9 @@ namespace Oqtane endpoints.MapControllers(); endpoints.MapFallbackToPage("/_Host"); }); + + // create a sync event to identify server application startup + sync.AddSyncEvent(-1, "Application", -1); } } } From 58c84da9c990f62bce927109a7476a1fdb66147a Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 1 Apr 2021 09:44:07 -0400 Subject: [PATCH 21/31] add ability to test SMTP connection in Site Settings --- Oqtane.Client/Modules/Admin/Site/Index.razor | 287 ++++++++++-------- .../Modules/Admin/UserProfile/Add.razor | 2 +- .../Modules/Admin/UserProfile/View.razor | 2 +- Oqtane.Client/UI/SiteRouter.razor | 2 +- .../Controllers/NotificationController.cs | 6 +- 5 files changed, 168 insertions(+), 131 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index bbfd28f7..62de4921 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -7,146 +7,150 @@ @inject IThemeService ThemeService @inject ISettingService SettingService @inject IStringLocalizer Localizer +@inject INotificationService NotificationService @if (_initialized) { - - - - - - - - - - - - - - - - - - - - - - - - - - @if (_layouts.Count > 0) - { +
- - - -
- - - -
- - - -
- - - -
- - - -
- - - -
+ + + + + + + + + + + + + + + + + +
- + - +
+ + + +
+ + + +
+ + + +
+ + + +
+
+ + + + + + + + + + + + - } - - - - - - - - - - - - - - - - -
+ + + +
+ + + +
+ + +
- - - -
- - - -
- - - -
- - - -
- + @if (_layouts.Count > 0) + { + + + + + + + + + } + + + + + + + + + + + + + + + + + +
@@ -201,6 +205,8 @@
- @Localizer["Please Note That SMTP Requires The Notification Job To Be Enabled In the Scheduled Jobs"] + @Localizer["Please Note That SMTP Requires The Notification Job To Be Enabled In Scheduled Jobs"]
+ +

@@ -482,7 +488,6 @@ await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await logger.LogInformation("Site Settings Saved {Site}", site); - AddModuleMessage(Localizer["Site Settings Saved"], MessageType.Success); } } @@ -502,4 +507,36 @@ AddModuleMessage(Localizer["Error Saving Site"], MessageType.Error); } } + + private async Task SendEmail() + { + if (_smtphost != "" && _smtpport != "" && _smtpsender != "") + { + try + { + var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); + SettingService.SetSetting(settings, "SMTPHost", _smtphost); + SettingService.SetSetting(settings, "SMTPPort", _smtpport); + SettingService.SetSetting(settings, "SMTPSSL", _smtpssl); + SettingService.SetSetting(settings, "SMTPUsername", _smtpusername); + SettingService.SetSetting(settings, "SMTPPassword", _smtppassword); + SettingService.SetSetting(settings, "SMTPSender", _smtpsender); + await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); + await logger.LogInformation("Site SMTP Settings Saved"); + + await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User.DisplayName, PageState.User.Email, PageState.User.DisplayName, PageState.User.Email, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly.")); + AddModuleMessage(Localizer["SMTP Settings Saved And A Message Has Been Sent To The Email Address Associated To Your User Account... Please Wait A Few Minutes For Delivery. If You Do Not Receive The Email Please Review The Notification Job In Scheduled Jobs For Any Log Details."], MessageType.Info); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Testing SMTP Configuration"); + AddModuleMessage(Localizer["Error Testing SMTP Configuration"], MessageType.Error); + } + } + else + { + AddModuleMessage(Localizer["You Must Specify The SMTP Host, Port, And Sender"], MessageType.Warning); + } + + } } diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Add.razor b/Oqtane.Client/Modules/Admin/UserProfile/Add.razor index dcc96ff4..48959e80 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Add.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Add.razor @@ -55,7 +55,7 @@ { var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body, null); notification = await NotificationService.AddNotificationAsync(notification); - await logger.LogInformation("Notification Created {Notification}", notification); + await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId); NavigationManager.NavigateTo(NavigateUrl()); } else diff --git a/Oqtane.Client/Modules/Admin/UserProfile/View.razor b/Oqtane.Client/Modules/Admin/UserProfile/View.razor index ff9f3937..263a0345 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/View.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/View.razor @@ -183,7 +183,7 @@ { var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body, notificationid); notification = await NotificationService.AddNotificationAsync(notification); - await logger.LogInformation("Notification Created {Notification}", notification); + await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId); NavigationManager.NavigateTo(NavigateUrl()); } else diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index ea3148dc..70bda7df 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -113,7 +113,7 @@ // if running on WebAssembly reload the client application if the server application was restarted if (runtime == Shared.Runtime.WebAssembly && PageState != null && alias.SyncEvents.Exists(item => item.TenantId == -1)) { - NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority + "?reload", true); + NavigationManager.NavigateTo(_absoluteUri + (!_absoluteUri.Contains("?") ? "?" : "&") + "reload", true); } if (alias.SyncEvents.Exists(item => item.EntityName == EntityNames.Site && item.EntityId == alias.SiteId)) { diff --git a/Oqtane.Server/Controllers/NotificationController.cs b/Oqtane.Server/Controllers/NotificationController.cs index 5c73eb78..8fbb4872 100644 --- a/Oqtane.Server/Controllers/NotificationController.cs +++ b/Oqtane.Server/Controllers/NotificationController.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using Oqtane.Enums; @@ -65,7 +65,7 @@ namespace Oqtane.Controllers if (IsAuthorized(notification.FromUserId)) { notification = _notifications.AddNotification(notification); - _logger.Log(LogLevel.Information, this, LogFunction.Create, "Notification Added {Notification}", notification); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Notification Added {NotificationId}", notification.NotificationId); } return notification; } @@ -78,7 +78,7 @@ namespace Oqtane.Controllers if (IsAuthorized(notification.FromUserId)) { notification = _notifications.UpdateNotification(notification); - _logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {Folder}", notification); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId); } return notification; } From af5f79d343ed18e308775fe401551b3424e26863 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 1 Apr 2021 17:58:59 -0400 Subject: [PATCH 22/31] fix dropdown list UX behavior where there is a default option --- Oqtane.Client/Modules/Admin/Pages/Add.razor | 11 ++------- Oqtane.Client/Modules/Admin/Pages/Edit.razor | 24 ++++--------------- Oqtane.Client/Modules/Admin/Site/Index.razor | 11 ++------- Oqtane.Client/Modules/Admin/Sites/Edit.razor | 11 ++------- .../Modules/Controls/FileManager.razor | 22 ++++------------- 5 files changed, 15 insertions(+), 64 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Pages/Add.razor b/Oqtane.Client/Modules/Admin/Pages/Add.razor index 171ab885..b0234619 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Add.razor @@ -101,18 +101,11 @@ diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 5efdd992..e1229e43 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -23,18 +23,11 @@ @@ -112,18 +105,11 @@ diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 62de4921..4fcb5dd3 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -82,18 +82,11 @@ diff --git a/Oqtane.Client/Modules/Admin/Sites/Edit.razor b/Oqtane.Client/Modules/Admin/Sites/Edit.razor index b1afc85c..6d28aee6 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Edit.razor @@ -31,18 +31,11 @@ diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index f4650202..90bc48c5 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -12,21 +12,14 @@ @if (ShowFolders || FolderId <= 0) {
- @if (string.IsNullOrEmpty(Folder)) { } @foreach (Folder folder in _folders) { - if (folder.FolderId == FolderId) - { - - } - else - { - - } + }
@@ -34,18 +27,11 @@ @if (ShowFiles) {
- @foreach (File file in _files) { - if (file.FileId == FileId) - { - - } - else - { - - } + }
From 60a1f4ed157d509788982113f5a366ecbdfaf877 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 2 Apr 2021 07:01:27 +0200 Subject: [PATCH 23/31] Added functionality, when the EnterKey is press the login command is run. --- Oqtane.Client/Modules/Admin/Login/Index.razor | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index bbd2428a..2ba8c918 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -11,10 +11,10 @@ } -
- @foreach (var theme in _themes) { - if (theme.TypeName == _themetype) - { - - } - else - { - - } + } - @foreach (Page page in _pageList) { - if (page.PageId.ToString() == _parentid) - { - - } - else - { - - } + } - + @foreach (var theme in _themes) { - if (theme.TypeName == _themetype) - { - - } - else - { - - } + } - @foreach (var theme in _themes) { - if (theme.TypeName == _themetype) - { - - } - else - { - - } + } - @foreach (var theme in _themes) { - if (theme.TypeName == _themetype) - { - - } - else - { - - } + }
+ + + + + + + + + + + + + + + + + @if (!string.IsNullOrEmpty(_location)) + { + + + + + } +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ +} +else +{ + +} + +@code { + private string _themename = string.Empty; + private string _owner = string.Empty; + private string _theme = string.Empty; + private string _template = "-"; + private string _reference = Constants.Version; + private string _location = string.Empty; + + private Dictionary _systeminfo; + private List _templates; + + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; + + protected override async Task OnInitializedAsync() + { + try + { + _themename = SettingService.GetSetting(ModuleState.Settings, "ThemeName", ""); + _systeminfo = await SystemService.GetSystemInfoAsync(); + _templates = await ThemeService.GetThemeTemplatesAsync(); + + if (string.IsNullOrEmpty(_themename)) + { + AddModuleMessage(Localizer["Please Note That The Theme Creator Is Only Intended To Be Used In A Development Environment"], MessageType.Info); + } + else + { + AddModuleMessage(Localizer["Once You Have Compiled The Theme And Restarted The Application You Can Activate The Theme Below"], MessageType.Info); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading Theme Creator"); + } + } + + private async Task CreateTheme() + { + try + { + if (IsValid(_owner) && IsValid(_theme) && _owner != _theme && _template != "-") + { + var theme = new Theme { Owner = _owner, Name = _theme, Template = _template, Version = _reference }; + theme = await ThemeService.CreateThemeAsync(theme); + + var settings = ModuleState.Settings; + SettingService.SetSetting(settings, "ThemeName", theme.ThemeName); + await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); + + GetLocation(); + + AddModuleMessage(Localizer["The Source Code For Your Theme Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must Restart Your Application To Apply These Changes.", NavigateUrl("admin/system")], MessageType.Success); + } + else + { + AddModuleMessage(Localizer["You Must Provide A Valid Owner Name And Theme Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same ) And Choose A Template"], MessageType.Warning); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Creating Theme"); + } + } + + private async Task ActivateTheme() + { + try + { + if (!string.IsNullOrEmpty(_themename)) + { + await PageModuleService.DeletePageModuleAsync(ModuleState.PageModuleId); + await ModuleService.DeleteModuleAsync(ModuleState.ModuleId); + NavigationManager.NavigateTo(NavigateUrl()); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Activating Theme"); + } + } + + private bool IsValid(string name) + { + // must contain letters, underscores and digits and first character must be letter or underscore + return !string.IsNullOrEmpty(name) && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$"); + } + + private void TemplateChanged(ChangeEventArgs e) + { + _template = (string)e.Value; + GetLocation(); + } + + private void GetLocation() + { + _location = string.Empty; + if (_template != "-" && _systeminfo != null && _systeminfo.ContainsKey("serverpath")) + { + string[] path = _systeminfo["serverpath"].Split(Path.DirectorySeparatorChar); + _location = string.Join(Path.DirectorySeparatorChar, path, 0, path.Length - 2) + + Path.DirectorySeparatorChar + _owner + "." + _theme; + } + StateHasChanged(); + } +} diff --git a/Oqtane.Client/Modules/Admin/ThemeCreator/ModuleInfo.cs b/Oqtane.Client/Modules/Admin/ThemeCreator/ModuleInfo.cs new file mode 100644 index 00000000..621b0953 --- /dev/null +++ b/Oqtane.Client/Modules/Admin/ThemeCreator/ModuleInfo.cs @@ -0,0 +1,15 @@ +using Oqtane.Models; + +namespace Oqtane.Modules.Admin.ThemeCreator +{ + public class ModuleInfo : IModule + { + public ModuleDefinition ModuleDefinition => new ModuleDefinition + { + Name = "Theme Creator", + Description = "Enables software developers to quickly create themes by automating many of the initial theme creation tasks", + Version = "1.0.0", + Categories = "Developer" + }; + } +} diff --git a/Oqtane.Client/Services/Interfaces/IThemeService.cs b/Oqtane.Client/Services/Interfaces/IThemeService.cs index b75a2988..609dd00c 100644 --- a/Oqtane.Client/Services/Interfaces/IThemeService.cs +++ b/Oqtane.Client/Services/Interfaces/IThemeService.cs @@ -1,4 +1,4 @@ -using Oqtane.Models; +using Oqtane.Models; using System.Collections.Generic; using System.Threading.Tasks; @@ -12,5 +12,7 @@ namespace Oqtane.Services List GetContainerControls(List themes, string themeName); Task InstallThemesAsync(); Task DeleteThemeAsync(string themeName); + Task CreateThemeAsync(Theme theme); + Task> GetThemeTemplatesAsync(); } } diff --git a/Oqtane.Client/Services/ThemeService.cs b/Oqtane.Client/Services/ThemeService.cs index bd349b93..c58c7178 100644 --- a/Oqtane.Client/Services/ThemeService.cs +++ b/Oqtane.Client/Services/ThemeService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -50,5 +50,16 @@ namespace Oqtane.Services { await DeleteAsync($"{ApiUrl}/{themeName}"); } + + public async Task CreateThemeAsync(Theme theme) + { + return await PostJsonAsync($"{ApiUrl}", theme); + } + + public async Task> GetThemeTemplatesAsync() + { + List templates = await GetJsonAsync>($"{ApiUrl}/templates"); + return templates; + } } } diff --git a/Oqtane.Client/wwwroot/Themes/Siliqon.TestTheme/Theme.css b/Oqtane.Client/wwwroot/Themes/Siliqon.TestTheme/Theme.css new file mode 100644 index 00000000..93d1b672 --- /dev/null +++ b/Oqtane.Client/wwwroot/Themes/Siliqon.TestTheme/Theme.css @@ -0,0 +1,83 @@ +/* Oqtane Styles */ + +body { + padding-top: 7rem; +} + +.controls { + z-index: 2000; + padding-top: 15px; + padding-bottom: 15px; + margin-right: 10px; +} + +.app-menu .nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + +.app-menu .nav-item a { + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; +} + +.app-menu .nav-item a.active { + background-color: rgba(255,255,255,0.25); + color: white; +} + +.app-menu .nav-item a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.app-menu .nav-link .oi { + width: 1.5rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); + margin-left: auto; +} + +div.app-moduleactions a.dropdown-toggle, div.app-moduleactions div.dropdown-menu { + color:#ffffff; +} + +@media (max-width: 767px) { + + .app-menu { + width: 100% + } + + .navbar { + position: fixed; + top: 60px; + width: 100%; + } + + .controls { + height: 60px; + top: 15px; + position: fixed; + top: 0px; + width: 100%; + background-color: rgb(0, 0, 0); + } + + .controls-group { + float: right; + margin-right: 25px; + } + + .content { + position: relative; + top: 60px; + } +} diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index 8769c120..b9d7d47f 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -175,9 +175,9 @@ namespace Oqtane.Controllers // GET: api//templates [HttpGet("templates")] [Authorize(Roles = RoleNames.Host)] - public List Get() + public List GetTemplates() { - var templates = new List(); + var templates = new List(); string templatePath = Utilities.PathCombine(_environment.WebRootPath, "Modules", "Templates", Path.DirectorySeparatorChar.ToString()); foreach (string directory in Directory.GetDirectories(templatePath)) { @@ -186,7 +186,7 @@ namespace Oqtane.Controllers return templates; } - // POST api/?moduleid=x + // POST api/ [HttpPost] [Authorize(Roles = RoleNames.Host)] public ModuleDefinition Post([FromBody] ModuleDefinition moduleDefinition) diff --git a/Oqtane.Server/Controllers/ThemeController.cs b/Oqtane.Server/Controllers/ThemeController.cs index 851aeb68..6b2f7892 100644 --- a/Oqtane.Server/Controllers/ThemeController.cs +++ b/Oqtane.Server/Controllers/ThemeController.cs @@ -103,5 +103,91 @@ namespace Oqtane.Controllers } } + // GET: api//templates + [HttpGet("templates")] + [Authorize(Roles = RoleNames.Host)] + public List GetTemplates() + { + var templates = new List(); + string templatePath = Utilities.PathCombine(_environment.WebRootPath, "Themes", "Templates", Path.DirectorySeparatorChar.ToString()); + foreach (string directory in Directory.GetDirectories(templatePath)) + { + templates.Add(directory.Replace(templatePath, "")); + } + return templates; + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Host)] + public Theme Post([FromBody] Theme theme) + { + if (ModelState.IsValid) + { + string rootPath; + DirectoryInfo rootFolder = Directory.GetParent(_environment.ContentRootPath); + string templatePath = Utilities.PathCombine(_environment.WebRootPath, "Themes", "Templates", theme.Template, Path.DirectorySeparatorChar.ToString()); + + rootPath = Utilities.PathCombine(rootFolder.Parent.FullName, theme.Owner + "." + theme.Name, Path.DirectorySeparatorChar.ToString()); + theme.ThemeName = theme.Owner + "." + theme.Name + ", " + theme.Owner + "." + theme.Name + ".Client.Oqtane"; + + ProcessTemplatesRecursively(new DirectoryInfo(templatePath), rootPath, rootFolder.Name, templatePath, theme); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Theme Created {Theme}", theme); + } + + return theme; + } + + private void ProcessTemplatesRecursively(DirectoryInfo current, string rootPath, string rootFolder, string templatePath, Theme theme) + { + // process folder + string folderPath = Utilities.PathCombine(rootPath, current.FullName.Replace(templatePath, "")); + folderPath = folderPath.Replace("[Owner]", theme.Owner); + folderPath = folderPath.Replace("[Theme]", theme.Name); + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + FileInfo[] files = current.GetFiles("*.*"); + if (files != null) + { + foreach (FileInfo file in files) + { + // process file + string filePath = Path.Combine(folderPath, file.Name); + filePath = filePath.Replace("[Owner]", theme.Owner); + filePath = filePath.Replace("[Theme]", theme.Name); + + string text = System.IO.File.ReadAllText(file.FullName); + text = text.Replace("[Owner]", theme.Owner); + text = text.Replace("[Theme]", theme.Name); + text = text.Replace("[RootPath]", rootPath); + text = text.Replace("[RootFolder]", rootFolder); + text = text.Replace("[Folder]", folderPath); + text = text.Replace("[File]", Path.GetFileName(filePath)); + if (theme.Version == "local") + { + text = text.Replace("[FrameworkVersion]", Constants.Version); + text = text.Replace("[ClientReference]", "..\\..\\oqtane.framework\\Oqtane.Server\\bin\\Debug\\net5.0\\Oqtane.Client.dll"); + text = text.Replace("[SharedReference]", "..\\..\\oqtane.framework\\Oqtane.Server\\bin\\Debug\\net5.0\\Oqtane.Shared.dll"); + } + else + { + text = text.Replace("[FrameworkVersion]", theme.Version); + text = text.Replace("[ClientReference]", ""); + text = text.Replace("[SharedReference]", ""); + } + System.IO.File.WriteAllText(filePath, text); + } + + DirectoryInfo[] folders = current.GetDirectories(); + + foreach (DirectoryInfo folder in folders.Reverse()) + { + ProcessTemplatesRecursively(folder, rootPath, rootFolder, templatePath, theme); + } + } + } } } diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/AssemblyInfo.cs b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/AssemblyInfo.cs new file mode 100644 index 00000000..91d5ec4b --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Resources; +using Microsoft.Extensions.Localization; + +[assembly: RootNamespace("[Owner].[Theme].Client")] \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/Container1.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/Container1.razor new file mode 100644 index 00000000..2778aa22 --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/Container1.razor @@ -0,0 +1,20 @@ +@namespace [Owner].[Theme] +@inherits ContainerBase + +
+
+
+

+
+
+
+
+
+ +
+
+
+ +@code { + public override string Name => "Container1"; +} \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs new file mode 100644 index 00000000..3f543b38 --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs @@ -0,0 +1,15 @@ +using Oqtane.Models; +using Oqtane.Themes; + +namespace [Owner].[Theme] +{ + public class ThemeInfo : ITheme + { + public Theme Theme => new Theme + { + Name = "[Theme]", + Version = "1.0.0" + }; + + } +} diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor new file mode 100644 index 00000000..7ffefe5f --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor @@ -0,0 +1,107 @@ +@namespace [Owner].[Theme] +@inherits ThemeBase + +
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+ +
+ +@code { + public override string Name => "Theme1"; + + public override string Panes => "Content,Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width"; + + public override List Resources => new List() + { + new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css", Integrity = "sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk", CrossOrigin = "anonymous" }, + new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }, + new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://code.jquery.com/jquery-3.5.1.slim.min.js", Integrity = "sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj", CrossOrigin = "anonymous" }, + new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js", Integrity = "sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo", CrossOrigin = "anonymous" }, + new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js", Integrity = "sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI", CrossOrigin = "anonymous" } + }; +} diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].[Theme].Client.csproj new file mode 100644 index 00000000..746694b4 --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].[Theme].Client.csproj @@ -0,0 +1,34 @@ + + + + net5.0 + 3.0 + 1.0.0 + [Owner] + [Owner] + [Description] + [Owner].[Theme] + [Owner] + [Owner].[Theme].Client.Oqtane + + + + + + + + + + + + [ClientReference] + [SharedReference] + + + + + false + false + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/_Imports.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/_Imports.razor new file mode 100644 index 00000000..d206b36e --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/_Imports.razor @@ -0,0 +1,21 @@ +@using System +@using System.Linq +@using System.Collections.Generic +@using System.Net.Http +@using System.Net.Http.Json + +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop + +@using Oqtane.Models +@using Oqtane.Modules +@using Oqtane.Modules.Controls +@using Oqtane.Providers +@using Oqtane.Security +@using Oqtane.Services +@using Oqtane.Shared +@using Oqtane.Themes +@using Oqtane.Themes.Controls +@using Oqtane.UI +@using Oqtane.Enums \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].[Theme]/Theme.css b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].[Theme]/Theme.css new file mode 100644 index 00000000..b8056c79 --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].[Theme]/Theme.css @@ -0,0 +1,83 @@ +/* Oqtane Styles */ + +body { + padding-top: 7rem; +} + +.controls { + z-index: 2000; + padding-top: 15px; + padding-bottom: 15px; + margin-right: 10px; +} + +.app-menu .nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + +.app-menu .nav-item a { + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; +} + +.app-menu .nav-item a.active { + background-color: rgba(255,255,255,0.25); + color: white; +} + +.app-menu .nav-item a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.app-menu .nav-link .oi { + width: 1.5rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); + margin-left: auto; +} + +div.app-moduleactions a.dropdown-toggle, div.app-moduleactions div.dropdown-menu { + color:#ffffff; +} + +@media (max-width: 767px) { + + .app-menu { + width: 100% + } + + .navbar { + position: fixed; + top: 60px; + width: 100%; + } + + .controls { + height: 60px; + top: 15px; + position: fixed; + top: 0px; + width: 100%; + background-color: rgb(0, 0, 0); + } + + .controls-group { + float: right; + margin-right: 25px; + } + + .content { + position: relative; + top: 60px; + } +} diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].[Theme].Package.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].[Theme].Package.csproj new file mode 100644 index 00000000..30043010 --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].[Theme].Package.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + false + + + + + + + + + + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].[Theme].nuspec b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].[Theme].nuspec new file mode 100644 index 00000000..e73d4618 --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].[Theme].nuspec @@ -0,0 +1,27 @@ + + + + [Owner].[Theme] + 1.0.0 + [Owner] + [Owner] + [Theme] + [Theme] + [Owner] + false + MIT + https://github.com/oqtane/oqtane.framework + https://www.oqtane.org/Portals/0/icon.jpg + oqtane module + + + + + + + + + + + + \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.cmd b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.cmd new file mode 100644 index 00000000..7eea5a36 --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.cmd @@ -0,0 +1,3 @@ +XCOPY "..\Client\bin\Debug\net5.0\[Owner].[Theme].Client.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net5.0\" /Y +XCOPY "..\Client\bin\Debug\net5.0\[Owner].[Theme].Client.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net5.0\" /Y +XCOPY "..\Client\wwwroot\*" "..\..\[RootFolder]\Oqtane.Client\wwwroot\" /Y /S /I diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd new file mode 100644 index 00000000..3d4edfe1 --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd @@ -0,0 +1,2 @@ +"..\..\[RootFolder]\oqtane.package\nuget.exe" pack [Owner].[Theme].nuspec +XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\wwwroot\Themes\" /Y diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].[Theme].sln b/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].[Theme].sln new file mode 100644 index 00000000..7c1739bd --- /dev/null +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].[Theme].sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28621.142 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Owner].[Theme].Client", "Client\[Owner].[Theme].Client.csproj", "{AA8E58A1-CD09-4208-BF66-A8BB341FD669}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "[Owner].[Theme].Package", "Package\[Owner].[Theme].Package.csproj", "{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + Wasm|Any CPU = Wasm|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.Build.0 = Release|Any CPU + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Wasm|Any CPU.ActiveCfg = Release|Any CPU + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Wasm|Any CPU.Build.0 = Release|Any CPU + {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Release|Any CPU.Build.0 = Release|Any CPU + {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Wasm|Any CPU.ActiveCfg = Debug|Any CPU + {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Wasm|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1D016F15-46FE-4726-8DFD-2E4FD4DC7668} + EndGlobalSection +EndGlobal diff --git a/Oqtane.Shared/Models/Theme.cs b/Oqtane.Shared/Models/Theme.cs index ee420dd9..398cc537 100644 --- a/Oqtane.Shared/Models/Theme.cs +++ b/Oqtane.Shared/Models/Theme.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Oqtane.Models @@ -14,6 +14,7 @@ namespace Oqtane.Models Contact = ""; License = ""; Dependencies = ""; + Template = ""; } public string ThemeName { get; set; } @@ -28,6 +29,7 @@ namespace Oqtane.Models public List Themes { get; set; } public List Layouts { get; set; } public List Containers { get; set; } + public string Template { get; set; } //[Obsolete("This property is obsolete. Use Themes instead.", false)] public string ThemeControls { get; set; }