fix: API documentation UI flow improvements (#5618)

This commit is contained in:
Nivedin 2025-11-25 23:06:02 +05:30 committed by GitHub
parent 03212386fb
commit a5fb7cb0d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 88 additions and 44 deletions

View file

@ -530,7 +530,7 @@
"copy": "Copy response",
"example_copied": "Response example copied to clipboard!",
"example_copy_failed": "Failed to copy response example",
"headers": "Headers",
"headers": "Response Headers",
"no_examples": "No response examples available",
"title": "Response Examples"
},

View file

@ -258,7 +258,7 @@
:shortcut="['I']"
@click="
() => {
emit('open-documentation')
handleDocumentationAction()
hide()
}
"
@ -683,6 +683,19 @@ const handleMockServerAction = () => {
emit("create-mock-server")
}
const handleDocumentationAction = () => {
const currentUser = platform.auth.getCurrentUser()
if (!currentUser) {
// Show login modal if user is not authenticated
invokeAction("modals.login.toggle")
return
}
// User is authenticated, proceed with opening documentation
emit("open-documentation")
}
const resetDragState = () => {
dragging.value = false
ordering.value = false

View file

@ -139,7 +139,7 @@
:shortcut="['I']"
@click="
() => {
emit('open-request-documentation')
handleDocumentationAction()
hide()
}
"
@ -241,6 +241,8 @@ import {
} from "~/newstore/reordering"
import { useReadonlyStream } from "~/composables/stream"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import { platform } from "~/platform"
import { invokeAction } from "~/helpers/actions"
type CollectionType = "my-collections" | "team-collections"
@ -455,6 +457,19 @@ const isRequestLoading = computed(() => {
return false
})
const handleDocumentationAction = () => {
const currentUser = platform.auth.getCurrentUser()
if (!currentUser) {
// Show login modal if user is not authenticated
invokeAction("modals.login.toggle")
return
}
// User is authenticated, proceed with opening documentation
emit("open-request-documentation")
}
const resetDragState = () => {
dragging.value = false
ordering.value = false

View file

@ -356,7 +356,7 @@ const scrollToItem = (id: string): void => {
block: "start",
})
} else {
console.log("Item not found:", id)
console.error("Item not found:", id)
}
}, 50)
})
@ -375,10 +375,9 @@ const scrollToItemByNameAndType = (
if (itemIndex !== -1) {
const targetItem = props.allItems[itemIndex]
console.log(`Found ${type} at index: ${itemIndex}`)
scrollToItem(targetItem.id)
} else {
console.log(`${type} with name "${name}" not found in allItems`)
console.error(`${type} with name "${name}" not found in allItems`)
}
}

View file

@ -76,6 +76,7 @@
<div class="flex space-x-2">
<HoppButtonPrimary
v-if="mode === 'view' || mode === 'update'"
:label="t('documentation.publish.view_published')"
:icon="IconExternalLink"
@click="viewPublished"

View file

@ -148,6 +148,7 @@
</template>
</tippy>
<HoppButtonSecondary
v-if="currentCollection"
:icon="isDocumentationProcessing ? IconLoader2 : IconFileText"
:label="
isDocumentationProcessing
@ -427,7 +428,7 @@ const checkPublishedDocsStatus = async () => {
getTeamPublishedDocs(props.teamID, props.collectionID),
TE.match(
(error) => {
console.log("No published docs found or error:", error)
console.error("No published docs found or error:", error)
isCheckingPublishedStatus.value = false
},
(docs) => {
@ -454,11 +455,10 @@ const checkPublishedDocsStatus = async () => {
getUserPublishedDocs(),
TE.match(
(error) => {
console.log("No published docs found or error:", error)
console.error("No published docs found or error:", error)
isCheckingPublishedStatus.value = false
},
(docs) => {
console.log("//published-docs///", docs)
// Find published doc for this collection
const publishedDoc = docs.find(
(doc) => doc.collection.id === props.collectionID
@ -484,9 +484,7 @@ const checkPublishedDocsStatus = async () => {
watch(
() => props.show,
async (newVal) => {
console.log("///show///", newVal)
if (newVal) {
console.log("//check-published-docs-status///")
// Check for existing published docs when modal opens
await checkPublishedDocsStatus()
} else {

View file

@ -14,7 +14,7 @@
<span>{{ t("documentation.auth.title") }}</span>
<span
v-if="auth?.authType === 'inherit'"
class="ml-2 font-semibold capitalize px-2 py-1 text-tiny rounded bg-divider text-secondaryDark"
class="ml-2 font-semibold capitalize px-2 py-1 text-tiny rounded bg-divider text-secondaryDark truncate"
>
({{
t("documentation.inherited_with_type", {

View file

@ -18,7 +18,7 @@
<!-- Display body content based on type -->
<div v-if="body.contentType === 'application/json'">
<pre
class="bg-primaryLight p-3 rounded my-2 overflow-auto max-h-64 text-sm font-mono text-secondaryLight"
class="bg-primaryLight p-3 rounded my-2 overflow-auto max-h-64 text-xs font-mono text-secondaryLight"
>{{ formatJSON(body.body) }}</pre
>
</div>
@ -54,7 +54,7 @@
<div v-else>
<pre
class="bg-primaryLight p-3 rounded my-2 overflow-auto max-h-64 text-sm font-mono text-secondaryLight"
class="bg-primaryLight p-3 rounded my-2 overflow-auto max-h-64 font-mono text-secondaryLight"
>{{ body.body }}</pre
>
</div>

View file

@ -31,7 +31,7 @@
</div>
<HoppSmartItem
:icon="IconCopy"
:title="t('documentation.copy_response')"
:title="t('documentation.response.copy')"
@click="copyResponseExample(example)"
/>
</div>
@ -44,7 +44,7 @@
<HoppSmartTab
v-if="example.body"
id="body"
:label="t('documentation.response_body')"
:label="t('documentation.response.body')"
class="flex h-full w-full flex-1 flex-col"
>
<div class="p-4">
@ -66,7 +66,8 @@
<HoppSmartTab
v-if="example.headers && example.headers.length > 0"
id="headers"
:label="`${t('documentation.response_headers')} (${example.headers.length})`"
:label="t('documentation.response.headers')"
:info="`${example.headers.length}`"
class="flex h-full w-full flex-1 flex-col"
>
<div class="p-4">
@ -91,7 +92,7 @@
:key="headerIndex"
class="border-t border-divider"
>
<td class="py-2 px-3 text-accent text-xs">
<td class="py-2 px-3 text-xs">
{{ header.key }}
</td>
<td class="py-2 px-3 text-secondaryLight text-xs">

View file

@ -2956,7 +2956,6 @@ const editProperties = async (payload: {
collection: HoppCollection | TeamCollection
}) => {
const { collection, collectionIndex } = payload
console.log("collection", collection)
const collectionId = collection.id ?? collectionIndex.split("/").pop()
@ -3270,14 +3269,11 @@ const sortCollections = (payload: {
const openDocumentation = ({
pathOrID,
collectionRefID,
collection,
}: {
pathOrID: string
collectionRefID: string
collection: HoppCollection | TeamCollection
}) => {
console.log("Open documentation for", pathOrID, collectionRefID, collection)
editingCollectionPath.value = pathOrID
editingCollection.value = collection
editingCollectionIsTeam.value =
@ -3295,24 +3291,12 @@ const openDocumentation = ({
const openRequestDocumentation = ({
folderPath,
requestIndex,
requestRefID,
request,
}: {
folderPath: string
requestIndex: string
requestRefID?: string
request: HoppRESTRequest
}) => {
console.log(
"Open documentation for request",
folderPath,
requestIndex,
requestRefID,
request
)
// editingCollectionPath.value = pathOrID
// editingCollection.value = collection
editingRequest.value = request
editingFolderPath.value = folderPath
editingRequestIndex.value = parseInt(requestIndex)

View file

@ -133,7 +133,7 @@ const scrollToItem = (id: string): void => {
block: "start",
})
} else {
console.log("Item not found:", id)
console.error("Item not found:", id)
}
}, 100)
}
@ -149,7 +149,7 @@ const scrollToItemByName = (name: string, type: "request" | "folder") => {
if (item) {
scrollToItem(item.id)
} else {
console.log(`${type} with name "${name}" not found in allItems`)
console.error(`${type} with name "${name}" not found in allItems`)
}
}

View file

@ -37,6 +37,8 @@ export const harImporter = (
authActive: true,
},
headers: [],
description: null,
variables: [],
})
return E.right([collection])

View file

@ -228,6 +228,7 @@ const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
//insomnia doesn't have saved response
responses: {},
description: req.meta?.description ?? null,
})
const getHoppFolder = (
@ -243,6 +244,7 @@ const getHoppFolder = (
auth: { authType: "inherit", authActive: true },
headers: [],
variables: getCollectionVariables(undefined, folderRes), // undefined is used to indicate no environment variables for v4 and below
description: folderRes.meta?.description ?? null,
})
const getHoppCollections = (docs: InsomniaDoc[]) => {
@ -280,6 +282,7 @@ const getParsedHoppFolder = (
auth: { authType: "inherit", authActive: true },
headers: [],
variables: getCollectionVariables(collection.environment),
description: collection.meta.description ?? null,
})
}
@ -300,6 +303,8 @@ const getParsedHoppRequest = (req: InsomniaRequestResource) => {
//insomnia doesn't have saved response
responses: {},
description: req.meta?.description ?? null,
})
}
@ -317,6 +322,7 @@ const getParsedHoppCollections = (docs: InsomniaDocV5[]): HoppCollection[] =>
auth: { authType: "inherit", authActive: true },
headers: [],
variables: getCollectionVariables(doc.environments?.data),
description: doc.meta.description ?? null,
})
}

View file

@ -13,7 +13,11 @@ export type InsomniaPathParameter = {
value: string
}
export type InsomniaFolderResource = ImportRequest & { _type: "request_group" }
export type InsomniaFolderResource = ImportRequest & {
_type: "request_group"
description?: string
meta?: InsomniaMetaV5
}
export type InsomniaRequestResource = Omit<
ImportRequest,
"headers" | "parameters"
@ -24,6 +28,7 @@ export type InsomniaRequestResource = Omit<
} & {
headers: (Header & { description: string })[]
parameters: (Parameter & { description: string })[]
meta?: InsomniaMetaV5
}
/**

View file

@ -929,6 +929,7 @@ const convertPathToHoppReqs = (
} = {
request: makeRESTRequest({
name: info.operationId ?? info.summary ?? "Untitled Request",
description: info.description ?? null,
method: method.toUpperCase(),
endpoint,
@ -1000,6 +1001,17 @@ const convertOpenApiDocsToHopp = (
const collections = docs.map((doc) => {
const name = doc.info.title
const description = doc.info.description ?? null
// Extract tag descriptions from OpenAPI spec
const tagDescriptions: Record<string, string> = {}
if ("tags" in doc && Array.isArray(doc.tags)) {
doc.tags.forEach((tag: any) => {
if (tag.name && tag.description) {
tagDescriptions[tag.name] = tag.description
}
})
}
const paths = Object.entries(doc.paths ?? {})
.map(([pathName, pathObj]) =>
@ -1032,15 +1044,19 @@ const convertOpenApiDocsToHopp = (
folders: Object.entries(requestsByTags).map(([name, paths]) =>
makeCollection({
name,
description: tagDescriptions[name] ?? null,
requests: paths,
folders: [],
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
})
),
requests: requestsWithoutTags,
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
description,
})
})

View file

@ -67,12 +67,17 @@ const buildHarPostParams = (
return req.body.body.flatMap((entry) => {
if (entry.isFile) {
// We support multiple files
return entry.value.map(
const values = Array.isArray(entry.value) ? entry.value : [entry.value]
return values.map(
(file) =>
<Har.Param>{
name: entry.key,
fileName: entry.key, // TODO: Blob doesn't contain file info, anyway to bring file name here ?
contentType: entry.contentType ? entry.contentType : file?.type,
contentType: entry.contentType
? entry.contentType
: typeof file === "object" && file && "type" in file
? file.type
: undefined,
}
)
}
@ -80,7 +85,7 @@ const buildHarPostParams = (
if (entry.contentType) {
return {
name: entry.key,
value: entry.value,
value: entry.value as string,
fileName: entry.key,
contentType: entry.contentType,
}
@ -88,7 +93,7 @@ const buildHarPostParams = (
return {
name: entry.key,
value: entry.value,
value: entry.value as string,
contentType: entry.contentType,
}
})
@ -111,7 +116,7 @@ const buildHarPostData = (req: HoppRESTRequest): Har.PostData | undefined => {
return {
mimeType: req.body.contentType, // Let's assume by default content type is JSON
text: req.body.body,
text: (req.body.body as string) ?? "",
}
}

View file

@ -314,8 +314,7 @@ export function getFinalBodyFromRequest(
// we split array blobs into separate entries (FormData will then join them together during exec)
arrayFlatMap((x) =>
x.isFile
? // @ts-expect-error TODO: Fix this type error
x.value.map((v) => ({
? (Array.isArray(x.value) ? x.value : [x.value]).map((v) => ({
key: parseTemplateString(x.key, envVariables),
value: v as string | Blob,
contentType: x.contentType,