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) { module.exports = function (api) {
if (api.env("test") && !api.caller(isBabelLoader)) { if (api.env("test") && !api.caller(isBabelLoader)) {
return { return {
plugins: [
"@babel/plugin-proposal-class-properties"
],
presets: [ presets: [
[ [
"@babel/preset-env", "@babel/preset-env",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,6 +66,7 @@
<script> <script>
import { fb } from "~/helpers/fb" import { fb } from "~/helpers/fb"
import { getSettingSubject } from "~/newstore/settings"
export default { export default {
data() { data() {
@@ -82,6 +83,11 @@ export default {
}, },
} }
}, },
subscriptions() {
return {
SYNC_ENVIRONMENTS: getSettingSubject("syncEnvironments"),
}
},
computed: { computed: {
environments() { environments() {
return fb.currentUser !== null return fb.currentUser !== null
@@ -153,11 +159,9 @@ export default {
this.$data.editingEnvironmentIndex = undefined this.$data.editingEnvironmentIndex = undefined
}, },
syncEnvironments() { syncEnvironments() {
if (fb.currentUser !== null && fb.currentSettings[1]) { if (fb.currentUser !== null && this.SYNC_ENVIRONMENTS) {
if (fb.currentSettings[1].value) {
fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments))) fb.writeEnvironments(JSON.parse(JSON.stringify(this.$store.state.postwoman.environments)))
} }
}
}, },
}, },
beforeDestroy() { 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 from "firebase/app"
import "firebase/firestore" import "firebase/firestore"
import "firebase/auth" import "firebase/auth"
import { ReplaySubject } from "rxjs"
import { getSettingSubject, applySetting } from "~/newstore/settings"
// Initialize Firebase, copied from cloud console // Initialize Firebase, copied from cloud console
const firebaseConfig = { const firebaseConfig = {
@@ -38,6 +40,41 @@ export class FirebaseInstance {
this.currentGraphqlCollections = [] this.currentGraphqlCollections = []
this.currentEnvironments = [] 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) => { this.app.auth().onAuthStateChanged((user) => {
if (user) { if (user) {
this.currentUser = user this.currentUser = user
@@ -87,6 +124,14 @@ export class FirebaseInstance {
settings.push(setting) settings.push(setting)
}) })
this.currentSettings = settings this.currentSettings = settings
settings.forEach((e) => {
if (e && e.name && e.value != null) {
applySetting(e.name, e.value)
}
})
loadedSettings = true
}) })
this.usersCollection 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, cancelRunningExtensionRequest,
hasExtensionInstalled, hasExtensionInstalled,
} from "./strategies/ExtensionStrategy" } from "./strategies/ExtensionStrategy"
import { settingsStore } from "~/newstore/settings"
export const cancelRunningRequest = (store) => { export const cancelRunningRequest = () => {
if (isExtensionsAllowed(store) && hasExtensionInstalled()) { if (isExtensionsAllowed() && hasExtensionInstalled()) {
cancelRunningExtensionRequest() cancelRunningExtensionRequest()
} else { } else {
cancelRunningAxiosRequest() cancelRunningAxiosRequest()
} }
} }
const isExtensionsAllowed = ({ state }) => const isExtensionsAllowed = () => settingsStore.value.EXTENSIONS_ENABLED
typeof state.postwoman.settings.EXTENSIONS_ENABLED === "undefined" ||
state.postwoman.settings.EXTENSIONS_ENABLED
const runAppropriateStrategy = (req, store) => { const runAppropriateStrategy = (req) => {
if (isExtensionsAllowed(store) && hasExtensionInstalled()) { if (isExtensionsAllowed() && hasExtensionInstalled()) {
return ExtensionStrategy(req, store) return ExtensionStrategy(req)
} }
return AxiosStrategy(req, store) return AxiosStrategy(req)
} }
export const sendNetworkRequest = (req, store) => export const sendNetworkRequest = (req) =>
runAppropriateStrategy(req, store).finally(() => window.$nuxt.$loading.finish()) runAppropriateStrategy(req).finally(() => window.$nuxt.$loading.finish())

View File

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

View File

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

View File

@@ -2,24 +2,25 @@ import axios from "axios"
import axiosStrategy from "../AxiosStrategy" import axiosStrategy from "../AxiosStrategy"
jest.mock("axios") jest.mock("axios")
jest.mock("~/newstore/settings", () => {
return {
__esModule: true,
settingsStore: {
value: {
PROXY_ENABLED: false
}
}
}
})
axios.CancelToken.source.mockReturnValue({ token: "test" }) axios.CancelToken.source.mockReturnValue({ token: "test" })
axios.mockResolvedValue({}) axios.mockResolvedValue({})
describe("axiosStrategy", () => { describe("axiosStrategy", () => {
describe("No-Proxy Requests", () => { 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 () => { test("sends request to the actual sender if proxy disabled", async () => {
await axiosStrategy({ url: "test" }, store) await axiosStrategy({ url: "test" })
expect(axios).toBeCalledWith( expect(axios).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -29,7 +30,7 @@ describe("axiosStrategy", () => {
}) })
test("asks axios to return data as arraybuffer", async () => { test("asks axios to return data as arraybuffer", async () => {
await axiosStrategy({ url: "test" }, store) await axiosStrategy({ url: "test" })
expect(axios).toBeCalledWith( expect(axios).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -39,21 +40,21 @@ describe("axiosStrategy", () => {
}) })
test("resolves successful requests", async () => { test("resolves successful requests", async () => {
await expect(axiosStrategy({}, store)).resolves.toBeDefined() await expect(axiosStrategy({})).resolves.toBeDefined()
}) })
test("rejects cancel errors with text 'cancellation'", async () => { test("rejects cancel errors with text 'cancellation'", async () => {
axios.isCancel.mockReturnValueOnce(true) axios.isCancel.mockReturnValueOnce(true)
axios.mockRejectedValue("err") axios.mockRejectedValue("err")
expect(axiosStrategy({}, store)).rejects.toBe("cancellation") expect(axiosStrategy({})).rejects.toBe("cancellation")
}) })
test("rejects non-cancellation errors as-is", async () => { test("rejects non-cancellation errors as-is", async () => {
axios.isCancel.mockReturnValueOnce(false) axios.isCancel.mockReturnValueOnce(false)
axios.mockRejectedValue("err") 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, __esModule: true,
decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`), decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`),
})) }))
jest.mock("~/newstore/settings", () => {
return {
__esModule: true,
settingsStore: {
value: {
PROXY_ENABLED: true,
PROXY_URL: "test"
}
}
}
})
describe("cancelRunningAxiosRequest", () => { describe("cancelRunningAxiosRequest", () => {
test("cancels axios request and does that only 1 time", () => { test("cancels axios request and does that only 1 time", () => {
@@ -17,16 +28,6 @@ describe("cancelRunningAxiosRequest", () => {
describe("axiosStrategy", () => { describe("axiosStrategy", () => {
describe("Proxy Requests", () => { 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 () => { test("sends POST request to proxy if proxy is enabled", async () => {
let passedURL let passedURL
@@ -36,7 +37,7 @@ describe("axiosStrategy", () => {
return Promise.resolve({ data: { success: true, isBinary: false } }) return Promise.resolve({ data: { success: true, isBinary: false } })
}) })
await axiosStrategy({}, store) await axiosStrategy({})
expect(passedURL).toEqual("test") expect(passedURL).toEqual("test")
}) })
@@ -55,7 +56,7 @@ describe("axiosStrategy", () => {
return Promise.resolve({ data: { success: true, isBinary: false } }) return Promise.resolve({ data: { success: true, isBinary: false } })
}) })
await axiosStrategy(reqFields, store) await axiosStrategy(reqFields)
expect(passedFields).toMatchObject(reqFields) expect(passedFields).toMatchObject(reqFields)
}) })
@@ -68,7 +69,7 @@ describe("axiosStrategy", () => {
return Promise.resolve({ data: { success: true, isBinary: false } }) return Promise.resolve({ data: { success: true, isBinary: false } })
}) })
await axiosStrategy({}, store) await axiosStrategy({})
expect(passedFields).toHaveProperty("wantsBinary") 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 () => { 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 () => { 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 () => { 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", data: "testdata-converted",
}) })
}) })
@@ -131,7 +132,7 @@ describe("axiosStrategy", () => {
}, },
}) })
await expect(axiosStrategy({}, store)).resolves.toMatchObject({ await expect(axiosStrategy({})).resolves.toMatchObject({
data: "testdata", data: "testdata",
}) })
}) })
@@ -140,14 +141,14 @@ describe("axiosStrategy", () => {
jest.spyOn(axios, "post").mockRejectedValue("errr") jest.spyOn(axios, "post").mockRejectedValue("errr")
jest.spyOn(axios, "isCancel").mockReturnValueOnce(true) 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 () => { test("non-cancellation errors are thrown", async () => {
jest.spyOn(axios, "post").mockRejectedValue("errr") jest.spyOn(axios, "post").mockRejectedValue("errr")
jest.spyOn(axios, "isCancel").mockReturnValueOnce(false) 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`), 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", () => { describe("hasExtensionInstalled", () => {
test("returns true if extension is present and hooked", () => { test("returns true if extension is present and hooked", () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = {} global.__POSTWOMAN_EXTENSION_HOOK__ = {}
@@ -127,16 +140,6 @@ describe("extensionStrategy", () => {
}) })
describe("Proxy Requests", () => { describe("Proxy Requests", () => {
const store = {
state: {
postwoman: {
settings: {
PROXY_ENABLED: true,
PROXY_URL: "testURL",
},
},
},
}
test("asks extension to send request", async () => { test("asks extension to send request", async () => {
global.__POSTWOMAN_EXTENSION_HOOK__ = { global.__POSTWOMAN_EXTENSION_HOOK__ = {
@@ -147,7 +150,7 @@ describe("extensionStrategy", () => {
data: '{"success":true,"data":""}', data: '{"success":true,"data":""}',
}) })
await extensionStrategy({}, store) await extensionStrategy({})
expect(sendReqFunc).toHaveBeenCalledTimes(1) 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") expect(passedMethod).toEqual("post")
}) })
@@ -196,7 +199,7 @@ describe("extensionStrategy", () => {
}) })
}) })
await extensionStrategy(reqFields, store) await extensionStrategy(reqFields)
expect(passedFields).toMatchObject(reqFields) expect(passedFields).toMatchObject(reqFields)
}) })
@@ -216,7 +219,7 @@ describe("extensionStrategy", () => {
}) })
}) })
await extensionStrategy({}, store) await extensionStrategy({})
expect(passedFields).toHaveProperty("wantsBinary") expect(passedFields).toHaveProperty("wantsBinary")
}) })
@@ -230,7 +233,7 @@ describe("extensionStrategy", () => {
data: '{"success":false,"data": { "message": "testerr" } }', 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 () => { 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": {} }', 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 () => { test("checks for proxy response success and doesn't throw for success", async () => {
@@ -254,7 +257,7 @@ describe("extensionStrategy", () => {
data: '{"success":true,"data": {} }', 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 () => { 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" }', data: '{"success": true, "isBinary": true, "data": "testdata" }',
}) })
await expect(extensionStrategy({}, store)).resolves.toMatchObject({ await expect(extensionStrategy({})).resolves.toMatchObject({
data: "testdata-converted", data: "testdata-converted",
}) })
}) })
@@ -280,7 +283,7 @@ describe("extensionStrategy", () => {
data: '{"success": true, "isBinary": false, "data": "testdata" }', data: '{"success": true, "isBinary": false, "data": "testdata" }',
}) })
await expect(extensionStrategy({}, store)).resolves.toMatchObject({ await expect(extensionStrategy({})).resolves.toMatchObject({
data: "testdata", data: "testdata",
}) })
}) })
@@ -292,96 +295,7 @@ describe("extensionStrategy", () => {
sendReqFunc.mockRejectedValue("err") sendReqFunc.mockRejectedValue("err")
await expect(extensionStrategy({}, store)).rejects.toBe("err") await expect(extensionStrategy({})).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")
}) })
}) })
}) })

View File

@@ -14,6 +14,9 @@
</template> </template>
<script> <script>
import { setupLocalPersistence } from "~/newstore/localpersistence"
import { performMigrations } from "~/helpers/migrations"
export default { export default {
beforeMount() { beforeMount() {
let color = localStorage.getItem("THEME_COLOR") || "green" let color = localStorage.getItem("THEME_COLOR") || "green"
@@ -24,13 +27,7 @@ export default {
document.body.classList.add("afterLoad") document.body.classList.add("afterLoad")
} }
// Migrate old default proxy URL to the new proxy URL (if not set / overridden) performMigrations()
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/"
}
console.log( console.log(
"%cWe ❤︎ open source!", "%cWe ❤︎ open source!",
@@ -61,6 +58,8 @@ export default {
} }
}) })
} }
setupLocalPersistence()
}, },
beforeDestroy() { beforeDestroy() {
document.removeEventListener("keydown", this._keyListener) 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: [
"~/plugins/vuex-persist", "~/plugins/vuex-persist",
"~/plugins/v-tooltip", "~/plugins/v-tooltip",
"~/plugins/vue-rx",
{ src: "~/plugins/web-worker", ssr: false }, { src: "~/plugins/web-worker", ssr: false },
], ],
@@ -112,6 +113,8 @@ export default {
"@nuxtjs/color-mode", "@nuxtjs/color-mode",
// https: //github.com/nuxt-community/google-fonts-module // https: //github.com/nuxt-community/google-fonts-module
"@nuxtjs/google-fonts", "@nuxtjs/google-fonts",
// https://github.com/nuxt/typescript
"@nuxt/typescript-build",
], ],
// Modules (https://go.nuxtjs.dev/config-modules) // 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", "firebase": "^8.3.1",
"graphql": "^15.5.0", "graphql": "^15.5.0",
"graphql-language-service-interface": "^2.8.2", "graphql-language-service-interface": "^2.8.2",
"lodash": "^4.17.20",
"mustache": "^4.1.0", "mustache": "^4.1.0",
"nuxt": "^2.15.3", "nuxt": "^2.15.3",
"nuxt-i18n": "^6.21.1", "nuxt-i18n": "^6.21.1",
"paho-mqtt": "^1.1.0", "paho-mqtt": "^1.1.0",
"rxjs": "^6.6.3",
"socket.io-client": "^4.0.0", "socket.io-client": "^4.0.0",
"socketio-wildcard": "^2.0.0", "socketio-wildcard": "^2.0.0",
"tern": "^0.24.3", "tern": "^0.24.3",
"v-tooltip": "^2.1.3", "v-tooltip": "^2.1.3",
"vue-rx": "^6.2.0",
"vuejs-auto-complete": "^0.9.0", "vuejs-auto-complete": "^0.9.0",
"vuex-persist": "^3.1.3", "vuex-persist": "^3.1.3",
"yargs-parser": "^20.2.7" "yargs-parser": "^20.2.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.13.10", "@babel/core": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/preset-env": "^7.13.10", "@babel/preset-env": "^7.13.10",
"@nuxt/types": "^2.15.3",
"@nuxt/typescript-build": "^2.1.0",
"@nuxtjs/color-mode": "^2.0.5", "@nuxtjs/color-mode": "^2.0.5",
"@nuxtjs/google-analytics": "^2.4.0", "@nuxtjs/google-analytics": "^2.4.0",
"@nuxtjs/google-fonts": "^1.3.0", "@nuxtjs/google-fonts": "^1.3.0",
"@nuxtjs/pwa": "^3.3.5", "@nuxtjs/pwa": "^3.3.5",
"@nuxtjs/tailwindcss": "^4.0.1", "@nuxtjs/tailwindcss": "^4.0.1",
"@testing-library/jest-dom": "^5.11.9", "@testing-library/jest-dom": "^5.11.9",
"@types/lodash": "^4.14.168",
"@vue/test-utils": "^1.1.3", "@vue/test-utils": "^1.1.3",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-jest": "^26.6.3", "babel-jest": "^26.6.3",
"eslint": "^7.22.0", "eslint": "^7.22.0",
"eslint-plugin-vue": "^7.7.0", "eslint-plugin-vue": "^7.7.0",
"firebase-mock": "^2.3.2",
"husky": "^5.2.0", "husky": "^5.2.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"jest-serializer-vue": "^2.0.2", "jest-serializer-vue": "^2.0.2",
@@ -68,11 +74,13 @@
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"sass": "^1.32.8", "sass": "^1.32.8",
"sass-loader": "^10.1.1", "sass-loader": "^10.1.1",
"ts-jest": "^26.5.4",
"vue-jest": "^3.0.7", "vue-jest": "^3.0.7",
"worker-loader": "^3.0.8" "worker-loader": "^3.0.8"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [
"ts",
"js", "js",
"json", "json",
"vue" "vue"
@@ -98,6 +106,7 @@
"<rootDir>/components/**/*.vue", "<rootDir>/components/**/*.vue",
"<rootDir>/pages/*.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 { commonHeaders } from "~/helpers/headers"
import { getPlatformSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { sendNetworkRequest } from "~/helpers/network" import { sendNetworkRequest } from "~/helpers/network"
import { fb } from "~/helpers/fb" import { getSettingSubject } from "~/newstore/settings"
export default { export default {
data() { data() {
@@ -469,13 +469,11 @@ export default {
activeSidebar: true, activeSidebar: true,
editRequest: {}, editRequest: {},
showSaveRequestModal: false, 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: { watch: {
@@ -681,7 +679,7 @@ export default {
const rootTypeName = this.resolveRootType(type).name const rootTypeName = this.resolveRootType(type).name
const target = document.getElementById(`type_${rootTypeName}`) const target = document.getElementById(`type_${rootTypeName}`)
if (target && this.settings.SCROLL_INTO_ENABLED) { if (target && this.SCROLL_INTO_ENABLED) {
this.$refs.gqlTabs.$el this.$refs.gqlTabs.$el
.querySelector(".gqlTabs") .querySelector(".gqlTabs")
.scrollTo({ top: target.offsetTop, behavior: "smooth" }) .scrollTo({ top: target.offsetTop, behavior: "smooth" })
@@ -739,7 +737,7 @@ export default {
this.$nuxt.$loading.start() this.$nuxt.$loading.start()
this.response = this.$t("loading") this.response = this.$t("loading")
if (this.settings.SCROLL_INTO_ENABLED) this.scrollInto("response") if (this.SCROLL_INTO_ENABLED) this.scrollInto("response")
try { try {
let headers = {} let headers = {}
@@ -769,7 +767,7 @@ export default {
star: false, star: false,
headers: this.headers, 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 // HACK: Temporary trailing null character issue from the extension fix
const responseText = new TextDecoder("utf-8").decode(res.data).replace(/\0+$/, "") const responseText = new TextDecoder("utf-8").decode(res.data).replace(/\0+$/, "")
@@ -946,7 +944,7 @@ export default {
this.$nuxt.$loading.start() this.$nuxt.$loading.start()
this.schema = this.$t("loading") this.schema = this.$t("loading")
if (this.settings.SCROLL_INTO_ENABLED) this.scrollInto("schema") if (this.SCROLL_INTO_ENABLED) this.scrollInto("schema")
try { try {
const query = JSON.stringify({ const query = JSON.stringify({

View File

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

View File

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