diff --git a/client/js/app.js b/client/js/app.js
index 7c4b5899..9a9c3b1b 100644
--- a/client/js/app.js
+++ b/client/js/app.js
@@ -38,6 +38,7 @@ import historyComponent from './components/history.vue'
import loadingSpinnerComponent from './components/loading-spinner.vue'
import modalCreatePageComponent from './components/modal-create-page.vue'
import modalCreateUserComponent from './components/modal-create-user.vue'
+import modalDeletePageComponent from './components/modal-delete-page.vue'
import modalDeleteUserComponent from './components/modal-delete-user.vue'
import modalDiscardPageComponent from './components/modal-discard-page.vue'
import modalMovePageComponent from './components/modal-move-page.vue'
@@ -86,6 +87,7 @@ Vue.component('history', historyComponent)
Vue.component('loadingSpinner', loadingSpinnerComponent)
Vue.component('modalCreatePage', modalCreatePageComponent)
Vue.component('modalCreateUser', modalCreateUserComponent)
+Vue.component('modalDeletePage', modalDeletePageComponent)
Vue.component('modalDeleteUser', modalDeleteUserComponent)
Vue.component('modalDiscardPage', modalDiscardPageComponent)
Vue.component('modalMovePage', modalMovePageComponent)
diff --git a/client/js/components/modal-delete-page.vue b/client/js/components/modal-delete-page.vue
new file mode 100644
index 00000000..e188e794
--- /dev/null
+++ b/client/js/components/modal-delete-page.vue
@@ -0,0 +1,66 @@
+
+ transition(:duration="400")
+ .modal(v-show='isShown', v-cloak)
+ transition(name='modal-background')
+ .modal-background(v-show='isShown')
+ .modal-container
+ transition(name='modal-content')
+ .modal-content(v-show='isShown')
+ header.is-red
+ span {{ $t('modal.deletepagetitle') }}
+ p.modal-notify(v-bind:class='{ "is-active": isLoading }'): i
+ section
+ span {{ $t('modal.deletepagewarning') }}
+ footer
+ a.button.is-grey.is-outlined(v-on:click='discard') {{ $t('modal.discard') }}
+ a.button.is-red(v-on:click='deletePage') {{ $t('modal.delete') }}
+
+
+
+
diff --git a/client/js/store/index.js b/client/js/store/index.js
index cf151045..471c9d87 100644
--- a/client/js/store/index.js
+++ b/client/js/store/index.js
@@ -10,6 +10,7 @@ import editorVideo from './modules/editor-video'
import modalCreatePage from './modules/modal-create-page'
import modalCreateUser from './modules/modal-create-user'
import modalDeleteUser from './modules/modal-delete-user'
+import modalDeletePage from './modules/modal-delete-page'
import modalDiscardPage from './modules/modal-discard-page'
import modalMovePage from './modules/modal-move-page'
import modalProfile2fa from './modules/modal-profile-2fa'
@@ -39,6 +40,7 @@ export default new Vuex.Store({
editorVideo,
modalCreatePage,
modalCreateUser,
+ modalDeletePage,
modalDeleteUser,
modalDiscardPage,
modalMovePage,
diff --git a/client/js/store/modules/modal-delete-page.js b/client/js/store/modules/modal-delete-page.js
new file mode 100644
index 00000000..7dc8d763
--- /dev/null
+++ b/client/js/store/modules/modal-delete-page.js
@@ -0,0 +1,14 @@
+export default {
+ namespaced: true,
+ state: {
+ shown: false
+ },
+ getters: {},
+ mutations: {
+ shownChange: (state, shownState) => { state.shown = shownState }
+ },
+ actions: {
+ open({ commit }) { commit('shownChange', true) },
+ close({ commit }) { commit('shownChange', false) }
+ }
+}
diff --git a/client/scss/components/nav.scss b/client/scss/components/nav.scss
index c5a76ff3..0590ec37 100644
--- a/client/scss/components/nav.scss
+++ b/client/scss/components/nav.scss
@@ -173,7 +173,11 @@
&.is-outlined {
background-color: mc($primary, '500');
color: mc($primary, '100');
- }
+ }
+
+ &.is-icon-only i {
+ margin-right: 0;
+ }
&:hover {
background-color: mc($primary, '700');
diff --git a/server/controllers/pages.js b/server/controllers/pages.js
index 951294e3..c974112c 100644
--- a/server/controllers/pages.js
+++ b/server/controllers/pages.js
@@ -288,4 +288,29 @@ router.put('/*', (req, res, next) => {
})
})
+/**
+ * Delete document
+ */
+router.delete('/*', (req, res, next) => {
+ if (!res.locals.rights.write) {
+ return res.json({
+ ok: false,
+ error: lang.t('errors:forbidden')
+ })
+ }
+
+ let safePath = entryHelper.parsePath(req.path)
+
+ entries.remove(safePath, req.user).then(() => {
+ res.json({
+ ok: true
+ })
+ }).catch((err) => {
+ res.json({
+ ok: false,
+ error: err.message
+ })
+ })
+})
+
module.exports = router
diff --git a/server/libs/entries.js b/server/libs/entries.js
index a5e7b830..af6f1104 100644
--- a/server/libs/entries.js
+++ b/server/libs/entries.js
@@ -388,6 +388,32 @@ module.exports = {
})
},
+ /**
+ * Delete a document
+ *
+ * @param {String} entryPath The current entry path
+ * @param {Object} author The author user object
+ * @return {Promise} Promise of the operation
+ */
+ remove(entryPath, author) {
+ if (_.isEmpty(entryPath) || entryPath === 'home') {
+ return Promise.reject(new Error(lang.t('errors:invalidpath')))
+ }
+
+ return git.deleteDocument(entryPath, author).then(() => {
+ // Delete old cache version
+
+ let oldEntryCachePath = entryHelper.getCachePath(entryPath)
+ fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true }) // eslint-disable-line handle-callback-err
+
+ // Delete old index entry
+ search.delete(entryPath)
+
+ // Delete entry
+ return db.Entry.deleteOne({ _id: entryPath })
+ })
+ },
+
/**
* Generate a starter page content based on the entry path
*
diff --git a/server/libs/git.js b/server/libs/git.js
index 805acb55..7787f37a 100644
--- a/server/libs/git.js
+++ b/server/libs/git.js
@@ -245,6 +245,29 @@ module.exports = {
})
},
+ /**
+ * Delete a document.
+ *
+ * @param {String} entryPath The entry path
+ * @return {Promise} Resolve on success
+ */
+ deleteDocument(entryPath, author) {
+ let self = this
+ let gitFilePath = entryPath + '.md'
+
+ return this._git.exec('rm', [gitFilePath]).then((cProc) => {
+ let out = cProc.stdout.toString()
+ if (_.includes(out, 'fatal')) {
+ let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
+ throw new Error(errorMsg)
+ }
+ let commitUsr = securityHelper.sanitizeCommitUser(author)
+ return self._git.exec('commit', ['-m', lang.t('git:deleted', { path: gitFilePath }), '--author="' + commitUsr.name + ' <' + commitUsr.email + '>"']).catch((err) => {
+ if (_.includes(err.stdout, 'nothing to commit')) { return true }
+ })
+ })
+ },
+
/**
* Commits uploads changes.
*
diff --git a/server/locales/en/browser.json b/server/locales/en/browser.json
index 9d4e4f93..651fbd5f 100644
--- a/server/locales/en/browser.json
+++ b/server/locales/en/browser.json
@@ -77,6 +77,8 @@
"delete": "Delete",
"deletefiletitle": "Delete?",
"deletefilewarn": "Are you sure you want to delete",
+ "deletepagewarning": "Are you sure you want to delete this page? This action cannot be undone!",
+ "deletepagetitle": "Delete this page?",
"deleteusertitle": "Delete User Account?",
"deleteuserwarning": "Are you sure you want to delete this user account? This action cannot be undone!",
"discard": "Discard",
@@ -113,4 +115,4 @@
"placeholder": "Search...",
"results": "Search Results"
}
-}
\ No newline at end of file
+}
diff --git a/server/views/pages/view.pug b/server/views/pages/view.pug
index 773f0b66..76462c38 100644
--- a/server/views/pages/view.pug
+++ b/server/views/pages/view.pug
@@ -12,6 +12,8 @@ block rootNavRight
loading-spinner
.nav-item
if rights.write && pageData.meta.path !== 'home'
+ a.button.is-outlined.is-icon-only(@click='$store.dispatch("modalDeletePage/open")')
+ i.nc-icon-outline.ui-1_trash
a.button.is-outlined(v-on:click='$store.dispatch("modalMovePage/open")')
i.nc-icon-outline.arrows-1_shuffle-98
span= t('nav.move')
@@ -83,4 +85,5 @@ block content
modal-create-page(basepath=pageData.meta.path)
modal-move-page(current-path=pageData.meta.path)
+ modal-delete-page(current-path=pageData.meta.path)
anchor
diff --git a/yarn.lock b/yarn.lock
index 5c7a6749..65a2b572 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5137,7 +5137,7 @@ onetime@^2.0.0:
dependencies:
mimic-fn "^1.0.0"
-opencollective@^1.0.3:
+opencollective@^1.0.3, opencollective@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/opencollective/-/opencollective-1.0.3.tgz#aee6372bc28144583690c3ca8daecfc120dd0ef1"
dependencies: