Merge pull request #1367 from hoppscotch/feat/gist

Collections and environments + GitHub gist
This commit is contained in:
Andrew Bastin
2020-12-08 23:08:24 -05:00
committed by GitHub
6 changed files with 261 additions and 12 deletions

View File

@@ -6,6 +6,44 @@
<div class="row-wrapper"> <div class="row-wrapper">
<h3 class="title">{{ $t("import_export") }} {{ $t("collections") }}</h3> <h3 class="title">{{ $t("import_export") }} {{ $t("collections") }}</h3>
<div> <div>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button class="icon" @click="readCollectionGist" v-close-popover>
<i class="material-icons">code</i>
<span>{{ $t("import_from_gist") }}</span>
</button>
</div>
<div
v-tooltip.bottom="{
content: !fb.currentUser
? $t('login_with_github_to') + $t('create_secret_gist')
: fb.currentUser.provider !== 'github.com'
? $t('login_with_github_to') + $t('create_secret_gist')
: null,
}"
>
<button
:disabled="
!fb.currentUser
? true
: fb.currentUser.provider !== 'github.com'
? true
: false
"
class="icon"
@click="createCollectionGist"
v-close-popover
>
<i class="material-icons">code</i>
<span>{{ $t("create_secret_gist") }}</span>
</button>
</div>
</template>
</v-popover>
<button class="icon" @click="hideModal"> <button class="icon" @click="hideModal">
<i class="material-icons">close</i> <i class="material-icons">close</i>
</button> </button>
@@ -93,6 +131,57 @@ export default {
}, },
}, },
methods: { methods: {
async createCollectionGist() {
await this.$axios
.$post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-collections.json": {
content: this.collectionJson,
},
},
},
{
headers: {
Authorization: `token ${fb.currentUser.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
)
.then((response) => {
this.$toast.success(this.$t("gist_created"), {
icon: "done",
})
window.open(response.html_url)
})
.catch((error) => {
this.$toast.error(this.$t("something_went_wrong"), {
icon: "error",
})
console.log(error)
})
},
async readCollectionGist() {
let gist = prompt(this.$t("enter_gist_url"))
if (!gist) return
await this.$axios
.$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
headers: {
Accept: "application/vnd.github.v3+json",
},
})
.then((response) => {
let collections = JSON.parse(Object.values(response.files)[0].content)
this.$store.commit("postwoman/replaceCollections", collections)
this.fileImported()
this.syncToFBCollections()
})
.catch((error) => {
this.failedImport()
console.log(error)
})
},
hideModal() { hideModal() {
this.$emit("hide-modal") this.$emit("hide-modal")
}, },

View File

@@ -6,6 +6,44 @@
<div class="row-wrapper"> <div class="row-wrapper">
<h3 class="title">{{ $t("import_export") }} {{ $t("environments") }}</h3> <h3 class="title">{{ $t("import_export") }} {{ $t("environments") }}</h3>
<div> <div>
<v-popover>
<button class="tooltip-target icon" v-tooltip.left="$t('more')">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button class="icon" @click="readEnvironmentGist" v-close-popover>
<i class="material-icons">code</i>
<span>{{ $t("import_from_gist") }}</span>
</button>
</div>
<div
v-tooltip.bottom="{
content: !fb.currentUser
? $t('login_with_github_to') + $t('create_secret_gist')
: fb.currentUser.provider !== 'github.com'
? $t('login_with_github_to') + $t('create_secret_gist')
: null,
}"
>
<button
:disabled="
!fb.currentUser
? true
: fb.currentUser.provider !== 'github.com'
? true
: false
"
class="icon"
@click="createEnvironmentGist"
v-close-popover
>
<i class="material-icons">code</i>
<span>{{ $t("create_secret_gist") }}</span>
</button>
</div>
</template>
</v-popover>
<button class="icon" @click="hideModal"> <button class="icon" @click="hideModal">
<i class="material-icons">close</i> <i class="material-icons">close</i>
</button> </button>
@@ -93,6 +131,57 @@ export default {
}, },
}, },
methods: { methods: {
async createEnvironmentGist() {
await this.$axios
.$post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-environments.json": {
content: this.environmentJson,
},
},
},
{
headers: {
Authorization: `token ${fb.currentUser.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
)
.then((response) => {
this.$toast.success(this.$t("gist_created"), {
icon: "done",
})
window.open(response.html_url)
})
.catch((error) => {
this.$toast.error(this.$t("something_went_wrong"), {
icon: "error",
})
console.log(error)
})
},
async readEnvironmentGist() {
let gist = prompt(this.$t("enter_gist_url"))
if (!gist) return
await this.$axios
.$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
headers: {
Accept: "application/vnd.github.v3+json",
},
})
.then((response) => {
let environments = JSON.parse(Object.values(response.files)[0].content)
this.$store.commit("postwoman/replaceEnvironments", environments)
this.fileImported()
this.syncToFBEnvironments()
})
.catch((error) => {
this.failedImport()
console.log(error)
})
},
hideModal() { hideModal() {
this.$emit("hide-modal") this.$emit("hide-modal")
}, },

View File

@@ -62,6 +62,7 @@ export default {
this.showLoginSuccess() this.showLoginSuccess()
} catch (err) { } catch (err) {
console.log(err)
// An error happened. // An error happened.
if (err.code === "auth/account-exists-with-different-credential") { if (err.code === "auth/account-exists-with-different-credential") {
// Step 2. // Step 2.
@@ -80,22 +81,23 @@ export default {
// Asks the user their password. // Asks the user their password.
// In real scenario, you should handle this asynchronously. // In real scenario, you should handle this asynchronously.
const password = promptUserForPassword() // TODO: implement promptUserForPassword. const password = promptUserForPassword() // TODO: implement promptUserForPassword.
const user = await fb.signInWithEmailAndPassword(email, password)
const user = await fb.signInWithEmailAndPassword(email, password)
await user.linkWithCredential(pendingCred) await user.linkWithCredential(pendingCred)
this.showLoginSuccess() this.showLoginSuccess()
return return
} }
this.$toast.info(`${this.$t("login_with")}`, { this.$toast.info(`${this.$t("account_exists")}`, {
icon: "vpn_key", icon: "vpn_key",
duration: null, duration: null,
closeOnSwipe: false, closeOnSwipe: false,
action: { action: {
text: this.$t("yes"), text: this.$t("yes"),
onClick: async (e, toastObject) => { onClick: async (e, toastObject) => {
const user = await fb.signInWithGithub() const { user } = await fb.signInWithGithub()
await user.linkAndRetrieveDataWithCredential(pendingCred) await user.linkAndRetrieveDataWithCredential(pendingCred)
this.showLoginSuccess() this.showLoginSuccess()
@@ -109,7 +111,9 @@ export default {
}, },
async signInWithGithub() { async signInWithGithub() {
try { try {
const { additionalUserInfo } = await fb.signInUserWithGithub() const { credential, additionalUserInfo } = await fb.signInUserWithGithub()
fb.setProviderInfo(credential.providerId, credential.accessToken)
if (additionalUserInfo.isNewUser) { if (additionalUserInfo.isNewUser) {
this.$toast.info(`${this.$t("turn_on")} ${this.$t("sync")}`, { this.$toast.info(`${this.$t("turn_on")} ${this.$t("sync")}`, {
@@ -131,6 +135,7 @@ export default {
this.showLoginSuccess() this.showLoginSuccess()
} catch (err) { } catch (err) {
console.log(err)
// An error happened. // An error happened.
if (err.code === "auth/account-exists-with-different-credential") { if (err.code === "auth/account-exists-with-different-credential") {
// Step 2. // Step 2.
@@ -158,7 +163,7 @@ export default {
return return
} }
this.$toast.info(`${this.$t("login_with")}`, { this.$toast.info(`${this.$t("account_exists")}`, {
icon: "vpn_key", icon: "vpn_key",
duration: null, duration: null,
closeOnSwipe: false, closeOnSwipe: false,

View File

@@ -205,7 +205,11 @@ describe("FirebaseInstance", () => {
const fbFunc = jest.spyOn(mockAuth, "signInWithPopup") const fbFunc = jest.spyOn(mockAuth, "signInWithPopup")
const fb = new FirebaseInstance(mocksdk, { const fb = new FirebaseInstance(mocksdk, {
github: () => {}, github: () => {
return {
addScope: () => {}
}
},
}) })
signOutUser() signOutUser()
@@ -218,7 +222,11 @@ describe("FirebaseInstance", () => {
const fbFunc = jest.spyOn(mockAuth, "signInWithPopup") const fbFunc = jest.spyOn(mockAuth, "signInWithPopup")
const fb = new FirebaseInstance(mocksdk, { const fb = new FirebaseInstance(mocksdk, {
github: () => {}, github: () => {
return {
addScope: () => {}
}
},
}) })
signOutUser() signOutUser()
@@ -231,7 +239,11 @@ describe("FirebaseInstance", () => {
const fbFunc = jest.spyOn(mockAuth, "signInWithPopup") const fbFunc = jest.spyOn(mockAuth, "signInWithPopup")
const fb = new FirebaseInstance(mocksdk, { const fb = new FirebaseInstance(mocksdk, {
github: () => {}, github: () => {
return {
addScope: () => {}
}
},
}) })
signOutUser() signOutUser()
@@ -240,11 +252,35 @@ describe("FirebaseInstance", () => {
await expect(fb.signInUserWithGithub()).rejects.toEqual("test error") await expect(fb.signInUserWithGithub()).rejects.toEqual("test error")
}) })
test("adds 'repo gist' scope", async () => {
const fbFunc = jest.spyOn(mockAuth, "signInWithPopup")
const addScopeMock = jest.fn()
const fb = new FirebaseInstance(mocksdk, {
github: () => {
return {
addScope: addScopeMock
}
},
})
signOutUser()
fbFunc.mockImplementation(() => Promise.resolve("test"))
await fb.signInUserWithGithub()
expect(addScopeMock).toBeCalledWith("repo gist")
})
test("resolves the response the firebase request resolves", async () => { test("resolves the response the firebase request resolves", async () => {
const fbFunc = jest.spyOn(mockAuth, "signInWithPopup") const fbFunc = jest.spyOn(mockAuth, "signInWithPopup")
const fb = new FirebaseInstance(mocksdk, { const fb = new FirebaseInstance(mocksdk, {
github: () => {}, github: () => {
return {
addScope: () => {}
}
},
}) })
signOutUser() signOutUser()

View File

@@ -36,6 +36,7 @@ export class FirebaseInstance {
this.app.auth().onAuthStateChanged((user) => { this.app.auth().onAuthStateChanged((user) => {
if (user) { if (user) {
this.currentUser = user this.currentUser = user
this.currentUser.providerData.forEach((profile) => { this.currentUser.providerData.forEach((profile) => {
let us = { let us = {
updatedOn: new Date(), updatedOn: new Date(),
@@ -47,10 +48,15 @@ export class FirebaseInstance {
} }
this.usersCollection this.usersCollection
.doc(this.currentUser.uid) .doc(this.currentUser.uid)
.set(us) .set(us, { merge: true })
.catch((e) => console.error("error updating", us, e)) .catch((e) => console.error("error updating", us, e))
}) })
this.usersCollection.doc(this.currentUser.uid).onSnapshot((doc) => {
this.currentUser.provider = doc.data().provider
this.currentUser.accessToken = doc.data().accessToken
})
this.usersCollection this.usersCollection
.doc(this.currentUser.uid) .doc(this.currentUser.uid)
.collection("feeds") .collection("feeds")
@@ -131,7 +137,7 @@ export class FirebaseInstance {
} }
async signInUserWithGithub() { async signInUserWithGithub() {
return await this.app.auth().signInWithPopup(this.authProviders.github()) return await this.app.auth().signInWithPopup(this.authProviders.github().addScope("repo gist"))
} }
async signInWithEmailAndPassword(email, password) { async signInWithEmailAndPassword(email, password) {
@@ -288,6 +294,24 @@ export class FirebaseInstance {
throw e throw e
} }
} }
async setProviderInfo(id, token) {
const us = {
updatedOn: new Date(),
provider: id,
accessToken: token,
}
try {
await this.usersCollection
.doc(this.currentUser.uid)
.update(us)
.catch((e) => console.error("error updating", us, e))
} catch (e) {
console.error("error updating", ev, e)
throw e
}
}
} }
export const fb = new FirebaseInstance(firebase.initializeApp(firebaseConfig), authProviders) export const fb = new FirebaseInstance(firebase.initializeApp(firebaseConfig), authProviders)

View File

@@ -294,5 +294,11 @@
"experiments": "Experiments", "experiments": "Experiments",
"experiments_notice": "This is a collection of experiments we're working on that might turn out to be useful, fun, both, or neither. They're not final and may not be stable, so if something overly weird happens, don't panic. Just turn the dang thing off. Jokes aside, ", "experiments_notice": "This is a collection of experiments we're working on that might turn out to be useful, fun, both, or neither. They're not final and may not be stable, so if something overly weird happens, don't panic. Just turn the dang thing off. Jokes aside, ",
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting", "use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
"select_environment": "Select environment" "select_environment": "Select environment",
"login_with_github_to": "Login with GitHub to ",
"create_secret_gist": "Create secret Gist",
"gist_created": "Gist created",
"import_from_gist": "Import from Gist",
"enter_gist_url": "Enter Gist URL",
"account_exists": "Account exists with different credential - Login to link both accounts"
} }