From 1522b26997f64fa2b3cca266f1849dc0dae26ba9 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 28 May 2023 03:06:27 +0000 Subject: [PATCH] feat: admin language switcher --- server/graph/resolvers/localization.mjs | 2 +- server/graph/schemas/localization.graphql | 1 + ux/src/App.vue | 88 ++++++++++++----------- ux/src/boot/apollo.js | 8 ++- ux/src/boot/i18n.js | 6 +- ux/src/components/HeaderNav.vue | 4 +- ux/src/layouts/AdminLayout.vue | 41 +++++++---- ux/src/stores/admin.js | 41 +++++++---- ux/src/stores/common.js | 43 +++++++++++ ux/src/stores/site.js | 41 +++++++---- 10 files changed, 186 insertions(+), 89 deletions(-) create mode 100644 ux/src/stores/common.js diff --git a/server/graph/resolvers/localization.mjs b/server/graph/resolvers/localization.mjs index 6b1c1eb1..d5dedf9b 100644 --- a/server/graph/resolvers/localization.mjs +++ b/server/graph/resolvers/localization.mjs @@ -5,7 +5,7 @@ export default { Query: { async locales(obj, args, context, info) { 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 return _.map(remoteLocales, rl => { let isInstalled = _.some(localLocales, ['code', rl.code]) diff --git a/server/graph/schemas/localization.graphql b/server/graph/schemas/localization.graphql index 64c5f921..5efc53bd 100644 --- a/server/graph/schemas/localization.graphql +++ b/server/graph/schemas/localization.graphql @@ -31,6 +31,7 @@ type LocalizationLocale { installDate: Date isInstalled: Boolean isRTL: Boolean + language: String name: String nativeName: String region: String diff --git a/ux/src/App.vue b/ux/src/App.vue index 1e859139..21f73cb4 100644 --- a/ux/src/App.vue +++ b/ux/src/App.vue @@ -5,15 +5,16 @@ router-view diff --git a/ux/src/boot/apollo.js b/ux/src/boot/apollo.js index 0b6f8455..c1ceee2c 100644 --- a/ux/src/boot/apollo.js +++ b/ux/src/boot/apollo.js @@ -3,10 +3,14 @@ import { ApolloClient, InMemoryCache } from '@apollo/client/core' import { setContext } from '@apollo/client/link/context' 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 const authLink = setContext(async (req, { headers }) => { - const token = store.state.value.user.token + const token = userStore.token return { headers: { ...headers, diff --git a/ux/src/boot/i18n.js b/ux/src/boot/i18n.js index 993529c0..146f6138 100644 --- a/ux/src/boot/i18n.js +++ b/ux/src/boot/i18n.js @@ -1,10 +1,14 @@ import { boot } from 'quasar/wrappers' import { createI18n } from 'vue-i18n' +import { useCommonStore } from 'src/stores/common' + export default boot(({ app }) => { + const commonStore = useCommonStore() + const i18n = createI18n({ legacy: false, - locale: 'en', + locale: commonStore.locale || 'en', fallbackLocale: 'en', fallbackWarn: false, messages: {} diff --git a/ux/src/components/HeaderNav.vue b/ux/src/components/HeaderNav.vue index d26ef848..9aaf1870 100644 --- a/ux/src/components/HeaderNav.vue +++ b/ux/src/components/HeaderNav.vue @@ -68,7 +68,7 @@ q-header.bg-header.text-white.site-header( q-space transition(name='syncing') q-spinner-tail( - v-show='siteStore.routerLoading' + v-show='commonStore.routerLoading' color='accent' size='24px' ) @@ -130,6 +130,7 @@ import { useI18n } from 'vue-i18n' import { useQuasar } from 'quasar' import { reactive } from 'vue' +import { useCommonStore } from 'src/stores/common' import { useSiteStore } from 'src/stores/site' import { useUserStore } from 'src/stores/user' @@ -139,6 +140,7 @@ const $q = useQuasar() // STORES +const commonStore = useCommonStore() const siteStore = useSiteStore() const userStore = useUserStore() diff --git a/ux/src/layouts/AdminLayout.vue b/ux/src/layouts/AdminLayout.vue index ed1954e3..3e398c4a 100644 --- a/ux/src/layouts/AdminLayout.vue +++ b/ux/src/layouts/AdminLayout.vue @@ -18,11 +18,25 @@ q-layout.admin(view='hHh Lpr lff') q-space transition(name='syncing') q-spinner-tail( - v-show='siteStore.routerLoading' + v-show='commonStore.routerLoading' color='accent' 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 q-drawer.admin-sidebar(v-model='leftDrawerOpen', show-if-above, bordered) q-scroll-area.admin-nav( @@ -108,6 +122,9 @@ q-layout.admin(view='hHh Lpr lff') q-item-section(avatar) q-icon(name='img:/_assets/icons/fluent-ssd.svg') 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-section(avatar) q-icon(name='img:/_assets/icons/fluent-paint-roller.svg') @@ -159,7 +176,7 @@ q-layout.admin(view='hHh Lpr lff') q-icon(name='img:/_assets/icons/fluent-message-settings.svg') q-item-section {{ t('admin.mail.title') }} 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-section(avatar) q-icon(name='img:/_assets/icons/fluent-rich-text-converter.svg') @@ -169,7 +186,7 @@ q-layout.admin(view='hHh Lpr lff') q-icon(name='img:/_assets/icons/fluent-bot.svg') q-item-section {{ t('admin.scheduler.title') }} 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-section(avatar) q-icon(name='img:/_assets/icons/fluent-protect.svg') @@ -223,6 +240,7 @@ import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { useAdminStore } from 'src/stores/admin' +import { useCommonStore } from 'src/stores/common' import { useFlagsStore } from 'src/stores/flags' import { useSiteStore } from 'src/stores/site' import { useUserStore } from 'src/stores/user' @@ -244,6 +262,7 @@ const $q = useQuasar() // STORES const adminStore = useAdminStore() +const commonStore = useCommonStore() const flagsStore = useFlagsStore() const siteStore = useSiteStore() const userStore = useUserStore() @@ -266,13 +285,6 @@ useMeta({ // DATA 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 = { right: '1px', borderRadius: '5px', @@ -292,6 +304,9 @@ const siteSectionShown = computed(() => { const usersSectionShown = computed(() => { return userStore.can('manage:groups') || userStore.can('manage:users') }) +const overlayIsShown = computed(() => { + return Boolean(adminStore.overlay) +}) // WATCHERS @@ -308,9 +323,6 @@ watch(() => adminStore.sites, (newValue) => { }) } }) -watch(() => adminStore.overlay, (newValue) => { - overlayIsShown.value = !!newValue -}) watch(() => adminStore.currentSiteId, (newValue) => { if (newValue && route.params.siteid !== newValue) { router.push({ params: { siteid: newValue } }) @@ -325,6 +337,7 @@ onMounted(async () => { return } + adminStore.fetchLocales() await adminStore.fetchSites() if (route.params.siteid) { adminStore.$patch({ diff --git a/ux/src/stores/admin.js b/ux/src/stores/admin.js index 8481482f..62a70b90 100644 --- a/ux/src/stores/admin.js +++ b/ux/src/stores/admin.js @@ -35,24 +35,20 @@ export const useAdminStore = defineStore('admin', { } }, actions: { - async fetchSites () { + async fetchLocales () { const resp = await APOLLO_CLIENT.query({ query: gql` - query getSites { - sites { - id - hostname - isEnabled - title + query getAdminLocales { + locales { + code + language + name + nativeName } } - `, - fetchPolicy: 'network-only' + ` }) - this.sites = cloneDeep(resp?.data?.sites ?? []) - if (!this.currentSiteId) { - this.currentSiteId = this.sites[0].id - } + this.locales = cloneDeep(resp?.data?.locales ?? []) }, async fetchInfo () { const resp = await APOLLO_CLIENT.query({ @@ -78,6 +74,25 @@ export const useAdminStore = defineStore('admin', { this.info.isApiEnabled = clone(resp?.data?.apiState ?? false) this.info.isMailConfigured = clone(resp?.data?.systemInfo?.isMailConfigured ?? 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 + } } } }) diff --git a/ux/src/stores/common.js b/ux/src/stores/common.js new file mode 100644 index 00000000..44c7f5eb --- /dev/null +++ b/ux/src/stores/common.js @@ -0,0 +1,43 @@ +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) + } + } +}) diff --git a/ux/src/stores/site.js b/ux/src/stores/site.js index 057bdb5b..c08759aa 100644 --- a/ux/src/stores/site.js +++ b/ux/src/stores/site.js @@ -6,9 +6,7 @@ import { useUserStore } from './user' export const useSiteStore = defineStore('site', { state: () => ({ - routerLoading: false, id: null, - useLocales: false, hostname: '', company: '', contentLicense: '', @@ -39,6 +37,10 @@ export const useSiteStore = defineStore('site', { markdown: false, wysiwyg: false }, + locales: { + primary: 'en', + active: ['en'] + }, theme: { dark: false, injectCSS: '', @@ -84,6 +86,9 @@ export const useSiteStore = defineStore('site', { opacity: isDark ? 0.25 : 1 } } + }, + useLocales: (state) => { + return state.locales?.active?.length > 1 } }, actions: { @@ -104,20 +109,9 @@ export const useSiteStore = defineStore('site', { hostname: $hostname exact: false ) { - id - hostname - title - description - logoText company contentLicense - footerExtra - features { - profile - ratingsMode - reasonForChange - search - } + description editors { asciidoc { isActive @@ -129,6 +123,20 @@ export const useSiteStore = defineStore('site', { isActive } } + features { + profile + ratingsMode + reasonForChange + search + } + footerExtra + hostname + id + locales { + primary + active + } + logoText theme { dark colorPrimary @@ -144,6 +152,7 @@ export const useSiteStore = defineStore('site', { baseFont contentFont } + title } } `, @@ -171,6 +180,10 @@ export const useSiteStore = defineStore('site', { markdown: clone(siteInfo.editors.markdown.isActive), wysiwyg: clone(siteInfo.editors.wysiwyg.isActive) }, + locales: { + primary: clone(siteInfo.locales.primary), + active: clone(siteInfo.locales.active) + }, theme: { ...this.theme, ...clone(siteInfo.theme)