Browse Source

feat: timezone + dateFOrmat + appearance profile settings

NGPixel 5 years ago
parent
commit
d2b99a2032

+ 22 - 3
client/client-app.js

@@ -15,7 +15,7 @@ import Vuetify from 'vuetify/lib'
 import Velocity from 'velocity-animate'
 import Vuescroll from 'vuescroll/dist/vuescroll-native'
 import Hammer from 'hammerjs'
-import moment from 'moment'
+import moment from 'moment-timezone'
 import VueMoment from 'vue-moment'
 import store from './store'
 import Cookies from 'js-cookie'
@@ -189,6 +189,12 @@ let bootstrap = () => {
   // ====================================
 
   const i18n = localization.init()
+
+  let darkModeEnabled = siteConfig.darkMode
+  if ((store.get('user/appearance') || '').length > 0) {
+    darkModeEnabled = (store.get('user/appearance') === 'dark')
+  }
+
   window.WIKI = new Vue({
     el: '#root',
     components: {},
@@ -199,9 +205,22 @@ let bootstrap = () => {
     vuetify: new Vuetify({
       rtl: siteConfig.rtl,
       theme: {
-        dark: siteConfig.darkMode
+        dark: darkModeEnabled
       }
-    })
+    }),
+    mounted () {
+      this.$moment.locale(siteConfig.lang)
+      if ((store.get('user/dateFormat') || '').length > 0) {
+        this.$moment.updateLocale(this.$moment.locale(), {
+          longDateFormat: {
+            'L': store.get('user/dateFormat')
+          }
+        })
+      }
+      if ((store.get('user/timezone') || '').length > 0) {
+        this.$moment.tz.setDefault(store.get('user/timezone'))
+      }
+    }
   })
 
   // ----------------------------------

+ 1 - 1
client/components/admin/admin-users-edit.vue

@@ -133,7 +133,7 @@
               v-divider
               v-list-item
                 v-list-item-avatar(size='32')
-                  v-icon mdi-textbox-password
+                  v-icon mdi-form-textbox-password
                 v-list-item-content
                   v-list-item-title {{$t('admin:users.password')}}
                   v-list-item-subtitle ••••••••

+ 1 - 2
client/components/profile.vue

@@ -22,7 +22,7 @@
         //-     v-list-item-title {{$t('profile:comments.title')}}
         //-     v-list-item-subtitle.caption.grey--text.text--lighten-1 Coming soon
 
-    v-content(:class='darkMode ? "grey darken-4" : "grey lighten-5"')
+    v-content(:class='$vuetify.theme.dark ? "grey darken-4" : "grey lighten-5"')
       transition(name='profile-router')
         router-view
 
@@ -42,7 +42,6 @@ const router = new VueRouter({
   routes: [
     { path: '/', redirect: '/profile' },
     { path: '/profile', component: () => import(/* webpackChunkName: "profile" */ './profile/profile.vue') },
-    // { path: '/preferences', component: () => import(/* webpackChunkName: "profile" */ './profile/preferences.vue') },
     { path: '/pages', component: () => import(/* webpackChunkName: "profile" */ './profile/pages.vue') },
     { path: '/comments', component: () => import(/* webpackChunkName: "profile" */ './profile/comments.vue') }
   ]

+ 0 - 69
client/components/profile/preferences.vue

@@ -1,69 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, fill-height, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .headline.primary--text Preferences
-        .subheading.grey--text Site settings
-        v-form.pt-3
-          v-layout(row wrap)
-            v-flex(lg6 xs12)
-              v-form
-                v-card
-                  v-toolbar(color='primary', dark, dense, flat)
-                    v-toolbar-title
-                      .subheading Display
-                  v-card-text
-                    v-subheader.pl-0 Locale
-                    v-select(
-                      outline
-                      background-color='grey lighten-2'
-                      hide-details
-                      )
-                    v-divider.mt-3
-                    v-subheader.pl-0 Timezone
-                    v-select(
-                      outline
-                      background-color='grey lighten-2'
-                      hide-details
-                      )
-                  v-card-chin
-                    v-spacer
-                    v-btn(color='primary')
-                      v-icon(left) chevron_right
-                      span Save
-            v-flex(lg6 xs12)
-              v-card
-                v-toolbar(color='primary', dark, dense, flat)
-                  v-toolbar-title
-                    .subheading Editing
-                v-card-text
-                  v-subheader.pl-0 Default Editor
-                  v-select(
-                    outline
-                    background-color='grey lighten-2'
-                    hide-details
-                    )
-                v-card-chin
-                  v-spacer
-                  v-btn(color='primary')
-                    v-icon(left) chevron_right
-                    span Save
-
-</template>
-
-<script>
-/* global siteConfig */
-
-export default {
-  data() {
-    return { }
-  },
-  computed: {
-    darkMode() { return siteConfig.darkMode }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 206 - 59
client/components/profile/profile.vue

@@ -8,12 +8,15 @@
             .headline.primary--text.animated.fadeInLeft {{$t('profile:title')}}
             .subheading.grey--text.animated.fadeInLeft {{$t('profile:subtitle')}}
           v-spacer
+          v-btn.animated.fadeInDown(color='success', depressed, @click='saveProfile', :loading='saveLoading', large)
+            v-icon(left) mdi-check
+            span {{$t('common:actions.save')}}
           //- v-btn.animated.fadeInDown(outlined, color='primary', disabled).mr-0
           //-   v-icon(left) mdi-earth
           //-   span {{$t('profile:viewPublicProfile')}}
       v-flex(lg6 xs12)
         v-card.animated.fadeInUp
-          v-toolbar(color='primary', dark, dense, flat)
+          v-toolbar(color='blue-grey', dark, dense, flat)
             v-toolbar-title.subtitle-1 {{$t('profile:myInfo')}}
           v-list(two-line, dense)
             v-list-item
@@ -105,56 +108,9 @@
                       @keydown.enter='editPop.jobTitle = false'
                       @keydown.esc='editPop.jobTitle = false'
                     )
-            v-divider
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-map-clock-outline
-              v-list-item-content
-                v-list-item-title {{$t('profile:timezone')}}
-                v-list-item-subtitle {{ user.timezone }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.timezone'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  max-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptTimezone`)')
-                      v-icon(left) mdi-pencil
-                      span {{ $t('common:actions:edit') }}
-                  v-card(flat)
-                    v-select(
-                      ref='iptTimezone'
-                      :items='timezones'
-                      v-model='user.timezone'
-                      :label='$t(`profile:timezone`)'
-                      solo
-                      flat
-                      dense
-                      hide-details
-                      @keydown.enter='editPop.timezone = false'
-                      @keydown.esc='editPop.timezone = false'
-                      style='height: 44px;'
-                    )
-                    v-card-chin
-                      v-spacer
-                      v-btn(
-                        small
-                        text
-                        color='primary'
-                        @click='editPop.timezone = false'
-                        )
-                        v-icon(left) mdi-check
-                        span {{$t('common:actions.ok')}}
-          v-card-chin
-            v-spacer
-            v-btn.px-4(color='success', depressed, @click='saveProfile', :loading='saveLoading')
-              v-icon(left) mdi-content-save
-              span {{$t('common:actions.save')}}
+
         v-card.mt-3.animated.fadeInUp.wait-p2s
-          v-toolbar(color='primary', dark, dense, flat)
+          v-toolbar(color='blue-grey', dark, dense, flat)
             v-toolbar-title
               .subtitle-1 {{$t('profile:auth.title')}}
           v-card-text.pt-0
@@ -181,7 +137,7 @@
                 outlined
                 :label='$t(`profile:auth.currentPassword`)'
                 type='password'
-                prepend-inner-icon='mdi-textbox-password'
+                prepend-inner-icon='mdi-form-textbox-password'
                 )
               v-text-field(
                 ref='iptNewPass'
@@ -189,7 +145,7 @@
                 outlined
                 :label='$t(`profile:auth.newPassword`)'
                 type='password'
-                prepend-inner-icon='mdi-textbox-password'
+                prepend-inner-icon='mdi-form-textbox-password'
                 autocomplete='off'
                 counter='255'
                 loading
@@ -201,7 +157,7 @@
                 outlined
                 :label='$t(`profile:auth.verifyPassword`)'
                 type='password'
-                prepend-inner-icon='mdi-textbox-password'
+                prepend-inner-icon='mdi-form-textbox-password'
                 autocomplete='off'
                 hide-details
                 )
@@ -212,7 +168,7 @@
               span {{$t('profile:auth.changePassword')}}
       v-flex(lg6 xs12)
         //- v-card
-        //-   v-toolbar(color='primary', dark, dense, flat)
+        //-   v-toolbar(color='blue-grey', dark, dense, flat)
         //-     v-toolbar-title
         //-       .subtitle-1 Picture
         //-   v-card-title
@@ -223,6 +179,139 @@
         //-     v-btn(outlined).mx-4 Upload Picture
         //-     v-btn(outlined, disabled) Remove Picture
         v-card.animated.fadeInUp.wait-p2s
+          v-toolbar(color='blue-grey', dark, dense, flat)
+            v-toolbar-title.subtitle-1 {{$t('profile:preferences')}}
+          v-list(two-line, dense)
+            v-list-item
+              v-list-item-avatar(size='32')
+                v-icon mdi-map-clock-outline
+              v-list-item-content
+                v-list-item-title {{$t('profile:timezone')}}
+                v-list-item-subtitle {{ user.timezone }}
+              v-list-item-action
+                v-menu(
+                  v-model='editPop.timezone'
+                  :close-on-content-click='false'
+                  min-width='350'
+                  max-width='350'
+                  left
+                  )
+                  template(v-slot:activator='{ on }')
+                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptTimezone`)')
+                      v-icon(left) mdi-pencil
+                      span {{ $t('common:actions:edit') }}
+                  v-card(flat)
+                    v-select(
+                      ref='iptTimezone'
+                      :items='timezones'
+                      v-model='user.timezone'
+                      :label='$t(`profile:timezone`)'
+                      solo
+                      flat
+                      dense
+                      hide-details
+                      @keydown.enter='editPop.timezone = false'
+                      @keydown.esc='editPop.timezone = false'
+                      style='height: 38px;'
+                    )
+                    v-card-chin
+                      v-spacer
+                      v-btn(
+                        small
+                        text
+                        color='primary'
+                        @click='editPop.timezone = false'
+                        )
+                        v-icon(left) mdi-check
+                        span {{$t('common:actions.ok')}}
+            v-divider
+            v-list-item
+              v-list-item-avatar(size='32')
+                v-icon mdi-calendar-month-outline
+              v-list-item-content
+                v-list-item-title {{$t('profile:dateFormat')}}
+                v-list-item-subtitle {{ user.dateFormat && user.dateFormat.length > 0 ? user.dateFormat : $t('profile:localeDefault') }}
+              v-list-item-action
+                v-menu(
+                  v-model='editPop.dateFormat'
+                  :close-on-content-click='false'
+                  min-width='350'
+                  max-width='350'
+                  left
+                  )
+                  template(v-slot:activator='{ on }')
+                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptDateFormat`)')
+                      v-icon(left) mdi-pencil
+                      span {{ $t('common:actions:edit') }}
+                  v-card(flat)
+                    v-select(
+                      ref='iptDateFormat'
+                      :items='dateFormats'
+                      v-model='user.dateFormat'
+                      :label='$t(`profile:dateFormat`)'
+                      solo
+                      flat
+                      dense
+                      hide-details
+                      @keydown.enter='editPop.dateFormat = false'
+                      @keydown.esc='editPop.dateFormat = false'
+                      style='height: 38px;'
+                    )
+                    v-card-chin
+                      v-spacer
+                      v-btn(
+                        small
+                        text
+                        color='primary'
+                        @click='editPop.dateFormat = false'
+                        )
+                        v-icon(left) mdi-check
+                        span {{$t('common:actions.ok')}}
+            v-divider
+            v-list-item
+              v-list-item-avatar(size='32')
+                v-icon mdi-palette
+              v-list-item-content
+                v-list-item-title {{$t('profile:appearance')}}
+                v-list-item-subtitle {{ currentAppearance }}
+              v-list-item-action
+                v-menu(
+                  v-model='editPop.appearance'
+                  :close-on-content-click='false'
+                  min-width='350'
+                  max-width='350'
+                  left
+                  )
+                  template(v-slot:activator='{ on }')
+                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptAppearance`)')
+                      v-icon(left) mdi-pencil
+                      span {{ $t('common:actions:edit') }}
+                  v-card(flat)
+                    v-select(
+                      ref='iptAppearance'
+                      :items='appearances'
+                      v-model='user.appearance'
+                      :label='$t(`profile:appearance`)'
+                      solo
+                      flat
+                      dense
+                      hide-details
+                      @keydown.enter='editPop.appearance = false'
+                      @keydown.esc='editPop.appearance = false'
+                      style='height: 38px;'
+                    )
+                    v-card-chin
+                      v-spacer
+                      v-btn(
+                        small
+                        text
+                        color='primary'
+                        @click='editPop.appearance = false'
+                        )
+                        v-icon(left) mdi-check
+                        span {{$t('common:actions.ok')}}
+
+        v-card.mt-3.animated.fadeInUp.wait-p3s
           v-toolbar(color='primary', dark, dense, flat)
             v-toolbar-title
               .subtitle-1 {{$t('profile:groups.title')}}
@@ -234,7 +323,8 @@
                 v-list-item-content
                   v-list-item-title.body-2 {{grp}}
               v-divider(v-if='idx < user.groups.length - 1')
-        v-card.mt-3.animated.fadeInUp.wait-p3s
+
+        v-card.mt-3.animated.fadeInUp.wait-p4s
           v-toolbar(color='teal', dark, dense, flat)
             v-toolbar-title
               .subtitle-1 {{$t('profile:activity.title')}}
@@ -261,6 +351,8 @@ import validate from 'validate.js'
 
 import PasswordStrength from '../common/password-strength.vue'
 
+/* global WIKI, siteConfig */
+
 export default {
   i18nOptions: {
     namespaces: ['profile', 'auth']
@@ -277,6 +369,8 @@ export default {
         location: '',
         jobTitle: '',
         timezone: '',
+        dateFormat: '',
+        appearance: '',
         createdAt: '1970-01-01',
         updatedAt: '1970-01-01',
         lastLoginAt: '1970-01-01',
@@ -289,7 +383,9 @@ export default {
         name: false,
         location: false,
         jobTitle: false,
-        timezone: false
+        timezone: false,
+        dateFormat: false,
+        appearance: false
       },
       timezones: [
         { text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },
@@ -546,6 +642,26 @@ export default {
     }
   },
   computed: {
+    dateFormats () {
+      return [
+        { text: this.$t('profile:localeDefault'), value: '' },
+        { text: 'DD/MM/YYYY', value: 'DD/MM/YYYY' },
+        { text: 'DD.MM.YYYY', value: 'DD.MM.YYYY' },
+        { text: 'MM/DD/YYYY', value: 'MM/DD/YYYY' },
+        { text: 'YYYY-MM-DD', value: 'YYYY-MM-DD' },
+        { text: 'YYYY/MM/DD', value: 'YYYY/MM/DD' }
+      ]
+    },
+    appearances () {
+      return [
+        { text: this.$t('profile:appearanceDefault'), value: '' },
+        { text: this.$t('profile:appearanceLight'), value: 'light' },
+        { text: this.$t('profile:appearanceDark'), value: 'dark' }
+      ]
+    },
+    currentAppearance () {
+      return _.get(_.find(this.appearances, ['value', this.user.appearance]), 'text', false) || this.$t('profile:appearanceDefault')
+    },
     pictureUrl: get('user/pictureUrl'),
     picture () {
       if (this.pictureUrl && this.pictureUrl.length > 1) {
@@ -566,6 +682,33 @@ export default {
       }
     }
   },
+  watch: {
+    'user.appearance': (newValue, oldValue) => {
+      if (newValue === '') {
+        WIKI.$vuetify.theme.dark = siteConfig.darkMode
+      } else {
+        WIKI.$vuetify.theme.dark = (newValue === 'dark')
+      }
+    },
+    'user.dateFormat': (newValue, oldValue) => {
+      if (newValue === '') {
+        WIKI.$moment.updateLocale(WIKI.$moment.locale(), null)
+      } else {
+        WIKI.$moment.updateLocale(WIKI.$moment.locale(), {
+          longDateFormat: {
+            'L': newValue
+          }
+        })
+      }
+    },
+    'user.timezone': (newValue, oldValue) => {
+      if (newValue === '') {
+        WIKI.$moment.tz.setDefault()
+      } else {
+        WIKI.$moment.tz.setDefault(newValue)
+      }
+    }
+  },
   methods: {
     /**
      * Focus an input after delay
@@ -587,9 +730,9 @@ export default {
       try {
         const respRaw = await this.$apollo.mutate({
           mutation: gql`
-            mutation ($name: String!, $location: String!, $jobTitle: String!, $timezone: String!) {
+            mutation ($name: String!, $location: String!, $jobTitle: String!, $timezone: String!, $dateFormat: String!, $appearance: String!) {
               users {
-                updateProfile(name: $name, location: $location, jobTitle: $jobTitle, timezone: $timezone) {
+                updateProfile(name: $name, location: $location, jobTitle: $jobTitle, timezone: $timezone, dateFormat: $dateFormat, appearance: $appearance) {
                   responseResult {
                     succeeded
                     errorCode
@@ -605,7 +748,9 @@ export default {
             name: this.user.name,
             location: this.user.location,
             jobTitle: this.user.jobTitle,
-            timezone: this.user.timezone
+            timezone: this.user.timezone,
+            dateFormat: this.user.dateFormat,
+            appearance: this.user.appearance
           }
         })
         const resp = _.get(respRaw, 'data.users.updateProfile.responseResult', {})
@@ -752,6 +897,8 @@ export default {
               location
               jobTitle
               timezone
+              dateFormat
+              appearance
               createdAt
               updatedAt
               lastLoginAt

+ 9 - 3
client/store/user.js

@@ -9,6 +9,9 @@ const state = {
   pictureUrl: '',
   localeCode: '',
   defaultEditor: '',
+  timezone: '',
+  dateFormat: '',
+  appearance: '',
   permissions: [],
   iat: 0,
   exp: 0,
@@ -28,9 +31,12 @@ export default {
           st.id = jwtData.id
           st.email = jwtData.email
           st.name = jwtData.name
-          st.pictureUrl = jwtData.pictureUrl
-          st.localeCode = jwtData.localeCode
-          st.defaultEditor = jwtData.defaultEditor
+          st.pictureUrl = jwtData.av
+          st.localeCode = jwtData.lc
+          st.timezone = jwtData.tz || Intl.DateTimeFormat().resolvedOptions().timeZone || ''
+          st.dateFormat = jwtData.df || ''
+          st.appearance = jwtData.ap || ''
+          // st.defaultEditor = jwtData.defaultEditor
           st.permissions = jwtData.permissions
           st.iat = jwtData.iat
           st.exp = jwtData.exp

+ 5 - 0
dev/webpack/webpack.dev.js

@@ -8,6 +8,7 @@ const { VueLoaderPlugin } = require('vue-loader')
 const CopyWebpackPlugin = require('copy-webpack-plugin')
 const HtmlWebpackPlugin = require('html-webpack-plugin')
 const HtmlWebpackPugPlugin = require('html-webpack-pug-plugin')
+const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin')
 const SriWebpackPlugin = require('webpack-subresource-integrity')
 const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')
 const WriteFilePlugin = require('write-file-webpack-plugin')
@@ -179,6 +180,10 @@ module.exports = {
   plugins: [
     new VueLoaderPlugin(),
     new VuetifyLoaderPlugin(),
+    new MomentTimezoneDataPlugin({
+      startYear: 2017,
+      endYear: (new Date().getFullYear()) + 5
+    }),
     new CopyWebpackPlugin([
       { from: 'client/static' },
       { from: './node_modules/prismjs/components', to: 'js/prism' }

+ 5 - 0
dev/webpack/webpack.prod.js

@@ -10,6 +10,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
 const HtmlWebpackPlugin = require('html-webpack-plugin')
 const HtmlWebpackPugPlugin = require('html-webpack-pug-plugin')
 const MiniCssExtractPlugin = require('mini-css-extract-plugin')
+const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin')
 const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
 const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
 const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')
@@ -184,6 +185,10 @@ module.exports = {
     new VueLoaderPlugin(),
     new VuetifyLoaderPlugin(),
     new webpack.BannerPlugin('Wiki.js - wiki.js.org - Licensed under AGPL'),
+    new MomentTimezoneDataPlugin({
+      startYear: 2017,
+      endYear: (new Date().getFullYear()) + 5
+    }),
     new CopyWebpackPlugin([
       { from: 'client/static' },
       { from: './node_modules/prismjs/components', to: 'js/prism' }

+ 1 - 0
package.json

@@ -254,6 +254,7 @@
     "mermaid": "8.5.0",
     "mini-css-extract-plugin": "0.9.0",
     "moment-duration-format": "2.3.2",
+    "moment-timezone-data-webpack-plugin": "1.3.0",
     "offline-plugin": "5.0.7",
     "optimize-css-assets-webpack-plugin": "5.0.3",
     "postcss-cssnext": "3.1.0",

+ 15 - 0
server/db/migrations-sqlite/2.4.13.js

@@ -0,0 +1,15 @@
+exports.up = knex => {
+  return knex.schema
+    .alterTable('pages', table => {
+      table.json('extra').notNullable().defaultTo('{}')
+    })
+    .alterTable('pageHistory', table => {
+      table.json('extra').notNullable().defaultTo('{}')
+    })
+    .alterTable('users', table => {
+      table.string('dateFormat').notNullable().defaultTo('')
+      table.string('appearance').notNullable().defaultTo('')
+    })
+}
+
+exports.down = knex => { }

+ 15 - 0
server/db/migrations/2.4.13.js

@@ -0,0 +1,15 @@
+exports.up = knex => {
+  return knex.schema
+    .alterTable('pages', table => {
+      table.json('extra').notNullable().defaultTo('{}')
+    })
+    .alterTable('pageHistory', table => {
+      table.json('extra').notNullable().defaultTo('{}')
+    })
+    .alterTable('users', table => {
+      table.string('dateFormat').notNullable().defaultTo('')
+      table.string('appearance').notNullable().defaultTo('')
+    })
+}
+
+exports.down = knex => { }

+ 11 - 1
server/graph/resolvers/user.js

@@ -147,12 +147,22 @@ module.exports = {
           throw new WIKI.Error.AuthAccountNotVerified()
         }
 
+        if (!['', 'DD/MM/YYYY', 'DD.MM.YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'].includes(args.dateFormat)) {
+          throw new WIKI.Error.InputInvalid()
+        }
+
+        if (!['', 'light', 'dark'].includes(args.appearance)) {
+          throw new WIKI.Error.InputInvalid()
+        }
+
         await WIKI.models.users.updateUser({
           id: usr.id,
           name: _.trim(args.name),
           jobTitle: _.trim(args.jobTitle),
           location: _.trim(args.location),
-          timezone: args.timezone
+          timezone: args.timezone,
+          dateFormat: args.dateFormat,
+          appearance: args.appearance
         })
 
         const newToken = await WIKI.models.users.refreshToken(usr.id)

+ 8 - 0
server/graph/schemas/user.graphql

@@ -57,6 +57,8 @@ type UserMutation {
     location: String
     jobTitle: String
     timezone: String
+    dateFormat: String
+    appearance: String
   ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
 
   delete(
@@ -84,6 +86,8 @@ type UserMutation {
     location: String!
     jobTitle: String!
     timezone: String!
+    dateFormat: String!
+    appearance: String!
   ): UserTokenResponse
 
   changePassword(
@@ -128,6 +132,8 @@ type User {
   location: String!
   jobTitle: String!
   timezone: String!
+  dateFormat: String!
+  appearance: String!
   createdAt: Date!
   updatedAt: Date!
   lastLoginAt: Date
@@ -145,6 +151,8 @@ type UserProfile {
   location: String!
   jobTitle: String!
   timezone: String!
+  dateFormat: String!
+  appearance: String!
   createdAt: Date!
   updatedAt: Date!
   lastLoginAt: Date

+ 13 - 5
server/models/users.js

@@ -350,10 +350,12 @@ module.exports = class User extends Model {
         id: user.id,
         email: user.email,
         name: user.name,
-        pictureUrl: user.pictureUrl,
-        timezone: user.timezone,
-        localeCode: user.localeCode,
-        defaultEditor: user.defaultEditor,
+        av: user.pictureUrl,
+        tz: user.timezone,
+        lc: user.localeCode,
+        df: user.dateFormat,
+        ap: user.appearance,
+        // defaultEditor: user.defaultEditor,
         permissions: user.getGlobalPermissions(),
         groups: user.getGroups()
       }, {
@@ -548,7 +550,7 @@ module.exports = class User extends Model {
    *
    * @param {Object} param0 User ID and fields to update
    */
-  static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone }) {
+  static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone, dateFormat, appearance }) {
     const usr = await WIKI.models.users.query().findById(id)
     if (usr) {
       let usrData = {}
@@ -594,6 +596,12 @@ module.exports = class User extends Model {
       if (!_.isEmpty(timezone) && timezone !== usr.timezone) {
         usrData.timezone = timezone
       }
+      if (!_.isNil(dateFormat) && dateFormat !== usr.dateFormat) {
+        usrData.dateFormat = dateFormat
+      }
+      if (!_.isNil(appearance) && appearance !== usr.appearance) {
+        usrData.appearance = appearance
+      }
       await WIKI.models.users.query().patch(usrData).findById(id)
     } else {
       throw new WIKI.Error.UserNotFound()

+ 8 - 0
yarn.lock

@@ -10363,6 +10363,14 @@ moment-mini@^2.22.1:
   resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.24.0.tgz#fa68d98f7fe93ae65bf1262f6abb5fb6983d8d18"
   integrity sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==
 
+moment-timezone-data-webpack-plugin@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/moment-timezone-data-webpack-plugin/-/moment-timezone-data-webpack-plugin-1.3.0.tgz#8d5b7ffe42f0506933195779063a74cfff11aab1"
+  integrity sha512-0V0xnHZpdHLsSerIQ2yNEPBC3uJWfU/zNT3nB0PO+tjmGHuNeUWqNDiw7ZpLo54uER6/OAE75EJ7ThmlwkGuZw==
+  dependencies:
+    find-cache-dir "^3.0.0"
+    make-dir "^3.0.0"
+
 moment-timezone@0.5.28:
   version "0.5.28"
   resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338"