Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
55b0b00c
Unverified
Commit
55b0b00c
authored
Sep 12, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: update profile + user theme
parent
8e87a5d4
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
311 additions
and
144 deletions
+311
-144
3.0.0.js
server/db/migrations/3.0.0.js
+2
-2
user.js
server/graph/resolvers/user.js
+49
-41
user.graphql
server/graph/schemas/user.graphql
+16
-29
users.js
server/models/users.js
+1
-1
quasar.config.js
ux/quasar.config.js
+6
-6
App.vue
ux/src/App.vue
+28
-3
apollo.js
ux/src/boot/apollo.js
+2
-2
AccountMenu.vue
ux/src/components/AccountMenu.vue
+7
-13
HeaderNav.vue
ux/src/components/HeaderNav.vue
+2
-2
UserEditOverlay.vue
ux/src/components/UserEditOverlay.vue
+27
-9
en.json
ux/src/i18n/locales/en.json
+9
-2
ProfileLayout.vue
ux/src/layouts/ProfileLayout.vue
+2
-1
Login.vue
ux/src/pages/Login.vue
+2
-2
Profile.vue
ux/src/pages/Profile.vue
+93
-14
user.js
ux/src/stores/user.js
+65
-17
No files found.
server/db/migrations/3.0.0.js
View file @
55b0b00c
...
...
@@ -580,7 +580,7 @@ exports.up = async knex => {
timezone
:
'America/New_York'
,
dateFormat
:
'YYYY-MM-DD'
,
timeFormat
:
'12h'
,
darkMode
:
false
appearance
:
'site'
},
localeCode
:
'en'
},
...
...
@@ -597,7 +597,7 @@ exports.up = async knex => {
timezone
:
'America/New_York'
,
dateFormat
:
'YYYY-MM-DD'
,
timeFormat
:
'12h'
,
darkMode
:
false
appearance
:
'site'
},
localeCode
:
'en'
}
...
...
server/graph/resolvers/user.js
View file @
55b0b00c
...
...
@@ -40,6 +40,10 @@ module.exports = {
async
userById
(
obj
,
args
,
context
,
info
)
{
const
usr
=
await
WIKI
.
models
.
users
.
query
().
findById
(
args
.
id
)
if
(
!
usr
)
{
throw
new
Error
(
'Invalid User'
)
}
// const str = _.get(WIKI.auth.strategies, usr.providerKey)
// str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])
// usr.providerName = str.displayName
...
...
@@ -56,25 +60,25 @@ module.exports = {
return
usr
},
async
profile
(
obj
,
args
,
context
,
info
)
{
if
(
!
context
.
req
.
user
||
context
.
req
.
user
.
id
<
1
||
context
.
req
.
user
.
id
===
2
)
{
throw
new
WIKI
.
Error
.
AuthRequired
()
}
const
usr
=
await
WIKI
.
models
.
users
.
query
().
findById
(
context
.
req
.
user
.
id
)
if
(
!
usr
.
isActive
)
{
throw
new
WIKI
.
Error
.
AuthAccountBanned
()
}
//
async profile (obj, args, context, info) {
//
if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {
//
throw new WIKI.Error.AuthRequired()
//
}
//
const usr = await WIKI.models.users.query().findById(context.req.user.id)
//
if (!usr.isActive) {
//
throw new WIKI.Error.AuthAccountBanned()
//
}
const
providerInfo
=
_
.
get
(
WIKI
.
auth
.
strategies
,
usr
.
providerKey
,
{})
//
const providerInfo = _.get(WIKI.auth.strategies, usr.providerKey, {})
usr
.
providerName
=
providerInfo
.
displayName
||
'Unknown'
usr
.
lastLoginAt
=
usr
.
lastLoginAt
||
usr
.
updatedAt
usr
.
password
=
''
usr
.
providerId
=
''
usr
.
tfaSecret
=
''
//
usr.providerName = providerInfo.displayName || 'Unknown'
//
usr.lastLoginAt = usr.lastLoginAt || usr.updatedAt
//
usr.password = ''
//
usr.providerId = ''
//
usr.tfaSecret = ''
return
usr
},
//
return usr
//
},
async
lastLogins
(
obj
,
args
,
context
,
info
)
{
return
WIKI
.
models
.
users
.
query
()
.
select
(
'id'
,
'name'
,
'lastLoginAt'
)
...
...
@@ -193,7 +197,7 @@ module.exports = {
},
async
updateProfile
(
obj
,
args
,
context
)
{
try
{
if
(
!
context
.
req
.
user
||
context
.
req
.
user
.
id
<
1
||
context
.
req
.
user
.
id
===
2
)
{
if
(
!
context
.
req
.
user
||
context
.
req
.
user
.
id
===
WIKI
.
auth
.
guest
.
id
)
{
throw
new
WIKI
.
Error
.
AuthRequired
()
}
const
usr
=
await
WIKI
.
models
.
users
.
query
().
findById
(
context
.
req
.
user
.
id
)
...
...
@@ -204,29 +208,33 @@ 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
))
{
if
(
args
.
dateFormat
&&
!
[
''
,
'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
))
{
if
(
args
.
appearance
&&
!
[
'site
'
,
'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
,
dateFormat
:
args
.
dateFormat
,
appearance
:
args
.
appearance
await
WIKI
.
models
.
users
.
query
().
findById
(
usr
.
id
).
patch
({
name
:
args
.
name
?.
trim
()
??
usr
.
name
,
meta
:
{
...
usr
.
meta
,
location
:
args
.
location
?.
trim
()
??
usr
.
meta
.
location
,
jobTitle
:
args
.
jobTitle
?.
trim
()
??
usr
.
meta
.
jobTitle
,
pronouns
:
args
.
pronouns
?.
trim
()
??
usr
.
meta
.
pronouns
},
prefs
:
{
...
usr
.
prefs
,
timezone
:
args
.
timezone
||
usr
.
prefs
.
timezone
,
dateFormat
:
args
.
dateFormat
??
usr
.
prefs
.
dateFormat
,
timeFormat
:
args
.
timeFormat
??
usr
.
prefs
.
timeFormat
,
appearance
:
args
.
appearance
||
usr
.
prefs
.
appearance
}
})
const
newToken
=
await
WIKI
.
models
.
users
.
refreshToken
(
usr
.
id
)
return
{
operation
:
graphHelper
.
generateSuccess
(
'User profile updated successfully'
),
jwt
:
newToken
.
token
operation
:
graphHelper
.
generateSuccess
(
'User profile updated successfully'
)
}
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
...
...
@@ -273,15 +281,15 @@ module.exports = {
groups
(
usr
)
{
return
usr
.
$relatedQuery
(
'groups'
)
}
},
UserProfile
:
{
async
groups
(
usr
)
{
const
usrGroups
=
await
usr
.
$relatedQuery
(
'groups'
)
return
usrGroups
.
map
(
g
=>
g
.
name
)
},
async
pagesTotal
(
usr
)
{
const
result
=
await
WIKI
.
models
.
pages
.
query
().
count
(
'* as total'
).
where
(
'creatorId'
,
usr
.
id
).
first
()
return
_
.
toSafeInteger
(
result
.
total
)
}
}
// UserProfile: {
// async groups (usr) {
// const usrGroups = await usr.$relatedQuery('groups')
// return usrGroups.map(g => g.name)
// },
// async pagesTotal (usr) {
// const result = await WIKI.models.pages.query().count('* as total').where('creatorId', usr.id).first()
// return _.toSafeInteger(result.total)
// }
// }
}
server/graph/schemas/user.graphql
View file @
55b0b00c
...
...
@@ -16,8 +16,6 @@ extend type Query {
id
:
UUID
!
):
User
profile
:
UserProfile
lastLogins
:
[
UserLastLogin
]
}
...
...
@@ -66,13 +64,15 @@ extend type Mutation {
):
DefaultResponse
updateProfile
(
name
:
String
!
location
:
String
!
jobTitle
:
String
!
timezone
:
String
!
dateFormat
:
String
!
appearance
:
String
!
):
UserTokenResponse
name
:
String
location
:
String
jobTitle
:
String
pronouns
:
String
timezone
:
String
dateFormat
:
String
timeFormat
:
String
appearance
:
UserSiteAppearance
):
DefaultResponse
}
# -----------------------------------------------
...
...
@@ -110,32 +110,13 @@ type User {
isVerified
:
Boolean
meta
:
JSON
prefs
:
JSON
pictureUrl
:
String
createdAt
:
Date
updatedAt
:
Date
lastLoginAt
:
Date
groups
:
[
Group
]
}
type
UserProfile
{
id
:
Int
name
:
String
email
:
String
providerKey
:
String
providerName
:
String
isSystem
:
Boolean
isVerified
:
Boolean
location
:
String
jobTitle
:
String
timezone
:
String
dateFormat
:
String
appearance
:
String
createdAt
:
Date
updatedAt
:
Date
lastLoginAt
:
Date
groups
:
[
String
]
pagesTotal
:
Int
}
type
UserTokenResponse
{
operation
:
Operation
jwt
:
String
...
...
@@ -150,6 +131,12 @@ enum UserOrderBy {
lastLoginAt
}
enum
UserSiteAppearance
{
site
light
dark
}
input
UserUpdateInput
{
email
:
String
name
:
String
...
...
server/models/users.js
View file @
55b0b00c
...
...
@@ -22,7 +22,7 @@ module.exports = class User extends Model {
properties
:
{
id
:
{
type
:
'string'
},
email
:
{
type
:
'string'
,
format
:
'email'
},
email
:
{
type
:
'string'
},
name
:
{
type
:
'string'
,
minLength
:
1
,
maxLength
:
255
},
pictureUrl
:
{
type
:
'string'
},
isSystem
:
{
type
:
'boolean'
},
...
...
ux/quasar.config.js
View file @
55b0b00c
...
...
@@ -77,12 +77,12 @@ module.exports = configure(function (/* ctx */) {
extendViteConf
(
viteConf
)
{
viteConf
.
build
.
assetsDir
=
'_assets'
viteConf
.
build
.
rollupOptions
=
{
...
viteConf
.
build
.
rollupOptions
??
{},
external
:
[
/^
\/
_site
\/
/
]
}
//
viteConf.build.rollupOptions = {
//
...viteConf.build.rollupOptions ?? {},
//
external: [
//
/^\/_site\//
//
]
//
}
},
// viteVuePluginOptions: {},
...
...
ux/src/App.vue
View file @
55b0b00c
...
...
@@ -3,9 +3,10 @@ router-view
</
template
>
<
script
setup
>
import
{
nextTick
,
onMounted
,
reactive
}
from
'vue'
import
{
nextTick
,
onMounted
,
reactive
,
watch
}
from
'vue'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useSiteStore
}
from
'src/stores/site'
import
{
useUserStore
}
from
'src/stores/user'
import
{
setCssVar
,
useQuasar
}
from
'quasar'
/* global siteConfig */
...
...
@@ -17,6 +18,7 @@ const $q = useQuasar()
// STORES
const
siteStore
=
useSiteStore
()
const
userStore
=
useUserStore
()
// ROUTER
...
...
@@ -28,10 +30,24 @@ const state = reactive({
isInitialized
:
false
})
// WATCHERS
watch
(()
=>
userStore
.
appearance
,
(
newValue
)
=>
{
if
(
newValue
===
'site'
)
{
$q
.
dark
.
set
(
siteStore
.
theme
.
dark
)
}
else
{
$q
.
dark
.
set
(
newValue
===
'dark'
)
}
})
// THEME
function
applyTheme
()
{
$q
.
dark
.
set
(
siteStore
.
theme
.
dark
)
if
(
userStore
.
appearance
===
'site'
)
{
$q
.
dark
.
set
(
siteStore
.
theme
.
dark
)
}
else
{
$q
.
dark
.
set
(
userStore
.
appearance
===
'dark'
)
}
setCssVar
(
'primary'
,
siteStore
.
theme
.
colorPrimary
)
setCssVar
(
'secondary'
,
siteStore
.
theme
.
colorSecondary
)
setCssVar
(
'accent'
,
siteStore
.
theme
.
colorAccent
)
...
...
@@ -51,12 +67,21 @@ if (typeof siteConfig !== 'undefined') {
router
.
beforeEach
(
async
(
to
,
from
)
=>
{
siteStore
.
routerLoading
=
true
// Site Info
if
(
!
siteStore
.
id
)
{
console
.
info
(
'No pre-cached site config. Loading site info...'
)
await
siteStore
.
loadSite
(
window
.
location
.
hostname
)
console
.
info
(
`Using Site ID
${
siteStore
.
id
}
`
)
applyTheme
()
}
// User Auth
await
userStore
.
refreshAuth
()
// User Profile
if
(
userStore
.
authenticated
&&
!
userStore
.
profileLoaded
)
{
console
.
info
(
`Refreshing user
${
userStore
.
id
}
profile...`
)
await
userStore
.
refreshProfile
()
}
// Apply Theme
applyTheme
()
})
router
.
afterEach
(()
=>
{
if
(
!
state
.
isInitialized
)
{
...
...
ux/src/boot/apollo.js
View file @
55b0b00c
...
...
@@ -3,10 +3,10 @@ import { ApolloClient, InMemoryCache } from '@apollo/client/core'
import
{
setContext
}
from
'@apollo/client/link/context'
import
{
createUploadLink
}
from
'apollo-upload-client'
export
default
boot
(({
app
})
=>
{
export
default
boot
(({
app
,
store
})
=>
{
// Authentication Link
const
authLink
=
setContext
(
async
(
req
,
{
headers
})
=>
{
const
token
=
'test'
// await window.auth0Client.getTokenSilently()
const
token
=
store
.
state
.
value
.
user
.
token
return
{
headers
:
{
...
headers
,
...
...
ux/src/components/AccountMenu.vue
View file @
55b0b00c
<
template
lang=
'pug'
>
q-btn.q-ml-md(flat, round, dense, color='grey')
q-icon(v-if='!
state.user.picture
', name='las la-user-circle')
q-icon(v-if='!
userStore.authenticated || !userStore.pictureUrl
', name='las la-user-circle')
q-avatar(v-else)
img(:src='
state.user.picture
')
img(:src='
userStore.pictureUrl
')
q-menu(auto-close)
q-card(flat, style='width: 300px;', :dark='false')
q-card-section(align='center')
.text-subtitle1.text-grey-7
{{
state
.
user
.
name
}}
.text-caption.text-grey-8
{{
state
.
user
.
email
}}
.text-subtitle1.text-grey-7
{{
userStore
.
name
}}
.text-caption.text-grey-8
{{
userStore
.
email
}}
q-separator(:dark='false')
q-card-actions(align='center')
q-btn(
...
...
@@ -15,7 +15,7 @@ q-btn.q-ml-md(flat, round, dense, color='grey')
label='Profile'
icon='las la-user-alt'
color='primary'
href
='/_profile'
to
='/_profile'
no-caps
)
q-btn(flat
...
...
@@ -29,13 +29,7 @@ q-btn.q-ml-md(flat, round, dense, color='grey')
</
template
>
<
script
setup
>
import
{
reactive
}
from
'vue
'
import
{
useUserStore
}
from
'src/stores/user
'
const
state
=
reactive
({
user
:
{
name
:
'John Doe'
,
email
:
'test@example.com'
,
picture
:
null
}
})
const
userStore
=
useUserStore
()
</
script
>
ux/src/components/HeaderNav.vue
View file @
55b0b00c
...
...
@@ -17,10 +17,10 @@ q-header.bg-header.text-white.site-header(
size='34px'
square
)
img(
src='/_site/logo
')
img(
:src='`/_site/logo`
')
img(
v-else
src='/_site/logo
'
:src='`/_site/logo`
'
style='height: 34px'
)
q-toolbar-title.text-h6(v-if='siteStore.logoText')
{{
siteStore
.
title
}}
...
...
ux/src/components/UserEditOverlay.vue
View file @
55b0b00c
...
...
@@ -117,6 +117,19 @@ q-layout(view='hHh lpR fFf', container)
dense
:aria-label='t(`admin.users.jobTitle`)'
)
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='gender')
q-item-section
q-item-label
{{
t
(
`admin.users.pronouns`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.pronounsHint`
)
}}
q-item-section
q-input(
outlined
v-model='state.user.meta.pronouns'
dense
:aria-label='t(`admin.users.pronouns`)'
)
q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.user.meta')
q-card-section
...
...
@@ -181,18 +194,23 @@ q-layout(view='hHh lpR fFf', container)
]`
)
q-separator.q-my-sm(inset)
q-item
(tag='label', v-ripple)
q-item
blueprint-icon(icon='light-on')
q-item-section
q-item-label
{{
t
(
`admin.users.
darkMod
e`
)
}}
q-item-label
{{
t
(
`admin.users.
appearanc
e`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.darkModeHint`
)
}}
q-item-section(avatar)
q-toggle(
v-model='state.user.prefs.darkMode'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='t(`admin.users.darkMode`)'
q-item-section.col-auto
q-btn-toggle(
v-model='state.user.prefs.appearance'
push
glossy
no-caps
toggle-color='primary'
:options=`[
{ label: t('profile.appearanceDefault'), value: 'site' },
{ label: t('profile.appearanceLight'), value: 'light' },
{ label: t('profile.appearanceDark'), value: 'dark' }
]`
)
.col-12.col-lg-4
...
...
ux/src/i18n/locales/en.json
View file @
55b0b00c
...
...
@@ -1359,7 +1359,7 @@
"profile.activity.lastUpdatedOn"
:
"Profile last updated on"
,
"profile.activity.pagesCreated"
:
"Pages created"
,
"profile.activity.title"
:
"Activity"
,
"profile.appearance"
:
"Appearance"
,
"profile.appearance"
:
"
Site
Appearance"
,
"profile.appearanceDark"
:
"Dark"
,
"profile.appearanceDefault"
:
"Site Default"
,
"profile.appearanceLight"
:
"Light"
,
...
...
@@ -1498,5 +1498,12 @@
"admin.utilities.disconnectWSHint"
:
"Force all active websocket connections to be closed."
,
"admin.utilities.disconnectWSSuccess"
:
"All active websocket connections have been terminated."
,
"admin.login.bgUploadSuccess"
:
"Login background image uploaded successfully."
,
"admin.login.saveSuccess"
:
"Login configuration saved successfully."
"admin.login.saveSuccess"
:
"Login configuration saved successfully."
,
"profile.appearanceHint"
:
"Use the light or dark theme."
,
"profile.saving"
:
"Saving profile..."
,
"profile.saveSuccess"
:
"Profile saved successfully."
,
"profile.saveFailed"
:
"Failed to save profile changes."
,
"admin.users.pronouns"
:
"Pronouns"
,
"admin.users.pronounsHint"
:
"The pronouns used to address this user."
,
"admin.users.appearance"
:
"Site Appearance"
}
ux/src/layouts/ProfileLayout.vue
View file @
55b0b00c
...
...
@@ -31,6 +31,7 @@ q-layout(view='hHh Lpr lff')
q-item(
clickable
v-ripple
href='/logout'
)
q-item-section(side)
q-icon(name='las la-sign-out-alt', color='negative')
...
...
@@ -80,7 +81,7 @@ const sidenav = [
},
{
key
:
'password'
,
label
:
'
Password
'
,
label
:
'
Authentication
'
,
icon
:
'las la-key'
},
{
...
...
ux/src/pages/Login.vue
View file @
55b0b00c
...
...
@@ -2,12 +2,12 @@
.auth
.auth-content
.auth-logo
img(
src='/_site/logo
' :alt='siteStore.title')
img(
:src='`/_site/logo`
' :alt='siteStore.title')
h2.auth-site-title(v-if='siteStore.logoText')
{{
siteStore
.
title
}}
p.text-grey-7 Login to continue
auth-login-panel
.auth-bg(aria-hidden="true")
img(
src='/_site/loginbg
' alt='')
img(
:src='`/_site/loginbg`
' alt='')
</
template
>
<
script
setup
>
...
...
ux/src/pages/Profile.vue
View file @
55b0b00c
...
...
@@ -121,25 +121,27 @@ q-page.q-py-md(:style-fn='pageStyle')
:options='timeFormats'
)
q-separator.q-my-sm(inset)
q-item
(tag='label', v-ripple)
q-item
blueprint-icon(icon='light-on')
q-item-section
q-item-label
{{
t
(
`profile.darkMode`
)
}}
q-item-label(caption)
{{
t
(
`profile.darkModeHint`
)
}}
q-item-section(avatar)
q-toggle(
v-model='state.config.darkMode'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='t(`profile.darkMode`)'
q-item-label
{{
t
(
`profile.appearance`
)
}}
q-item-label(caption)
{{
t
(
`profile.appearanceHint`
)
}}
q-item-section.col-auto
q-btn-toggle(
v-model='state.config.appearance'
push
glossy
no-caps
toggle-color='primary'
:options='appearances'
)
.actions-bar.q-mt-lg
q-btn(
icon='las la-check'
unelevated
label='Save Changes
'
:label='t(`common.actions.saveChanges`)
'
color='secondary'
@click='save'
)
</
template
>
...
...
@@ -152,6 +154,7 @@ import { onMounted, reactive, watch } from 'vue'
import
{
useSiteStore
}
from
'src/stores/site'
import
{
useDataStore
}
from
'src/stores/data'
import
{
useUserStore
}
from
'src/stores/user'
// QUASAR
...
...
@@ -161,6 +164,7 @@ const $q = useQuasar()
const
siteStore
=
useSiteStore
()
const
dataStore
=
useDataStore
()
const
userStore
=
useUserStore
()
// I18N
...
...
@@ -176,14 +180,15 @@ useMeta({
const
state
=
reactive
({
config
:
{
name
:
'
John Doe
'
,
email
:
'
john.doe@company.com
'
,
name
:
''
,
email
:
''
,
location
:
''
,
jobTitle
:
''
,
pronouns
:
''
,
timezone
:
''
,
dateFormat
:
''
,
timeFormat
:
'12h'
,
darkMode
:
false
appearance
:
'site'
}
})
...
...
@@ -199,6 +204,11 @@ const timeFormats = [
{
value
:
'12h'
,
label
:
t
(
'admin.general.defaultTimeFormat12h'
)
},
{
value
:
'24h'
,
label
:
t
(
'admin.general.defaultTimeFormat24h'
)
}
]
const
appearances
=
[
{
value
:
'site'
,
label
:
t
(
'profile.appearanceDefault'
)
},
{
value
:
'light'
,
label
:
t
(
'profile.appearanceLight'
)
},
{
value
:
'dark'
,
label
:
t
(
'profile.appearanceDark'
)
}
]
// METHODS
...
...
@@ -207,4 +217,73 @@ function pageStyle (offset, height) {
'min-height'
:
`
${
height
-
100
-
offset
}
px`
}
}
async
function
save
()
{
$q
.
loading
.
show
({
message
:
t
(
'profile.saving'
)
})
try
{
const
respRaw
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation saveProfile (
$name: String
$location: String
$jobTitle: String
$pronouns: String
$timezone: String
$dateFormat: String
$timeFormat: String
$appearance: UserSiteAppearance
) {
updateProfile (
name: $name
location: $location
jobTitle: $jobTitle
pronouns: $pronouns
timezone: $timezone
dateFormat: $dateFormat
timeFormat: $timeFormat
appearance: $appearance
) {
operation {
succeeded
message
}
}
}
`
,
variables
:
state
.
config
})
if
(
respRaw
.
data
?.
updateProfile
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'profile.saveSuccess'
)
})
userStore
.
$patch
(
state
.
config
)
}
else
{
throw
new
Error
(
respRaw
.
data
?.
updateProfile
?.
operation
?.
message
||
'An unexpected error occured'
)
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
t
(
'profile.saveFailed'
),
caption
:
err
.
message
})
}
$q
.
loading
.
hide
()
}
// MOUNTED
onMounted
(()
=>
{
state
.
config
.
name
=
userStore
.
name
||
''
state
.
config
.
email
=
userStore
.
email
state
.
config
.
location
=
userStore
.
location
||
''
state
.
config
.
jobTitle
=
userStore
.
jobTitle
||
''
state
.
config
.
pronouns
=
userStore
.
pronouns
||
''
state
.
config
.
timezone
=
userStore
.
timezone
||
Intl
.
DateTimeFormat
().
resolvedOptions
().
timeZone
||
''
state
.
config
.
dateFormat
=
userStore
.
dateFormat
||
''
state
.
config
.
timeFormat
=
userStore
.
timeFormat
||
'12h'
state
.
config
.
appearance
=
userStore
.
appearance
||
'site'
})
</
script
>
ux/src/stores/user.js
View file @
55b0b00c
import
{
defineStore
}
from
'pinia'
import
jwtDecode
from
'jwt-decode'
import
Cookies
from
'js-cookie'
import
gql
from
'graphql-tag'
import
{
DateTime
}
from
'luxon'
export
const
useUserStore
=
defineStore
(
'user'
,
{
state
:
()
=>
({
id
:
0
,
id
:
'10000000-0000-4000-8000-000000000001'
,
email
:
''
,
name
:
''
,
pictureUrl
:
''
,
localeCode
:
''
,
defaultEditor
:
''
,
timezone
:
''
,
dateFormat
:
''
,
appearance
:
''
,
dateFormat
:
'YYYY-MM-DD'
,
timeFormat
:
'12h'
,
appearance
:
'site'
,
permissions
:
[],
iat
:
0
,
exp
:
0
,
authenticated
:
false
exp
:
null
,
authenticated
:
false
,
token
:
''
,
profileLoaded
:
false
}),
getters
:
{},
actions
:
{
refreshAuth
()
{
async
refreshAuth
()
{
const
jwtCookie
=
Cookies
.
get
(
'jwt'
)
if
(
jwtCookie
)
{
try
{
const
jwtData
=
jwtDecode
(
jwtCookie
)
this
.
id
=
jwtData
.
id
this
.
email
=
jwtData
.
email
this
.
name
=
jwtData
.
name
this
.
pictureUrl
=
jwtData
.
av
this
.
localeCode
=
jwtData
.
lc
this
.
timezone
=
jwtData
.
tz
||
Intl
.
DateTimeFormat
().
resolvedOptions
().
timeZone
||
''
this
.
dateFormat
=
jwtData
.
df
||
''
this
.
appearance
=
jwtData
.
ap
||
''
// this.defaultEditor = jwtData.defaultEditor
this
.
permissions
=
jwtData
.
permissions
this
.
iat
=
jwtData
.
iat
this
.
exp
=
jwtData
.
exp
this
.
authenticated
=
true
this
.
exp
=
DateTime
.
fromSeconds
(
jwtData
.
exp
,
{
zone
:
'utc'
})
this
.
token
=
jwtCookie
if
(
this
.
exp
<=
DateTime
.
utc
())
{
console
.
info
(
'Token has expired. Attempting renew...'
)
}
else
{
this
.
authenticated
=
true
}
}
catch
(
err
)
{
console
.
debug
(
'Invalid JWT. Silent authentication skipped.'
)
}
}
},
async
refreshProfile
()
{
if
(
!
this
.
authenticated
||
!
this
.
id
)
{
return
}
try
{
const
respRaw
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query refreshProfile (
$id: UUID!
) {
userById(id: $id) {
id
name
email
meta
prefs
lastLoginAt
groups {
id
name
}
}
}
`
,
variables
:
{
id
:
this
.
id
}
})
const
resp
=
respRaw
?.
data
?.
userById
if
(
!
resp
||
resp
.
id
!==
this
.
id
)
{
throw
new
Error
(
'Failed to fetch user profile!'
)
}
this
.
name
=
resp
.
name
||
'Unknown User'
this
.
email
=
resp
.
email
this
.
pictureUrl
=
(
resp
.
pictureUrl
===
'local'
)
?
`/_user/
${
this
.
id
}
/avatar`
:
resp
.
pictureUrl
this
.
location
=
resp
.
meta
.
location
||
''
this
.
jobTitle
=
resp
.
meta
.
jobTitle
||
''
this
.
pronouns
=
resp
.
meta
.
pronouns
||
''
this
.
timezone
=
resp
.
prefs
.
timezone
||
Intl
.
DateTimeFormat
().
resolvedOptions
().
timeZone
||
''
this
.
dateFormat
=
resp
.
prefs
.
dateFormat
||
''
this
.
timeFormat
=
resp
.
prefs
.
timeFormat
||
'12h'
this
.
appearance
=
resp
.
prefs
.
appearance
||
'site'
this
.
profileLoaded
=
true
}
catch
(
err
)
{
console
.
warn
(
err
)
}
}
}
})
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment