feat: implement export single collection
This commit is contained in:
@@ -126,11 +126,11 @@
|
|||||||
<SmartItem
|
<SmartItem
|
||||||
ref="exportAction"
|
ref="exportAction"
|
||||||
svg="download"
|
svg="download"
|
||||||
:label="$t('export.as_json')"
|
:label="$t('export.export')"
|
||||||
:shortcut="['X']"
|
:shortcut="['X']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
$emit('export-collection')
|
exportCollection()
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -265,6 +265,23 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
exportCollection() {
|
||||||
|
const collectionJSON = JSON.stringify(this.collection)
|
||||||
|
|
||||||
|
const file = new Blob([collectionJSON], { type: "application/json" })
|
||||||
|
const a = document.createElement("a")
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
|
||||||
|
a.download = `${this.collection.name}.json`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
this.$toast.success(this.$t("state.download_started").toString())
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
toggleShowChildren() {
|
toggleShowChildren() {
|
||||||
if (this.$props.saveRequest)
|
if (this.$props.saveRequest)
|
||||||
this.$emit("select", {
|
this.$emit("select", {
|
||||||
@@ -284,7 +301,7 @@ export default defineComponent({
|
|||||||
collectionID: this.collection.id,
|
collectionID: this.collection.id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dropEvent({ dataTransfer }) {
|
dropEvent({ dataTransfer }: any) {
|
||||||
this.dragging = !this.dragging
|
this.dragging = !this.dragging
|
||||||
const folderPath = dataTransfer.getData("folderPath")
|
const folderPath = dataTransfer.getData("folderPath")
|
||||||
const requestIndex = dataTransfer.getData("requestIndex")
|
const requestIndex = dataTransfer.getData("requestIndex")
|
||||||
|
|||||||
@@ -106,11 +106,11 @@
|
|||||||
<SmartItem
|
<SmartItem
|
||||||
ref="exportAction"
|
ref="exportAction"
|
||||||
svg="download"
|
svg="download"
|
||||||
:label="$t('export.as_json')"
|
:label="$t('export.export')"
|
||||||
:shortcut="['X']"
|
:shortcut="['X']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
$emit('export-collection')
|
exportFolder()
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -253,6 +253,23 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
exportFolder() {
|
||||||
|
const folderJSON = JSON.stringify(this.folder)
|
||||||
|
|
||||||
|
const file = new Blob([folderJSON], { type: "application/json" })
|
||||||
|
const a = document.createElement("a")
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
|
||||||
|
a.download = `${this.folder.name}.json`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
this.$toast.success(this.$t("state.download_started").toString())
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
toggleShowChildren() {
|
toggleShowChildren() {
|
||||||
if (this.$props.saveRequest)
|
if (this.$props.saveRequest)
|
||||||
this.$emit("select", {
|
this.$emit("select", {
|
||||||
|
|||||||
@@ -127,18 +127,10 @@
|
|||||||
<SmartItem
|
<SmartItem
|
||||||
ref="exportAction"
|
ref="exportAction"
|
||||||
svg="download"
|
svg="download"
|
||||||
:label="$t('export.as_json')"
|
:label="$t('export.export')"
|
||||||
:shortcut="['X']"
|
:shortcut="['X']"
|
||||||
:loading="exportLoading"
|
:loading="exportLoading"
|
||||||
@click.native="
|
@click.native="exportCollection"
|
||||||
() => {
|
|
||||||
$emit('export-collection')
|
|
||||||
// TODO: remove the below line
|
|
||||||
exportLoading = true
|
|
||||||
// TODO: remove the below line, instead hide the tooltip after finishing export
|
|
||||||
options.tippy().hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</tippy>
|
</tippy>
|
||||||
@@ -230,6 +222,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
|
import {
|
||||||
|
getCompleteCollectionTree,
|
||||||
|
teamCollToHoppRESTColl,
|
||||||
|
} from "~/helpers/backend/helpers"
|
||||||
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
|
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
|
||||||
import { useI18n } from "~/helpers/utils/composables"
|
import { useI18n } from "~/helpers/utils/composables"
|
||||||
|
|
||||||
@@ -286,7 +282,44 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
editRequest(event) {
|
async exportCollection() {
|
||||||
|
this.exportLoading = true
|
||||||
|
|
||||||
|
const result = await getCompleteCollectionTree(this.collection.id)()
|
||||||
|
|
||||||
|
if (E.isLeft(result)) {
|
||||||
|
this.$toast.error(this.$t("error.something_went_wrong").toString())
|
||||||
|
console.log(result.left)
|
||||||
|
this.exportLoading = false
|
||||||
|
this.options.tippy().hide()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoppColl = teamCollToHoppRESTColl(result.right)
|
||||||
|
|
||||||
|
const collectionJSON = JSON.stringify(hoppColl)
|
||||||
|
|
||||||
|
const file = new Blob([collectionJSON], { type: "application/json" })
|
||||||
|
const a = document.createElement("a")
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
|
||||||
|
a.download = `${hoppColl.name}.json`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
this.$toast.success(this.$t("state.download_started").toString())
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
this.exportLoading = false
|
||||||
|
|
||||||
|
this.options.tippy().hide()
|
||||||
|
},
|
||||||
|
editRequest(event: any) {
|
||||||
this.$emit("edit-request", event)
|
this.$emit("edit-request", event)
|
||||||
if (this.$props.saveRequest)
|
if (this.$props.saveRequest)
|
||||||
this.$emit("select", {
|
this.$emit("select", {
|
||||||
@@ -315,10 +348,10 @@ export default defineComponent({
|
|||||||
collectionID: this.collection.id,
|
collectionID: this.collection.id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
expandCollection(collectionID) {
|
expandCollection(collectionID: string) {
|
||||||
this.$emit("expand-collection", collectionID)
|
this.$emit("expand-collection", collectionID)
|
||||||
},
|
},
|
||||||
async dropEvent({ dataTransfer }) {
|
async dropEvent({ dataTransfer }: any) {
|
||||||
this.dragging = !this.dragging
|
this.dragging = !this.dragging
|
||||||
const requestIndex = dataTransfer.getData("requestIndex")
|
const requestIndex = dataTransfer.getData("requestIndex")
|
||||||
const moveRequestResult = await moveRESTTeamRequest(
|
const moveRequestResult = await moveRESTTeamRequest(
|
||||||
@@ -328,7 +361,7 @@ export default defineComponent({
|
|||||||
if (E.isLeft(moveRequestResult))
|
if (E.isLeft(moveRequestResult))
|
||||||
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
|
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
|
||||||
},
|
},
|
||||||
removeRequest({ collectionIndex, folderName, requestIndex }) {
|
removeRequest({ collectionIndex, folderName, requestIndex }: any) {
|
||||||
this.$emit("remove-request", {
|
this.$emit("remove-request", {
|
||||||
collectionIndex,
|
collectionIndex,
|
||||||
folderName,
|
folderName,
|
||||||
|
|||||||
@@ -108,18 +108,10 @@
|
|||||||
<SmartItem
|
<SmartItem
|
||||||
ref="exportAction"
|
ref="exportAction"
|
||||||
svg="download"
|
svg="download"
|
||||||
:label="$t('export.as_json')"
|
:label="$t('export.export')"
|
||||||
:shortcut="['X']"
|
:shortcut="['X']"
|
||||||
:loading="exportLoading"
|
:loading="exportLoading"
|
||||||
@click.native="
|
@click.native="exportFolder"
|
||||||
() => {
|
|
||||||
$emit('export-collection')
|
|
||||||
// TODO: remove the below line
|
|
||||||
exportLoading = true
|
|
||||||
// TODO: remove the below line, instead hide the tooltip after finishing export
|
|
||||||
options.tippy().hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</tippy>
|
</tippy>
|
||||||
@@ -211,6 +203,10 @@ import { defineComponent, ref } from "@nuxtjs/composition-api"
|
|||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||||
import { DeleteCollectionDocument } from "~/helpers/backend/graphql"
|
import { DeleteCollectionDocument } from "~/helpers/backend/graphql"
|
||||||
|
import {
|
||||||
|
getCompleteCollectionTree,
|
||||||
|
teamCollToHoppRESTColl,
|
||||||
|
} from "~/helpers/backend/helpers"
|
||||||
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
|
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -263,6 +259,43 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async exportFolder() {
|
||||||
|
this.exportLoading = true
|
||||||
|
|
||||||
|
const result = await getCompleteCollectionTree(this.folder.id)()
|
||||||
|
|
||||||
|
if (E.isLeft(result)) {
|
||||||
|
this.$toast.error(this.$t("error.something_went_wrong").toString())
|
||||||
|
console.log(result.left)
|
||||||
|
this.exportLoading = false
|
||||||
|
this.options.tippy().hide()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoppColl = teamCollToHoppRESTColl(result.right)
|
||||||
|
|
||||||
|
const collectionJSON = JSON.stringify(hoppColl)
|
||||||
|
|
||||||
|
const file = new Blob([collectionJSON], { type: "application/json" })
|
||||||
|
const a = document.createElement("a")
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
|
||||||
|
a.download = `${hoppColl.name}.json`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
this.$toast.success(this.$t("state.download_started").toString())
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
this.exportLoading = false
|
||||||
|
|
||||||
|
this.options.tippy().hide()
|
||||||
|
},
|
||||||
toggleShowChildren() {
|
toggleShowChildren() {
|
||||||
if (this.$props.saveRequest)
|
if (this.$props.saveRequest)
|
||||||
this.$emit("select", {
|
this.$emit("select", {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
query GetCollectionChildrenIDs($collectionID: ID!, $cursor: String) {
|
||||||
|
collection(collectionID: $collectionID) {
|
||||||
|
children(cursor: $cursor) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
query GetCollectionTitle($collectionID: ID!) {
|
||||||
|
collection(collectionID: $collectionID) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
127
packages/hoppscotch-app/helpers/backend/helpers.ts
Normal file
127
packages/hoppscotch-app/helpers/backend/helpers.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import * as A from "fp-ts/Array"
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import { pipe, flow } from "fp-ts/function"
|
||||||
|
import {
|
||||||
|
HoppCollection,
|
||||||
|
HoppRESTRequest,
|
||||||
|
makeCollection,
|
||||||
|
translateToNewRequest,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
import { TeamCollection } from "../teams/TeamCollection"
|
||||||
|
import { TeamRequest } from "../teams/TeamRequest"
|
||||||
|
import { GQLError, runGQLQuery } from "./GQLClient"
|
||||||
|
import {
|
||||||
|
GetCollectionChildrenIDsDocument,
|
||||||
|
GetCollectionRequestsDocument,
|
||||||
|
GetCollectionTitleDocument,
|
||||||
|
} from "./graphql"
|
||||||
|
|
||||||
|
const BACKEND_PAGE_SIZE = 10
|
||||||
|
|
||||||
|
const getCollectionChildrenIDs = async (collID: string) => {
|
||||||
|
const collsList: string[] = []
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const data = await runGQLQuery({
|
||||||
|
query: GetCollectionChildrenIDsDocument,
|
||||||
|
variables: {
|
||||||
|
collectionID: collID,
|
||||||
|
cursor:
|
||||||
|
collsList.length > 0 ? collsList[collsList.length - 1] : undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (E.isLeft(data)) {
|
||||||
|
return E.left(data.left)
|
||||||
|
}
|
||||||
|
|
||||||
|
collsList.push(...data.right.collection!.children.map((x) => x.id))
|
||||||
|
|
||||||
|
if (data.right.collection!.children.length !== BACKEND_PAGE_SIZE) break
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right(collsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCollectionRequests = async (collID: string) => {
|
||||||
|
const reqList: TeamRequest[] = []
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const data = await runGQLQuery({
|
||||||
|
query: GetCollectionRequestsDocument,
|
||||||
|
variables: {
|
||||||
|
collectionID: collID,
|
||||||
|
cursor: reqList.length > 0 ? reqList[reqList.length - 1].id : undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (E.isLeft(data)) {
|
||||||
|
return E.left(data.left)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqList.push(
|
||||||
|
...data.right.requestsInCollection.map(
|
||||||
|
(x) =>
|
||||||
|
<TeamRequest>{
|
||||||
|
id: x.id,
|
||||||
|
request: translateToNewRequest(JSON.parse(x.request)),
|
||||||
|
collectionID: collID,
|
||||||
|
title: x.title,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (data.right.requestsInCollection.length !== BACKEND_PAGE_SIZE) break
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right(reqList)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCompleteCollectionTree = (
|
||||||
|
collID: string
|
||||||
|
): TE.TaskEither<GQLError<string>, TeamCollection> =>
|
||||||
|
pipe(
|
||||||
|
TE.Do,
|
||||||
|
|
||||||
|
TE.bind("title", () =>
|
||||||
|
pipe(
|
||||||
|
() =>
|
||||||
|
runGQLQuery({
|
||||||
|
query: GetCollectionTitleDocument,
|
||||||
|
variables: {
|
||||||
|
collectionID: collID,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
TE.map((x) => x.collection!.title)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
TE.bind("children", () =>
|
||||||
|
pipe(
|
||||||
|
// TaskEither -> () => Promise<Either>
|
||||||
|
() => getCollectionChildrenIDs(collID),
|
||||||
|
TE.chain(flow(A.map(getCompleteCollectionTree), TE.sequenceArray))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
TE.bind("requests", () => () => getCollectionRequests(collID)),
|
||||||
|
|
||||||
|
TE.map(
|
||||||
|
({ title, children, requests }) =>
|
||||||
|
<TeamCollection>{
|
||||||
|
id: collID,
|
||||||
|
children,
|
||||||
|
requests,
|
||||||
|
title,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const teamCollToHoppRESTColl = (
|
||||||
|
coll: TeamCollection
|
||||||
|
): HoppCollection<HoppRESTRequest> =>
|
||||||
|
makeCollection({
|
||||||
|
name: coll.title,
|
||||||
|
folders: coll.children?.map(teamCollToHoppRESTColl) ?? [],
|
||||||
|
requests: coll.requests?.map((x) => x.request) ?? [],
|
||||||
|
})
|
||||||
@@ -187,6 +187,7 @@
|
|||||||
"test_script_fail": "Could not execute post-request script"
|
"test_script_fail": "Could not execute post-request script"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
|
"export": "Export",
|
||||||
"as_json": "Export as JSON",
|
"as_json": "Export as JSON",
|
||||||
"create_secret_gist": "Create secret Gist",
|
"create_secret_gist": "Create secret Gist",
|
||||||
"gist_created": "Gist created",
|
"gist_created": "Gist created",
|
||||||
|
|||||||
Reference in New Issue
Block a user