Revamp of the Settings State System along with TypeScript support (#1560)

* Add vue-rx, rxjs and lodash as dependencies

* Added vue-rx plugin integration to nuxt config

* Initial settings store implementation

* Add babel plugin for private class properties to for Jest

* Add DispatchingStore test spec

* Initial settings code

* Reactive Streams for fb current user and id token

* Fix typo

* Migrate index and graphql pages to the new store

* Migrate network strategy to the new store

* Fixed Section.vue errors

* Fix getSettingSubject issue

* Migrate fb settings reference in components to the new state system

* Add typings for lodash as dev dependency

* Load setting

* Load initial sync setting values

* Update proxy url

* Add typescript support

* Rewrite Settings store to TypeScript

* Port Settings page to TypeScript as reference

* Move all store migrations to a separate file

* Delete test file for fb.js

* Add ts-jest as dev dependency

* Remove firebase-mock as dependency

* Remove FRAME_COLORS_ENABLED settings value
This commit is contained in:
Andrew Bastin
2021-03-23 11:18:14 -04:00
committed by GitHub
parent 64f64b9e31
commit 5fce1118f6
47 changed files with 32426 additions and 9143 deletions

View File

@@ -0,0 +1,56 @@
import { Subject, BehaviorSubject } from "rxjs"
import { map } from "rxjs/operators"
import assign from "lodash/assign"
import clone from "lodash/clone"
type Dispatch<StoreType, DispatchersType extends Dispatchers<StoreType>, K extends keyof DispatchersType> = {
dispatcher: K & string,
payload: any
}
export type Dispatchers<StoreType> = {
[ key: string ]: (currentVal: StoreType, payload: any) => Partial<StoreType>
}
export default class DispatchingStore<StoreType, DispatchersType extends Dispatchers<StoreType>> {
#state$: BehaviorSubject<StoreType>
#dispatchers: Dispatchers<StoreType>
#dispatches$: Subject<Dispatch<StoreType, DispatchersType, keyof DispatchersType>> = new Subject()
constructor(initialValue: StoreType, dispatchers: DispatchersType) {
this.#state$ = new BehaviorSubject(initialValue)
this.#dispatchers = dispatchers
this.#dispatches$
.pipe(
map(
({ dispatcher, payload }) => this.#dispatchers[dispatcher](this.value, payload)
)
).subscribe(val => {
const data = clone(this.value)
assign(data, val)
this.#state$.next(data)
})
}
get subject$() {
return this.#state$
}
get value() {
return this.subject$.value
}
get dispatches$() {
return this.#dispatches$
}
dispatch({ dispatcher, payload }: Dispatch<StoreType, DispatchersType, keyof DispatchersType>) {
if (!this.#dispatchers[dispatcher]) throw new Error(`Undefined dispatch type '${dispatcher}'`)
this.#dispatches$.next({ dispatcher, payload })
}
}

View File

@@ -0,0 +1,185 @@
import { BehaviorSubject, Subject } from "rxjs"
import isEqual from "lodash/isEqual"
import DispatchingStore from "~/newstore/DispatchingStore"
describe("DispatchingStore", () => {
test("'subject$' property properly returns an BehaviorSubject", () => {
const store = new DispatchingStore({}, {})
expect(store.subject$ instanceof BehaviorSubject).toEqual(true)
})
test("'value' property properly returns the current state value", () => {
const store = new DispatchingStore({}, {})
expect(store.value).toEqual({})
})
test("'dispatches$' property properly returns a Subject", () => {
const store = new DispatchingStore({}, {})
expect(store.dispatches$ instanceof Subject).toEqual(true)
})
test("dispatch with invalid dispatcher are thrown", () => {
const store = new DispatchingStore({}, {})
expect(() => {
store.dispatch({
dispatcher: "non-existent",
payload: {}
})
}).toThrow()
})
test("valid dispatcher calls run without throwing", () => {
const store = new DispatchingStore({}, {
testDispatcher(_currentValue, _payload) {
// Nothing here
}
})
expect(() => {
store.dispatch({
dispatcher: "testDispatcher",
payload: {}
})
}).not.toThrow()
})
test("only correct dispatcher method is ran", () => {
const dispatchFn = jest.fn().mockReturnValue({})
const dontCallDispatchFn = jest.fn().mockReturnValue({})
const store = new DispatchingStore({}, {
testDispatcher: dispatchFn,
dontCallDispatcher: dontCallDispatchFn
})
store.dispatch({
dispatcher: "testDispatcher",
payload: {}
})
expect(dispatchFn).toHaveBeenCalledTimes(1)
expect(dontCallDispatchFn).not.toHaveBeenCalled()
})
test("passes current value and the payload to the dispatcher", () => {
const testInitValue = { name: "bob" }
const testPayload = { name: "alice" }
const testDispatchFn = jest.fn().mockReturnValue({})
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn
})
store.dispatch({
dispatcher: "testDispatcher",
payload: testPayload
})
expect(testDispatchFn).toHaveBeenCalledWith(testInitValue, testPayload)
})
test("dispatcher returns are used to update the store correctly", () => {
const testInitValue = { name: "bob" }
const testDispatchReturnVal = { name: "alice" }
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn
})
store.dispatch({
dispatcher: "testDispatcher",
payload: {} // Payload doesn't matter because the function is mocked
})
expect(store.value).toEqual(testDispatchReturnVal)
})
test("dispatching patches in new values if not existing on the store", () => {
const testInitValue = { name: "bob" }
const testDispatchReturnVal = { age: 25 }
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn
})
store.dispatch({
dispatcher: "testDispatcher",
payload: {}
})
expect(store.value).toEqual({
name: "bob",
age: 25
})
})
test("emits the current store value to the new subscribers", done => {
const testInitValue = { name: "bob" }
const testDispatchFn = jest.fn().mockReturnValue({})
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn
})
store.subject$.subscribe(value => {
if (value === testInitValue) {
done()
}
})
})
test("emits the dispatched store value to the subscribers", done => {
const testInitValue = { name: "bob" }
const testDispatchReturnVal = { age: 25 }
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn
})
store.subject$.subscribe(value => {
if (isEqual(value, { name: "bob", age: 25 })) {
done()
}
})
store.dispatch({
dispatcher: "testDispatcher",
payload: {}
})
})
test("dispatching emits the new dispatch requests to the subscribers", () => {
const testInitValue = { name: "bob" }
const testPayload = { age: 25 }
const testDispatchFn = jest.fn().mockReturnValue({})
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn
})
store.dispatches$.subscribe(value => {
if (isEqual(value, { dispatcher: "testDispatcher", payload: testPayload })) {
done()
}
})
store.dispatch({
dispatcher: "testDispatcher",
payload: {}
})
})
})

View File

@@ -0,0 +1,36 @@
import { settingsStore, bulkApplySettings, defaultSettings } from "./settings"
import clone from "lodash/clone"
import assign from "lodash/assign"
function checkAndMigrateOldSettings() {
// Don't do migration if the new settings object exists
if (window.localStorage.getItem("settings")) return
const vuexData = JSON.parse(window.localStorage.getItem("vuex") || "{}")
if (vuexData === {}) return
const settingsData = clone(defaultSettings)
assign(settingsData, vuexData.postwoman.settings)
window.localStorage.setItem("settings", JSON.stringify(settingsData))
}
function setupSettingsPersistence() {
const settingsData = JSON.parse(window.localStorage.getItem("settings") || "{}")
if (settingsData) {
bulkApplySettings(settingsData)
}
settingsStore.subject$
.subscribe(settings => {
window.localStorage.setItem("settings", JSON.stringify(settings))
})
}
export function setupLocalPersistence() {
checkAndMigrateOldSettings()
setupSettingsPersistence()
}

91
newstore/settings.ts Normal file
View File

@@ -0,0 +1,91 @@
import { pluck, distinctUntilChanged } from "rxjs/operators"
import has from "lodash/has"
import DispatchingStore from "./DispatchingStore"
import type { Dispatchers } from "./DispatchingStore"
import { Observable } from "rxjs"
import type { KeysMatching } from "~/types/ts-utils"
export const defaultSettings = {
syncCollections: true,
syncHistory: true,
syncEnvironments: true,
SCROLL_INTO_ENABLED: true,
PROXY_ENABLED: false,
PROXY_URL: "https://proxy.hoppscotch.io/",
PROXY_KEY: "",
EXTENSIONS_ENABLED: true,
EXPERIMENTAL_URL_BAR_ENABLED: false,
URL_EXCLUDES: {
auth: true,
httpUser: true,
httpPassword: true,
bearerToken: true
}
}
export type SettingsType = typeof defaultSettings
const validKeys = Object.keys(defaultSettings)
const dispatchers: Dispatchers<SettingsType> = {
bulkApplySettings(_currentState, payload: Partial<SettingsType>) {
return payload
},
toggleSetting(currentState, { settingKey }: { settingKey: KeysMatching<SettingsType, boolean> }) {
if (!has(currentState, settingKey)) {
console.log(`Toggling of a non-existent setting key '${settingKey}' ignored.`)
return {}
}
const result: Partial<SettingsType> = {}
result[settingKey] = !currentState[settingKey]
return result
},
applySetting<K extends keyof SettingsType>(_currentState: SettingsType, { settingKey, value }: { settingKey: K, value: SettingsType[K] }) {
if (!validKeys.includes(settingKey)) {
console.log(`Ignoring non-existent setting key '${settingKey}' assignment`)
return {}
}
const result: Partial<SettingsType> = {}
result[settingKey] = value
return result
}
}
export const settingsStore = new DispatchingStore(defaultSettings, dispatchers)
export function getSettingSubject<K extends keyof SettingsType>(settingKey: K): Observable<SettingsType[K]> {
return settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged())
}
export function bulkApplySettings(settingsObj: Partial<SettingsType>) {
settingsStore.dispatch({
dispatcher: "bulkApplySettings",
payload: settingsObj
})
}
export function toggleSetting(settingKey: KeysMatching<SettingsType, boolean>) {
settingsStore.dispatch({
dispatcher: "toggleSetting",
payload: {
settingKey
}
})
}
export function applySetting<K extends keyof SettingsType>(settingKey: K, value: SettingsType[K]) {
settingsStore.dispatch({
dispatcher: "applySetting",
payload: {
settingKey,
value
}
})
}