Browse Source

Edit save + git commit + push sync

pull/1/head
NGPixel 8 years ago
parent
commit
0f06ab6dc8
9 changed files with 170 additions and 34 deletions
  1. 4
      README.md
  2. 2
      assets/js/app.js
  3. 2
      client/js/app.js
  4. 16
      client/js/pages/edit.js
  5. 34
      controllers/pages.js
  6. 94
      models/entries.js
  7. 44
      models/git.js
  8. 2
      views/pages/edit.pug
  9. 6
      views/pages/view.pug

4
README.md

@ -4,13 +4,13 @@
# 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)
[![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/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)
[![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
*Under development*

2
assets/js/app.js

@ -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.")})}))});

2
client/js/app.js

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

16
client/js/pages/edit.js

@ -11,7 +11,21 @@ if($('#page-type-edit').length) {
$('.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.');
});
});

34
controllers/pages.js

@ -4,6 +4,13 @@ var express = require('express');
var router = express.Router();
var _ = require('lodash');
// ==========================================
// EDIT MODE
// ==========================================
/**
* Edit document in Markdown
*/
router.get('/edit/*', (req, res, next) => {
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) => {
res.send('CREATE MODE');
});
// ==========================================
// VIEW MODE
// ==========================================
/**
* Home
* View document
*/
router.get('/*', (req, res, next) => {

94
models/entries.js

@ -2,7 +2,7 @@
var Promise = require('bluebird'),
path = require('path'),
fs = Promise.promisifyAll(require("fs")),
fs = Promise.promisifyAll(require("fs-extra")),
_ = require('lodash'),
farmhash = require('farmhash'),
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
* @return {Object} Page Data
* @param {String} entryPath The entry path
* @return {Promise<Object>} Page Data
*/
fetch(entryPath) {
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 st.isFile();
@ -78,16 +78,16 @@ module.exports = {
/**
* Fetches the original document entry
*
* @param {String} entryPath The entry path
* @param {Object} options The options
* @return {Object} Page data
* @param {String} entryPath The entry path
* @param {Object} options The options
* @return {Promise<Object>} Page data
*/
fetchOriginal(entryPath, options) {
let self = this;
let fpath = path.join(self._repoPath, entryPath + '.md');
let cpath = path.join(self._cachePath, farmhash.fingerprint32(entryPath) + '.bson');
let fpath = self.getFullPath(entryPath);
let cpath = self.getCachePath(entryPath);
options = _.defaults(options, {
parseMarkdown: true,
@ -174,8 +174,8 @@ module.exports = {
/**
* Gets the parent information.
*
* @param {String} entryPath The entry path
* @return {Object|False} The parent information.
* @param {String} entryPath The entry path
* @return {Promise<Object|False>} The parent information.
*/
getParentInfo(entryPath) {
@ -183,10 +183,10 @@ module.exports = {
if(_.includes(entryPath, '/')) {
let parentParts = _.split(entryPath, '/');
let parentPath = _.join(_.initial(parentParts),'/');
let parentParts = _.initial(_.split(entryPath, '/'));
let parentPath = _.join(parentParts,'/');
let parentFile = _.last(parentParts);
let fpath = path.join(self._repoPath, parentPath + '.md');
let fpath = self.getFullPath(parentPath);
return fs.statAsync(fpath).then((st) => {
if(st.isFile()) {
@ -210,6 +210,70 @@ module.exports = {
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);
});
}
};

44
models/git.js

@ -131,6 +131,11 @@ module.exports = {
},
/**
* Sync with the remote repository
*
* @return {Promise} Resolve on sync success
*/
resync() {
let self = this;
@ -149,23 +154,20 @@ module.exports = {
// 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();
if(!_.includes(out, 'nothing to commit')) {
// Add, commit and push
if(_.includes(out, 'commit')) {
winston.info('[GIT] Performing push to remote repository...');
return self._git.add('-A').then(() => {
return self._git.commit("Resync");
}).then(() => {
return self._git.push('origin', self._repo.branch);
}).then(() => {
return self._git.push('origin', self._repo.branch).then(() => {
return winston.info('[GIT] Push completed.');
});
} else {
winston.info('[GIT] Repository is already up to date. Nothing to commit.');
winston.info('[GIT] Repository is already in sync.');
}
return true;
@ -178,6 +180,30 @@ module.exports = {
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);
});
}
};

2
views/pages/edit.pug

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

6
views/pages/view.pug

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