refactor: monorepo+pnpm (removed husky)

This commit is contained in:
Andrew Bastin
2021-09-10 00:28:28 +05:30
parent 917550ff4d
commit b28f82a881
445 changed files with 81301 additions and 63752 deletions

View File

@@ -0,0 +1,74 @@
<template>
<SmartModal v-if="show" :title="$t('collection.new')" @close="hideModal">
<template #body>
<div class="flex flex-col px-2">
<input
id="selectLabelGqlAdd"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addNewCollection"
/>
<label for="selectLabelGqlAdd">
{{ $t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary
:label="$t('action.save')"
@click.native="addNewCollection"
/>
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { HoppGQLRequest } from "~/helpers/types/HoppGQLRequest"
import { addGraphqlCollection, makeCollection } from "~/newstore/collections"
export default defineComponent({
props: {
show: Boolean,
},
data() {
return {
name: null as string | null,
}
},
methods: {
addNewCollection() {
if (!this.name) {
this.$toast.error(this.$t("collection.invalid_name").toString(), {
icon: "error_outline",
})
return
}
addGraphqlCollection(
makeCollection<HoppGQLRequest>({
name: this.name,
folders: [],
requests: [],
})
)
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -0,0 +1,66 @@
<template>
<SmartModal
v-if="show"
:title="$t('folder.new')"
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col px-2">
<input
id="selectLabelGqlAddFolder"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addFolder"
/>
<label for="selectLabelGqlAddFolder">
{{ $t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary :label="$t('action.save')" @click.native="addFolder" />
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
export default defineComponent({
props: {
show: Boolean,
folderPath: { type: String, default: null },
collectionIndex: { type: Number, default: null },
},
data() {
return {
name: null,
}
},
methods: {
addFolder() {
// TODO: Blocking when name is null ?
this.$emit("add-folder", {
name: this.name,
path: this.folderPath || `${this.collectionIndex}`,
})
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -0,0 +1,237 @@
<template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-center group"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
>
<span
class="cursor-pointer flex px-4 justify-center items-center"
@click="toggleShowChildren()"
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:name="getCollectionIcon"
/>
</span>
<span
class="
cursor-pointer
flex flex-1
min-w-0
py-2
pr-2
transition
group-hover:text-secondaryDark
"
@click="toggleShowChildren()"
>
<span class="truncate"> {{ collection.name }} </span>
</span>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="folder-plus"
:title="$t('folder.new')"
class="hidden group-hover:inline-flex"
@click.native="
$emit('add-folder', {
path: `${collectionIndex}`,
})
"
/>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
svg="more-vertical"
/>
</template>
<SmartItem
svg="folder-plus"
:label="$t('folder.new')"
@click.native="
$emit('add-folder', {
path: `${collectionIndex}`,
})
$refs.options.tippy().hide()
"
/>
<SmartItem
svg="edit"
:label="$t('action.edit')"
@click.native="
$emit('edit-collection')
$refs.options.tippy().hide()
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
confirmRemove = true
$refs.options.tippy().hide()
"
/>
</tippy>
</span>
</div>
</div>
<div v-if="showChildren || isFiltered">
<CollectionsGraphqlFolder
v-for="(folder, index) in collection.folders"
:key="`folder-${index}`"
class="border-l border-dividerLight ml-6"
:picked="picked"
:saving-mode="savingMode"
:folder="folder"
:folder-index="index"
:folder-path="`${collectionIndex}/${index}`"
:collection-index="collectionIndex"
:doc="doc"
:is-filtered="isFiltered"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
/>
<CollectionsGraphqlRequest
v-for="(request, index) in collection.requests"
:key="`request-${index}`"
class="border-l border-dividerLight ml-6"
:picked="picked"
:saving-mode="savingMode"
:request="request"
:collection-index="collectionIndex"
:folder-index="-1"
:folder-name="collection.name"
:folder-path="`${collectionIndex}`"
:request-index="index"
:doc="doc"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
/>
<div
v-if="
collection.folders.length === 0 && collection.requests.length === 0
"
class="
border-l border-dividerLight
flex flex-col
text-secondaryLight
ml-6
p-4
items-center
justify-center
"
>
<i class="opacity-75 pb-2 material-icons">folder_open</i>
<span class="text-center">
{{ $t("empty.collection") }}
</span>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('confirm.remove_collection')"
@hide-modal="confirmRemove = false"
@resolve="removeCollection"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import {
removeGraphqlCollection,
moveGraphqlRequest,
} from "~/newstore/collections"
export default defineComponent({
props: {
picked: { type: Object, default: null },
// Whether the viewing context is related to picking (activates 'select' events)
savingMode: { type: Boolean, default: false },
collectionIndex: { type: Number, default: null },
collection: { type: Object, default: () => {} },
doc: Boolean,
isFiltered: Boolean,
},
data() {
return {
showChildren: false,
dragging: false,
selectedFolder: {},
confirmRemove: false,
}
},
computed: {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "gql-my-collection" &&
this.picked.collectionIndex === this.collectionIndex
)
},
getCollectionIcon() {
if (this.isSelected) return "check-circle"
else if (!this.showChildren && !this.isFiltered) return "folder"
else if (this.showChildren || this.isFiltered) return "folder-minus"
else return "folder"
},
},
methods: {
pick() {
this.$emit("select", {
picked: {
pickedType: "gql-my-collection",
collectionIndex: this.collectionIndex,
},
})
},
toggleShowChildren() {
if (this.savingMode) {
this.pick()
}
this.showChildren = !this.showChildren
},
removeCollection() {
// Cancel pick if picked collection is deleted
if (
this.picked &&
this.picked.pickedType === "gql-my-collection" &&
this.picked.collectionIndex === this.collectionIndex
) {
this.$emit("select", { picked: null })
}
removeGraphqlCollection(this.collectionIndex)
this.$toast.success(this.$t("state.deleted").toString(), {
icon: "delete",
})
},
dropEvent({ dataTransfer }: any) {
this.dragging = !this.dragging
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
moveGraphqlRequest(folderPath, requestIndex, `${this.collectionIndex}`)
},
},
})
</script>

View File

@@ -0,0 +1,72 @@
<template>
<SmartModal v-if="show" :title="$t('collection.edit')" @close="hideModal">
<template #body>
<div class="flex flex-col px-2">
<input
id="selectLabelGqlEdit"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveCollection"
/>
<label for="selectLabelGqlEdit">
{{ $t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary
:label="$t('action.save')"
@click.native="saveCollection"
/>
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { editGraphqlCollection } from "~/newstore/collections"
export default defineComponent({
props: {
show: Boolean,
editingCollection: { type: Object, default: () => {} },
editingCollectionIndex: { type: Number, default: null },
},
data() {
return {
name: null as string | null,
}
},
methods: {
saveCollection() {
if (!this.name) {
this.$toast.error(this.$t("collection.invalid_name").toString(), {
icon: "error_outline",
})
return
}
const collectionUpdated = {
...(this.editingCollection as any),
name: this.name,
}
editGraphqlCollection(this.editingCollectionIndex, collectionUpdated)
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -0,0 +1,71 @@
<template>
<SmartModal
v-if="show"
:title="$t('folder.edit')"
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col px-2">
<input
id="selectLabelGqlEditFolder"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="editFolder"
/>
<label for="selectLabelGqlEditFolder">
{{ $t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary :label="$t('action.save')" @click.native="editFolder" />
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { editGraphqlFolder } from "~/newstore/collections"
export default defineComponent({
props: {
show: Boolean,
folder: { type: Object, default: () => {} },
folderPath: { type: String, default: null },
},
data() {
return {
name: "",
}
},
methods: {
editFolder() {
if (!this.name) {
this.$toast.error(this.$t("collection.invalid_name").toString(), {
icon: "error_outline",
})
return
}
editGraphqlFolder(this.folderPath, {
...(this.folder as any),
name: this.name,
})
this.hideModal()
},
hideModal() {
this.name = ""
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -0,0 +1,74 @@
<template>
<SmartModal v-if="show" :title="$t('modal.edit_request')" @close="hideModal">
<template #body>
<div class="flex flex-col px-2">
<input
id="selectLabelGqlEditReq"
v-model="requestUpdateData.name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveRequest"
/>
<label for="selectLabelGqlEditReq">
{{ $t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary :label="$t('action.save')" @click.native="saveRequest" />
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent, PropType } from "@nuxtjs/composition-api"
import { HoppGQLRequest } from "~/helpers/types/HoppGQLRequest"
import { editGraphqlRequest } from "~/newstore/collections"
export default defineComponent({
props: {
show: Boolean,
folderPath: { type: String, default: null },
request: { type: Object as PropType<HoppGQLRequest>, default: () => {} },
requestIndex: { type: Number, default: null },
},
data() {
return {
requestUpdateData: {
name: null as any | null,
},
}
},
methods: {
saveRequest() {
if (!this.requestUpdateData.name) {
this.$toast.error(this.$t("collection.invalid_name").toString(), {
icon: "error_outline",
})
return
}
const requestUpdated = {
...this.$props.request,
name: this.$data.requestUpdateData.name || this.$props.request.name,
}
editGraphqlRequest(this.folderPath, this.requestIndex, requestUpdated)
this.hideModal()
},
hideModal() {
this.requestUpdateData = { name: null }
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -0,0 +1,235 @@
<template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-center group"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
>
<span
class="cursor-pointer flex px-4 justify-center items-center"
@click="toggleShowChildren()"
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:name="getCollectionIcon"
/>
</span>
<span
class="
cursor-pointer
flex flex-1
min-w-0
py-2
pr-2
transition
group-hover:text-secondaryDark
"
@click="toggleShowChildren()"
>
<span class="truncate">
{{ folder.name ? folder.name : folder.title }}
</span>
</span>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="folder-plus"
:title="$t('folder.new')"
class="hidden group-hover:inline-flex"
@click.native="$emit('add-folder', { folder, path: folderPath })"
/>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
svg="more-vertical"
/>
</template>
<SmartItem
svg="folder-plus"
:label="$t('folder.new')"
@click.native="
$emit('add-folder', { folder, path: folderPath })
$refs.options.tippy().hide()
"
/>
<SmartItem
svg="edit"
:label="$t('action.edit')"
@click.native="
$emit('edit-folder', { folder, folderPath })
$refs.options.tippy().hide()
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
confirmRemove = true
$refs.options.tippy().hide()
"
/>
</tippy>
</span>
</div>
</div>
<div v-if="showChildren || isFiltered">
<CollectionsGraphqlFolder
v-for="(subFolder, subFolderIndex) in folder.folders"
:key="`subFolder-${subFolderIndex}`"
class="border-l border-dividerLight ml-6"
:picked="picked"
:saving-mode="savingMode"
:folder="subFolder"
:folder-index="subFolderIndex"
:folder-path="`${folderPath}/${subFolderIndex}`"
:collection-index="collectionIndex"
:doc="doc"
:is-filtered="isFiltered"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
/>
<CollectionsGraphqlRequest
v-for="(request, index) in folder.requests"
:key="`request-${index}`"
class="border-l border-dividerLight ml-6"
:picked="picked"
:saving-mode="savingMode"
:request="request"
:collection-index="collectionIndex"
:folder-index="folderIndex"
:folder-path="folderPath"
:folder-name="folder.name"
:request-index="index"
:doc="doc"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
/>
<div
v-if="
folder.folders &&
folder.folders.length === 0 &&
folder.requests &&
folder.requests.length === 0
"
class="
border-l border-dividerLight
flex flex-col
text-secondaryLight
ml-6
p-4
items-center
justify-center
"
>
<i class="opacity-75 pb-2 material-icons">folder_open</i>
<span class="text-center">
{{ $t("empty.folder") }}
</span>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('confirm.remove_folder')"
@hide-modal="confirmRemove = false"
@resolve="removeFolder"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
export default defineComponent({
name: "Folder",
props: {
picked: { type: Object, default: null },
// Whether the request is in a selectable mode (activates 'select' event)
savingMode: { type: Boolean, default: false },
folder: { type: Object, default: () => {} },
folderIndex: { type: Number, default: null },
collectionIndex: { type: Number, default: null },
folderPath: { type: String, default: null },
doc: Boolean,
isFiltered: Boolean,
},
data() {
return {
showChildren: false,
dragging: false,
confirmRemove: false,
}
},
computed: {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "gql-my-folder" &&
this.picked.folderPath === this.folderPath
)
},
getCollectionIcon() {
if (this.isSelected) return "check-circle"
else if (!this.showChildren && !this.isFiltered) return "folder"
else if (this.showChildren || this.isFiltered) return "folder-minus"
else return "folder"
},
},
methods: {
pick() {
this.$emit("select", {
picked: {
pickedType: "gql-my-folder",
folderPath: this.folderPath,
},
})
},
toggleShowChildren() {
if (this.savingMode) {
this.pick()
}
this.showChildren = !this.showChildren
},
removeFolder() {
// Cancel pick if the picked folder is deleted
if (
this.picked &&
this.picked.pickedType === "gql-my-folder" &&
this.picked.folderPath === this.folderPath
) {
this.$emit("select", { picked: null })
}
removeGraphqlFolder(this.folderPath)
this.$toast.success(this.$t("state.deleted").toString(), {
icon: "delete",
})
},
dropEvent({ dataTransfer }: any) {
this.dragging = !this.dragging
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
moveGraphqlRequest(folderPath, requestIndex, this.folderPath)
},
},
})
</script>

View File

@@ -0,0 +1,398 @@
<template>
<SmartModal
v-if="show"
:title="`${$t('modal.import_export')} ${$t('modal.collections')}`"
@close="hideModal"
>
<template #actions>
<span>
<tippy ref="options" interactive trigger="click" theme="popover" arrow>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
class="rounded"
svg="more-vertical"
/>
</template>
<SmartItem
icon="assignment_returned"
:label="$t('import.from_gist')"
@click.native="
readCollectionGist
$refs.options.tippy().hide()
"
/>
<span
v-tippy="{ theme: 'tooltip' }"
:title="
!currentUser
? $t('export.require_github')
: currentUser.provider !== 'github.com'
? $t('export.require_github')
: null
"
>
<SmartItem
:disabled="
!currentUser
? true
: currentUser.provider !== 'github.com'
? true
: false
"
icon="assignment_turned_in"
:label="$t('export.create_secret_gist')"
@click.native="
createCollectionGist()
$refs.options.tippy().hide()
"
/>
</span>
</tippy>
</span>
</template>
<template #body>
<div class="flex flex-col space-y-2">
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.replace_current')"
svg="file"
:label="$t('action.replace_json')"
@click.native="openDialogChooseFileToReplaceWith"
/>
<input
ref="inputChooseFileToReplaceWith"
class="input"
type="file"
accept="application/json"
@change="replaceWithJSON"
/>
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.preserve_current')"
svg="folder-plus"
:label="$t('import.json')"
@click.native="openDialogChooseFileToImportFrom"
/>
<input
ref="inputChooseFileToImportFrom"
class="input"
type="file"
accept="application/json"
@change="importFromJSON"
/>
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
svg="download"
:label="$t('export.as_json')"
@click.native="exportJSON"
/>
</div>
</template>
</SmartModal>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import { currentUser$ } from "~/helpers/fb/auth"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {
graphqlCollections$,
setGraphqlCollections,
appendGraphqlCollections,
} from "~/newstore/collections"
export default defineComponent({
props: {
show: Boolean,
},
setup() {
return {
collections: useReadonlyStream(graphqlCollections$, []),
currentUser: useReadonlyStream(currentUser$, null),
}
},
computed: {
collectionJson() {
return JSON.stringify(this.collections, null, 2)
},
},
methods: {
async createCollectionGist() {
await this.$axios
.$post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-collections.json": {
content: this.collectionJson,
},
},
},
{
headers: {
Authorization: `token ${this.currentUser.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
)
.then((res) => {
this.$toast.success(this.$t("export.gist_created"), {
icon: "done",
})
window.open(res.html_url)
})
.catch((e) => {
this.$toast.error(this.$t("error.something_went_wrong"), {
icon: "error_outline",
})
console.error(e)
})
},
async readCollectionGist() {
const gist = prompt(this.$t("import.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(({ files }) => {
const collections = JSON.parse(Object.values(files)[0].content)
setGraphqlCollections(collections)
this.fileImported()
})
.catch((e) => {
this.failedImport()
console.error(e)
})
},
hideModal() {
this.$emit("hide-modal")
},
openDialogChooseFileToReplaceWith() {
this.$refs.inputChooseFileToReplaceWith.click()
},
openDialogChooseFileToImportFrom() {
this.$refs.inputChooseFileToImportFrom.click()
},
replaceWithJSON() {
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
return
}
setGraphqlCollections(collections)
this.fileImported()
}
reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
this.$refs.inputChooseFileToReplaceWith.value = ""
},
importFromJSON() {
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
// replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
collections = JSON.parse(
content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")
)
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
return
}
appendGraphqlCollections(collections)
this.fileImported()
}
reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
this.$refs.inputChooseFileToImportFrom.value = ""
},
exportJSON() {
const dataToWrite = this.collectionJson
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
this.$toast.success(this.$t("state.download_started"), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
},
fileImported() {
this.$toast.success(this.$t("state.file_imported"), {
icon: "folder_shared",
})
},
failedImport() {
this.$toast.error(this.$t("import.failed"), {
icon: "error_outline",
})
},
parsePostmanCollection({ info, name, item }) {
const hoppscotchCollection = {
name: "",
folders: [],
requests: [],
}
hoppscotchCollection.name = info ? info.name : name
if (item && item.length > 0) {
for (const collectionItem of item) {
if (collectionItem.request) {
if (
Object.prototype.hasOwnProperty.call(
hoppscotchCollection,
"folders"
)
) {
hoppscotchCollection.name = info ? info.name : name
hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
} else {
hoppscotchCollection.name = name || ""
hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
} else if (this.hasFolder(collectionItem)) {
hoppscotchCollection.folders.push(
this.parsePostmanCollection(collectionItem)
)
} else {
hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
}
}
return hoppscotchCollection
},
parsePostmanRequest({ name, request }) {
const pwRequest = {
url: "",
path: "",
method: "",
auth: "",
httpUser: "",
httpPassword: "",
passwordFieldType: "password",
bearerToken: "",
headers: [],
params: [],
bodyParams: [],
rawParams: "",
rawInput: false,
contentType: "",
requestType: "",
name: "",
}
pwRequest.name = name
const requestObjectUrl = request.url.raw.match(
/^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
)
if (requestObjectUrl) {
pwRequest.url = requestObjectUrl[1]
pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
}
pwRequest.method = request.method
const itemAuth = request.auth ? request.auth : ""
const authType = itemAuth ? itemAuth.type : ""
if (authType === "basic") {
pwRequest.auth = "Basic Auth"
pwRequest.httpUser =
itemAuth.basic[0].key === "username"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
pwRequest.httpPassword =
itemAuth.basic[0].key === "password"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
} else if (authType === "oauth2") {
pwRequest.auth = "OAuth 2.0"
pwRequest.bearerToken =
itemAuth.oauth2[0].key === "accessToken"
? itemAuth.oauth2[0].value
: itemAuth.oauth2[1].value
} else if (authType === "bearer") {
pwRequest.auth = "Bearer Token"
pwRequest.bearerToken = itemAuth.bearer[0].value
}
const requestObjectHeaders = request.header
if (requestObjectHeaders) {
pwRequest.headers = requestObjectHeaders
for (const header of pwRequest.headers) {
delete header.name
delete header.type
}
}
const requestObjectParams = request.url.query
if (requestObjectParams) {
pwRequest.params = requestObjectParams
for (const param of pwRequest.params) {
delete param.disabled
}
}
if (request.body) {
if (request.body.mode === "urlencoded") {
const params = request.body.urlencoded
pwRequest.bodyParams = params || []
for (const param of pwRequest.bodyParams) {
delete param.type
}
} else if (request.body.mode === "raw") {
pwRequest.rawInput = true
pwRequest.rawParams = request.body.raw
}
}
return pwRequest
},
hasFolder(item) {
return Object.prototype.hasOwnProperty.call(item, "item")
},
},
})
</script>

View File

@@ -0,0 +1,185 @@
<template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-center group"
draggable="true"
@dragstart="dragStart"
@dragover.stop
@dragleave="dragging = false"
@dragend="dragging = false"
>
<span
class="
cursor-pointer
flex
px-2
w-16
justify-center
items-center
truncate
"
@click="!doc ? selectRequest() : {}"
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:name="isSelected ? 'check-circle' : 'file'"
/>
</span>
<span
class="
cursor-pointer
flex flex-1
min-w-0
py-2
pr-2
transition
group-hover:text-secondaryDark
"
@click="!doc ? selectRequest() : {}"
>
<span class="truncate"> {{ request.name }} </span>
</span>
<div class="flex">
<ButtonSecondary
v-if="!savingMode"
v-tippy="{ theme: 'tooltip' }"
svg="rotate-ccw"
:title="$t('action.restore')"
class="hidden group-hover:inline-flex"
@click.native="!doc ? selectRequest() : {}"
/>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
svg="more-vertical"
/>
</template>
<SmartItem
svg="edit"
:label="$t('action.edit')"
@click.native="
$emit('edit-request', {
request,
requestIndex,
folderPath,
})
$refs.options.tippy().hide()
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
confirmRemove = true
$refs.options.tippy().hide()
"
/>
</tippy>
</span>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('confirm.remove_request')"
@hide-modal="confirmRemove = false"
@resolve="removeRequest"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "@nuxtjs/composition-api"
import { HoppGQLRequest, makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
import { removeGraphqlRequest } from "~/newstore/collections"
import { setGQLSession } from "~/newstore/GQLSession"
export default defineComponent({
props: {
// Whether the object is selected (show the tick mark)
picked: { type: Object, default: null },
// Whether the request is being saved (activate 'select' event)
savingMode: { type: Boolean, default: false },
request: { type: Object as PropType<HoppGQLRequest>, default: () => {} },
folderPath: { type: String, default: null },
requestIndex: { type: Number, default: null },
doc: Boolean,
},
data() {
return {
dragging: false,
confirmRemove: false,
}
},
computed: {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "gql-my-request" &&
this.picked.folderPath === this.folderPath &&
this.picked.requestIndex === this.requestIndex
)
},
},
methods: {
pick() {
this.$emit("select", {
picked: {
pickedType: "gql-my-request",
folderPath: this.folderPath,
requestIndex: this.requestIndex,
},
})
},
selectRequest() {
if (this.savingMode) {
this.pick()
} else {
setGQLSession({
request: makeGQLRequest({
name: this.$props.request.name,
url: this.$props.request.url,
query: this.$props.request.query,
headers: this.$props.request.headers,
variables: this.$props.request.variables,
}),
schema: "",
response: "",
})
}
},
dragStart({ dataTransfer }: any) {
this.dragging = !this.dragging
dataTransfer.setData("folderPath", this.folderPath)
dataTransfer.setData("requestIndex", this.requestIndex)
},
removeRequest() {
// Cancel pick if the picked request is deleted
if (
this.picked &&
this.picked.pickedType === "gql-my-request" &&
this.picked.folderPath === this.folderPath &&
this.picked.requestIndex === this.requestIndex
) {
this.$emit("select", { picked: null })
}
removeGraphqlRequest(this.folderPath, this.requestIndex)
this.$toast.success(this.$t("state.deleted").toString(), {
icon: "delete",
})
},
},
})
</script>

View File

@@ -0,0 +1,282 @@
<template>
<AppSection
label="collections"
:class="{ 'rounded border border-divider': savingMode }"
>
<div
class="
divide-y divide-dividerLight
border-b border-dividerLight
flex flex-col
top-sidebarPrimaryStickyFold
z-10
sticky
"
:class="{ 'bg-primary': !savingMode }"
>
<input
v-if="showCollActions"
v-model="filterText"
type="search"
autocomplete="off"
:placeholder="$t('action.search')"
class="bg-transparent flex w-full py-2 px-4"
/>
<div class="flex flex-1 justify-between">
<ButtonSecondary
svg="plus"
:label="$t('action.new')"
class="!rounded-none"
@click.native="displayModalAdd(true)"
/>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/collections"
blank
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-if="showCollActions"
v-tippy="{ theme: 'tooltip' }"
:title="$t('modal.import_export')"
svg="archive"
@click.native="displayModalImportExport(true)"
/>
</div>
</div>
</div>
<div class="flex-col">
<CollectionsGraphqlCollection
v-for="(collection, index) in filteredCollections"
:key="`collection-${index}`"
:picked="picked"
:name="collection.name"
:collection-index="index"
:collection="collection"
:doc="doc"
:is-filtered="filterText.length > 0"
:saving-mode="savingMode"
@edit-collection="editCollection(collection, index)"
@add-folder="addFolder($event)"
@edit-folder="editFolder($event)"
@edit-request="editRequest($event)"
@select-collection="$emit('use-collection', collection)"
@select="$emit('select', $event)"
/>
</div>
<div
v-if="collections.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
>
<span class="text-center pb-4">
{{ $t("empty.collections") }}
</span>
<ButtonSecondary
:label="$t('add.new')"
filled
@click.native="displayModalAdd(true)"
/>
</div>
<div
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
>
<i class="opacity-75 pb-2 material-icons">manage_search</i>
<span class="text-center">
{{ $t("state.nothing_found") }} "{{ filterText }}"
</span>
</div>
<CollectionsGraphqlAdd
:show="showModalAdd"
@hide-modal="displayModalAdd(false)"
/>
<CollectionsGraphqlEdit
:show="showModalEdit"
:editing-collection="editingCollection"
:editing-collection-index="editingCollectionIndex"
@hide-modal="displayModalEdit(false)"
/>
<CollectionsGraphqlAddFolder
:show="showModalAddFolder"
:folder-path="editingFolderPath"
@add-folder="onAddFolder($event)"
@hide-modal="displayModalAddFolder(false)"
/>
<CollectionsGraphqlEditFolder
:show="showModalEditFolder"
:collection-index="editingCollectionIndex"
:folder="editingFolder"
:folder-index="editingFolderIndex"
:folder-path="editingFolderPath"
@hide-modal="displayModalEditFolder(false)"
/>
<CollectionsGraphqlEditRequest
:show="showModalEditRequest"
:folder-path="editingFolderPath"
:request="editingRequest"
:request-index="editingRequestIndex"
@hide-modal="displayModalEditRequest(false)"
/>
<CollectionsGraphqlImportExport
:show="showModalImportExport"
@hide-modal="displayModalImportExport(false)"
/>
</AppSection>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import clone from "lodash/clone"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { graphqlCollections$, addGraphqlFolder } from "~/newstore/collections"
export default defineComponent({
props: {
// Whether to activate the ability to pick items (activates 'select' events)
savingMode: { type: Boolean, default: false },
doc: { type: Boolean, default: false },
picked: { type: Object, default: null },
// Whether to show the 'New' and 'Import/Export' actions
showCollActions: { type: Boolean, default: true },
},
setup() {
return {
collections: useReadonlyStream(graphqlCollections$, []),
}
},
data() {
return {
showModalAdd: false,
showModalEdit: false,
showModalImportExport: false,
showModalAddFolder: false,
showModalEditFolder: false,
showModalEditRequest: false,
editingCollection: undefined,
editingCollectionIndex: undefined,
editingFolder: undefined,
editingFolderName: undefined,
editingFolderIndex: undefined,
editingFolderPath: undefined,
editingRequest: undefined,
editingRequestIndex: undefined,
filterText: "",
}
},
computed: {
filteredCollections() {
const collections = clone(this.collections)
if (!this.filterText) return collections
const filterText = this.filterText.toLowerCase()
const filteredCollections = []
for (const collection of collections) {
const filteredRequests = []
const filteredFolders = []
for (const request of collection.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredRequests.push(request)
}
for (const folder of collection.folders) {
const filteredFolderRequests = []
for (const request of folder.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredFolderRequests.push(request)
}
if (filteredFolderRequests.length > 0) {
const filteredFolder = Object.assign({}, folder)
filteredFolder.requests = filteredFolderRequests
filteredFolders.push(filteredFolder)
}
}
if (filteredRequests.length + filteredFolders.length > 0) {
const filteredCollection = Object.assign({}, collection)
filteredCollection.requests = filteredRequests
filteredCollection.folders = filteredFolders
filteredCollections.push(filteredCollection)
}
}
return filteredCollections
},
},
methods: {
displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay
},
displayModalEdit(shouldDisplay) {
this.showModalEdit = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalImportExport(shouldDisplay) {
this.showModalImportExport = shouldDisplay
},
displayModalAddFolder(shouldDisplay) {
this.showModalAddFolder = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalEditFolder(shouldDisplay) {
this.showModalEditFolder = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalEditRequest(shouldDisplay) {
this.showModalEditRequest = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
editCollection(collection, collectionIndex) {
this.$data.editingCollection = collection
this.$data.editingCollectionIndex = collectionIndex
this.displayModalEdit(true)
},
onAddFolder({ name, path }) {
addGraphqlFolder(name, path)
this.displayModalAddFolder(false)
},
addFolder(payload) {
const { path } = payload
this.$data.editingFolderPath = path
this.displayModalAddFolder(true)
},
editFolder(payload) {
const { folder, folderPath } = payload
this.editingFolder = folder
this.editingFolderPath = folderPath
this.displayModalEditFolder(true)
},
editRequest(payload) {
const {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
} = payload
this.$data.editingFolderPath = folderPath
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolderIndex = folderIndex
this.$data.editingFolderName = folderName
this.$data.editingRequest = request
this.$data.editingRequestIndex = requestIndex
this.displayModalEditRequest(true)
},
resetSelectedData() {
this.$data.editingCollection = undefined
this.$data.editingCollectionIndex = undefined
this.$data.editingFolder = undefined
this.$data.editingFolderIndex = undefined
this.$data.editingRequest = undefined
this.$data.editingRequestIndex = undefined
},
},
})
</script>