api-client/packages/hoppscotch-common/src/components/collections/documentation/CollectionStructure.vue
Mir Arif Hasan 803e4633a2
feat: api documentation versioning (#5676)
Co-authored-by: nivedin <nivedinp@gmail.com>
Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
2026-02-23 20:11:55 +05:30

266 lines
6.9 KiB
Vue

<template>
<div
class="rounded-md border border-divider"
:class="{
'w-64': isDocModal,
'w-full': compact,
}"
>
<div
class="sticky top-0 z-[99] py-2 border-b border-divider bg-primaryLight flex items-center justify-between space-x-3"
>
<div
class="font-medium text-secondaryDark flex flex-1 items-center text-xs px-4 truncate cursor-pointer transition-colors"
@click="scrollToTop"
>
<span class="truncate">
{{ collectionName }}
</span>
</div>
<HoppSmartItem
v-if="hasItems(collectionFolders) || hasItems(collectionRequests)"
:icon="allExpanded ? IconCheveronsUp : IconCheveronsDown"
class="focus-visible:bg-transparent hover:bg-transparent"
@click="toggleAllFolders"
/>
</div>
<!-- Tree structure -->
<div
class="overflow-y-auto"
:class="{
'max-h-[400px]': isDocModal,
}"
>
<div v-if="hasItems(collectionFolders)">
<CollectionsDocumentationFolderItem
v-for="(rootFolder, rootFolderIndex) in collectionFolders"
:key="getFolderId(rootFolder, rootFolderIndex)"
:folder="rootFolder"
:folder-index="rootFolderIndex"
:depth="0"
:expanded-folders="expandedFolders"
@toggle-folder="toggleFolder"
@request-select="onRequestSelect"
@folder-select="onFolderSelect"
/>
</div>
<!-- Root Requests -->
<div v-if="hasItems(collectionRequests)" class="ml-4">
<CollectionsDocumentationRequestItem
v-for="(request, requestIndex) in collectionRequests"
:key="getRequestId(request, requestIndex)"
:request="request as HoppRESTRequest"
:depth="0"
@request-select="onRequestSelect"
/>
</div>
<div
v-if="!hasItems(collectionFolders) && !hasItems(collectionRequests)"
class="p-3 text-center text-secondaryLight text-xs italic"
>
{{ t("documentation.no_requests_or_folders") }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {
HoppCollection,
HoppRESTRequest,
HoppGQLRequest,
} from "@hoppscotch/data"
import { ref, reactive, watch, computed } from "vue"
import IconCheveronsDown from "~icons/lucide/chevrons-down"
import IconCheveronsUp from "~icons/lucide/chevrons-up"
import { useService } from "dioc/vue"
import { TeamCollectionsService } from "~/services/team-collection.service"
import { useI18n } from "~/composables/i18n"
const t = useI18n()
type ExpandedFoldersType = { [key: string]: boolean }
type HoppRequest = HoppRESTRequest | HoppGQLRequest
type ItemWithPossibleId = {
id?: string
_ref_id?: string
name?: string
[key: string]: unknown
}
const props = withDefaults(
defineProps<{
collection: HoppCollection
initiallyExpanded?: boolean
isDocModal?: boolean
compact?: boolean
}>(),
{
initiallyExpanded: false,
isDocModal: true,
compact: false,
}
)
const emit = defineEmits<{
(e: "request-select", request: HoppRESTRequest): void
(e: "folder-select", folder: HoppCollection): void
(e: "scroll-to-top"): void
}>()
const expandedFolders = reactive<ExpandedFoldersType>({})
const allExpanded = ref<boolean>(props.initiallyExpanded || false)
const teamCollectionService = useService(TeamCollectionsService)
const collectionFolders = computed<HoppCollection[]>(() => {
return props.collection.folders || []
})
const collectionRequests = computed<HoppRequest[]>(() => {
return props.collection.requests
})
const collectionName = computed<string>(() => {
return props.collection.name || t("documentation.untitled_collection")
})
/**
* Generate a fallback ID for items that don't have one
* @param item The folder or request item
* @param index The index of the item in its parent array
* @param prefix The prefix to use for the generated ID
* @returns A generated ID string
*/
const generateFallbackId = (
item: ItemWithPossibleId,
index: number,
prefix: string
): string => {
return (
item.id ||
item._ref_id ||
`${prefix}-${item.name?.replace(/\s+/g, "-").toLowerCase()}-${index}`
)
}
/**
* Get a reliable ID for a folder, with fallback generation
* @param folder The folder object
* @param index The folder's index in its parent array
* @returns A reliable ID string
*/
const getFolderId = (folder: HoppCollection, index: number): string => {
return generateFallbackId(folder, index, "folder")
}
// Initialize folder structure with first level expanded
watch(
() => props.collection,
() => {
const folders = collectionFolders.value
if (folders?.length) {
folders.forEach((folder: HoppCollection, index: number) => {
const folderId = getFolderId(folder, index)
expandedFolders[folderId] = true
})
}
},
{ immediate: true }
)
const toggleFolder = async (folderId: string) => {
expandedFolders[folderId] = !expandedFolders[folderId]
await teamCollectionService.expandCollection(folderId)
}
const toggleAllFolders = () => {
allExpanded.value = !allExpanded.value
const processAllFolders = (folders: HoppCollection[]) => {
folders.forEach((folder, index) => {
const folderId = getFolderId(folder, index)
expandedFolders[folderId] = allExpanded.value
if (folder.folders && folder.folders.length > 0) {
processAllFolders(folder.folders)
}
})
}
const folders = collectionFolders.value
if (folders?.length) {
processAllFolders(folders)
}
}
const onRequestSelect = (request: HoppRESTRequest) => {
emit("request-select", request)
}
const onFolderSelect = (folder: HoppCollection) => {
emit("folder-select", folder)
}
/**
* Emits event to scroll to the top of the documentation
*/
const scrollToTop = () => {
emit("scroll-to-top")
}
/**
* Type guard to check if a request is a REST request
* @returns True if the request is a REST request
*/
/**
* Check if a value exists and has length > 0
* @param value Array to check
* @returns Boolean indicating if array has items
*/
const hasItems = <T,>(value: T[] | undefined): boolean => {
return !!value && value.length > 0
}
/**
* Get a reliable ID for a request, with fallback generation
* @param request The request object
* @param index The request's index in its parent array
* @returns A reliable ID string
*/
const getRequestId = (request: HoppRequest, index: number): string => {
return generateFallbackId(request, index, "request")
}
</script>
<style scoped>
.scrollable-structure {
scrollbar-width: thin;
}
/* Custom scrollbar styling */
.scrollable-structure::-webkit-scrollbar {
width: 6px;
}
.scrollable-structure::-webkit-scrollbar-track {
@apply bg-transparent;
}
.scrollable-structure::-webkit-scrollbar-thumb {
@apply bg-divider rounded-full;
}
.scrollable-structure::-webkit-scrollbar-thumb:hover {
@apply bg-dividerLight;
}
/* Animation for folder expansion */
.transition-transform-2 {
@apply transition-transform duration-200 ease-in-out;
}
</style>