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

@@ -5,6 +5,9 @@ function isBabelLoader(caller) {
module.exports = function (api) {
if (api.env("test") && !api.caller(isBabelLoader)) {
return {
plugins: [
"@babel/plugin-proposal-class-properties"
],
presets: [
[
"@babel/preset-env",

View File

@@ -34,14 +34,15 @@ fieldset {
}
</style>
<script>
export default {
<script lang="ts">
import Vue from "vue"
export default Vue.extend({
computed: {
sectionString() {
sectionString(): string {
return `${this.$route.path.replace(/\/+$/, "")}/${this.label}`
},
},
props: {
label: {
type: String,
@@ -58,9 +59,9 @@ export default {
// Save collapsed section into the collapsedSections array
this.$store.commit("setCollapsedSection", this.sectionString)
},
isCollapsed(label) {
isCollapsed(_label: string) {
return this.$store.state.theme.collapsedSections.includes(this.sectionString) || false
},
},
}
})
</script>

View File

@@ -38,6 +38,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -48,16 +49,19 @@ export default {
name: undefined,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
addNewCollection() {
if (!this.$data.name) {

View File

@@ -114,6 +114,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -130,16 +131,19 @@ export default {
confirmRemove: false,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
toggleShowChildren() {
this.showChildren = !this.showChildren

View File

@@ -38,6 +38,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -50,16 +51,19 @@ export default {
name: undefined,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
saveCollection() {
if (!this.$data.name) {

View File

@@ -38,6 +38,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -51,16 +52,19 @@ export default {
name: undefined,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
editFolder() {
this.$store.commit("postwoman/editFolder", {

View File

@@ -38,6 +38,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -55,16 +56,19 @@ export default {
},
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
saveRequest() {
const requestUpdated = {

View File

@@ -111,6 +111,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
name: "folder",
@@ -129,16 +130,19 @@ export default {
confirmRemove: false,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
toggleShowChildren() {
this.showChildren = !this.showChildren

View File

@@ -113,6 +113,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
data() {
@@ -121,6 +122,9 @@ export default {
showJsonCode: false,
}
},
subscriptions() {
SYNC_COLLECTIONS: getSettingSubject("syncCollections")
},
props: {
show: Boolean,
},
@@ -264,14 +268,12 @@ export default {
this.fileImported()
},
syncToFBCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
fileImported() {
this.$toast.info(this.$t("file_imported"), {

View File

@@ -61,6 +61,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -84,16 +85,19 @@ export default {
confirmRemove: false,
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
selectRequest() {
this.$store.commit("postwoman/selectRequest", { request: this.request })

View File

@@ -71,6 +71,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -88,6 +89,11 @@ export default {
},
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
watch: {
"requestData.collectionIndex": function resetFolderAndRequestIndex() {
// if user has chosen some folder, than selected other collection, which doesn't have any folders
@@ -144,14 +150,12 @@ export default {
},
methods: {
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
saveRequestAs() {
const userDidntSpecifyCollection = this.$data.requestData.collectionIndex === undefined

View File

@@ -87,6 +87,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -111,6 +112,11 @@ export default {
filterText: "",
}
},
subscriptions() {
return {
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
}
},
computed: {
collections() {
return fb.currentUser !== null
@@ -242,14 +248,12 @@ export default {
this.$data.editingRequestIndex = undefined
},
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
)
}
}
},
},
beforeDestroy() {

View File

@@ -38,6 +38,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -48,13 +49,16 @@ export default {
name: undefined,
}
},
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments")
}
},
methods: {
syncEnvironments() {
if (fb.currentUser !== null && fb.currentSettings[1]) {
if (fb.currentSettings[1].value) {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
}
}
},
addNewEnvironment() {
if (!this.$data.name) {

View File

@@ -102,6 +102,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -115,6 +116,11 @@ export default {
doneButton: '<i class="material-icons">done</i>',
}
},
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments")
}
},
watch: {
editingEnvironment(update) {
this.name =
@@ -135,11 +141,9 @@ export default {
},
methods: {
syncEnvironments() {
if (fb.currentUser !== null && fb.currentSettings[1]) {
if (fb.currentSettings[1].value) {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
}
}
},
clearContent({ target }) {
this.$store.commit("postwoman/removeVariables", [])

View File

@@ -38,6 +38,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
props: {
@@ -49,13 +50,16 @@ export default {
confirmRemove: false,
}
},
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments")
}
},
methods: {
syncEnvironments() {
if (fb.currentUser !== null && fb.currentSettings[1]) {
if (fb.currentSettings[1].value) {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
}
}
},
removeEnvironment() {
this.$store.commit("postwoman/removeEnvironment", this.environmentIndex)

View File

@@ -113,6 +113,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
data() {
@@ -121,6 +122,11 @@ export default {
showJsonCode: false,
}
},
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments")
}
},
props: {
show: Boolean,
},
@@ -256,11 +262,9 @@ export default {
this.fileImported()
},
syncToFBEnvironments() {
if (fb.currentUser !== null && fb.currentSettings[1]) {
if (fb.currentSettings[1].value) {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
}
}
},
fileImported() {
this.$toast.info(this.$t("file_imported"), {

View File

@@ -66,6 +66,7 @@
<script>
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
data() {
@@ -82,6 +83,11 @@ export default {
},
}
},
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments"),
}
},
computed: {
environments() {
return fb.currentUser !== null
@@ -153,11 +159,9 @@ export default {
this.$data.editingEnvironmentIndex = undefined
},
syncEnvironments() {
if (fb.currentUser !== null && fb.currentSettings[1]) {
if (fb.currentSettings[1].value) {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
}
}
},
},
beforeDestroy() {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
import { cancelRunningRequest, sendNetworkRequest } from "../network"
import AxiosStrategy, { cancelRunningAxiosRequest } from "../strategies/AxiosStrategy"
import ExtensionStrategy, {
cancelRunningExtensionRequest,
hasExtensionInstalled,
} from "../strategies/ExtensionStrategy"
jest.mock("../strategies/AxiosStrategy", () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
cancelRunningAxiosRequest: jest.fn(() => Promise.resolve()),
}))
jest.mock("../strategies/ExtensionStrategy", () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
cancelRunningExtensionRequest: jest.fn(() => Promise.resolve()),
hasExtensionInstalled: jest.fn(),
}))
jest.mock("~/newstore/settings", () => {
return {
settingsStore: {
value: {
EXTENSIONS_ENABLED: false
}
}
}
})
global.$nuxt = {
$loading: {
finish: jest.fn(() => Promise.resolve()),
},
}
beforeEach(() => {
jest.clearAllMocks() // Reset the call count for the mock functions
})
describe("cancelRunningRequest", () => {
test("cancels only axios request if extension not allowed in settings and extension is installed", () => {
hasExtensionInstalled.mockReturnValue(true)
cancelRunningRequest()
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
})
test("cancels only axios request if extension is not allowed and not installed", () => {
hasExtensionInstalled.mockReturnValue(false)
cancelRunningRequest()
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
})
})
describe("sendNetworkRequest", () => {
test("runs only axios request if extension not allowed in settings and extension is installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(true)
await sendNetworkRequest({})
expect(ExtensionStrategy).not.toHaveBeenCalled()
expect(AxiosStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
test("runs only axios request if extension is not allowed and not installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(false)
await sendNetworkRequest({})
expect(ExtensionStrategy).not.toHaveBeenCalled()
expect(AxiosStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,82 @@
import { cancelRunningRequest, sendNetworkRequest } from "../network"
import AxiosStrategy, { cancelRunningAxiosRequest } from "../strategies/AxiosStrategy"
import ExtensionStrategy, {
cancelRunningExtensionRequest,
hasExtensionInstalled,
} from "../strategies/ExtensionStrategy"
jest.mock("../strategies/AxiosStrategy", () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
cancelRunningAxiosRequest: jest.fn(() => Promise.resolve()),
}))
jest.mock("../strategies/ExtensionStrategy", () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
cancelRunningExtensionRequest: jest.fn(() => Promise.resolve()),
hasExtensionInstalled: jest.fn(),
}))
jest.mock("~/newstore/settings", () => {
return {
settingsStore: {
value: {
EXTENSIONS_ENABLED: true
}
}
}
})
global.$nuxt = {
$loading: {
finish: jest.fn(() => Promise.resolve()),
},
}
beforeEach(() => {
jest.clearAllMocks() // Reset the call count for the mock functions
})
describe("cancelRunningRequest", () => {
test("cancels only extension request if extension allowed in settings and is installed", () => {
hasExtensionInstalled.mockReturnValue(true)
cancelRunningRequest()
expect(cancelRunningAxiosRequest).not.toHaveBeenCalled()
expect(cancelRunningExtensionRequest).toHaveBeenCalled()
})
test("cancels only axios request if extension is allowed but not installed", () => {
hasExtensionInstalled.mockReturnValue(false)
cancelRunningRequest()
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
})
})
describe("sendNetworkRequest", () => {
test("runs only extension request if extension allowed in settings and is installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(true)
await sendNetworkRequest({})
expect(AxiosStrategy).not.toHaveBeenCalled()
expect(ExtensionStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
test("runs only axios request if extension is allowed but not installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(false)
await sendNetworkRequest({})
expect(ExtensionStrategy).not.toHaveBeenCalled()
expect(AxiosStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
})

View File

@@ -1,176 +0,0 @@
import { cancelRunningRequest, sendNetworkRequest } from "../network"
import AxiosStrategy, { cancelRunningAxiosRequest } from "../strategies/AxiosStrategy"
import ExtensionStrategy, {
cancelRunningExtensionRequest,
hasExtensionInstalled,
} from "../strategies/ExtensionStrategy"
jest.mock("../strategies/AxiosStrategy", () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
cancelRunningAxiosRequest: jest.fn(() => Promise.resolve()),
}))
jest.mock("../strategies/ExtensionStrategy", () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
cancelRunningExtensionRequest: jest.fn(() => Promise.resolve()),
hasExtensionInstalled: jest.fn(),
}))
const extensionAllowedStore = {
state: {
postwoman: {
settings: {
EXTENSIONS_ENABLED: true,
},
},
},
}
const extensionNotAllowedStore = {
state: {
postwoman: {
settings: {
EXTENSIONS_ENABLED: false,
},
},
},
}
const extensionUndefinedStore = {
state: {
postwoman: {
settings: {},
},
},
}
global.$nuxt = {
$loading: {
finish: jest.fn(() => Promise.resolve()),
},
}
beforeEach(() => {
jest.clearAllMocks() // Reset the call count for the mock functions
})
describe("cancelRunningRequest", () => {
test("cancels only extension request if extension allowed in settings and is installed", () => {
hasExtensionInstalled.mockReturnValue(true)
cancelRunningRequest(extensionAllowedStore)
expect(cancelRunningAxiosRequest).not.toHaveBeenCalled()
expect(cancelRunningExtensionRequest).toHaveBeenCalled()
})
test("cancels only extension request if extension setting is undefined and extension is installed", () => {
hasExtensionInstalled.mockReturnValue(true)
cancelRunningRequest(extensionUndefinedStore)
expect(cancelRunningAxiosRequest).not.toHaveBeenCalled()
expect(cancelRunningExtensionRequest).toHaveBeenCalled()
})
test("cancels only axios request if extension not allowed in settings and extension is installed", () => {
hasExtensionInstalled.mockReturnValue(true)
cancelRunningRequest(extensionNotAllowedStore)
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
})
test("cancels only axios request if extension is allowed but not installed", () => {
hasExtensionInstalled.mockReturnValue(false)
cancelRunningRequest(extensionAllowedStore)
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
})
test("cancels only axios request if extension is not allowed and not installed", () => {
hasExtensionInstalled.mockReturnValue(false)
cancelRunningRequest(extensionNotAllowedStore)
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
})
test("cancels only axios request if extension setting is undefined and not installed", () => {
hasExtensionInstalled.mockReturnValue(false)
cancelRunningRequest(extensionUndefinedStore)
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
})
})
describe("sendNetworkRequest", () => {
test("runs only extension request if extension allowed in settings and is installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(true)
await sendNetworkRequest({}, extensionAllowedStore)
expect(AxiosStrategy).not.toHaveBeenCalled()
expect(ExtensionStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
test("runs only extension request if extension setting is undefined and extension is installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(true)
await sendNetworkRequest({}, extensionUndefinedStore)
expect(AxiosStrategy).not.toHaveBeenCalled()
expect(ExtensionStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
test("runs only axios request if extension not allowed in settings and extension is installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(true)
await sendNetworkRequest({}, extensionNotAllowedStore)
expect(ExtensionStrategy).not.toHaveBeenCalled()
expect(AxiosStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
test("runs only axios request if extension is allowed but not installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(false)
await sendNetworkRequest({}, extensionAllowedStore)
expect(ExtensionStrategy).not.toHaveBeenCalled()
expect(AxiosStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
test("runs only axios request if extension is not allowed and not installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(false)
await sendNetworkRequest({}, extensionNotAllowedStore)
expect(ExtensionStrategy).not.toHaveBeenCalled()
expect(AxiosStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
test("runs only axios request if extension setting is undefined and not installed and clears the progress bar", async () => {
hasExtensionInstalled.mockReturnValue(false)
await sendNetworkRequest({}, extensionUndefinedStore)
expect(ExtensionStrategy).not.toHaveBeenCalled()
expect(AxiosStrategy).toHaveBeenCalled()
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
})
})

View File

@@ -1,6 +1,8 @@
import firebase from "firebase/app"
import "firebase/firestore"
import "firebase/auth"
import { ReplaySubject } from "rxjs"
import { getSettingSubject, applySetting } from "~/newstore/settings"
// Initialize Firebase, copied from cloud console
const firebaseConfig = {
@@ -38,6 +40,41 @@ export class FirebaseInstance {
this.currentGraphqlCollections = []
this.currentEnvironments = []
this.currentUser$ = new ReplaySubject(1)
this.idToken$ = new ReplaySubject(1)
let loadedSettings = false
getSettingSubject("syncCollections").subscribe((status) => {
if (this.currentUser && loadedSettings) {
this.writeSettings("syncCollections", status)
}
})
getSettingSubject("syncHistory").subscribe((status) => {
if (this.currentUser && loadedSettings) {
this.writeSettings("syncHistory", status)
}
})
getSettingSubject("syncEnvironments").subscribe((status) => {
if (this.currentUser && loadedSettings) {
this.writeSettings("syncEnvironments", status)
}
})
this.app.auth().onIdTokenChanged((user) => {
if (user) {
user.getIdToken().then((token) => {
this.idToken = token
this.idToken$.next(token)
})
} else {
this.idToken = null
this.idToken$.next(null)
}
})
this.app.auth().onAuthStateChanged((user) => {
if (user) {
this.currentUser = user
@@ -87,6 +124,14 @@ export class FirebaseInstance {
settings.push(setting)
})
this.currentSettings = settings
settings.forEach((e) => {
if (e && e.name && e.value != null) {
applySetting(e.name, e.value)
}
})
loadedSettings = true
})
this.usersCollection

17
helpers/migrations.ts Normal file
View File

@@ -0,0 +1,17 @@
import { settingsStore, applySetting } from "~/newstore/settings"
/*
* This file contains all the migrations we have to perform overtime in various (persisted)
* state/store entries
*/
export function performMigrations(): void {
// Migrate old default proxy URL to the new proxy URL (if not set / overridden)
if (
settingsStore.value.PROXY_URL === "https://hoppscotch.apollosoftware.xyz/"
) {
applySetting("PROXY_URL", "https://proxy.hoppscotch.io/")
}
}

View File

@@ -3,26 +3,25 @@ import ExtensionStrategy, {
cancelRunningExtensionRequest,
hasExtensionInstalled,
} from "./strategies/ExtensionStrategy"
import { settingsStore } from "~/newstore/settings"
export const cancelRunningRequest = (store) => {
if (isExtensionsAllowed(store) && hasExtensionInstalled()) {
export const cancelRunningRequest = () => {
if (isExtensionsAllowed() && hasExtensionInstalled()) {
cancelRunningExtensionRequest()
} else {
cancelRunningAxiosRequest()
}
}
const isExtensionsAllowed = ({ state }) =>
typeof state.postwoman.settings.EXTENSIONS_ENABLED === "undefined" ||
state.postwoman.settings.EXTENSIONS_ENABLED
const isExtensionsAllowed = () => settingsStore.value.EXTENSIONS_ENABLED
const runAppropriateStrategy = (req, store) => {
if (isExtensionsAllowed(store) && hasExtensionInstalled()) {
return ExtensionStrategy(req, store)
const runAppropriateStrategy = (req) => {
if (isExtensionsAllowed() && hasExtensionInstalled()) {
return ExtensionStrategy(req)
}
return AxiosStrategy(req, store)
return AxiosStrategy(req)
}
export const sendNetworkRequest = (req, store) =>
runAppropriateStrategy(req, store).finally(() => window.$nuxt.$loading.finish())
export const sendNetworkRequest = (req) =>
runAppropriateStrategy(req).finally(() => window.$nuxt.$loading.finish())

View File

@@ -1,5 +1,6 @@
import axios from "axios"
import { decodeB64StringToArrayBuffer } from "../utils/b64"
import { settingsStore } from "~/newstore/settings"
let cancelSource = axios.CancelToken.source()
@@ -10,10 +11,10 @@ export const cancelRunningAxiosRequest = () => {
cancelSource = axios.CancelToken.source()
}
const axiosWithProxy = async (req, { state }) => {
const axiosWithProxy = async (req) => {
try {
const { data } = await axios.post(
state.postwoman.settings.PROXY_URL || "https://proxy.hoppscotch.io",
settingsStore.value.PROXY_URL || "https://proxy.hoppscotch.io",
{
...req,
wantsBinary: true,
@@ -60,11 +61,11 @@ const axiosWithoutProxy = async (req, _store) => {
}
}
const axiosStrategy = (req, store) => {
if (store.state.postwoman.settings.PROXY_ENABLED) {
return axiosWithProxy(req, store)
const axiosStrategy = (req) => {
if (settingsStore.value.PROXY_ENABLED) {
return axiosWithProxy(req)
}
return axiosWithoutProxy(req, store)
return axiosWithoutProxy(req)
}
export const testables = {

View File

@@ -1,4 +1,5 @@
import { decodeB64StringToArrayBuffer } from "../utils/b64"
import { settingsStore } from "~/newstore/settings"
export const hasExtensionInstalled = () =>
typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined"
@@ -15,12 +16,12 @@ export const cancelRunningExtensionRequest = () => {
}
}
const extensionWithProxy = async (req, { state }) => {
const extensionWithProxy = async (req) => {
const backupTimeDataStart = new Date().getTime()
const res = await window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest({
method: "post",
url: state.postwoman.settings.PROXY_URL || "https://proxy.hoppscotch.io",
url: settingsStore.value.PROXY_URL || "https://proxy.hoppscotch.io/",
data: {
...req,
wantsBinary: true,
@@ -53,7 +54,7 @@ const extensionWithProxy = async (req, { state }) => {
return parsedData
}
const extensionWithoutProxy = async (req, _store) => {
const extensionWithoutProxy = async (req) => {
const backupTimeDataStart = new Date().getTime()
const res = await window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest({
@@ -74,11 +75,11 @@ const extensionWithoutProxy = async (req, _store) => {
return res
}
const extensionStrategy = (req, store) => {
if (store.state.postwoman.settings.PROXY_ENABLED) {
return extensionWithProxy(req, store)
const extensionStrategy = (req) => {
if (settingsStore.value.PROXY_ENABLED) {
return extensionWithProxy(req)
}
return extensionWithoutProxy(req, store)
return extensionWithoutProxy(req)
}
export default extensionStrategy

View File

@@ -2,24 +2,25 @@ import axios from "axios"
import axiosStrategy from "../AxiosStrategy"
jest.mock("axios")
jest.mock("~/newstore/settings", () => {
return {
__esModule: true,
settingsStore: {
value: {
PROXY_ENABLED: false
}
}
}
})
axios.CancelToken.source.mockReturnValue({ token: "test" })
axios.mockResolvedValue({})
describe("axiosStrategy", () => {
describe("No-Proxy Requests", () => {
const store = {
state: {
postwoman: {
settings: {
PROXY_ENABLED: false,
PROXY_URL: "test",
},
},
},
}
test("sends request to the actual sender if proxy disabled", async () => {
await axiosStrategy({ url: "test" }, store)
await axiosStrategy({ url: "test" })
expect(axios).toBeCalledWith(
expect.objectContaining({
@@ -29,7 +30,7 @@ describe("axiosStrategy", () => {
})
test("asks axios to return data as arraybuffer", async () => {
await axiosStrategy({ url: "test" }, store)
await axiosStrategy({ url: "test" })
expect(axios).toBeCalledWith(
expect.objectContaining({
@@ -39,21 +40,21 @@ describe("axiosStrategy", () => {
})
test("resolves successful requests", async () => {
await expect(axiosStrategy({}, store)).resolves.toBeDefined()
await expect(axiosStrategy({})).resolves.toBeDefined()
})
test("rejects cancel errors with text 'cancellation'", async () => {
axios.isCancel.mockReturnValueOnce(true)
axios.mockRejectedValue("err")
expect(axiosStrategy({}, store)).rejects.toBe("cancellation")
expect(axiosStrategy({})).rejects.toBe("cancellation")
})
test("rejects non-cancellation errors as-is", async () => {
axios.isCancel.mockReturnValueOnce(false)
axios.mockRejectedValue("err")
expect(axiosStrategy({}, store)).rejects.toBe("err")
expect(axiosStrategy({})).rejects.toBe("err")
})
})
})

View File

@@ -5,6 +5,17 @@ jest.mock("../../utils/b64", () => ({
__esModule: true,
decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`),
}))
jest.mock("~/newstore/settings", () => {
return {
__esModule: true,
settingsStore: {
value: {
PROXY_ENABLED: true,
PROXY_URL: "test"
}
}
}
})
describe("cancelRunningAxiosRequest", () => {
test("cancels axios request and does that only 1 time", () => {
@@ -17,16 +28,6 @@ describe("cancelRunningAxiosRequest", () => {
describe("axiosStrategy", () => {
describe("Proxy Requests", () => {
const store = {
state: {
postwoman: {
settings: {
PROXY_ENABLED: true,
PROXY_URL: "test",
},
},
},
}
test("sends POST request to proxy if proxy is enabled", async () => {
let passedURL
@@ -36,7 +37,7 @@ describe("axiosStrategy", () => {
return Promise.resolve({ data: { success: true, isBinary: false } })
})
await axiosStrategy({}, store)
await axiosStrategy({})
expect(passedURL).toEqual("test")
})
@@ -55,7 +56,7 @@ describe("axiosStrategy", () => {
return Promise.resolve({ data: { success: true, isBinary: false } })
})
await axiosStrategy(reqFields, store)
await axiosStrategy(reqFields)
expect(passedFields).toMatchObject(reqFields)
})
@@ -68,7 +69,7 @@ describe("axiosStrategy", () => {
return Promise.resolve({ data: { success: true, isBinary: false } })
})
await axiosStrategy({}, store)
await axiosStrategy({})
expect(passedFields).toHaveProperty("wantsBinary")
})
@@ -83,7 +84,7 @@ describe("axiosStrategy", () => {
},
})
await expect(axiosStrategy({}, store)).rejects.toThrow("test message")
await expect(axiosStrategy({})).rejects.toThrow("test message")
})
test("checks for proxy response success field and throws error 'Proxy Error' for non-success", async () => {
@@ -94,7 +95,7 @@ describe("axiosStrategy", () => {
},
})
await expect(axiosStrategy({}, store)).rejects.toThrow("Proxy Error")
await expect(axiosStrategy({})).rejects.toThrow("Proxy Error")
})
test("checks for proxy response success and doesn't throw for success", async () => {
@@ -105,7 +106,7 @@ describe("axiosStrategy", () => {
},
})
await expect(axiosStrategy({}, store)).resolves.toBeDefined()
await expect(axiosStrategy({})).resolves.toBeDefined()
})
test("checks isBinary response field and resolve with the converted value if so", async () => {
@@ -117,7 +118,7 @@ describe("axiosStrategy", () => {
},
})
await expect(axiosStrategy({}, store)).resolves.toMatchObject({
await expect(axiosStrategy({})).resolves.toMatchObject({
data: "testdata-converted",
})
})
@@ -131,7 +132,7 @@ describe("axiosStrategy", () => {
},
})
await expect(axiosStrategy({}, store)).resolves.toMatchObject({
await expect(axiosStrategy({})).resolves.toMatchObject({
data: "testdata",
})
})
@@ -140,14 +141,14 @@ describe("axiosStrategy", () => {
jest.spyOn(axios, "post").mockRejectedValue("errr")
jest.spyOn(axios, "isCancel").mockReturnValueOnce(true)
await expect(axiosStrategy({}, store)).rejects.toBe("cancellation")
await expect(axiosStrategy({})).rejects.toBe("cancellation")
})
test("non-cancellation errors are thrown", async () => {
jest.spyOn(axios, "post").mockRejectedValue("errr")
jest.spyOn(axios, "isCancel").mockReturnValueOnce(false)
await expect(axiosStrategy({}, store)).rejects.toBe("errr")
await expect(axiosStrategy({})).rejects.toBe("errr")
})
})
})

View File

@@ -0,0 +1,220 @@
import extensionStrategy, {
hasExtensionInstalled,
hasChromeExtensionInstalled,
hasFirefoxExtensionInstalled,
cancelRunningExtensionRequest,
} from "../ExtensionStrategy"
jest.mock("../../utils/b64", () => ({
__esModule: true,
decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`),
}))
jest.mock("~/newstore/settings", () => {
return {
__esModule: true,
settingsStore: {
value: {
EXTENSIONS_ENABLED: true,
PROXY_ENABLED: false
}
}
}
})
describe("hasExtensionInstalled", () => {
test("returns true if extension is present and hooked", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
expect(hasExtensionInstalled()).toEqual(true)
})
test("returns false if extension not present or not hooked", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
expect(hasExtensionInstalled()).toEqual(false)
})
})
describe("hasChromeExtensionInstalled", () => {
test("returns true if extension is hooked and browser is chrome", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
expect(hasChromeExtensionInstalled()).toEqual(true)
})
test("returns false if extension is hooked and browser is not chrome", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
expect(hasChromeExtensionInstalled()).toEqual(false)
})
test("returns false if extension not installed and browser is chrome", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
expect(hasChromeExtensionInstalled()).toEqual(false)
})
test("returns false if extension not installed and browser is not chrome", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
expect(hasChromeExtensionInstalled()).toEqual(false)
})
})
describe("hasFirefoxExtensionInstalled", () => {
test("returns true if extension is hooked and browser is firefox", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
expect(hasFirefoxExtensionInstalled()).toEqual(true)
})
test("returns false if extension is hooked and browser is not firefox", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
expect(hasFirefoxExtensionInstalled()).toEqual(false)
})
test("returns false if extension not installed and browser is firefox", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
expect(hasFirefoxExtensionInstalled()).toEqual(false)
})
test("returns false if extension not installed and browser is not firefox", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
expect(hasFirefoxExtensionInstalled()).toEqual(false)
})
})
describe("cancelRunningExtensionRequest", () => {
const cancelFunc = jest.fn()
beforeEach(() => {
cancelFunc.mockClear()
})
test("cancels request if extension installed and function present in hook", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {
cancelRunningRequest: cancelFunc,
}
cancelRunningExtensionRequest()
expect(cancelFunc).toHaveBeenCalledTimes(1)
})
test("does not cancel request if extension not installed", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
cancelRunningExtensionRequest()
expect(cancelFunc).not.toHaveBeenCalled()
})
test("does not cancel request if extension installed but function not present", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
cancelRunningExtensionRequest()
expect(cancelFunc).not.toHaveBeenCalled()
})
})
describe("extensionStrategy", () => {
const sendReqFunc = jest.fn()
beforeEach(() => {
sendReqFunc.mockClear()
})
describe("Non-Proxy Requests", () => {
test("ask extension to send request", async () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockResolvedValue({
data: '{"success":true,"data":""}',
})
await extensionStrategy({})
expect(sendReqFunc).toHaveBeenCalledTimes(1)
})
test("sends request to the actual sender if proxy disabled", async () => {
let passedUrl
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockImplementation(({ method, url }) => {
passedUrl = url
return Promise.resolve({
data: '{"success":true,"data":""}',
})
})
await extensionStrategy({ url: "test" })
expect(passedUrl).toEqual("test")
})
test("asks extension to get binary data", async () => {
let passedFields
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockImplementation((fields) => {
passedFields = fields
return Promise.resolve({
data: '{"success":true,"data":""}',
})
})
await extensionStrategy({})
expect(passedFields).toHaveProperty("wantsBinary")
})
test("resolves successful requests", async () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockResolvedValue({
data: '{"success":true,"data":""}',
})
await expect(extensionStrategy({})).resolves.toBeDefined()
})
test("rejects errors as-is", async () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockRejectedValue("err")
await expect(extensionStrategy({})).rejects.toBe("err")
})
})
})

View File

@@ -10,6 +10,19 @@ jest.mock("../../utils/b64", () => ({
decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`),
}))
jest.mock("~/newstore/settings", () => {
return {
__esModule: true,
settingsStore: {
value: {
EXTENSIONS_ENABLED: true,
PROXY_ENABLED: true,
PROXY_URL: "test"
}
}
}
})
describe("hasExtensionInstalled", () => {
test("returns true if extension is present and hooked", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
@@ -127,16 +140,6 @@ describe("extensionStrategy", () => {
})
describe("Proxy Requests", () => {
const store = {
state: {
postwoman: {
settings: {
PROXY_ENABLED: true,
PROXY_URL: "testURL",
},
},
},
}
test("asks extension to send request", async () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {
@@ -147,7 +150,7 @@ describe("extensionStrategy", () => {
data: '{"success":true,"data":""}',
})
await extensionStrategy({}, store)
await extensionStrategy({})
expect(sendReqFunc).toHaveBeenCalledTimes(1)
})
@@ -169,9 +172,9 @@ describe("extensionStrategy", () => {
})
})
await extensionStrategy({}, store)
await extensionStrategy({})
expect(passedUrl).toEqual(store.state.postwoman.settings.PROXY_URL)
expect(passedUrl).toEqual("test")
expect(passedMethod).toEqual("post")
})
@@ -196,7 +199,7 @@ describe("extensionStrategy", () => {
})
})
await extensionStrategy(reqFields, store)
await extensionStrategy(reqFields)
expect(passedFields).toMatchObject(reqFields)
})
@@ -216,7 +219,7 @@ describe("extensionStrategy", () => {
})
})
await extensionStrategy({}, store)
await extensionStrategy({})
expect(passedFields).toHaveProperty("wantsBinary")
})
@@ -230,7 +233,7 @@ describe("extensionStrategy", () => {
data: '{"success":false,"data": { "message": "testerr" } }',
})
await expect(extensionStrategy({}, store)).rejects.toThrow("testerr")
await expect(extensionStrategy({})).rejects.toThrow("testerr")
})
test("checks for proxy response success field and throws error 'Proxy Error' for non-success", async () => {
@@ -242,7 +245,7 @@ describe("extensionStrategy", () => {
data: '{"success":false,"data": {} }',
})
await expect(extensionStrategy({}, store)).rejects.toThrow("Proxy Error")
await expect(extensionStrategy({})).rejects.toThrow("Proxy Error")
})
test("checks for proxy response success and doesn't throw for success", async () => {
@@ -254,7 +257,7 @@ describe("extensionStrategy", () => {
data: '{"success":true,"data": {} }',
})
await expect(extensionStrategy({}, store)).resolves.toBeDefined()
await expect(extensionStrategy({})).resolves.toBeDefined()
})
test("checks isBinary response field and resolve with the converted value if so", async () => {
@@ -266,7 +269,7 @@ describe("extensionStrategy", () => {
data: '{"success": true, "isBinary": true, "data": "testdata" }',
})
await expect(extensionStrategy({}, store)).resolves.toMatchObject({
await expect(extensionStrategy({})).resolves.toMatchObject({
data: "testdata-converted",
})
})
@@ -280,7 +283,7 @@ describe("extensionStrategy", () => {
data: '{"success": true, "isBinary": false, "data": "testdata" }',
})
await expect(extensionStrategy({}, store)).resolves.toMatchObject({
await expect(extensionStrategy({})).resolves.toMatchObject({
data: "testdata",
})
})
@@ -292,96 +295,7 @@ describe("extensionStrategy", () => {
sendReqFunc.mockRejectedValue("err")
await expect(extensionStrategy({}, store)).rejects.toBe("err")
})
})
describe("Non-Proxy Requests", () => {
const store = {
state: {
postwoman: {
settings: {
PROXY_ENABLED: false,
PROXY_URL: "testURL",
},
},
},
}
test("ask extension to send request", async () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockResolvedValue({
data: '{"success":true,"data":""}',
})
await extensionStrategy({}, store)
expect(sendReqFunc).toHaveBeenCalledTimes(1)
})
test("sends request to the actual sender if proxy disabled", async () => {
let passedUrl
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockImplementation(({ method, url }) => {
passedUrl = url
return Promise.resolve({
data: '{"success":true,"data":""}',
})
})
await extensionStrategy({ url: "test" }, store)
expect(passedUrl).toEqual("test")
})
test("asks extension to get binary data", async () => {
let passedFields
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockImplementation((fields) => {
passedFields = fields
return Promise.resolve({
data: '{"success":true,"data":""}',
})
})
await extensionStrategy({}, store)
expect(passedFields).toHaveProperty("wantsBinary")
})
test("resolves successful requests", async () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockResolvedValue({
data: '{"success":true,"data":""}',
})
await expect(extensionStrategy({}, store)).resolves.toBeDefined()
})
test("rejects errors as-is", async () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {
sendRequest: sendReqFunc,
}
sendReqFunc.mockRejectedValue("err")
await expect(extensionStrategy({}, store)).rejects.toBe("err")
await expect(extensionStrategy({})).rejects.toBe("err")
})
})
})

View File

@@ -14,6 +14,9 @@
</template>
<script>
import { setupLocalPersistence } from "~/newstore/localpersistence"
import { performMigrations } from "~/helpers/migrations"
export default {
beforeMount() {
let color = localStorage.getItem("THEME_COLOR") || "green"
@@ -24,13 +27,7 @@ export default {
document.body.classList.add("afterLoad")
}
// Migrate old default proxy URL to the new proxy URL (if not set / overridden)
if (
this.$store.state.postwoman.settings.PROXY_URL &&
this.$store.state.postwoman.settings.PROXY_URL === "https://hoppscotch.apollosoftware.xyz/"
) {
this.$store.state.postwoman.settings.PROXY_URL = "https://proxy.hoppscotch.io/"
}
performMigrations()
console.log(
"%cWe ❤︎ open source!",
@@ -61,6 +58,8 @@ export default {
}
})
}
setupLocalPersistence()
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)

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
}
})
}

View File

@@ -92,6 +92,7 @@ export default {
plugins: [
"~/plugins/vuex-persist",
"~/plugins/v-tooltip",
"~/plugins/vue-rx",
{ src: "~/plugins/web-worker", ssr: false },
],
@@ -112,6 +113,8 @@ export default {
"@nuxtjs/color-mode",
// https: //github.com/nuxt-community/google-fonts-module
"@nuxtjs/google-fonts",
// https://github.com/nuxt/typescript
"@nuxt/typescript-build",
],
// Modules (https://go.nuxtjs.dev/config-modules)

38473
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,33 +32,39 @@
"firebase": "^8.3.1",
"graphql": "^15.5.0",
"graphql-language-service-interface": "^2.8.2",
"lodash": "^4.17.20",
"mustache": "^4.1.0",
"nuxt": "^2.15.3",
"nuxt-i18n": "^6.21.1",
"paho-mqtt": "^1.1.0",
"rxjs": "^6.6.3",
"socket.io-client": "^4.0.0",
"socketio-wildcard": "^2.0.0",
"tern": "^0.24.3",
"v-tooltip": "^2.1.3",
"vue-rx": "^6.2.0",
"vuejs-auto-complete": "^0.9.0",
"vuex-persist": "^3.1.3",
"yargs-parser": "^20.2.7"
},
"devDependencies": {
"@babel/core": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/preset-env": "^7.13.10",
"@nuxt/types": "^2.15.3",
"@nuxt/typescript-build": "^2.1.0",
"@nuxtjs/color-mode": "^2.0.5",
"@nuxtjs/google-analytics": "^2.4.0",
"@nuxtjs/google-fonts": "^1.3.0",
"@nuxtjs/pwa": "^3.3.5",
"@nuxtjs/tailwindcss": "^4.0.1",
"@testing-library/jest-dom": "^5.11.9",
"@types/lodash": "^4.14.168",
"@vue/test-utils": "^1.1.3",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^26.6.3",
"eslint": "^7.22.0",
"eslint-plugin-vue": "^7.7.0",
"firebase-mock": "^2.3.2",
"husky": "^5.2.0",
"jest": "^26.6.3",
"jest-serializer-vue": "^2.0.2",
@@ -68,11 +74,13 @@
"raw-loader": "^4.0.2",
"sass": "^1.32.8",
"sass-loader": "^10.1.1",
"ts-jest": "^26.5.4",
"vue-jest": "^3.0.7",
"worker-loader": "^3.0.8"
},
"jest": {
"moduleFileExtensions": [
"ts",
"js",
"json",
"vue"
@@ -98,6 +106,7 @@
"<rootDir>/components/**/*.vue",
"<rootDir>/pages/*.vue"
],
"testURL": "http://localhost/"
"testURL": "http://localhost/",
"preset": "ts-jest/presets/js-with-babel"
}
}

View File

@@ -448,7 +448,7 @@ import * as gql from "graphql"
import { commonHeaders } from "~/helpers/headers"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { sendNetworkRequest } from "~/helpers/network"
import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default {
data() {
@@ -469,13 +469,11 @@ export default {
activeSidebar: true,
editRequest: {},
showSaveRequestModal: false,
settings: {
SCROLL_INTO_ENABLED:
typeof this.$store.state.postwoman.settings.SCROLL_INTO_ENABLED !== "undefined"
? this.$store.state.postwoman.settings.SCROLL_INTO_ENABLED
: true,
}
},
subscriptions() {
return {
SCROLL_INTO_ENABLED: getSettingSubject("SCROLL_INTO_ENABLED"),
}
},
watch: {
@@ -681,7 +679,7 @@ export default {
const rootTypeName = this.resolveRootType(type).name
const target = document.getElementById(`type_${rootTypeName}`)
if (target && this.settings.SCROLL_INTO_ENABLED) {
if (target && this.SCROLL_INTO_ENABLED) {
this.$refs.gqlTabs.$el
.querySelector(".gqlTabs")
.scrollTo({ top: target.offsetTop, behavior: "smooth" })
@@ -739,7 +737,7 @@ export default {
this.$nuxt.$loading.start()
this.response = this.$t("loading")
if (this.settings.SCROLL_INTO_ENABLED) this.scrollInto("response")
if (this.SCROLL_INTO_ENABLED) this.scrollInto("response")
try {
let headers = {}
@@ -769,7 +767,7 @@ export default {
star: false,
headers: this.headers,
}
const res = await sendNetworkRequest(reqOptions, this.$store)
const res = await sendNetworkRequest(reqOptions)
// HACK: Temporary trailing null character issue from the extension fix
const responseText = new TextDecoder("utf-8").decode(res.data).replace(/\0+$/, "")
@@ -946,7 +944,7 @@ export default {
this.$nuxt.$loading.start()
this.schema = this.$t("loading")
if (this.settings.SCROLL_INTO_ENABLED) this.scrollInto("schema")
if (this.SCROLL_INTO_ENABLED) this.scrollInto("schema")
try {
const query = JSON.stringify({

View File

@@ -38,7 +38,7 @@
<li>
<label for="url">{{ $t("url") }}</label>
<input
v-if="!this.$store.state.postwoman.settings.EXPERIMENTAL_URL_BAR_ENABLED"
v-if="!EXPERIMENTAL_URL_BAR_ENABLED"
:class="{ error: !isValidURL }"
class="border-dashed md:border-l border-brdColor"
@keyup.enter="isValidURL ? sendRequest() : null"
@@ -280,7 +280,7 @@
</li>
</ul>
<div class="row-wrapper">
<SmartToggle :on="!urlExcludes.auth" @change="setExclude('auth', !$event)">
<SmartToggle :on="!URL_EXCLUDES.auth" @change="setExclude('auth', !$event)">
{{ $t("include_in_url") }}
</SmartToggle>
</div>
@@ -665,6 +665,8 @@ import { parseUrlAndPath } from "~/helpers/utils/uri"
import { httpValid } from "~/helpers/utils/valid"
import { knownContentTypes, isJSONContentType } from "~/helpers/utils/contenttypes"
import { generateCodeWithGenerator } from "~/helpers/codegen/codegen"
import { getSettingSubject, applySetting } from "~/newstore/settings"
import clone from "lodash/clone"
export default {
data() {
@@ -693,7 +695,6 @@ export default {
showTokenRequestList: false,
showSaveRequestModal: false,
editRequest: {},
urlExcludes: {},
activeSidebar: true,
fb,
customMethod: false,
@@ -701,12 +702,6 @@ export default {
filenames: "",
navigatorShare: navigator.share,
runningRequest: false,
settings: {
SCROLL_INTO_ENABLED:
typeof this.$store.state.postwoman.settings.SCROLL_INTO_ENABLED !== "undefined"
? this.$store.state.postwoman.settings.SCROLL_INTO_ENABLED
: true,
},
currentMethodIndex: 0,
methodMenuItems: [
"GET",
@@ -722,16 +717,18 @@ export default {
],
}
},
subscriptions() {
return {
SCROLL_INTO_ENABLED: getSettingSubject("SCROLL_INTO_ENABLED"),
PROXY_ENABLED: getSettingSubject("PROXY_ENABLED"),
URL_EXCLUDES: getSettingSubject("URL_EXCLUDES"),
EXPERIMENTAL_URL_BAR_ENABLED: getSettingSubject("EXPERIMENTAL_URL_BAR_ENABLED"),
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
SYNC_HISTORY: getSettingSubject("syncHistory"),
}
},
watch: {
urlExcludes: {
deep: true,
handler() {
this.$store.commit("postwoman/applySetting", [
"URL_EXCLUDES",
Object.assign({}, this.urlExcludes),
])
},
},
canListParameters: {
immediate: true,
handler(canListParameters) {
@@ -1230,7 +1227,7 @@ export default {
this.requestType = entry.requestType
this.testScript = entry.testScript
this.testsEnabled = entry.usesPostScripts
if (this.settings.SCROLL_INTO_ENABLED) this.scrollInto("request")
if (this.SCROLL_INTO_ENABLED) this.scrollInto("request")
},
async makeRequest(auth, headers, requestBody, preRequestScript) {
const requestOptions = {
@@ -1260,14 +1257,14 @@ export default {
if (typeof requestOptions.data === "string") {
requestOptions.data = parseTemplateString(requestOptions.data)
}
return await sendNetworkRequest(requestOptions, this.$store)
return await sendNetworkRequest(requestOptions)
},
cancelRequest() {
cancelRunningRequest(this.$store)
cancelRunningRequest()
},
async sendRequest() {
this.$toast.clear()
if (this.settings.SCROLL_INTO_ENABLED) this.scrollInto("response")
if (this.SCROLL_INTO_ENABLED) this.scrollInto("response")
if (!this.isValidURL) {
this.$toast.error(this.$t("url_invalid_format"), {
icon: "error",
@@ -1396,11 +1393,9 @@ export default {
}
this.$refs.historyComponent.addEntry(entry)
if (fb.currentUser !== null && fb.currentSettings[2]) {
if (fb.currentSettings[2].value) {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeHistory(entry)
}
}
})()
} catch (error) {
this.runningRequest = false
@@ -1456,11 +1451,9 @@ export default {
}
this.$refs.historyComponent.addEntry(entry)
if (fb.currentUser !== null && fb.currentSettings[2]) {
if (fb.currentSettings[2].value) {
if (fb.currentUser !== null && this.SYNC_HISTORY) {
fb.writeHistory(entry)
}
}
return
} else {
this.response.status = error.message
@@ -1468,7 +1461,7 @@ export default {
this.$toast.error(`${error} ${this.$t("f12_details")}`, {
icon: "error",
})
if (!this.$store.state.postwoman.settings.PROXY_ENABLED) {
if (!this.PROXY_ENABLED) {
this.$toast.info(this.$t("enable_proxy"), {
icon: "help",
duration: 8000,
@@ -1629,10 +1622,10 @@ export default {
"method",
"url",
"path",
!this.urlExcludes.auth ? "auth" : null,
!this.urlExcludes.httpUser ? "httpUser" : null,
!this.urlExcludes.httpPassword ? "httpPassword" : null,
!this.urlExcludes.bearerToken ? "bearerToken" : null,
!this.URL_EXCLUDES.auth ? "auth" : null,
!this.URL_EXCLUDES.httpUser ? "httpUser" : null,
!this.URL_EXCLUDES.httpPassword ? "httpPassword" : null,
!this.URL_EXCLUDES.bearerToken ? "bearerToken" : null,
"contentType",
]
.filter((item) => item !== null)
@@ -1818,14 +1811,19 @@ export default {
this.editRequest = {}
},
setExclude(excludedField, excluded) {
const update = clone(this.URL_EXCLUDES)
if (excludedField === "auth") {
this.urlExcludes.auth = excluded
this.urlExcludes.httpUser = excluded
this.urlExcludes.httpPassword = excluded
this.urlExcludes.bearerToken = excluded
update.auth = excluded
update.httpUser = excluded
update.httpPassword = excluded
update.bearerToken = excluded
} else {
this.urlExcludes[excludedField] = excluded
update[excludedField] = excluded
}
applySetting("URL_EXCLUDES", update)
this.setRouteQueryState()
},
updateRawBody(rawParams) {
@@ -1987,13 +1985,6 @@ export default {
await this.oauthRedirectReq()
},
created() {
this.urlExcludes = this.$store.state.postwoman.settings.URL_EXCLUDES || {
// Exclude authentication by default for security reasons.
auth: true,
httpUser: true,
httpPassword: true,
bearerToken: true,
}
if (Object.keys(this.$route.query).length) this.setRouteQueries(this.$route.query)
this.$watch(
(vm) => [

View File

@@ -24,16 +24,33 @@
</button>
<br />
<FirebaseLogout />
<p v-for="setting in fb.currentSettings" :key="setting.id">
<p>
<SmartToggle
:key="setting.name"
:on="setting.value"
@change="toggleSettings(setting.name, setting.value)"
:on="SYNC_COLLECTIONS"
@change="toggleSettings('syncCollections', !SYNC_COLLECTIONS)"
>
{{ $t(setting.name) + " " + $t("sync") }}
{{ setting.value ? $t("enabled") : $t("disabled") }}
{{ $t("syncCollections") + " " + $t("sync") }}
{{ SYNC_COLLECTIONS ? $t("enabled") : $t("disabled") }}
</SmartToggle>
</p>
<p>
<SmartToggle
:on="SYNC_ENVIRONMENTS"
@change="toggleSettings('syncEnvironments', !SYNC_ENVIRONMENTS)"
>
{{ $t("syncEnvironments") + " " + $t("sync") }}
{{ SYNC_ENVIRONMENTS ? $t("enabled") : $t("disabled") }}
</SmartToggle>
</p>
<p>
<SmartToggle :on="SYNC_HISTORY" @change="toggleSettings('syncHistory', !SYNC_HISTORY)">
{{ $t("syncHistory") + " " + $t("sync") }}
{{ SYNC_HISTORY ? $t("enabled") : $t("disabled") }}
</SmartToggle>
</p>
<p v-if="fb.currentSettings.length !== 3">
<button @click="initSettings">
<i class="material-icons">sync</i>
@@ -56,12 +73,9 @@
<SmartColorModePicker />
<SmartAccentModePicker />
<span>
<SmartToggle
:on="settings.SCROLL_INTO_ENABLED"
@change="toggleSetting('SCROLL_INTO_ENABLED')"
>
<SmartToggle :on="SCROLL_INTO_ENABLED" @change="toggleSetting('SCROLL_INTO_ENABLED')">
{{ $t("scrollInto_use_toggle") }}
{{ settings.SCROLL_INTO_ENABLED ? $t("enabled") : $t("disabled") }}
{{ SCROLL_INTO_ENABLED ? $t("enabled") : $t("disabled") }}
</SmartToggle>
</span>
</div>
@@ -71,10 +85,7 @@
<div class="flex flex-col">
<label>{{ $t("extensions") }}</label>
<div class="row-wrapper">
<SmartToggle
:on="settings.EXTENSIONS_ENABLED"
@change="toggleSetting('EXTENSIONS_ENABLED')"
>
<SmartToggle :on="EXTENSIONS_ENABLED" @change="toggleSetting('EXTENSIONS_ENABLED')">
{{ $t("extensions_use_toggle") }}
</SmartToggle>
</div>
@@ -92,9 +103,9 @@
<label>{{ $t("proxy") }}</label>
<div class="row-wrapper">
<span>
<SmartToggle :on="settings.PROXY_ENABLED" @change="toggleSetting('PROXY_ENABLED')">
<SmartToggle :on="PROXY_ENABLED" @change="toggleSetting('PROXY_ENABLED')">
{{ $t("proxy") }}
{{ settings.PROXY_ENABLED ? $t("enabled") : $t("disabled") }}
{{ PROXY_ENABLED ? $t("enabled") : $t("disabled") }}
</SmartToggle>
</span>
<a
@@ -116,8 +127,8 @@
<input
id="url"
type="url"
v-model="settings.PROXY_URL"
:disabled="!settings.PROXY_ENABLED"
v-model="PROXY_URL"
:disabled="!PROXY_ENABLED"
:placeholder="$t('url')"
/>
<p class="info">
@@ -166,7 +177,7 @@
</p>
<div class="row-wrapper">
<SmartToggle
:on="settings.EXPERIMENTAL_URL_BAR_ENABLED"
:on="EXPERIMENTAL_URL_BAR_ENABLED"
@change="toggleSetting('EXPERIMENTAL_URL_BAR_ENABLED')"
>
{{ $t("use_experimental_url_bar") }}
@@ -177,40 +188,54 @@
</div>
</template>
<script>
<script lang="ts">
import { fb } from "~/helpers/fb"
import { hasExtensionInstalled } from "../helpers/strategies/ExtensionStrategy"
import {
getSettingSubject,
applySetting,
toggleSetting,
defaultSettings,
} from "~/newstore/settings"
import type { KeysMatching } from "~/types/ts-utils"
export default {
import Vue from "vue"
type SettingsType = typeof defaultSettings
export default Vue.extend({
data() {
return {
extensionVersion: hasExtensionInstalled()
? window.__POSTWOMAN_EXTENSION_HOOK__.getVersion()
: null,
settings: {
SCROLL_INTO_ENABLED:
typeof this.$store.state.postwoman.settings.SCROLL_INTO_ENABLED !== "undefined"
? this.$store.state.postwoman.settings.SCROLL_INTO_ENABLED
: true,
PROXY_ENABLED: this.$store.state.postwoman.settings.PROXY_ENABLED || false,
PROXY_URL: this.$store.state.postwoman.settings.PROXY_URL || "https://proxy.hoppscotch.io",
PROXY_KEY: this.$store.state.postwoman.settings.PROXY_KEY || "",
EXTENSIONS_ENABLED:
typeof this.$store.state.postwoman.settings.EXTENSIONS_ENABLED !== "undefined"
? this.$store.state.postwoman.settings.EXTENSIONS_ENABLED
: true,
EXPERIMENTAL_URL_BAR_ENABLED:
typeof this.$store.state.postwoman.settings.EXPERIMENTAL_URL_BAR_ENABLED !== "undefined"
? this.$store.state.postwoman.settings.EXPERIMENTAL_URL_BAR_ENABLED
: false,
},
doneButton: '<i class="material-icons">done</i>',
fb,
SYNC_COLLECTIONS: true,
SYNC_ENVIRONMENTS: true,
SYNC_HISTORY: true,
PROXY_URL: "",
PROXY_KEY: "",
}
},
subscriptions() {
return {
SCROLL_INTO_ENABLED: getSettingSubject("SCROLL_INTO_ENABLED"),
PROXY_ENABLED: getSettingSubject("PROXY_ENABLED"),
PROXY_URL: getSettingSubject("PROXY_URL"),
PROXY_KEY: getSettingSubject("PROXY_KEY"),
EXTENSIONS_ENABLED: getSettingSubject("EXTENSIONS_ENABLED"),
EXPERIMENTAL_URL_BAR_ENABLED: getSettingSubject("EXPERIMENTAL_URL_BAR_ENABLED"),
SYNC_COLLECTIONS: getSettingSubject("syncCollections"),
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments"),
SYNC_HISTORY: getSettingSubject("syncHistory"),
}
},
watch: {
@@ -223,16 +248,15 @@ export default {
},
},
methods: {
applySetting(key, value) {
this.settings[key] = value
this.$store.commit("postwoman/applySetting", [key, value])
applySetting<K extends keyof SettingsType>(key: K, value: SettingsType[K]) {
applySetting(key, value)
},
toggleSetting(key) {
this.settings[key] = !this.settings[key]
this.$store.commit("postwoman/applySetting", [key, this.settings[key]])
toggleSetting<K extends KeysMatching<SettingsType, boolean>>(key: K) {
toggleSetting(key)
},
toggleSettings(name, value) {
fb.writeSettings(name, !value)
toggleSettings<K extends KeysMatching<SettingsType, boolean>>(name: K, value: SettingsType[K]) {
this.applySetting(name, value)
if (name === "syncCollections" && value) {
this.syncCollections()
}
@@ -241,21 +265,21 @@ export default {
}
},
initSettings() {
fb.writeSettings("syncHistory", true)
fb.writeSettings("syncCollections", true)
fb.writeSettings("syncEnvironments", true)
applySetting("syncHistory", true)
applySetting("syncCollections", true)
applySetting("syncEnvironments", true)
},
resetProxy({ target }) {
this.settings.PROXY_URL = `https://proxy.hoppscotch.io`
resetProxy({ target }: { target: HTMLElement }) {
applySetting("PROXY_URL", `https://proxy.hoppscotch.io/`)
target.innerHTML = this.doneButton
this.$toast.info(this.$t("cleared"), {
icon: "clear_all",
})
setTimeout(() => (target.innerHTML = '<i class="material-icons">clear_all</i>'), 1000)
},
syncCollections() {
if (fb.currentUser !== null && fb.currentSettings[0]) {
if (fb.currentSettings[0].value) {
syncCollections(): void {
if (fb.currentUser !== null && this.SYNC_COLLECTIONS) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections)),
"collections"
@@ -265,21 +289,18 @@ export default {
"collectionsGraphql"
)
}
}
},
syncEnvironments() {
if (fb.currentUser !== null && fb.currentSettings[1]) {
if (fb.currentSettings[1].value) {
syncEnvironments(): void {
if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
}
}
},
},
computed: {
proxySettings() {
proxySettings(): { url: string; key: string } {
return {
url: this.settings.PROXY_URL,
key: this.settings.PROXY_KEY,
url: this.PROXY_URL,
key: this.PROXY_KEY,
}
},
},
@@ -288,5 +309,5 @@ export default {
title: `Settings • Hoppscotch`,
}
},
}
})
</script>

4
plugins/vue-rx.js Normal file
View File

@@ -0,0 +1,4 @@
import Vue from "vue"
import VueRx from "vue-rx"
Vue.use(VueRx)

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
"esModuleInterop": true,
"allowJs": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@/*": ["./*"]
},
"types": ["@types/node", "@nuxt/types", "nuxt-i18n", "@nuxtjs/toast", "vue-rx"]
},
"exclude": ["node_modules"]
}

21
types/pw-ext-hook.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
interface PWExtensionRequestInfo {
method: string
url: string
data: any & { wantsBinary: boolean }
}
interface PWExtensionResponse {
data: any
config?: {
timeData?: {
startTime: number
endTime: number
}
}
}
interface PWExtensionHook {
getVersion: () => { major: number, minor: number }
sendRequest: (req: PWExtensionRequestInfo) => Promise<PWExtensionResponse>
cancelRunningRequest: () => void
}

1
types/ts-utils.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export type KeysMatching<T, V> = {[K in keyof T]-?: T[K] extends V ? K : never}[keyof T];

7
types/window.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
export {}
declare global {
interface Window {
__POSTWOMAN_EXTENSION_HOOK__: PWExtensionHook
}
}

4
vue-shim.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from 'vue'
export default Vue
}