Browse Source

feat: unsaved changes prompt + beta preparations UI

pull/760/head
Nicolas Giard 5 years ago
parent
commit
8c6aca6623
16 changed files with 272 additions and 232 deletions
  1. 48
      client/components/admin/admin-general.vue
  2. 2
      client/components/admin/admin-groups.vue
  3. 8
      client/components/admin/admin-pages.vue
  4. 1
      client/components/admin/admin-search.vue
  5. 95
      client/components/admin/admin-users-authorize.vue
  6. 48
      client/components/admin/admin-users-create.vue
  7. 14
      client/components/admin/admin-users.vue
  8. 123
      client/components/editor.vue
  9. 103
      client/components/editor/editor-modal-editorselect.vue
  10. 40
      client/components/editor/editor-modal-unsaved.vue
  11. 6
      client/graph/admin/site/site-mutation-save-config.gql
  12. 3
      client/graph/admin/site/site-query-config.gql
  13. 1
      client/store/editor.js
  14. 3
      server/graph/resolvers/site.js
  15. 6
      server/graph/schemas/site.graphql
  16. 3
      server/setup.js

48
client/components/admin/admin-general.vue

@ -61,36 +61,41 @@
persistent-hint
)
v-divider
v-subheader Analytics
v-subheader Analytics #[v-chip.ml-2(label, color='grey', small, outline) coming soon]
.px-3.pb-3
v-text-field(
v-select.mt-2(
outline
label='Analytics Service Provider'
:items='analyticsServices'
v-model='config.analyticsService'
prepend-icon='timeline'
)
v-text-field.mt-2(
v-if='config.analyticsService !== ``'
outline
label='Google Analytics ID'
label='Property Tracking ID'
:counter='255'
v-model='config.ga'
v-model='config.analyticsId'
prepend-icon='timeline'
persistent-hint
hint='Property tracking ID for Google Analytics. Leave empty to disable.'
hint='A unique identifier provided by your analytics service provider.'
)
v-flex(lg6 xs12)
v-card.wiki-form
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title
.subheading {{ $t('admin:general.siteBranding') }}
v-spacer
v-chip(label, color='white', small).primary--text coming soon
v-subheader Logo
v-subheader Logo #[v-chip.ml-2(label, color='grey', small, outline) coming soon]
v-card-text
v-layout.px-3(row, align-center)
v-avatar(size='120', :color='$vuetify.dark ? `grey darken-2` : `grey lighten-3`', :tile='config.logoIsSquare')
.ml-4
v-layout(row, align-center)
v-btn(color='teal', depressed, dark)
v-icon(left) cloud_upload
span Upload Logo
v-btn(color='teal', depressed, disabled)
v-icon(left) clear
span Clear
v-btn.mx-0(color='teal', depressed, disabled)
v-icon(left) cloud_upload
span Upload Logo
v-btn(color='teal', depressed, disabled)
v-icon(left) clear
span Clear
.caption.grey--text An image of 120x120 pixels is recommended for best results.
.caption.grey--text SVG, PNG or JPG files only.
v-switch(
@ -117,6 +122,8 @@
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title
.subheading Features
v-spacer
v-chip(label, color='white', small).primary--text coming soon
v-card-text
v-switch(
label='Page Ratings'
@ -153,6 +160,11 @@ import siteUpdateConfigMutation from 'gql/admin/site/site-mutation-save-config.g
export default {
data() {
return {
analyticsServices: [
{ text: 'None', value: '' },
{ text: 'Google Analytics', value: 'ga' },
{ text: 'Google Tag Manager', value: 'gtm' },
],
metaRobots: [
{ text: 'Index', value: 'index' },
{ text: 'Follow', value: 'follow' },
@ -164,7 +176,8 @@ export default {
title: '',
description: '',
robots: [],
ga: '',
analyticsService: '',
analyticsId: '',
company: '',
hasLogo: false,
logoIsSquare: false,
@ -189,7 +202,8 @@ export default {
title: this.config.title || '',
description: this.config.description || '',
robots: this.config.robots || [],
ga: this.config.ga || '',
analyticsService: this.config.analyticsService || '',
analyticsId: this.config.analyticsId || '',
company: this.config.company || '',
hasLogo: this.config.hasLogo || false,
logoIsSquare: this.config.logoIsSquare || false,

2
client/components/admin/admin-groups.vue

@ -54,7 +54,7 @@
span System Group
template(slot='no-data')
v-alert.ma-3(icon='warning', :value='true', outline) No groups to display.
.text-xs-center.py-2(v-if='groups.length > 15')
.text-xs-center.py-2(v-if='this.pages > 0')
v-pagination(v-model='pagination.page', :length='pages')
</template>

8
client/components/admin/admin-pages.vue

@ -6,11 +6,11 @@
img(src='/svg/icon-file.svg', alt='Page', style='width: 80px;')
.admin-header-title
.headline.blue--text.text--darken-2 Pages
.subheading.grey--text Manage pages
.subheading.grey--text Manage pages #[v-chip(label, color='primary', small).white--text coming soon]
v-spacer
v-btn(color='grey', outline, @click='refresh', large)
v-btn(color='grey', outline, @click='refresh', large, disabled)
v-icon.grey--text refresh
v-btn(color='primary', depressed, large, @click='newpage')
v-btn(color='primary', depressed, large, @click='newpage', disabled)
v-icon(left) add
span New Page
v-card.mt-3
@ -31,7 +31,7 @@
td {{ props.item.updatedAt | moment('calendar') }}
template(slot='no-data')
v-alert.ma-3(icon='warning', :value='true', outline) No pages to display.
.text-xs-center.py-2(v-if='groups.length > 15')
.text-xs-center.py-2(v-if='this.pages > 0')
v-pagination(v-model='pagination.page', :length='pages')
page-selector(v-model='pageSelectorShown', mode='new')

1
client/components/admin/admin-search.vue

@ -35,6 +35,7 @@
:value='engine.key'
color='primary'
hide-details
disabled
)
v-tab-item(v-for='(engine, n) in activeEngines', :key='engine.key', :transition='false', :reverse-transition='false')

95
client/components/admin/admin-users-authorize.vue

@ -1,95 +0,0 @@
<template lang="pug">
v-dialog(v-model='isShown', max-width='550')
v-card.wiki-form
.dialog-header.is-short
span Authorize Social User
v-spacer
v-chip(label, color='white', small).primary--text coming soon
v-card-text
v-alert.mb-4.deep-orange.lighten-5.radius-7(
v-if='providers.length < 1'
color='deep-orange'
icon='warning'
outline
:value='true'
) You must enable at least 1 social strategy first.
v-select.md2(
:items='providers'
item-text='title'
item-value='key'
outline
prepend-icon='business'
v-model='provider'
label='Provider'
)
v-text-field.md2(
outline
prepend-icon='email'
v-model='email'
label='Email Address'
ref='emailInput'
)
v-text-field.md2(
outline
prepend-icon='person'
v-model='name'
label='Name'
)
v-card-chin
v-spacer
v-btn(flat, @click='isShown = false') Cancel
v-btn(color='primary', @click='authorizeUser', :disabled='providers.length < 1 || true') Authorize
</template>
<script>
import _ from 'lodash'
import providersQuery from 'gql/admin/users/users-query-strategies.gql'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
providers: [],
provider: '',
email: '',
name: ''
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
}
},
watch: {
value(newValue, oldValue) {
if (newValue) {
this.$nextTick(() => {
this.$refs.emailInput.focus()
})
}
}
},
methods: {
async authorizeUser() {
}
},
apollo: {
providers: {
query: providersQuery,
fetchPolicy: 'network-only',
update: (data) => _.reject(data.authentication.strategies, ['key', 'local']),
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')
}
}
}
}
</script>

48
client/components/admin/admin-users-create.vue

@ -1,8 +1,18 @@
<template lang="pug">
v-dialog(v-model='isShown', max-width='550')
v-card.wiki-form
.dialog-header.is-short New Local User
.dialog-header.is-short
span New User
v-card-text
v-select.md2(
:items='providers'
item-text='title'
item-value='key'
outline
prepend-icon='business'
v-model='provider'
label='Provider'
)
v-text-field.md2(
outline
prepend-icon='email'
@ -11,12 +21,7 @@
ref='emailInput'
)
v-text-field.md2(
outline
prepend-icon='person'
v-model='name'
label='Name'
)
v-text-field.md2(
v-if='provider === `local`'
outline
prepend-icon='lock'
append-icon='casino'
@ -25,15 +30,24 @@
counter='255'
@click:append='generatePwd'
)
v-text-field.md2(
outline
prepend-icon='person'
v-model='name'
label='Name'
)
v-card-chin
v-spacer
v-btn(flat, @click='isShown = false') Cancel
v-btn(color='primary', @click='createUser') Create User
v-btn(color='primary', @click='newUser') Create
</template>
<script>
import _ from 'lodash'
import uuidv4 from 'uuid/v4'
import providersQuery from 'gql/admin/users/users-query-strategies.gql'
export default {
props: {
value: {
@ -43,11 +57,11 @@ export default {
},
data() {
return {
providers: [],
provider: 'local',
email: '',
name: '',
password: '',
jobTitle: '',
location: ''
name: ''
}
},
computed: {
@ -66,12 +80,22 @@ export default {
}
},
methods: {
async createUser() {
async newUser() {
},
generatePwd() {
this.password = uuidv4().slice(-12)
}
},
apollo: {
providers: {
query: providersQuery,
fetchPolicy: 'network-only',
update: (data) => data.authentication.strategies,
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')
}
}
}
}
</script>

14
client/components/admin/admin-users.vue

@ -10,12 +10,9 @@
v-spacer
v-btn(outline, color='grey', large, @click='refresh')
v-icon refresh
v-btn(color='primary', large, outline, @click='authorizeUser')
v-icon(left) lock_outline
span Authorize Social User
v-btn(color='primary', large, depressed, @click='createUser')
v-icon(left) add
span New Local User
span New User
v-card.mt-3
v-data-table(
v-model='selected'
@ -54,24 +51,21 @@
template(slot='no-data')
.pa-3
v-alert(icon='warning', :value='true', outline) No users to display!
v-card-chin
v-card-chin(v-if='this.pages > 0')
v-spacer
v-pagination(v-model='pagination.page', :length='pages')
v-spacer
user-authorize(v-model='isAuthorizeDialogShown')
user-create(v-model='isCreateDialogShown')
</template>
<script>
import usersQuery from 'gql/admin/users/users-query-list.gql'
import UserAuthorize from './admin-users-authorize.vue'
import UserCreate from './admin-users-create.vue'
export default {
components: {
UserAuthorize,
UserCreate
},
data() {
@ -88,7 +82,6 @@ export default {
{ text: '', value: 'actions', sortable: false, width: 50 }
],
search: '',
isAuthorizeDialogShown: false,
isCreateDialogShown: false
}
},
@ -102,9 +95,6 @@ export default {
}
},
methods: {
authorizeUser() {
this.isAuthorizeDialogShown = true
},
createUser() {
this.isCreateDialogShown = true
},

123
client/components/editor.vue

@ -19,7 +19,7 @@
v-icon(color='blue', :left='$vuetify.breakpoint.lgAndUp') sort_by_alpha
span.white--text(v-if='$vuetify.breakpoint.lgAndUp') {{ $t('editor:page') }}
v-btn(
v-if='mode === `create`'
v-if='mode === `create` && path !== `home`'
outline
color='red'
:class='{ "is-icon": $vuetify.breakpoint.mdAndDown }'
@ -29,74 +29,12 @@
span.white--text(v-if='$vuetify.breakpoint.lgAndUp') {{ $t('common:actions.discard') }}
v-content
component(:is='currentEditor')
v-btn(fixed, bottom, right, color='red', round, @click='exit', dark)
v-icon(left) close
span Close Editor
editor-modal-properties(v-model='dialogProps')
v-dialog(v-model='dialogEditorSelector', persistent, max-width='700')
v-card.radius-7(color='blue darken-3', dark)
v-card-text.text-xs-center.py-4
.subheading Which editor do you want to use for this page?
v-container(grid-list-lg, fluid)
v-layout(row, wrap, justify-center)
v-flex(xs4)
v-card.radius-7.grey(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("api")')
img(src='/svg/icon-rest-api.svg', alt='API', style='width: 36px;')
.body-2.mt-2.grey--text.text--darken-2 API Docs
.caption.grey--text.text--darken-1 REST / GraphQL
v-flex(xs4)
v-card.radius-7(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("code")')
img(src='/svg/icon-source-code.svg', alt='Code', style='width: 36px;')
.body-2.mt-2 Code
.caption.grey--text Raw HTML
v-flex(xs4)
v-card.radius-7(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("markdown")')
img(src='/svg/icon-markdown.svg', alt='Markdown', style='width: 36px;')
.body-2.mt-2 Markdown
.caption.grey--text Default
v-flex(xs4)
v-card.radius-7.grey(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("tabular")')
img(src='/svg/icon-table.svg', alt='Tabular', style='width: 36px;')
.body-2.grey--text.mt-2.text--darken-2 Tabular
.caption.grey--text.text--darken-1 Excel-like
v-flex(xs4)
v-card.radius-7.grey(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("wysiwyg")')
img(src='/svg/icon-open-in-browser.svg', alt='Visual Builder', style='width: 36px;')
.body-2.mt-2.grey--text.text--darken-2 Visual Builder
.caption.grey--text.text--darken-1 Drag-n-drop
v-flex(xs4)
v-card.radius-7.grey(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("wikitext")')
img(src='/svg/icon-news.svg', alt='WikiText', style='width: 36px;')
.body-2.grey--text.mt-2.text--darken-2 WikiText
.caption.grey--text.text--darken-1 MediaWiki Format
.caption.blue--text.text--lighten-2 This cannot be changed once the page is created.
editor-modal-editorselect(v-model='dialogEditorSelector')
editor-modal-unsaved(v-model='dialogUnsaved', @discard='exitGo')
loader(v-model='dialogProgress', :title='$t(`editor:save.processing`)', :subtitle='$t(`editor:save.pleaseWait`)')
v-snackbar(
@ -132,7 +70,9 @@ export default {
editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'),
editorMarkdown: () => import(/* webpackChunkName: "editor-markdown", webpackMode: "lazy" */ './editor/editor-markdown.vue'),
editorWysiwyg: () => import(/* webpackChunkName: "editor-wysiwyg", webpackMode: "lazy" */ './editor/editor-wysiwyg.vue'),
editorModalProperties: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-properties.vue')
editorModalEditorselect: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-editorselect.vue'),
editorModalProperties: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-properties.vue'),
editorModalUnsaved: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-unsaved.vue')
},
props: {
locale: {
@ -178,18 +118,29 @@ export default {
},
data() {
return {
currentEditor: '',
dialogProps: false,
dialogProgress: false,
dialogEditorSelector: false
dialogEditorSelector: false,
dialogUnsaved: false,
initContentParsed: ''
}
},
computed: {
currentEditor: sync('editor/editor'),
darkMode: get('site/dark'),
mode: get('editor/mode'),
notification: get('notification'),
notificationState: sync('notification@isActive')
},
watch: {
currentEditor(newValue, oldValue) {
if (newValue !== '' && this.mode === 'create') {
_.delay(() => {
this.dialogProps = true
}, 500)
}
}
},
created() {
this.$store.commit('page/SET_ID', this.pageId)
this.$store.commit('page/SET_DESCRIPTION', this.description)
@ -203,25 +154,18 @@ export default {
},
mounted() {
this.$store.set('editor/mode', this.initMode || 'create')
this.$store.set('editor/content', this.initContent ? Base64.decode(this.initContent) : '# Header\n\nYour content here')
this.initContentParsed = this.initContent ? Base64.decode(this.initContent) : '# Header\n\nYour content here'
this.$store.set('editor/content', this.initContentParsed)
if (this.mode === 'create') {
_.delay(() => {
this.dialogEditorSelector = true
}, 500)
} else {
this.selectEditor(this.initEditor || 'markdown')
this.currentEditor = `editor${_.startCase(this.initEditor || 'markdown')}`
}
},
methods: {
selectEditor(name) {
this.currentEditor = `editor${_.startCase(name)}`
this.dialogEditorSelector = false
if (this.mode === 'create') {
_.delay(() => {
this.dialogProps = true
}, 500)
}
},
openPropsModal(name) {
this.dialogProps = true
},
@ -310,8 +254,19 @@ export default {
}
this.hideProgressDialog()
},
exit() {
async exit() {
if (this.initContentParsed !== this.$store.get('editor/content')) {
this.dialogUnsaved = true
} else {
this.exitGo()
}
},
exitGo() {
this.$store.commit(`loadingStart`, 'editor-close')
this.currentEditor = ''
_.delay(() => {
window.location.assign(`/${this.$store.get('page/path')}`)
}, 500)
}
}
}

103
client/components/editor/editor-modal-editorselect.vue

@ -0,0 +1,103 @@
<template lang='pug'>
v-dialog(v-model='isShown', persistent, max-width='700')
v-card.radius-7(color='blue darken-3', dark)
v-card-text.text-xs-center.py-4
.subheading Which editor do you want to use for this page?
v-container(grid-list-lg, fluid)
v-layout(row, wrap, justify-center)
v-flex(xs4)
v-card.radius-7.grey(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("api")')
img(src='/svg/icon-rest-api.svg', alt='API', style='width: 36px;')
.body-2.mt-2.grey--text.text--darken-2 API Docs
.caption.grey--text.text--darken-1 REST / GraphQL
v-flex(xs4)
v-card.radius-7(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("code")')
img(src='/svg/icon-source-code.svg', alt='Code', style='width: 36px;')
.body-2.mt-2 Code
.caption.grey--text Raw HTML
v-flex(xs4)
v-card.radius-7(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("markdown")')
img(src='/svg/icon-markdown.svg', alt='Markdown', style='width: 36px;')
.body-2.mt-2 Markdown
.caption.grey--text Default
v-flex(xs4)
v-card.radius-7.grey(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("tabular")')
img(src='/svg/icon-table.svg', alt='Tabular', style='width: 36px;')
.body-2.grey--text.mt-2.text--darken-2 Tabular
.caption.grey--text.text--darken-1 Excel-like
v-flex(xs4)
v-card.radius-7.grey(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("wysiwyg")')
img(src='/svg/icon-open-in-browser.svg', alt='Visual Builder', style='width: 36px;')
.body-2.mt-2.grey--text.text--darken-2 Visual Builder
.caption.grey--text.text--darken-1 Drag-n-drop
v-flex(xs4)
v-card.radius-7.grey(
hover
light
ripple
)
v-card-text.text-xs-center(@click='selectEditor("wikitext")')
img(src='/svg/icon-news.svg', alt='WikiText', style='width: 36px;')
.body-2.grey--text.mt-2.text--darken-2 WikiText
.caption.grey--text.text--darken-1 MediaWiki Format
.caption.blue--text.text--lighten-2 This cannot be changed once the page is created.
</template>
<script>
import _ from 'lodash'
import { sync } from 'vuex-pathify'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return { }
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
},
currentEditor: sync('editor/editor'),
},
methods: {
selectEditor(name) {
this.currentEditor = `editor${_.startCase(name)}`
this.isShown = false
}
}
}
</script>
<style lang='scss'>
</style>

40
client/components/editor/editor-modal-unsaved.vue

@ -0,0 +1,40 @@
<template lang="pug">
v-dialog(v-model='isShown', max-width='550')
v-card.wiki-form
.dialog-header.is-short.is-red
v-icon.mr-2(color='white') warning
span Discard Unsaved Changes?
v-card-text
.body-2 You have unsaved changes. Are you sure you want to leave the editor and discard any modifications you made since the last save?
v-card-chin
v-spacer
v-btn(flat, @click='isShown = false') Cancel
v-btn(color='red', @click='discard', dark) Discard Changes
</template>
<script>
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return { }
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
}
},
methods: {
async discard() {
this.isShown = false
this.$emit('discard', true)
}
}
}
</script>

6
client/graph/admin/site/site-mutation-save-config.gql

@ -3,7 +3,8 @@ mutation (
$title: String!
$description: String!
$robots: [String]!
$ga: String!
$analyticsService: String!
$analyticsId: String!
$company: String!
$hasLogo: Boolean!
$logoIsSquare: Boolean!
@ -17,7 +18,8 @@ mutation (
title: $title,
description: $description,
robots: $robots,
ga: $ga,
analyticsService: $analyticsService,
analyticsId: $analyticsId,
company: $company,
hasLogo: $hasLogo,
logoIsSquare: $logoIsSquare,

3
client/graph/admin/site/site-query-config.gql

@ -5,7 +5,8 @@
title
description
robots
ga
analyticsService
analyticsId
company
hasLogo
logoIsSquare

1
client/store/editor.js

@ -1,6 +1,7 @@
import { make } from 'vuex-pathify'
const state = {
editor: '',
content: '',
mode: 'create'
}

3
server/graph/resolvers/site.js

@ -31,7 +31,8 @@ module.exports = {
WIKI.config.seo = {
description: args.description,
robots: args.robots,
ga: args.ga
analyticsService: args.analyticsService,
analyticsId: args.analyticsId
}
WIKI.config.logo = {
hasLogo: args.hasLogo,

6
server/graph/schemas/site.graphql

@ -28,7 +28,8 @@ type SiteMutation {
title: String!
description: String!
robots: [String]!
ga: String!
analyticsService: String!
analyticsId: String!
company: String!
hasLogo: Boolean!
logoIsSquare: Boolean!
@ -47,7 +48,8 @@ type SiteConfig {
title: String!
description: String!
robots: [String]!
ga: String!
analyticsService: String!
analyticsId: String!
company: String!
hasLogo: Boolean!
logoIsSquare: Boolean!

3
server/setup.js

@ -140,7 +140,8 @@ module.exports = () => {
_.set(WIKI.config, 'seo', {
description: '',
robots: ['index', 'follow'],
ga: ''
analyticsService: '',
analyticsId: ''
})
_.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
_.set(WIKI.config, 'telemetry', {

Loading…
Cancel
Save