Edit save + git commit + push sync

This commit is contained in:
NGPixel 2016-08-29 01:21:35 -04:00
parent 8fbce25f5d
commit 0f06ab6dc8
9 changed files with 170 additions and 34 deletions

View File

@ -4,13 +4,13 @@
# Requarks Wiki # Requarks Wiki
[![Release](https://img.shields.io/github/release/Requarks/wiki.svg?maxAge=86400)](https://github.com/Requarks/wiki/releases)
[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg)](https://github.com/requarks/wiki/blob/master/LICENSE) [![License](https://img.shields.io/badge/license-AGPLv3-blue.svg)](https://github.com/requarks/wiki/blob/master/LICENSE)
[![Build Status](https://travis-ci.org/Requarks/wiki.svg?branch=master)](https://travis-ci.org/Requarks/wiki) [![Build Status](https://travis-ci.org/Requarks/wiki.svg?branch=master)](https://travis-ci.org/Requarks/wiki)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1d0217a3153c4595bdedb322263e55c8)](https://www.codacy.com/app/Requarks/wiki) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1d0217a3153c4595bdedb322263e55c8)](https://www.codacy.com/app/Requarks/wiki)
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/df3886d694254a248a7585a90bc5faed)](https://www.codacy.com/app/requarks/wiki)
[![Dependency Status](https://gemnasium.com/badges/github.com/Requarks/wiki.svg)](https://gemnasium.com/github.com/Requarks/wiki) [![Dependency Status](https://gemnasium.com/badges/github.com/Requarks/wiki.svg)](https://gemnasium.com/github.com/Requarks/wiki)
[![Known Vulnerabilities](https://snyk.io/test/github/requarks/wiki/badge.svg)](https://snyk.io/test/github/requarks/wiki) [![Known Vulnerabilities](https://snyk.io/test/github/requarks/wiki/badge.svg)](https://snyk.io/test/github/requarks/wiki)
[![Documentation](http://inch-ci.org/github/requarks/wiki.svg?branch=master)](https://requarks-wiki.readme.io/) [![Documentation](http://inch-ci.org/github/Requarks/wiki.svg?branch=master)](https://requarks-wiki.readme.io/)
##### A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown ##### A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown
*Under development* *Under development*

View File

@ -1 +1 @@
"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var a=t[n];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),Object.defineProperty(e,a.key,a)}}return function(t,n,a){return n&&e(t.prototype,n),a&&e(t,a),t}}(),Alerts=function(){function e(){_classCallCheck(this,e);var t=this;t.mdl=new Vue({el:"#alerts",data:{children:[]},methods:{acknowledge:function(e){t.close(e)}}}),t.uidNext=1}return _createClass(e,[{key:"push",value:function(e){var t=this,n=_.defaults(e,{_uid:t.uidNext,class:"is-info",message:"---",sticky:!1,title:"---"});t.mdl.children.push(n),n.sticky||_.delay(function(){t.close(n._uid)},5e3),t.uidNext++}},{key:"pushError",value:function(e,t){this.push({class:"is-danger",message:t,sticky:!1,title:e})}},{key:"pushSuccess",value:function(e,t){this.push({class:"is-success",message:t,sticky:!1,title:e})}},{key:"close",value:function(e){var t=this,n=_.findIndex(t.mdl.children,["_uid",e]),a=_.nth(t.mdl.children,n);n>=0&&a&&(a.class+=" exit",t.mdl.children.$set(n,a),_.delay(function(){t.mdl.children.$remove(a)},500))}}]),e}();jQuery(document).ready(function(e){e("a").smoothScroll({speed:400,offset:-20});new Sticky(".stickyscroll");e(window).bind("beforeunload",function(){e("#notifload").addClass("active")}),e(document).ajaxSend(function(){e("#notifload").addClass("active")}).ajaxComplete(function(){e("#notifload").removeClass("active")});var t=new Alerts;if(alertsData&&_.forEach(alertsData,function(e){t.push(e)}),1===e("#mk-editor").length){new SimpleMDE({autofocus:!0,autoDownloadFontAwesome:!1,element:e("#mk-editor").get(0),hideIcons:["heading","quote"],placeholder:"Enter Markdown formatted content here...",showIcons:["strikethrough","heading-1","heading-2","heading-3","code","table","horizontal-rule"],spellChecker:!1,status:!1})}e("#page-type-edit").length&&(e(".btn-edit-discard").on("click",function(t){e("#modal-edit-discard").toggleClass("is-active")}),e(".btn-edit-save").on("click",function(e){}))}); "use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var a=t[n];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),Object.defineProperty(e,a.key,a)}}return function(t,n,a){return n&&e(t.prototype,n),a&&e(t,a),t}}(),Alerts=function(){function e(){_classCallCheck(this,e);var t=this;t.mdl=new Vue({el:"#alerts",data:{children:[]},methods:{acknowledge:function(e){t.close(e)}}}),t.uidNext=1}return _createClass(e,[{key:"push",value:function(e){var t=this,n=_.defaults(e,{_uid:t.uidNext,class:"is-info",message:"---",sticky:!1,title:"---"});t.mdl.children.push(n),n.sticky||_.delay(function(){t.close(n._uid)},5e3),t.uidNext++}},{key:"pushError",value:function(e,t){this.push({class:"is-danger",message:t,sticky:!1,title:e})}},{key:"pushSuccess",value:function(e,t){this.push({class:"is-success",message:t,sticky:!1,title:e})}},{key:"close",value:function(e){var t=this,n=_.findIndex(t.mdl.children,["_uid",e]),a=_.nth(t.mdl.children,n);n>=0&&a&&(a.class+=" exit",t.mdl.children.$set(n,a),_.delay(function(){t.mdl.children.$remove(a)},500))}}]),e}();jQuery(document).ready(function(e){e("a").smoothScroll({speed:400,offset:-20});new Sticky(".stickyscroll");e(window).bind("beforeunload",function(){e("#notifload").addClass("active")}),e(document).ajaxSend(function(){e("#notifload").addClass("active")}).ajaxComplete(function(){e("#notifload").removeClass("active")});var t=new Alerts;if(alertsData&&_.forEach(alertsData,function(e){t.push(e)}),1===e("#mk-editor").length)var n=new SimpleMDE({autofocus:!0,autoDownloadFontAwesome:!1,element:e("#mk-editor").get(0),hideIcons:["heading","quote"],placeholder:"Enter Markdown formatted content here...",showIcons:["strikethrough","heading-1","heading-2","heading-3","code","table","horizontal-rule"],spellChecker:!1,status:!1});e("#page-type-edit").length&&(e(".btn-edit-discard").on("click",function(t){e("#modal-edit-discard").toggleClass("is-active")}),e(".btn-edit-save").on("click",function(a){e.ajax(window.location.href,{data:{markdown:n.value()},dataType:"json",method:"PUT"}).then(function(n,a,o){n.ok?window.location.assign("/"+e("#page-type-edit").data("entrypath")):t.pushError("Something went wrong",n.error)},function(e,n,a){t.pushError("Something went wrong","Save operation failed.")})}))});

View File

@ -39,7 +39,7 @@ jQuery( document ).ready(function( $ ) {
if($('#mk-editor').length === 1) { if($('#mk-editor').length === 1) {
let mde = new SimpleMDE({ var mde = new SimpleMDE({
autofocus: true, autofocus: true,
autoDownloadFontAwesome: false, autoDownloadFontAwesome: false,
element: $("#mk-editor").get(0), element: $("#mk-editor").get(0),

View File

@ -11,7 +11,21 @@ if($('#page-type-edit').length) {
$('.btn-edit-save').on('click', (ev) => { $('.btn-edit-save').on('click', (ev) => {
$.ajax(window.location.href, {
data: {
markdown: mde.value()
},
dataType: 'json',
method: 'PUT'
}).then((rData, rStatus, rXHR) => {
if(rData.ok) {
window.location.assign('/' + $('#page-type-edit').data('entrypath'));
} else {
alerts.pushError('Something went wrong', rData.error);
}
}, (rXHR, rStatus, err) => {
alerts.pushError('Something went wrong', 'Save operation failed.');
});
}); });

View File

@ -4,6 +4,13 @@ var express = require('express');
var router = express.Router(); var router = express.Router();
var _ = require('lodash'); var _ = require('lodash');
// ==========================================
// EDIT MODE
// ==========================================
/**
* Edit document in Markdown
*/
router.get('/edit/*', (req, res, next) => { router.get('/edit/*', (req, res, next) => {
let safePath = entries.parsePath(_.replace(req.path, '/edit', '')); let safePath = entries.parsePath(_.replace(req.path, '/edit', ''));
@ -30,12 +37,37 @@ router.get('/edit/*', (req, res, next) => {
}); });
router.put('/edit/*', (req, res, next) => {
let safePath = entries.parsePath(_.replace(req.path, '/edit', ''));
entries.update(safePath, req.body.markdown).then(() => {
res.json({
ok: true
});
}).catch((err) => {
res.json({
ok: false,
error: err.message
});
});
});
// ==========================================
// CREATE MODE
// ==========================================
router.get('/new/*', (req, res, next) => { router.get('/new/*', (req, res, next) => {
res.send('CREATE MODE'); res.send('CREATE MODE');
}); });
// ==========================================
// VIEW MODE
// ==========================================
/** /**
* Home * View document
*/ */
router.get('/*', (req, res, next) => { router.get('/*', (req, res, next) => {

View File

@ -2,7 +2,7 @@
var Promise = require('bluebird'), var Promise = require('bluebird'),
path = require('path'), path = require('path'),
fs = Promise.promisifyAll(require("fs")), fs = Promise.promisifyAll(require("fs-extra")),
_ = require('lodash'), _ = require('lodash'),
farmhash = require('farmhash'), farmhash = require('farmhash'),
BSONModule = require('bson'), BSONModule = require('bson'),
@ -34,16 +34,16 @@ module.exports = {
}, },
/** /**
* Fetch an entry from cache, otherwise the original * Fetch a document from cache, otherwise the original
* *
* @param {String} entryPath The entry path * @param {String} entryPath The entry path
* @return {Object} Page Data * @return {Promise<Object>} Page Data
*/ */
fetch(entryPath) { fetch(entryPath) {
let self = this; let self = this;
let cpath = path.join(self._cachePath, farmhash.fingerprint32(entryPath) + '.bson'); let cpath = self.getCachePath(entryPath);
return fs.statAsync(cpath).then((st) => { return fs.statAsync(cpath).then((st) => {
return st.isFile(); return st.isFile();
@ -78,16 +78,16 @@ module.exports = {
/** /**
* Fetches the original document entry * Fetches the original document entry
* *
* @param {String} entryPath The entry path * @param {String} entryPath The entry path
* @param {Object} options The options * @param {Object} options The options
* @return {Object} Page data * @return {Promise<Object>} Page data
*/ */
fetchOriginal(entryPath, options) { fetchOriginal(entryPath, options) {
let self = this; let self = this;
let fpath = path.join(self._repoPath, entryPath + '.md'); let fpath = self.getFullPath(entryPath);
let cpath = path.join(self._cachePath, farmhash.fingerprint32(entryPath) + '.bson'); let cpath = self.getCachePath(entryPath);
options = _.defaults(options, { options = _.defaults(options, {
parseMarkdown: true, parseMarkdown: true,
@ -174,8 +174,8 @@ module.exports = {
/** /**
* Gets the parent information. * Gets the parent information.
* *
* @param {String} entryPath The entry path * @param {String} entryPath The entry path
* @return {Object|False} The parent information. * @return {Promise<Object|False>} The parent information.
*/ */
getParentInfo(entryPath) { getParentInfo(entryPath) {
@ -183,10 +183,10 @@ module.exports = {
if(_.includes(entryPath, '/')) { if(_.includes(entryPath, '/')) {
let parentParts = _.split(entryPath, '/'); let parentParts = _.initial(_.split(entryPath, '/'));
let parentPath = _.join(_.initial(parentParts),'/'); let parentPath = _.join(parentParts,'/');
let parentFile = _.last(parentParts); let parentFile = _.last(parentParts);
let fpath = path.join(self._repoPath, parentPath + '.md'); let fpath = self.getFullPath(parentPath);
return fs.statAsync(fpath).then((st) => { return fs.statAsync(fpath).then((st) => {
if(st.isFile()) { if(st.isFile()) {
@ -210,6 +210,70 @@ module.exports = {
return Promise.reject(new Error('Parent entry is root.')); return Promise.reject(new Error('Parent entry is root.'));
} }
},
/**
* Gets the full original path of a document.
*
* @param {String} entryPath The entry path
* @return {String} The full path.
*/
getFullPath(entryPath) {
return path.join(this._repoPath, entryPath + '.md');
},
/**
* Gets the full cache path of a document.
*
* @param {String} entryPath The entry path
* @return {String} The full cache path.
*/
getCachePath(entryPath) {
return path.join(this._cachePath, farmhash.fingerprint32(entryPath) + '.bson');
},
/**
* Update an existing document
*
* @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
* @return {Promise<Boolean>} True on success, false on failure
*/
update(entryPath, contents) {
let self = this;
let fpath = self.getFullPath(entryPath);
return fs.statAsync(fpath).then((st) => {
if(st.isFile()) {
return self.makePersistent(entryPath, contents).then(() => {
return self.fetchOriginal(entryPath, {});
});
} else {
return Promise.reject(new Error('Entry does not exist!'));
}
}).catch((err) => {
return new Error('Entry does not exist!');
});
},
/**
* Makes a document persistent to disk and git repository
*
* @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
* @return {Promise<Boolean>} True on success, false on failure
*/
makePersistent(entryPath, contents) {
let self = this;
let fpath = self.getFullPath(entryPath);
return fs.outputFileAsync(fpath, contents).then(() => {
return git.commitDocument(entryPath);
});
} }
}; };

View File

@ -131,6 +131,11 @@ module.exports = {
}, },
/**
* Sync with the remote repository
*
* @return {Promise} Resolve on sync success
*/
resync() { resync() {
let self = this; let self = this;
@ -149,23 +154,20 @@ module.exports = {
// Check for changes // Check for changes
return self._git.exec('status').then((cProc) => { return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
let out = cProc.stdout.toString(); let out = cProc.stdout.toString();
if(!_.includes(out, 'nothing to commit')) {
// Add, commit and push if(_.includes(out, 'commit')) {
winston.info('[GIT] Performing push to remote repository...'); winston.info('[GIT] Performing push to remote repository...');
return self._git.add('-A').then(() => { return self._git.push('origin', self._repo.branch).then(() => {
return self._git.commit("Resync");
}).then(() => {
return self._git.push('origin', self._repo.branch);
}).then(() => {
return winston.info('[GIT] Push completed.'); return winston.info('[GIT] Push completed.');
}); });
} else { } else {
winston.info('[GIT] Repository is already up to date. Nothing to commit.');
winston.info('[GIT] Repository is already in sync.');
} }
return true; return true;
@ -178,6 +180,30 @@ module.exports = {
throw err; throw err;
}); });
},
/**
* Commits a document.
*
* @param {String} entryPath The entry path
* @return {Promise} Resolve on commit success
*/
commitDocument(entryPath) {
let self = this;
let gitFilePath = entryPath + '.md';
let commitMsg = '';
return self._git.exec('ls-files', gitFilePath).then((cProc) => {
let out = cProc.stdout.toString();
return _.includes(out, gitFilePath);
}).then((isTracked) => {
commitMsg = (isTracked) ? 'Updated ' + gitFilePath : 'Added ' + gitFilePath;
return self._git.add(gitFilePath);
}).then(() => {
return self._git.commit(commitMsg);
});
} }
}; };

View File

@ -21,7 +21,7 @@ block rootNavRight
block content block content
#page-type-edit #page-type-edit(data-entrypath=pageData.meta.path)
section.section.is-small section.section.is-small
textarea#mk-editor= pageData.markdown textarea#mk-editor= pageData.markdown

View File

@ -26,7 +26,7 @@ block rootNavRight
block content block content
#page-type-view #page-type-view(data-entrypath=pageData.meta.path)
section.section section.section
.container.is-fluid .container.is-fluid
.columns .columns
@ -70,9 +70,9 @@ block content
p.card-header-title Create New Page p.card-header-title Create New Page
.card-content .card-content
.content .content
label.label Enter the full path: label.label Enter the new document name:
p.control p.control
input.input(type='text', placeholder='/path', value='/storage/new-page') input.input(type='text', placeholder='page-name')
footer.card-footer footer.card-footer
a.card-footer-item(onclick='$(".modal").removeClass("is-active");') Discard a.card-footer-item(onclick='$(".modal").removeClass("is-active");') Discard
a.card-footer-item.featured Create a.card-footer-item.featured Create