Unverified Commit 1522b269 authored by NGPixel's avatar NGPixel

feat: admin language switcher

parent e89f304d
...@@ -5,7 +5,7 @@ export default { ...@@ -5,7 +5,7 @@ export default {
Query: { Query: {
async locales(obj, args, context, info) { async locales(obj, args, context, info) {
let remoteLocales = await WIKI.cache.get('locales') let remoteLocales = await WIKI.cache.get('locales')
let localLocales = await WIKI.db.locales.query().select('code', 'isRTL', 'name', 'nativeName', 'createdAt', 'updatedAt', 'completeness') let localLocales = await WIKI.db.locales.query().select('code', 'isRTL', 'language', 'name', 'nativeName', 'createdAt', 'updatedAt', 'completeness')
remoteLocales = remoteLocales || localLocales remoteLocales = remoteLocales || localLocales
return _.map(remoteLocales, rl => { return _.map(remoteLocales, rl => {
let isInstalled = _.some(localLocales, ['code', rl.code]) let isInstalled = _.some(localLocales, ['code', rl.code])
......
...@@ -31,6 +31,7 @@ type LocalizationLocale { ...@@ -31,6 +31,7 @@ type LocalizationLocale {
installDate: Date installDate: Date
isInstalled: Boolean isInstalled: Boolean
isRTL: Boolean isRTL: Boolean
language: String
name: String name: String
nativeName: String nativeName: String
region: String region: String
......
...@@ -5,15 +5,16 @@ router-view ...@@ -5,15 +5,16 @@ router-view
<script setup> <script setup>
import { nextTick, onMounted, reactive, watch } from 'vue' import { nextTick, onMounted, reactive, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useFlagsStore } from 'src/stores/flags'
import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
import { setCssVar, useQuasar } from 'quasar' import { setCssVar, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import gql from 'graphql-tag'
import '@mdi/font/css/materialdesignicons.css' import '@mdi/font/css/materialdesignicons.css'
import { useCommonStore } from './stores/common'
import { useFlagsStore } from 'src/stores/flags'
import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
/* global siteConfig */ /* global siteConfig */
// QUASAR // QUASAR
...@@ -22,6 +23,7 @@ const $q = useQuasar() ...@@ -22,6 +23,7 @@ const $q = useQuasar()
// STORES // STORES
const commonStore = useCommonStore()
const flagsStore = useFlagsStore() const flagsStore = useFlagsStore()
const siteStore = useSiteStore() const siteStore = useSiteStore()
const userStore = useUserStore() const userStore = useUserStore()
...@@ -54,6 +56,25 @@ watch(() => userStore.cvd, () => { ...@@ -54,6 +56,25 @@ watch(() => userStore.cvd, () => {
applyTheme() applyTheme()
}) })
watch(() => commonStore.locale, applyLocale)
// LOCALE
async function applyLocale (locale) {
if (!i18n.availableLocales.includes(locale)) {
try {
i18n.setLocaleMessage(locale, await commonStore.fetchLocaleStrings(locale))
} catch (err) {
$q.notify({
type: 'negative',
message: `Failed to load ${locale} locale strings.`,
caption: err.message
})
}
}
i18n.locale.value = locale
}
// THEME // THEME
async function applyTheme () { async function applyTheme () {
...@@ -89,35 +110,6 @@ async function applyTheme () { ...@@ -89,35 +110,6 @@ async function applyTheme () {
} }
} }
// LOCALE
async function fetchLocaleStrings (locale) {
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query fetchLocaleStrings (
$locale: String!
) {
localeStrings (
locale: $locale
)
}
`,
fetchPolicy: 'cache-first',
variables: {
locale
}
})
return resp?.data?.localeStrings
} catch (err) {
console.warn(err)
$q.notify({
type: 'negative',
message: 'Failed to load locale strings.'
})
}
}
// INIT SITE STORE // INIT SITE STORE
if (typeof siteConfig !== 'undefined') { if (typeof siteConfig !== 'undefined') {
...@@ -128,30 +120,40 @@ if (typeof siteConfig !== 'undefined') { ...@@ -128,30 +120,40 @@ if (typeof siteConfig !== 'undefined') {
applyTheme() applyTheme()
} }
// ROUTE GUARDS
router.beforeEach(async (to, from) => { router.beforeEach(async (to, from) => {
siteStore.routerLoading = true commonStore.routerLoading = true
// System Flags
// -> System Flags
if (!flagsStore.loaded) { if (!flagsStore.loaded) {
flagsStore.load() flagsStore.load()
} }
// Site Info
// -> Site Info
if (!siteStore.id) { if (!siteStore.id) {
console.info('No pre-cached site config. Loading site info...') console.info('No pre-cached site config. Loading site info...')
await siteStore.loadSite(window.location.hostname) await siteStore.loadSite(window.location.hostname)
console.info(`Using Site ID ${siteStore.id}`) console.info(`Using Site ID ${siteStore.id}`)
} }
// Locales
if (!i18n.availableLocales.includes('en')) { // -> Locale
i18n.setLocaleMessage('en', await fetchLocaleStrings('en')) if (!commonStore.desiredLocale || !siteStore.locales.active.includes(commonStore.desiredLocale)) {
commonStore.setLocale(siteStore.locales.primary)
} else {
applyLocale(commonStore.desiredLocale)
} }
// User Auth
// -> User Auth
await userStore.refreshAuth() await userStore.refreshAuth()
// User Profile
// -> User Profile
if (userStore.authenticated && !userStore.profileLoaded) { if (userStore.authenticated && !userStore.profileLoaded) {
console.info(`Refreshing user ${userStore.id} profile...`) console.info(`Refreshing user ${userStore.id} profile...`)
await userStore.refreshProfile() await userStore.refreshProfile()
} }
// Page Permissions
// -> Page Permissions
await userStore.fetchPagePermissions(to.path) await userStore.fetchPagePermissions(to.path)
}) })
...@@ -177,7 +179,7 @@ router.afterEach(() => { ...@@ -177,7 +179,7 @@ router.afterEach(() => {
applyTheme() applyTheme()
document.querySelector('.init-loading').remove() document.querySelector('.init-loading').remove()
} }
siteStore.routerLoading = false commonStore.routerLoading = false
}) })
</script> </script>
...@@ -3,10 +3,14 @@ import { ApolloClient, InMemoryCache } from '@apollo/client/core' ...@@ -3,10 +3,14 @@ import { ApolloClient, InMemoryCache } from '@apollo/client/core'
import { setContext } from '@apollo/client/link/context' import { setContext } from '@apollo/client/link/context'
import { createUploadLink } from 'apollo-upload-client' import { createUploadLink } from 'apollo-upload-client'
export default boot(({ app, store }) => { import { useUserStore } from 'src/stores/user'
export default boot(({ app }) => {
const userStore = useUserStore()
// Authentication Link // Authentication Link
const authLink = setContext(async (req, { headers }) => { const authLink = setContext(async (req, { headers }) => {
const token = store.state.value.user.token const token = userStore.token
return { return {
headers: { headers: {
...headers, ...headers,
......
import { boot } from 'quasar/wrappers' import { boot } from 'quasar/wrappers'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import { useCommonStore } from 'src/stores/common'
export default boot(({ app }) => { export default boot(({ app }) => {
const commonStore = useCommonStore()
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
locale: 'en', locale: commonStore.locale || 'en',
fallbackLocale: 'en', fallbackLocale: 'en',
fallbackWarn: false, fallbackWarn: false,
messages: {} messages: {}
......
...@@ -68,7 +68,7 @@ q-header.bg-header.text-white.site-header( ...@@ -68,7 +68,7 @@ q-header.bg-header.text-white.site-header(
q-space q-space
transition(name='syncing') transition(name='syncing')
q-spinner-tail( q-spinner-tail(
v-show='siteStore.routerLoading' v-show='commonStore.routerLoading'
color='accent' color='accent'
size='24px' size='24px'
) )
...@@ -130,6 +130,7 @@ import { useI18n } from 'vue-i18n' ...@@ -130,6 +130,7 @@ import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { reactive } from 'vue' import { reactive } from 'vue'
import { useCommonStore } from 'src/stores/common'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user' import { useUserStore } from 'src/stores/user'
...@@ -139,6 +140,7 @@ const $q = useQuasar() ...@@ -139,6 +140,7 @@ const $q = useQuasar()
// STORES // STORES
const commonStore = useCommonStore()
const siteStore = useSiteStore() const siteStore = useSiteStore()
const userStore = useUserStore() const userStore = useUserStore()
......
...@@ -18,11 +18,25 @@ q-layout.admin(view='hHh Lpr lff') ...@@ -18,11 +18,25 @@ q-layout.admin(view='hHh Lpr lff')
q-space q-space
transition(name='syncing') transition(name='syncing')
q-spinner-tail( q-spinner-tail(
v-show='siteStore.routerLoading' v-show='commonStore.routerLoading'
color='accent' color='accent'
size='24px' size='24px'
) )
q-btn.q-ml-md(flat, dense, icon='las la-times-circle', label='Exit' color='pink', to='/') q-btn.q-ml-md(flat, dense, icon='las la-times-circle', :label='t(`common.actions.exit`)' color='pink', to='/')
q-btn.q-ml-md(flat, dense, icon='las la-language', :label='commonStore.locale' color='grey-4')
q-menu.translucent-menu(auto-close, anchor='bottom right', self='top right')
q-list(separator)
q-item(
v-for='lang of adminStore.locales'
clickable
@click='commonStore.setLocale(lang.code)'
)
q-item-section(side)
q-avatar(rounded, :color='lang.code === commonStore.locale ? `secondary` : `primary`', text-color='white', size='sm')
.text-caption.text-uppercase: strong {{ lang.language }}
q-item-section
q-item-label {{ lang.name }}
q-item-label(caption) {{ lang.nativeName }}
account-menu account-menu
q-drawer.admin-sidebar(v-model='leftDrawerOpen', show-if-above, bordered) q-drawer.admin-sidebar(v-model='leftDrawerOpen', show-if-above, bordered)
q-scroll-area.admin-nav( q-scroll-area.admin-nav(
...@@ -108,6 +122,9 @@ q-layout.admin(view='hHh Lpr lff') ...@@ -108,6 +122,9 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-ssd.svg') q-icon(name='img:/_assets/icons/fluent-ssd.svg')
q-item-section {{ t('admin.storage.title') }} q-item-section {{ t('admin.storage.title') }}
q-item-section(side)
//- TODO: Reflect site storage status
status-light(:color='true ? `positive` : `warning`', :pulse='false')
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/theme`', v-ripple, active-class='bg-primary text-white', v-if='userStore.can(`manage:sites`) || userStore.can(`manage:theme`)') q-item(:to='`/_admin/` + adminStore.currentSiteId + `/theme`', v-ripple, active-class='bg-primary text-white', v-if='userStore.can(`manage:sites`) || userStore.can(`manage:theme`)')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-paint-roller.svg') q-icon(name='img:/_assets/icons/fluent-paint-roller.svg')
...@@ -159,7 +176,7 @@ q-layout.admin(view='hHh Lpr lff') ...@@ -159,7 +176,7 @@ q-layout.admin(view='hHh Lpr lff')
q-icon(name='img:/_assets/icons/fluent-message-settings.svg') q-icon(name='img:/_assets/icons/fluent-message-settings.svg')
q-item-section {{ t('admin.mail.title') }} q-item-section {{ t('admin.mail.title') }}
q-item-section(side) q-item-section(side)
status-light(:color='adminStore.info.isMailConfigured ? `positive` : `warning`') status-light(:color='adminStore.info.isMailConfigured ? `positive` : `warning`', :pulse='!adminStore.info.isMailConfigured')
q-item(to='/_admin/rendering', v-ripple, active-class='bg-primary text-white') q-item(to='/_admin/rendering', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-rich-text-converter.svg') q-icon(name='img:/_assets/icons/fluent-rich-text-converter.svg')
...@@ -169,7 +186,7 @@ q-layout.admin(view='hHh Lpr lff') ...@@ -169,7 +186,7 @@ q-layout.admin(view='hHh Lpr lff')
q-icon(name='img:/_assets/icons/fluent-bot.svg') q-icon(name='img:/_assets/icons/fluent-bot.svg')
q-item-section {{ t('admin.scheduler.title') }} q-item-section {{ t('admin.scheduler.title') }}
q-item-section(side) q-item-section(side)
status-light(:color='adminStore.info.isSchedulerHealthy ? `positive` : `warning`') status-light(:color='adminStore.info.isSchedulerHealthy ? `positive` : `warning`', :pulse='!adminStore.info.isSchedulerHealthy')
q-item(to='/_admin/security', v-ripple, active-class='bg-primary text-white') q-item(to='/_admin/security', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-protect.svg') q-icon(name='img:/_assets/icons/fluent-protect.svg')
...@@ -223,6 +240,7 @@ import { useRouter, useRoute } from 'vue-router' ...@@ -223,6 +240,7 @@ import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAdminStore } from 'src/stores/admin' import { useAdminStore } from 'src/stores/admin'
import { useCommonStore } from 'src/stores/common'
import { useFlagsStore } from 'src/stores/flags' import { useFlagsStore } from 'src/stores/flags'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user' import { useUserStore } from 'src/stores/user'
...@@ -244,6 +262,7 @@ const $q = useQuasar() ...@@ -244,6 +262,7 @@ const $q = useQuasar()
// STORES // STORES
const adminStore = useAdminStore() const adminStore = useAdminStore()
const commonStore = useCommonStore()
const flagsStore = useFlagsStore() const flagsStore = useFlagsStore()
const siteStore = useSiteStore() const siteStore = useSiteStore()
const userStore = useUserStore() const userStore = useUserStore()
...@@ -266,13 +285,6 @@ useMeta({ ...@@ -266,13 +285,6 @@ useMeta({
// DATA // DATA
const leftDrawerOpen = ref(true) const leftDrawerOpen = ref(true)
const overlayIsShown = ref(false)
const search = ref('')
const user = reactive({
name: 'John Doe',
email: 'test@example.com',
picture: null
})
const thumbStyle = { const thumbStyle = {
right: '1px', right: '1px',
borderRadius: '5px', borderRadius: '5px',
...@@ -292,6 +304,9 @@ const siteSectionShown = computed(() => { ...@@ -292,6 +304,9 @@ const siteSectionShown = computed(() => {
const usersSectionShown = computed(() => { const usersSectionShown = computed(() => {
return userStore.can('manage:groups') || userStore.can('manage:users') return userStore.can('manage:groups') || userStore.can('manage:users')
}) })
const overlayIsShown = computed(() => {
return Boolean(adminStore.overlay)
})
// WATCHERS // WATCHERS
...@@ -308,9 +323,6 @@ watch(() => adminStore.sites, (newValue) => { ...@@ -308,9 +323,6 @@ watch(() => adminStore.sites, (newValue) => {
}) })
} }
}) })
watch(() => adminStore.overlay, (newValue) => {
overlayIsShown.value = !!newValue
})
watch(() => adminStore.currentSiteId, (newValue) => { watch(() => adminStore.currentSiteId, (newValue) => {
if (newValue && route.params.siteid !== newValue) { if (newValue && route.params.siteid !== newValue) {
router.push({ params: { siteid: newValue } }) router.push({ params: { siteid: newValue } })
...@@ -325,6 +337,7 @@ onMounted(async () => { ...@@ -325,6 +337,7 @@ onMounted(async () => {
return return
} }
adminStore.fetchLocales()
await adminStore.fetchSites() await adminStore.fetchSites()
if (route.params.siteid) { if (route.params.siteid) {
adminStore.$patch({ adminStore.$patch({
......
...@@ -35,24 +35,20 @@ export const useAdminStore = defineStore('admin', { ...@@ -35,24 +35,20 @@ export const useAdminStore = defineStore('admin', {
} }
}, },
actions: { actions: {
async fetchSites () { async fetchLocales () {
const resp = await APOLLO_CLIENT.query({ const resp = await APOLLO_CLIENT.query({
query: gql` query: gql`
query getSites { query getAdminLocales {
sites { locales {
id code
hostname language
isEnabled name
title nativeName
} }
} }
`, `
fetchPolicy: 'network-only'
}) })
this.sites = cloneDeep(resp?.data?.sites ?? []) this.locales = cloneDeep(resp?.data?.locales ?? [])
if (!this.currentSiteId) {
this.currentSiteId = this.sites[0].id
}
}, },
async fetchInfo () { async fetchInfo () {
const resp = await APOLLO_CLIENT.query({ const resp = await APOLLO_CLIENT.query({
...@@ -78,6 +74,25 @@ export const useAdminStore = defineStore('admin', { ...@@ -78,6 +74,25 @@ export const useAdminStore = defineStore('admin', {
this.info.isApiEnabled = clone(resp?.data?.apiState ?? false) this.info.isApiEnabled = clone(resp?.data?.apiState ?? false)
this.info.isMailConfigured = clone(resp?.data?.systemInfo?.isMailConfigured ?? false) this.info.isMailConfigured = clone(resp?.data?.systemInfo?.isMailConfigured ?? false)
this.info.isSchedulerHealthy = clone(resp?.data?.systemInfo?.isSchedulerHealthy ?? false) this.info.isSchedulerHealthy = clone(resp?.data?.systemInfo?.isSchedulerHealthy ?? false)
},
async fetchSites () {
const resp = await APOLLO_CLIENT.query({
query: gql`
query getSites {
sites {
id
hostname
isEnabled
title
}
}
`,
fetchPolicy: 'network-only'
})
this.sites = cloneDeep(resp?.data?.sites ?? [])
if (!this.currentSiteId) {
this.currentSiteId = this.sites[0].id
}
} }
} }
}) })
import { defineStore } from 'pinia'
import gql from 'graphql-tag'
export const useCommonStore = defineStore('common', {
state: () => ({
routerLoading: false,
locale: localStorage.getItem('locale') || 'en',
desiredLocale: localStorage.getItem('locale')
}),
getters: {},
actions: {
async fetchLocaleStrings (locale) {
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query fetchLocaleStrings (
$locale: String!
) {
localeStrings (
locale: $locale
)
}
`,
fetchPolicy: 'cache-first',
variables: {
locale
}
})
return resp?.data?.localeStrings
} catch (err) {
console.warn(err)
throw err
}
},
setLocale (locale) {
this.$patch({
locale,
desiredLocale: locale
})
localStorage.setItem('locale', locale)
}
}
})
...@@ -6,9 +6,7 @@ import { useUserStore } from './user' ...@@ -6,9 +6,7 @@ import { useUserStore } from './user'
export const useSiteStore = defineStore('site', { export const useSiteStore = defineStore('site', {
state: () => ({ state: () => ({
routerLoading: false,
id: null, id: null,
useLocales: false,
hostname: '', hostname: '',
company: '', company: '',
contentLicense: '', contentLicense: '',
...@@ -39,6 +37,10 @@ export const useSiteStore = defineStore('site', { ...@@ -39,6 +37,10 @@ export const useSiteStore = defineStore('site', {
markdown: false, markdown: false,
wysiwyg: false wysiwyg: false
}, },
locales: {
primary: 'en',
active: ['en']
},
theme: { theme: {
dark: false, dark: false,
injectCSS: '', injectCSS: '',
...@@ -84,6 +86,9 @@ export const useSiteStore = defineStore('site', { ...@@ -84,6 +86,9 @@ export const useSiteStore = defineStore('site', {
opacity: isDark ? 0.25 : 1 opacity: isDark ? 0.25 : 1
} }
} }
},
useLocales: (state) => {
return state.locales?.active?.length > 1
} }
}, },
actions: { actions: {
...@@ -104,20 +109,9 @@ export const useSiteStore = defineStore('site', { ...@@ -104,20 +109,9 @@ export const useSiteStore = defineStore('site', {
hostname: $hostname hostname: $hostname
exact: false exact: false
) { ) {
id
hostname
title
description
logoText
company company
contentLicense contentLicense
footerExtra description
features {
profile
ratingsMode
reasonForChange
search
}
editors { editors {
asciidoc { asciidoc {
isActive isActive
...@@ -129,6 +123,20 @@ export const useSiteStore = defineStore('site', { ...@@ -129,6 +123,20 @@ export const useSiteStore = defineStore('site', {
isActive isActive
} }
} }
features {
profile
ratingsMode
reasonForChange
search
}
footerExtra
hostname
id
locales {
primary
active
}
logoText
theme { theme {
dark dark
colorPrimary colorPrimary
...@@ -144,6 +152,7 @@ export const useSiteStore = defineStore('site', { ...@@ -144,6 +152,7 @@ export const useSiteStore = defineStore('site', {
baseFont baseFont
contentFont contentFont
} }
title
} }
} }
`, `,
...@@ -171,6 +180,10 @@ export const useSiteStore = defineStore('site', { ...@@ -171,6 +180,10 @@ export const useSiteStore = defineStore('site', {
markdown: clone(siteInfo.editors.markdown.isActive), markdown: clone(siteInfo.editors.markdown.isActive),
wysiwyg: clone(siteInfo.editors.wysiwyg.isActive) wysiwyg: clone(siteInfo.editors.wysiwyg.isActive)
}, },
locales: {
primary: clone(siteInfo.locales.primary),
active: clone(siteInfo.locales.active)
},
theme: { theme: {
...this.theme, ...this.theme,
...clone(siteInfo.theme) ...clone(siteInfo.theme)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment