From 3f0adc5dafc6fc74a34d5376c64eabbdad58a07d Mon Sep 17 00:00:00 2001
From: NGPixel <github@ngpixel.com>
Date: Sun, 18 Mar 2018 23:12:56 -0400
Subject: [PATCH] feat: navigation, editor improvements + graphql refactor

---
 client/app.js                                 |   4 -
 client/components/admin-auth.vue              |  21 ++-
 client/components/admin-dev.vue               |   7 +
 client/components/admin-groups.vue            |  77 ++++-----
 client/components/admin-system.vue            |  30 +++-
 client/components/editor-code.vue             |  19 +--
 client/components/editor-modal-access.vue     |   2 +-
 client/components/editor-modal-properties.vue |   2 +-
 client/components/editor.vue                  |  25 ++-
 client/components/login.vue                   |  52 +++++-
 client/components/nav-header.vue              |  41 ++++-
 client/components/navigator.vue               | 155 ------------------
 client/constants/graphql.js                   | 103 ------------
 client/constants/index.js                     |   5 -
 client/modules/localization.js                |  16 +-
 client/scss/pages/_welcome.scss               |   1 +
 server/graph/resolvers/folder.js              |   2 +-
 server/graph/resolvers/group.js               |  20 ++-
 server/graph/schemas/common.graphql           |  33 ----
 server/graph/schemas/group.graphql            |  69 ++++++++
 20 files changed, 284 insertions(+), 400 deletions(-)
 delete mode 100644 client/components/navigator.vue
 delete mode 100644 client/constants/graphql.js
 delete mode 100644 client/constants/index.js
 create mode 100644 server/graph/schemas/group.graphql

diff --git a/client/app.js b/client/app.js
index 1f5914e2..9091a7ee 100644
--- a/client/app.js
+++ b/client/app.js
@@ -1,7 +1,5 @@
 'use strict'
 
-import CONSTANTS from './constants'
-
 import Vue from 'vue'
 import VueRouter from 'vue-router'
 import VueClipboards from 'vue-clipboards'
@@ -35,7 +33,6 @@ import helpers from './helpers'
 
 window.WIKI = null
 window.boot = boot
-window.CONSTANTS = CONSTANTS
 window.Hammer = Hammer
 
 // ====================================
@@ -78,7 +75,6 @@ Vue.component('admin', () => import(/* webpackChunkName: "admin" */ './component
 Vue.component('editor', () => import(/* webpackChunkName: "editor" */ './components/editor.vue'))
 Vue.component('login', () => import(/* webpackMode: "eager" */ './components/login.vue'))
 Vue.component('nav-header', () => import(/* webpackMode: "eager" */ './components/nav-header.vue'))
-Vue.component('navigator', () => import(/* webpackMode: "eager" */ './components/navigator.vue'))
 Vue.component('setup', () => import(/* webpackChunkName: "setup" */ './components/setup.vue'))
 
 let bootstrap = () => {
diff --git a/client/components/admin-auth.vue b/client/components/admin-auth.vue
index cd1a2ef2..2e02e106 100644
--- a/client/components/admin-auth.vue
+++ b/client/components/admin-auth.vue
@@ -66,8 +66,7 @@
 
 <script>
 import _ from 'lodash'
-
-/* global CONSTANTS */
+import gql from 'graphql-tag'
 
 export default {
   data() {
@@ -84,7 +83,23 @@ export default {
   },
   apollo: {
     providers: {
-      query: CONSTANTS.GRAPH.AUTHENTICATION.QUERY_PROVIDERS,
+      query: gql`
+        query {
+          authentication {
+            providers {
+              isEnabled
+              key
+              props
+              title
+              useForm
+              config {
+                key
+                value
+              }
+            }
+          }
+        }
+      `,
       update: (data) => data.authentication.providers
     }
   },
diff --git a/client/components/admin-dev.vue b/client/components/admin-dev.vue
index 43458664..1bd73675 100644
--- a/client/components/admin-dev.vue
+++ b/client/components/admin-dev.vue
@@ -114,5 +114,12 @@ export default {
 
 #voyager {
   height: calc(100vh - 250px);
+
+  .title-area {
+    display: none;
+  }
+  .type-doc {
+    margin-top: 5px;
+  }
 }
 </style>
diff --git a/client/components/admin-groups.vue b/client/components/admin-groups.vue
index dbab6f95..c03bf0da 100644
--- a/client/components/admin-groups.vue
+++ b/client/components/admin-groups.vue
@@ -5,54 +5,41 @@
       .subheading.grey--text Manage groups
     v-card
       v-card-title
-        v-btn(color='primary', dark)
-          v-icon(left) add
-          | New Group
+        v-dialog(v-model='newGroupDialog', max-width='500')
+          v-btn(color='primary', dark, slot='activator')
+            v-icon(left) add
+            | New Group
+          v-card
+            v-card-title.headline.grey--text.text--darken-2 New Group
+            v-card-text
+              v-text-field(v-model='newGroupName', label='Group Name', autofocus, counter='255')
+            v-card-actions
+              v-spacer
+              v-btn(flat, @click='newGroupDialog = false') Cancel
+              v-btn(color='primary', @click='createGroup') Create
         v-btn(icon)
           v-icon.grey--text refresh
         v-spacer
         v-text-field(append-icon='search', label='Search', single-line, hide-details, v-model='search')
       v-data-table(
         v-model='selected'
-        :items='items',
+        :items='groups',
         :headers='headers',
         :search='search',
         :pagination.sync='pagination',
         :rows-per-page-items='[15]'
-        select-all,
         hide-actions,
         disable-initial-sort
       )
-        template(slot='headers', slot-scope='props')
-          tr
-            th(width='50')
-            th.text-xs-right(
-              width='80'
-              :class='[`column sortable`, pagination.descending ? `desc` : `asc`, pagination.sortBy === `id` ? `active` : ``]'
-              @click='changeSort(`id`)'
-            )
-              v-icon(small) arrow_upward
-              | ID
-            th.text-xs-left(
-              v-for='header in props.headers'
-              :key='header.text'
-              :width='header.width'
-              :class='[`column sortable`, pagination.descending ? `desc` : `asc`, header.value === pagination.sortBy ? `active` : ``]'
-              @click='changeSort(header.value)'
-            )
-              | {{ header.text }}
-              v-icon(small) arrow_upward
         template(slot='items', slot-scope='props')
           tr(:active='props.selected')
-            td
-              v-checkbox(hide-details, :input-value='props.selected', color='blue darken-2', @click='props.selected = !props.selected')
             td.text-xs-right {{ props.item.id }}
             td {{ props.item.name }}
             td {{ props.item.userCount }}
             td: v-btn(icon): v-icon.grey--text.text--darken-1 more_horiz
         template(slot='no-data')
-          v-alert(icon='warning', :value='true') No users to display!
-      .text-xs-center.py-2(v-if='items.length > 15')
+          v-alert.ma-3(icon='warning', :value='true', outline) No groups to display.
+      .text-xs-center.py-2(v-if='groups.length > 15')
         v-pagination(v-model='pagination.page', :length='pages')
 </template>
 
@@ -60,13 +47,13 @@
 export default {
   data() {
     return {
+      newGroupDialog: false,
+      newGroupName: '',
       selected: [],
       pagination: {},
-      items: [
-        { id: 1, name: 'Administrators', userCount: 1 },
-        { id: 2, name: 'Users', userCount: 23 }
-      ],
+      groups: [],
       headers: [
+        { text: 'ID', value: 'id', width: 50, align: 'right' },
         { text: 'Name', value: 'name' },
         { text: 'Users', value: 'userCount', width: 200 },
         { text: '', value: 'actions', sortable: false, width: 50 }
@@ -84,20 +71,18 @@ export default {
     }
   },
   methods: {
-    changeSort (column) {
-      if (this.pagination.sortBy === column) {
-        this.pagination.descending = !this.pagination.descending
-      } else {
-        this.pagination.sortBy = column
-        this.pagination.descending = false
-      }
-    },
-    toggleAll () {
-      if (this.selected.length) {
-        this.selected = []
-      } else {
-        this.selected = this.items.slice()
-      }
+    async createGroup() {
+      // try {
+      //   const resp = await this.$apollo.mutate({
+      //     mutation: CONSTANTS.GRAPH.GROUPS.CREATE,
+      //     variables: {
+      //       name: this.newGroupName
+      //     }
+      //   })
+
+      // } catch (err) {
+
+      // }
     }
   }
 }
diff --git a/client/components/admin-system.vue b/client/components/admin-system.vue
index 5b2009fe..85b91389 100644
--- a/client/components/admin-system.vue
+++ b/client/components/admin-system.vue
@@ -23,7 +23,7 @@
                     v-list-tile-title Latest Version
                     v-list-tile-sub-title {{ info.latestVersion }}
                   v-list-tile-action
-                    v-list-tile-action-text Published 4 days ago
+                    v-list-tile-action-text Published X days ago
 
                 v-divider
 
@@ -105,12 +105,12 @@
 </template>
 
 <script>
+import gql from 'graphql-tag'
+
 import IconCube from 'mdi/cube'
 import IconDatabase from 'mdi/database'
 import IconNodeJs from 'mdi/nodejs'
 
-/* global CONSTANTS */
-
 export default {
   components: {
     IconCube,
@@ -125,7 +125,29 @@ export default {
   },
   apollo: {
     info: {
-      query: CONSTANTS.GRAPH.SYSTEM.QUERY_INFO,
+      query: gql`
+        query {
+          system {
+            info {
+              currentVersion
+              latestVersion
+              latestVersionReleaseDate
+              operatingSystem
+              hostname
+              cpuCores
+              ramTotal
+              workingDirectory
+              nodeVersion
+              redisVersion
+              redisUsedRAM
+              redisTotalRAM
+              redisHost
+              postgreVersion
+              postgreHost
+            }
+          }
+        }
+      `,
       update: (data) => data.system.info
     }
   },
diff --git a/client/components/editor-code.vue b/client/components/editor-code.vue
index df3b2f7b..abe986ef 100644
--- a/client/components/editor-code.vue
+++ b/client/components/editor-code.vue
@@ -1,8 +1,5 @@
 <template lang='pug'>
   .editor-code
-    v-toolbar(color='blue', flat, dense, dark)
-      v-icon(color='blue lighten-5') edit
-      v-toolbar-title.white--text Sample Page
     .editor-code-toolbar
       .editor-code-toolbar-group
         .editor-code-toolbar-item(@click='toggleAround("**", "**")')
@@ -70,7 +67,7 @@
           .editor-code-preview-title(@click='previewShown = false') Preview
           .editor-code-preview-content.markdown-content(ref='editorPreview', v-html='previewHTML')
 
-      v-speed-dial(v-model='fabInsertMenu', :open-on-hover='true', direction='top', transition='slide-y-reverse-transition', :fixed='true', :right='!isMobile', :left='isMobile', :bottom='true')
+      v-speed-dial(v-model='fabInsertMenu', :open-on-hover='true', direction='top', transition='slide-y-reverse-transition', fixed, right, bottom)
         v-btn(color='blue', fab, dark, v-model='fabInsertMenu', slot='activator')
           v-icon add_circle
           v-icon close
@@ -79,16 +76,6 @@
         v-btn(color='red', fab, dark): v-icon play_circle_outline
         v-btn(color='purple', fab, dark): v-icon multiline_chart
         v-btn(color='indigo', fab, dark): v-icon functions
-      v-speed-dial(v-model='fabMainMenu', :open-on-hover='true', :direction='saveMenuDirection', transition='slide-x-reverse-transition', :fixed='true', :right='true', :top='!isMobile', :bottom='isMobile')
-        v-btn(color='white', fab, light, v-model='fabMainMenu' slot='activator')
-          v-icon(color='blue darken-2') blur_on
-          v-icon(color='blue darken-2') close
-        v-btn(color='blue-grey', fab, dark, @click.native.stop='$parent.openModal(`properties`)'): v-icon sort_by_alpha
-        v-btn(color='green', fab, dark, @click.native.stop='$parent.save()'): v-icon save
-        v-btn(color='red', fab, dark, small): v-icon not_interested
-        v-btn(color='orange', fab, dark, small, @click.native.stop='$parent.openModal(`access`)'): v-icon vpn_lock
-        v-btn(color='indigo', fab, dark, small): v-icon restore
-        v-btn(color='brown', fab, dark, small): v-icon archive
 </template>
 
 <script>
@@ -184,7 +171,6 @@ export default {
   },
   data() {
     return {
-      fabMainMenu: false,
       fabInsertMenu: false,
       code: '# Header 1\n\nSample **Text**\nhttp://wiki.js.org\n:rocket: :) :( :| :P\n\n## Header 2\nSample Text\n\n```javascript\nvar test = require("test");\n\n// some comment\nconst foo = bar(\'param\') + 1.234;\n```\n\n### Header 3\nLorem *ipsum* ~~text~~',
       cmOptions: {
@@ -210,9 +196,6 @@ export default {
     },
     isMobile() {
       return this.$vuetify.breakpoint.smAndDown
-    },
-    saveMenuDirection() {
-      return this.isMobile ? 'top' : 'bottom'
     }
   },
   methods: {
diff --git a/client/components/editor-modal-access.vue b/client/components/editor-modal-access.vue
index ffe85536..5ca5a7e0 100644
--- a/client/components/editor-modal-access.vue
+++ b/client/components/editor-modal-access.vue
@@ -49,7 +49,7 @@ export default {
   methods: {
     close() {
       this.isShown = false
-      this.$parent.closeModal()
+      this.$parent.$parent.closeModal()
     }
   }
 }
diff --git a/client/components/editor-modal-properties.vue b/client/components/editor-modal-properties.vue
index 23a683f2..27e228b2 100644
--- a/client/components/editor-modal-properties.vue
+++ b/client/components/editor-modal-properties.vue
@@ -31,7 +31,7 @@ export default {
   methods: {
     close() {
       this.isShown = false
-      this.$parent.closeModal()
+      this.$parent.$parent.closeModal()
     }
   }
 }
diff --git a/client/components/editor.vue b/client/components/editor.vue
index 8d36a62b..2b6739a3 100644
--- a/client/components/editor.vue
+++ b/client/components/editor.vue
@@ -1,13 +1,22 @@
 <template lang="pug">
   .editor
-    editor-code
-    component(:is='currentModal')
-    v-dialog(v-model='dialogProgress', persistent, max-width='300')
-      v-card
-        v-progress-linear.my-0(indeterminate, color='primary', height='5')
-        v-card-text.text-xs-center
-          .headline Saving
-          .caption Please wait...
+    nav-header
+      template(slot='actions')
+        v-btn(outline, color='green', @click.native.stop='save')
+          v-icon(color='green', left) save
+          span.white--text Save
+        v-btn(icon): v-icon(color='red') close
+        v-btn(icon, @click.native.stop='openModal(`properties`)'): v-icon(color='white') sort_by_alpha
+        v-btn(icon, @click.native.stop='openModal(`access`)'): v-icon(color='white') vpn_lock
+    v-content
+      editor-code
+      component(:is='currentModal')
+      v-dialog(v-model='dialogProgress', persistent, max-width='300')
+        v-card
+          v-progress-linear.my-0(indeterminate, color='primary', height='5')
+          v-card-text.text-xs-center
+            .headline Saving
+            .caption Please wait...
 </template>
 
 <script>
diff --git a/client/components/login.vue b/client/components/login.vue
index 50bf3d37..48c2ad6c 100644
--- a/client/components/login.vue
+++ b/client/components/login.vue
@@ -1,5 +1,6 @@
 <template lang="pug">
   v-app
+    nav-header
     .login(:class='{ "is-error": error }')
       .login-container(:class='{ "is-expanded": strategies.length > 1, "is-loading": isLoading }')
         .login-providers(v-show='strategies.length > 1')
@@ -37,9 +38,10 @@
 </template>
 
 <script>
-/* global CONSTANTS, graphQL, siteConfig */
+/* global graphQL, siteConfig */
 
 import _ from 'lodash'
+import gql from 'graphql-tag'
 
 export default {
   data () {
@@ -80,7 +82,21 @@ export default {
     refreshStrategies () {
       this.isLoading = true
       graphQL.query({
-        query: CONSTANTS.GRAPH.AUTHENTICATION.QUERY_LOGIN_PROVIDERS
+        query: gql`
+          query {
+            authentication {
+              providers(
+                filter: "isEnabled eq true",
+                orderBy: "title ASC"
+              ) {
+                key
+                title
+                useForm
+                icon
+              }
+            }
+          }
+        `
       }).then(resp => {
         if (_.has(resp, 'data.authentication.providers')) {
           this.strategies = _.get(resp, 'data.authentication.providers', [])
@@ -116,7 +132,22 @@ export default {
       } else {
         this.isLoading = true
         graphQL.mutate({
-          mutation: CONSTANTS.GRAPH.AUTHENTICATION.MUTATION_LOGIN,
+          mutation: gql`
+            mutation($username: String!, $password: String!, $provider: String!) {
+              authentication {
+                login(username: $username, password: $password, provider: $provider) {
+                  operation {
+                    succeeded
+                    code
+                    slug
+                    message
+                  }
+                  tfaRequired
+                  tfaLoginToken
+                }
+              }
+            }
+          `,
           variables: {
             username: this.username,
             password: this.password,
@@ -169,7 +200,20 @@ export default {
       } else {
         this.isLoading = true
         graphQL.mutate({
-          mutation: CONSTANTS.GRAPH.AUTHENTICATION.MUTATION_LOGINTFA,
+          mutation: gql`
+            mutation($loginToken: String!, $securityCode: String!) {
+              authentication {
+                loginTFA(loginToken: $loginToken, securityCode: $securityCode) {
+                  operation {
+                    succeeded
+                    code
+                    slug
+                    message
+                  }
+                }
+              }
+            }
+          `,
           variables: {
             loginToken: this.loginToken,
             securityCode: this.securityCode
diff --git a/client/components/nav-header.vue b/client/components/nav-header.vue
index b00945ea..7268d84f 100644
--- a/client/components/nav-header.vue
+++ b/client/components/nav-header.vue
@@ -1,7 +1,40 @@
 <template lang='pug'>
-  v-toolbar(color='black', dark, app, clipped-left, fixed, flat)
-    v-toolbar-side-icon(@click.native='')
-      v-icon view_module
+  v-toolbar(color='black', dark, app, clipped-left, fixed, flat, dense)
+    v-menu(open-on-hover, offset-y, bottom, left, nudge-top='-18', min-width='250')
+      v-toolbar-side-icon(slot='activator')
+        v-icon view_module
+      v-list(dense)
+        v-list-tile(avatar, href='/')
+          v-list-tile-avatar: v-icon(color='blue') home
+          v-list-tile-content Home
+        v-list-tile(avatar, @click='')
+          v-list-tile-avatar: v-icon(color='green') add_box
+          v-list-tile-content New Page
+        v-divider.my-0
+        v-subheader Current Page
+        v-list-tile(avatar, @click='')
+          v-list-tile-avatar: v-icon(color='indigo') edit
+          v-list-tile-content Edit
+        v-list-tile(avatar, @click='')
+          v-list-tile-avatar: v-icon(color='indigo') history
+          v-list-tile-content History
+        v-list-tile(avatar, @click='')
+          v-list-tile-avatar: v-icon(color='indigo') code
+          v-list-tile-content View Source
+        v-list-tile(avatar, @click='')
+          v-list-tile-avatar: v-icon(color='indigo') forward
+          v-list-tile-content Move / Rename
+        v-list-tile(avatar, @click='')
+          v-list-tile-avatar: v-icon(color='red darken-2') delete
+          v-list-tile-content Delete
+        v-divider.my-0
+        v-subheader Assets
+        v-list-tile(avatar, @click='')
+          v-list-tile-avatar: v-icon(color='blue-grey') burst_mode
+          v-list-tile-content Images
+        v-list-tile(avatar, @click='')
+          v-list-tile-avatar: v-icon(color='blue-grey') description
+          v-list-tile-content Files
     v-toolbar-title
       span.subheading Wiki.js
     v-spacer
@@ -29,6 +62,7 @@
         )
     v-spacer
     v-progress-circular.mr-3(indeterminate, color='blue', v-show='$apollo.loading')
+    slot(name='actions')
     transition(name='navHeaderSearch')
       v-btn(icon, @click='searchToggle', v-if='!searchIsShown')
         v-icon(color='grey') search
@@ -57,6 +91,7 @@
 export default {
   data() {
     return {
+      menuIsShown: true,
       searchIsLoading: false,
       searchIsShown: false,
       search: ''
diff --git a/client/components/navigator.vue b/client/components/navigator.vue
deleted file mode 100644
index 3820a201..00000000
--- a/client/components/navigator.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-<template lang="pug">
-  .navigator
-    .navigator-bar
-      .navigator-fab
-        .navigator-fab-button(@click='toggleMainMenu')
-          svg.icons.is-24(role='img')
-            title Navigation
-            use(xlink:href='#gg-apps-grid')
-      .navigator-title
-        h1 {{ siteTitle }}
-      .navigator-subtitle(:class='subtitleClass')
-        transition(name='navigator-subtitle-icon')
-          svg.icons.is-24.navigator-subtitle-icon(role='img', v-if='subtitleIcon')
-            title {{subtitleText}}
-            use(:xlink:href='subtitleIconClass')
-        h2 {{subtitleText}}
-      .navigator-action
-        .navigator-action-item(:class='{"is-active": userMenuShown}', @click='toggleUserMenu')
-          svg.icons.is-32(role='img')
-            title User
-            use(xlink:href='#nc-user-circle')
-          transition(name='navigator-action-item-dropdown')
-            ul.navigator-action-item-dropdown(v-show='userMenuShown', v-cloak)
-              li
-                label Account
-                svg.icons.is-24(role='img')
-                  title Account
-                  use(xlink:href='#nc-man-green')
-              li(@click='logout')
-                label Sign out
-                svg.icons.is-24(role='img')
-                  title Sign Out
-                  use(xlink:href='#nc-exit')
-    transition(name='navigator-sd')
-      .navigator-sd(v-show='sdShown', v-cloak)
-        .navigator-sd-actions
-          a.is-active(href='', title='Search')
-            svg.icons.is-24(role='img')
-              title Search
-              use(xlink:href='#gg-search')
-          a(href='', title='New Document')
-            svg.icons.is-24(role='img')
-              title New Document
-              use(xlink:href='#nc-plus-circle')
-          a(href='', title='Edit Document')
-            svg.icons.is-24(role='img')
-              title Edit Document
-              use(xlink:href='#nc-pen-red')
-          a(href='', title='History')
-            svg.icons.is-24(role='img')
-              title History
-              use(xlink:href='#nc-restore')
-          a(href='', title='View Source')
-            svg.icons.is-24(role='img')
-              title View Source
-              use(xlink:href='#nc-code-editor')
-          a(href='', title='Move Document')
-            svg.icons.is-24(role='img')
-              title Move Document
-              use(xlink:href='#nc-move')
-          a(href='', title='Delete Document')
-            svg.icons.is-24(role='img')
-              title Delete Document
-              use(xlink:href='#nc-trash')
-        .navigator-sd-search
-          input(type='text', ref='iptSearch', placeholder='Search')
-        .navigator-sd-results
-        .navigator-sd-footer
-          a(href='', title='Settings')
-            svg.icons.is-24(role='img')
-              title Settings
-              use(xlink:href='#nc-gear')
-          a(href='', title='Users')
-            svg.icons.is-24(role='img')
-              title Users
-              use(xlink:href='#nc-users')
-          a(href='', title='Assets')
-            svg.icons.is-24(role='img')
-              title Assets
-              use(xlink:href='#nc-image')
-</template>
-
-<script>
-/* global siteConfig */
-
-import { mapState } from 'vuex'
-
-export default {
-  data() {
-    return {
-      sdShown: false,
-      userMenuShown: false
-    }
-  },
-  computed: {
-    ...mapState('navigator', [
-      'subtitleShown',
-      'subtitleStyle',
-      'subtitleText',
-      'subtitleIcon'
-    ]),
-    siteTitle() {
-      return siteConfig.title
-    },
-    subtitleClass() {
-      return {
-        'is-active': this.subtitleShown,
-        'is-error': this.subtitleStyle === 'error',
-        'is-warning': this.subtitleStyle === 'warning',
-        'is-success': this.subtitleStyle === 'success',
-        'is-info': this.subtitleStyle === 'info'
-      }
-    },
-    subtitleIconClass() { return '#' + this.subtitleIcon }
-  },
-  methods: {
-    toggleMainMenu() {
-      this.sdShown = !this.sdShown
-      this.userMenuShown = false
-      if (this.sdShown) {
-        this.$nextTick(() => {
-          this.bindOutsideClick()
-          this.$refs.iptSearch.focus()
-        })
-      } else {
-        this.unbindOutsideClick()
-      }
-    },
-    toggleUserMenu() {
-      this.userMenuShown = !this.userMenuShown
-      this.sdShown = false
-      if (this.userMenuShown) {
-        this.bindOutsideClick()
-      } else {
-        this.unbindOutsideClick()
-      }
-    },
-    bindOutsideClick() {
-      document.addEventListener('mousedown', this.handleOutsideClick, false)
-    },
-    unbindOutsideClick() {
-      document.removeEventListener('mousedown', this.handleOutsideClick, false)
-    },
-    handleOutsideClick(ev) {
-      if (!this.$el.contains(ev.target)) {
-        this.sdShown = false
-        this.userMenuShown = false
-      }
-    },
-    logout() {
-      window.location.assign(this.$helpers.resolvePath('logout'))
-    }
-  }
-}
-</script>
diff --git a/client/constants/graphql.js b/client/constants/graphql.js
deleted file mode 100644
index 94cb8b0b..00000000
--- a/client/constants/graphql.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import gql from 'graphql-tag'
-
-export default {
-  AUTHENTICATION: {
-    QUERY_PROVIDERS: gql`
-      query {
-        authentication {
-          providers {
-            isEnabled
-            key
-            props
-            title
-            useForm
-            config {
-              key
-              value
-            }
-          }
-        }
-      }
-    `,
-    QUERY_LOGIN_PROVIDERS: gql`
-      query {
-        authentication {
-          providers(
-            filter: "isEnabled eq true",
-            orderBy: "title ASC"
-          ) {
-            key
-            title
-            useForm
-            icon
-          }
-        }
-      }
-    `,
-    MUTATION_LOGIN: gql`
-      mutation($username: String!, $password: String!, $provider: String!) {
-        authentication {
-          login(username: $username, password: $password, provider: $provider) {
-            operation {
-              succeeded
-              code
-              slug
-              message
-            }
-            tfaRequired
-            tfaLoginToken
-          }
-        }
-      }
-    `,
-    MUTATION_LOGINTFA: gql`
-      mutation($loginToken: String!, $securityCode: String!) {
-        authentication {
-          loginTFA(loginToken: $loginToken, securityCode: $securityCode) {
-            operation {
-              succeeded
-              code
-              slug
-              message
-            }
-          }
-        }
-      }
-    `
-  },
-  SYSTEM: {
-    QUERY_INFO: gql`
-      query {
-        system {
-          info {
-            currentVersion
-            latestVersion
-            latestVersionReleaseDate
-            operatingSystem
-            hostname
-            cpuCores
-            ramTotal
-            workingDirectory
-            nodeVersion
-            redisVersion
-            redisUsedRAM
-            redisTotalRAM
-            redisHost
-            postgreVersion
-            postgreHost
-          }
-        }
-      }
-    `
-  },
-  TRANSLATIONS: {
-    QUERY_NAMESPACE: gql`
-      query($locale: String!, $namespace: String!) {
-        translations(locale:$locale, namespace:$namespace) {
-          key
-          value
-        }
-      }
-    `
-  }
-}
diff --git a/client/constants/index.js b/client/constants/index.js
deleted file mode 100644
index 267f49dd..00000000
--- a/client/constants/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import GRAPH from './graphql'
-
-export default {
-  GRAPH
-}
diff --git a/client/modules/localization.js b/client/modules/localization.js
index 26e36c9c..47f32519 100644
--- a/client/modules/localization.js
+++ b/client/modules/localization.js
@@ -2,9 +2,10 @@ import i18next from 'i18next'
 import i18nextXHR from 'i18next-xhr-backend'
 import i18nextCache from 'i18next-localstorage-cache'
 import VueI18Next from '@panter/vue-i18next'
-import loSet from 'lodash/set'
+import _ from 'lodash'
+import gql from 'graphql-tag'
 
-/* global siteConfig, graphQL, CONSTANTS */
+/* global siteConfig, graphQL */
 
 module.exports = {
   VueI18Next,
@@ -19,7 +20,14 @@ module.exports = {
           ajax: (url, opts, cb, data) => {
             let langParams = url.split('/')
             graphQL.query({
-              query: CONSTANTS.GRAPH.TRANSLATIONS.QUERY_NAMESPACE,
+              query: gql`
+                query($locale: String!, $namespace: String!) {
+                  translations(locale:$locale, namespace:$namespace) {
+                    key
+                    value
+                  }
+                }
+              `,
               variables: {
                 locale: langParams[0],
                 namespace: langParams[1]
@@ -28,7 +36,7 @@ module.exports = {
               let ns = {}
               if (resp.data.translations.length > 0) {
                 resp.data.translations.forEach(entry => {
-                  loSet(ns, entry.key, entry.value)
+                  _.set(ns, entry.key, entry.value)
                 })
               }
               return cb(ns, {status: '200'})
diff --git a/client/scss/pages/_welcome.scss b/client/scss/pages/_welcome.scss
index e8c72cf2..fe72345b 100644
--- a/client/scss/pages/_welcome.scss
+++ b/client/scss/pages/_welcome.scss
@@ -44,6 +44,7 @@
     }
   }
   h1 {
+    font-size: 1.5rem;
     margin-bottom: 1rem;
     z-index: 2;
   }
diff --git a/server/graph/resolvers/folder.js b/server/graph/resolvers/folder.js
index f1e341dd..5a28f603 100644
--- a/server/graph/resolvers/folder.js
+++ b/server/graph/resolvers/folder.js
@@ -11,7 +11,7 @@ module.exports = {
     createFolder(obj, args) {
       return WIKI.db.Folder.create(args)
     },
-    deleteGroup(obj, args) {
+    deleteFolder(obj, args) {
       return WIKI.db.Folder.destroy({
         where: {
           id: args.id
diff --git a/server/graph/resolvers/group.js b/server/graph/resolvers/group.js
index ee4852de..f27d1b96 100644
--- a/server/graph/resolvers/group.js
+++ b/server/graph/resolvers/group.js
@@ -5,12 +5,18 @@ const gql = require('graphql')
 
 module.exports = {
   Query: {
-    groups(obj, args, context, info) {
+    async groups() { return {} }
+  },
+  Mutation: {
+    async groups() { return {} }
+  },
+  GroupQuery: {
+    list(obj, args, context, info) {
       return WIKI.db.Group.findAll({ where: args })
     }
   },
-  Mutation: {
-    assignUserToGroup(obj, args) {
+  GroupMutation: {
+    assignUser(obj, args) {
       return WIKI.db.Group.findById(args.groupId).then(grp => {
         if (!grp) {
           throw new gql.GraphQLError('Invalid Group ID')
@@ -23,10 +29,10 @@ module.exports = {
         })
       })
     },
-    createGroup(obj, args) {
+    create(obj, args) {
       return WIKI.db.Group.create(args)
     },
-    deleteGroup(obj, args) {
+    delete(obj, args) {
       return WIKI.db.Group.destroy({
         where: {
           id: args.id
@@ -34,7 +40,7 @@ module.exports = {
         limit: 1
       })
     },
-    removeUserFromGroup(obj, args) {
+    unassignUser(obj, args) {
       return WIKI.db.Group.findById(args.groupId).then(grp => {
         if (!grp) {
           throw new gql.GraphQLError('Invalid Group ID')
@@ -47,7 +53,7 @@ module.exports = {
         })
       })
     },
-    renameGroup(obj, args) {
+    update(obj, args) {
       return WIKI.db.Group.update({
         name: args.name
       }, {
diff --git a/server/graph/schemas/common.graphql b/server/graph/schemas/common.graphql
index ce687589..bd79e557 100644
--- a/server/graph/schemas/common.graphql
+++ b/server/graph/schemas/common.graphql
@@ -95,15 +95,6 @@ type Folder implements Base {
   files: [File]
 }
 
-type Group implements Base {
-  id: Int!
-  createdAt: Date
-  updatedAt: Date
-  name: String!
-  users: [User]
-  rights: [Right]
-}
-
 type Right implements Base {
   id: Int!
   createdAt: Date
@@ -168,7 +159,6 @@ type Query {
   documents(id: Int, path: String): [Document]
   files(id: Int): [File]
   folders(id: Int, name: String): [Folder]
-  groups(id: Int, name: String): [Group]
   rights(id: Int): [Right]
   search(q: String, tags: [String]): [SearchResult]
   settings(key: String): [Setting]
@@ -192,11 +182,6 @@ type Mutation {
     documentId: Int!
   ): OperationResult
 
-  assignUserToGroup(
-    userId: Int!
-    groupId: Int!
-  ): OperationResult
-
   createComment(
     userId: Int!
     documentId: Int!
@@ -213,10 +198,6 @@ type Mutation {
     name: String!
   ): Folder
 
-  createGroup(
-    name: String!
-  ): Group
-
   createTag(
     name: String!
   ): Tag
@@ -246,10 +227,6 @@ type Mutation {
     id: Int!
   ): OperationResult
 
-  deleteGroup(
-    id: Int!
-  ): OperationResult
-
   deleteTag(
     id: Int!
   ): OperationResult
@@ -306,11 +283,6 @@ type Mutation {
     name: String!
   ): OperationResult
 
-  renameGroup(
-    id: Int!
-    name: String!
-  ): OperationResult
-
   renameTag(
     id: Int!
     key: String!
@@ -325,11 +297,6 @@ type Mutation {
     rightId: Int!
   ): OperationResult
 
-  removeUserFromGroup(
-    userId: Int!
-    groupId: Int!
-  ): OperationResult
-
   resetUserPassword(
     id: Int!
   ): OperationResult
diff --git a/server/graph/schemas/group.graphql b/server/graph/schemas/group.graphql
new file mode 100644
index 00000000..082f0bd5
--- /dev/null
+++ b/server/graph/schemas/group.graphql
@@ -0,0 +1,69 @@
+# ===============================================
+# GROUPS
+# ===============================================
+
+extend type Query {
+  groups: GroupQuery
+}
+
+extend type Mutation {
+  groups: GroupMutation
+}
+
+# -----------------------------------------------
+# QUERIES
+# -----------------------------------------------
+
+type GroupQuery {
+  list(
+    filter: String
+    orderBy: String
+  ): [Group]
+}
+
+# -----------------------------------------------
+# MUTATIONS
+# -----------------------------------------------
+
+type GroupMutation {
+  create(
+    name: String!
+  ): GroupResponse
+
+  update(
+    id: Int!
+    name: String!
+  ): GroupResponse
+
+  delete(
+    id: Int!
+  ): DefaultResponse
+
+  assignUser(
+    groupId: Int!
+    userId: Int!
+  ): DefaultResponse
+
+  unassignUser(
+    groupId: Int!
+    userId: Int!
+  ): DefaultResponse
+}
+
+# -----------------------------------------------
+# TYPES
+# -----------------------------------------------
+
+type GroupResponse {
+  operation: ResponseStatus!
+  group: Group
+}
+
+type Group {
+  id: Int!
+  name: String!
+  rights: [String]
+  users: [User]
+  createdAt: Date!
+  updatedAt: Date!
+}