Add policy editor to the admin screen

WIP - Adding save dialog to enter description of change, version, author etc.

admin.js
Added Markdown custom renderer
Added policy reload and policy save
This commit is contained in:
chris.watts90@outlook.com 2019-10-18 09:51:24 +01:00
parent d5c822c143
commit 22d142204d
2 changed files with 386 additions and 233 deletions

View File

@ -1,109 +1,169 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Flexitime Admin page</title> <title>Flexitime Admin page</title>
<link rel="stylesheet preload" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <link rel="stylesheet preload" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link href="spa.min.css" rel="stylesheet" /> <link href="spa.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sammy.js/0.7.6/sammy.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/sammy.js/0.7.6/sammy.js" type="text/javascript"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head> <link rel="stylesheet" href="css/easymde.min.css" />
<body> <link rel="stylesheet" href="css/highlightjs.min.css" />
<nav class="navbar navbar-default"> </head>
<div class="container-fluid"> <body>
<!-- Brand and toggle get grouped for better mobile display --> <nav class="navbar navbar-default">
<div class="navbar-header"> <div class="container-fluid">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <!-- Brand and toggle get grouped for better mobile display -->
<span class="sr-only">Toggle navigation</span> <div class="navbar-header">
<span class="icon-bar"></span> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="icon-bar"></span> <span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> <span class="icon-bar"></span>
<a class="navbar-brand" href="#">Big Brother</a> <span class="icon-bar"></span>
</div> </button>
<a class="navbar-brand" href="#">Big Brother</a>
<!-- Collect the nav links, forms, and other content for toggling --> </div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav"> <!-- Collect the nav links, forms, and other content for toggling -->
<li><a class="indent-nav-xs" href="index.html">Home</a></li> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<li class="hidden-xs"> <ul class="nav navbar-nav">
<a data-toggle="modal" data-target="#aboutDialog">About</a> <li><a class="indent-nav-xs" href="index.html">Home</a></li>
</li> <li class="hidden-xs">
</ul> <a data-toggle="modal" data-target="#aboutDialog">About</a>
</div> </li>
</div> </ul>
</nav> </div>
<div id="GroupAdminPage" class="container"> </div>
<div class="row"> </nav>
<h2 class="col-md-4">Groups</h2> <div id="GroupAdminPage" class="container">
<button class="col-md-1 btn btn-default pull-right" style="margin-top: 20px;" data-bind="click: $root.newGroupForm"> <div class="row">
<span class="glyphicon glyphicon-plus"></span> <h2 class="col-md-4">Groups</h2>
</button> <button class="col-md-1 btn btn-default pull-right" style="margin-top: 20px;" data-bind="click: $root.newGroupForm">
</div> <span class="glyphicon glyphicon-plus"></span>
<hr/> </button>
<div class="row"> </div>
<div data-bind="with: groupsList, css:{'col-md-8': !$root.groupFormHidden(), 'col-md-12':$root.groupFormHidden()}"> <hr />
<table class="table table-striped"> <div class="row">
<thead> <div data-bind="with: groupsList, css:{'col-md-8': !$root.groupFormHidden(), 'col-md-12':$root.groupFormHidden()}">
<tr> <table class="table table-striped">
<th>Name</th> <thead>
<th class="col-md-3">User Count</th> <tr>
<th class="col-md-1"></th> <th>Name</th>
</tr> <th class="col-md-3">User Count</th>
</thead> <th class="col-md-1"></th>
<tbody data-bind="foreach: Groups"> </tr>
<tr data-bind="click: $root.editGroupClick"> </thead>
<td class="valign" data-bind="text: Name"></td> <tbody data-bind="foreach: Groups">
<td class="valign" data-bind="text: UserCount"></td> <tr data-bind="click: $root.editGroupClick">
<td><button class="btn btn-link" data-bind="enable: UserCount=='0', click: function(data, event){$root.deleteGroup(Id);}" type="button">Delete</button></td> <td class="valign" data-bind="text: Name"></td>
</tr> <td class="valign" data-bind="text: UserCount"></td>
</tbody> <td><button class="btn btn-link" data-bind="enable: UserCount=='0', click: function(data, event){$root.deleteGroup(Id);}" type="button">Delete</button></td>
</table> </tr>
</div> </tbody>
<form method="post" action="#editgroup" data-bind="with: $root.groupEditItem(), css:{'col-md-4':!$root.groupFormHidden()}"> </table>
<input type="hidden" name="id" data-bind="value: Id"> </div>
<div class="form-group"> <form method="post" action="#editgroup" data-bind="with: $root.groupEditItem(), css:{'col-md-4':!$root.groupFormHidden()}">
<label for="groupNameEdit">Group Name</label> <input type="hidden" name="id" data-bind="value: Id">
<input for="Name" type="text" class="form-control" id="groupNameEdit" placeholder="Group Name" data-bind="value: Name"/> <div class="form-group">
</div> <label for="groupNameEdit">Group Name</label>
<button pageDestination="Users" class="btn btn-secondary" type="button" <input for="Name" type="text" class="form-control" id="groupNameEdit" placeholder="Group Name" data-bind="value: Name" />
data-bind="click: $root.hideGroupForm">Cancel</button> </div>
<button pageDestination="Users" class="btn btn-secondary" type="button"
<button type="submit" class="btn btn-primary">Submit</button> data-bind="click: $root.hideGroupForm">
</form> Cancel
</div> </button>
</div>
<div id="CardManagement" class="container"> <button type="submit" class="btn btn-primary">Submit</button>
<div class="row"> </form>
<h2 class="col-md-4">Unassigned Cards</h2> </div>
<button class="col-md-1 btn btn-default pull-right" style="margin-top: 20px;" data-bind="click: $root.clearUnassignedCards"> </div>
<span class="glyphicon glyphicon-trash"></span> <div id="CardManagement" class="container">
</button> <div class="row">
</div> <h2 class="col-md-4">Unassigned Cards</h2>
<div class="row"> <button class="col-md-1 btn btn-default pull-right" style="margin-top: 20px;" data-bind="click: $root.clearUnassignedCards">
<div data-bind="with: unassignedCardList"> <span class="glyphicon glyphicon-trash"></span>
<table class="table table-striped"> </button>
<thead> </div>
<tr> <div class="row">
<th>Card Unique Id</th> <div data-bind="with: unassignedCardList">
<th>Last Used Date</th> <table class="table table-striped">
</tr> <thead>
</thead> <tr>
<tbody data-bind="foreach: data"> <th>Card Unique Id</th>
<tr> <th>Last Used Date</th>
<td data-bind="text: UniqueId"></td> </tr>
<td data-bind="text: $root.convertToDisplayDateTime(LastUsed)"></td> </thead>
</tr> <tbody data-bind="foreach: data">
</tbody> <tr>
</table> <td data-bind="text: UniqueId"></td>
</div> <td data-bind="text: $root.convertToDisplayDateTime(LastUsed)"></td>
</div> </tr>
</div> </tbody>
<script src="Helpers.min.js" type="text/javascript"></script> </table>
<script src="admin.js" type="text/javascript"></script> </div>
</body> </div>
</div>
<div id="PolicyManagement" class="container">
<div id="row">
<ul class="nav nav-tabs">
<li class="active"><a href="#edit" data-toggle="tab" >Edit</a></li>
<li ><a href="#preview" data-toggle="tab" >Preview</a></li>
</ul>
<div class="tab-content" style="max-height: 500px; min-height: 400px;overflow-y: auto">
<div id="edit" class="tab-pane fade in active">
<textarea id="policyEditor"></textarea>
</div>
<div id="preview" class="tab-pane fade in">
<div data-bind="html: previewHtml" class="well-lg"></div>
</div>
</div>
<button class="btn btn-primary pull-right" data-bind="click: $root.policySave">Save</button>
<button class="btn btn-secondary pull-right" data-bind="click: $root.policyReload">Cancel</button>
</div>
</div>
<div id="saveDialog" class="modal fade" role="dialog" data-bind="with: policyData">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 data-bind="text: ApplicationName"></h4>
</div>
<div class="modal-body">
<table class="table">
<tr>
<th>Change Author</th>
<td data-bind="text: ChangeAuthor"></td>
</tr>
<tr>
<th>Change Date</th>
<td><input id="changeDatePicker" type="datetime" data-bind="value: ChangeDate"/></td>
</tr>
<tr>
<th>Version</th>
<td data-bind="text: Version"></td>
</tr>
<tr>
<th>Database Provider</th>
<td data-bind="text: DataBaseProvider"></td>
</tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script src="Helpers.min.js" type="text/javascript"></script>
<script src="js/easymde.min.js" type="text/javascript"></script>
<script src="js/highlightjs.min.js" type="text/javascript"></script>
<script src="js/htmlparser.min.js" type="text/javascript"></script>
<script src="js/marked.min.js"></script>
<script src="admin.js" type="text/javascript"></script>
</body>
</html> </html>

View File

@ -1,126 +1,219 @@
function AdminVM() { function AdminVM() {
var self = this; var self = this;
self.groupsList = ko.observable(null); self.groupsList = ko.observable(null);
self.groupEditItem = ko.observable(null); self.groupEditItem = ko.observable(null);
self.unassignedCardList = ko.observable(null); self.unassignedCardList = ko.observable(null);
self.helpers = new Helpers(); self.helpers = new Helpers();
self.uiPages = { self.policyMarkdown = "";
overview: "overview", self.previewHtml = ko.observable("");
group: "groups", self.renderer = new marked.Renderer();
home: function () { return this.overview; } self.renderer.blockquote = function(quote) {
}; return "<blockquote class=\"blockquote\">" + quote + "</blockquote>";
self.apiEndpoints = { };
deleteGroups:"/api/groups/delete", self.renderer.heading = function (text, level) {
getGroups: "/api/groups",
editGroup: "/api/groups/edit", var parserHandler = new Tautologistics.NodeHtmlParser.DefaultHandler(function (error) {
getUnassignedCards: "/api/cards/unassigned", if (error)
clearUnassignedCards: "/api/cards/unassigned" throw new Error("Cannot parse \"" + text + "\" in markdown file.");
}; });
self.clearGroupForm = function () { var parser = new Tautologistics.NodeHtmlParser.Parser(parserHandler);
self.helpers.goToMenuOption(self.uiPages.group);
self.groupEditItem(null); parser.parseComplete(text);
}; var escaped = "unknown";
self.hideGroupForm = function () {
self.groupEditItem(null); if (parserHandler.dom.length > 0) {
}; escaped = parserHandler.dom[0].raw.toLowerCase().trim().replace(/ /g, "-");
self.newGroupForm = function () { }
self.groupEditItem({ Id: -1, Name: "" });
self.helpers.goToMenuOption(self.uiPages.group); return "<h" + level + " id=\"" + escaped + "\">"
}; // NOTE: We're setting display none INLINE, so you have
self.groupFormHidden = ko.computed(function () { // to override with !important if you want it to show.
return self.groupEditItem() == null; + " <a class=\"heading-anchor\" style=\"display:none;\" href=\"#" + escaped + '">'
}, self); + " <i class=\"oi oi-link-intact\"></i>"
self.editGroupClick = function (data) { + " </a>"
self.helpers.goToMenuOption(self.uiPages.group); + text
self.groupEditItem(data); + "</h" + level + ">";
}; };
self.getGroups = function () { self.renderer.table = function(header, body) {
var url = self.helpers.createRequestUrl(self.apiEndpoints.getGroups, null, false); return "<table class=\"table\">" +
$.getJSON(url, function (res) { "<thead class=\"thead-default\">" +
self.groupsList(res); header +
}).fail(function (resp, status, error) { "</thead>" +
console.log("error - getGroups"); "<tbody>" +
var errorObj = self.helpers.processRequestFailure(resp, status, error); body +
}); "</tbody>" +
}; "</table>";
self.deleteGroup = function (groupId) { };
var url = self.helpers.createRequestUrl(self.apiEndpoints.deleteGroups, /*
[{ key: "groupId", value: groupId }], * Adds highlight.js classes to `code` blocks
false, */
false); self.renderer.code = function(code, language) {
$.ajax({
url: url, var valid = !!(language && hljs.getLanguage(language));
type: 'DELETE', var highlighted = valid ? hljs.highlight(language, code).value : code
success: function () {
console.log("deleted " + groupId); return "<pre><code class=\"hljs lang-" + language + "\">" + highlighted + "</code></pre>";
self.hideGroupForm(); };
self.helpers.goToMenuOption(self.uiPages.home()); self.editor = new EasyMDE({
} element: document.getElementById("policyEditor"),
}); showIcons: ["bold", "italic", "strikethrough", "heading", "heading-smaller", "heading-bigger", "heading-1", "heading-2", "heading-3", "code", "quote", "unordered-list", "ordered-list", "clean-block", "link", "table", "horizontal-rule", "guide", "table"],
console.log("delete: " + groupId); hideIcons: ["preview", "side-by-side", "fullscreen"],
}; renderingConfig: {
self.submitGroupEdit = function(group) { markedOptions: {
var url = self.helpers.createRequestUrl(self.apiEndpoints.editGroup, null, false); renderer: self.renderer
$.post(url, group, function () { }
}, "json") }
.done(function () { });
self.groupEditItem(null); self.editor.codemirror.on("changes",
self.helpers.goToMenuOption(self.uiPages.home()); function () {
}) self.previewHtml(self.editor.options.previewRender(self.editor.value()));
.fail(function (resp, status, error) { });
self.helpers.goToMenuOption(self.uiPages.home()); self.uiPages = {
var errorObj = self.helpers.processRequestFailure(resp, status, error); overview: "overview",
}); group: "groups",
}; home: function () { return this.overview; }
self.getUnassignedCardData = function() { };
var url = self.helpers.createRequestUrl(self.apiEndpoints.getUnassignedCards, null, false); self.apiEndpoints = {
$.getJSON(url, deleteGroups:"/api/groups/delete",
function(res) { getGroups: "/api/groups",
self.unassignedCardList(res); editGroup: "/api/groups/edit",
}).fail(function(resp, status, error) { getUnassignedCards: "/api/cards/unassigned",
console.log("error - getUnassignedCards"); clearUnassignedCards: "/api/cards/unassigned",
var errorObj = self.helpers.processRequestFailure(resp, status, error); getPolicy:"/api/app/policy",
}); savePolicy:"/api/app/policy"
}; };
self.clearUnassignedCards = function() { self.clearGroupForm = function () {
var url = self.helpers.createRequestUrl(self.apiEndpoints.clearUnassignedCards, null, false); self.helpers.goToMenuOption(self.uiPages.group);
$.ajax({ self.groupEditItem(null);
type: "DELETE", };
url: url, self.hideGroupForm = function () {
data: "", self.groupEditItem(null);
success: function() { };
self.getUnassignedCardData(); //update self.newGroupForm = function () {
}, self.groupEditItem({ Id: -1, Name: "" });
error: function(jqxhr, status, error) { self.helpers.goToMenuOption(self.uiPages.group);
console.log("error - clearUnassignedCards"); };
var errorObj = self.helpers.processRequestFailure(resp, status, error); self.groupFormHidden = ko.computed(function () {
} return self.groupEditItem() == null;
}); }, self);
}; self.editGroupClick = function (data) {
self.padNumber = function (number) { self.helpers.goToMenuOption(self.uiPages.group);
return (number < 10 ? "0" : "") + number; self.groupEditItem(data);
}; };
self.convertToDisplayDateTime = function (dateValue) { self.getGroups = function () {
var date = new Date(dateValue); // dd MM YY HH:mm:ss e.g.: 01 Mar 17 17:34:02 var url = self.helpers.createRequestUrl(self.apiEndpoints.getGroups, null, false);
return date.getDate() + " " $.getJSON(url, function (res) {
+ date.toLocaleString("en-us", { month: "long" }) + " " self.groupsList(res);
+ (date.getYear()-100) + " " }).fail(function (resp, status, error) {
+ self.padNumber(date.getHours()) + ":" console.log("error - getGroups");
+ self.padNumber(date.getMinutes()) + ":" var errorObj = self.helpers.processRequestFailure(resp, status, error);
+ self.padNumber(date.getSeconds()); });
}; };
Sammy(function () { self.deleteGroup = function (groupId) {
this.disable_push_state = true; var url = self.helpers.createRequestUrl(self.apiEndpoints.deleteGroups,
this.get("#overview", function () { [{ key: "groupId", value: groupId }],
self.getGroups(); false,
self.getUnassignedCardData(); false);
}); $.ajax({
this.post("#editgroup", function () { url: url,
self.submitGroupEdit(self.groupEditItem()); type: "DELETE",
return false; success: function () {
}); console.log("deleted " + groupId);
//default route (home page) self.hideGroupForm();
this.get("", function () { this.app.runRoute("get", "#" + self.uiPages.home()) }); self.helpers.goToMenuOption(self.uiPages.home());
}).run(); }
}; });
console.log("delete: " + groupId);
};
self.submitGroupEdit = function(group) {
var url = self.helpers.createRequestUrl(self.apiEndpoints.editGroup, null, false);
$.post(url, group, function () {
}, "json")
.done(function () {
self.groupEditItem(null);
self.helpers.goToMenuOption(self.uiPages.home());
})
.fail(function (resp, status, error) {
self.helpers.goToMenuOption(self.uiPages.home());
var errorObj = self.helpers.processRequestFailure(resp, status, error);
});
};
self.getUnassignedCardData = function() {
var url = self.helpers.createRequestUrl(self.apiEndpoints.getUnassignedCards, null, false);
$.getJSON(url,
function(res) {
self.unassignedCardList(res);
}).fail(function(resp, status, error) {
console.log("error - getUnassignedCards");
var errorObj = self.helpers.processRequestFailure(resp, status, error);
});
};
self.clearUnassignedCards = function() {
var url = self.helpers.createRequestUrl(self.apiEndpoints.clearUnassignedCards, null, false);
$.ajax({
type: "DELETE",
url: url,
data: "",
success: function() {
self.getUnassignedCardData(); //update
},
error: function(jqxhr, status, error) {
console.log("error - clearUnassignedCards");
var errorObj = self.helpers.processRequestFailure(resp, status, error);
}
});
};
self.padNumber = function (number) {
return (number < 10 ? "0" : "") + number;
};
self.policySave = function () {
var url = self.helpers.createRequestUrl(self.apiEndpoints.savePolicy, null, false);
var policy = { markdown: self.editor.value(), html: self.previewHtml() };
console.log(policy);
$.post(url, policy, function() {
}, "json")
.done(function() {
self.policyReload();
}).fail(function(resp, status, error) {
var errorObj = self.helpers.processRequestFailure(resp, status, error);
console.log(errorObj);
});
};
self.policyReload = function() {
var url = self.helpers.createRequestUrl(self.apiEndpoints.getPolicy,
[], false, false);
$.getJSON(url, function (res) {
console.log(res);
self.editor.value(res.Markdown);
self.previewHtml(res.Html);
}).fail(function (resp, status, error) {
console.log("error - policyReload");
var errObj = self.helpers.processRequestFailure(resp, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "policyReload");
});
};
self.convertToDisplayDateTime = function (dateValue) {
var date = new Date(dateValue); // dd MM YY HH:mm:ss e.g.: 01 Mar 17 17:34:02
return date.getDate() + " "
+ date.toLocaleString("en-us", { month: "long" }) + " "
+ (date.getYear()-100) + " "
+ self.padNumber(date.getHours()) + ":"
+ self.padNumber(date.getMinutes()) + ":"
+ self.padNumber(date.getSeconds());
};
Sammy(function () {
this.disable_push_state = true;
this.get("#overview", function () {
self.getGroups();
self.getUnassignedCardData();
self.policyReload();
});
this.post("#editgroup", function () {
self.submitGroupEdit(self.groupEditItem());
return false;
});
//default route (home page)
this.get("", function () { this.app.runRoute("get", "#" + self.uiPages.home()) });
}).run();
};
ko.applyBindings(new AdminVM()); ko.applyBindings(new AdminVM());