Files
hoppscotch/packages/hoppscotch-common/src/components/collections/MyCollections.vue

730 lines
20 KiB
Vue

<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
:style="
saveRequest
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
: 'top: var(--upper-primary-sticky-fold)'
"
>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('action.new')"
class="!rounded-none"
@click="emit('display-modal-add')"
/>
<span class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/collections"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }"
:icon="IconArchive"
:title="t('modal.import_export')"
@click="emit('display-modal-import-export')"
/>
</span>
</div>
<div class="flex flex-col flex-1">
<HoppSmartTree :adapter="myAdapter">
<template
#content="{ node, toggleChildren, isOpen, highlightChildren }"
>
<CollectionsCollection
v-if="node.data.type === 'collections'"
:id="node.id"
:parent-i-d="node.data.data.parentIndex"
:data="node.data.data.data"
:collections-type="collectionsType.type"
:is-open="isOpen"
:is-last-item="node.data.isLastItem"
:is-selected="
isSelected({
collectionIndex: parseInt(node.id),
})
"
folder-type="collection"
@add-request="
node.data.type === 'collections' &&
emit('add-request', {
path: node.id,
folder: node.data.data.data,
})
"
@add-folder="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
@edit-collection="
node.data.type === 'collections' &&
emit('edit-collection', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data="
node.data.type === 'collections' &&
emit('export-data', node.data.data.data)
"
@remove-collection="emit('remove-collection', node.id)"
@drop-event="dropEvent($event, node.id)"
@drag-event="dragEvent($event, node.id)"
@update-collection-order="
updateCollectionOrder($event, {
destinationCollectionIndex: node.id,
destinationCollectionParentIndex: node.data.data.parentIndex,
})
"
@update-last-collection-order="
updateCollectionOrder($event, {
destinationCollectionIndex: null,
destinationCollectionParentIndex: node.data.data.parentIndex,
})
"
@dragging="
(isDraging) => highlightChildren(isDraging ? node.id : null)
"
@toggle-children="
() => {
toggleChildren(),
saveRequest &&
emit('select', {
pickedType: 'my-collection',
collectionIndex: parseInt(node.id),
})
}
"
/>
<CollectionsCollection
v-if="node.data.type === 'folders'"
:id="node.id"
:parent-i-d="node.data.data.parentIndex"
:data="node.data.data.data"
:collections-type="collectionsType.type"
:is-open="isOpen"
:is-last-item="node.data.isLastItem"
:is-selected="
isSelected({
folderPath: node.id,
})
"
folder-type="folder"
@add-request="
node.data.type === 'folders' &&
emit('add-request', {
path: node.id,
folder: node.data.data.data,
})
"
@add-folder="
node.data.type === 'folders' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
@edit-collection="
node.data.type === 'folders' &&
emit('edit-folder', {
folderPath: node.id,
folder: node.data.data.data,
})
"
@export-data="
node.data.type === 'folders' &&
emit('export-data', node.data.data.data)
"
@remove-collection="emit('remove-folder', node.id)"
@drop-event="dropEvent($event, node.id)"
@drag-event="dragEvent($event, node.id)"
@update-collection-order="
updateCollectionOrder($event, {
destinationCollectionIndex: node.id,
destinationCollectionParentIndex: node.data.data.parentIndex,
})
"
@update-last-collection-order="
updateCollectionOrder($event, {
destinationCollectionIndex: null,
destinationCollectionParentIndex: node.data.data.parentIndex,
})
"
@dragging="
(isDraging) => highlightChildren(isDraging ? node.id : null)
"
@toggle-children="
() => {
toggleChildren(),
saveRequest &&
emit('select', {
pickedType: 'my-folder',
folderPath: node.id,
})
}
"
/>
<CollectionsRequest
v-if="node.data.type === 'requests'"
:request="node.data.data.data"
:request-i-d="node.id"
:parent-i-d="node.data.data.parentIndex"
:collections-type="collectionsType.type"
:save-request="saveRequest"
:is-last-item="node.data.isLastItem"
:is-active="
isActiveRequest(
node.data.data.parentIndex,
parseInt(pathToIndex(node.id))
)
"
:is-selected="
isSelected({
folderPath: node.data.data.parentIndex,
requestIndex: parseInt(pathToIndex(node.id)),
})
"
@edit-request="
node.data.type === 'requests' &&
emit('edit-request', {
folderPath: node.data.data.parentIndex,
requestIndex: pathToIndex(node.id),
request: node.data.data.data,
})
"
@duplicate-request="
node.data.type === 'requests' &&
emit('duplicate-request', {
folderPath: node.data.data.parentIndex,
request: node.data.data.data,
})
"
@remove-request="
node.data.type === 'requests' &&
emit('remove-request', {
folderPath: node.data.data.parentIndex,
requestIndex: pathToIndex(node.id),
})
"
@select-request="
node.data.type === 'requests' &&
selectRequest({
request: node.data.data.data,
folderPath: node.data.data.parentIndex,
requestIndex: pathToIndex(node.id),
})
"
@drag-request="
dragRequest($event, {
folderPath: node.data.data.parentIndex,
requestIndex: node.id,
})
"
@update-request-order="
updateRequestOrder($event, {
folderPath: node.data.data.parentIndex,
requestIndex: node.id,
})
"
@update-last-request-order="
updateRequestOrder($event, {
folderPath: node.data.data.parentIndex,
requestIndex: null,
})
"
/>
</template>
<template #emptyNode="{ node }">
<HoppSmartPlaceholder
v-if="filterText.length !== 0 && filteredCollections.length === 0"
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node === null"
:src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="emit('display-modal-add')"
/>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'"
:src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'"
:src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.folder')}`"
:text="t('empty.folder')"
>
</HoppSmartPlaceholder>
</template>
</HoppSmartTree>
</div>
</div>
</template>
<script setup lang="ts">
import IconArchive from "~icons/lucide/archive"
import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import {
ChildrenResult,
SmartTreeAdapter,
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import { Picked } from "~/helpers/types/HoppPicked.js"
import { currentActiveTab } from "~/helpers/rest/tab"
export type Collection = {
type: "collections"
isLastItem: boolean
data: {
parentIndex: null
data: HoppCollection<HoppRESTRequest>
}
}
type Folder = {
type: "folders"
isLastItem: boolean
data: {
parentIndex: string
data: HoppCollection<HoppRESTRequest>
}
}
type Requests = {
type: "requests"
isLastItem: boolean
data: {
parentIndex: string
data: HoppRESTRequest
}
}
const t = useI18n()
const colorMode = useColorMode()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
| {
type: "team-collections"
selectedTeam: SelectedTeam
}
| { type: "my-collections"; selectedTeam: undefined }
const props = defineProps({
filteredCollections: {
type: Array as PropType<HoppCollection<HoppRESTRequest>[]>,
default: () => [],
required: true,
},
collectionsType: {
type: Object as PropType<CollectionType>,
default: () => ({ type: "my-collections", selectedTeam: undefined }),
required: true,
},
filterText: {
type: String as PropType<string>,
default: "",
required: true,
},
saveRequest: {
type: Boolean,
default: false,
required: false,
},
picked: {
type: Object as PropType<Picked | null>,
default: null,
required: false,
},
})
const emit = defineEmits<{
(event: "display-modal-add"): void
(
event: "add-request",
payload: {
path: string
folder: HoppCollection<HoppRESTRequest>
}
): void
(
event: "add-folder",
payload: {
path: string
folder: HoppCollection<HoppRESTRequest>
}
): void
(
event: "edit-collection",
payload: {
collectionIndex: string
collection: HoppCollection<HoppRESTRequest>
}
): void
(
event: "edit-folder",
payload: {
folderPath: string
folder: HoppCollection<HoppRESTRequest>
}
): void
(
event: "edit-request",
payload: {
folderPath: string
requestIndex: string
request: HoppRESTRequest
}
): void
(
event: "duplicate-request",
payload: {
folderPath: string
request: HoppRESTRequest
}
): void
(event: "export-data", payload: HoppCollection<HoppRESTRequest>): void
(event: "remove-collection", payload: string): void
(event: "remove-folder", payload: string): void
(
event: "remove-request",
payload: {
folderPath: string | null
requestIndex: string
}
): void
(
event: "select-request",
payload: {
request: HoppRESTRequest
folderPath: string
requestIndex: string
isActive: boolean
}
): void
(
event: "drop-request",
payload: {
folderPath: string
requestIndex: string
destinationCollectionIndex: string
}
): void
(
event: "drop-collection",
payload: {
collectionIndexDragged: string
destinationCollectionIndex: string
}
): void
(
event: "update-request-order",
payload: {
dragedRequestIndex: string
destinationRequestIndex: string | null
destinationCollectionIndex: string
}
): void
(
event: "update-collection-order",
payload: {
dragedCollectionIndex: string
destinationCollection: {
destinationCollectionIndex: string | null
destinationCollectionParentIndex: string | null
}
}
): void
(event: "select", payload: Picked | null): void
(event: "display-modal-import-export"): void
}>()
const refFilterCollection = toRef(props, "filteredCollections")
const pathToIndex = (path: string) => {
const pathArr = path.split("/")
return pathArr[pathArr.length - 1]
}
const isSelected = ({
collectionIndex,
folderPath,
requestIndex,
}: {
collectionIndex?: number | undefined
folderPath?: string | undefined
requestIndex?: number | undefined
}) => {
if (collectionIndex !== undefined) {
return (
props.picked &&
props.picked.pickedType === "my-collection" &&
props.picked.collectionIndex === collectionIndex
)
} else if (requestIndex !== undefined && folderPath !== undefined) {
return (
props.picked &&
props.picked.pickedType === "my-request" &&
props.picked.folderPath === folderPath &&
props.picked.requestIndex === requestIndex
)
} else {
return (
props.picked &&
props.picked.pickedType === "my-folder" &&
props.picked.folderPath === folderPath
)
}
}
const active = computed(() => currentActiveTab.value.document.saveContext)
const isActiveRequest = (folderPath: string, requestIndex: number) => {
return pipe(
active.value,
O.fromNullable,
O.filter(
(active) =>
active.originLocation === "user-collection" &&
active.folderPath === folderPath &&
active.requestIndex === requestIndex
),
O.isSome
)
}
const selectRequest = (data: {
request: HoppRESTRequest
folderPath: string
requestIndex: string
}) => {
const { request, folderPath, requestIndex } = data
if (props.saveRequest) {
emit("select", {
pickedType: "my-request",
folderPath: folderPath,
requestIndex: parseInt(requestIndex),
})
} else {
emit("select-request", {
request,
folderPath,
requestIndex,
isActive: isActiveRequest(folderPath, parseInt(requestIndex)),
})
}
}
const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
dataTransfer.setData("collectionIndex", collectionIndex)
}
const dragRequest = (
dataTransfer: DataTransfer,
{
folderPath,
requestIndex,
}: { folderPath: string | null; requestIndex: string }
) => {
if (!folderPath) return
dataTransfer.setData("folderPath", folderPath)
dataTransfer.setData("requestIndex", requestIndex)
}
const dropEvent = (
dataTransfer: DataTransfer,
destinationCollectionIndex: string
) => {
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
if (folderPath && requestIndex) {
emit("drop-request", {
folderPath,
requestIndex,
destinationCollectionIndex,
})
} else {
emit("drop-collection", {
collectionIndexDragged,
destinationCollectionIndex,
})
}
}
const updateRequestOrder = (
dataTransfer: DataTransfer,
{
folderPath,
requestIndex,
}: { folderPath: string | null; requestIndex: string | null }
) => {
if (!folderPath) return
const dragedRequestIndex = dataTransfer.getData("requestIndex")
const destinationRequestIndex = requestIndex
const destinationCollectionIndex = folderPath
emit("update-request-order", {
dragedRequestIndex,
destinationRequestIndex,
destinationCollectionIndex,
})
}
const updateCollectionOrder = (
dataTransfer: DataTransfer,
destinationCollection: {
destinationCollectionIndex: string | null
destinationCollectionParentIndex: string | null
}
) => {
const dragedCollectionIndex = dataTransfer.getData("collectionIndex")
emit("update-collection-order", {
dragedCollectionIndex,
destinationCollection,
})
}
type MyCollectionNode = Collection | Folder | Requests
class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
constructor(public data: Ref<HoppCollection<HoppRESTRequest>[]>) {}
navigateToFolderWithIndexPath(
collections: HoppCollection<HoppRESTRequest>[],
indexPaths: number[]
) {
if (indexPaths.length === 0) return null
let target = collections[indexPaths.shift() as number]
while (indexPaths.length > 0)
target = target.folders[indexPaths.shift() as number]
return target !== undefined ? target : null
}
getChildren(id: string | null): Ref<ChildrenResult<MyCollectionNode>> {
return computed(() => {
if (id === null) {
const data = this.data.value.map((item, index) => ({
id: index.toString(),
data: {
type: "collections",
isLastItem: index === this.data.value.length - 1,
data: {
parentIndex: null,
data: item,
},
},
}))
return {
status: "loaded",
data: data,
} as ChildrenResult<Collection>
}
const indexPath = id.split("/").map((x) => parseInt(x))
const item = this.navigateToFolderWithIndexPath(
this.data.value,
indexPath
)
if (item) {
const data = [
...item.folders.map((folder, index) => ({
id: `${id}/${index}`,
data: {
isLastItem:
item.folders && item.folders.length > 1
? index === item.folders.length - 1
: false,
type: "folders",
data: {
parentIndex: id,
data: folder,
},
},
})),
...item.requests.map((requests, index) => ({
id: `${id}/${index}`,
data: {
isLastItem:
item.requests && item.requests.length > 1
? index === item.requests.length - 1
: false,
type: "requests",
data: {
parentIndex: id,
data: requests,
},
},
})),
]
return {
status: "loaded",
data: data,
} as ChildrenResult<Folder | Requests>
} else {
return {
status: "loaded",
data: [],
}
}
})
}
}
const myAdapter: SmartTreeAdapter<MyCollectionNode> = new MyCollectionsAdapter(
refFilterCollection
)
</script>