Cherry picking refactored fb.js from #879 by @AndrewBastin (#1286)

* Cherry picking refactored fb.js from #879 by @AndrewBastin

* Fixed a minor UI glitch in History section

* Removed logout success toast testcase

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
Liyas Thomas
2020-10-17 14:53:19 +05:30
committed by GitHub
parent b6b3cbcb9a
commit 22a3bba6ab
13 changed files with 2408 additions and 473 deletions

1
__mocks__/svgMock.js Normal file
View File

@@ -0,0 +1 @@
export default {}

View File

@@ -293,6 +293,7 @@ hr {
button { button {
@apply justify-start; @apply justify-start;
@apply text-left;
} }
} }

View File

@@ -0,0 +1,133 @@
import feeds from "../feeds"
import { shallowMount } from "@vue/test-utils"
jest.mock("~/helpers/fb", () => {
return {
__esModule: true,
fb: {
currentFeeds: [
{
id: "test1",
label: "First",
message: "First Message",
},
{
id: "test2",
label: "Second",
},
{
id: "test3",
message: "Third Message",
},
{
id: "test4",
},
],
deleteFeed: jest.fn(() => Promise.resolve()),
},
}
})
const { fb } = require("~/helpers/fb")
const factory = () =>
shallowMount(feeds, {
mocks: {
$t: (text) => text,
$toast: {
error: jest.fn(),
},
},
})
beforeEach(() => {
fb.deleteFeed.mockClear()
})
describe("feeds", () => {
test("mounts properly when proper components are given", () => {
const wrapper = factory()
expect(wrapper).toBeTruthy()
})
test("renders all the current feeds", () => {
const wrapper = factory()
expect(wrapper.findAll("div[data-test='list-item']").wrappers).toHaveLength(4)
})
test("feeds with no label displays the 'no_label' message", () => {
const wrapper = factory()
expect(
wrapper
.findAll("label[data-test='list-label']")
.wrappers.map((e) => e.text())
.filter((text) => text == "no_label")
).toHaveLength(2)
})
test("feeds with no message displays the 'empty' message", () => {
const wrapper = factory()
expect(
wrapper
.findAll("li[data-test='list-message']")
.wrappers.map((e) => e.text())
.filter((text) => text == "empty")
).toHaveLength(2)
})
test("labels in the list are proper", () => {
const wrapper = factory()
expect(wrapper.findAll("label[data-test='list-label']").wrappers.map((e) => e.text())).toEqual([
"First",
"Second",
"no_label",
"no_label",
])
})
test("messages in the list are proper", () => {
const wrapper = factory()
expect(wrapper.findAll("li[data-test='list-message']").wrappers.map((e) => e.text())).toEqual([
"First Message",
"empty",
"Third Message",
"empty",
])
})
test("clicking on the delete button deletes the feed", async () => {
const wrapper = factory()
const deleteButton = wrapper.find("button")
await deleteButton.trigger("click")
expect(fb.deleteFeed).toHaveBeenCalledTimes(1)
})
test("correct feed is passed to from the list for deletion", async () => {
const wrapper = factory()
const deleteButton = wrapper.find("button")
await deleteButton.trigger("click")
expect(fb.deleteFeed).toHaveBeenCalledWith("test1")
})
test("renders the 'empty' label if no elements in the current feeds", () => {
jest.spyOn(fb, "currentFeeds", "get").mockReturnValueOnce([])
const wrapper = factory()
expect(wrapper.findAll("li").wrappers).toHaveLength(1)
expect(wrapper.find("li").text()).toEqual("empty")
})
})

View File

@@ -0,0 +1,93 @@
import inputform from "../inputform"
import { shallowMount } from "@vue/test-utils"
jest.mock("~/helpers/fb", () => {
return {
__esModule: true,
fb: {
writeFeeds: jest.fn(() => Promise.resolve()),
},
}
})
const { fb } = require("~/helpers/fb")
const factory = () =>
shallowMount(inputform, {
mocks: {
$t: (text) => text,
},
})
beforeEach(() => {
fb.writeFeeds.mockClear()
})
describe("inputform", () => {
test("mounts properly", () => {
const wrapper = factory()
expect(wrapper).toBeTruthy()
})
test("calls writeFeeds when submitted properly", async () => {
const wrapper = factory()
const addButton = wrapper.find("button")
const [messageInput, labelInput] = wrapper.findAll("input").wrappers
await messageInput.setValue("test message")
await labelInput.setValue("test label")
await addButton.trigger("click")
expect(fb.writeFeeds).toHaveBeenCalledTimes(1)
})
test("doesn't call writeFeeds when submitted without a data", async () => {
const wrapper = factory()
const addButton = wrapper.find("button")
await addButton.trigger("click")
expect(fb.writeFeeds).not.toHaveBeenCalled()
})
test("doesn't call writeFeeds when message or label is null", async () => {
const wrapper = factory()
const addButton = wrapper.find("button")
const [messageInput, labelInput] = wrapper.findAll("input").wrappers
await messageInput.setValue(null)
await labelInput.setValue(null)
await addButton.trigger("click")
expect(fb.writeFeeds).not.toHaveBeenCalled()
})
test("doesn't call writeFeeds when message or label is empty", async () => {
const wrapper = factory()
const addButton = wrapper.find("button")
const [messageInput, labelInput] = wrapper.findAll("input").wrappers
await messageInput.setValue("")
await labelInput.setValue("")
await addButton.trigger("click")
expect(fb.writeFeeds).not.toHaveBeenCalled()
})
test("calls writeFeeds with correct values", async () => {
const wrapper = factory()
const addButton = wrapper.find("button")
const [messageInput, labelInput] = wrapper.findAll("input").wrappers
await messageInput.setValue("test message")
await labelInput.setValue("test label")
await addButton.trigger("click")
expect(fb.writeFeeds).toHaveBeenCalledWith("test message", "test label")
})
})

View File

@@ -0,0 +1,66 @@
import logout from "../logout"
import { shallowMount, createLocalVue } from "@vue/test-utils"
jest.mock("~/helpers/fb", () => {
return {
__esModule: true,
fb: {
signOutUser: jest.fn(() => Promise.resolve()),
},
}
})
const { fb } = require("~/helpers/fb")
const $toast = {
info: jest.fn(),
show: jest.fn(),
}
const localVue = createLocalVue()
localVue.directive("close-popover", {})
const factory = () =>
shallowMount(logout, {
mocks: {
$t: (text) => text,
$toast,
},
localVue,
})
beforeEach(() => {
fb.signOutUser.mockClear()
$toast.info.mockClear()
$toast.show.mockClear()
})
describe("logout", () => {
test("mounts properly", () => {
const wrapper = factory()
expect(wrapper).toBeTruthy()
})
test("clicking the logout button fires the logout firebase function", async () => {
const wrapper = factory()
const button = wrapper.find("button")
await button.trigger("click")
expect(fb.signOutUser).toHaveBeenCalledTimes(1)
})
test("failed signout request fires a error toast", async () => {
fb.signOutUser.mockImplementationOnce(() => Promise.reject("test reject"))
const wrapper = factory()
const button = wrapper.find("button")
await button.trigger("click")
expect($toast.show).toHaveBeenCalledTimes(1)
expect($toast.show).toHaveBeenCalledWith("test reject", expect.anything())
})
})

View File

@@ -5,9 +5,9 @@
:key="feed.id" :key="feed.id"
class="flex-col py-2 border-b border-dashed border-brdColor" class="flex-col py-2 border-b border-dashed border-brdColor"
> >
<div class="show-on-large-screen"> <div data-test="list-item" class="show-on-large-screen">
<li class="info"> <li class="info">
<label> <label data-test="list-label">
{{ feed.label || $t("no_label") }} {{ feed.label || $t("no_label") }}
</label> </label>
</li> </li>
@@ -16,7 +16,7 @@
</button> </button>
</div> </div>
<div class="show-on-large-screen"> <div class="show-on-large-screen">
<li class="info clamb-3"> <li data-test="list-message" class="info clamb-3">
<label>{{ feed.message || $t("empty") }}</label> <label>{{ feed.message || $t("empty") }}</label>
</li> </li>
</div> </div>
@@ -54,8 +54,8 @@ export default {
} }
}, },
methods: { methods: {
deleteFeed(feed) { async deleteFeed(feed) {
fb.deleteFeed(feed.id) await fb.deleteFeed(feed.id)
this.$toast.error(this.$t("deleted"), { this.$toast.error(this.$t("deleted"), {
icon: "delete", icon: "delete",
}) })

View File

@@ -39,205 +39,144 @@ export default {
icon: "vpn_key", icon: "vpn_key",
}) })
}, },
signInWithGoogle() { async signInWithGoogle() {
const provider = new firebase.auth.GoogleAuthProvider() try {
const self = this const { additionUserInfo } = await fb.signInUserWithGoogle()
firebase
.auth() if (additionalUserInfo.isNewUser) {
.signInWithPopup(provider) this.$toast.info(`${this.$t("turn_on")} ${this.$t("sync")}`, {
.then(({ additionalUserInfo }) => { icon: "sync",
if (additionalUserInfo.isNewUser) { duration: null,
self.$toast.info(`${self.$t("turn_on")} ${self.$t("sync")}`, { closeOnSwipe: false,
icon: "sync", action: {
duration: null, text: this.$t("yes"),
closeOnSwipe: false, onClick: (e, toastObject) => {
action: { fb.writeSettings("syncHistory", true)
text: self.$t("yes"), fb.writeSettings("syncCollections", true)
onClick: (e, toastObject) => { fb.writeSettings("syncEnvironments", true)
fb.writeSettings("syncHistory", true) this.$router.push({ path: "/settings" })
fb.writeSettings("syncCollections", true) toastObject.remove()
fb.writeSettings("syncEnvironments", true)
self.$router.push({ path: "/settings" })
toastObject.remove()
},
}, },
}) },
} })
self.showLoginSuccess() }
})
.catch((err) => {
// An error happened.
if (err.code === "auth/account-exists-with-different-credential") {
// Step 2.
// User's email already exists.
// The pending Google credential.
const pendingCred = err.credential
// The provider account's email address.
const email = err.email
// Get sign-in methods for this email.
firebase
.auth()
.fetchSignInMethodsForEmail(email)
.then((methods) => {
// Step 3.
// If the user has several sign-in methods,
// the first method in the list will be the "recommended" method to use.
if (methods[0] === "password") {
// Asks the user their password.
// In real scenario, you should handle this asynchronously.
const password = promptUserForPassword() // TODO: implement promptUserForPassword.
auth
.signInWithEmailAndPassword(email, password)
.then((
user // Step 4a.
) => user.linkWithCredential(pendingCred))
.then(() => {
// Google account successfully linked to the existing Firebase user.
self.showLoginSuccess()
})
return
}
self.$toast.info(`${self.$t("login_with")}`, { this.showLoginSuccess()
icon: "vpn_key", } catch (err) {
duration: null, // An error happened.
closeOnSwipe: false, if (err.code === "auth/account-exists-with-different-credential") {
action: { // Step 2.
text: self.$t("yes"), // User's email already exists.
onClick: (e, toastObject) => { // The pending Google credential.
// All the other cases are external providers. const pendingCred = err.credential
// Construct provider object for that provider. // The provider account's email address.
// TODO: implement getProviderForProviderId. const email = err.email
const provider = new firebase.auth.GithubAuthProvider() // Get sign-in methods for this email.
// At this point, you should let the user know that they already has an account const methods = await fb.getSignInMethodsForEmail(email)
// but with a different provider, and let them validate the fact they want to
// sign in with this provider.
// Sign in to provider. Note: browsers usually block popup triggered asynchronously,
// so in real scenario you should ask the user to click on a "continue" button
// that will trigger the signInWithPopup.
firebase
.auth()
.signInWithPopup(provider)
.then(({ user }) => {
// Remember that the user may have signed in with an account that has a different email
// address than the first one. This can happen as Firebase doesn't control the provider's
// sign in flow and the user is free to login using whichever account they own.
// Step 4b.
// Link to Google credential.
// As we have access to the pending credential, we can directly call the link method.
user.linkAndRetrieveDataWithCredential(pendingCred).then((usercred) => {
// Google account successfully linked to the existing Firebase user.
self.showLoginSuccess()
})
})
toastObject.remove() // Step 3.
}, // If the user has several sign-in methods,
}, // the first method in the list will be the "recommended" method to use.
}) if (methods[0] === "password") {
}) // Asks the user their password.
// In real scenario, you should handle this asynchronously.
const password = promptUserForPassword() // TODO: implement promptUserForPassword.
const user = await fb.signInWithEmailAndPassword(email, password)
await user.linkWithCredential(pendingCred)
this.showLoginSuccess()
return
} }
})
this.$toast.info(`${this.$t("login_with")}`, {
icon: "vpn_key",
duration: null,
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: async (e, toastObject) => {
const user = await fb.signInWithGithub()
await user.linkAndRetrieveDataWithCredential(pendingCred)
this.showLoginSuccess()
toastObject.remove()
},
},
})
}
}
}, },
signInWithGithub() { async signInWithGithub() {
const provider = new firebase.auth.GithubAuthProvider() try {
const self = this const { additionalUserInfo } = await fb.signInUserWithGithub()
firebase
.auth() if (additionalUserInfo.isNewUser) {
.signInWithPopup(provider) this.$toast.info(`${this.$t("turn_on")} ${this.$t("sync")}`, {
.then(({ additionalUserInfo }) => { icon: "sync",
if (additionalUserInfo.isNewUser) { duration: null,
self.$toast.info(`${self.$t("turn_on")} ${self.$t("sync")}`, { closeOnSwipe: false,
icon: "sync", action: {
duration: null, text: this.$t("yes"),
closeOnSwipe: false, onClick: (e, toastObject) => {
action: { fb.writeSettings("syncHistory", true)
text: self.$t("yes"), fb.writeSettings("syncCollections", true)
onClick: (e, toastObject) => { fb.writeSettings("syncEnvironments", true)
fb.writeSettings("syncHistory", true) this.$router.push({ path: "/settings" })
fb.writeSettings("syncCollections", true) toastObject.remove()
fb.writeSettings("syncEnvironments", true)
self.$router.push({ path: "/settings" })
toastObject.remove()
},
}, },
}) },
} })
self.showLoginSuccess() }
})
.catch((err) => {
// An error happened.
if (err.code === "auth/account-exists-with-different-credential") {
// Step 2.
// User's email already exists.
// The pending Google credential.
const pendingCred = err.credential
// The provider account's email address.
const email = err.email
// Get sign-in methods for this email.
firebase
.auth()
.fetchSignInMethodsForEmail(email)
.then((methods) => {
// Step 3.
// If the user has several sign-in methods,
// the first method in the list will be the "recommended" method to use.
if (methods[0] === "password") {
// Asks the user their password.
// In real scenario, you should handle this asynchronously.
const password = promptUserForPassword() // TODO: implement promptUserForPassword.
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((
user // Step 4a.
) => user.linkWithCredential(pendingCred))
.then(() => {
// Google account successfully linked to the existing Firebase user.
self.showLoginSuccess()
})
return
}
self.$toast.info(`${self.$t("login_with")}`, { this.showLoginSuccess()
icon: "vpn_key", } catch (err) {
duration: null, // An error happened.
closeOnSwipe: false, if (err.code === "auth/account-exists-with-different-credential") {
action: { // Step 2.
text: self.$t("yes"), // User's email already exists.
onClick: (e, toastObject) => { // The pending Google credential.
// All the other cases are external providers. const pendingCred = err.credential
// Construct provider object for that provider. // The provider account's email address.
// TODO: implement getProviderForProviderId. const email = err.email
const provider = new firebase.auth.GoogleAuthProvider() // Get sign-in methods for this email.
// At this point, you should let the user know that they already has an account const methods = await fb.getSignInMethodsForEmail(email)
// but with a different provider, and let them validate the fact they want to
// sign in with this provider.
// Sign in to provider. Note: browsers usually block popup triggered asynchronously,
// so in real scenario you should ask the user to click on a "continue" button
// that will trigger the signInWithPopup.
firebase
.auth()
.signInWithPopup(provider)
.then(({ user }) => {
// Remember that the user may have signed in with an account that has a different email
// address than the first one. This can happen as Firebase doesn't control the provider's
// sign in flow and the user is free to login using whichever account they own.
// Step 4b.
// Link to Google credential.
// As we have access to the pending credential, we can directly call the link method.
user.linkAndRetrieveDataWithCredential(pendingCred).then((usercred) => {
self.showLoginSuccess()
})
})
toastObject.remove() // Step 3.
}, // If the user has several sign-in methods,
}, // the first method in the list will be the "recommended" method to use.
}) if (methods[0] === "password") {
}) // Asks the user their password.
// In real scenario, you should handle this asynchronously.
const password = promptUserForPassword() // TODO: implement promptUserForPassword.
const user = await fb.signInWithEmailAndPassword(email, password)
await user.linkWithCredential(pendingCred)
this.showLoginSuccess()
return
} }
})
this.$toast.info(`${this.$t("login_with")}`, {
icon: "vpn_key",
duration: null,
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: async (e, toastObject) => {
const { user } = await fb.signInUserWithGoogle()
await user.linkAndRetrieveDataWithCredential(pendingCred)
this.showLoginSuccess()
toastObject.remove()
},
},
})
}
}
}, },
}, },
} }

View File

@@ -8,7 +8,6 @@
</template> </template>
<script> <script>
import firebase from "firebase/app"
import { fb } from "~/helpers/fb" import { fb } from "~/helpers/fb"
import exitToAppIcon from "~/static/icons/exit_to_app-24px.svg?inline" import exitToAppIcon from "~/static/icons/exit_to_app-24px.svg?inline"
@@ -20,20 +19,18 @@ export default {
} }
}, },
methods: { methods: {
logout() { async logout() {
fb.currentUser = null try {
const self = this await fb.signOutUser()
firebase
.auth() this.$toast.info(this.$t("logged_out"), {
.signOut() icon: "vpn_key",
.catch((err) => {
self.$toast.show(err.message || err, {
icon: "error",
})
}) })
self.$toast.info(this.$t("logged_out"), { } catch (err) {
icon: "vpn_key", this.$toast.show(err.message || err, {
}) icon: "error",
})
}
}, },
}, },
} }

View File

@@ -2,26 +2,13 @@
<pw-section class="green" icon="history" :label="$t('history')" ref="history"> <pw-section class="green" icon="history" :label="$t('history')" ref="history">
<div class="show-on-large-screen"> <div class="show-on-large-screen">
<input aria-label="Search" type="search" :placeholder="$t('search')" v-model="filterText" /> <input aria-label="Search" type="search" :placeholder="$t('search')" v-model="filterText" />
<!-- <button class="icon"> <button class="icon">
<i class="material-icons">search</i> <i class="material-icons">search</i>
</button> --> </button>
</div> </div>
<div class="virtual-list" :class="{ filled: filteredHistory.length }"> <div class="virtual-list" :class="{ filled: filteredHistory.length }">
<ul <ul v-for="(entry, index) in filteredHistory" :key="index" class="entry">
v-for="(entry, index) in filteredHistory"
:key="index"
class="flex-col border-b border-dashed border-brdColor"
>
<div class="show-on-large-screen"> <div class="show-on-large-screen">
<button
class="icon"
:id="'use-button#' + index"
@click="useHistory(entry)"
:aria-label="$t('edit')"
v-tooltip="$t('restore')"
>
<i class="material-icons">restore</i>
</button>
<button <button
class="icon" class="icon"
:class="{ stared: entry.star }" :class="{ stared: entry.star }"
@@ -34,46 +21,16 @@
{{ entry.star ? "star" : "star_border" }} {{ entry.star ? "star" : "star_border" }}
</i> </i>
</button> </button>
<li class="relative"> <li>
<input
:aria-label="$t('method')"
type="text"
readonly
:value="`${entry.method} ${entry.status}`"
:class="findEntryStatus(entry).className"
:style="{ '--status-code': entry.status }"
class="bg-transparent"
/>
</li>
<v-popover>
<button class="tooltip-target icon" v-tooltip="$t('options')">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
class="icon"
:id="'delete-button#' + index"
@click="deleteHistory(entry)"
:aria-label="$t('delete')"
v-close-popover
>
<deleteIcon class="material-icons" />
<span>{{ $t("delete") }}</span>
</button>
</div>
</template>
</v-popover>
<!-- <li class="relative">
<input <input
:aria-label="$t('label')" :aria-label="$t('label')"
type="text" type="text"
readonly readonly
:value="entry.label" :value="entry.label"
:placeholder="$t('no_label')" :placeholder="$t('no_label')"
class="bg-transparent" class="bg-color"
/> />
</li> --> </li>
<!-- <li> <!-- <li>
<button <button
class="icon" class="icon"
@@ -88,21 +45,67 @@
</i> </i>
</button> </button>
</li> --> </li> -->
<v-popover>
<button class="tooltip-target icon" v-tooltip="$t('options')">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
class="icon"
:id="'use-button#' + index"
@click="useHistory(entry)"
:aria-label="$t('edit')"
v-close-popover
>
<i class="material-icons">restore</i>
<span>{{ $t("restore") }}</span>
</button>
</div>
<div>
<button
class="icon"
:id="'delete-button#' + index"
@click="deleteHistory(entry)"
:aria-label="$t('delete')"
v-close-popover
>
<deleteIcon class="material-icons" />
<span>{{ $t("delete") }}</span>
</button>
</div>
</template>
</v-popover>
</div>
<div class="show-on-large-screen">
<li class="method-list-item">
<input
:aria-label="$t('method')"
type="text"
readonly
:value="entry.method"
:class="findEntryStatus(entry).className"
:style="{ '--status-code': entry.status }"
/>
<span
class="entry-status-code"
:class="findEntryStatus(entry).className"
:style="{ '--status-code': entry.status }"
>{{ entry.status }}</span
>
</li>
</div> </div>
<!-- <div class="show-on-large-screen">
</div> -->
<div class="show-on-large-screen"> <div class="show-on-large-screen">
<li> <li>
<input <input
:aria-label="$t('url')" :aria-label="$t('url')"
type="text" type="text"
readonly readonly
:value="`${entry.url}${entry.path}`" :value="entry.url"
:placeholder="$t('no_url')" :placeholder="$t('no_url')"
class="bg-transparent"
/> />
</li> </li>
<!-- <li> <li>
<input <input
:aria-label="$t('path')" :aria-label="$t('path')"
type="text" type="text"
@@ -110,7 +113,7 @@
:value="entry.path" :value="entry.path"
:placeholder="$t('no_path')" :placeholder="$t('no_path')"
/> />
</li> --> </li>
</div> </div>
<transition name="fade"> <transition name="fade">
<div v-if="showMore" class="show-on-large-screen"> <div v-if="showMore" class="show-on-large-screen">
@@ -145,7 +148,7 @@
</transition> </transition>
</ul> </ul>
</div> </div>
<ul class="flex-col" :class="{ hidden: filteredHistory.length != 0 || history.length === 0 }"> <ul :class="{ hidden: filteredHistory.length != 0 || history.length === 0 }">
<li> <li>
<label>{{ $t("nothing_found") }} "{{ filterText }}"</label> <label>{{ $t("nothing_found") }} "{{ filterText }}"</label>
</li> </li>
@@ -245,27 +248,50 @@
<style scoped lang="scss"> <style scoped lang="scss">
.virtual-list { .virtual-list {
max-height: calc(100vh - 288px); max-height: calc(100vh - 290px);
[readonly] { [readonly] {
@apply cursor-default; cursor: default;
} }
} }
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
@apply transition; transition: all 0.2s;
@apply ease-in-out;
@apply duration-200;
} }
.fade-enter, .fade-enter,
.fade-leave-to { .fade-leave-to {
@apply opacity-0; opacity: 0;
} }
.stared { .stared {
@apply text-yellow-200; color: #f8e81c !important;
}
ul,
ol {
flex-direction: column;
}
.method-list-item {
position: relative;
span {
position: absolute;
top: 10px;
right: 10px;
font-family: "Roboto Mono", monospace;
font-weight: 400;
background-color: transparent;
padding: 2px 6px;
border-radius: 8px;
}
}
.entry {
border-bottom: 1px dashed var(--brd-color);
padding: 0 0 8px;
} }
@media (max-width: 720px) { @media (max-width: 720px) {
@@ -274,7 +300,7 @@
} }
.labels { .labels {
@apply hidden; display: none;
} }
} }
</style> </style>
@@ -316,22 +342,20 @@ export default {
fb.currentUser !== null fb.currentUser !== null
? fb.currentHistory ? fb.currentHistory
: JSON.parse(window.localStorage.getItem("history")) || [] : JSON.parse(window.localStorage.getItem("history")) || []
return this.history return this.history.filter((entry) => {
.filter((entry) => { const filterText = this.filterText.toLowerCase()
const filterText = this.filterText.toLowerCase() return Object.keys(entry).some((key) => {
return Object.keys(entry).some((key) => { let value = entry[key]
let value = entry[key] value = typeof value !== "string" ? value.toString() : value
value = typeof value !== "string" ? value.toString() : value return value.toLowerCase().includes(filterText)
return value.toLowerCase().includes(filterText)
})
}) })
.reverse() })
}, },
}, },
methods: { methods: {
clearHistory() { async clearHistory() {
if (fb.currentUser !== null) { if (fb.currentUser !== null) {
fb.clearHistory() await fb.clearHistory()
} }
this.history = [] this.history = []
this.filterText = "" this.filterText = ""
@@ -352,9 +376,9 @@ export default {
} }
) )
}, },
deleteHistory(entry) { async deleteHistory(entry) {
if (fb.currentUser !== null) { if (fb.currentUser !== null) {
fb.deleteHistory(entry) await fb.deleteHistory(entry)
} }
this.history.splice(this.history.indexOf(entry), 1) this.history.splice(this.history.indexOf(entry), 1)
if (this.history.length === 0) { if (this.history.length === 0) {
@@ -440,9 +464,9 @@ export default {
toggleCollapse() { toggleCollapse() {
this.showMore = !this.showMore this.showMore = !this.showMore
}, },
toggleStar(entry) { async toggleStar(entry) {
if (fb.currentUser !== null) { if (fb.currentUser !== null) {
fb.toggleStar(entry, !entry.star) await fb.toggleStar(entry, !entry.star)
} }
entry.star = !entry.star entry.star = !entry.star
updateOnLocalStorage("history", this.history) updateOnLocalStorage("history", this.history)

1309
helpers/__tests__/fb.spec.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,213 +13,313 @@ const firebaseConfig = {
appId: process.env.APP_ID || "1:421993993223:web:ec0baa8ee8c02ffa1fc6a2", appId: process.env.APP_ID || "1:421993993223:web:ec0baa8ee8c02ffa1fc6a2",
measurementId: process.env.MEASUREMENT_ID || "G-ERJ6025CEB", measurementId: process.env.MEASUREMENT_ID || "G-ERJ6025CEB",
} }
firebase.initializeApp(firebaseConfig)
// a reference to the users collection export const authProviders = {
const usersCollection = firebase.firestore().collection("users") google: () => new firebase.auth.GoogleAuthProvider(),
github: () => new firebase.auth.GithubAuthProvider(),
}
// the shared state object that any vue component export class FirebaseInstance {
// can get access to constructor(fbapp, authProviders) {
export const fb = { this.app = fbapp
currentUser: null, this.authProviders = authProviders
currentFeeds: [],
currentSettings: [], this.usersCollection = this.app.firestore().collection("users")
currentHistory: [],
currentCollections: [], this.currentUser = null
currentEnvironments: [], this.currentFeeds = []
writeFeeds: async (message, label) => { this.currentSettings = []
this.currentHistory = []
this.currentCollections = []
this.currentEnvironments = []
this.currentTeams = []
this.app.auth().onAuthStateChanged((user) => {
if (user) {
this.currentUser = user
this.currentUser.providerData.forEach((profile) => {
let us = {
updatedOn: new Date(),
provider: profile.providerId,
name: profile.displayName,
email: profile.email,
photoUrl: profile.photoURL,
uid: profile.uid,
}
this.usersCollection
.doc(this.currentUser.uid)
.set(us)
.catch((e) => console.error("error updating", us, e))
})
this.usersCollection
.doc(this.currentUser.uid)
.collection("feeds")
.orderBy("createdOn", "desc")
.onSnapshot((feedsRef) => {
const feeds = []
feedsRef.forEach((doc) => {
const feed = doc.data()
feed.id = doc.id
feeds.push(feed)
})
this.currentFeeds = feeds
})
this.usersCollection
.doc(this.currentUser.uid)
.collection("settings")
.onSnapshot((settingsRef) => {
const settings = []
settingsRef.forEach((doc) => {
const setting = doc.data()
setting.id = doc.id
settings.push(setting)
})
this.currentSettings = settings
})
this.usersCollection
.doc(this.currentUser.uid)
.collection("history")
.onSnapshot((historyRef) => {
const history = []
historyRef.forEach((doc) => {
const entry = doc.data()
entry.id = doc.id
history.push(entry)
})
this.currentHistory = history
})
this.usersCollection
.doc(this.currentUser.uid)
.collection("collections")
.onSnapshot((collectionsRef) => {
const collections = []
collectionsRef.forEach((doc) => {
const collection = doc.data()
collection.id = doc.id
collections.push(collection)
})
if (collections.length > 0) {
this.currentCollections = collections[0].collection
}
})
this.usersCollection
.doc(this.currentUser.uid)
.collection("environments")
.onSnapshot((environmentsRef) => {
const environments = []
environmentsRef.forEach((doc) => {
const environment = doc.data()
environment.id = doc.id
environments.push(environment)
})
if (environments.length > 0) {
this.currentEnvironments = environments[0].environment
}
})
this.usersCollection
.doc(this.currentUser.uid)
.collection("teams")
.onSnapshot((teamsRef) => {
const teams = []
teamsRef.forEach((doc) => {
const team = doc.data()
team.id = doc.id
teams.push(team)
})
this.currentTeams = teams[0].team
})
} else {
this.currentUser = null
}
})
}
async signInUserWithGoogle() {
return await this.app.auth().signInWithPopup(this.authProviders.google())
}
async signInUserWithGithub() {
return await this.app.auth().signInWithPopup(this.authProviders.github())
}
async signInWithEmailAndPassword(email, password) {
return await this.app.auth().signInWithEmailAndPassword(email, password)
}
async getSignInMethodsForEmail(email) {
return await this.app.auth().fetchSignInMethodsForEmail(email)
}
async signOutUser() {
if (!this.currentUser) throw new Error("No user has logged in")
await this.app.auth().signOut()
this.currentUser = null
}
async writeFeeds(message, label) {
const dt = { const dt = {
createdOn: new Date(), createdOn: new Date(),
author: fb.currentUser.uid, author: this.currentUser.uid,
author_name: fb.currentUser.displayName, author_name: this.currentUser.displayName,
author_image: fb.currentUser.photoURL, author_image: this.currentUser.photoURL,
message, message,
label, label,
} }
usersCollection
.doc(fb.currentUser.uid) try {
.collection("feeds") await this.usersCollection.doc(this.currentUser.uid).collection("feeds").add(dt)
.add(dt) } catch (e) {
.catch((e) => console.error("error inserting", dt, e)) console.error("error inserting", dt, e)
}, throw e
deleteFeed: (id) => { }
usersCollection }
.doc(fb.currentUser.uid)
.collection("feeds") async deleteFeed(id) {
.doc(id) try {
.delete() await this.usersCollection.doc(this.currentUser.uid).collection("feeds").doc(id).delete()
.catch((e) => console.error("error deleting", id, e)) } catch (e) {
}, console.error("error deleting", id, e)
writeSettings: async (setting, value) => { throw e
}
}
async writeSettings(setting, value) {
const st = { const st = {
updatedOn: new Date(), updatedOn: new Date(),
author: fb.currentUser.uid, author: this.currentUser.uid,
author_name: fb.currentUser.displayName, author_name: this.currentUser.displayName,
author_image: fb.currentUser.photoURL, author_image: this.currentUser.photoURL,
name: setting, name: setting,
value, value,
} }
usersCollection
.doc(fb.currentUser.uid) try {
.collection("settings") await this.usersCollection
.doc(setting) .doc(this.currentUser.uid)
.set(st) .collection("settings")
.catch((e) => console.error("error updating", st, e)) .doc(setting)
}, .set(st)
writeHistory: async (entry) => { } catch (e) {
console.error("error updating", st, e)
throw e
}
}
async writeHistory(entry) {
const hs = entry const hs = entry
usersCollection
.doc(fb.currentUser.uid) try {
.collection("history") await this.usersCollection.doc(this.currentUser.uid).collection("history").add(hs)
.add(hs) } catch (e) {
.catch((e) => console.error("error inserting", hs, e)) console.error("error inserting", hs, e)
}, throw e
deleteHistory: (entry) => { }
usersCollection }
.doc(fb.currentUser.uid)
.collection("history") async deleteHistory(entry) {
.doc(entry.id) try {
.delete() await this.usersCollection
.catch((e) => console.error("error deleting", entry, e)) .doc(this.currentUser.uid)
}, .collection("history")
clearHistory: () => { .doc(entry.id)
usersCollection .delete()
.doc(fb.currentUser.uid) } catch (e) {
console.error("error deleting", entry, e)
throw e
}
}
async clearHistory() {
const { docs } = await this.usersCollection
.doc(this.currentUser.uid)
.collection("history") .collection("history")
.get() .get()
.then(({ docs }) => {
docs.forEach((e) => fb.deleteHistory(e)) await Promise.all(docs.map((e) => this.deleteHistory(e)))
}) }
},
toggleStar: (entry, value) => { async toggleStar(entry, value) {
usersCollection try {
.doc(fb.currentUser.uid) await this.usersCollection
.collection("history") .doc(this.currentUser.uid)
.doc(entry.id) .collection("history")
.update({ star: value }) .doc(entry.id)
.catch((e) => console.error("error deleting", entry, e)) .update({ star: value })
}, } catch (e) {
writeCollections: async (collection) => { console.error("error deleting", entry, e)
throw e
}
}
async writeCollections(collection) {
const cl = { const cl = {
updatedOn: new Date(), updatedOn: new Date(),
author: fb.currentUser.uid, author: this.currentUser.uid,
author_name: fb.currentUser.displayName, author_name: this.currentUser.displayName,
author_image: fb.currentUser.photoURL, author_image: this.currentUser.photoURL,
collection, collection,
} }
usersCollection
.doc(fb.currentUser.uid) try {
.collection("collections") await this.usersCollection
.doc("sync") .doc(this.currentUser.uid)
.set(cl) .collection("collections")
.catch((e) => console.error("error updating", cl, e)) .doc("sync")
}, .set(cl)
writeEnvironments: async (environment) => { } catch (e) {
console.error("error updating", cl, e)
throw e
}
}
async writeEnvironments(environment) {
const ev = { const ev = {
updatedOn: new Date(), updatedOn: new Date(),
author: fb.currentUser.uid, author: this.currentUser.uid,
author_name: fb.currentUser.displayName, author_name: this.currentUser.displayName,
author_image: fb.currentUser.photoURL, author_image: this.currentUser.photoURL,
environment, environment,
} }
usersCollection
.doc(fb.currentUser.uid) try {
.collection("environments") await this.usersCollection
.doc("sync") .doc(this.currentUser.uid)
.set(ev) .collection("environments")
.catch((e) => console.error("error updating", ev, e)) .doc("sync")
}, .set(ev)
} catch (e) {
console.error("error updating", ev, e)
throw e
}
}
async writeTeams(team) {
const ev = {
updatedOn: new Date(),
author: this.currentUser.uid,
author_name: this.currentUser.displayName,
author_image: this.currentUser.photoURL,
team: team,
}
try {
await this.usersCollection.doc(this.currentUser.uid).collection("teams").doc("sync").set(ev)
} catch (e) {
console.error("error updating", ev, e)
throw e
}
}
} }
// When a user logs in or out, save that in the store export const fb = new FirebaseInstance(firebase.initializeApp(firebaseConfig), authProviders)
firebase.auth().onAuthStateChanged((user) => {
if (user) {
fb.currentUser = user
fb.currentUser.providerData.forEach((profile) => {
let us = {
updatedOn: new Date(),
provider: profile.providerId,
name: profile.displayName,
email: profile.email,
photoUrl: profile.photoURL,
uid: profile.uid,
}
usersCollection
.doc(fb.currentUser.uid)
.set(us)
.catch((e) => console.error("error updating", us, e))
})
usersCollection
.doc(fb.currentUser.uid)
.collection("feeds")
.orderBy("createdOn", "desc")
.onSnapshot((feedsRef) => {
const feeds = []
feedsRef.forEach((doc) => {
const feed = doc.data()
feed.id = doc.id
feeds.push(feed)
})
fb.currentFeeds = feeds
})
usersCollection
.doc(fb.currentUser.uid)
.collection("settings")
.onSnapshot((settingsRef) => {
const settings = []
settingsRef.forEach((doc) => {
const setting = doc.data()
setting.id = doc.id
settings.push(setting)
})
fb.currentSettings = settings
})
usersCollection
.doc(fb.currentUser.uid)
.collection("history")
.onSnapshot((historyRef) => {
const history = []
historyRef.forEach((doc) => {
const entry = doc.data()
entry.id = doc.id
history.push(entry)
})
fb.currentHistory = history
})
usersCollection
.doc(fb.currentUser.uid)
.collection("collections")
.onSnapshot((collectionsRef) => {
const collections = []
collectionsRef.forEach((doc) => {
const collection = doc.data()
collection.id = doc.id
collections.push(collection)
})
if (collections.length > 0) {
fb.currentCollections = collections[0].collection
}
})
usersCollection
.doc(fb.currentUser.uid)
.collection("environments")
.onSnapshot((environmentsRef) => {
const environments = []
environmentsRef.forEach((doc) => {
const environment = doc.data()
environment.id = doc.id
environments.push(environment)
})
if (environments.length > 0) {
fb.currentEnvironments = environments[0].environment
}
})
} else {
fb.currentUser = null
}
})

270
package-lock.json generated
View File

@@ -7964,6 +7964,66 @@
"@firebase/util": "0.3.2" "@firebase/util": "0.3.2"
} }
}, },
"firebase-auto-ids": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/firebase-auto-ids/-/firebase-auto-ids-1.1.0.tgz",
"integrity": "sha1-N4/7hZCIjDfrLdi3BGWa0PSfkGE=",
"dev": true
},
"firebase-mock": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/firebase-mock/-/firebase-mock-2.3.2.tgz",
"integrity": "sha512-K5Bl46uCBbMBxK/qnSM5PvJXTy7cyUsPabfUysKEmXX26aZs4QpbLgwL+0qaNw+NOyIKSe3vXsi/Ktm5NJP35w==",
"dev": true,
"requires": {
"firebase-auto-ids": "~1.1.0",
"lodash.assign": "^4.2.0",
"lodash.assignin": "^4.2.0",
"lodash.bind": "^4.2.1",
"lodash.clone": "^4.5.0",
"lodash.clonedeep": "^4.5.0",
"lodash.clonedeepwith": "^4.5.0",
"lodash.compact": "^3.0.1",
"lodash.difference": "^4.5.0",
"lodash.every": "^4.6.0",
"lodash.filter": "^4.6.0",
"lodash.find": "^4.6.0",
"lodash.findindex": "^4.6.0",
"lodash.foreach": "^4.5.0",
"lodash.forin": "^4.4.0",
"lodash.get": "^4.4.2",
"lodash.has": "^4.5.2",
"lodash.includes": "^4.3.0",
"lodash.indexof": "^4.0.5",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isfunction": "^3.0.9",
"lodash.isnumber": "^3.0.3",
"lodash.isobject": "^3.0.2",
"lodash.isstring": "^4.0.1",
"lodash.isundefined": "^3.0.1",
"lodash.keys": "^4.2.0",
"lodash.map": "^4.6.0",
"lodash.merge": "^4.6.1",
"lodash.noop": "^3.0.1",
"lodash.orderby": "^4.6.0",
"lodash.reduce": "^4.6.0",
"lodash.remove": "^4.7.0",
"lodash.set": "^4.3.2",
"lodash.size": "^4.2.0",
"lodash.toarray": "^4.4.0",
"lodash.union": "^4.6.0",
"rsvp": "^3.6.2"
},
"dependencies": {
"rsvp": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz",
"integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==",
"dev": true
}
}
},
"flat": { "flat": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
@@ -10624,21 +10684,225 @@
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0="
}, },
"lodash.assign": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
"dev": true
},
"lodash.assignin": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
"integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=",
"dev": true
},
"lodash.bind": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz",
"integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=",
"dev": true
},
"lodash.camelcase": { "lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
}, },
"lodash.clone": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz",
"integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=",
"dev": true
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"lodash.clonedeepwith": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeepwith/-/lodash.clonedeepwith-4.5.0.tgz",
"integrity": "sha1-buMFc6A6GmDWcKYu8zwQzxr9vdQ=",
"dev": true
},
"lodash.compact": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/lodash.compact/-/lodash.compact-3.0.1.tgz",
"integrity": "sha1-VAzjg3dFl1gHRx4WtKK6IeclbKU=",
"dev": true
},
"lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=",
"dev": true
},
"lodash.every": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.every/-/lodash.every-4.6.0.tgz",
"integrity": "sha1-64mYS+vENkJ5uzrvu9HKGb+mxqc=",
"dev": true
},
"lodash.filter": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",
"integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=",
"dev": true
},
"lodash.find": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz",
"integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=",
"dev": true
},
"lodash.findindex": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.findindex/-/lodash.findindex-4.6.0.tgz",
"integrity": "sha1-oyRd7mH7m24GJLU1ElYku2nBEQY=",
"dev": true
},
"lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=",
"dev": true
},
"lodash.forin": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.forin/-/lodash.forin-4.4.0.tgz",
"integrity": "sha1-XT8grlZAEfvog4H32YlJyclRlzE=",
"dev": true
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true
},
"lodash.has": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz",
"integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=",
"dev": true
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=",
"dev": true
},
"lodash.indexof": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/lodash.indexof/-/lodash.indexof-4.0.5.tgz",
"integrity": "sha1-U3FK3Czd1u2HY4+JOqm2wk4x7zw=",
"dev": true
},
"lodash.isempty": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
"integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=",
"dev": true
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
"dev": true
},
"lodash.isfunction": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==",
"dev": true
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=",
"dev": true
},
"lodash.isobject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
"integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=",
"dev": true
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=",
"dev": true
},
"lodash.isundefined": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz",
"integrity": "sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g=",
"dev": true
},
"lodash.kebabcase": { "lodash.kebabcase": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
"integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=" "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY="
}, },
"lodash.keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-4.2.0.tgz",
"integrity": "sha1-oIYCrBLk+4P5H8H7ejYKTZujUgU=",
"dev": true
},
"lodash.map": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
"integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=",
"dev": true
},
"lodash.memoize": { "lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
}, },
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"lodash.noop": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz",
"integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw=",
"dev": true
},
"lodash.orderby": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.orderby/-/lodash.orderby-4.6.0.tgz",
"integrity": "sha1-5pfwTOXXhSL1TZM4syuBozk+TrM=",
"dev": true
},
"lodash.reduce": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
"integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=",
"dev": true
},
"lodash.remove": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.remove/-/lodash.remove-4.7.0.tgz",
"integrity": "sha1-8x0x58OaBpDVB07A02JxYjNO5iY=",
"dev": true
},
"lodash.set": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
"dev": true
},
"lodash.size": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.size/-/lodash.size-4.2.0.tgz",
"integrity": "sha1-cf517T6r2yvLc6GwtPUcOS7ie4Y=",
"dev": true
},
"lodash.sortby": { "lodash.sortby": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -10668,6 +10932,12 @@
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=",
"dev": true "dev": true
}, },
"lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=",
"dev": true
},
"lodash.unionby": { "lodash.unionby": {
"version": "4.8.0", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/lodash.unionby/-/lodash.unionby-4.8.0.tgz", "resolved": "https://registry.npmjs.org/lodash.unionby/-/lodash.unionby-4.8.0.tgz",

View File

@@ -61,6 +61,7 @@
"babel-jest": "^26.5.2", "babel-jest": "^26.5.2",
"eslint": "^7.11.0", "eslint": "^7.11.0",
"eslint-plugin-vue": "^7.0.1", "eslint-plugin-vue": "^7.0.1",
"firebase-mock": "^2.3.2",
"husky": "^4.3.0", "husky": "^4.3.0",
"jest": "^26.5.3", "jest": "^26.5.3",
"jest-serializer-vue": "^2.0.2", "jest-serializer-vue": "^2.0.2",
@@ -78,6 +79,7 @@
], ],
"watchman": false, "watchman": false,
"moduleNameMapper": { "moduleNameMapper": {
".+\\.(svg)\\?inline$": "<rootDir>/__mocks__/svgMock.js",
"^~/(.*)$": "<rootDir>/$1", "^~/(.*)$": "<rootDir>/$1",
"^~~/(.*)$": "<rootDir>/$1" "^~~/(.*)$": "<rootDir>/$1"
}, },