From d2b99a2032aa6fead54d4b70f955f740ecc5308a Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 3 May 2020 00:38:02 -0400 Subject: [PATCH] feat: timezone + dateFOrmat + appearance profile settings --- client/client-app.js | 25 +- client/components/admin/admin-users-edit.vue | 2 +- client/components/profile.vue | 3 +- client/components/profile/preferences.vue | 69 ---- client/components/profile/profile.vue | 313 ++++++++++++++----- client/store/user.js | 12 +- dev/webpack/webpack.dev.js | 5 + dev/webpack/webpack.prod.js | 5 + package.json | 1 + server/db/migrations-sqlite/2.4.13.js | 15 + server/db/migrations/2.4.13.js | 15 + server/graph/resolvers/user.js | 12 +- server/graph/schemas/user.graphql | 8 + server/models/users.js | 18 +- yarn.lock | 8 + 15 files changed, 344 insertions(+), 167 deletions(-) delete mode 100644 client/components/profile/preferences.vue create mode 100644 server/db/migrations-sqlite/2.4.13.js create mode 100644 server/db/migrations/2.4.13.js diff --git a/client/client-app.js b/client/client-app.js index f65ef5bb..a939ef43 100644 --- a/client/client-app.js +++ b/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')) + } + } }) // ---------------------------------- diff --git a/client/components/admin/admin-users-edit.vue b/client/components/admin/admin-users-edit.vue index 8da7556b..1f55ca69 100644 --- a/client/components/admin/admin-users-edit.vue +++ b/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 •••••••• diff --git a/client/components/profile.vue b/client/components/profile.vue index aa96883d..ab8d51a1 100644 --- a/client/components/profile.vue +++ b/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') } ] diff --git a/client/components/profile/preferences.vue b/client/components/profile/preferences.vue deleted file mode 100644 index c9ea0c67..00000000 --- a/client/components/profile/preferences.vue +++ /dev/null @@ -1,69 +0,0 @@ - - - - - diff --git a/client/components/profile/profile.vue b/client/components/profile/profile.vue index 8d86e58d..58b06560 100644 --- a/client/components/profile/profile.vue +++ b/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,7 +108,80 @@ @keydown.enter='editPop.jobTitle = false' @keydown.esc='editPop.jobTitle = false' ) - v-divider + + v-card.mt-3.animated.fadeInUp.wait-p2s + v-toolbar(color='blue-grey', dark, dense, flat) + v-toolbar-title + .subtitle-1 {{$t('profile:auth.title')}} + v-card-text.pt-0 + v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.provider')}} + v-toolbar( + flat + :color='$vuetify.theme.dark ? "grey darken-2" : "purple lighten-5"' + dense + :class='$vuetify.theme.dark ? "grey--text text--lighten-1" : "purple--text text--darken-4"' + ) + v-icon(:color='$vuetify.theme.dark ? "grey lighten-1" : "purple darken-4"') mdi-shield-lock + .subheading.ml-3 {{ user.providerName }} + //- v-divider.mt-3 + //- v-subheader.pl-0: span.subtitle-2 Two-Factor Authentication (2FA) + //- .caption.mb-2 2FA adds an extra layer of security by requiring a unique code generated on your smartphone when signing in. + //- v-btn(color='purple darken-4', disabled).ml-0 Enable 2FA + //- v-btn(color='purple darken-4', dark, depressed, disabled).ml-0 Disable 2FA + template(v-if='user.providerKey === `local`') + v-divider.mt-3 + v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.changePassword')}} + v-text-field( + ref='iptCurrentPass' + v-model='currentPass' + outlined + :label='$t(`profile:auth.currentPassword`)' + type='password' + prepend-inner-icon='mdi-form-textbox-password' + ) + v-text-field( + ref='iptNewPass' + v-model='newPass' + outlined + :label='$t(`profile:auth.newPassword`)' + type='password' + prepend-inner-icon='mdi-form-textbox-password' + autocomplete='off' + counter='255' + loading + ) + password-strength(slot='progress', v-model='newPass') + v-text-field( + ref='iptVerifyPass' + v-model='verifyPass' + outlined + :label='$t(`profile:auth.verifyPassword`)' + type='password' + prepend-inner-icon='mdi-form-textbox-password' + autocomplete='off' + hide-details + ) + v-card-chin + v-spacer + v-btn.px-4(color='purple darken-4', dark, depressed, @click='changePassword', :loading='changePassLoading') + v-icon(left) mdi-progress-check + span {{$t('profile:auth.changePassword')}} + v-flex(lg6 xs12) + //- v-card + //- v-toolbar(color='blue-grey', dark, dense, flat) + //- v-toolbar-title + //- .subtitle-1 Picture + //- v-card-title + //- v-avatar.blue(v-if='picture.kind === `initials`', :size='40') + //- span.white--text.subheading {{picture.initials}} + //- v-avatar(v-else-if='picture.kind === `image`', :size='40') + //- v-img(:src='picture.url') + //- 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 @@ -136,7 +212,7 @@ hide-details @keydown.enter='editPop.timezone = false' @keydown.esc='editPop.timezone = false' - style='height: 44px;' + style='height: 38px;' ) v-card-chin v-spacer @@ -148,81 +224,94 @@ ) 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-title - .subtitle-1 {{$t('profile:auth.title')}} - v-card-text.pt-0 - v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.provider')}} - v-toolbar( - flat - :color='$vuetify.theme.dark ? "grey darken-2" : "purple lighten-5"' - dense - :class='$vuetify.theme.dark ? "grey--text text--lighten-1" : "purple--text text--darken-4"' - ) - v-icon(:color='$vuetify.theme.dark ? "grey lighten-1" : "purple darken-4"') mdi-shield-lock - .subheading.ml-3 {{ user.providerName }} - //- v-divider.mt-3 - //- v-subheader.pl-0: span.subtitle-2 Two-Factor Authentication (2FA) - //- .caption.mb-2 2FA adds an extra layer of security by requiring a unique code generated on your smartphone when signing in. - //- v-btn(color='purple darken-4', disabled).ml-0 Enable 2FA - //- v-btn(color='purple darken-4', dark, depressed, disabled).ml-0 Disable 2FA - template(v-if='user.providerKey === `local`') - v-divider.mt-3 - v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.changePassword')}} - v-text-field( - ref='iptCurrentPass' - v-model='currentPass' - outlined - :label='$t(`profile:auth.currentPassword`)' - type='password' - prepend-inner-icon='mdi-textbox-password' - ) - v-text-field( - ref='iptNewPass' - v-model='newPass' - outlined - :label='$t(`profile:auth.newPassword`)' - type='password' - prepend-inner-icon='mdi-textbox-password' - autocomplete='off' - counter='255' - loading - ) - password-strength(slot='progress', v-model='newPass') - v-text-field( - ref='iptVerifyPass' - v-model='verifyPass' - outlined - :label='$t(`profile:auth.verifyPassword`)' - type='password' - prepend-inner-icon='mdi-textbox-password' - autocomplete='off' - hide-details - ) - v-card-chin - v-spacer - v-btn.px-4(color='purple darken-4', dark, depressed, @click='changePassword', :loading='changePassLoading') - v-icon(left) mdi-progress-check - span {{$t('profile:auth.changePassword')}} - v-flex(lg6 xs12) - //- v-card - //- v-toolbar(color='primary', dark, dense, flat) - //- v-toolbar-title - //- .subtitle-1 Picture - //- v-card-title - //- v-avatar.blue(v-if='picture.kind === `initials`', :size='40') - //- span.white--text.subheading {{picture.initials}} - //- v-avatar(v-else-if='picture.kind === `image`', :size='40') - //- v-img(:src='picture.url') - //- v-btn(outlined).mx-4 Upload Picture - //- v-btn(outlined, disabled) Remove Picture - v-card.animated.fadeInUp.wait-p2s + 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 diff --git a/client/store/user.js b/client/store/user.js index ca2bf4ea..e27bac7d 100644 --- a/client/store/user.js +++ b/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 diff --git a/dev/webpack/webpack.dev.js b/dev/webpack/webpack.dev.js index 79710337..3a5e50e3 100644 --- a/dev/webpack/webpack.dev.js +++ b/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' } diff --git a/dev/webpack/webpack.prod.js b/dev/webpack/webpack.prod.js index 431f6282..62f06023 100644 --- a/dev/webpack/webpack.prod.js +++ b/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' } diff --git a/package.json b/package.json index a79fc2f4..52a6c955 100644 --- a/package.json +++ b/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", diff --git a/server/db/migrations-sqlite/2.4.13.js b/server/db/migrations-sqlite/2.4.13.js new file mode 100644 index 00000000..396a3d37 --- /dev/null +++ b/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 => { } diff --git a/server/db/migrations/2.4.13.js b/server/db/migrations/2.4.13.js new file mode 100644 index 00000000..396a3d37 --- /dev/null +++ b/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 => { } diff --git a/server/graph/resolvers/user.js b/server/graph/resolvers/user.js index e453849a..8abb31f2 100644 --- a/server/graph/resolvers/user.js +++ b/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) diff --git a/server/graph/schemas/user.graphql b/server/graph/schemas/user.graphql index aa17fcac..58b473ff 100644 --- a/server/graph/schemas/user.graphql +++ b/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 diff --git a/server/models/users.js b/server/models/users.js index 27e28d57..7ef55715 100644 --- a/server/models/users.js +++ b/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() diff --git a/yarn.lock b/yarn.lock index 9c522b55..3be395a6 100644 --- a/yarn.lock +++ b/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"