diff --git a/generators/spa/index.js b/generators/spa/index.js index 4a325bde..57ba7749 100644 --- a/generators/spa/index.js +++ b/generators/spa/index.js @@ -181,6 +181,24 @@ module.exports = class extends DnnGeneratorBase { template ); + this.fs.copyTpl( + this.templatePath('common/Data/**'), + this.destinationPath(moduleName + '/Data/'), + template + ); + + this.fs.copyTpl( + this.templatePath('common/ViewModels/**'), + this.destinationPath(moduleName + '/ViewModels/'), + template + ); + + this.fs.copyTpl( + this.templatePath('common/Providers/**'), + this.destinationPath(moduleName + '/Providers/'), + template + ); + // Do all templated copies this.fs.copyTpl( this.templatePath('../../common/src/**'), @@ -205,6 +223,12 @@ module.exports = class extends DnnGeneratorBase { this.destinationPath(moduleName + '/RouteConfig.cs'), template ); + + this.fs.copyTpl( + this.templatePath('common/Constants.cs'), + this.destinationPath(moduleName + '/Constants.cs'), + template + ); this.fs.copyTpl( this.templatePath('common/manifest.dnn'), @@ -213,8 +237,38 @@ module.exports = class extends DnnGeneratorBase { ); this.fs.copyTpl( - this.templatePath('../../common/csproj/_Project.csproj'), + this.templatePath('common/symbols.dnn'), + this.destinationPath(moduleName + '/' + moduleName + '_Symbols.dnn'), + template + ); + + this.fs.copyTpl( + this.templatePath('common/License.txt'), + this.destinationPath(moduleName + '/License.txt'), + template + ); + + this.fs.copyTpl( + this.templatePath('common/ReleaseNotes.txt'), + this.destinationPath(moduleName + '/ReleaseNotes.txt'), + template + ); + + this.fs.copyTpl( + this.templatePath(spaType + '/common/Module.csproj'), this.destinationPath(moduleName + '/' + moduleName + '.csproj'), + template + ); + + this.fs.copyTpl( + this.templatePath(spaType + '/common/Module.build'), + this.destinationPath(moduleName + '/Module.build'), + template + ); + + this.fs.copyTpl( + this.templatePath('common/Data/ModuleContext.cs'), + this.destinationPath(moduleName + '/Data/' + moduleName + 'Context.cs'), template ); @@ -258,7 +312,7 @@ module.exports = class extends DnnGeneratorBase { 'html-webpack-plugin': '^3.2.0', // eslint-disable-next-line prettier/prettier 'marked': '^0.5.2', - 'node-sass': '^4.11.0', + 'node-sass': '^8.0.0', 'sass-loader': '^7.1.0', 'style-loader': '^0.23.1', // eslint-disable-next-line prettier/prettier @@ -268,10 +322,13 @@ module.exports = class extends DnnGeneratorBase { 'webpack-node-externals': '^1.7.2' }, dependencies: { - 'prop-types': '^15.6.2', - // eslint-disable-next-line prettier/prettier - 'react': '^16.6.3', - 'react-dom': '^16.6.3' + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1", + "web-vitals": "^2.1.4" } }; @@ -290,7 +347,6 @@ module.exports = class extends DnnGeneratorBase { 'eslint': '^5.8.0', 'eslint-loader': '^2.1.1', 'eslint-plugin-react': '^7.11.1', - 'react-hot-loader': '^4.3.12' }; } else { this._writeTsConfig(); diff --git a/generators/spa/templates/ReactJS/common/Module.build b/generators/spa/templates/ReactJS/common/Module.build new file mode 100644 index 00000000..62283058 --- /dev/null +++ b/generators/spa/templates/ReactJS/common/Module.build @@ -0,0 +1,53 @@ + + + <%= moduleName %> + <%= moduleName %> + <%= fullNamespace %> + zip + $(MSBuildProjectDirectory)\..\..\Build + $(MSBuildProjectDirectory)\..\..\Website + $(WebsitePath)\Install\Module + $(WebsitePath)\DesktopModules\$(ModulePath) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/generators/spa/templates/ReactJS/common/Module.csproj b/generators/spa/templates/ReactJS/common/Module.csproj new file mode 100644 index 00000000..862151ec --- /dev/null +++ b/generators/spa/templates/ReactJS/common/Module.csproj @@ -0,0 +1,160 @@ + + + + Debug + AnyCPU + + + 2.0 + {<%= guid %>} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + <%= fullNamespace %> + <%= fullNamespace %> + v4.8 + false + + + + + + .\ + true + + + 12.0 + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + bin\<%= fullNamespace %>.xml + + + pdbonly + true + bin\ + TRACE + prompt + 4 + bin\<%= fullNamespace %>.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + + + + Designer + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + True + + + + + + + + diff --git a/generators/spa/templates/ReactJS/jsx/src/app.jsx b/generators/spa/templates/ReactJS/jsx/src/app.jsx index 6225b5ac..e9fa749d 100644 --- a/generators/spa/templates/ReactJS/jsx/src/app.jsx +++ b/generators/spa/templates/ReactJS/jsx/src/app.jsx @@ -1,12 +1,10 @@ import React from "react"; import * as ReactDOM from "react-dom"; -import Hello from "./components/Hello"; +import Items from "./components/Items"; ReactDOM.render( -
-
- -
-
, + + + , document.getElementById("<%= namespace.toLowerCase() %><%= moduleName %>") ); \ No newline at end of file diff --git a/generators/spa/templates/ReactJS/jsx/src/components/Items.jsx b/generators/spa/templates/ReactJS/jsx/src/components/Items.jsx new file mode 100644 index 00000000..534edd4b --- /dev/null +++ b/generators/spa/templates/ReactJS/jsx/src/components/Items.jsx @@ -0,0 +1,221 @@ +import React from "react"; +import { useState, useEffect } from "react"; +const moduleId = window.getmoduleId(); +const serviceFramework = window.$.ServicesFramework(moduleId); +const token = serviceFramework.getAntiForgeryValue(); +const tabId = serviceFramework.getTabId(); +const baseUrl ="DesktopModules/<%= moduleName %>/API/"; + +const Items = () => { + const [modal, setModal] = useState(false); + const [items, setItems] = useState(null); + const [users, setUsers] = useState(null); + const [description, setDescription] = useState(null); + const [name, setName] = useState(null); + const [assignedUser, setUser] = useState(null); + const [id, setId] = useState(0); + // SETTINGS + const [idSetting, setIdSetting] = useState(false); + const [nameSetting, setNameSetting] = useState(false); + const [descriptionSetting, setDescriptionSetting] = useState(false); + const [createdOnDateSetting, setCreatedOnDateSetting] = useState(false); + + const loadItems = () => { + const url = baseUrl+"Item/GetList"; + fetch(url, + { + method: "GET", + headers: { + "ModuleId": moduleId, + "RequestVerificationToken": token, + "TabId": tabId + } + }).then(res => { + return res.json(); + }).then(data => { + setItems(data); + }); + }; + + const loadSettings = () => { + const url = baseUrl+"Settings/LoadSettings"; + fetch(url, + { + method: "GET", + headers: { + "ModuleId": moduleId, + "RequestVerificationToken": token, + "TabId": tabId + } + }).then(res => { + return res.json(); + }).then(data => { + setIdSetting(data.itemId === "true" ? true : false); + setNameSetting(data.name === "true" ? true : false); + setDescriptionSetting(data.description === "true" ? true : false); + setCreatedOnDateSetting(data.createdOnDate === "true" ? true : false); + }); + }; + + const cancelAdd = () => { + setModal(false); + resetItem(); + }; + + const resetItem = () => { + setName(null); + setId(0); + setDescription(null); + setUser(null); + }; + + const editItem = (item) => { + setName(item.name); + setUser(item.assignedUser); + setDescription(item.description); + setId(item.id); + setModal(true); + }; + + const saveChanges = () => { + const item = { id, name, description, assignedUser }; + const url = baseUrl + "Item/Save"; + fetch(url, + { + method: "POST", + headers: { + "ModuleId": moduleId, + "RequestVerificationToken": token, + "TabId": tabId, + "Content-Type": "application/json" + }, + body: JSON.stringify(item) + }).then(res => { + if (res.ok) { + resetItem(); + setModal(false); + loadItems(); + } + }); + }; + + const loadUsers = () => { + const url = baseUrl + "User/GetList"; + fetch(url, + { + method: "GET", + headers: { + "ModuleId": moduleId, + "RequestVerificationToken": token, + "TabId": tabId + } + }).then(res => { + return res.json(); + }).then(data => { + setUsers(data); + }); + }; + + useEffect(() => { + loadItems(); + loadSettings(); + loadUsers(); + }, []); + + const removeItem = (itemId) => { + const url = baseUrl + "Item/Delete?itemId=" + itemId; + + if (confirm("Do you want to remove this item?")) { + fetch(url, + { + method: "DELETE", + headers: { + "ModuleId": moduleId, + "RequestVerificationToken": token, + "TabId": tabId + } + }).then(res => { + if (!res.ok) { + throw new Error("HTTP status " + res.status); + } else { + loadItems(); + } + }); + } + }; + + return (
+
+

Item list

+
+ + + + + {idSetting && } + {nameSetting && } + {descriptionSetting && } + + + + + + {items && items.map((item) => ( + {idSetting && } + {nameSetting && } + {descriptionSetting && } + {createdOnDateSetting && } + + ))} + +
IdNameDescriptionCreated on
{item.id}{item.name}{item.description}{item.createdOnDate} + + +
+ {modal && } +
); +}; + +export default Items; diff --git a/generators/spa/templates/common/Components/BaseClasses/ApiControllerBase.cs b/generators/spa/templates/common/Components/BaseClasses/ApiControllerBase.cs new file mode 100644 index 00000000..0c544be3 --- /dev/null +++ b/generators/spa/templates/common/Components/BaseClasses/ApiControllerBase.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using DotNetNuke.Web.Api; +using <%= fullNamespace %>.Data; + +namespace <%= fullNamespace %>.Components.BaseClasses +{ + public class ApiControllerBase : DnnApiController + { + private const string DBCONTEXT_KEY = "<%= moduleName %>Context_Instance"; + + public <%= moduleName %>Context DbCtx + { + get + { + return GetContext(); + } + } + + /// + /// Returns a DbContext object for use with this request. Instanciates a new DbContext when requested in the parameter. When using createNewInstance, you should always dispose your DbContext yourself. + /// + /// + /// + protected <%= moduleName %>Context GetContext(bool createNewInstance = false) + { + // if a new instance is requested: return one + if (createNewInstance) return new <%= moduleName %>Context(); + + // get a reference to the HttpContext + var ctx = Request.Properties["MS_HttpContext"] as HttpContextWrapper; + + <%= moduleName %>Context retval = null; + // se if we have one in the HttpContext already + if (ctx.Items[DBCONTEXT_KEY] == null) + { + retval = new <%= moduleName %>Context(); + // store in HttpContext + ctx.Items[DBCONTEXT_KEY] = retval; + } + else + { + // get from HttpContext + retval = (<%= moduleName %>Context)ctx.Items[DBCONTEXT_KEY]; + } + + return retval; + } + + protected override void Dispose(bool disposing) + { + // get a reference to the HttpContext + var ctx = Request.Properties["MS_HttpContext"] as HttpContextWrapper; + + // dispose of stored DbContext + if (ctx.Items[DBCONTEXT_KEY] != null) + { + var dbctx = (<%= moduleName %>Context)ctx.Items[DBCONTEXT_KEY]; + dbctx.Dispose(); + } + + base.Dispose(disposing); + } + } +} diff --git a/generators/spa/templates/common/Constants.cs b/generators/spa/templates/common/Constants.cs new file mode 100644 index 00000000..f46e659e --- /dev/null +++ b/generators/spa/templates/common/Constants.cs @@ -0,0 +1,27 @@ +/* +Copyright Upendo Ventures, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +namespace <%= fullNamespace %> +{ + public static class QuickSettings + { + public const string MODSETTING_Name = "Name"; + public const string MODSETTING_Description = "Description"; + public const string MODSETTING_ItemId = "ItemId"; + public const string MODSETTING_AssignedUserId = "AssignedUserId"; + public const string MODSETTING_CreatedByUserId = "CreatedByUserId"; + public const string MODSETTING_CreatedOnDate = "CreatedOnDate"; + } +} \ No newline at end of file diff --git a/generators/spa/templates/common/Controllers/ItemController.cs b/generators/spa/templates/common/Controllers/ItemController.cs new file mode 100644 index 00000000..564f56fe --- /dev/null +++ b/generators/spa/templates/common/Controllers/ItemController.cs @@ -0,0 +1,111 @@ +using DotNetNuke.Common; +using DotNetNuke.Common.Utilities; +using DotNetNuke.Security; +using DotNetNuke.UI.Modules; +using DotNetNuke.Web.Api; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Web.Http; +using <%= fullNamespace %>.Components.BaseClasses; +using <%= fullNamespace %>.Data; +using <%= fullNamespace %>.ViewModels; + +namespace <%= fullNamespace %>.Controllers +{ + [SupportedModules("<%= moduleName %>")] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)] + + public class ItemController : ApiControllerBase + { + public ItemController() { } + [HttpDelete] + public HttpResponseMessage Delete(int itemId) + { + var item = DbCtx.Items.FirstOrDefault(i => i.ItemId == itemId); + if (item != null) + { + DbCtx.Items.Remove(item); + DbCtx.SaveChanges(); + } + + return Request.CreateResponse(System.Net.HttpStatusCode.NoContent); + } + + [HttpGet] + public HttpResponseMessage Get(int itemId) + { + var itemVm = new ItemViewModel(DbCtx.Items.FirstOrDefault(i => i.ItemId == itemId)); + + return Request.CreateResponse(itemVm); + } + + [HttpGet] + public HttpResponseMessage GetList() + { + List retval = new List(); + List items; + + items = DbCtx.Items.Where(i => i.ModuleId == ActiveModule.ModuleID).ToList(); + items.ForEach(i => retval.Add(new ItemViewModel(i, Globals.IsEditMode()))); + + return Request.CreateResponse(retval); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public HttpResponseMessage Save(ItemViewModel item) + { + if (item.Id > 0) + { + var t = Update(item); + return Request.CreateResponse(System.Net.HttpStatusCode.NoContent); + } + else + { + var t = Create(item); + return Request.CreateResponse(t.ItemId); + } + + } + + private Item Create(ItemViewModel item) + { + Item t = new Item + { + ItemName = item.Name, + ItemDescription = item.Description, + AssignedUserId = item.AssignedUser, + ModuleId = ActiveModule.ModuleID, + CreatedByUserId = UserInfo.UserID, + LastModifiedByUserId = UserInfo.UserID, + CreatedOnDate = DateTime.UtcNow, + LastModifiedOnDate = DateTime.UtcNow + }; + DbCtx.Items.Add(t); + DbCtx.SaveChanges(); + + return t; + } + + private Item Update(ItemViewModel item) + { + + var t = DbCtx.Items.FirstOrDefault(i => i.ItemId == item.Id); + if (t != null) + { + t.ItemName = item.Name; + t.ItemDescription = item.Description; + t.AssignedUserId = item.AssignedUser; + t.LastModifiedByUserId = UserInfo.UserID; + t.LastModifiedOnDate = DateTime.UtcNow; + } + DbCtx.SaveChanges(); + + return t; + } + } +} diff --git a/generators/spa/templates/common/Controllers/SettingsController.cs b/generators/spa/templates/common/Controllers/SettingsController.cs new file mode 100644 index 00000000..b83e92cc --- /dev/null +++ b/generators/spa/templates/common/Controllers/SettingsController.cs @@ -0,0 +1,84 @@ +/* +Copyright Upendo Ventures, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +using System.Net.Http; +using System.Net; +using System.Web.Http; +using DotNetNuke.Entities.Modules; +using DotNetNuke.Web.Api; +using DotNetNuke.Security; +using Telerik.Web.UI.Calendar.Utils; +using <%= fullNamespace %>; +using <%= fullNamespace %>.ViewModels; + +namespace <%= fullNamespace %>.Controllers +{ + [SupportedModules("<%= moduleName %>")] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)] + public class SettingsController : DnnApiController + { + public SettingsController() { } + + [HttpGet] //[baseURL]/settings/load + + public HttpResponseMessage LoadSettings() + { + var settings = new SettingsViewModel(); + + if (ActiveModule.ModuleSettings.ContainsKey(QuickSettings.MODSETTING_Name)) + { + settings.Name = ActiveModule.ModuleSettings[QuickSettings.MODSETTING_Name].ToString(); + } + if (ActiveModule.ModuleSettings.ContainsKey(QuickSettings.MODSETTING_Description)) + { + settings.Description = ActiveModule.ModuleSettings[QuickSettings.MODSETTING_Description].ToString(); + } + if (ActiveModule.ModuleSettings.ContainsKey(QuickSettings.MODSETTING_CreatedByUserId)) + { + settings.CreatedByUserId = ActiveModule.ModuleSettings[QuickSettings.MODSETTING_CreatedByUserId].ToString(); + } + if (ActiveModule.ModuleSettings.ContainsKey(QuickSettings.MODSETTING_AssignedUserId)) + { + settings.AssignedUserId = ActiveModule.ModuleSettings[QuickSettings.MODSETTING_AssignedUserId].ToString(); + } + if (ActiveModule.ModuleSettings.ContainsKey(QuickSettings.MODSETTING_ItemId)) + { + settings.ItemId = ActiveModule.ModuleSettings[QuickSettings.MODSETTING_ItemId].ToString(); + } + if (ActiveModule.ModuleSettings.ContainsKey(QuickSettings.MODSETTING_CreatedOnDate)) + { + settings.CreatedOnDate = ActiveModule.ModuleSettings[QuickSettings.MODSETTING_CreatedOnDate].ToString(); + } + + return Request.CreateResponse(HttpStatusCode.OK, settings); + } + + [HttpPost] //[baseURL]/settings/save + [ActionName("save")] + [ValidateAntiForgeryToken] + public HttpResponseMessage SaveSettings(SettingsViewModel settings) + { + ModuleController.Instance.UpdateModuleSetting(ActiveModule.ModuleID, QuickSettings.MODSETTING_Name, settings.Name); + ModuleController.Instance.UpdateModuleSetting(ActiveModule.ModuleID, QuickSettings.MODSETTING_Description, settings.Description); + ModuleController.Instance.UpdateModuleSetting(ActiveModule.ModuleID, QuickSettings.MODSETTING_CreatedByUserId, settings.CreatedByUserId); + ModuleController.Instance.UpdateModuleSetting(ActiveModule.ModuleID, QuickSettings.MODSETTING_AssignedUserId, settings.AssignedUserId); + ModuleController.Instance.UpdateModuleSetting(ActiveModule.ModuleID, QuickSettings.MODSETTING_ItemId, settings.ItemId); + ModuleController.Instance.UpdateModuleSetting(ActiveModule.ModuleID, QuickSettings.MODSETTING_CreatedOnDate, settings.CreatedOnDate); + + return Request.CreateResponse(HttpStatusCode.OK, "Success"); + } + } + +} diff --git a/generators/spa/templates/common/Controllers/UserController.cs b/generators/spa/templates/common/Controllers/UserController.cs new file mode 100644 index 00000000..b9ce01c5 --- /dev/null +++ b/generators/spa/templates/common/Controllers/UserController.cs @@ -0,0 +1,33 @@ +using <%= fullNamespace %>.ViewModels; +using DotNetNuke.Entities.Users; +using DotNetNuke.Security; +using DotNetNuke.Web.Api; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; + +namespace <%= fullNamespace %>.Controllers +{ + [SupportedModules("<%= moduleName %>")] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)] + public class UserController : DnnApiController + { + public UserController() { } + + public HttpResponseMessage Dummy() + { + return Request.CreateErrorResponse(HttpStatusCode.MethodNotAllowed, "Dummy called"); + } + public HttpResponseMessage GetList() + { + + var userlist = DotNetNuke.Entities.Users.UserController.GetUsers(this.PortalSettings.PortalId); + var users = userlist.Cast().ToList() + .Select(user => new UserViewModel(user)) + .ToList(); + + return Request.CreateResponse(users); + } + } +} diff --git a/generators/spa/templates/common/Data/Item.cs b/generators/spa/templates/common/Data/Item.cs new file mode 100644 index 00000000..7cd44b3c --- /dev/null +++ b/generators/spa/templates/common/Data/Item.cs @@ -0,0 +1,33 @@ +namespace <%= fullNamespace %>.Data +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.ComponentModel.DataAnnotations.Schema; + using System.Data.Entity.Spatial; + + public partial class Item + { + [Key] + public int ItemId { get; set; } + + [Required] + public string ItemName { get; set; } + + [Required] + public string ItemDescription { get; set; } + + public int? AssignedUserId { get; set; } + + public int ModuleId { get; set; } + + public DateTime CreatedOnDate { get; set; } + + public int CreatedByUserId { get; set; } + + public DateTime LastModifiedOnDate { get; set; } + + public int LastModifiedByUserId { get; set; } + + } +} \ No newline at end of file diff --git a/generators/spa/templates/common/Data/ModuleContext.cs b/generators/spa/templates/common/Data/ModuleContext.cs new file mode 100644 index 00000000..b16fe731 --- /dev/null +++ b/generators/spa/templates/common/Data/ModuleContext.cs @@ -0,0 +1,36 @@ +using System; +using System.Data.Entity; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using DotNetNuke.Data; +using DotNetNuke.Framework.Providers; + +namespace <%= fullNamespace %>.Data +{ + public class <%= moduleName %>Context: DbContext + { + public <%= moduleName %>Context() + : base("name=SiteSqlServer") + { + } + + public virtual DbSet Items { get; set; } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + string moduleQualifier = "<%= moduleName %>_"; + // get the dnn data provider configuration + ProviderConfiguration pc = ProviderConfiguration.GetProviderConfiguration("data"); + // Read the configuration specific information for this provider + Provider provider = (Provider)pc.Providers[pc.DefaultProvider]; + // get the objectQualifier + String objectQualifier = provider.Attributes["objectQualifier"]; + // append an underscore when it's not there + if (!string.IsNullOrEmpty(objectQualifier) && !objectQualifier.EndsWith("_")) + objectQualifier += "_"; + + // map the object to the right table + modelBuilder.Entity().ToTable($"{objectQualifier}{moduleQualifier}Items"); + } + } +} diff --git a/generators/spa/templates/common/License.txt b/generators/spa/templates/common/License.txt new file mode 100644 index 00000000..ea13267a --- /dev/null +++ b/generators/spa/templates/common/License.txt @@ -0,0 +1,3 @@ +

<%= moduleName %> Modules Extension for DNN

+

<%= company %> <%= companyUrl %>

+

Your License Here

diff --git a/generators/spa/templates/common/Providers/DataProviders/SqlDataProvider/00.00.01.SqlDataProvider b/generators/spa/templates/common/Providers/DataProviders/SqlDataProvider/00.00.01.SqlDataProvider new file mode 100644 index 00000000..8511abb4 --- /dev/null +++ b/generators/spa/templates/common/Providers/DataProviders/SqlDataProvider/00.00.01.SqlDataProvider @@ -0,0 +1,42 @@ +/************************************************************/ +/***** SqlDataProvider *****/ +/***** *****/ +/***** *****/ +/***** Note: To manually execute this script you must *****/ +/***** perform a search and replace operation *****/ +/***** for {databaseOwner} and {objectQualifier} *****/ +/***** *****/ +/************************************************************/ + +IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}<%= moduleName %>_Items]') AND type in (N'U')) +DROP TABLE {databaseOwner}[{objectQualifier}<%= moduleName %>_Items] +GO + +CREATE TABLE {databaseOwner}{objectQualifier}<%= moduleName %>_Items + ( + ItemId int NOT NULL IDENTITY (1, 1), + ItemName nvarchar(MAX) NOT NULL, + ItemDescription nvarchar(MAX) NOT NULL, + AssignedUserId int NULL, + ModuleId int NOT NULL, + CreatedOnDate datetime NOT NULL, + CreatedByUserId int NOT NULL, + LastModifiedOnDate datetime NOT NULL, + LastModifiedByUserId int NOT NULL + ) ON [PRIMARY] + TEXTIMAGE_ON [PRIMARY] +GO + + +ALTER TABLE {databaseOwner}{objectQualifier}<%= moduleName %>_Items ADD CONSTRAINT + PK_{objectQualifier}<%= moduleName %>_Items PRIMARY KEY CLUSTERED + ( + ItemId + ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] + +GO + + +/************************************************************/ +/***** SqlDataProvider *****/ +/************************************************************/ diff --git a/generators/spa/templates/common/Providers/DataProviders/SqlDataProvider/Uninstall.SqlDataProvider b/generators/spa/templates/common/Providers/DataProviders/SqlDataProvider/Uninstall.SqlDataProvider new file mode 100644 index 00000000..712e6962 --- /dev/null +++ b/generators/spa/templates/common/Providers/DataProviders/SqlDataProvider/Uninstall.SqlDataProvider @@ -0,0 +1,3 @@ +IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}<%= moduleName %>_Items]') AND type in (N'U')) +DROP TABLE {databaseOwner}[{objectQualifier}<%= moduleName %>_Items] +GO diff --git a/generators/spa/templates/common/ReleaseNotes.txt b/generators/spa/templates/common/ReleaseNotes.txt new file mode 100644 index 00000000..c85f014c --- /dev/null +++ b/generators/spa/templates/common/ReleaseNotes.txt @@ -0,0 +1,10 @@ +

<%= moduleName %>

+

+ <%= company %>
+ <%= emailAddy %> +

+
+
+

About the <%= moduleName %>

+

This module is intended as an example of how to build DNN Modules with VueJS.

+
diff --git a/generators/spa/templates/common/RouteConfig.cs b/generators/spa/templates/common/RouteConfig.cs index 6e16a68d..25a3bc21 100644 --- a/generators/spa/templates/common/RouteConfig.cs +++ b/generators/spa/templates/common/RouteConfig.cs @@ -7,15 +7,26 @@ ' */ using DotNetNuke.Web.Api; +using System.Web.Http; -namespace <%= namespace%>.Modules.<%= moduleName %> +namespace <%= fullNamespace %> { - public class RouteConfig : IServiceRouteMapper + /// + /// The ServiceRouteMapper tells the DNN Web API Framework what routes this module uses + /// + public class ServiceRouteMapper : IServiceRouteMapper { + /// + /// RegisterRoutes is used to register the module's routes + /// + /// public void RegisterRoutes(IMapRoute mapRouteManager) { - mapRouteManager.MapHttpRoute("<%= namespace%>.Modules.<%= moduleName %>", "<%= namespace%>.Modules.<%= moduleName %>", "{controller}/{action}", new[] - {"<%= namespace%>.Modules.<%= moduleName %>.Controllers"}); + mapRouteManager.MapHttpRoute( + moduleFolderName: "<%= moduleName %>", + routeName: "default", + url: "{controller}/{action}", + namespaces: new[] { "<%= fullNamespace %>.Controllers" }); } } -} +} \ No newline at end of file diff --git a/generators/spa/templates/common/ViewModels/ItemViewModel.cs b/generators/spa/templates/common/ViewModels/ItemViewModel.cs new file mode 100644 index 00000000..c3e49e61 --- /dev/null +++ b/generators/spa/templates/common/ViewModels/ItemViewModel.cs @@ -0,0 +1,46 @@ +using <%= fullNamespace %>.Components; +using <%= fullNamespace %>.Data; +using Newtonsoft.Json; + +namespace <%= fullNamespace %>.ViewModels +{ + [JsonObject(MemberSerialization.OptIn)] + public class ItemViewModel + { + public ItemViewModel(Item t) + { + Id = t.ItemId; + Name = t.ItemName; + Description = t.ItemDescription; + AssignedUser = t.AssignedUserId; + CreatedOnDate = t.CreatedOnDate.ToShortDateString(); + } + + public ItemViewModel(Item t, bool canEdit) : this(t) + { + CanEdit = canEdit; + } + + + public ItemViewModel() { } + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("assignedUser")] + public int? AssignedUser { get; set; } + + [JsonProperty("canEdit")] + public bool CanEdit { get; } + + [JsonProperty("createdOnDate")] + public string CreatedOnDate { get; } + + } +} \ No newline at end of file diff --git a/generators/spa/templates/common/ViewModels/SettingsViewModel.cs b/generators/spa/templates/common/ViewModels/SettingsViewModel.cs new file mode 100644 index 00000000..988de0e5 --- /dev/null +++ b/generators/spa/templates/common/ViewModels/SettingsViewModel.cs @@ -0,0 +1,47 @@ +/* +Copyright Upendo Ventures, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +using System; +using Newtonsoft.Json; + +namespace <%= fullNamespace %>.ViewModels +{ + [Serializable] + [JsonObject(MemberSerialization.OptIn)] + public class SettingsViewModel + { + public SettingsViewModel() + { + } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("itemId")] + public string ItemId { get; set; } + + [JsonProperty("assignedUserId")] + public string AssignedUserId { get; set; } + + [JsonProperty("createdByUserId")] + public string CreatedByUserId { get; set; } + + [JsonProperty("createdOnDate")] + public string CreatedOnDate { get; set; } + } +} diff --git a/generators/spa/templates/common/ViewModels/UserViewModel.cs b/generators/spa/templates/common/ViewModels/UserViewModel.cs new file mode 100644 index 00000000..b467f743 --- /dev/null +++ b/generators/spa/templates/common/ViewModels/UserViewModel.cs @@ -0,0 +1,27 @@ +using DotNetNuke.Entities.Users; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace <%= fullNamespace %>.ViewModels +{ + [JsonObject(MemberSerialization.OptIn)] + public class UserViewModel + { + public UserViewModel(UserInfo t) + { + Id = t.UserID; + Name = t.DisplayName; + } + + public UserViewModel() { } + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/generators/spa/templates/common/manifest.dnn b/generators/spa/templates/common/manifest.dnn index 7292f1d7..3ce00da8 100644 --- a/generators/spa/templates/common/manifest.dnn +++ b/generators/spa/templates/common/manifest.dnn @@ -49,7 +49,7 @@ - DesktopModules/<%= moduleName %>/View.html + DesktopModules/<%= moduleName %>/dist/View.html False <%= moduleName %> SPA View @@ -58,7 +58,7 @@ Edit - DesktopModules/<%= moduleName %>/Edit.html + DesktopModules/<%= moduleName %>/dist/Edit.html False Add/Edit Menu Item Edit @@ -69,7 +69,7 @@ QuickSettings - DesktopModules/<%= moduleName %>/Settings.html + DesktopModules/<%= moduleName %>/dist/Settings.html False <%= moduleName %> Settings Edit @@ -94,12 +94,19 @@ - + + <%= fullNamespace %>.dll + bin + + + EntityFramework.dll + bin + + + EntityFramework.SqlServer.dll bin - <%= namespace %>.<%= moduleName %>.dll - <%= version %> diff --git a/generators/spa/templates/common/src/Edit.html b/generators/spa/templates/common/src/Edit.html index efcdb29a..663f5bae 100644 --- a/generators/spa/templates/common/src/Edit.html +++ b/generators/spa/templates/common/src/Edit.html @@ -1,3 +1,3 @@ 
[CSS:{ path: "~/DesktopModules/<%= namespace %>/<%= moduleName %>/Resources/module.css"}] -[JavaScript:{ path: "~/DesktopModules/<%= namespace %>/<%= moduleName %>/Resources/scripts/edit-bundle.js", provider: "DnnFormBottomProvider"}] +[JavaScript:{ path: "~/DesktopModules/<%= namespace %>/<%= moduleName %>/dist/Resources/scripts/edit-bundle.js", provider: "DnnFormBottomProvider"}] diff --git a/generators/spa/templates/common/src/Resources/scripts/QuickSettings.js b/generators/spa/templates/common/src/Resources/scripts/QuickSettings.js new file mode 100644 index 00000000..3ce532c1 --- /dev/null +++ b/generators/spa/templates/common/src/Resources/scripts/QuickSettings.js @@ -0,0 +1,97 @@ +var dnnspamodule = dnnspamodule || {}; + +dnnspamodule.quickSettings = function (root, moduleId) { + console.log(moduleId); + let utils = new common.Utils(); + let alert = new common.Alert(); + let parentSelector = "[id='" + root + "']"; + // Setup your settings service endpoint + let service = { + baseUrl: "DesktopModules/<%= moduleName %>/API/", + framework: $.ServicesFramework(moduleId), + controller: "Settings" + }; + + let SaveSettings = function () { + + let name = $("#Name").is(":checked"); + let description = $("#Description").is(":checked"); + let assignedUserId = $("#AssignedUserId").is(":checked"); + let createdOnDate = $("#CreatedOnDate").is(":checked"); + let itemId = $("#ItemId").is(":checked"); + + let deferred = $.Deferred(); + let params = { + name: name, + description: description, + assignedUserId: assignedUserId, + createdOnDate: createdOnDate, + itemId: itemId + }; + + utils.get("POST", "save", service, params, + function (data) { + + deferred.resolve(); + location.reload(); + }, + function (error, exception) { + // fail + let deferred = $.Deferred(); + deferred.reject(); + alert.danger({ + selector: parentSelector, + text: error.responseText, + status: error.status + }); + }, + function () { + }); + + return deferred.promise(); + }; + + let CancelSettings = function () { + let deferred = $.Deferred(); + deferred.resolve(); + return deferred.promise(); + }; + + let LoadSettings = function () { + let params = {}; + + utils.get("GET", "LoadSettings", service, params, + function (data) { + $("#CreatedOnDate").prop("checked", data.createdOnDate == "true"); + $("#Name").prop("checked", data.name == "true"); + $("#Description").prop("checked", data.description == "true"); + $("#ItemId").prop("checked", data.itemId == "true"); + }, + function (error, exception) { + // fail + console.log("12345657897"); + console.log(error); + alert.danger({ + selector: parentSelector, + text: error.responseText, + status: error.status + }); + }, + function () { + }); + }; + + let init = function () { + // Wire up the default save and cancel buttons + $(root).dnnQuickSettings({ + moduleId: moduleId, + onSave: SaveSettings, + onCancel: CancelSettings + }); + LoadSettings(); + }; + + return { + init: init + }; +}; \ No newline at end of file diff --git a/generators/spa/templates/common/src/Resources/scripts/common.js b/generators/spa/templates/common/src/Resources/scripts/common.js new file mode 100644 index 00000000..2d51536d --- /dev/null +++ b/generators/spa/templates/common/src/Resources/scripts/common.js @@ -0,0 +1,152 @@ +var common = common || {}; + +common.Utils = function () { + + let get = function (httpMethod, action, service, params, success, fail, always) { + let jqxhr = $.ajax({ + url: service.baseUrl + service.controller + "/" + action, + beforeSend: service.framework.setModuleHeaders, + type: httpMethod, + async: true, + data: httpMethod == "GET" ? "" : JSON.stringify(params), + dataType: httpMethod == "GET" ? "" : "json", + contentType: httpMethod == "GET" ? "" : "application/json; charset=UTF-8" + }).done(function (data) { + if (typeof (success) === "function") { + success(data); + } + }).fail(function (error, exception) { + if (typeof (fail) === "function") { + fail(error, exception); + } + }).always(function () { + if (typeof (always) === "function") { + always(); + } + }); + }; + + let addRewriteQueryString = function (hash, decode) { + let path = location.pathname; + let queryString = path.substring(path.search("/ctl/") + 1); + let keyValues = queryString.split("/"); + + for (let i = 0; i < keyValues.length; i += 2) { + hash[decode(keyValues[i])] = decode(keyValues[i + 1]); + } + return hash; + }; + + let getQueryStrings = function () { + let assoc = {}; + let decode = function (s) { return decodeURIComponent(s.replace(/\+/g, " ")); }; + let queryString = location.search.substring(1); + let keyValues = queryString.split("&"); + + for (let i = 0; i < keyValues.length; i++) { + let key = keyValues[i].split("="); + if (key.length > 1) { + assoc[decode(key[0])] = decode(key[1]); + } + } + return addRewriteQueryString(assoc, decode); + }; + + let loading = function (icon, cssClass) { + $(icon).toggleClass(cssClass).toggleClass("fa-refresh fa-spin"); + }; + + return { + get: get, + getQueryStrings: getQueryStrings, + addRewriteQueryString: addRewriteQueryString, + loading: loading + }; +}; + + +common.Alert = function () { + + let success = function (message) { + message.redirect = typeof (message.redirect) !== "undefined" ? message.redirect : false; + + $("") + .appendTo($(message.selector)) + .slideDown(800, function () { + if (message.redirect) { + setTimeout(function () { + location.href = typeof (message.postBack) !== "undefined" ? message.postBack : "/"; + }, 3000); + } + }); + }; + + let info = function (message) { + message.redirect = typeof (message.redirect) !== "undefined" ? message.redirect : false; + + $("") + .appendTo($(message.selector)) + .slideDown(800, function () { + if (message.redirect) { + setTimeout(function () { + location.href = typeof (message.postBack) !== "undefined" ? message.postBack : "/"; + }, 3000); + } + }); + }; + + let warning = function (message) { + message.redirect = typeof (message.redirect) !== "undefined" ? message.redirect : false; + + $("") + .appendTo($(message.selector)) + .slideDown(800, function () { + if (message.redirect) { + setTimeout(function () { + location.href = typeof (message.postBack) !== "undefined" ? message.postBack : "/"; + }, 3000); + } + }); + }; + + let danger = function (message) { + message.redirect = typeof (message.redirect) !== "undefined" ? message.redirect : false; + + $("") + .prependTo($(message.selector)) + .slideDown(800, function () { + if (message.redirect) { + setTimeout(function () { + location.href = typeof (message.postBack) !== "undefined" ? message.postBack : "/"; + }, 3000); + } + }); + }; + + let dismiss = function (alert, callback) { + let alertPanel = $(alert.selector).find(".alert-panel"); + + $.each(alertPanel, function (index, el) { + + $(el).slideUp(800, function () { + $(el).remove(); + + if (typeof (callback) === "function") { + callback(); + } + }); + }); + + if (alertPanel.length == 0 && typeof (callback) === "function") { + callback(); + } + }; + + return { + success: success, + info: info, + warning: warning, + danger: danger, + dismiss: dismiss + }; +}; diff --git a/generators/spa/templates/common/src/Resources/scripts/useFetch.js b/generators/spa/templates/common/src/Resources/scripts/useFetch.js new file mode 100644 index 00000000..80311b9e --- /dev/null +++ b/generators/spa/templates/common/src/Resources/scripts/useFetch.js @@ -0,0 +1,47 @@ +import { useState, useEffect } from "react"; + +const useFetch = (url) => { + const [data, setData] = useState(null); + const [isPending, setIsPending] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const abortCont = new AbortController(); + fetch(url,{ + method: "GET", + headers: { + "ModuleId": 389, + "RequestVerificationToken": "vt4k-ZjhQv_b6yTjs5I0DhumWcY_8sMm52euNE-MzEFLJVg3EtMu5GkKRGtWCgowSCPA58POdpzdprZA0", + "TabId": 36 + }, + signal: abortCont.signal + }) + .then(res => { + if (!res.ok) { // error coming back from server + throw Error("could not fetch the data for that resource"); + } + return res.json(); + }) + .then(data => { + setIsPending(false); + setData(data); + setError(null); + }) + .catch(err => { + if (err.name === "AbortError") { + console.log("fetch aborted"); + } else { + // auto catches network / connection error + setIsPending(false); + setError(err.message); + } + }); + + // abort the fetch + return () => abortCont.abort(); + }, [url]); + + return { data, isPending, error }; +}; + +export default useFetch; \ No newline at end of file diff --git a/generators/spa/templates/common/src/Settings.html b/generators/spa/templates/common/src/Settings.html index 49bb70be..92233d09 100644 --- a/generators/spa/templates/common/src/Settings.html +++ b/generators/spa/templates/common/src/Settings.html @@ -1,3 +1,27 @@ -
-[CSS:{ path: "~/DesktopModules/<%= namespace %>/<%= moduleName %>/Resources/module.css"}] -[JavaScript:{ path: "~/DesktopModules/<%= namespace %>/<%= moduleName %>/Resources/scripts/settings-bundle.js", provider: "DnnFormBottomProvider"}] +[JavaScript:{ jsname: "JQuery" }] +[JavaScript:{ jsname: "Knockout" }] +[JavaScript:{ path: "~/Resources/Shared/scripts/dnn.jquery.js"}] +[JavaScript:{ path: "~/DesktopModules/<%= moduleName %>/dist/Resources/scripts/common.js"}] +[JavaScript:{ path: "~/DesktopModules/<%= moduleName %>/dist/Resources/scripts/QuickSettings.js"}] + +
+ +
+ +
+ +
+ +
+ +
+
+
+ \ No newline at end of file diff --git a/generators/spa/templates/common/src/View.html b/generators/spa/templates/common/src/View.html index 044e14d6..fc423e2e 100644 --- a/generators/spa/templates/common/src/View.html +++ b/generators/spa/templates/common/src/View.html @@ -2,8 +2,13 @@ securityAccessLevel : "Edit", title: "Edit", titleKey: "EditModule", - localResourceFile: "~/DesktopModules/<%= namespace %>/<%= moduleName %>/App_LocalResources/View.resx" + localResourceFile: "~/DesktopModules/<%= moduleName %>/App_LocalResources/View.resx" }]
-[CSS:{ path: "~/DesktopModules/<%= namespace %>/<%= moduleName %>/Resources/module.css"}] -[JavaScript:{ path: "~/DesktopModules/<%= namespace %>/<%= moduleName %>/Resources/scripts/app-bundle.js", provider: "DnnFormBottomProvider"}] + +[CSS:{ path: "~/DesktopModules/<%= moduleName %>/Resources/module.css"}] +[JavaScript:{ path: "~/DesktopModules/<%= moduleName %>/dist/Resources/scripts/app-bundle.js", provider: "DnnFormBottomProvider"}]