fix: API documentation UI flow improvements (#5618)
This commit is contained in:
parent
03212386fb
commit
a5fb7cb0d2
17 changed files with 88 additions and 44 deletions
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ export const harImporter = (
|
|||
authActive: true,
|
||||
},
|
||||
headers: [],
|
||||
description: null,
|
||||
variables: [],
|
||||
})
|
||||
|
||||
return E.right([collection])
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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) ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue