mirror of https://github.com/nocodb/nocodb
90 changed files with 680 additions and 503 deletions
@ -0,0 +1,20 @@
|
||||
export * from './useApi' |
||||
export * from './useGlobal' |
||||
export * from './useUIPermission' |
||||
export * from './useAttachment' |
||||
export * from './useBelongsTo' |
||||
export * from './useColors' |
||||
export * from './useColumn' |
||||
export * from './useGridViewColumnWidth' |
||||
export * from './useHasMany' |
||||
export * from './useManyToMany' |
||||
export * from './useMetas' |
||||
export * from './useProject' |
||||
export * from './useTableCreate' |
||||
export * from './useTabs' |
||||
export * from './useViewColumns' |
||||
export * from './useViewData' |
||||
export * from './useViewFilters' |
||||
export * from './useViews' |
||||
export * from './useViewSorts' |
||||
export * from './useVirtualCell' |
@ -0,0 +1,26 @@
|
||||
import type { Api } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' |
||||
import type { EventHook, MaybeRef } from '@vueuse/core' |
||||
|
||||
export interface UseApiReturn<D = any, R = any> { |
||||
api: Api<any> |
||||
isLoading: Ref<boolean> |
||||
error: Ref<AxiosError<D, R> | null> |
||||
response: Ref<AxiosResponse<D, R> | null> |
||||
onError: EventHook<AxiosError<D, R>>['on'] |
||||
onResponse: EventHook<AxiosResponse<D, R>>['on'] |
||||
} |
||||
|
||||
/** {@link Api} options */ |
||||
export interface CreateApiOptions { |
||||
baseURL?: string |
||||
} |
||||
|
||||
export interface UseApiProps<D = any> { |
||||
/** additional axios config for requests */ |
||||
axiosConfig?: MaybeRef<AxiosRequestConfig<D>> |
||||
/** {@link Api} options */ |
||||
apiOptions?: CreateApiOptions |
||||
useGlobalInstance?: boolean |
||||
} |
@ -0,0 +1,54 @@
|
||||
import { notification } from 'ant-design-vue' |
||||
import type { Actions, State } from './types' |
||||
import { useNuxtApp } from '#imports' |
||||
|
||||
export function useGlobalActions(state: State): Actions { |
||||
// todo replace with just `new Api()`? Would solve recursion issues
|
||||
/** we have to use the globally injected api instance, otherwise we run into recursion as `useApi` calls `useGlobal` */ |
||||
const { $api } = useNuxtApp() |
||||
|
||||
/** 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.jwtPayload.value) { |
||||
state.user.value = { |
||||
id: state.jwtPayload.value.id, |
||||
email: state.jwtPayload.value.email, |
||||
firstname: state.jwtPayload.value.firstname, |
||||
lastname: state.jwtPayload.value.lastname, |
||||
roles: state.jwtPayload.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 } |
||||
} |
@ -0,0 +1,25 @@
|
||||
import type { Getters, State } from './types' |
||||
import { computed } from '#imports' |
||||
|
||||
export function useGlobalGetters(state: State): 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.jwtPayload.value && |
||||
state.jwtPayload.value.exp && |
||||
state.jwtPayload.value.exp > state.timestamp.value / 1000 |
||||
), |
||||
) |
||||
|
||||
/** global loading state */ |
||||
let loading = $ref(false) |
||||
const isLoading = computed({ |
||||
get: () => state.runningRequests.count.value > 0 || loading, |
||||
set: (_loading) => (loading = _loading), |
||||
}) |
||||
|
||||
return { signedIn, isLoading } |
||||
} |
@ -0,0 +1,71 @@
|
||||
import { useGlobalState } from './state' |
||||
import { useGlobalActions } from './actions' |
||||
import type { UseGlobalReturn } from './types' |
||||
import { useGlobalGetters } from './getters' |
||||
import { useNuxtApp, watch } from '#imports' |
||||
|
||||
/** |
||||
* Global state is injected by {@link import('~/plugins/state') state} plugin into our nuxt app (available as `$state`). |
||||
* You can still call `useGlobal` to receive the `$state` object and access the global state. |
||||
* If it's not available yet, a new global state object is created and injected into the nuxt app. |
||||
* |
||||
* Part of the state is stored in {@link WindowLocalStorage localStorage}, so it will be available even if the user closes the browser tab. |
||||
* Check the {@link StoredState StoredState} type for more information. |
||||
* |
||||
* @example |
||||
* ```js
|
||||
* import { useNuxtApp } from '#app' |
||||
* |
||||
* const { $state } = useNuxtApp() |
||||
* |
||||
* const token = $state.token.value |
||||
* const user = $state.user.value |
||||
* ``` |
||||
* |
||||
* @example |
||||
* ```js
|
||||
* import { useGlobal } from '#imports' |
||||
* |
||||
* const globalState = useGlobal() |
||||
* |
||||
* cont token = globalState.token.value |
||||
* const user = globalState.user.value |
||||
* |
||||
* console.log(state.isLoading.value) // isLoading = true if any api request is still running
|
||||
* ``` |
||||
*/ |
||||
export const useGlobal = (): UseGlobalReturn => { |
||||
const { $state, provide } = useNuxtApp() |
||||
|
||||
/** If state already exists, return it */ |
||||
if (typeof $state !== 'undefined') 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.jwtPayload.value && |
||||
state.jwtPayload.value.exp && |
||||
state.jwtPayload.value.exp - 5 * 60 < state.timestamp.value / 1000 |
||||
), |
||||
async (expiring) => { |
||||
if (getters.signedIn.value && state.jwtPayload.value && expiring) { |
||||
await actions.refreshToken() |
||||
} |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
|
||||
const globalState = { ...state, ...getters, ...actions } as UseGlobalReturn |
||||
|
||||
/** provide a fresh state instance into nuxt app */ |
||||
provide('state', globalState) |
||||
|
||||
return globalState |
||||
} |
@ -0,0 +1,95 @@
|
||||
import { usePreferredLanguages, useStorage } from '@vueuse/core' |
||||
import { useJwt } from '@vueuse/integrations/useJwt' |
||||
import type { JwtPayload } from 'jwt-decode' |
||||
import type { State, StoredState } from './types' |
||||
import { computed, ref, toRefs, useCounter, useNuxtApp, useTimestamp } from '#imports' |
||||
import type { User } from '~/lib' |
||||
|
||||
export function useGlobalState(storageKey = 'nocodb-gui-v2'): 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 |
||||
|
||||
/** reactive timestamp to check token expiry against */ |
||||
const timestamp = useTimestamp({ immediate: true, interval: 100 }) |
||||
|
||||
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, |
||||
feedbackForm: { |
||||
url: 'https://docs.google.com/forms/d/e/1FAIpQLSeTlAfZjszgr53lArz3NvUEnJGOT9JtG9NAU5d0oQwunDS2Pw/viewform?embedded=true', |
||||
createdAt: new Date('2020-01-01T00:00:00.000Z').toISOString(), |
||||
isHidden: false, |
||||
}, |
||||
} |
||||
|
||||
/** saves a reactive state, any change to these values will write/delete to localStorage */ |
||||
const storage = useStorage<StoredState>(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<JwtPayload & User>(token) |
||||
|
||||
/** is sidebar open */ |
||||
const sidebarOpen = ref(false) |
||||
|
||||
/** currently running requests */ |
||||
const runningRequests = useCounter() |
||||
|
||||
/** global error */ |
||||
const error = ref() |
||||
|
||||
return { |
||||
...toRefs(storage.value), |
||||
storage, |
||||
token, |
||||
jwtPayload: payload, |
||||
sidebarOpen, |
||||
timestamp, |
||||
runningRequests, |
||||
error, |
||||
} |
||||
} |
@ -0,0 +1,45 @@
|
||||
import type { ComputedRef, Ref, ToRefs } from 'vue' |
||||
import type { WritableComputedRef } from '@vue/reactivity' |
||||
import type { JwtPayload } from 'jwt-decode' |
||||
import type { User } from '~/lib' |
||||
import type { useCounter } from '#imports' |
||||
|
||||
export interface FeedbackForm { |
||||
url: string |
||||
createdAt: string |
||||
isHidden: boolean |
||||
lastFormPollDate?: string |
||||
} |
||||
|
||||
export interface StoredState { |
||||
token: string | null |
||||
user: User | null |
||||
lang: string |
||||
darkMode: boolean |
||||
feedbackForm: FeedbackForm |
||||
} |
||||
|
||||
export type State = ToRefs<Omit<StoredState, 'token'>> & { |
||||
storage: Ref<StoredState> |
||||
token: WritableComputedRef<StoredState['token']> |
||||
jwtPayload: ComputedRef<(JwtPayload & User) | null> |
||||
sidebarOpen: Ref<boolean> |
||||
timestamp: Ref<number> |
||||
runningRequests: ReturnType<typeof useCounter> |
||||
error: Ref<any> |
||||
} |
||||
|
||||
export interface Getters { |
||||
signedIn: ComputedRef<boolean> |
||||
isLoading: WritableComputedRef<boolean> |
||||
} |
||||
|
||||
export interface Actions { |
||||
signOut: () => void |
||||
signIn: (token: string) => void |
||||
refreshToken: () => void |
||||
} |
||||
|
||||
export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'> |
||||
|
||||
export type UseGlobalReturn = Getters & Actions & ReadonlyState |
@ -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<string>((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<StoredState>(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<JwtPayload & User>($$(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 } |
||||
} |
@ -1,5 +0,0 @@
|
||||
export default { |
||||
url: 'https://docs.google.com/forms/d/e/1FAIpQLSeTlAfZjszgr53lArz3NvUEnJGOT9JtG9NAU5d0oQwunDS2Pw/viewform?embedded=true', |
||||
createdAt: new Date('2020-01-01T00:00:00.000Z').toISOString(), |
||||
isHidden: false, |
||||
} |
@ -1,7 +1,6 @@
|
||||
import { defineNuxtPlugin } from '#imports' |
||||
import { createApiInstance } from '~/composables/useApi' |
||||
import { defineNuxtPlugin, useApi } from '#imports' |
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => { |
||||
/** injects a global api instance */ |
||||
nuxtApp.provide('api', createApiInstance(nuxtApp)) |
||||
nuxtApp.provide('api', useApi().api) |
||||
}) |
||||
|
Loading…
Reference in new issue