[Feat: GraphQL sidebar] GraphQL History (#1528)

* Create REQUIREMENTS.md

* graphql history UI

* rest history emit

* removed requirements file

* add, delete, clear, star history and sync with firstore

* use history

* empty schema

* remove other tabs

* computed query, setting headers

* binding props

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>

* remove print

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>

* remove dummy data

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>

* add docs tab

* date, time attribute --> updatedOn

* Removed margin from sidebar Tab

* removed v-bind

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>

* use shortcut for v-bind

* use shortcut for v-bind

* use unfold icons

* use template literals in :key

* history clear bug

* delete history bug

* minor translation

* remove console logs

* remove unused css

* add stared style

* remove absolute styles

* tests for graphql card

* tests for rest card

* tests for clear history added

* mount, clear, use, delete history tests added

* Rename card.vue to Card.vue

* Rename card.vue to Card.vue

* use computed

Co-authored-by: Isha Gupta <40794215+IshaGupta18@users.noreply.github.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
Osheen Sachdev
2021-03-11 08:42:09 +05:30
committed by GitHub
parent 51bd3455cc
commit 0b00842c50
10 changed files with 1019 additions and 229 deletions

View File

@@ -0,0 +1,109 @@
import GraphqlCard from "../graphql/Card"
import { mount } from "@vue/test-utils"
const factory = (props) => {
return mount(GraphqlCard, {
propsData: props,
stubs: {
"v-popover": {
template: "<div><slot /><slot name='popover' :is-open=true /></div>",
},
},
mocks: {
$t: (text) => text,
},
directives: {
tooltip() {
/* stub */
},
closePopover() {
/* stub */
},
},
})
}
const url = "https://dummydata.com"
const query = `query getUser($uid: String!) {
user(uid: $uid) {
name
}
}`
describe("GraphqlCard", () => {
test("Mounts properly if props are given", () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
star: false,
},
})
expect(wrapper).toBeTruthy()
})
test("toggle-star emitted on clicking on star button", async () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
star: true,
},
})
wrapper.find("button[data-testid='star_button']").trigger("click")
expect(wrapper.emitted("toggle-star")).toBeTruthy()
})
test("query expands on clicking the show more button", async () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
star: true,
},
})
expect(wrapper.vm.query).toStrictEqual([
`query getUser($uid: String!) {`,
` user(uid: $uid) {`,
`...`,
])
await wrapper.find("button[data-testid='query_expand']").trigger("click")
expect(wrapper.vm.query).toStrictEqual([
`query getUser($uid: String!) {`,
` user(uid: $uid) {`,
` name`,
` }`,
`}`,
])
})
test("use-entry emit on clicking the restore button", async () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
star: true,
},
})
await wrapper.find("button[data-testid='restore_history_entry']").trigger("click")
expect(wrapper.emitted("use-entry")).toBeTruthy()
})
test("delete-entry emit on clicking the delete button", async () => {
const wrapper = factory({
entry: {
type: "graphql",
url: url,
query: query,
star: true,
},
})
await wrapper.find("button[data-testid=delete_history_entry]").trigger("click")
expect(wrapper.emitted("delete-entry")).toBeTruthy()
})
})

View File

@@ -0,0 +1,285 @@
import History from "../"
import { fb } from "~/helpers/fb"
import { shallowMount } from "@vue/test-utils"
import HistoryRestCard from "../rest/Card"
const restHistory = [
{
id: "0",
type: "rest",
},
{
id: "1",
type: "rest",
},
{
id: "2",
type: "rest",
},
]
const graphqlHistory = [
{
id: "0",
type: "graphql",
},
{
id: "1",
type: "graphql",
},
{
id: "2",
type: "graphql",
},
]
var localStorageMock = (function () {
var store = {
history: JSON.stringify(restHistory),
graphqlHistory: JSON.stringify(graphqlHistory),
}
return {
getItem: function (key) {
return store[key]
},
setItem: jest.fn(),
clear: jest.fn(),
removeItem: jest.fn(),
}
})()
Object.defineProperty(window, "localStorage", { value: localStorageMock })
jest.mock("~/helpers/fb", () => ({
__esModule: true,
fb: {
currentUser: null,
currentHistory: restHistory,
currentGraphqlHistory: graphqlHistory,
clearHistory: jest.fn(),
clearGraphqlHistory: jest.fn(),
deleteHistory: jest.fn(),
deleteGraphqlHistory: jest.fn(),
},
}))
const factory = (props) => {
return shallowMount(History, {
propsData: props,
stubs: {
"v-popover": {
template: "<div><slot /><slot name='popover' :is-open=true /></div>",
},
HistoryRestCard: {
template: "<div data-testid='rest_card' />",
},
HistoryGraphqlCard: {
template: "<div data-testid='graphql_card' />",
},
AppSection: {
template: "<div><slot /></div>",
},
},
mocks: {
$t: (text) => text,
$toast: {
error() {},
},
},
directives: {
tooltip() {
/* stub */
},
closePopover() {
/* stub */
},
},
})
}
beforeEach(() => {
fb.clearHistory.mockClear()
fb.clearGraphqlHistory.mockClear()
fb.deleteHistory.mockClear()
fb.deleteGraphqlHistory.mockClear()
window.localStorage.setItem.mockClear()
})
describe("Mount History", () => {
test("Mounts rest history without login", async () => {
const wrapper = factory({
page: "rest",
})
expect(wrapper).toBeTruthy()
})
test("Mounts rest history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper).toBeTruthy()
})
test("Mounts graphql history without login", async () => {
const wrapper = factory({
page: "rest",
})
expect(wrapper).toBeTruthy()
})
test("Mounts graphql history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper).toBeTruthy()
})
})
describe("Clear History", () => {
test("Clear rest history without login", async () => {
fb.currentUser = null
const wrapper = factory({
page: "rest",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(restHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='confirm_clear_history']").trigger("click")
expect(fb.clearHistory).not.toHaveBeenCalled()
expect(window.localStorage.setItem).toHaveBeenCalledWith("history", JSON.stringify([]))
})
test("Clear rest history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(restHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='confirm_clear_history']").trigger("click")
expect(fb.clearHistory).toHaveBeenCalledTimes(1)
expect(window.localStorage.setItem).toHaveBeenCalledWith("history", JSON.stringify([]))
})
test("Dont confirm Clear rest history", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(restHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='reject_clear_history']").trigger("click")
expect(fb.clearHistory).not.toHaveBeenCalled()
expect(window.localStorage.setItem).not.toHaveBeenCalledWith("history", JSON.stringify([]))
})
test("Clear graphql history without login", async () => {
fb.currentUser = null
const wrapper = factory({
page: "graphql",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(graphqlHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='confirm_clear_history']").trigger("click")
expect(fb.clearGraphqlHistory).not.toHaveBeenCalled()
expect(window.localStorage.setItem).toHaveBeenCalledWith("graphqlHistory", JSON.stringify([]))
})
test("Clear graphql history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "graphql",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(graphqlHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='confirm_clear_history']").trigger("click")
expect(fb.clearGraphqlHistory).toHaveBeenCalledTimes(1)
expect(window.localStorage.setItem).toHaveBeenCalledWith("graphqlHistory", JSON.stringify([]))
})
test("Dont confirm Clear graphql history", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "graphql",
})
expect(wrapper.vm.filteredHistory).toStrictEqual(graphqlHistory)
await wrapper.find("button[data-testid='clear_history']").trigger("click")
await wrapper.find("button[data-testid='reject_clear_history']").trigger("click")
expect(window.localStorage.setItem).not.toHaveBeenCalledWith(
"graphqlHistory",
JSON.stringify([])
)
})
})
describe("Use History", () => {
test("use rest history", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper.findAll("div[data-testid='rest_card']").length).toEqual(restHistory.length)
var index = restHistory.length - 1
wrapper.findAll("div[data-testid='rest_card']").at(index).vm.$emit("use-entry")
expect(wrapper.emitted("useHistory")).toBeTruthy()
expect(wrapper.emitted("useHistory")[0]).toStrictEqual([restHistory[index]])
})
test("use graphql history", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "graphql",
})
expect(wrapper.findAll("div[data-testid='graphql_card']").length).toEqual(graphqlHistory.length)
var index = restHistory.length - 1
wrapper.findAll("div[data-testid='graphql_card']").at(index).vm.$emit("use-entry")
expect(wrapper.emitted("useHistory")).toBeTruthy()
expect(wrapper.emitted("useHistory")[0]).toStrictEqual([graphqlHistory[index]])
})
})
describe("Delete History", () => {
test("delete rest history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "rest",
})
expect(wrapper.findAll("div[data-testid='rest_card']").length).toEqual(restHistory.length)
var index = 1
wrapper.findAll("div[data-testid='rest_card']").at(index).vm.$emit("delete-entry")
expect(fb.deleteHistory).toBeCalledWith(restHistory[index])
})
test("delete rest history without login", async () => {
fb.currentUser = null
const wrapper = factory({
page: "rest",
})
expect(wrapper.findAll("div[data-testid='rest_card']").length).toEqual(restHistory.length)
var index = 1
wrapper.findAll("div[data-testid='rest_card']").at(index).vm.$emit("delete-entry")
expect(window.localStorage.setItem).toBeCalledWith(
"history",
JSON.stringify(restHistory.filter((entry) => entry.id != index))
)
})
test("delete graphql history with login", async () => {
fb.currentUser = "user"
const wrapper = factory({
page: "graphql",
})
expect(wrapper.findAll("div[data-testid='graphql_card']").length).toEqual(graphqlHistory.length)
var index = 1
wrapper.findAll("div[data-testid='graphql_card']").at(index).vm.$emit("delete-entry")
expect(fb.deleteGraphqlHistory).toBeCalledWith(graphqlHistory[index])
})
test("delete graphql history without login", async () => {
fb.currentUser = null
const wrapper = factory({
page: "graphql",
})
expect(wrapper.findAll("div[data-testid='graphql_card']").length).toEqual(graphqlHistory.length)
var index = 1
wrapper.findAll("div[data-testid='graphql_card']").at(index).vm.$emit("delete-entry")
expect(window.localStorage.setItem).toBeCalledWith(
"graphqlHistory",
JSON.stringify(graphqlHistory.filter((entry) => entry.id != index))
)
})
})

View File

@@ -0,0 +1,58 @@
import RestCard from "../rest/Card"
import { mount } from "@vue/test-utils"
const factory = (props) => {
return mount(RestCard, {
propsData: props,
stubs: {
"v-popover": {
template: "<div><slot /><slot name='popover' :is-open=true /></div>",
},
},
mocks: {
$t: (text) => text,
},
directives: {
tooltip() {
/* stub */
},
closePopover() {
/* stub */
},
},
})
}
const url = "https://dummydata.com/get"
const entry = {
type: "rest",
url: url,
method: "GET",
status: 200,
star: false,
}
describe("RestCard", () => {
test("Mounts properly if props are given", () => {
const wrapper = factory({ entry })
expect(wrapper).toBeTruthy()
})
test("toggle-star emitted on clicking on star button", async () => {
const wrapper = factory({ entry })
wrapper.find("button[data-testid='star_button']").trigger("click")
expect(wrapper.emitted("toggle-star")).toBeTruthy()
})
test("use-entry emit on clicking the restore button", async () => {
const wrapper = factory({ entry })
await wrapper.find("button[data-testid='restore_history_entry']").trigger("click")
expect(wrapper.emitted("use-entry")).toBeTruthy()
})
test("delete-entry emit on clicking the delete button", async () => {
const wrapper = factory({ entry })
await wrapper.find("button[data-testid=delete_history_entry]").trigger("click")
expect(wrapper.emitted("delete-entry")).toBeTruthy()
})
})

View File

@@ -0,0 +1,147 @@
<template>
<div>
<div class="show-on-large-screen">
<li>
<input
data-testid="'url'"
:aria-label="$t('url')"
type="text"
readonly
:value="entry.url"
:placeholder="$t('empty_req_name')"
class="bg-transparent"
/>
</li>
<button
data-testid="star_button"
class="icon"
:class="{ stared: entry.star }"
@click="$emit('toggle-star')"
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
>
<i class="material-icons">
{{ entry.star ? "star" : "star_border" }}
</i>
</button>
<button
data-testid="query_expand"
class="icon"
@click="expand = !expand"
v-tooltip="{
content: expand ? $t('hide_more') : $t('show_more'),
}"
>
<i class="material-icons">
{{ expand ? "unfold_less" : "unfold_more" }}
</i>
</button>
<v-popover>
<button data-testid="options" class="tooltip-target icon" v-tooltip="$t('options')">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
data-testid="restore_history_entry"
class="icon"
@click="$emit('use-entry')"
:aria-label="$t('restore')"
v-close-popover
>
<i class="material-icons">restore</i>
<span>{{ $t("restore") }}</span>
</button>
</div>
<div>
<button
data-testid="delete_history_entry"
class="icon"
@click="$emit('delete-entry')"
:aria-label="$t('delete')"
v-close-popover
>
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
</div>
</template>
</v-popover>
</div>
<div class="show-on-large-screen">
<li data-testid="'query'">
<input
v-for="(line, index) in query"
:key="`line-${index}`"
:aria-label="$t('query')"
type="text"
readonly
:value="`${line}`"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
</div>
<transition name="fade">
<div v-if="showMore" class="show-on-large-screen">
<li>
<input
:aria-label="$t('time')"
type="text"
readonly
:value="entry.time"
v-tooltip="entry.date"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
<li>
<input
:aria-label="$t('duration')"
type="text"
readonly
:value="entry.duration"
:placeholder="$t('no_duration')"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
<li>
<input
:aria-label="$t('prerequest_script')"
type="text"
readonly
:value="entry.preRequestScript"
:placeholder="$t('no_prerequest_script')"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
</div>
</transition>
</div>
</template>
<style scoped lang="scss">
.stared {
color: #f8e81c !important;
}
</style>
<script>
export default {
props: {
entry: Object,
showMore: Boolean,
},
data() {
return {
expand: false,
}
},
computed: {
query() {
return this.expand
? this.entry.query.split("\n")
: this.entry.query.split("\n").slice(0, 2).concat(["..."])
},
},
}
</script>

View File

@@ -13,127 +13,24 @@
class="divide-y virtual-list divide-dashed divide-brdColor" class="divide-y virtual-list divide-dashed divide-brdColor"
:class="{ filled: filteredHistory.length }" :class="{ filled: filteredHistory.length }"
> >
<ul v-for="(entry, index) in filteredHistory" :key="index"> <ul v-for="(entry, index) in filteredHistory" :key="`entry-${index}`">
<div class="show-on-large-screen"> <HistoryRestCard
<span v-if="page == 'rest'"
class="p-2 m-2" :entry="entry"
:class="findEntryStatus(entry).className" :id="index"
:style="{ '--status-code': entry.status }" :showMore="showMore"
> @toggle-star="toggleStar(entry)"
{{ `${entry.method} \xA0 • \xA0 ${entry.status}` }} @delete-entry="deleteHistory(entry)"
</span> @use-entry="useHistory(entry)"
<li> />
<input <HistoryGraphqlCard
:aria-label="$t('token_req_name')" v-if="page == 'graphql'"
type="text" :entry="entry"
readonly :showMore="showMore"
:value="entry.name" @toggle-star="toggleStar(entry)"
:placeholder="$t('empty_req_name')" @delete-entry="deleteHistory(entry)"
class="bg-transparent" @use-entry="useHistory(entry)"
/> />
</li>
<button
class="icon"
:class="{ stared: entry.star }"
@click="toggleStar(entry)"
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
>
<i class="material-icons">
{{ entry.star ? "star" : "star_border" }}
</i>
</button>
<!-- <li>
<button
class="icon"
v-tooltip="{
content: !entry.usesScripts
? 'No pre-request script'
: 'Used pre-request script'
}"
>
<i class="material-icons">
{{ !entry.usesScripts ? "http" : "code" }}
</i>
</button>
</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"
@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"
@click="deleteHistory(entry)"
:aria-label="$t('delete')"
v-close-popover
>
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
</div>
</template>
</v-popover>
</div>
<div class="show-on-large-screen">
<li>
<input
:aria-label="$t('url')"
type="text"
readonly
:value="`${entry.url}${entry.path}`"
:placeholder="$t('no_url')"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
</div>
<transition name="fade">
<div v-if="showMore" class="show-on-large-screen">
<li>
<input
:aria-label="$t('time')"
type="text"
readonly
:value="entry.time"
v-tooltip="entry.date"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
<li>
<input
:aria-label="$t('duration')"
type="text"
readonly
:value="entry.duration"
:placeholder="$t('no_duration')"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
<li>
<input
:aria-label="$t('prerequest_script')"
type="text"
readonly
:value="entry.preRequestScript"
:placeholder="$t('no_prerequest_script')"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
</div>
</transition>
</ul> </ul>
</div> </div>
<p :class="{ hidden: filteredHistory.length != 0 || history.length === 0 }" class="info"> <p :class="{ hidden: filteredHistory.length != 0 || history.length === 0 }" class="info">
@@ -144,12 +41,17 @@
</p> </p>
<div v-if="history.length !== 0" class="rounded-b-lg bg-bgDarkColor"> <div v-if="history.length !== 0" class="rounded-b-lg bg-bgDarkColor">
<div class="row-wrapper" v-if="!isClearingHistory"> <div class="row-wrapper" v-if="!isClearingHistory">
<button class="icon" :disabled="history.length === 0" @click="enableHistoryClearing"> <button
data-testid="clear_history"
class="icon"
:disabled="history.length === 0"
@click="enableHistoryClearing"
>
<i class="material-icons">clear_all</i> <i class="material-icons">clear_all</i>
<span>{{ $t("clear_all") }}</span> <span>{{ $t("clear_all") }}</span>
</button> </button>
<v-popover> <v-popover>
<button class="tooltip-target icon" v-tooltip="$t('sort')"> <button v-if="this.page == 'rest'" class="tooltip-target icon" v-tooltip="$t('sort')">
<i class="material-icons">sort</i> <i class="material-icons">sort</i>
</button> </button>
<template slot="popover"> <template slot="popover">
@@ -203,10 +105,20 @@
<div class="row-wrapper" v-else> <div class="row-wrapper" v-else>
<p class="info"><i class="material-icons">help_outline</i> {{ $t("are_you_sure") }}</p> <p class="info"><i class="material-icons">help_outline</i> {{ $t("are_you_sure") }}</p>
<div> <div>
<button class="icon" @click="clearHistory" v-tooltip="$t('yes')"> <button
data-testid="confirm_clear_history"
class="icon"
@click="clearHistory"
v-tooltip="$t('yes')"
>
<i class="material-icons">done</i> <i class="material-icons">done</i>
</button> </button>
<button class="icon" @click="disableHistoryClearing" v-tooltip="$t('no')"> <button
data-testid="reject_clear_history"
class="icon"
@click="disableHistoryClearing"
v-tooltip="$t('no')"
>
<i class="material-icons">close</i> <i class="material-icons">close</i>
</button> </button>
</div> </div>
@@ -218,36 +130,26 @@
<style scoped lang="scss"> <style scoped lang="scss">
.virtual-list { .virtual-list {
max-height: calc(100vh - 270px); max-height: calc(100vh - 270px);
[readonly] { [readonly] {
cursor: default; cursor: default;
} }
} }
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: all 0.2s; transition: all 0.2s;
} }
.fade-enter, .fade-enter,
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
} }
.stared {
color: #f8e81c !important;
}
ul, ul,
ol { ol {
flex-direction: column; flex-direction: column;
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.virtual-list.filled { .virtual-list.filled {
min-height: 320px; min-height: 320px;
} }
.labels { .labels {
display: none; display: none;
} }
@@ -255,19 +157,23 @@ ol {
</style> </style>
<script> <script>
import findStatusGroup from "~/helpers/findStatusGroup"
import { fb } from "~/helpers/fb" import { fb } from "~/helpers/fb"
const updateOnLocalStorage = (propertyName, property) => const updateOnLocalStorage = (propertyName, property) =>
window.localStorage.setItem(propertyName, JSON.stringify(property)) window.localStorage.setItem(propertyName, JSON.stringify(property))
export default { export default {
props: {
page: String,
},
data() { data() {
return { return {
history: history:
fb.currentUser !== null fb.currentUser !== null
? fb.currentHistory ? fb.currentHistory
: JSON.parse(window.localStorage.getItem("history")) || [], : JSON.parse(
window.localStorage.getItem(this.page == "rest" ? "history" : "graphqlHistory")
) || [],
filterText: "", filterText: "",
showFilter: false, showFilter: false,
isClearingHistory: false, isClearingHistory: false,
@@ -283,8 +189,12 @@ export default {
filteredHistory() { filteredHistory() {
this.history = this.history =
fb.currentUser !== null fb.currentUser !== null
? fb.currentHistory ? this.page == "rest"
: JSON.parse(window.localStorage.getItem("history")) || [] ? fb.currentHistory
: fb.currentGraphqlHistory
: JSON.parse(
window.localStorage.getItem(this.page == "rest" ? "history" : "graphqlHistory")
) || []
return this.history.filter((entry) => { return this.history.filter((entry) => {
const filterText = this.filterText.toLowerCase() const filterText = this.filterText.toLowerCase()
return Object.keys(entry).some((key) => { return Object.keys(entry).some((key) => {
@@ -298,12 +208,12 @@ export default {
methods: { methods: {
async clearHistory() { async clearHistory() {
if (fb.currentUser !== null) { if (fb.currentUser !== null) {
await fb.clearHistory() this.page == "rest" ? await fb.clearHistory() : await fb.clearGraphqlHistory()
} }
this.history = [] this.history = []
this.filterText = "" this.filterText = ""
this.disableHistoryClearing() this.disableHistoryClearing()
updateOnLocalStorage("history", this.history) updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
this.$toast.error(this.$t("history_deleted"), { this.$toast.error(this.$t("history_deleted"), {
icon: "delete", icon: "delete",
}) })
@@ -311,30 +221,25 @@ export default {
useHistory(entry) { useHistory(entry) {
this.$emit("useHistory", entry) this.$emit("useHistory", entry)
}, },
findEntryStatus({ status }) {
const foundStatusGroup = findStatusGroup(status)
return (
foundStatusGroup || {
className: "",
}
)
},
async deleteHistory(entry) { async deleteHistory(entry) {
if (fb.currentUser !== null) {
await fb.deleteHistory(entry)
}
this.history.splice(this.history.indexOf(entry), 1)
if (this.history.length === 0) { if (this.history.length === 0) {
this.filterText = "" this.filterText = ""
} }
updateOnLocalStorage("history", this.history) if (fb.currentUser !== null) {
await (this.page == "rest" ? fb.deleteHistory(entry) : fb.deleteGraphqlHistory(entry))
this.history = fb.currentHistory
updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
} else {
this.history.splice(this.history.indexOf(entry), 1)
updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
}
this.$toast.error(this.$t("deleted"), { this.$toast.error(this.$t("deleted"), {
icon: "delete", icon: "delete",
}) })
}, },
addEntry(entry) { addEntry(entry) {
this.history.push(entry) this.history.push(entry)
updateOnLocalStorage("history", this.history) updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
}, },
enableHistoryClearing() { enableHistoryClearing() {
if (!this.history || !this.history.length) return if (!this.history || !this.history.length) return
@@ -409,10 +314,12 @@ export default {
}, },
async toggleStar(entry) { async toggleStar(entry) {
if (fb.currentUser !== null) { if (fb.currentUser !== null) {
await fb.toggleStar(entry, !entry.star) this.page == "rest"
? await fb.toggleStar(entry, !entry.star)
: await fb.toggleGraphqlHistoryStar(entry, !entry.star)
} }
entry.star = !entry.star entry.star = !entry.star
updateOnLocalStorage("history", this.history) updateOnLocalStorage(this.page == "rest" ? "history" : "graphqlHistory", this.history)
}, },
}, },
} }

View File

@@ -0,0 +1,160 @@
<template>
<div>
<div class="show-on-large-screen">
<span
class="p-2 m-2"
:class="entryStatus.className"
:style="{ '--status-code': entry.status }"
>
{{ `${entry.method} \xA0 • \xA0 ${entry.status}` }}
</span>
<li>
<input
:aria-label="$t('token_req_name')"
type="text"
readonly
:value="entry.name"
:placeholder="$t('empty_req_name')"
class="bg-transparent"
/>
</li>
<button
data-testid="star_button"
class="icon"
:class="{ stared: entry.star }"
@click="$emit('toggle-star')"
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star'),
}"
>
<i class="material-icons">
{{ entry.star ? "star" : "star_border" }}
</i>
</button>
<!-- <li>
<button
class="icon"
v-tooltip="{
content: !entry.usesScripts
? 'No pre-request script'
: 'Used pre-request script'
}"
>
<i class="material-icons">
{{ !entry.usesScripts ? "http" : "code" }}
</i>
</button>
</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
data-testid="restore_history_entry"
class="icon"
@click="$emit('use-entry')"
:aria-label="$t('edit')"
v-close-popover
>
<i class="material-icons">restore</i>
<span>{{ $t("restore") }}</span>
</button>
</div>
<div>
<button
data-testid="delete_history_entry"
class="icon"
@click="$emit('delete-entry')"
:aria-label="$t('delete')"
v-close-popover
>
<i class="material-icons">delete</i>
<span>{{ $t("delete") }}</span>
</button>
</div>
</template>
</v-popover>
</div>
<div class="show-on-large-screen">
<li>
<input
:aria-label="$t('url')"
type="text"
readonly
:value="`${entry.url}${entry.path}`"
:placeholder="$t('no_url')"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
</div>
<transition name="fade">
<div v-if="showMore" class="show-on-large-screen">
<li>
<input
:aria-label="$t('time')"
type="text"
readonly
:value="entry.time"
v-tooltip="entry.date"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
<li>
<input
:aria-label="$t('duration')"
type="text"
readonly
:value="entry.duration"
:placeholder="$t('no_duration')"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
<li>
<input
:aria-label="$t('prerequest_script')"
type="text"
readonly
:value="entry.preRequestScript"
:placeholder="$t('no_prerequest_script')"
class="pt-0 mt-0 text-sm bg-transparent text-fgLightColor"
/>
</li>
</div>
</transition>
</div>
</template>
<style scoped lang="scss">
.stared {
color: #f8e81c !important;
}
</style>
<script>
import findStatusGroup from "~/helpers/findStatusGroup"
export default {
props: {
entry: Object,
showMore: Boolean,
},
data() {
return {
expand: false,
}
},
computed: {
entryStatus() {
const foundStatusGroup = findStatusGroup(this.entry.status)
console.log(foundStatusGroup)
return (
foundStatusGroup || {
className: "",
}
)
},
},
}
</script>

View File

@@ -30,6 +30,7 @@ export class FirebaseInstance {
this.currentFeeds = [] this.currentFeeds = []
this.currentSettings = [] this.currentSettings = []
this.currentHistory = [] this.currentHistory = []
this.currentGraphqlHistory = []
this.currentCollections = [] this.currentCollections = []
this.currentEnvironments = [] this.currentEnvironments = []
@@ -97,6 +98,19 @@ export class FirebaseInstance {
this.currentHistory = history this.currentHistory = history
}) })
this.usersCollection
.doc(this.currentUser.uid)
.collection("graphqlHistory")
.onSnapshot((historyRef) => {
const history = []
historyRef.forEach((doc) => {
const entry = doc.data()
entry.id = doc.id
history.push(entry)
})
this.currentGraphqlHistory = history
})
this.usersCollection this.usersCollection
.doc(this.currentUser.uid) .doc(this.currentUser.uid)
.collection("collections") .collection("collections")
@@ -215,6 +229,17 @@ export class FirebaseInstance {
} }
} }
async writeGraphqlHistory(entry) {
const hs = entry
try {
await this.usersCollection.doc(this.currentUser.uid).collection("graphqlHistory").add(hs)
} catch (e) {
console.error("error inserting", hs, e)
throw e
}
}
async deleteHistory(entry) { async deleteHistory(entry) {
try { try {
await this.usersCollection await this.usersCollection
@@ -228,6 +253,19 @@ export class FirebaseInstance {
} }
} }
async deleteGraphqlHistory(entry) {
try {
await this.usersCollection
.doc(this.currentUser.uid)
.collection("graphqlHistory")
.doc(entry.id)
.delete()
} catch (e) {
console.error("error deleting", entry, e)
throw e
}
}
async clearHistory() { async clearHistory() {
const { docs } = await this.usersCollection const { docs } = await this.usersCollection
.doc(this.currentUser.uid) .doc(this.currentUser.uid)
@@ -237,6 +275,15 @@ export class FirebaseInstance {
await Promise.all(docs.map((e) => this.deleteHistory(e))) await Promise.all(docs.map((e) => this.deleteHistory(e)))
} }
async clearGraphqlHistory() {
const { docs } = await this.usersCollection
.doc(this.currentUser.uid)
.collection("graphqlHistory")
.get()
await Promise.all(docs.map((e) => this.deleteGraphqlHistory(e)))
}
async toggleStar(entry, value) { async toggleStar(entry, value) {
try { try {
await this.usersCollection await this.usersCollection
@@ -251,6 +298,20 @@ export class FirebaseInstance {
} }
} }
async toggleGraphqlHistoryStar(entry, value) {
try {
await this.usersCollection
.doc(this.currentUser.uid)
.collection("graphqlHistory")
.doc(entry.id)
.update({ star: value })
} catch (e) {
console.error("error deleting", entry, e)
throw e
}
}
async writeCollections(collection) { async writeCollections(collection) {
const cl = { const cl = {
updatedOn: new Date(), updatedOn: new Date(),

9
package-lock.json generated
View File

@@ -10947,6 +10947,12 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
}, },
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"optional": true
},
"nanoid": { "nanoid": {
"version": "3.1.20", "version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
@@ -16734,7 +16740,8 @@
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"optional": true, "optional": true,
"requires": { "requires": {
"bindings": "^1.5.0" "bindings": "^1.5.0",
"nan": "^2.12.1"
} }
}, },
"glob-parent": { "glob-parent": {

View File

@@ -318,79 +318,102 @@
</div> </div>
<aside class="sticky-inner inner-right lg:max-w-md"> <aside class="sticky-inner inner-right lg:max-w-md">
<AppSection class="purple" :label="$t('docs')" ref="docs" no-legend> <SmartTabs>
<section class="flex-col"> <SmartTab :id="'docs'" :label="`Docs`" :selected="true">
<input <AppSection class="purple" :label="$t('docs')" ref="docs" no-legend>
type="text" <section class="flex-col">
:placeholder="$t('search')" <input
v-model="graphqlFieldsFilterText" type="text"
class="rounded-t-lg" :placeholder="$t('search')"
v-model="graphqlFieldsFilterText"
class="rounded-t-lg"
/>
<SmartTabs ref="gqlTabs" styles="m-4">
<div class="gqlTabs">
<SmartTab
v-if="queryFields.length > 0"
:id="'queries'"
:label="$t('queries')"
:selected="true"
>
<div v-for="field in filteredQueryFields" :key="field.name">
<GraphqlField :gqlField="field" :jumpTypeCallback="handleJumpToType" />
</div>
</SmartTab>
<SmartTab
v-if="mutationFields.length > 0"
:id="'mutations'"
:label="$t('mutations')"
>
<div v-for="field in filteredMutationFields" :key="field.name">
<GraphqlField :gqlField="field" :jumpTypeCallback="handleJumpToType" />
</div>
</SmartTab>
<SmartTab
v-if="subscriptionFields.length > 0"
:id="'subscriptions'"
:label="$t('subscriptions')"
>
<div v-for="field in filteredSubscriptionFields" :key="field.name">
<GraphqlField :gqlField="field" :jumpTypeCallback="handleJumpToType" />
</div>
</SmartTab>
<SmartTab
v-if="graphqlTypes.length > 0"
:id="'types'"
:label="$t('types')"
ref="typesTab"
>
<div v-for="type in filteredGraphqlTypes" :key="type.name">
<GraphqlType
:gqlType="type"
:gqlTypes="graphqlTypes"
:isHighlighted="isGqlTypeHighlighted({ gqlType: type })"
:highlightedFields="getGqlTypeHighlightedFields({ gqlType: type })"
:jumpTypeCallback="handleJumpToType"
/>
</div>
</SmartTab>
</div>
</SmartTabs>
</section>
<p
v-if="
queryFields.length === 0 &&
mutationFields.length === 0 &&
subscriptionFields.length === 0 &&
graphqlTypes.length === 0
"
class="info"
>
{{ $t("send_request_first") }}
</p>
</AppSection>
</SmartTab>
<SmartTab :id="'history'" :label="$t('history')">
<History
@useHistory="handleUseHistory"
ref="graphqlHistoryComponent"
:page="'graphql'"
/> />
<SmartTabs ref="gqlTabs" styles="m-4"> </SmartTab>
<div class="gqlTabs">
<SmartTab
v-if="queryFields.length > 0"
:id="'queries'"
:label="$t('queries')"
:selected="true"
>
<div v-for="field in filteredQueryFields" :key="field.name">
<GraphqlField :gqlField="field" :jumpTypeCallback="handleJumpToType" />
</div>
</SmartTab>
<SmartTab <!-- <SmartTab :id="'collections'" :label="$t('collections')">
v-if="mutationFields.length > 0" <Collections />
:id="'mutations'" </SmartTab>
:label="$t('mutations')"
>
<div v-for="field in filteredMutationFields" :key="field.name">
<GraphqlField :gqlField="field" :jumpTypeCallback="handleJumpToType" />
</div>
</SmartTab>
<SmartTab <SmartTab :id="'env'" :label="$t('environments')">
v-if="subscriptionFields.length > 0" <Environments @use-environment="useSelectedEnvironment($event)" />
:id="'subscriptions'" </SmartTab>
:label="$t('subscriptions')"
>
<div v-for="field in filteredSubscriptionFields" :key="field.name">
<GraphqlField :gqlField="field" :jumpTypeCallback="handleJumpToType" />
</div>
</SmartTab>
<SmartTab <SmartTab :id="'notes'" :label="$t('notes')">
v-if="graphqlTypes.length > 0" <HttpNotes />
:id="'types'" </SmartTab> -->
:label="$t('types')" </SmartTabs>
ref="typesTab"
>
<div v-for="type in filteredGraphqlTypes" :key="type.name">
<GraphqlType
:gqlType="type"
:gqlTypes="graphqlTypes"
:isHighlighted="isGqlTypeHighlighted({ gqlType: type })"
:highlightedFields="getGqlTypeHighlightedFields({ gqlType: type })"
:jumpTypeCallback="handleJumpToType"
/>
</div>
</SmartTab>
</div>
</SmartTabs>
</section>
<p
v-if="
queryFields.length === 0 &&
mutationFields.length === 0 &&
subscriptionFields.length === 0 &&
graphqlTypes.length === 0
"
class="info"
>
{{ $t("send_request_first") }}
</p>
</AppSection>
</aside> </aside>
</div> </div>
</div> </div>
@@ -412,6 +435,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"
export default { export default {
data() { data() {
@@ -429,6 +453,7 @@ export default {
graphqlFieldsFilterText: undefined, graphqlFieldsFilterText: undefined,
isPollingSchema: false, isPollingSchema: false,
timeoutSubscription: null, timeoutSubscription: null,
activeSidebar: true,
settings: { settings: {
SCROLL_INTO_ENABLED: SCROLL_INTO_ENABLED:
@@ -528,6 +553,17 @@ export default {
next() next()
}, },
methods: { methods: {
useSelectedEnvironment(event) {
console.log("use selected environment")
},
handleUseHistory(entry) {
this.url = entry.url
this.headers = entry.headers
this.gqlQueryString = entry.query
this.response = entry.responseText
this.variableString = entry.variables
this.schema = ""
},
isGqlTypeHighlighted({ gqlType }) { isGqlTypeHighlighted({ gqlType }) {
if (!this.graphqlFieldsFilterText) return false if (!this.graphqlFieldsFilterText) return false
@@ -683,7 +719,13 @@ export default {
}, },
data: JSON.stringify({ query: gqlQueryString, variables }), data: JSON.stringify({ query: gqlQueryString, variables }),
} }
let entry = {
url: this.url,
query: gqlQueryString,
variables: this.variableString,
star: false,
headers: this.headers,
}
const res = await sendNetworkRequest(reqOptions, this.$store) const res = await sendNetworkRequest(reqOptions, this.$store)
// HACK: Temporary trailing null character issue from the extension fix // HACK: Temporary trailing null character issue from the extension fix
@@ -696,6 +738,20 @@ export default {
this.$toast.info(this.$t("finished_in", { duration }), { this.$toast.info(this.$t("finished_in", { duration }), {
icon: "done", icon: "done",
}) })
entry = {
...entry,
response: this.response,
date: new Date().toLocaleDateString(),
time: new Date().toLocaleTimeString(),
}
this.$refs.graphqlHistoryComponent.addEntry(entry)
if (fb.currentUser !== null && fb.currentSettings[2]) {
if (fb.currentSettings[2].value) {
fb.writeGraphqlHistory(entry)
}
}
} catch (error) { } catch (error) {
this.response = `${error}. ${this.$t("check_console_details")}` this.response = `${error}. ${this.$t("check_console_details")}`
this.$nuxt.$loading.finish() this.$nuxt.$loading.finish()

View File

@@ -532,7 +532,7 @@
<section> <section>
<SmartTabs> <SmartTabs>
<SmartTab :id="'history'" :label="$t('history')" :selected="true"> <SmartTab :id="'history'" :label="$t('history')" :selected="true">
<HttpHistory @useHistory="handleUseHistory" ref="historyComponent" /> <History :page="'rest'" @useHistory="handleUseHistory" ref="historyComponent" />
</SmartTab> </SmartTab>
<SmartTab :id="'collections'" :label="$t('collections')"> <SmartTab :id="'collections'" :label="$t('collections')">