Edit save + git commit + push sync
This commit is contained in:
		| @@ -4,13 +4,13 @@ | ||||
|  | ||||
| # Requarks Wiki | ||||
|  | ||||
| [](https://github.com/Requarks/wiki/releases) | ||||
| [](https://github.com/requarks/wiki/blob/master/LICENSE) | ||||
| [](https://travis-ci.org/Requarks/wiki) | ||||
| [](https://www.codacy.com/app/Requarks/wiki) | ||||
| [](https://www.codacy.com/app/requarks/wiki) | ||||
| [](https://gemnasium.com/github.com/Requarks/wiki) | ||||
| [](https://snyk.io/test/github/requarks/wiki) | ||||
| [](https://requarks-wiki.readme.io/) | ||||
| [](https://requarks-wiki.readme.io/) | ||||
|  | ||||
| ##### A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown | ||||
| *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) { | ||||
|  | ||||
| 		let mde = new SimpleMDE({ | ||||
| 		var mde = new SimpleMDE({ | ||||
| 			autofocus: true, | ||||
| 			autoDownloadFontAwesome: false, | ||||
| 			element: $("#mk-editor").get(0), | ||||
|   | ||||
| @@ -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.'); | ||||
| 		}); | ||||
|  | ||||
| 	}); | ||||
|  | ||||
|   | ||||
| @@ -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) => { | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| 	 * @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(); | ||||
| @@ -80,14 +80,14 @@ module.exports = { | ||||
| 	 * | ||||
| 	 * @param      {String}           entryPath  The entry path | ||||
| 	 * @param      {Object}           options    The options | ||||
| 	 * @return     {Object}  Page data | ||||
| 	 * @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, | ||||
| @@ -175,7 +175,7 @@ module.exports = { | ||||
| 	 * Gets the parent information. | ||||
| 	 * | ||||
| 	 * @param      {String}                 entryPath  The entry path | ||||
| 	 * @return     {Object|False}  The parent information. | ||||
| 	 * @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); | ||||
| 		}); | ||||
|  | ||||
| 	} | ||||
|  | ||||
| }; | ||||
| @@ -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); | ||||
| 		}); | ||||
|  | ||||
| 	} | ||||
|  | ||||
| }; | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user