chore: split app to commons and web (squash commit)
This commit is contained in:
32
packages/hoppscotch-common/src/modules/crisp.ts
Normal file
32
packages/hoppscotch-common/src/modules/crisp.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { HoppModule } from "."
|
||||
|
||||
export const showChat = () => {
|
||||
;(window as any).$crisp.push([
|
||||
"do",
|
||||
"chat:show",
|
||||
(window as any).$crisp.push(["do", "chat:open"]),
|
||||
])
|
||||
}
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit() {
|
||||
// TODO: Env variable this ?
|
||||
;(window as any).$crisp = []
|
||||
;(window as any).CRISP_WEBSITE_ID = "3ad30257-c192-4773-955d-fb05a4b41af3"
|
||||
|
||||
const d = document
|
||||
const s = d.createElement("script")
|
||||
|
||||
s.src = "https://client.crisp.chat/l.js"
|
||||
s.async = true
|
||||
d.getElementsByTagName("head")[0].appendChild(s)
|
||||
;(window as any).$crisp.push(["do", "chat:hide"])
|
||||
;(window as any).$crisp.push([
|
||||
"on",
|
||||
"chat:closed",
|
||||
() => {
|
||||
;(window as any).$crisp.push(["do", "chat:hide"])
|
||||
},
|
||||
])
|
||||
},
|
||||
}
|
||||
21
packages/hoppscotch-common/src/modules/head.ts
Normal file
21
packages/hoppscotch-common/src/modules/head.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createHead, useHead } from "@vueuse/head"
|
||||
import { APP_INFO } from "~/../meta"
|
||||
import { HoppModule } from "."
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
const head = createHead({
|
||||
title: `${APP_INFO.name} • ${APP_INFO.shortDescription}`,
|
||||
titleTemplate(title) {
|
||||
return title === "Hoppscotch" ? title : `${title} • Hoppscotch`
|
||||
},
|
||||
})
|
||||
|
||||
app.use(head)
|
||||
},
|
||||
|
||||
onRootSetup() {
|
||||
// Load the defaults into the app
|
||||
useHead({})
|
||||
},
|
||||
}
|
||||
58
packages/hoppscotch-common/src/modules/hoppExtension.ts
Normal file
58
packages/hoppscotch-common/src/modules/hoppExtension.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { HoppModule } from "."
|
||||
import {
|
||||
changeExtensionStatus,
|
||||
ExtensionStatus,
|
||||
} from "~/newstore/HoppExtension"
|
||||
import { ref } from "vue"
|
||||
import { defineSubscribableObject } from "~/helpers/strategies/ExtensionStrategy"
|
||||
|
||||
/* Module defining the hooking mechanism between Hoppscotch and the Hoppscotch Browser Extension */
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit() {
|
||||
const extensionPollIntervalId = ref<ReturnType<typeof setInterval>>()
|
||||
|
||||
if (window.__HOPP_EXTENSION_STATUS_PROXY__) {
|
||||
changeExtensionStatus(window.__HOPP_EXTENSION_STATUS_PROXY__.status)
|
||||
|
||||
window.__HOPP_EXTENSION_STATUS_PROXY__.subscribe(
|
||||
"status",
|
||||
(status: ExtensionStatus) => changeExtensionStatus(status)
|
||||
)
|
||||
} else {
|
||||
const statusProxy = defineSubscribableObject({
|
||||
status: "waiting" as ExtensionStatus,
|
||||
})
|
||||
|
||||
window.__HOPP_EXTENSION_STATUS_PROXY__ = statusProxy
|
||||
statusProxy.subscribe("status", (status: ExtensionStatus) =>
|
||||
changeExtensionStatus(status)
|
||||
)
|
||||
|
||||
/**
|
||||
* Keeping identifying extension backward compatible
|
||||
* We are assuming the default version is 0.24 or later. So if the extension exists, its identified immediately,
|
||||
* then we use a poll to find the version, this will get the version for 0.24 and any other version
|
||||
* of the extension, but will have a slight lag.
|
||||
* 0.24 users will get the benefits of 0.24, while the extension won't break for the old users
|
||||
*/
|
||||
extensionPollIntervalId.value = setInterval(() => {
|
||||
if (typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined") {
|
||||
if (extensionPollIntervalId.value)
|
||||
clearInterval(extensionPollIntervalId.value)
|
||||
|
||||
const version = window.__POSTWOMAN_EXTENSION_HOOK__.getVersion()
|
||||
|
||||
// When the version is not 0.24 or higher, the extension wont do this. so we have to do it manually
|
||||
if (
|
||||
version.major === 0 &&
|
||||
version.minor <= 23 &&
|
||||
window.__HOPP_EXTENSION_STATUS_PROXY__
|
||||
) {
|
||||
window.__HOPP_EXTENSION_STATUS_PROXY__.status = "available"
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
},
|
||||
}
|
||||
156
packages/hoppscotch-common/src/modules/i18n.ts
Normal file
156
packages/hoppscotch-common/src/modules/i18n.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import * as R from "fp-ts/Record"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { createI18n, I18n, I18nOptions } from "vue-i18n"
|
||||
import { HoppModule } from "."
|
||||
|
||||
import languages from "../../languages.json"
|
||||
|
||||
import en from "../../locales/en.json"
|
||||
import { throwError } from "~/helpers/functional/error"
|
||||
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
|
||||
|
||||
/*
|
||||
In context of this file, we have 2 main kinds of things.
|
||||
1. Locale -> A locale is termed as the i18n entries present in the /locales folder
|
||||
2. Language -> A language is an entry in the /languages.json folder
|
||||
|
||||
Each language entry should correspond to a locale entry.
|
||||
*/
|
||||
|
||||
/*
|
||||
* As we migrate out of Nuxt I18n into our own system for i18n management,
|
||||
* Some stuff has changed regarding how it works.
|
||||
*
|
||||
* The previous system works by using paths to represent locales to load.
|
||||
* Basically, /es/realtime will load the /realtime page but with 'es' language
|
||||
*
|
||||
* In the new system instead of relying on the lang code, we store the language
|
||||
* in the application local config store (localStorage). The URLs don't have
|
||||
* a locale path effect
|
||||
*/
|
||||
|
||||
// TODO: Syncing into settings ?
|
||||
|
||||
const LOCALES = import.meta.glob("../../locales/*.json")
|
||||
|
||||
type LanguagesDef = {
|
||||
code: string
|
||||
file: string
|
||||
iso: string
|
||||
name: string
|
||||
dir?: "ltr" | "rtl" // Text Orientation (defaults to 'ltr')
|
||||
}
|
||||
|
||||
const FALLBACK_LANG_CODE = "en"
|
||||
|
||||
// TypeScript cannot understand dir is restricted to "ltr" or "rtl" yet, hence assertion
|
||||
export const APP_LANGUAGES: LanguagesDef[] = languages as LanguagesDef[]
|
||||
|
||||
export const APP_LANG_CODES = languages.map(({ code }) => code)
|
||||
|
||||
export const FALLBACK_LANG = pipe(
|
||||
APP_LANGUAGES,
|
||||
A.findFirst((x) => x.code === FALLBACK_LANG_CODE),
|
||||
O.getOrElseW(() =>
|
||||
throwError(`Could not find the fallback language '${FALLBACK_LANG_CODE}'`)
|
||||
)
|
||||
)
|
||||
|
||||
// A reference to the i18n instance
|
||||
let i18nInstance: I18n<any, any, any> | null = null
|
||||
|
||||
const resolveCurrentLocale = () =>
|
||||
pipe(
|
||||
// Resolve from locale and make sure it is in languages
|
||||
getLocalConfig("locale"),
|
||||
O.fromNullable,
|
||||
O.filter((locale) =>
|
||||
pipe(
|
||||
APP_LANGUAGES,
|
||||
A.some(({ code }) => code === locale)
|
||||
)
|
||||
),
|
||||
|
||||
// Else load from navigator.language
|
||||
O.alt(() =>
|
||||
pipe(
|
||||
APP_LANGUAGES,
|
||||
A.findFirst(({ code }) => navigator.language.startsWith(code)), // en-US should also match to en
|
||||
O.map(({ code }) => code)
|
||||
)
|
||||
),
|
||||
|
||||
// Else load fallback
|
||||
O.getOrElse(() => FALLBACK_LANG_CODE)
|
||||
)
|
||||
|
||||
/**
|
||||
* Changes the application language. This function returns a promise as
|
||||
* the locale files are lazy loaded on demand
|
||||
* @param locale The locale code of the language to load
|
||||
*/
|
||||
export const changeAppLanguage = async (locale: string) => {
|
||||
const localeData = (
|
||||
(await pipe(
|
||||
LOCALES,
|
||||
R.lookup(`../../locales/${locale}.json`),
|
||||
O.getOrElseW(() =>
|
||||
throwError(
|
||||
`Tried to change app language to non-existent locale '${locale}'`
|
||||
)
|
||||
)
|
||||
)()) as any
|
||||
).default
|
||||
|
||||
if (!i18nInstance) {
|
||||
throw new Error("Tried to change language without active i18n instance")
|
||||
}
|
||||
|
||||
i18nInstance.global.setLocaleMessage(locale, localeData)
|
||||
|
||||
// TODO: Look into the type issues here
|
||||
i18nInstance.global.locale.value = locale
|
||||
|
||||
setLocalConfig("locale", locale)
|
||||
}
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
const i18n = createI18n(<I18nOptions>{
|
||||
locale: "en", // TODO: i18n system!
|
||||
fallbackLocale: "en",
|
||||
legacy: false,
|
||||
allowComposition: true,
|
||||
|
||||
// TODO: Fix this to allow for dynamic imports
|
||||
messages: {
|
||||
en,
|
||||
},
|
||||
})
|
||||
|
||||
app.use(i18n)
|
||||
|
||||
i18nInstance = i18n
|
||||
|
||||
// TODO: Global loading state to hide the resolved lang loading
|
||||
const currentLocale = resolveCurrentLocale()
|
||||
changeAppLanguage(currentLocale)
|
||||
|
||||
setLocalConfig("locale", currentLocale)
|
||||
},
|
||||
onBeforeRouteChange(to, _, router) {
|
||||
// Convert old locale path format to new format
|
||||
const oldLocalePathLangCode = APP_LANG_CODES.find((langCode) =>
|
||||
to.path.startsWith(`/${langCode}/`)
|
||||
)
|
||||
|
||||
// Change language to the correct lang code
|
||||
if (oldLocalePathLangCode) {
|
||||
changeAppLanguage(oldLocalePathLangCode)
|
||||
|
||||
router.replace(to.path.substring(`/${oldLocalePathLangCode}`.length))
|
||||
}
|
||||
},
|
||||
}
|
||||
52
packages/hoppscotch-common/src/modules/index.ts
Normal file
52
packages/hoppscotch-common/src/modules/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { App } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as A from "fp-ts/Array"
|
||||
import { RouteLocationNormalized, Router } from "vue-router"
|
||||
|
||||
export type HoppModule = {
|
||||
/**
|
||||
* Define this function to get access to Vue App instance and augment
|
||||
* it (installing components, directives and plugins). Also useful for
|
||||
* early generic initializations. This function should be called first
|
||||
*/
|
||||
onVueAppInit?: (app: App) => void
|
||||
|
||||
/**
|
||||
* Called when the router is done initializing.
|
||||
* Used if a module requires access to the router instance
|
||||
*/
|
||||
onRouterInit?: (app: App, router: Router) => void
|
||||
|
||||
/**
|
||||
* Called when the root component (App.vue) is running setup.
|
||||
* This function is generally called last in the lifecycle.
|
||||
* This function executes with a component setup context, so you can
|
||||
* run composables within this and it should just be scoped to the
|
||||
* root component
|
||||
*/
|
||||
onRootSetup?: () => void
|
||||
|
||||
/**
|
||||
* Called by the router to tell all the modules before a route navigation
|
||||
* is made.
|
||||
*/
|
||||
onBeforeRouteChange?: (
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
router: Router
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Called by the router to tell all the modules that a route navigation has completed
|
||||
*/
|
||||
onAfterRouteChange?: (to: RouteLocationNormalized, router: Router) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* All the modules Hoppscotch loads into the app
|
||||
*/
|
||||
export const HOPP_MODULES = pipe(
|
||||
import.meta.glob("@modules/*.ts", { eager: true }),
|
||||
Object.values,
|
||||
A.map(({ default: defaultVal }) => defaultVal as HoppModule)
|
||||
)
|
||||
56
packages/hoppscotch-common/src/modules/loadingbar.ts
Normal file
56
packages/hoppscotch-common/src/modules/loadingbar.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { HoppModule } from "."
|
||||
import NProgress from "nprogress"
|
||||
|
||||
let deferedProgressHandle: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/**
|
||||
* Starts animating the global progress bar
|
||||
* @param deferTime How much time to defer the global progress bar rendering to
|
||||
*/
|
||||
export const startPageProgress = (deferTime?: number) => {
|
||||
if (deferedProgressHandle) clearTimeout(deferedProgressHandle)
|
||||
|
||||
// If deferTime is specified, queue it
|
||||
if (deferTime !== undefined) {
|
||||
deferedProgressHandle = setTimeout(() => {
|
||||
NProgress.start()
|
||||
}, deferTime)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
NProgress.start()
|
||||
}
|
||||
|
||||
export const completePageProgress = () => {
|
||||
if (deferedProgressHandle) {
|
||||
clearTimeout(deferedProgressHandle)
|
||||
deferedProgressHandle = null
|
||||
}
|
||||
|
||||
NProgress.done()
|
||||
}
|
||||
|
||||
export const removePageProgress = () => {
|
||||
if (deferedProgressHandle) {
|
||||
clearTimeout(deferedProgressHandle)
|
||||
deferedProgressHandle = null
|
||||
}
|
||||
|
||||
NProgress.remove()
|
||||
}
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit() {
|
||||
NProgress.configure({ showSpinner: false })
|
||||
},
|
||||
onBeforeRouteChange(to, from) {
|
||||
// Show progressbar on page change
|
||||
if (to.path !== from.path) {
|
||||
startPageProgress(500)
|
||||
}
|
||||
},
|
||||
onAfterRouteChange() {
|
||||
completePageProgress()
|
||||
},
|
||||
}
|
||||
78
packages/hoppscotch-common/src/modules/pwa.ts
Normal file
78
packages/hoppscotch-common/src/modules/pwa.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { HoppModule } from "."
|
||||
import { ref, onMounted } from "vue"
|
||||
import { usePwaPrompt } from "@composables/pwa"
|
||||
import { registerSW } from "virtual:pwa-register"
|
||||
|
||||
export type HoppPWARegistrationStatus =
|
||||
| { status: "NOT_INSTALLED" }
|
||||
| { status: "INSTALLED"; registration: ServiceWorkerRegistration | undefined }
|
||||
| { status: "INSTALL_FAILED"; error: any }
|
||||
|
||||
export const pwaNeedsRefresh = ref(false)
|
||||
export const pwaReadyForOffline = ref(false)
|
||||
export const pwaDefferedPrompt = ref<Event | null>(null)
|
||||
export const pwaRegistered = ref<HoppPWARegistrationStatus>({
|
||||
status: "NOT_INSTALLED",
|
||||
})
|
||||
|
||||
let updateApp: (reloadPage?: boolean) => Promise<void> | undefined
|
||||
|
||||
export const refreshAppForPWAUpdate = async () => {
|
||||
await updateApp?.(true)
|
||||
}
|
||||
|
||||
export const installPWA = async () => {
|
||||
if (pwaDefferedPrompt.value) {
|
||||
;(pwaDefferedPrompt.value as any).prompt()
|
||||
const { outcome }: { outcome: string } = await (
|
||||
pwaDefferedPrompt.value as any
|
||||
).userChoice
|
||||
|
||||
if (outcome === "accepted") {
|
||||
console.info("Hoppscotch was installed successfully.")
|
||||
} else {
|
||||
console.info(
|
||||
"Hoppscotch could not be installed. (Installation rejected by user.)"
|
||||
)
|
||||
}
|
||||
|
||||
pwaDefferedPrompt.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Update install prompt stuff
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit() {
|
||||
window.addEventListener("beforeinstallprompt", (event) => {
|
||||
pwaDefferedPrompt.value = event
|
||||
})
|
||||
|
||||
updateApp = registerSW({
|
||||
immediate: true,
|
||||
onNeedRefresh() {
|
||||
pwaNeedsRefresh.value = true
|
||||
},
|
||||
onOfflineReady() {
|
||||
pwaReadyForOffline.value = true
|
||||
},
|
||||
onRegistered(registration) {
|
||||
pwaRegistered.value = {
|
||||
status: "INSTALLED",
|
||||
registration,
|
||||
}
|
||||
},
|
||||
onRegisterError(error) {
|
||||
pwaRegistered.value = {
|
||||
status: "INSTALL_FAILED",
|
||||
error,
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
onRootSetup() {
|
||||
onMounted(() => {
|
||||
usePwaPrompt()
|
||||
})
|
||||
},
|
||||
}
|
||||
75
packages/hoppscotch-common/src/modules/router.ts
Normal file
75
packages/hoppscotch-common/src/modules/router.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { HoppModule, HOPP_MODULES } from "."
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
RouteLocationNormalized,
|
||||
} from "vue-router"
|
||||
import { setupLayouts } from "virtual:generated-layouts"
|
||||
import generatedRoutes from "virtual:generated-pages"
|
||||
import { logPageView } from "~/helpers/fb/analytics"
|
||||
import { readonly, ref } from "vue"
|
||||
|
||||
const routes = setupLayouts(generatedRoutes)
|
||||
|
||||
/**
|
||||
* A reactive value signifying whether we are currently navigating
|
||||
* into the first route the application is routing into.
|
||||
* Useful, if you want to do stuff for the initial page load (for example splash screens!)
|
||||
*/
|
||||
const _isLoadingInitialRoute = ref(false)
|
||||
|
||||
/**
|
||||
* Says whether a given route looks like an initial route which
|
||||
* is loaded as the first route.
|
||||
*
|
||||
* NOTE: This function assumes Vue Router represents that initial route
|
||||
* in the way we expect (fullPath == "/" and name == undefined). If this
|
||||
* function breaks later on, most probs vue-router updated its semantics
|
||||
* and we have to correct this function.
|
||||
*/
|
||||
function isInitialRoute(route: RouteLocationNormalized) {
|
||||
return route.fullPath === "/" && route.name === undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* A reactive value signifying whether we are currently navigating
|
||||
* into the first route the application is routing into.
|
||||
* Useful, if you want to do stuff for the initial page load (for example splash screens!)
|
||||
*
|
||||
* NOTE: This reactive value is READONLY
|
||||
*/
|
||||
export const isLoadingInitialRoute = readonly(_isLoadingInitialRoute)
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
_isLoadingInitialRoute.value = isInitialRoute(from)
|
||||
|
||||
HOPP_MODULES.forEach((mod) => {
|
||||
mod.onBeforeRouteChange?.(to, from, router)
|
||||
})
|
||||
})
|
||||
|
||||
// Instead of this a better architecture is for the router
|
||||
// module to expose a stream of router events that can be independently
|
||||
// subbed to
|
||||
router.afterEach((to) => {
|
||||
logPageView(to.fullPath)
|
||||
|
||||
_isLoadingInitialRoute.value = false
|
||||
|
||||
HOPP_MODULES.forEach((mod) => {
|
||||
mod.onAfterRouteChange?.(to, router)
|
||||
})
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
|
||||
HOPP_MODULES.forEach((mod) => mod.onRouterInit?.(app, router))
|
||||
},
|
||||
}
|
||||
198
packages/hoppscotch-common/src/modules/sentry.ts
Normal file
198
packages/hoppscotch-common/src/modules/sentry.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { HoppModule } from "."
|
||||
import * as Sentry from "@sentry/vue"
|
||||
import { BrowserTracing } from "@sentry/tracing"
|
||||
import { Route } from "@sentry/vue/types/router"
|
||||
import { RouteLocationNormalized, Router } from "vue-router"
|
||||
import { settingsStore } from "~/newstore/settings"
|
||||
import { App } from "vue"
|
||||
import { APP_IS_IN_DEV_MODE } from "~/helpers/dev"
|
||||
import { gqlClientError$ } from "~/helpers/backend/GQLClient"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
|
||||
/**
|
||||
* The tag names we allow giving to Sentry
|
||||
*/
|
||||
type SentryTag = "BACKEND_OPERATIONS"
|
||||
|
||||
interface SentryVueRouter {
|
||||
onError: (fn: (err: Error) => void) => void
|
||||
beforeEach: (fn: (to: Route, from: Route, next: () => void) => void) => void
|
||||
}
|
||||
|
||||
function normalizedRouteToSentryRoute(route: RouteLocationNormalized): Route {
|
||||
return {
|
||||
matched: route.matched,
|
||||
// route.params' type translates just to a fancy version of this, hence assertion
|
||||
params: route.params as Route["params"],
|
||||
path: route.path,
|
||||
// route.query's type translates just to a fancy version of this, hence assertion
|
||||
query: route.query as Route["query"],
|
||||
name: route.name,
|
||||
}
|
||||
}
|
||||
|
||||
function getInstrumentationVueRouter(router: Router): SentryVueRouter {
|
||||
return <SentryVueRouter>{
|
||||
onError: router.onError,
|
||||
beforeEach(func) {
|
||||
router.beforeEach((to, from, next) => {
|
||||
func(
|
||||
normalizedRouteToSentryRoute(to),
|
||||
normalizedRouteToSentryRoute(from),
|
||||
next
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let sentryActive = false
|
||||
|
||||
function initSentry(dsn: string, router: Router, app: App) {
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn,
|
||||
release: import.meta.env.VITE_SENTRY_RELEASE_TAG ?? undefined,
|
||||
environment: APP_IS_IN_DEV_MODE
|
||||
? "dev"
|
||||
: import.meta.env.VITE_SENTRY_ENVIRONMENT,
|
||||
integrations: [
|
||||
new BrowserTracing({
|
||||
routingInstrumentation: Sentry.vueRouterInstrumentation(
|
||||
getInstrumentationVueRouter(router)
|
||||
),
|
||||
// TODO: We may want to limit this later on
|
||||
tracingOrigins: [new URL(import.meta.env.VITE_BACKEND_GQL_URL).origin],
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 0.8,
|
||||
})
|
||||
sentryActive = true
|
||||
}
|
||||
|
||||
function deinitSentry() {
|
||||
Sentry.close()
|
||||
sentryActive = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a set of related errors to Sentry
|
||||
* @param errs The errors to report
|
||||
* @param tag The tag for the errord
|
||||
* @param extraTags Additional tag data to add
|
||||
* @param extras Extra information to attach
|
||||
*/
|
||||
function reportErrors(
|
||||
errs: Error[],
|
||||
tag: SentryTag,
|
||||
extraTags: Record<string, string | number | boolean> | null = null,
|
||||
extras: any = undefined
|
||||
) {
|
||||
if (sentryActive) {
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setTag("tag", tag)
|
||||
if (extraTags) {
|
||||
Object.entries(extraTags).forEach(([key, value]) => {
|
||||
scope.setTag(key, value)
|
||||
})
|
||||
}
|
||||
if (extras !== null && extras === undefined) scope.setExtras(extras)
|
||||
|
||||
scope.addAttachment({
|
||||
filename: "extras-dump.json",
|
||||
data: JSON.stringify(extras),
|
||||
contentType: "application/json",
|
||||
})
|
||||
|
||||
errs.forEach((err) => Sentry.captureException(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a specific error to Sentry
|
||||
* @param err The error to report
|
||||
* @param tag The tag for the error
|
||||
* @param extraTags Additional tag data to add
|
||||
* @param extras Extra information to attach
|
||||
*/
|
||||
function reportError(
|
||||
err: Error,
|
||||
tag: SentryTag,
|
||||
extraTags: Record<string, string | number | boolean> | null = null,
|
||||
extras: any = undefined
|
||||
) {
|
||||
reportErrors([err], tag, extraTags, extras)
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to events occuring in various subsystems in the app
|
||||
* for personalized error reporting
|
||||
*/
|
||||
function subscribeToAppEventsForReporting() {
|
||||
gqlClientError$.subscribe((ev) => {
|
||||
switch (ev.type) {
|
||||
case "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT":
|
||||
reportErrors(ev.errors, "BACKEND_OPERATIONS", { from: ev.type })
|
||||
break
|
||||
|
||||
case "CLIENT_REPORTED_ERROR":
|
||||
reportError(
|
||||
ev.error,
|
||||
"BACKEND_OPERATIONS",
|
||||
{ from: ev.type },
|
||||
{ op: ev.op }
|
||||
)
|
||||
break
|
||||
|
||||
case "GQL_CLIENT_REPORTED_ERROR":
|
||||
reportError(
|
||||
new Error("Backend Query Failed"),
|
||||
"BACKEND_OPERATIONS",
|
||||
{ opType: ev.opType },
|
||||
{
|
||||
opResult: ev.opResult,
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to app system events for adding
|
||||
* additional data tags for the error reporting
|
||||
*/
|
||||
function subscribeForAppDataTags() {
|
||||
currentUser$.subscribe((user) => {
|
||||
if (sentryActive) {
|
||||
Sentry.setTag("user_logged_in", !!user)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default <HoppModule>{
|
||||
onRouterInit(app, router) {
|
||||
if (!import.meta.env.VITE_SENTRY_DSN) {
|
||||
console.log(
|
||||
"Sentry tracing is not enabled because 'VITE_SENTRY_DSN' env is not defined"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (settingsStore.value.TELEMETRY_ENABLED) {
|
||||
initSentry(import.meta.env.VITE_SENTRY_DSN, router, app)
|
||||
}
|
||||
|
||||
settingsStore.subject$.subscribe(({ TELEMETRY_ENABLED }) => {
|
||||
if (!TELEMETRY_ENABLED && sentryActive) {
|
||||
deinitSentry()
|
||||
} else if (TELEMETRY_ENABLED && !sentryActive) {
|
||||
initSentry(import.meta.env.VITE_SENTRY_DSN!, router, app)
|
||||
}
|
||||
})
|
||||
|
||||
subscribeToAppEventsForReporting()
|
||||
subscribeForAppDataTags()
|
||||
},
|
||||
}
|
||||
90
packages/hoppscotch-common/src/modules/theming.ts
Normal file
90
packages/hoppscotch-common/src/modules/theming.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { usePreferredDark, useStorage } from "@vueuse/core"
|
||||
import { App, computed, reactive, Ref, watch } from "vue"
|
||||
import type { HoppBgColor } from "~/newstore/settings"
|
||||
import { useSettingStatic } from "@composables/settings"
|
||||
import { HoppModule } from "."
|
||||
import { hoppLocalConfigStorage } from "~/newstore/localpersistence"
|
||||
|
||||
export type HoppColorMode = {
|
||||
preference: HoppBgColor
|
||||
value: Readonly<Exclude<HoppBgColor, "system">>
|
||||
}
|
||||
|
||||
const applyColorMode = (app: App) => {
|
||||
const [settingPref] = useSettingStatic("BG_COLOR")
|
||||
|
||||
const currentLocalPreference = useStorage<HoppBgColor>(
|
||||
"nuxt-color-mode",
|
||||
"system",
|
||||
hoppLocalConfigStorage,
|
||||
{
|
||||
listenToStorageChanges: true,
|
||||
}
|
||||
)
|
||||
|
||||
const systemPrefersDark = usePreferredDark()
|
||||
|
||||
const selection = computed<Exclude<HoppBgColor, "system">>(() => {
|
||||
if (currentLocalPreference.value === "system") {
|
||||
return systemPrefersDark.value ? "dark" : "light"
|
||||
} else return currentLocalPreference.value
|
||||
})
|
||||
|
||||
watch(
|
||||
selection,
|
||||
(newSelection) => {
|
||||
document.documentElement.setAttribute("class", newSelection)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
settingPref,
|
||||
(newPref) => {
|
||||
currentLocalPreference.value = newPref
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const exposed: HoppColorMode = reactive({
|
||||
preference: currentLocalPreference,
|
||||
// Marking as readonly to not allow writes to this ref
|
||||
value: selection as Readonly<Ref<Exclude<HoppBgColor, "system">>>,
|
||||
})
|
||||
|
||||
app.provide("colorMode", exposed)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const applyAccentColor = (_app: App) => {
|
||||
const [pref] = useSettingStatic("THEME_COLOR")
|
||||
|
||||
watch(
|
||||
pref,
|
||||
(newPref) => {
|
||||
document.documentElement.setAttribute("data-accent", newPref)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const applyFontSize = (_app: App) => {
|
||||
const [pref] = useSettingStatic("FONT_SIZE")
|
||||
|
||||
watch(
|
||||
pref,
|
||||
(newPref) => {
|
||||
document.documentElement.setAttribute("data-font-size", newPref)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
applyColorMode(app)
|
||||
applyAccentColor(app)
|
||||
applyFontSize(app)
|
||||
},
|
||||
}
|
||||
32
packages/hoppscotch-common/src/modules/tippy.ts
Normal file
32
packages/hoppscotch-common/src/modules/tippy.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { HoppModule } from "."
|
||||
import VueTippy, { roundArrow, setDefaultProps } from "vue-tippy"
|
||||
|
||||
import "tippy.js/dist/tippy.css"
|
||||
import "tippy.js/animations/scale-subtle.css"
|
||||
import "tippy.js/dist/border.css"
|
||||
import "tippy.js/dist/svg-arrow.css"
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
app.use(VueTippy)
|
||||
|
||||
setDefaultProps({
|
||||
animation: "scale-subtle",
|
||||
appendTo: document.body,
|
||||
allowHTML: false,
|
||||
animateFill: false,
|
||||
arrow: roundArrow + roundArrow,
|
||||
popperOptions: {
|
||||
// https://popper.js.org/docs/v2/utils/detect-overflow/
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
rootBoundary: "document",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
19
packages/hoppscotch-common/src/modules/toast.ts
Normal file
19
packages/hoppscotch-common/src/modules/toast.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Toasted from "@hoppscotch/vue-toasted"
|
||||
import type { ToastOptions } from "@hoppscotch/vue-toasted"
|
||||
import { HoppModule } from "."
|
||||
|
||||
import "@hoppscotch/vue-toasted/style.css"
|
||||
|
||||
// We are using a fork of Vue Toasted (github.com/clayzar/vue-toasted) which is a bit of
|
||||
// an untrusted fork, we will either want to make our own fork or move to a more stable one
|
||||
// The original Vue Toasted doesn't support Vue 3 and the OP has been irresponsive.
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
app.use(Toasted, <ToastOptions>{
|
||||
position: "bottom-center",
|
||||
duration: 3000,
|
||||
keepOnHover: true,
|
||||
})
|
||||
},
|
||||
}
|
||||
15
packages/hoppscotch-common/src/modules/v-focus.ts
Normal file
15
packages/hoppscotch-common/src/modules/v-focus.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { nextTick } from "vue"
|
||||
import { HoppModule } from "."
|
||||
|
||||
/*
|
||||
Declares a `v-focus` directive that can be used for components
|
||||
to acquire focus instantly once mounted
|
||||
*/
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
app.directive("focus", {
|
||||
mounted: (el) => nextTick(() => el.focus()),
|
||||
})
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user