Merge pull request #12 from oqtane/master

Sync master
This commit is contained in:
Jim Spillane 2020-05-20 13:04:32 -04:00 committed by GitHub
commit 24579dc4d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 11467 additions and 215 deletions

View File

@ -105,11 +105,11 @@
string[] path = systeminfo["serverpath"].Split('\\');
if (_template == "internal")
{
_location = string.Join("\\", path, 0, path.Length - 1) + "\\Oqtane.Client\\Modules\\" + _owner + "." + _module;
_location = string.Join("\\", path, 0, path.Length - 1) + "\\Oqtane.Client\\Modules\\" + _owner + "." + _module + "s";
}
else
{
_location = string.Join("\\", path, 0, path.Length - 2) + "\\" + _owner + "." + _module;
_location = string.Join("\\", path, 0, path.Length - 2) + "\\" + _owner + "." + _module + "s";
}
}
}

View File

@ -28,8 +28,14 @@
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Actions => "Add,Edit";
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
};
int _id;
string _name;
string _createdby;

View File

@ -62,7 +62,8 @@ else
- Repository\[Module]Respository.cs - implements repository interface methods for data access using EF Core<br />
- Repository\[Module]Context.cs - provides a DB Context for data access<br />
- Scripts\[Owner].[Module]s.1.0.0.sql - database schema definition script<br />
- Scripts\[Owner].[Module]s.Uninstall.sql - database uninstall script<br /><br />
- Scripts\[Owner].[Module]s.Uninstall.sql - database uninstall script<br />
- wwwroot\Module.css - module style sheet<br /><br />
[RootPath]Shared\<br />
- [Owner].[Module]s.csproj - shared project<br />
- Models\[Module].cs - model definition<br /><br />
@ -70,6 +71,11 @@ else
<!-- The content above is for informational purposes only and can be safely removed -->
@code {
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
};
List<[Module]> _[Module]s;
protected override async Task OnInitializedAsync()

View File

@ -13,9 +13,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0-rc1.20223.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0-rc1.20223.4" PrivateAssets="all" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0-rc1.20217.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -26,7 +26,7 @@
<file src="..\Server\bin\Release\netcoreapp3.1\[Owner].[Module]s.Server.Oqtane.pdb" target="lib" />
<file src="..\Shared\bin\Release\netstandard2.1\[Owner].[Module]s.Shared.Oqtane.dll" target="lib" />
<file src="..\Shared\bin\Release\netstandard2.1\[Owner].[Module]s.Shared.Oqtane.pdb" target="lib" />
<file src="..\wwwroot\**\*.*" target="wwwroot" />
<file src="..\content\**\*.*" target="content" />
<file src="..\Server\wwwroot\**\*.*" target="wwwroot" />
<file src="..\Server\content\**\*.*" target="content" />
</files>
</package>

View File

@ -19,11 +19,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="3.2.0-rc1.20223.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.4" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1 @@
/* Module Custom Styles */

View File

@ -0,0 +1 @@
This is the location where static resources such as images, style sheets, or scripts for this module will be located. Static assets can be organized in subfolders. When the module package is deployed the assets will be extracted under the web root in a folder that matches the module namespace.

View File

@ -1 +0,0 @@
This is the location where static resources such as images or style sheets for this module will be located. Static assets can be organized in subfolders. When the module package is deployed the assets will be extracted under the web root in a folder that matches the module name.

View File

@ -10,25 +10,13 @@ using [Owner].[Module]s.Repository;
namespace [Owner].[Module]s.Manager
{
public class [Module]Manager : IInstallable, IPortable
public class [Module]Manager : IPortable
{
private I[Module]Repository _[Module]s;
private ISqlRepository _sql;
public [Module]Manager(I[Module]Repository [Module]s, ISqlRepository sql)
public [Module]Manager(I[Module]Repository [Module]s)
{
_[Module]s = [Module]s;
_sql = sql;
}
public bool Install(Tenant tenant, string version)
{
return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module]s." + version + ".sql");
}
public bool Uninstall(Tenant tenant)
{
return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module]s.Uninstall.sql");
}
public string ExportModule(Module module)

View File

@ -123,8 +123,8 @@
_settingsModuleType = Type.GetType(ModuleState.ModuleType);
if (_settingsModuleType != null)
{
var moduleobject = Activator.CreateInstance(_settingsModuleType);
_settingstitle = (string)_settingsModuleType.GetProperty("Title").GetValue(moduleobject, null);
var moduleobject = Activator.CreateInstance(_settingsModuleType) as IModuleControl;
_settingstitle = moduleobject.Title;
if (string.IsNullOrEmpty(_settingstitle))
{
_settingstitle = "Other Settings";

View File

@ -26,7 +26,7 @@
<Label HelpText="Upload a framework package and select Install to complete the installation">Framework: </Label>
</td>
<td>
<FileManager Filter="nupkg" ShowFiles="false" Folder="Framework" />
<FileManager Filter="nupkg" Folder="Framework" />
</td>
</tr>
</table>

View File

@ -62,9 +62,9 @@
[Parameter]
public string Class { get; set; } // optional
[Parameter]
public bool Disabled { get; set; } // optional
[Parameter]
public bool Disabled { get; set; } // optional
[Parameter]
public string EditMode { get; set; } // optional - specifies if a user must be in edit mode to see the action - default is true
@ -109,8 +109,8 @@
Type moduleType = Type.GetType(typename);
if (moduleType != null)
{
var moduleobject = Activator.CreateInstance(moduleType);
security = (SecurityAccessLevel)moduleType.GetProperty("SecurityAccessLevel").GetValue(moduleobject, null);
var moduleobject = Activator.CreateInstance(moduleType) as IModuleControl;
security = moduleobject.SecurityAccessLevel;
}
else
{

View File

@ -101,8 +101,8 @@
var moduleType = Type.GetType(typename);
if (moduleType != null)
{
var moduleobject = Activator.CreateInstance(moduleType);
security = (SecurityAccessLevel)moduleType.GetProperty("SecurityAccessLevel").GetValue(moduleobject, null);
var moduleobject = Activator.CreateInstance(moduleType) as IModuleControl;
security = moduleobject.SecurityAccessLevel;
}
else
{

View File

@ -6,7 +6,7 @@
<LangVersion>7.3</LangVersion>
<RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations>
<Version>0.9.1</Version>
<Version>1.0.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -27,10 +27,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0-rc1.20223.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0-rc1.20223.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="3.1.2" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0-rc1.20217.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="3.1.4" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -12,6 +12,8 @@ using Oqtane.Modules;
using Oqtane.Shared;
using Oqtane.Providers;
using Microsoft.AspNetCore.Components.Authorization;
using System.IO.Compression;
using System.IO;
namespace Oqtane.Client
{
@ -90,15 +92,50 @@ namespace Oqtane.Client
private static async Task LoadClientAssemblies(HttpClient http)
{
var list = await http.GetFromJsonAsync<List<string>>($"/~/api/ModuleDefinition/load");
// get list of loaded assemblies on the client ( in the client-side hosting module the browser client has its own app domain )
var assemblyList = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList();
foreach (var name in list)
// get list of loaded assemblies on the client
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList();
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/~/api/Installation/load");
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
{
if (assemblyList.Contains(name)) continue;
// download assembly from server and load
var bytes = await http.GetByteArrayAsync($"/~/api/ModuleDefinition/load/{name}.dll");
Assembly.Load(bytes);
Dictionary<string, byte[]> dlls = new Dictionary<string, byte[]>();
Dictionary<string, byte[]> pdbs = new Dictionary<string, byte[]>();
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (!assemblies.Contains(Path.GetFileNameWithoutExtension(entry.Name)))
{
using (var memoryStream = new MemoryStream())
{
entry.Open().CopyTo(memoryStream);
byte[] file = memoryStream.ToArray();
switch (Path.GetExtension(entry.Name))
{
case ".dll":
dlls.Add(entry.Name, file);
break;
case ".pdb":
pdbs.Add(entry.Name, file);
break;
}
}
}
}
foreach (var item in dlls)
{
if (pdbs.ContainsKey(item.Key))
{
Assembly.Load(item.Value, pdbs[item.Key]);
}
else
{
Assembly.Load(item.Value);
}
}
}
}
}

View File

@ -51,7 +51,7 @@ namespace Oqtane.Services
public async Task CreateModuleDefinitionAsync(ModuleDefinition moduleDefinition, int moduleId)
{
await PostJsonAsync($"{Apiurl}?moduleid={moduleId.ToString()}", moduleDefinition);
await PostJsonAsync($"{Apiurl}?moduleid={moduleId}", moduleDefinition);
}
}
}

View File

@ -21,7 +21,9 @@
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/cyborg/bootstrap.min.css", Integrity = "sha384-l7xaoY0cJM4h9xh1RfazbgJVUZvdtyLWPueWNtLAphf/UbBgOVzqbOTogxPwYLHM", CrossOrigin = "anonymous" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "BootswatchCyborg.css" },
// remote stylesheets can be linked using the format below, however we want the default theme to display properly in local development scenarios where an Internet connection is not available
//new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/cyborg/bootstrap.min.css", Integrity = "sha384-l7xaoY0cJM4h9xh1RfazbgJVUZvdtyLWPueWNtLAphf/UbBgOVzqbOTogxPwYLHM", CrossOrigin = "anonymous" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }
};

View File

@ -8,6 +8,7 @@
<div class="row">
<div class="mx-auto text-center">
<img src="oqtane-black.png" />
<div style="font-weight: bold">Version: @Constants.Version</div>
</div>
</div>
<hr class="app-rule" />

View File

@ -179,7 +179,7 @@
// extract admin route elements from path
var segments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
int result;
// check if path has moduleid and control specification ie. page/moduleid/control/
// check if path has moduleid and action specification ie. pagename/moduleid/action/
if (segments.Length >= 2 && int.TryParse(segments[segments.Length - 2], out result))
{
action = segments[segments.Length - 1];
@ -188,7 +188,7 @@
}
else
{
// check if path has only moduleid specification ie. page/moduleid/
// check if path has moduleid specification ie. pagename/moduleid/
if (segments.Length >= 1 && int.TryParse(segments[segments.Length - 1], out result))
{
moduleid = result;
@ -361,21 +361,20 @@
string panes = "";
Type themetype = Type.GetType(page.ThemeType);
var themeobject = Activator.CreateInstance(themetype);
var themeobject = Activator.CreateInstance(themetype) as IThemeControl;
if (themeobject != null)
{
panes = (string)themetype.GetProperty("Panes").GetValue(themeobject, null);
var resources = (List<Resource>)themetype.GetProperty("Resources").GetValue(themeobject, null);
page.Resources = ManagePageResources(page.Resources, resources);
panes = themeobject.Panes;
page.Resources = ManagePageResources(page.Resources, themeobject.Resources);
}
if (!string.IsNullOrEmpty(page.LayoutType))
{
themetype = Type.GetType(page.LayoutType);
themeobject = Activator.CreateInstance(themetype);
if (themeobject != null)
Type layouttype = Type.GetType(page.LayoutType);
var layoutobject = Activator.CreateInstance(layouttype) as ILayoutControl;
if (layoutobject != null)
{
panes = (string)themetype.GetProperty("Panes").GetValue(themeobject, null);
panes = layoutobject.Panes;
}
}
@ -389,7 +388,7 @@
return page;
}
private (Page Page, List<Module> Modules) ProcessModules(Page page, List<Module> modules, int moduleid, string control, string defaultcontainertype)
private (Page Page, List<Module> Modules) ProcessModules(Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype)
{
var paneindex = new Dictionary<string, int>();
foreach (Module module in modules)
@ -406,20 +405,20 @@
typename = Constants.ErrorModule;
}
if (module.ModuleId == moduleid && control != "")
if (module.ModuleId == moduleid && action != "")
{
// check if the module defines custom routes
if (module.ModuleDefinition.ControlTypeRoutes != "")
{
foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
if (route.StartsWith(control + "="))
if (route.StartsWith(action + "="))
{
typename = route.Replace(control + "=", "");
typename = route.Replace(action + "=", "");
}
}
}
module.ModuleType = typename.Replace(Constants.ActionToken, control);
module.ModuleType = typename.Replace(Constants.ActionToken, action);
}
else
{
@ -428,25 +427,30 @@
// get additional metadata from IModuleControl interface
typename = module.ModuleType;
if (Constants.DefaultModuleActions.Contains(control))
if (Constants.DefaultModuleActions.Contains(action))
{
// core framework module action components
typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, control);
typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action);
}
Type moduletype = Type.GetType(typename);
if (moduletype != null)
// ensure component implements IModuleControl
if (moduletype != null && !moduletype.GetInterfaces().Contains(typeof(IModuleControl)))
{
var moduleobject = Activator.CreateInstance(moduletype);
var resources = (List<Resource>)moduletype.GetProperty("Resources").GetValue(moduleobject, null);
page.Resources = ManagePageResources(page.Resources, resources);
module.ModuleType = "";
}
if (moduletype != null && module.ModuleType != "")
{
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources);
// additional metadata needed for admin components
if (module.ModuleId == moduleid && control != "")
if (module.ModuleId == moduleid && action != "")
{
module.SecurityAccessLevel = (SecurityAccessLevel)moduletype.GetProperty("SecurityAccessLevel").GetValue(moduleobject, null);
module.ControlTitle = (string)moduletype.GetProperty("Title").GetValue(moduleobject);
module.Actions = (string)moduletype.GetProperty("Actions").GetValue(moduleobject);
module.UseAdminContainer = (bool)moduletype.GetProperty("UseAdminContainer").GetValue(moduleobject);
module.SecurityAccessLevel = moduleobject.SecurityAccessLevel;
module.ControlTitle = moduleobject.Title;
module.Actions = moduleobject.Actions;
module.UseAdminContainer = moduleobject.UseAdminContainer;
}
}

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Framework</id>
<version>0.9.1</version>
<version>1.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -18,8 +18,11 @@
</metadata>
<files>
<file src="..\Oqtane.Client\bin\Release\netstandard2.1\Oqtane.Client.dll" target="lib" />
<file src="..\Oqtane.Client\bin\Release\netstandard2.1\Oqtane.Client.pdb" target="lib" />
<file src="..\Oqtane.Server\bin\Release\netcoreapp3.1\Oqtane.Server.dll" target="lib" />
<file src="..\Oqtane.Server\bin\Release\netcoreapp3.1\Oqtane.Server.pdb" target="lib" />
<file src="..\Oqtane.Shared\bin\Release\netstandard2.1\Oqtane.Shared.dll" target="lib" />
<file src="..\Oqtane.Server\bin\Release\netcoreapp3.1\Oqtane.Upgrade.dll" target="lib" />
<file src="..\Oqtane.Shared\bin\Release\netstandard2.1\Oqtane.Shared.pdb" target="lib" />
<file src="..\Oqtane.Server\wwwroot\**\*.*" target="wwwroot" />
</files>
</package>

View File

@ -65,7 +65,7 @@ namespace Oqtane.Controllers
{
foreach (string file in Directory.GetFiles(folder))
{
files.Add(new Models.File {Name = Path.GetFileName(file), Extension = Path.GetExtension(file)?.Replace(".", "")});
files.Add(new Models.File { Name = Path.GetFileName(file), Extension = Path.GetExtension(file)?.Replace(".", "") });
}
}
}
@ -256,7 +256,7 @@ namespace Oqtane.Controllers
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
return;
}
string folderPath = "";
if (int.TryParse(folder, out int folderId))
@ -306,7 +306,8 @@ namespace Oqtane.Controllers
string token = ".part_";
string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "x_y"
int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1));
filename = filename?.Substring(0, filename.IndexOf(token)); // base filename
filename = Path.GetFileNameWithoutExtension(filename); // base filename
string[] fileParts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts
// if all of the file parts exist ( note that file parts can arrive out of order )
@ -363,13 +364,15 @@ namespace Oqtane.Controllers
}
// clean up file parts which are more than 2 hours old ( which can happen if a prior file upload failed )
fileParts = Directory.GetFiles(folder, "*" + token + "*");
foreach (string filepart in fileParts)
var cleanupFiles = Directory.EnumerateFiles(folder, "*" + token + "*")
.Where(f => Path.GetExtension(f).StartsWith(token));
foreach (var file in cleanupFiles)
{
DateTime createddate = System.IO.File.GetCreationTime(filepart).ToUniversalTime();
if (createddate < DateTime.UtcNow.AddHours(-2))
var createdDate = System.IO.File.GetCreationTime(file).ToUniversalTime();
if (createdDate < DateTime.UtcNow.AddHours(-2))
{
System.IO.File.Delete(filepart);
System.IO.File.Delete(file);
}
}
@ -480,7 +483,7 @@ namespace Oqtane.Controllers
string[] folders = folderpath.Split(separators, StringSplitOptions.RemoveEmptyEntries);
foreach (string folder in folders)
{
path = Utilities.PathCombine(path, folder,"\\");
path = Utilities.PathCombine(path, folder, "\\");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
@ -497,7 +500,7 @@ namespace Oqtane.Controllers
FileInfo fileinfo = new FileInfo(filepath);
file.Extension = fileinfo.Extension.ToLower().Replace(".", "");
file.Size = (int) fileinfo.Length;
file.Size = (int)fileinfo.Length;
file.ImageHeight = 0;
file.ImageWidth = 0;

View File

@ -4,6 +4,11 @@ using Microsoft.Extensions.Configuration;
using Oqtane.Models;
using Oqtane.Shared;
using Oqtane.Infrastructure;
using System;
using System.IO;
using System.Reflection;
using System.Linq;
using System.IO.Compression;
namespace Oqtane.Controllers
{
@ -55,5 +60,56 @@ namespace Oqtane.Controllers
_installationManager.UpgradeFramework();
return installation;
}
// GET api/<controller>/load
[HttpGet("load")]
public IActionResult Load()
{
if (_config.GetSection("Runtime").Value == "WebAssembly")
{
// get list of assemblies which should be downloaded to browser
var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies();
var list = assemblies.Select(a => a.GetName().Name).ToList();
var deps = assemblies.SelectMany(a => a.GetReferencedAssemblies()).Distinct();
list.AddRange(deps.Where(a => a.Name.EndsWith(".oqtane", StringComparison.OrdinalIgnoreCase)).Select(a => a.Name));
// create zip file containing assemblies and debug symbols
string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
byte[] zipfile;
using (var memoryStream = new MemoryStream())
{
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
ZipArchiveEntry entry;
foreach (string file in list)
{
entry = archive.CreateEntry(file + ".dll");
using (var filestream = new FileStream(Path.Combine(binfolder, file + ".dll"), FileMode.Open, FileAccess.Read))
using (var entrystream = entry.Open())
{
filestream.CopyTo(entrystream);
}
if (System.IO.File.Exists(Path.Combine(binfolder, file + ".pdb")))
{
entry = archive.CreateEntry(file + ".pdb");
using (var filestream = new FileStream(Path.Combine(binfolder, file + ".pdb"), FileMode.Open, FileAccess.Read))
using (var entrystream = entry.Open())
{
filestream.CopyTo(entrystream);
}
}
}
}
zipfile = memoryStream.ToArray();
}
return File(zipfile, "application/octet-stream", "oqtane.zip");
}
else
{
HttpContext.Response.StatusCode = 401;
return null;
}
}
}
}

View File

@ -15,8 +15,6 @@ using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using System.Xml.Linq;
using Microsoft.AspNetCore.Mvc.Formatters;
// ReSharper disable StringIndexOfIsCultureSpecific.1
namespace Oqtane.Controllers
{
@ -163,39 +161,6 @@ namespace Oqtane.Controllers
}
}
// GET api/<controller>/load
[HttpGet("load")]
public List<string> Load()
{
List<string> list = new List<string>();
if (_config.GetSection("Runtime").Value == "WebAssembly")
{
var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies();
list = AppDomain.CurrentDomain.GetOqtaneClientAssemblies().Select(a => a.GetName().Name).ToList();
var deps = assemblies.SelectMany(a => a.GetReferencedAssemblies()).Distinct();
list.AddRange(deps.Where(a => a.Name.EndsWith(".oqtane", StringComparison.OrdinalIgnoreCase)).Select(a => a.Name));
}
return list;
}
// GET api/<controller>/load/assembyname
[HttpGet("load/{assemblyname}")]
public IActionResult Load(string assemblyname)
{
if (_config.GetSection("Runtime").Value == "WebAssembly" && Path.GetExtension(assemblyname).ToLower() == ".dll")
{
string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
byte[] file = System.IO.File.ReadAllBytes(Path.Combine(binfolder, assemblyname));
return File(file, "application/octet-stream", assemblyname);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Download Assembly {Assembly}", assemblyname);
HttpContext.Response.StatusCode = 401;
return null;
}
}
// POST api/<controller>?moduleid=x
[HttpPost]
[Authorize(Roles = Constants.HostRole)]
@ -231,8 +196,8 @@ namespace Oqtane.Controllers
{
// add embedded resources to project
List<string> resources = new List<string>();
resources.Add(Utilities.PathCombine("Modules", moduleDefinition.Owner + "." + moduleDefinition.Name, "Scripts", moduleDefinition.Owner + "." + moduleDefinition.Name + "s.1.0.0.sql"));
resources.Add(Utilities.PathCombine("Modules", moduleDefinition.Owner + "." + moduleDefinition.Name, "Scripts", moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Uninstall.sql"));
resources.Add(Utilities.PathCombine("Modules", moduleDefinition.Owner + "." + moduleDefinition.Name + "s", "Scripts", moduleDefinition.Owner + "." + moduleDefinition.Name + "s.1.0.0.sql"));
resources.Add(Utilities.PathCombine("Modules", moduleDefinition.Owner + "." + moduleDefinition.Name + "s", "Scripts", moduleDefinition.Owner + "." + moduleDefinition.Name + "s.Uninstall.sql"));
EmbedResourceFiles(Utilities.PathCombine(rootPath, "Oqtane.Server", "Oqtane.Server.csproj"), resources);
}

View File

@ -52,10 +52,6 @@ namespace Oqtane.Infrastructure
// iterate through Nuget packages in source folder
foreach (string packagename in Directory.GetFiles(sourceFolder, "*.nupkg"))
{
string name = Path.GetFileNameWithoutExtension(packagename);
string[] segments = name?.Split('.');
if (segments != null) name = string.Join('.', segments, 0, segments.Length - 3);
// iterate through files
using (ZipArchive archive = ZipFile.OpenRead(packagename))
{
@ -84,6 +80,11 @@ namespace Oqtane.Infrastructure
// if compatible with framework version
if (frameworkversion == "" || Version.Parse(Constants.Version).CompareTo(Version.Parse(frameworkversion)) >= 0)
{
// module and theme packages must be in form of name.1.0.0.nupkg
string name = Path.GetFileNameWithoutExtension(packagename);
string[] segments = name?.Split('.');
if (segments != null) name = string.Join('.', segments, 0, segments.Length - 3);
// deploy to appropriate locations
foreach (ZipArchiveEntry entry in archive.Entries)
{
@ -93,25 +94,18 @@ namespace Oqtane.Infrastructure
switch (foldername)
{
case "lib":
if (binFolder != null) entry.ExtractToFile(Path.Combine(binFolder, filename), true);
filename = Path.Combine(binFolder, filename);
ExtractFile(entry, filename);
break;
case "wwwroot":
filename = Path.Combine(sourceFolder, Utilities.PathCombine(entry.FullName.Replace("wwwroot", name).Split('/')));
if (!Directory.Exists(Path.GetDirectoryName(filename)))
{
Directory.CreateDirectory(Path.GetDirectoryName(filename));
}
entry.ExtractToFile(filename, true);
ExtractFile(entry, filename);
break;
case "content":
if (Path.GetDirectoryName(entry.FullName) != "content") // assets must be in subfolders
{
filename = Path.Combine(webRootPath, Utilities.PathCombine(entry.FullName.Replace("content", "").Split('/')));
if (!Directory.Exists(Path.GetDirectoryName(filename)))
{
Directory.CreateDirectory(Path.GetDirectoryName(filename));
}
entry.ExtractToFile(filename, true);
ExtractFile(entry, filename);
}
break;
}
@ -128,6 +122,15 @@ namespace Oqtane.Infrastructure
return install;
}
private static void ExtractFile(ZipArchiveEntry entry, string filename)
{
if (!Directory.Exists(Path.GetDirectoryName(filename)))
{
Directory.CreateDirectory(Path.GetDirectoryName(filename));
}
entry.ExtractToFile(filename, true);
}
public void UpgradeFramework()
{
string folder = Path.Combine(_environment.WebRootPath, "Framework");

View File

@ -55,9 +55,9 @@ namespace Oqtane.SiteTemplates
new Permission(PermissionNames.View, Constants.AdminRole, true),
new Permission(PermissionNames.Edit, Constants.AdminRole, true)
}.EncodePermissions(),
Content = "<p><a href=\"https://www.oqtane.org\" target=\"_new\">Oqtane</a> is an open source <b>modular application framework</b> built from the ground up using modern .NET Core technology. It leverages the revolutionary new Blazor component model to create a <b>fully dynamic</b> web development experience which can be executed on a client or server. Whether you are looking for a platform to <b>accelerate your web development</b> efforts, or simply interested in exploring the anatomy of a large-scale Blazor application, Oqtane provides a solid foundation based on proven enterprise architectural principles.</p>" +
Content = "<p><a href=\"https://www.oqtane.org\" target=\"_new\">Oqtane</a> is an open source <b>modular application framework</b> that provides advanced functionality for developing web and mobile applications on ASP.NET Core. It leverages the revolutionary new Blazor component model to compose a <b>fully dynamic</b> web development experience which can be hosted either client-side or server-side. Whether you are looking for a platform to <b>accelerate your web development</b> efforts, or simply interested in exploring the anatomy of a large-scale Blazor application, Oqtane provides a solid foundation based on proven enterprise architectural principles.</p>" +
"<p align=\"center\"><a href=\"https://www.oqtane.org\" target=\"_new\"><img class=\"img-fluid\" src=\"oqtane-white.png\"></a></p><p align=\"center\"><a class=\"btn btn-primary\" href=\"https://www.oqtane.org/Community\" target=\"_new\">Join Our Community</a>&nbsp;&nbsp;<a class=\"btn btn-primary\" href=\"https://github.com/oqtane/oqtane.framework\" target=\"_new\">Clone Our Repo</a></p>" +
"<p><a href=\"https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor\" target=\"_new\">Blazor</a> is a single-page app framework that lets you build interactive web applications using C# instead of JavaScript. Client-side Blazor relies on WebAssembly, an open web standard that does not require plugins or code transpilation in order to run natively in a web browser. Server-side Blazor uses SignalR to host your application on a web server and provide a responsive and robust debugging experience. Blazor applications works in all modern web browsers, including mobile browsers.</p>" +
"<p><a href=\"https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor\" target=\"_new\">Blazor</a> is an open source and cross-platform web UI framework for building single-page apps using .NET and C# instead of JavaScript. Blazor WebAssembly relies on Wasm, an open web standard that does not require plugins or code transpilation in order to run natively in a web browser. Blazor Server uses SignalR to host your application on a web server and provide a responsive and robust development experience. Blazor applications work in all modern web browsers, including mobile browsers.</p>" +
"<p>Blazor is a feature of <a href=\"https://dotnet.microsoft.com/apps/aspnet\" target=\"_new\">ASP.NET Core 3</a>, the popular cross platform web development framework from Microsoft that extends the <a href=\"https://dotnet.microsoft.com/learn/dotnet/what-is-dotnet\" target=\"_new\" >.NET developer platform</a> with tools and libraries for building web apps.</p>"
},
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "MIT License", Pane = "Content",

View File

@ -4,7 +4,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>7.3</LangVersion>
<Configurations>Debug;Release</Configurations>
<Version>0.9.1</Version>
<Version>1.0.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -26,12 +26,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="dbup" Version="4.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="3.2.0-rc1.20223.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" />
<PackageReference Include="System.Drawing.Common" Version="4.7.0" />
</ItemGroup>
<ItemGroup>

View File

@ -213,8 +213,8 @@ namespace Oqtane.Repository
if (moduletype != null)
{
// get property values from IModule
var moduleobject = Activator.CreateInstance(moduletype);
moduledefinition = (ModuleDefinition) moduletype.GetProperty("ModuleDefinition").GetValue(moduleobject);
var moduleobject = Activator.CreateInstance(moduletype) as IModule;
moduledefinition = moduleobject.ModuleDefinition;
}
else
{
@ -261,8 +261,8 @@ namespace Oqtane.Repository
moduledefinition = moduledefinitions[index];
// actions
var modulecontrolobject = Activator.CreateInstance(modulecontroltype);
string actions = (string) modulecontroltype.GetProperty("Actions")?.GetValue(modulecontrolobject);
var modulecontrolobject = Activator.CreateInstance(modulecontroltype) as IModuleControl;
string actions = modulecontrolobject.Actions;
if (!string.IsNullOrEmpty(actions))
{
foreach (string action in actions.Split(','))

View File

@ -66,8 +66,8 @@ namespace Oqtane.Repository
.Where(item => item.GetInterfaces().Contains(typeof(ITheme))).FirstOrDefault();
if (themetype != null)
{
var themeobject = Activator.CreateInstance(themetype);
theme = (Theme)themetype.GetProperty("Theme").GetValue(themeobject);
var themeobject = Activator.CreateInstance(themetype) as ITheme;
theme = themeobject.Theme;
}
else
{

File diff suppressed because it is too large Load Diff

View File

@ -76,7 +76,8 @@ namespace System.Reflection
public static IEnumerable<Assembly> GetOqtaneClientAssemblies(this AppDomain appDomain)
{
return appDomain.GetOqtaneAssemblies()
.Where(a => a.GetTypes<IModuleControl>().Any() || a.GetTypes<IThemeControl>().Any() || a.GetTypes<IClientStartup>().Any());
.Where(a => a.GetTypes<IModuleControl>().Any() || a.GetTypes<IThemeControl>().Any() || a.GetTypes<IClientStartup>().Any())
.Where(a => Utilities.GetFullTypeName(a.GetName().Name) != "Oqtane.Client");
}
}
}

View File

@ -4,7 +4,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>7.3</LangVersion>
<Configurations>Debug;Release</Configurations>
<Version>0.9.1</Version>
<Version>1.0.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -18,9 +18,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.4" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageReference Include="System.Text.Json" Version="4.7.1" />
<PackageReference Include="System.Text.Json" Version="4.7.2" />
</ItemGroup>
</Project>

View File

@ -5,8 +5,8 @@ namespace Oqtane.Shared
public class Constants
{
public const string PackageId = "Oqtane.Framework";
public const string Version = "0.9.1";
public const string ReleaseVersions = "0.9.0,0.9.1";
public const string Version = "1.0.0";
public const string ReleaseVersions = "0.9.0,0.9.1,0.9.2,1.0.0";
public const string PageComponent = "Oqtane.UI.ThemeBuilder, Oqtane.Client";
public const string ContainerComponent = "Oqtane.UI.ContainerBuilder, Oqtane.Client";
@ -45,7 +45,7 @@ namespace Oqtane.Shared
public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png";
public const string UploadableFiles = "jpg,jpeg,jpe,gif,bmp,png,mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg";
public const string ReservedDevices = "CON,NUL,PRN,,COM0,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT0,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,CONIN$,CONOUT$";
public const string ReservedDevices = "CON,NUL,PRN,COM0,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT0,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,CONIN$,CONOUT$";
public static readonly char[] InvalidFileNameChars =
{
'\"', '<', '>', '|', '\0', (Char) 1, (Char) 2, (Char) 3, (Char) 4, (Char) 5, (Char) 6, (Char) 7, (Char) 8,

View File

@ -6,11 +6,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="3.1.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.0-preview-20200310-03" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="bunit" Version="1.0.0-beta-6" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit.core" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>

View File

@ -4,7 +4,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>7.3</LangVersion>
<OutputType>Exe</OutputType>
<Version>0.9.1</Version>
<Version>1.0.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Reflection;
@ -10,12 +11,18 @@ namespace Oqtane.Upgrade
{
static void Main(string[] args)
{
// for testing purposes set Oqtane.Upgrade as startup project and modify values below
//Array.Resize(ref args, 2);
//args[0] = @"C:\yourpath\oqtane.framework\Oqtane.Server";
//args[1] = @"C:\yourpath\oqtane.framework\Oqtane.Server\wwwroot";
// requires 2 arguments - the contentrootpath and the webrootpath of the site
if (args.Length == 2)
{
string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
string rootfolder = args[0];
string deployfolder = Path.Combine(args[1], "Framework");
string contentrootfolder = args[0];
string webrootfolder = args[1];
string deployfolder = Path.Combine(webrootfolder, "Framework");
if (Directory.Exists(deployfolder))
{
@ -29,9 +36,9 @@ namespace Oqtane.Upgrade
if (packagename != "")
{
// take the app offline
if (File.Exists(Path.Combine(rootfolder, "app_offline.bak")))
if (File.Exists(Path.Combine(webrootfolder, "app_offline.bak")))
{
File.Move(Path.Combine(rootfolder, "app_offline.bak"), Path.Combine(rootfolder, "app_offline.htm"));
File.Copy(Path.Combine(webrootfolder, "app_offline.bak"), Path.Combine(contentrootfolder, "app_offline.htm"), true);
}
// get list of files in package
@ -40,26 +47,29 @@ namespace Oqtane.Upgrade
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (Path.GetExtension(entry.FullName) == ".dll")
switch (Path.GetDirectoryName(entry.FullName).Split('\\')[0])
{
files.Add(Path.GetFileName(entry.FullName));
case "lib":
files.Add(Path.Combine(binfolder, Path.GetFileName(entry.FullName)));
break;
case "wwwroot":
files.Add(Path.Combine(webrootfolder, entry.FullName.Replace("wwwroot/", "").Replace("/","\\")));
break;
}
}
}
// ensure files are not locked
string filename;
if (CanAccessFiles(files, binfolder))
if (CanAccessFiles(files))
{
// create backup
foreach (string file in files)
{
filename = Path.Combine(binfolder, Path.GetFileName(file));
if (File.Exists(filename.Replace(".dll", ".bak")))
if (File.Exists(file + ".bak"))
{
File.Delete(filename.Replace(".dll", ".bak"));
File.Delete(file + ".bak");
}
File.Move(filename, filename.Replace(".dll", ".bak"));
File.Move(file, file + ".bak");
}
// extract files
@ -70,10 +80,19 @@ namespace Oqtane.Upgrade
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
filename = Path.GetFileName(entry.FullName);
string filename = "";
switch (Path.GetDirectoryName(entry.FullName).Split('\\')[0])
{
case "lib":
filename = Path.Combine(binfolder, Path.GetFileName(entry.FullName));
break;
case "wwwroot":
filename = Path.Combine(webrootfolder, entry.FullName.Replace("wwwroot/", "").Replace("/", "\\"));
break;
}
if (files.Contains(filename))
{
entry.ExtractToFile(Path.Combine(binfolder, filename), true);
entry.ExtractToFile(filename, true);
}
}
}
@ -90,42 +109,40 @@ namespace Oqtane.Upgrade
// clean up backup
foreach (string file in files)
{
filename = Path.Combine(binfolder, Path.GetFileName(file));
if (File.Exists(filename.Replace(".dll", ".bak")))
if (File.Exists(file + ".bak"))
{
File.Delete(filename.Replace(".dll", ".bak"));
File.Delete(file + ".bak");
}
}
// delete package
File.Delete(packagename);
}
else
{
// restore on failure
foreach (string file in files)
{
filename = Path.Combine(binfolder, Path.GetFileName(file));
if (File.Exists(filename))
if (File.Exists(file))
{
File.Delete(filename);
File.Delete(file);
}
File.Move(filename.Replace(".dll", ".bak"), filename);
File.Move(file + ".bak", file);
}
}
// delete package
File.Delete(packagename);
}
// bring the app back online
if (File.Exists(Path.Combine(rootfolder, "app_offline.htm")))
if (File.Exists(Path.Combine(contentrootfolder, "app_offline.htm")))
{
File.Move(Path.Combine(rootfolder, "app_offline.htm"), Path.Combine(rootfolder, "app_offline.bak"));
File.Delete(Path.Combine(contentrootfolder, "app_offline.htm"));
}
}
}
}
}
private static bool CanAccessFiles(List<string> files, string folder)
private static bool CanAccessFiles(List<string> files)
{
// ensure files are not locked by another process - the shutdownTimeLimit defines the duration for app shutdown
bool canAccess = true;
@ -133,7 +150,7 @@ namespace Oqtane.Upgrade
int i = 0;
while (i < (files.Count - 1) && canAccess)
{
string filepath = Path.Combine(folder, Path.GetFileName(files[i]));
string filepath = files[i];
int attempts = 0;
bool locked = true;
// try up to 30 times

View File

@ -3,28 +3,36 @@ Oqtane is a Modular Application Framework for Blazor
![Oqtane](https://github.com/oqtane/framework/blob/master/oqtane.png?raw=true "Oqtane")
Oqtane uses Blazor, a new web framework for .NET Core that lets you build interactive web UIs using C# instead of JavaScript. Blazor apps are composed of reusable web UI components implemented using C#, HTML, and CSS. Both client and server code is written in C#, allowing you to share code and libraries.
Oqtane uses Blazor, an open source and cross-platform web UI framework for building single-page apps using .NET and C# instead of JavaScript. Blazor apps are composed of reusable web UI components implemented using C#, HTML, and CSS. Both client and server code is written in C#, allowing you to share code and libraries.
Please note that this project is governed by the **[.NET Foundation Contributor Covenant Code of Conduct](https://dotnetfoundation.org/code-of-conduct)**
Please note that this project is owned by the .NET Foundation and is governed by the **[.NET Foundation Contributor Covenant Code of Conduct](https://dotnetfoundation.org/code-of-conduct)**
**To get started with Oqtane:**
1.&nbsp;Install **[.NET Core 3.2 Preview5 SDK (v3.1.201)](https://dotnet.microsoft.com/download/dotnet-core/3.1)**.
1.&nbsp;Install **[.NET Core 3.2 SDK (v3.1.300)](https://dotnet.microsoft.com/download/dotnet-core/thank-you/sdk-3.1.300-windows-x64-installer)**.
2.&nbsp;Install the Preview edition of [Visual Studio 2019](https://visualstudio.microsoft.com/vs/preview/) (version 16.6 or higher) with the **ASP.NET and web development** workload. If you do not have a SQL Server installation available already and you wish to use LocalDB for development, you must also install the **.NET desktop development workload**.
2.&nbsp;Install the Preview edition of [Visual Studio 2019](https://visualstudio.microsoft.com/vs/preview/) (version 16.6 or higher) with the **ASP.NET and web development** workload enabled. Oqtane works with all editions of Visual Studio from Community to Enterprise. If you do not have a SQL Server installation available already and you wish to use LocalDB for development, you must also install the **.NET desktop development workload**.
3.&nbsp;Download or Clone the Oqtane source code to your local system. Open the **Oqtane.sln** solution file and Build the solution.
NOTE: If you have already installed a previous version of Oqtane and you wish to install a newer version, there is currently no upgrade path from one version to the next. The recommended upgrade approach is to get the latest code and build it, and then reset the DefaultConnection value to "" in the appsettings.json file in the Oqtane.server project. This will trigger a re-install when you run the application which will execute the latest database scripts.
NOTE: If you have already installed a previous version of Oqtane and you wish to do a clean database install, simply reset the DefaultConnection value in the Oqtane.Server\appsettings.json file to "". This will trigger a re-install when you run the application which will execute the database installation scripts.
NOTE: If you want to submit pull requests make sure you install the [Github Extension For Visual Studio](https://visualstudio.github.com/). It is recommended you ignore any local changes you have made to the appsettings.json file before you submit a pull request. To automate this activity, open a command prompt and navigate to the /Oqtane.Server/ folder and enter the command "git update-index --skip-worktree appsettings.json"
# Roadmap
This project is a work in progress and the schedule for implementing enhancements is dependent upon the availability of community members who are willing/able to assist.
Note: We are planning to release V1 at the same time that Blazor WebAssembly ships on May 19, 2020
V.Next ( still in the process of being prioritized )
- [ ] Admin UI markup optimization
- [ ] DB Migrations for framework installation/upgrade
- [ ] Support for SQLite
- [ ] Static Localization ( ie. labels, help text, etc.. )
- [ ] Migrate to Code-Behind Pattern ( *.razor.cs )
- [ ] Generic Repository Pattern
- [ ] JwT token authentication ( possibly using IdentityServer )
- [ ] Optional Encryption for Settings Values
V1 (MVP)
V1.0.0 (MVP)
- [x] Multi-Tenant ( Shared Database & Isolated Database )
- [x] Modular Architecture / Headless API
- [x] Dynamic Page Compositing Model / Site & Page Management
@ -40,14 +48,6 @@ V1 (MVP)
- [x] Notifications / Email Delivery
- [x] Auto-Upgrade Framework
V.Next
- [ ] Use Migrations rather than SQL scripts for database installation/upgrade
- [ ] Optional Encryption of Settings Values ( ie. via an IsSecure flag )
- [ ] Localization
- [ ] Migrate to Code-Behind Pattern ( *.razor.cs )
- [ ] Generic Repository Pattern
- [ ] JwT token authentication
# Background
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Initially created as a proof of concept, Oqtane is a native Blazor application written from the ground up using modern .NET Core technology. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly templates (skins), and extensibility via third party modules.
@ -59,7 +59,7 @@ Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalke
Install Wizard:
![Installer](https://github.com/oqtane/framework/blob/master/installer.png?raw=true "Installer")
![Installer](https://github.com/oqtane/framework/blob/master/screenshots/Installer.png?raw=true "Installer")
Default view after installation:
@ -88,3 +88,8 @@ Control panel for adding, editing, and deleting pages as well as adding new modu
Admin dashboard for accessing the variuous administrative features of the framework:
![Admin Dashboard](https://github.com/oqtane/framework/blob/master/screenshots/screenshot6.png?raw=true "Admin Dashboard")
Responsive design mobile view:
![Mobile View](https://github.com/oqtane/framework/blob/master/screenshots/screenshot7.png?raw=true "Mobile View")

BIN
screenshots/Installer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 141 KiB

BIN
screenshots/screenshot7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB