import { pluck, distinctUntilChanged } from "rxjs/operators" import { cloneDeep, defaultsDeep, has } from "lodash-es" import { Observable } from "rxjs" import DispatchingStore, { defineDispatchers } from "./DispatchingStore" import type { KeysMatching } from "~/types/ts-utils" export const HoppBgColors = ["system", "light", "dark", "black"] as const export type HoppBgColor = (typeof HoppBgColors)[number] export const HoppAccentColors = [ "green", "teal", "blue", "indigo", "purple", "yellow", "orange", "red", "pink", ] as const export type HoppAccentColor = (typeof HoppAccentColors)[number] export type SettingsDef = { syncCollections: boolean syncHistory: boolean syncEnvironments: boolean PROXY_URL: string WRAP_LINES: { httpRequestBody: boolean httpResponseBody: boolean httpHeaders: boolean httpParams: boolean httpUrlEncoded: boolean httpPreRequest: boolean httpTest: boolean httpRequestVariables: boolean graphqlQuery: boolean graphqlResponseBody: boolean graphqlHeaders: boolean graphqlVariables: boolean graphqlSchema: boolean importCurl: boolean codeGen: boolean cookie: boolean } CURRENT_INTERCEPTOR_ID: string URL_EXCLUDES: { auth: boolean httpUser: boolean httpPassword: boolean bearerToken: boolean oauth2Token: boolean } THEME_COLOR: HoppAccentColor BG_COLOR: HoppBgColor TELEMETRY_ENABLED: boolean EXPAND_NAVIGATION: boolean SIDEBAR: boolean SIDEBAR_ON_LEFT: boolean COLUMN_LAYOUT: boolean } export const getDefaultSettings = (): SettingsDef => ({ syncCollections: true, syncHistory: true, syncEnvironments: true, WRAP_LINES: { httpRequestBody: true, httpResponseBody: true, httpHeaders: true, httpParams: true, httpUrlEncoded: true, httpPreRequest: true, httpTest: true, httpRequestVariables: true, graphqlQuery: true, graphqlResponseBody: true, graphqlHeaders: false, graphqlVariables: false, graphqlSchema: true, importCurl: true, codeGen: true, cookie: true, }, CURRENT_INTERCEPTOR_ID: "browser", // TODO: Allow the platform definition to take this place // TODO: Interceptor related settings should move under the interceptor systems PROXY_URL: "https://proxy.hoppscotch.io/", URL_EXCLUDES: { auth: true, httpUser: true, httpPassword: true, bearerToken: true, oauth2Token: true, }, THEME_COLOR: "indigo", BG_COLOR: "system", TELEMETRY_ENABLED: true, EXPAND_NAVIGATION: false, SIDEBAR: true, SIDEBAR_ON_LEFT: false, COLUMN_LAYOUT: true, }) type ApplySettingPayload = { [K in keyof SettingsDef]: { settingKey: K value: SettingsDef[K] } }[keyof SettingsDef] type ApplyNestedSettingPayload = { [K in KeysMatching>]: { [P in keyof SettingsDef[K]]: { settingKey: K property: P value: SettingsDef[K][P] } }[keyof SettingsDef[K]] }[KeysMatching>] const dispatchers = defineDispatchers({ bulkApplySettings(_currentState: SettingsDef, payload: Partial) { return payload }, toggleSetting( currentState: SettingsDef, { settingKey }: { settingKey: KeysMatching } ) { if (!has(currentState, settingKey)) { // console.log( // `Toggling of a non-existent setting key '${settingKey}' ignored` // ) return {} } const result: Partial = {} result[settingKey] = !currentState[settingKey] return result }, toggleNestedSetting( currentState: SettingsDef, { settingKey, property, }: { settingKey: KeysMatching> property: KeysMatching } ) { if (!has(currentState, [settingKey, property])) { return {} } const result: Partial = { [settingKey]: { ...currentState[settingKey], [property]: !currentState[settingKey][property], }, } return result }, applySetting( _currentState: SettingsDef, { settingKey, value }: ApplySettingPayload ) { const result: Partial = { [settingKey]: value, } return result }, applyNestedSetting( _currentState: SettingsDef, { settingKey, property, value }: ApplyNestedSettingPayload ) { const result: Partial = { [settingKey]: { [property]: value, }, } return result }, }) export const settingsStore = new DispatchingStore( getDefaultSettings(), dispatchers ) /** * An observable value to make avail all the state information at once */ export const settings$ = settingsStore.subject$.asObservable() export function getSettingSubject( settingKey: K ): Observable { return settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged()) } export function bulkApplySettings(settingsObj: Partial) { settingsStore.dispatch({ dispatcher: "bulkApplySettings", payload: settingsObj, }) } export function toggleSetting(settingKey: KeysMatching) { settingsStore.dispatch({ dispatcher: "toggleSetting", payload: { settingKey, }, }) } export function toggleNestedSetting< K extends KeysMatching>, P extends keyof SettingsDef[K], >(settingKey: K, property: P) { settingsStore.dispatch({ dispatcher: "toggleNestedSetting", payload: { settingKey, // @ts-expect-error TS is not able to understand the type semantics here property, }, }) } export function applySetting( settingKey: K, value: SettingsDef[K] ) { settingsStore.dispatch({ dispatcher: "applySetting", payload: { // @ts-expect-error TS is not able to understand the type semantics here settingKey, // @ts-expect-error TS is not able to understand the type semantics here value, }, }) } export function applyNestedSetting< K extends KeysMatching>, P extends keyof SettingsDef[K], R extends SettingsDef[K][P], >(settingKey: K, property: P, value: R) { settingsStore.dispatch({ dispatcher: "applyNestedSetting", payload: { settingKey, // @ts-expect-error TS is not able to understand the type semantics here property, value, }, }) } export function performSettingsDataMigrations(data: any): SettingsDef { const source = cloneDeep(data) if (source["EXTENSIONS_ENABLED"]) { const result = JSON.parse(source["EXTENSIONS_ENABLED"]) if (result) source["CURRENT_INTERCEPTOR_ID"] = "extension" delete source["EXTENSIONS_ENABLED"] } if (source["PROXY_ENABLED"]) { const result = JSON.parse(source["PROXY_ENABLED"]) if (result) source["CURRENT_INTERCEPTOR_ID"] = "proxy" delete source["PROXY_ENABLED"] } const final = defaultsDeep(source, getDefaultSettings()) return final }