From ec3b7af0cea0c4379e1493a1d769d72e0bc751e4 Mon Sep 17 00:00:00 2001 From: braks <78412429+bcakmakoglu@users.noreply.github.com> Date: Fri, 29 Jul 2022 11:25:45 +0200 Subject: [PATCH] refactor(gui-v2): replace `useGlobalState` with `useGlobal` # What's changed? * `useGlobal` replaces `useGlobalState` * split into actions, getters, state * injects into nuxt app on init (regardless where it's called) --- .../composables/useGlobal/actions.ts | 54 ++++++ .../composables/useGlobal/getters.ts | 20 +++ .../nc-gui-v2/composables/useGlobal/index.ts | 53 ++++++ .../nc-gui-v2/composables/useGlobal/state.ts | 86 +++++++++ .../composables/useGlobalState/index.ts | 169 ------------------ .../composables/useUIPermission/index.ts | 2 +- packages/nc-gui-v2/plugins/state.ts | 19 +- packages/nc-gui-v2/utils/colorsUtils.ts | 2 +- 8 files changed, 228 insertions(+), 177 deletions(-) create mode 100644 packages/nc-gui-v2/composables/useGlobal/actions.ts create mode 100644 packages/nc-gui-v2/composables/useGlobal/getters.ts create mode 100644 packages/nc-gui-v2/composables/useGlobal/index.ts create mode 100644 packages/nc-gui-v2/composables/useGlobal/state.ts diff --git a/packages/nc-gui-v2/composables/useGlobal/actions.ts b/packages/nc-gui-v2/composables/useGlobal/actions.ts new file mode 100644 index 0000000000..fbbc0ac019 --- /dev/null +++ b/packages/nc-gui-v2/composables/useGlobal/actions.ts @@ -0,0 +1,54 @@ +import { notification } from 'ant-design-vue' +import type { UseGlobalStateReturn } from './state' +import type { Actions } from '~/lib' +import { useNuxtApp } from '#imports' + +export function useGlobalActions(state: UseGlobalStateReturn) { + const { $api } = useNuxtApp() + + /** Actions */ + /** Sign out by deleting the token from localStorage */ + const signOut: Actions['signOut'] = () => { + state.token.value = null + state.user.value = null + } + + /** Sign in by setting the token in localStorage */ + const signIn: Actions['signIn'] = async (newToken) => { + state.token.value = newToken + + if (state.payload.value) { + state.user.value = { + id: state.payload.value.id, + email: state.payload.value.email, + firstname: state.payload.value.firstname, + lastname: state.payload.value.lastname, + roles: state.payload.value.roles, + } + } + } + + /** manually try to refresh token */ + const refreshToken = async () => { + $api.instance + .post('/auth/refresh-token', null, { + withCredentials: true, + }) + .then((response) => { + if (response.data?.token) { + signIn(response.data.token) + } + }) + .catch((err) => { + notification.error({ + // todo: add translation + message: err.message || 'You have been signed out.', + }) + console.error(err) + + signOut() + }) + } + + return { signIn, signOut, refreshToken } +} diff --git a/packages/nc-gui-v2/composables/useGlobal/getters.ts b/packages/nc-gui-v2/composables/useGlobal/getters.ts new file mode 100644 index 0000000000..057c9b831a --- /dev/null +++ b/packages/nc-gui-v2/composables/useGlobal/getters.ts @@ -0,0 +1,20 @@ +import type { UseGlobalStateReturn } from './state' +import type { Getters } from '~/lib' +import { computed } from '#imports' + +export function useGlobalGetters(state: UseGlobalStateReturn) { + /** Getters */ + /** Verify that a user is signed in by checking if token exists and is not expired */ + const signedIn: Getters['signedIn'] = computed( + () => + !!( + !!state.token && + state.token.value !== '' && + state.payload.value && + state.payload.value.exp && + state.payload.value.exp > state.timestamp.value / 1000 + ), + ) + + return { signedIn } +} diff --git a/packages/nc-gui-v2/composables/useGlobal/index.ts b/packages/nc-gui-v2/composables/useGlobal/index.ts new file mode 100644 index 0000000000..6e0e0b7191 --- /dev/null +++ b/packages/nc-gui-v2/composables/useGlobal/index.ts @@ -0,0 +1,53 @@ +import { useGlobalState } from './state' +import { useGlobalActions } from './actions' +import { toRefs, useNuxtApp, watch } from '#imports' +import type { GlobalState } from '~/lib' +import { useGlobalGetters } from '~/composables/useGlobal/getters' + +/** + * Global state is injected by {@link import('~/plugins/state') state} plugin into our nuxt app (available as `$state`). + * Manual initialization is unnecessary and should be avoided. + * + * The state is stored in {@link WindowLocalStorage localStorage}, so it will be available even if the user closes the browser tab. + * + * @example + * ```js + * import { useNuxtApp } from '#app' + * + * const { $state } = useNuxtApp() + * + * const token = $state.token.value + * const user = $state.user.value + * ``` + */ +export const useGlobal = (): GlobalState => { + const { $state, provide } = useNuxtApp() + + if ($state) { + console.warn( + '[useGlobalState] Global state is injected by state plugin. Manual initialization is unnecessary and should be avoided.', + ) + return $state + } + + const state = $(useGlobalState()) + + const getters = useGlobalGetters($$(state)) + + const actions = useGlobalActions($$(state)) + + /** try to refresh token before expiry (5 min before expiry) */ + watch( + () => !!(state.payload && state.payload.exp && state.payload.exp - 5 * 60 < state.timestamp / 1000), + async (expiring) => { + if (getters.signedIn.value && state.payload && expiring) { + await actions.refreshToken() + } + }, + { immediate: true }, + ) + + provide('state', state) + + return { ...toRefs(state), ...getters, ...actions } +} diff --git a/packages/nc-gui-v2/composables/useGlobal/state.ts b/packages/nc-gui-v2/composables/useGlobal/state.ts new file mode 100644 index 0000000000..b546afb5f5 --- /dev/null +++ b/packages/nc-gui-v2/composables/useGlobal/state.ts @@ -0,0 +1,86 @@ +import { usePreferredLanguages, useStorage } from '@vueuse/core' +import { useJwt } from '@vueuse/integrations/useJwt' +import type { JwtPayload } from 'jwt-decode' +import type { ComputedRef, Ref, ToRefs } from 'vue' +import type { WritableComputedRef } from '@vue/reactivity' +import { computed, ref, toRefs, useNuxtApp, useTimestamp } from '#imports' +import type { StoredState, User } from '~/lib' + +const storageKey = 'nocodb-gui-v2' + +export type UseGlobalStateReturn = ToRefs & { + token: WritableComputedRef + payload: ComputedRef<(JwtPayload & User) | null> + sidebarOpen: Ref + timestamp: Ref +} + +export function useGlobalState(): UseGlobalStateReturn { + /** get the preferred languages of a user, according to browser settings */ + const preferredLanguages = $(usePreferredLanguages()) + /** todo: reimplement; get the preferred dark mode setting, according to browser settings */ + // const prefersDarkMode = $(usePreferredDark()) + const prefersDarkMode = false + + const { + vueApp: { i18n }, + } = useNuxtApp() + + /** + * Set initial language based on browser settings. + * If the user has not set a preferred language, we fallback to 'en'. + * If the user has set a preferred language, we try to find a matching locale in the available locales. + */ + const preferredLanguage = preferredLanguages.reduce((locale, language) => { + /** split language to language and code, e.g. en-GB -> [en, GB] */ + const [lang, code] = language.split(/[_-]/) + + /** find all locales that match the language */ + let availableLocales = i18n.availableLocales.filter((locale) => locale.startsWith(lang)) + + /** If we can match more than one locale, we check if the code of the language matches as well */ + if (availableLocales.length > 1) { + availableLocales = availableLocales.filter((locale) => locale.endsWith(code)) + } + + /** if there are still multiple locales, pick the first one */ + const availableLocale = availableLocales[0] + + /** if we found a matching locale, return it */ + if (availableLocale) locale = availableLocale + + return locale + }, 'en' /** fallback locale */) + + /** State */ + const initialState: StoredState = { token: null, user: null, lang: preferredLanguage, darkMode: prefersDarkMode } + + /** saves a reactive state, any change to these values will write/delete to localStorage */ + const storage = useStorage(storageKey, initialState) + + /** force turn off of dark mode, regardless of previously stored settings */ + storage.value.darkMode = false + + /** current token ref, used by `useJwt` to reactively parse our token payload */ + const token = computed({ + get: () => storage.value.token || '', + set: (val) => (storage.value.token = val), + }) + + /** reactive token payload */ + const { payload } = useJwt(token.value) + + /** is sidebar open */ + const sidebarOpen = ref(false) + + /** reactive timestamp to check token expiry against */ + const timestamp = useTimestamp({ immediate: true, interval: 100 }) + + return { + ...(toRefs(storage) as any), + token, + payload, + sidebarOpen, + timestamp, + } +} diff --git a/packages/nc-gui-v2/composables/useGlobalState/index.ts b/packages/nc-gui-v2/composables/useGlobalState/index.ts index 026b749b79..e69de29bb2 100644 --- a/packages/nc-gui-v2/composables/useGlobalState/index.ts +++ b/packages/nc-gui-v2/composables/useGlobalState/index.ts @@ -1,169 +0,0 @@ -import { breakpointsTailwind, usePreferredLanguages, useStorage } from '@vueuse/core' -import { useJwt } from '@vueuse/integrations/useJwt' -import type { JwtPayload } from 'jwt-decode' -import { notification } from 'ant-design-vue' -import initialFeedBackForm from './initialFeedbackForm' -import { computed, ref, toRefs, useBreakpoints, useNuxtApp, useTimestamp, watch } from '#imports' -import type { Actions, Getters, GlobalState, StoredState, User } from '~/lib/types' - -const storageKey = 'nocodb-gui-v2' - -/** - * Global state is injected by {@link import('~/plugins/state') state} plugin into our nuxt app (available as `$state`). - * Manual initialization is unnecessary and should be avoided. - * - * The state is stored in {@link WindowLocalStorage localStorage}, so it will be available even if the user closes the browser tab. - * - * @example - * ```js - * import { useNuxtApp } from '#app' - * - * const { $state } = useNuxtApp() - * - * const token = $state.token.value - * const user = $state.user.value - * ``` - */ -export const useGlobalState = (): GlobalState => { - const { $state } = useNuxtApp() - - if ($state) { - console.warn( - '[useGlobalState] Global state is injected by state plugin. Manual initialization is unnecessary and should be avoided.', - ) - return $state - } - - /** get the preferred languages of a user, according to browser settings */ - const preferredLanguages = $(usePreferredLanguages()) - /** todo: reimplement; get the preferred dark mode setting, according to browser settings */ - // const prefersDarkMode = $(usePreferredDark()) - const prefersDarkMode = false - - /** get current breakpoints (for enabling sidebar) */ - const breakpoints = useBreakpoints(breakpointsTailwind) - - /** reactive timestamp to check token expiry against */ - const timestamp = $(useTimestamp({ immediate: true, interval: 100 })) - - const { - $api, - vueApp: { i18n }, - } = useNuxtApp() - - /** - * Set initial language based on browser settings. - * If the user has not set a preferred language, we fallback to 'en'. - * If the user has set a preferred language, we try to find a matching locale in the available locales. - */ - const preferredLanguage = preferredLanguages.reduce((locale, language) => { - /** split language to language and code, e.g. en-GB -> [en, GB] */ - const [lang, code] = language.split(/[_-]/) - - /** find all locales that match the language */ - let availableLocales = i18n.availableLocales.filter((locale) => locale.startsWith(lang)) - - /** If we can match more than one locale, we check if the code of the language matches as well */ - if (availableLocales.length > 1) { - availableLocales = availableLocales.filter((locale) => locale.endsWith(code)) - } - - /** if there are still multiple locales, pick the first one */ - const availableLocale = availableLocales[0] - - /** if we found a matching locale, return it */ - if (availableLocale) locale = availableLocale - - return locale - }, 'en' /** fallback locale */) - - /** State */ - const initialState: StoredState = { - token: null, - user: null, - lang: preferredLanguage, - darkMode: prefersDarkMode, - feedbackForm: initialFeedBackForm, - } - - /** saves a reactive state, any change to these values will write/delete to localStorage */ - const storage = $(useStorage(storageKey, initialState)) - - /** force turn off of dark mode, regardless of previously stored settings */ - storage.darkMode = false - - /** current token ref, used by `useJwt` to reactively parse our token payload */ - let token = $computed({ - get: () => storage.token || '', - set: (val) => (storage.token = val), - }) - - /** reactive token payload */ - const { payload } = $(useJwt($$(token!))) - - /** Getters */ - /** Verify that a user is signed in by checking if token exists and is not expired */ - const signedIn: Getters['signedIn'] = computed( - () => !!(!!token && token !== '' && payload && payload.exp && payload.exp > timestamp / 1000), - ) - - /** is sidebar open */ - const sidebarOpen = ref(signedIn.value && breakpoints.greater('md').value) - - /** Actions */ - /** Sign out by deleting the token from localStorage */ - const signOut: Actions['signOut'] = () => { - storage.token = null - storage.user = null - } - - /** Sign in by setting the token in localStorage */ - const signIn: Actions['signIn'] = async (newToken) => { - token = newToken - - if (payload) { - storage.user = { - id: payload.id, - email: payload.email, - firstname: payload.firstname, - lastname: payload.lastname, - roles: payload.roles, - } - } - } - - /** manually try to refresh token */ - const refreshToken = async () => { - $api.instance - .post('/auth/refresh-token', null, { - withCredentials: true, - }) - .then((response) => { - if (response.data?.token) { - signIn(response.data.token) - } - }) - .catch((err) => { - notification.error({ - // todo: add translation - message: err.message || 'You have been signed out.', - }) - console.error(err) - - signOut() - }) - } - - /** try to refresh token before expiry (5 min before expiry) */ - watch( - () => !!(payload && payload.exp && payload.exp - 5 * 60 < timestamp / 1000), - async (expiring) => { - if (signedIn.value && payload && expiring) { - await refreshToken() - } - }, - { immediate: true }, - ) - - return { ...toRefs(storage), signedIn, signOut, signIn, sidebarOpen } -} diff --git a/packages/nc-gui-v2/composables/useUIPermission/index.ts b/packages/nc-gui-v2/composables/useUIPermission/index.ts index 8ac272f97a..c2fc037893 100644 --- a/packages/nc-gui-v2/composables/useUIPermission/index.ts +++ b/packages/nc-gui-v2/composables/useUIPermission/index.ts @@ -2,7 +2,7 @@ import rolePermissions from './rolePermissions' import { useState } from '#app' import { USER_PROJECT_ROLES } from '~/lib/constants' -export default () => { +export function useUIPermission() { const { $state } = useNuxtApp() const projectRoles = useState>(USER_PROJECT_ROLES, () => ({})) diff --git a/packages/nc-gui-v2/plugins/state.ts b/packages/nc-gui-v2/plugins/state.ts index 36b60df19c..c7cd9d5fe2 100644 --- a/packages/nc-gui-v2/plugins/state.ts +++ b/packages/nc-gui-v2/plugins/state.ts @@ -1,6 +1,6 @@ +import { breakpointsTailwind } from '@vueuse/core' import { defineNuxtPlugin } from '#app' -import { useDark, watch } from '#imports' -import { useGlobalState } from '~/composables/useGlobalState' +import { useBreakpoints, useDark, useGlobal, watch } from '#imports' /** * Injects global state into nuxt app. @@ -15,20 +15,27 @@ import { useGlobalState } from '~/composables/useGlobalState' * ``` */ export default defineNuxtPlugin((nuxtApp) => { - const storage = useGlobalState() + const state = useGlobal() + const darkMode = useDark() + /** get current breakpoints (for enabling sidebar) */ + const breakpoints = useBreakpoints(breakpointsTailwind) + /** set i18n locale to stored language */ - nuxtApp.vueApp.i18n.locale.value = storage.lang.value + nuxtApp.vueApp.i18n.locale.value = state.lang.value /** set current dark mode from storage */ watch( - storage.darkMode, + state.darkMode, (newMode) => { darkMode.value = newMode }, { immediate: true }, ) - nuxtApp.provide('state', storage) + /** is initial sidebar open */ + state.sidebarOpen.value = state.signedIn.value && breakpoints.greater('md').value + + nuxtApp.provide('state', state) }) diff --git a/packages/nc-gui-v2/utils/colorsUtils.ts b/packages/nc-gui-v2/utils/colorsUtils.ts index 4071abc981..fe5a297dc3 100644 --- a/packages/nc-gui-v2/utils/colorsUtils.ts +++ b/packages/nc-gui-v2/utils/colorsUtils.ts @@ -1,4 +1,4 @@ -export default { +export const theme = { light: ['#ffdce5', '#fee2d5', '#ffeab6', '#d1f7c4', '#ede2fe', '#eee', '#cfdffe', '#d0f1fd', '#c2f5e8', '#ffdaf6'], dark: [ '#f82b6099',