From 0f06ab6dc80dc5414cc3d468787df8f83f8578ea Mon Sep 17 00:00:00 2001 From: NGPixel Date: Mon, 29 Aug 2016 01:21:35 -0400 Subject: [PATCH] Edit save + git commit + push sync --- README.md | 4 +- assets/js/app.js | 2 +- client/js/app.js | 2 +- client/js/pages/edit.js | 16 ++++++- controllers/pages.js | 34 ++++++++++++++- models/entries.js | 94 ++++++++++++++++++++++++++++++++++------- models/git.js | 44 +++++++++++++++---- views/pages/edit.pug | 2 +- views/pages/view.pug | 6 +-- 9 files changed, 170 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index b159bc81..2c6311bf 100644 --- a/README.md +++ b/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* diff --git a/assets/js/app.js b/assets/js/app.js index 7cfc64f0..dcbe2116 100644 --- a/assets/js/app.js +++ b/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=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){}))}); \ No newline at end of file +"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=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.")})}))}); \ No newline at end of file diff --git a/client/js/app.js b/client/js/app.js index b1532f25..22100b72 100644 --- a/client/js/app.js +++ b/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), diff --git a/client/js/pages/edit.js b/client/js/pages/edit.js index 953e7f42..f8eddcf3 100644 --- a/client/js/pages/edit.js +++ b/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.'); + }); }); diff --git a/controllers/pages.js b/controllers/pages.js index 2605f9eb..0d89da7c 100644 --- a/controllers/pages.js +++ b/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) => { diff --git a/models/entries.js b/models/entries.js index e8cd9aa4..4cf005df 100644 --- a/models/entries.js +++ b/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} 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} 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} 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} 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} 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); + }); + } }; \ No newline at end of file diff --git a/models/git.js b/models/git.js index f4fdf0ff..cdb2f43d 100644 --- a/models/git.js +++ b/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); + }); + } }; \ No newline at end of file diff --git a/views/pages/edit.pug b/views/pages/edit.pug index e33a8002..6156d060 100644 --- a/views/pages/edit.pug +++ b/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 diff --git a/views/pages/view.pug b/views/pages/view.pug index 5e37cacc..50132318 100644 --- a/views/pages/view.pug +++ b/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