Commit 658c105a authored by Nicolas Giard's avatar Nicolas Giard Committed by Nick

feat: delete page

parent faa1f389
......@@ -29,6 +29,7 @@ npm-debug.log*
/repo
/data
/uploads
/content
*.sqlite
# IDE exclude
......
......@@ -5,8 +5,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [2.0.0-beta.12] - 2018-01-27
### Added
- Added Patreon link in Contribute admin page
- Added Theme Code Injection feature
- Added Theme Code Injection functionality
- Added Theme CSS Injection code minification
- Added Page Delete functionality
### Fixed
- Fixed root admin refresh token fail
......@@ -14,6 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Changed
- Moved Insert Media button in Markdown editor
- Use semver for DB migrations ordering
## [2.0.0-beta.11] - 2018-01-20
- First beta release
......
......@@ -35,9 +35,12 @@ docker-dev-rebuild: ## Rebuild dockerized dev image
docker-dev-clean: ## Clean DB, redis and data folders
rm -rf ./data
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec db psql --dbname=wiki --username=postgres --command='DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public'
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec db psql --dbname=wiki --username=wikijs --command='DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public'
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec redis redis-cli flushall
docker-dev-bash: ## Rebuild dockerized dev image
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec wiki bash
docker-build: ## Run assets generation build in docker
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . run wiki yarn build
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down
......
......@@ -203,7 +203,7 @@ export default {
<style lang='scss'>
.admin {
&.theme--light {
&.theme--light .application--wrap {
background-color: lighten(mc('grey', '200'), 2%);
}
}
......
......@@ -80,6 +80,20 @@ export default {
disabled: false
},
{
permission: 'read:source',
hint: 'Can view pages source, as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'read:history',
hint: 'Can view pages history, as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'read:assets',
hint: 'Can view / use assets (such as images and files), as specified in the Page Rules',
warning: false,
......
......@@ -206,6 +206,8 @@ export default {
{ text: 'Create Pages', value: 'write:pages', icon: 'insert_drive_file' },
{ text: 'Edit + Move Pages', value: 'manage:pages', icon: 'insert_drive_file' },
{ text: 'Delete Pages', value: 'delete:pages', icon: 'insert_drive_file' },
{ text: 'View Pages Source', value: 'read:source', icon: 'code' },
{ text: 'View Pages History', value: 'read:history', icon: 'restore' },
{ text: 'Read / Use Assets', value: 'read:assets', icon: 'camera' },
{ text: 'Upload Assets', value: 'write:assets', icon: 'camera' },
{ text: 'Edit + Delete Assets', value: 'manage:assets', icon: 'camera' },
......
......@@ -131,6 +131,7 @@
span Login
page-selector(mode='create', v-model='newPageModal', :open-handler='pageNewCreate')
page-delete(v-model='deletePageModal', v-if='path && path.length')
</template>
<script>
......@@ -139,6 +140,9 @@ import _ from 'lodash'
import Cookies from 'js-cookie'
export default {
components: {
PageDelete: () => import('./page-delete.vue')
},
props: {
dense: {
type: Boolean,
......@@ -155,7 +159,8 @@ export default {
searchIsLoading: false,
searchIsShown: true,
search: '',
newPageModal: false
newPageModal: false,
deletePageModal: false
}
},
computed: {
......@@ -233,11 +238,7 @@ export default {
})
},
pageDelete () {
this.$store.commit('showNotification', {
style: 'indigo',
message: `Coming soon...`,
icon: 'directions_boat'
})
this.deletePageModal = true
},
assets () {
this.$store.commit('showNotification', {
......
<template lang='pug'>
v-dialog(v-model='isShown', max-width='550', persistent)
v-card.wiki-form
.dialog-header.is-short.is-red
v-icon.mr-2(color='white') highlight_off
span Delete Page
v-card-text
.body-2 Are you sure you want to delete page #[span.red--text.text--darken-2 {{pageTitle}}]?
.caption The page can be restored from the administration area.
v-chip.mt-3.ml-0.mr-1(label, color='red lighten-4', disabled, small)
.caption.red--text.text--darken-2 {{pageLocale.toUpperCase()}}
v-chip.mt-3.mx-0(label, color='red lighten-5', disabled, small)
span.red--text.text--darken-2 /{{pagePath}}
v-card-chin
v-spacer
v-btn(flat, @click='discard', :disabled='loading') Cancel
v-btn(color='red darken-2', @click='deletePage', :loading='loading').white--text Delete
</template>
<script>
import _ from 'lodash'
import { get } from 'vuex-pathify'
import deletePageMutation from 'gql/common/common-pages-mutation-delete.gql'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
},
pageTitle: get('page/title'),
pagePath: get('page/path'),
pageLocale: get('page/locale'),
pageId: get('page/id')
},
watch: {
isShown(newValue, oldValue) {
if (newValue) {
document.body.classList.add('page-deleted-pending')
}
}
},
methods: {
discard() {
document.body.classList.remove('page-deleted-pending')
this.isShown = false
},
async deletePage() {
this.loading = true
this.$store.commit(`loadingStart`, 'page-delete')
this.$nextTick(async () => {
try {
const resp = await this.$apollo.mutate({
mutation: deletePageMutation,
variables: {
id: this.pageId
}
})
if (_.get(resp, 'data.pages.delete.responseResult.succeeded', false)) {
this.isShown = false
_.delay(() => {
document.body.classList.add('page-deleted')
_.delay(() => {
window.location.assign('/')
}, 1200)
}, 400)
} else {
throw new Error(_.get(resp, 'data.pages.delete.responseResult.message', 'An unexpected error occured.'))
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.$store.commit(`loadingStop`, 'page-delete')
this.loading = false
})
}
}
}
</script>
<style lang='scss'>
body.page-deleted-pending {
.application {
background-color: mc('grey', '900');
}
.application--wrap {
transform: translateZ(-5vw) rotateX(2deg);
border-radius: 7px;
overflow: hidden;
}
}
body.page-deleted {
.application--wrap {
transform: translateZ(-1000vw) rotateX(60deg);
opacity: 0;
}
}
</style>
......@@ -14,7 +14,7 @@
outline
color='blue'
@click.native.stop='openPropsModal'
:class='{ "is-icon": $vuetify.breakpoint.mdAndDown, "mx-0": !welcomeMode, "ml-0": !welcomeMode }'
:class='{ "is-icon": $vuetify.breakpoint.mdAndDown, "mx-0": !welcomeMode, "ml-0": welcomeMode }'
)
v-icon(color='blue', :left='$vuetify.breakpoint.lgAndUp') sort_by_alpha
span.white--text(v-if='$vuetify.breakpoint.lgAndUp') {{ $t('editor:page') }}
......@@ -282,6 +282,10 @@ export default {
.editor {
background-color: mc('grey', '900') !important;
min-height: 100vh;
.application--wrap {
background-color: mc('grey', '900');
}
}
.atom-spinner.is-inline {
......
mutation($id: Int!) {
pages {
delete(id: $id) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
html {
box-sizing: border-box;
height: 100%;
perspective: 50vw;
background-color: mc('grey', '900');
}
*, *:before, *:after {
box-sizing: inherit;
......@@ -19,6 +21,17 @@ html {
}
}
.application--wrap {
transition: all 1.2s ease;
transform-style: preserve-3d;
transform-origin: 50% 50%;
background-color: #FFF;
@at-root .theme--dark & {
background-color: mc('grey', '900');
}
}
@for $i from 0 through 25 {
.radius-#{$i} {
......
......@@ -132,6 +132,10 @@ export default {
StatusIndicator
},
props: {
pageId: {
type: Number,
default: 0
},
locale: {
type: String,
default: 'en'
......@@ -229,6 +233,7 @@ export default {
this.$store.commit('page/SET_CREATED_AT', this.createdAt)
this.$store.commit('page/SET_DESCRIPTION', this.description)
this.$store.commit('page/SET_IS_PUBLISHED', this.isPublished)
this.$store.commit('page/SET_ID', this.pageId)
this.$store.commit('page/SET_LOCALE', this.locale)
this.$store.commit('page/SET_PATH', this.path)
this.$store.commit('page/SET_TAGS', this.tags)
......
......@@ -5,7 +5,7 @@ FROM node:10-alpine
LABEL maintainer "requarks.io"
RUN apk update && \
apk add bash curl git python make g++ --no-cache && \
apk add bash curl git python make g++ nano --no-cache && \
mkdir -p /wiki
WORKDIR /wiki
......
# -- DEV DOCKER-COMPOSE --
# -- DO NOT USE IN PRODUCTION! --
version: "3"
services:
......
......@@ -5,6 +5,8 @@ const Promise = require('bluebird')
const Knex = require('knex')
const Objection = require('objection')
const migrationSource = require('../db/migrator-source')
/* global WIKI */
/**
......@@ -89,12 +91,14 @@ module.exports = {
// Set init tasks
console.info(migrationSource)
let initTasks = {
// -> Migrate DB Schemas
async syncSchemas() {
return self.knex.migrate.latest({
directory: path.join(WIKI.SERVERPATH, 'db/migrations'),
tableName: 'migrations'
tableName: 'migrations',
migrationSource
})
}
}
......
const _ = require('lodash')
const cfgHelper = require('../helpers/config')
const Promise = require('bluebird')
const fs = require('fs-extra')
const path = require('path')
/* global WIKI */
......@@ -22,6 +24,9 @@ module.exports = {
}
})
// Clear content cache
fs.emptyDir(path.join(WIKI.ROOTPATH, 'data/cache'))
return this
},
/**
......
exports.up = knex => {
return knex.schema
.table('pageHistory', table => {
table.string('action').defaultTo('updated')
table.dropForeign('pageId')
})
}
exports.down = knex => {
return knex.schema
.table('pageHistory', table => {
table.dropColumn('action')
table.integer('pageId').unsigned().references('id').inTable('pages')
})
}
const path = require('path')
const fs = require('fs-extra')
const semver = require('semver')
/* global WIKI */
module.exports = {
/**
* Gets the migration names
* @returns Promise<string[]>
*/
async getMigrations() {
const absoluteDir = path.join(WIKI.SERVERPATH, 'db/migrations')
const migrationFiles = await fs.readdirAsync(absoluteDir)
return migrationFiles.sort(semver.compare).map(m => ({
file: m,
directory: absoluteDir
}))
},
getMigrationName(migration) {
return migration.file;
},
getMigration(migration) {
return require(path.join(WIKI.SERVERPATH, 'db/migrations', migration.file));
}
}
......@@ -29,8 +29,11 @@ module.exports = {
page
}
},
async delete(obj, args) {
await WIKI.models.groups.query().deleteById(args.id)
async delete(obj, args, context) {
await WIKI.models.pages.deletePage({
...args,
authorId: context.req.user.id
})
return {
responseResult: graphHelper.generateSuccess('Page has been deleted.')
}
......
......@@ -99,7 +99,8 @@ module.exports = class PageHistory extends Model {
path: opts.path,
publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '',
title: opts.title
title: opts.title,
action: opts.action || 'updated'
})
}
......@@ -109,6 +110,7 @@ module.exports = class PageHistory extends Model {
'pageHistory.id',
'pageHistory.path',
'pageHistory.authorId',
'pageHistory.action',
'pageHistory.createdAt',
{
authorName: 'author.name'
......@@ -130,6 +132,7 @@ module.exports = class PageHistory extends Model {
'pageHistory.id',
'pageHistory.path',
'pageHistory.authorId',
'pageHistory.action',
'pageHistory.createdAt',
{
authorName: 'author.name'
......
......@@ -96,6 +96,7 @@ module.exports = class Page extends Model {
static get cacheSchema() {
return new JSBinType({
id: 'uint',
authorId: 'uint',
authorName: 'string',
createdAt: 'string',
......@@ -150,7 +151,10 @@ module.exports = class Page extends Model {
if (!ogPage) {
throw new Error('Invalid Page Id')
}
await WIKI.models.pageHistory.addVersion(ogPage)
await WIKI.models.pageHistory.addVersion({
...ogPage,
action: 'updated'
})
await WIKI.models.pages.query().patch({
authorId: opts.authorId,
content: opts.content,
......@@ -174,6 +178,23 @@ module.exports = class Page extends Model {
return page
}
static async deletePage(opts) {
const page = await WIKI.models.pages.query().findById(opts.id)
if (!page) {
throw new Error('Invalid Page Id')
}
await WIKI.models.pageHistory.addVersion({
...page,
action: 'deleted'
})
await WIKI.models.pages.query().delete().where('id', page.id)
await WIKI.models.pages.deletePageFromCache(page)
await WIKI.models.storage.pageEvent({
event: 'deleted',
page
})
}
static async renderPage(page) {
const pipeline = await WIKI.models.renderers.getRenderingPipeline(page.contentType)
WIKI.queue.job.renderPage.add({
......@@ -232,6 +253,7 @@ module.exports = class Page extends Model {
static async savePageToCache(page) {
const cachePath = path.join(process.cwd(), `data/cache/${page.hash}.bin`)
await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({
id: page.id,
authorId: page.authorId,
authorName: page.authorName,
createdAt: page.createdAt,
......@@ -270,4 +292,8 @@ module.exports = class Page extends Model {
throw err
}
}
static async deletePageFromCache(page) {
return fs.remove(path.join(process.cwd(), `data/cache/${page.hash}.bin`))
}
}
......@@ -20,6 +20,7 @@ block body
:author-id=page.authorId
:is-published=page.isPublished.toString()
:toc=page.toc
:page-id=page.id
)
template(slot='sidebar')
each navItem in sidebar
......
This diff was suppressed by a .gitattributes entry.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment