Files
hoppscotch/packages/hoppscotch-common/src/components/collections/TeamCollections.vue
Anwarul Islam defece95fc feat: rest revamp (#2918)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-03-31 00:45:42 +05:30

755 lines
21 KiB
Vue

<template>
<div class="flex flex-col flex-1 bg-primary">
<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
v-if="hasNoTeamAccess"
v-tippy="{ theme: 'tooltip' }"
disabled
class="!rounded-none"
:icon="IconPlus"
:title="t('team.no_access')"
:label="t('action.new')"
/>
<HoppButtonSecondary
v-else
: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/features/collections"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }"
:disabled="
collectionsType.type === 'team-collections' &&
collectionsType.selectedTeam === undefined
"
:icon="IconArchive"
:title="t('modal.import_export')"
@click="emit('display-modal-import-export')"
/>
</span>
</div>
<div class="flex flex-col overflow-hidden">
<SmartTree :adapter="teamAdapter">
<template
#content="{ node, toggleChildren, isOpen, highlightChildren }"
>
<CollectionsCollection
v-if="node.data.type === 'collections'"
:id="node.data.data.data.id"
:parent-i-d="node.data.data.parentIndex"
:data="node.data.data.data"
:collections-type="collectionsType.type"
:is-open="isOpen"
:export-loading="exportLoading"
:has-no-team-access="hasNoTeamAccess"
:collection-move-loading="collectionMoveLoading"
:is-selected="
isSelected({
collectionID: 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, node.data.data.data.id)
"
@dragging="
(isDraging) =>
highlightChildren(isDraging ? node.data.data.data.id : null)
"
@toggle-children="
() => {
toggleChildren(),
saveRequest &&
emit('select', {
pickedType: 'teams-collection',
collectionID: node.id,
})
}
"
/>
<CollectionsCollection
v-if="node.data.type === 'folders'"
:id="node.data.data.data.id"
:parent-i-d="node.data.data.parentIndex"
:data="node.data.data.data"
:collections-type="collectionsType.type"
:is-open="isOpen"
:export-loading="exportLoading"
:has-no-team-access="hasNoTeamAccess"
:collection-move-loading="collectionMoveLoading"
:is-selected="
isSelected({
folderID: node.data.data.data.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', {
folder: node.data.data.data,
})
"
@export-data="
node.data.type === 'folders' &&
emit('export-data', node.data.data.data)
"
@remove-collection="
node.data.type === 'folders' &&
emit('remove-folder', node.data.data.data.id)
"
@drop-event="dropEvent($event, node.data.data.data.id)"
@drag-event="dragEvent($event, node.data.data.data.id)"
@update-collection-order="
updateCollectionOrder($event, node.data.data.data.id)
"
@dragging="
(isDraging) =>
highlightChildren(isDraging ? node.data.data.data.id : null)
"
@toggle-children="
() => {
toggleChildren(),
saveRequest &&
emit('select', {
pickedType: 'teams-folder',
folderID: node.data.data.data.id,
})
}
"
/>
<CollectionsRequest
v-if="node.data.type === 'requests'"
:request="node.data.data.data.request"
:request-i-d="node.data.data.data.id"
:parent-i-d="node.data.data.parentIndex"
:collections-type="collectionsType.type"
:duplicate-loading="duplicateLoading"
:is-active="isActiveRequest(node.data.data.data.id)"
:has-no-team-access="hasNoTeamAccess"
:request-move-loading="requestMoveLoading"
:is-selected="
isSelected({
requestID: node.data.data.data.id,
})
"
@edit-request="
node.data.type === 'requests' &&
emit('edit-request', {
requestIndex: node.data.data.data.id,
request: node.data.data.data.request,
})
"
@duplicate-request="
node.data.type === 'requests' &&
emit('duplicate-request', {
folderPath: node.data.data.parentIndex,
request: node.data.data.data.request,
})
"
@remove-request="
node.data.type === 'requests' &&
emit('remove-request', {
folderPath: null,
requestIndex: node.data.data.data.id,
})
"
@select-request="
node.data.type === 'requests' &&
selectRequest({
request: node.data.data.data.request,
requestIndex: node.data.data.data.id,
})
"
@drag-request="
dragRequest($event, {
folderPath: node.data.data.parentIndex,
requestIndex: node.data.data.data.id,
})
"
@update-request-order="
updateRequestOrder($event, {
folderPath: node.data.data.parentIndex,
requestIndex: node.data.data.data.id,
})
"
/>
</template>
<template #emptyNode="{ node }">
<div v-if="node === null">
<div
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
@drop="(e) => e.stopPropagation()"
>
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.collection')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.collections") }}
</span>
<HoppButtonSecondary
v-if="hasNoTeamAccess"
v-tippy="{ theme: 'tooltip' }"
disabled
filled
outline
:title="t('team.no_access')"
:label="t('action.new')"
/>
<HoppButtonSecondary
v-else
:icon="IconPlus"
:label="t('action.new')"
filled
outline
@click="emit('display-modal-add')"
/>
</div>
</div>
<div
v-else-if="node.data.type === 'collections'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
@drop="(e) => e.stopPropagation()"
>
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.collection')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.collections") }}
</span>
</div>
<div
v-else-if="node.data.type === 'folders'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
@drop="(e) => e.stopPropagation()"
>
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.folder')}`"
/>
<span class="text-center">
{{ t("empty.folder") }}
</span>
</div>
</template>
</SmartTree>
</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 { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { TeamRequest } from "~/helpers/teams/TeamRequest"
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
import { cloneDeep } from "lodash-es"
import { HoppRESTRequest } from "@hoppscotch/data"
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"
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({
collectionsType: {
type: Object as PropType<CollectionType>,
default: () => ({ type: "my-collections", selectedTeam: undefined }),
required: true,
},
teamCollectionList: {
type: Array as PropType<TeamCollection[]>,
default: () => [],
required: true,
},
teamLoadingCollections: {
type: Array as PropType<string[]>,
default: () => [],
required: true,
},
saveRequest: {
type: Boolean,
default: false,
required: false,
},
exportLoading: {
type: Boolean,
default: false,
required: false,
},
duplicateLoading: {
type: Boolean,
default: false,
required: false,
},
picked: {
type: Object as PropType<Picked | null>,
default: null,
required: false,
},
collectionMoveLoading: {
type: Array as PropType<string[]>,
default: () => [],
required: false,
},
requestMoveLoading: {
type: Array as PropType<string[]>,
default: () => [],
required: false,
},
})
const emit = defineEmits<{
(
event: "add-request",
payload: {
path: string
folder: TeamCollection
}
): void
(
event: "add-folder",
payload: {
path: string
folder: TeamCollection
}
): void
(
event: "edit-collection",
payload: {
collectionIndex: string
collection: TeamCollection
}
): void
(
event: "edit-folder",
payload: {
folder: TeamCollection
}
): void
(
event: "edit-request",
payload: {
requestIndex: string
request: HoppRESTRequest
}
): void
(
event: "duplicate-request",
payload: {
folderPath: string
request: HoppRESTRequest
}
): void
(event: "export-data", payload: TeamCollection): 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
requestIndex: string
isActive: boolean
folderPath?: string | undefined
}
): 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
destinationCollectionIndex: string
}
): void
(
event: "update-collection-order",
payload: {
dragedCollectionIndex: string
destinationCollectionIndex: string
}
): void
(event: "select", payload: Picked | null): void
(event: "expand-team-collection", payload: string): void
(event: "display-modal-add"): void
(event: "display-modal-import-export"): void
}>()
const teamCollectionsList = toRef(props, "teamCollectionList")
const hasNoTeamAccess = computed(
() =>
props.collectionsType.type === "team-collections" &&
(props.collectionsType.selectedTeam === undefined ||
props.collectionsType.selectedTeam.myRole === "VIEWER")
)
const isSelected = computed(() => {
return ({
collectionID,
folderID,
requestID,
}: {
collectionID?: string | undefined
folderID?: string | undefined
requestID?: string | undefined
}) => {
if (collectionID !== undefined) {
return (
props.picked &&
props.picked.pickedType === "teams-collection" &&
props.picked.collectionID === collectionID
)
} else if (requestID !== undefined) {
return (
props.picked &&
props.picked.pickedType === "teams-request" &&
props.picked.requestID === requestID
)
} else {
return (
props.picked &&
props.picked.pickedType === "teams-folder" &&
props.picked.folderID === folderID
)
}
}
})
const active = computed(() => currentActiveTab.value.document.saveContext)
const isActiveRequest = computed(() => {
return (requestID: string) => {
return pipe(
active.value,
O.fromNullable,
O.filter(
(active) =>
active.originLocation === "team-collection" &&
active.requestID === requestID
),
O.isSome
)
}
})
const selectRequest = (data: {
request: HoppRESTRequest
requestIndex: string
}) => {
const { request, requestIndex } = data
if (props.saveRequest) {
emit("select", {
pickedType: "teams-request",
requestID: requestIndex,
})
} else {
emit("select-request", {
request: request,
requestIndex: requestIndex,
isActive: isActiveRequest.value(requestIndex),
})
}
}
const dragRequest = (
dataTransfer: DataTransfer,
{
folderPath,
requestIndex,
}: { folderPath: string | null; requestIndex: string }
) => {
if (!folderPath) return
dataTransfer.setData("folderPath", folderPath)
dataTransfer.setData("requestIndex", requestIndex)
}
const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
dataTransfer.setData("collectionIndex", collectionIndex)
}
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 }
) => {
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,
destinationCollectionIndex: string
) => {
const dragedCollectionIndex = dataTransfer.getData("collectionIndex")
emit("update-collection-order", {
dragedCollectionIndex,
destinationCollectionIndex,
})
}
type TeamCollections = {
type: "collections"
data: {
parentIndex: null
data: TeamCollection
}
}
type TeamFolder = {
type: "folders"
data: {
parentIndex: string
data: TeamCollection
}
}
type TeamRequests = {
type: "requests"
data: {
parentIndex: string
data: TeamRequest
}
}
type TeamCollectionNode = TeamCollections | TeamFolder | TeamRequests
class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
constructor(public data: Ref<TeamCollection[]>) {}
findCollInTree(
tree: TeamCollection[],
targetID: string
): TeamCollection | null {
for (const coll of tree) {
// If the direct child matched, then return that
if (coll.id === targetID) return coll
// Else run it in the children
if (coll.children) {
const result = this.findCollInTree(coll.children, targetID)
if (result) return result
}
}
// If nothing matched, return null
return null
}
getChildren(id: string | null): Ref<ChildrenResult<TeamCollectionNode>> {
return computed(() => {
if (id === null) {
if (props.teamLoadingCollections.includes("root")) {
return {
status: "loading",
}
} else {
const data = this.data.value.map((item) => ({
id: item.id,
data: {
type: "collections",
data: {
parentIndex: null,
data: item,
},
},
}))
return {
status: "loaded",
data: cloneDeep(data),
} as ChildrenResult<TeamCollections>
}
} else {
const parsedID = id.split("/")[id.split("/").length - 1]
!props.teamLoadingCollections.includes(parsedID) &&
emit("expand-team-collection", parsedID)
if (props.teamLoadingCollections.includes(parsedID)) {
return {
status: "loading",
}
} else {
const items = this.findCollInTree(this.data.value, parsedID)
if (items) {
const data = [
...(items.children
? items.children.map((item) => ({
id: `${id}/${item.id}`,
data: {
type: "folders",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
...(items.requests
? items.requests.map((item) => ({
id: `${id}/${item.id}`,
data: {
type: "requests",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
]
return {
status: "loaded",
data: cloneDeep(data),
} as ChildrenResult<TeamFolder | TeamRequests>
} else {
return {
status: "loaded",
data: [],
}
}
}
}
})
}
}
const teamAdapter: SmartTreeAdapter<TeamCollectionNode> =
new TeamCollectionsAdapter(teamCollectionsList)
</script>