Edit save + git commit + push sync
This commit is contained in:
parent
8fbce25f5d
commit
0f06ab6dc8
@ -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*
|
||||||
|
@ -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.")})}))});
|
@ -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),
|
||||||
|
@ -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.');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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) => {
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user