feat(common): improve API documentation publishing UX (#6116)
Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
bc3dbdea42
commit
30df20ea7a
8 changed files with 307 additions and 220 deletions
|
|
@ -538,7 +538,6 @@
|
||||||
"versions": "Versions",
|
"versions": "Versions",
|
||||||
"create_new_version": "Create New Version",
|
"create_new_version": "Create New Version",
|
||||||
"invalid_version": "Version must only contain alphanumeric characters, dots, and hyphens",
|
"invalid_version": "Version must only contain alphanumeric characters, dots, and hyphens",
|
||||||
"snapshot_description": "This will snapshot the current documentation as this version",
|
|
||||||
"live": "Live",
|
"live": "Live",
|
||||||
"snapshot": "Snapshot",
|
"snapshot": "Snapshot",
|
||||||
"version_immutable": "Published versions are read-only snapshots",
|
"version_immutable": "Published versions are read-only snapshots",
|
||||||
|
|
@ -556,8 +555,9 @@
|
||||||
"snapshot_empty": "No requests or folders in this snapshot",
|
"snapshot_empty": "No requests or folders in this snapshot",
|
||||||
"snapshot_item_count": "{count} items",
|
"snapshot_item_count": "{count} items",
|
||||||
"auto_sync_live_notice": "This version auto-syncs with the live collection",
|
"auto_sync_live_notice": "This version auto-syncs with the live collection",
|
||||||
|
"snapshot_promote_warning": "Enabling auto-sync will replace this frozen snapshot with the live collection tree. This cannot be undone.",
|
||||||
|
"live_freeze_notice": "Auto-sync will be turned off and this version will be frozen at the current collection state.",
|
||||||
"untitled_project": "Untitled Project",
|
"untitled_project": "Untitled Project",
|
||||||
"first_publish_hint": "Your documentation will be published as a live version that automatically stays in sync with your collection",
|
|
||||||
"environment": "Environment",
|
"environment": "Environment",
|
||||||
"no_environment": "No environment",
|
"no_environment": "No environment",
|
||||||
"environment_description": "Attach an environment to resolve variables in the published documentation"
|
"environment_description": "Attach an environment to resolve variables in the published documentation"
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col space-y-6">
|
<div class="flex flex-col space-y-6">
|
||||||
<div>
|
<HoppSmartInput
|
||||||
<HoppSmartInput
|
v-model="titleModel"
|
||||||
v-model="titleModel"
|
:label="t('documentation.publish.doc_title')"
|
||||||
:label="t('documentation.publish.doc_title')"
|
type="text"
|
||||||
type="text"
|
input-styles="floating-input"
|
||||||
input-styles="floating-input"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<!-- Version Input -->
|
||||||
v-if="isFirstPublish"
|
<HoppSmartInput
|
||||||
class="flex items-start space-x-2 px-3 py-2.5 rounded-md bg-green-500/5 border border-green-500/15"
|
v-model="versionModel"
|
||||||
|
:label="t('documentation.publish.doc_version')"
|
||||||
|
:input-styles="[
|
||||||
|
'floating-input',
|
||||||
|
!isValidVersion && versionModel.length > 0
|
||||||
|
? '!border-red-500 !focus:border-red-500'
|
||||||
|
: '',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="!isValidVersion && versionModel.length > 0"
|
||||||
|
class="text-xs text-red-500 mt-1 block"
|
||||||
>
|
>
|
||||||
<icon-lucide-info
|
{{ t("documentation.publish.invalid_version") }}
|
||||||
class="w-3.5 h-3.5 text-green-600 flex-shrink-0 mt-0.5"
|
</span>
|
||||||
/>
|
|
||||||
<span class="text-xs text-green-600 leading-relaxed">
|
|
||||||
{{ t("documentation.publish.first_publish_hint") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Version Input (hidden for first publish) -->
|
<!-- Auto-sync Toggle -->
|
||||||
<div v-if="!isFirstPublish">
|
<div class="flex items-start">
|
||||||
<HoppSmartInput
|
|
||||||
v-model="versionModel"
|
|
||||||
:label="t('documentation.publish.doc_version')"
|
|
||||||
:disabled="mode === 'update'"
|
|
||||||
:input-styles="[
|
|
||||||
'floating-input',
|
|
||||||
!isValidVersion && versionModel.length > 0
|
|
||||||
? '!border-red-500 !focus:border-red-500'
|
|
||||||
: '',
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="!isValidVersion && versionModel.length > 0"
|
|
||||||
class="text-xs text-red-500 mt-1 block"
|
|
||||||
>
|
|
||||||
{{ t("documentation.publish.invalid_version") }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="mode === 'create' && isValidVersion"
|
|
||||||
class="text-xs text-secondaryLight mt-1 block"
|
|
||||||
>
|
|
||||||
{{ t("documentation.publish.snapshot_description") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Auto-sync Toggle (hidden for first publish and for live versions) -->
|
|
||||||
<div v-if="!isFirstPublish && !isAutoSyncLocked" class="flex items-start">
|
|
||||||
<HoppSmartCheckbox
|
<HoppSmartCheckbox
|
||||||
:on="autoSyncModel"
|
:on="autoSyncModel"
|
||||||
@change="autoSyncModel = !autoSyncModel"
|
@change="autoSyncModel = !autoSyncModel"
|
||||||
|
|
@ -65,6 +42,19 @@
|
||||||
</HoppSmartCheckbox>
|
</HoppSmartCheckbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Info notice: turning off auto-sync on a live version will freeze it -->
|
||||||
|
<div
|
||||||
|
v-if="mode === 'update' && !autoSyncModel"
|
||||||
|
class="flex items-start space-x-2 px-3 py-2.5 rounded-md bg-blue-500/5 border border-blue-500/20"
|
||||||
|
>
|
||||||
|
<icon-lucide-info
|
||||||
|
class="w-3.5 h-3.5 text-blue-600 flex-shrink-0 mt-0.5"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-blue-600 leading-relaxed">
|
||||||
|
{{ t("documentation.publish.live_freeze_notice") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Environment Selector -->
|
<!-- Environment Selector -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<span class="block text-sm font-medium text-secondaryDark">
|
<span class="block text-sm font-medium text-secondaryDark">
|
||||||
|
|
@ -130,8 +120,6 @@ const props = defineProps<{
|
||||||
autoSync: boolean
|
autoSync: boolean
|
||||||
selectedEnvironmentID: string | null
|
selectedEnvironmentID: string | null
|
||||||
publishedUrl: string | null
|
publishedUrl: string | null
|
||||||
isFirstPublish: boolean
|
|
||||||
isAutoSyncLocked: boolean
|
|
||||||
isValidVersion: boolean
|
isValidVersion: boolean
|
||||||
workspaceType: WorkspaceType
|
workspaceType: WorkspaceType
|
||||||
workspaceID: string
|
workspaceID: string
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,26 @@
|
||||||
v-if="show"
|
v-if="show"
|
||||||
dialog
|
dialog
|
||||||
:title="modalTitle"
|
:title="modalTitle"
|
||||||
:styles="mode === 'view' ? 'sm:max-w-6xl' : 'sm:max-w-2xl'"
|
:styles="
|
||||||
|
mode === 'view'
|
||||||
|
? 'sm:max-w-6xl xl:max-w-7xl 2xl:max-w-[80vw]'
|
||||||
|
: 'sm:max-w-2xl'
|
||||||
|
"
|
||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<CollectionsDocumentationPublishDocSnapshotPreview
|
<CollectionsDocumentationPublishDocSnapshotPreview
|
||||||
v-if="mode === 'view'"
|
v-if="mode === 'view'"
|
||||||
|
v-model:publish-title="publishTitle"
|
||||||
|
v-model:publish-version="publishVersion"
|
||||||
|
v-model:auto-sync="autoSync"
|
||||||
|
v-model:selected-environment-i-d="selectedEnvironmentID"
|
||||||
:existing-data="existingData"
|
:existing-data="existingData"
|
||||||
:published-url="publishedUrl"
|
:published-url="publishedUrl"
|
||||||
:show="show && mode === 'view'"
|
:show="show && mode === 'view'"
|
||||||
|
:is-valid-version="isValidVersion"
|
||||||
|
:workspace-type="workspaceType"
|
||||||
|
:workspace-i-d="workspaceID"
|
||||||
@copy-url="copyUrl"
|
@copy-url="copyUrl"
|
||||||
@view-published="viewPublished"
|
@view-published="viewPublished"
|
||||||
/>
|
/>
|
||||||
|
|
@ -24,8 +35,6 @@
|
||||||
v-model:auto-sync="autoSync"
|
v-model:auto-sync="autoSync"
|
||||||
v-model:selected-environment-i-d="selectedEnvironmentID"
|
v-model:selected-environment-i-d="selectedEnvironmentID"
|
||||||
:published-url="publishedUrl"
|
:published-url="publishedUrl"
|
||||||
:is-first-publish="isFirstPublish ?? false"
|
|
||||||
:is-auto-sync-locked="isAutoSyncLocked ?? false"
|
|
||||||
:is-valid-version="isValidVersion"
|
:is-valid-version="isValidVersion"
|
||||||
:workspace-type="workspaceType"
|
:workspace-type="workspaceType"
|
||||||
:workspace-i-d="workspaceID"
|
:workspace-i-d="workspaceID"
|
||||||
|
|
@ -46,7 +55,7 @@
|
||||||
@click="handlePublish"
|
@click="handlePublish"
|
||||||
/>
|
/>
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
v-else-if="mode === 'update'"
|
v-else-if="mode === 'update' || mode === 'view'"
|
||||||
:label="t('documentation.publish.update_button')"
|
:label="t('documentation.publish.update_button')"
|
||||||
:disabled="!canPublish || loading || !hasChanges"
|
:disabled="!canPublish || loading || !hasChanges"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
|
@ -112,9 +121,9 @@ const props = defineProps<{
|
||||||
workspaceID: string
|
workspaceID: string
|
||||||
mode?: "create" | "update" | "view"
|
mode?: "create" | "update" | "view"
|
||||||
isFirstPublish?: boolean
|
isFirstPublish?: boolean
|
||||||
isAutoSyncLocked?: boolean
|
|
||||||
publishedDocId?: string
|
publishedDocId?: string
|
||||||
existingData?: {
|
existingData?: {
|
||||||
|
id: string
|
||||||
title: string
|
title: string
|
||||||
version: string
|
version: string
|
||||||
autoSync: boolean
|
autoSync: boolean
|
||||||
|
|
@ -168,17 +177,30 @@ const initializeFormData = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for modal open/close
|
|
||||||
watch(
|
watch(
|
||||||
[() => props.existingData, () => props.show],
|
() => props.show,
|
||||||
([, isOpen]) => {
|
(isOpen) => {
|
||||||
if (isOpen) {
|
if (isOpen) initializeFormData()
|
||||||
initializeFormData()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Reinitialize only on doc switches (snapshot ↔ live) — same-doc refreshes must not clobber in-flight edits.
|
||||||
|
watch(
|
||||||
|
() => props.existingData?.id,
|
||||||
|
(newId, oldId) => {
|
||||||
|
if (props.show && newId && newId !== oldId) initializeFormData()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Same-doc URL can change when the backend rebuilds it (version rename). Sync display only.
|
||||||
|
watch(
|
||||||
|
() => props.existingData?.url,
|
||||||
|
(newUrl) => {
|
||||||
|
if (props.show && newUrl) publishedUrl.value = newUrl
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const modalTitle = computed(() => {
|
const modalTitle = computed(() => {
|
||||||
if (props.mode === "update") return t("documentation.publish.update_title")
|
if (props.mode === "update") return t("documentation.publish.update_title")
|
||||||
if (props.mode === "view") return t("documentation.publish.view_title")
|
if (props.mode === "view") return t("documentation.publish.view_title")
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,67 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col lg:flex-row gap-4 flex-1">
|
<div class="flex flex-col lg:flex-row gap-4 flex-1">
|
||||||
<!-- Left Metadata Panel -->
|
<!-- Left Metadata Panel -->
|
||||||
<div class="lg:w-72 flex-shrink-0 flex flex-col space-y-3">
|
<div
|
||||||
<!-- Title & version header -->
|
class="lg:w-96 flex-shrink-0 flex flex-col divide-y divide-divider space-y-4"
|
||||||
<div class="space-y-2">
|
>
|
||||||
<h3 class="text-sm font-semibold text-secondaryDark truncate">
|
<div class="space-y-4">
|
||||||
{{ existingData?.title }}
|
<HoppSmartInput
|
||||||
</h3>
|
v-model="titleModel"
|
||||||
<div class="flex items-center space-x-2">
|
:label="t('documentation.publish.doc_title')"
|
||||||
|
type="text"
|
||||||
|
input-styles="floating-input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="versionModel"
|
||||||
|
:label="t('documentation.publish.doc_version')"
|
||||||
|
:input-styles="[
|
||||||
|
'floating-input',
|
||||||
|
!isValidVersion && versionModel.length > 0
|
||||||
|
? '!border-red-500 !focus:border-red-500'
|
||||||
|
: '',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
class="text-xs text-secondaryLight rounded border border-dividerDark px-2 py-0.5"
|
v-if="!isValidVersion && versionModel.length > 0"
|
||||||
|
class="text-xs text-red-500 mt-1 block"
|
||||||
>
|
>
|
||||||
{{ existingData?.version }}
|
{{ t("documentation.publish.invalid_version") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Environment badge -->
|
|
||||||
<div
|
<div class="flex items-start">
|
||||||
v-if="existingData?.environmentName"
|
<HoppSmartCheckbox
|
||||||
class="flex items-center space-x-1.5"
|
:on="autoSyncModel"
|
||||||
>
|
@change="autoSyncModel = !autoSyncModel"
|
||||||
<icon-lucide-layers
|
>
|
||||||
class="w-3 h-3 text-secondaryLight flex-shrink-0"
|
<div>
|
||||||
/>
|
<span class="text-sm text-secondaryDark">
|
||||||
<span class="text-xs text-secondaryLight">
|
{{ t("documentation.publish.auto_sync") }}
|
||||||
{{ existingData.environmentName }}
|
</span>
|
||||||
|
</div>
|
||||||
|
</HoppSmartCheckbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Environment Selector -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<span class="block text-sm font-medium text-secondaryDark">
|
||||||
|
{{ t("documentation.publish.environment") }}
|
||||||
</span>
|
</span>
|
||||||
|
<p class="text-xs text-secondaryLight">
|
||||||
|
{{ t("documentation.publish.environment_description") }}
|
||||||
|
</p>
|
||||||
|
<CollectionsDocumentationEnvironmentPicker
|
||||||
|
v-model="environmentModel"
|
||||||
|
:workspace-type="workspaceType"
|
||||||
|
:workspace-i-d="workspaceID"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-divider" />
|
|
||||||
|
|
||||||
<!-- Published URL -->
|
<!-- Published URL -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3 py-4">
|
||||||
<div v-if="publishedUrl" class="space-y-1">
|
<div v-if="publishedUrl" class="space-y-1">
|
||||||
<label
|
<label
|
||||||
class="text-[10px] font-semibold uppercase tracking-wider text-secondaryLight"
|
class="text-[10px] font-semibold uppercase tracking-wider text-secondaryLight"
|
||||||
|
|
@ -66,20 +96,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status notice -->
|
<!-- Status notice: version is already live -->
|
||||||
<div
|
<div
|
||||||
v-if="existingData && !isLive"
|
v-if="existingData?.autoSync && autoSyncModel"
|
||||||
class="flex items-start space-x-2 px-3 py-2.5 rounded-md bg-primaryLight border border-divider"
|
|
||||||
>
|
|
||||||
<icon-lucide-lock
|
|
||||||
class="w-3.5 h-3.5 text-secondaryLight flex-shrink-0 mt-0.5"
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-secondaryLight leading-relaxed">
|
|
||||||
{{ t("documentation.publish.version_immutable") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="existingData && isLive"
|
|
||||||
class="flex items-start space-x-2 px-3 py-2.5 rounded-md bg-green-500/5 border border-green-500/15"
|
class="flex items-start space-x-2 px-3 py-2.5 rounded-md bg-green-500/5 border border-green-500/15"
|
||||||
>
|
>
|
||||||
<icon-lucide-refresh-cw
|
<icon-lucide-refresh-cw
|
||||||
|
|
@ -89,6 +108,32 @@
|
||||||
{{ t("documentation.publish.auto_sync_live_notice") }}
|
{{ t("documentation.publish.auto_sync_live_notice") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Info notice: turning off auto-sync on a live version will freeze it -->
|
||||||
|
<div
|
||||||
|
v-else-if="existingData?.autoSync && !autoSyncModel"
|
||||||
|
class="flex items-start space-x-2 px-3 py-2.5 rounded-md bg-blue-500/5 border border-blue-500/20"
|
||||||
|
>
|
||||||
|
<icon-lucide-info
|
||||||
|
class="w-3.5 h-3.5 text-blue-600 flex-shrink-0 mt-0.5"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-blue-600 leading-relaxed">
|
||||||
|
{{ t("documentation.publish.live_freeze_notice") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Destructive warning: promoting a snapshot to live will overwrite the frozen tree -->
|
||||||
|
<div
|
||||||
|
v-else-if="existingData?.autoSync === false && autoSyncModel"
|
||||||
|
class="flex items-start space-x-2 px-3 py-2.5 rounded-md bg-yellow-500/5 border border-yellow-500/20"
|
||||||
|
>
|
||||||
|
<icon-lucide-alert-triangle
|
||||||
|
class="w-3.5 h-3.5 text-yellow-600 flex-shrink-0 mt-0.5"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-yellow-600 leading-relaxed">
|
||||||
|
{{ t("documentation.publish.snapshot_promote_warning") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Snapshot Preview -->
|
<!-- Right: Snapshot Preview -->
|
||||||
|
|
@ -141,7 +186,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Snapshot content -->
|
<!-- Snapshot content -->
|
||||||
<div v-else-if="snapshotCollectionData" class="h-[60vh] flex flex-col">
|
<div
|
||||||
|
v-else-if="snapshotCollectionData"
|
||||||
|
class="flex-1 flex flex-col overflow-hidden max-h-[55vh]"
|
||||||
|
>
|
||||||
<DocumentationContent
|
<DocumentationContent
|
||||||
:collection-data="snapshotCollectionData"
|
:collection-data="snapshotCollectionData"
|
||||||
:all-items="snapshotItems"
|
:all-items="snapshotItems"
|
||||||
|
|
@ -176,7 +224,7 @@ import IconCopy from "~icons/lucide/copy"
|
||||||
import IconCheck from "~icons/lucide/check"
|
import IconCheck from "~icons/lucide/check"
|
||||||
import IconExternalLink from "~icons/lucide/external-link"
|
import IconExternalLink from "~icons/lucide/external-link"
|
||||||
import IconRefreshCw from "~icons/lucide/refresh-cw"
|
import IconRefreshCw from "~icons/lucide/refresh-cw"
|
||||||
import { isLiveVersion } from "~/services/documentation.service"
|
import { WorkspaceType } from "~/helpers/backend/graphql"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
|
|
@ -193,13 +241,44 @@ const props = defineProps<{
|
||||||
existingData?: ExistingData
|
existingData?: ExistingData
|
||||||
publishedUrl: string | null
|
publishedUrl: string | null
|
||||||
show: boolean
|
show: boolean
|
||||||
|
publishTitle: string
|
||||||
|
publishVersion: string
|
||||||
|
autoSync: boolean
|
||||||
|
selectedEnvironmentID: string | null
|
||||||
|
isValidVersion: boolean
|
||||||
|
workspaceType: WorkspaceType
|
||||||
|
workspaceID: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "copyUrl"): void
|
(e: "copyUrl"): void
|
||||||
(e: "viewPublished"): void
|
(e: "viewPublished"): void
|
||||||
|
(e: "update:publishTitle", value: string): void
|
||||||
|
(e: "update:publishVersion", value: string): void
|
||||||
|
(e: "update:autoSync", value: boolean): void
|
||||||
|
(e: "update:selectedEnvironmentID", value: string | null): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const titleModel = computed({
|
||||||
|
get: () => props.publishTitle,
|
||||||
|
set: (v) => emit("update:publishTitle", v),
|
||||||
|
})
|
||||||
|
|
||||||
|
const versionModel = computed({
|
||||||
|
get: () => props.publishVersion,
|
||||||
|
set: (v) => emit("update:publishVersion", v),
|
||||||
|
})
|
||||||
|
|
||||||
|
const autoSyncModel = computed({
|
||||||
|
get: () => props.autoSync,
|
||||||
|
set: (v) => emit("update:autoSync", v),
|
||||||
|
})
|
||||||
|
|
||||||
|
const environmentModel = computed({
|
||||||
|
get: () => props.selectedEnvironmentID,
|
||||||
|
set: (v) => emit("update:selectedEnvironmentID", v),
|
||||||
|
})
|
||||||
|
|
||||||
const copyIcon = refAutoReset(markRaw(IconCopy), 3000)
|
const copyIcon = refAutoReset(markRaw(IconCopy), 3000)
|
||||||
|
|
||||||
const handleCopyUrl = () => {
|
const handleCopyUrl = () => {
|
||||||
|
|
@ -221,14 +300,6 @@ const snapshotCollectionData = ref<HoppCollection | null>(null)
|
||||||
const snapshotItems = ref<SnapshotDocumentationItem[]>([])
|
const snapshotItems = ref<SnapshotDocumentationItem[]>([])
|
||||||
const snapshotEnvironmentVariables = ref<Environment["variables"]>([])
|
const snapshotEnvironmentVariables = ref<Environment["variables"]>([])
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the currently displayed published doc is the live (current) version.
|
|
||||||
*/
|
|
||||||
const isLive = computed(() => {
|
|
||||||
if (!props.existingData) return true
|
|
||||||
return isLiveVersion(props.existingData)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts slug and version from a published doc URL
|
* Extracts slug and version from a published doc URL
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,13 @@
|
||||||
<div
|
<div
|
||||||
class="flex items-center border border-accent pl-4 pr-2 rounded cursor-pointer"
|
class="flex items-center border border-accent pl-4 pr-2 rounded cursor-pointer"
|
||||||
>
|
>
|
||||||
<icon-lucide-globe class="svg-icons" />
|
<div class="relative flex items-center">
|
||||||
|
<icon-lucide-globe class="svg-icons" />
|
||||||
|
<span
|
||||||
|
v-if="selectedVersionDoc?.autoSync"
|
||||||
|
class="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-green-500 ring-1 ring-primary animate-pulse"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconCheveronDown"
|
:icon="IconCheveronDown"
|
||||||
reverse
|
reverse
|
||||||
|
|
@ -237,11 +242,6 @@
|
||||||
:workspace-i-d="isTeamCollection ? teamID || '' : ''"
|
:workspace-i-d="isTeamCollection ? teamID || '' : ''"
|
||||||
:mode="publishModalMode"
|
:mode="publishModalMode"
|
||||||
:is-first-publish="!isCollectionPublished && !isCreatingNewVersion"
|
:is-first-publish="!isCollectionPublished && !isCreatingNewVersion"
|
||||||
:is-auto-sync-locked="
|
|
||||||
!!selectedVersionDoc &&
|
|
||||||
isLiveVersion(selectedVersionDoc) &&
|
|
||||||
!isCreatingNewVersion
|
|
||||||
"
|
|
||||||
:published-doc-id="publishedDocId"
|
:published-doc-id="publishedDocId"
|
||||||
:existing-data="existingPublishedData"
|
:existing-data="existingPublishedData"
|
||||||
:loading="isProcessingPublish"
|
:loading="isProcessingPublish"
|
||||||
|
|
@ -383,9 +383,12 @@ const publishedDocs = computed(() => {
|
||||||
|
|
||||||
const selectedVersionDoc = ref<PublishedDocInfo | null>(null)
|
const selectedVersionDoc = ref<PublishedDocInfo | null>(null)
|
||||||
|
|
||||||
|
// When viewing a snapshot from the dropdown, we use a separate ref so the
|
||||||
|
// selected (dropdown) version isn't changed. The modal reads from this ref.
|
||||||
|
const viewingSnapshotDoc = ref<PublishedDocInfo | null>(null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the CURRENT version from the published docs list.
|
* Finds the live (auto-synced) version from the published docs list.
|
||||||
* The CURRENT version is the initial publish — identified by version string "CURRENT" (case-insensitive).
|
|
||||||
* Falls back to the last doc (oldest, since the list is in descending order).
|
* Falls back to the last doc (oldest, since the list is in descending order).
|
||||||
*/
|
*/
|
||||||
const findCurrentVersion = (docs: PublishedDocInfo[]): PublishedDocInfo => {
|
const findCurrentVersion = (docs: PublishedDocInfo[]): PublishedDocInfo => {
|
||||||
|
|
@ -395,6 +398,14 @@ const findCurrentVersion = (docs: PublishedDocInfo[]): PublishedDocInfo => {
|
||||||
watch(
|
watch(
|
||||||
publishedDocs,
|
publishedDocs,
|
||||||
(docs) => {
|
(docs) => {
|
||||||
|
// Keep the snapshot-viewing doc in sync with the latest data (by ID)
|
||||||
|
if (viewingSnapshotDoc.value) {
|
||||||
|
const foundViewing = docs?.find(
|
||||||
|
(d) => d.id === viewingSnapshotDoc.value?.id
|
||||||
|
)
|
||||||
|
viewingSnapshotDoc.value = foundViewing ?? null
|
||||||
|
}
|
||||||
|
|
||||||
if (docs && docs.length > 0) {
|
if (docs && docs.length > 0) {
|
||||||
// If we already have a selected version, try to keep it (by ID)
|
// If we already have a selected version, try to keep it (by ID)
|
||||||
if (selectedVersionDoc.value) {
|
if (selectedVersionDoc.value) {
|
||||||
|
|
@ -414,17 +425,25 @@ watch(
|
||||||
)
|
)
|
||||||
|
|
||||||
const isCollectionPublished = computed(() => publishedDocs.value.length > 0)
|
const isCollectionPublished = computed(() => publishedDocs.value.length > 0)
|
||||||
const publishedDocId = computed(() => selectedVersionDoc.value?.id)
|
|
||||||
|
// The doc that the publish modal should operate on — the snapshot-being-viewed
|
||||||
|
// takes precedence, otherwise fall back to the selected (dropdown) version.
|
||||||
|
const activeModalDoc = computed<PublishedDocInfo | null>(
|
||||||
|
() => viewingSnapshotDoc.value || selectedVersionDoc.value
|
||||||
|
)
|
||||||
|
|
||||||
|
const publishedDocId = computed(() => activeModalDoc.value?.id)
|
||||||
const existingPublishedData = computed(() => {
|
const existingPublishedData = computed(() => {
|
||||||
if (isCreatingNewVersion.value) return undefined
|
if (isCreatingNewVersion.value) return undefined
|
||||||
if (!selectedVersionDoc.value) return undefined
|
if (!activeModalDoc.value) return undefined
|
||||||
return {
|
return {
|
||||||
title: selectedVersionDoc.value.title,
|
id: activeModalDoc.value.id,
|
||||||
version: selectedVersionDoc.value.version,
|
title: activeModalDoc.value.title,
|
||||||
autoSync: selectedVersionDoc.value.autoSync,
|
version: activeModalDoc.value.version,
|
||||||
url: selectedVersionDoc.value.url,
|
autoSync: activeModalDoc.value.autoSync,
|
||||||
environmentName: selectedVersionDoc.value.environmentName ?? null,
|
url: activeModalDoc.value.url,
|
||||||
environmentID: selectedVersionDoc.value.environmentID ?? null,
|
environmentName: activeModalDoc.value.environmentName ?? null,
|
||||||
|
environmentID: activeModalDoc.value.environmentID ?? null,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -592,18 +611,22 @@ const openPublishModalForView = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles version selection from the dropdown.
|
* Handles version selection from the dropdown.
|
||||||
* For frozen (snapshot) versions, auto-opens the snapshot view modal.
|
* For frozen (snapshot) versions, auto-opens the snapshot view modal without
|
||||||
|
* changing the currently selected dropdown version.
|
||||||
* For live versions, just selects them (user can then click Edit).
|
* For live versions, just selects them (user can then click Edit).
|
||||||
*/
|
*/
|
||||||
const handleVersionSelect = (
|
const handleVersionSelect = (
|
||||||
doc: PublishedDocInfo,
|
doc: PublishedDocInfo,
|
||||||
hideDropdown: () => void
|
hideDropdown: () => void
|
||||||
) => {
|
) => {
|
||||||
selectedVersionDoc.value = doc
|
|
||||||
if (!isLiveVersion(doc)) {
|
if (!isLiveVersion(doc)) {
|
||||||
|
viewingSnapshotDoc.value = doc
|
||||||
hideDropdown()
|
hideDropdown()
|
||||||
openPublishModalForView()
|
openPublishModalForView()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
selectedVersionDoc.value = doc
|
||||||
|
hideDropdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
const createNewVersion = () => {
|
const createNewVersion = () => {
|
||||||
|
|
@ -616,11 +639,10 @@ watch(showPublishModal, (isOpen) => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
// Reset selection back to the CURRENT version so the dropdown
|
// Reset selection back to the CURRENT version so the dropdown
|
||||||
// label matches what the editor is actually showing
|
// label matches what the editor is actually showing
|
||||||
if (isViewingSnapshot.value || isCreatingNewVersion.value) {
|
if (isCreatingNewVersion.value && publishedDocs.value.length > 0) {
|
||||||
if (publishedDocs.value.length > 0) {
|
selectedVersionDoc.value = findCurrentVersion(publishedDocs.value)
|
||||||
selectedVersionDoc.value = findCurrentVersion(publishedDocs.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
viewingSnapshotDoc.value = null
|
||||||
isCreatingNewVersion.value = false
|
isCreatingNewVersion.value = false
|
||||||
isViewingSnapshot.value = false
|
isViewingSnapshot.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -1005,9 +1027,17 @@ const handlePublish = async (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select the new version and exit create mode
|
// Only select the new version if it's live; for non-live (snapshot)
|
||||||
selectedVersionDoc.value = newDocInfo
|
// versions, keep the previously selected live version in the dropdown
|
||||||
isCreatingNewVersion.value = false
|
// and close the modal (otherwise the mode would recompute to "update"
|
||||||
|
// for the still-selected live version).
|
||||||
|
if (isLiveVersion(newDocInfo)) {
|
||||||
|
selectedVersionDoc.value = newDocInfo
|
||||||
|
isCreatingNewVersion.value = false
|
||||||
|
} else {
|
||||||
|
isCreatingNewVersion.value = false
|
||||||
|
showPublishModal.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)()
|
)()
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,32 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span
|
<span
|
||||||
class="text-md font-bold text-secondaryDark px-6 py-1 rounded-full border border-dividerDark shadow"
|
class="text-md font-bold text-secondaryDark px-6 py-1 rounded-full"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
publishedDoc?.title || t("documentation.publish.untitled_project")
|
publishedDoc?.title || t("documentation.publish.untitled_project")
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div>
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- Live indicator pill -->
|
||||||
|
<div
|
||||||
|
v-if="isCurrentDocLive"
|
||||||
|
class="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-gradient-to-r from-green-500/10 to-emerald-500/10 shadow-sm shadow-green-500/10"
|
||||||
|
>
|
||||||
|
<span class="relative flex items-center justify-center">
|
||||||
|
<span
|
||||||
|
class="absolute w-2 h-2 rounded-full bg-green-500/40 animate-ping"
|
||||||
|
/>
|
||||||
|
<span class="relative w-1 h-1 rounded-full bg-green-500" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="text-[9px] font-bold uppercase tracking-wider text-green-600"
|
||||||
|
>
|
||||||
|
{{ t("documentation.publish.live") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Version dropdown (when multiple versions exist) -->
|
<!-- Version dropdown (when multiple versions exist) -->
|
||||||
<tippy
|
<tippy
|
||||||
v-if="versions.length"
|
v-if="versions.length"
|
||||||
|
|
@ -28,17 +46,11 @@
|
||||||
:on-shown="() => versionDropdownRef?.focus()"
|
:on-shown="() => versionDropdownRef?.focus()"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md cursor-pointer transition-colors"
|
class="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md cursor-pointer transition-colors bg-accent/10 text-accent hover:bg-accent/20 border border-dividerDark"
|
||||||
:class="
|
|
||||||
isCurrentDocLive
|
|
||||||
? 'bg-green-500/10 text-green-600 hover:bg-green-500/20'
|
|
||||||
: 'bg-accent/10 text-accent hover:bg-accent/20'
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
|
<icon-lucide-globe class="w-3.5 h-3.5" />
|
||||||
{{
|
{{
|
||||||
isCurrentDocLive
|
publishedDoc?.version || t("documentation.publish.published")
|
||||||
? t("documentation.publish.live")
|
|
||||||
: `${publishedDoc?.version}`
|
|
||||||
}}
|
}}
|
||||||
<icon-lucide-chevron-down class="w-3 h-3" />
|
<icon-lucide-chevron-down class="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -51,40 +63,30 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
<div
|
||||||
v-for="ver in versions"
|
v-for="ver in versions"
|
||||||
:key="ver.id"
|
:key="ver.id"
|
||||||
:label="getVersionLabel(ver)"
|
:class="{ 'version-item--live': isLiveVersion(ver) }"
|
||||||
:info-icon="
|
class="flex-1"
|
||||||
ver.version === publishedDoc?.version
|
|
||||||
? IconCheck
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
:active-info-icon="ver.version === publishedDoc?.version"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
navigateToVersion(ver)
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<HoppSmartItem
|
||||||
<span
|
:icon="IconGlobe"
|
||||||
class="px-1.5 py-0.5 text-[10px] font-semibold uppercase rounded mr-2"
|
:label="ver.version"
|
||||||
:class="
|
:info-icon="
|
||||||
isLiveVersion(ver)
|
ver.version === publishedDoc?.version
|
||||||
? 'bg-green-500/10 text-green-600'
|
? IconCheck
|
||||||
: 'bg-accent/10 text-accent'
|
: undefined
|
||||||
"
|
"
|
||||||
>
|
:active-info-icon="ver.version === publishedDoc?.version"
|
||||||
{{
|
class="w-full"
|
||||||
isLiveVersion(ver)
|
@click="
|
||||||
? t("documentation.publish.live")
|
() => {
|
||||||
: t("documentation.publish.snapshot")
|
navigateToVersion(ver)
|
||||||
}}
|
hide()
|
||||||
</span>
|
}
|
||||||
</template>
|
"
|
||||||
</HoppSmartItem>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</tippy>
|
</tippy>
|
||||||
|
|
@ -157,6 +159,7 @@ import { useRouter } from "vue-router"
|
||||||
import { computed, PropType, ref } from "vue"
|
import { computed, PropType, ref } from "vue"
|
||||||
import { PublishedDocs } from "~/helpers/backend/graphql"
|
import { PublishedDocs } from "~/helpers/backend/graphql"
|
||||||
import IconCheck from "~icons/lucide/check"
|
import IconCheck from "~icons/lucide/check"
|
||||||
|
import IconGlobe from "~icons/lucide/globe"
|
||||||
import IconLayers from "~icons/lucide/layers"
|
import IconLayers from "~icons/lucide/layers"
|
||||||
import { isLiveVersion } from "~/services/documentation.service"
|
import { isLiveVersion } from "~/services/documentation.service"
|
||||||
|
|
||||||
|
|
@ -203,22 +206,16 @@ const versionDropdownRef = ref<HTMLElement | null>(null)
|
||||||
const envDropdownRef = ref<HTMLElement | null>(null)
|
const envDropdownRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the currently displayed published doc is the live (current) version.
|
* Checks whether the currently displayed published doc is the live version —
|
||||||
* This is true if the doc is auto-synced, has the CURRENT version identifier, or has version 1.0.0 (legacy).
|
* this is purely based on the auto-sync flag.
|
||||||
*/
|
*/
|
||||||
const isCurrentDocLive = computed(() => {
|
const isCurrentDocLive = computed(() => {
|
||||||
if (!props.publishedDoc?.version) return true
|
if (!props.publishedDoc) return false
|
||||||
return isLiveVersion({
|
return isLiveVersion({
|
||||||
autoSync: props.publishedDoc.autoSync ?? false,
|
autoSync: props.publishedDoc.autoSync ?? false,
|
||||||
version: props.publishedDoc.version,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const getVersionLabel = (ver: PublishedDocVersion): string => {
|
|
||||||
if (isLiveVersion(ver)) return t("documentation.publish.live")
|
|
||||||
return `${ver.version}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigateToVersion = (ver: PublishedDocVersion) => {
|
const navigateToVersion = (ver: PublishedDocVersion) => {
|
||||||
if (ver.version === props.publishedDoc?.version) return
|
if (ver.version === props.publishedDoc?.version) return
|
||||||
|
|
||||||
|
|
@ -235,3 +232,10 @@ const navigateToVersion = (ver: PublishedDocVersion) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Color the leading globe icon green for live versions */
|
||||||
|
.version-item--live :deep(.svg-icons.mr-4) {
|
||||||
|
@apply text-green-500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import {
|
||||||
SetCollectionDocumentationOptions,
|
SetCollectionDocumentationOptions,
|
||||||
SetRequestDocumentationOptions,
|
SetRequestDocumentationOptions,
|
||||||
isLiveVersion,
|
isLiveVersion,
|
||||||
CURRENT_VERSION_TAG,
|
|
||||||
} from "../documentation.service"
|
} from "../documentation.service"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
|
|
@ -750,33 +749,11 @@ describe("DocumentationService", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("isLiveVersion", () => {
|
describe("isLiveVersion", () => {
|
||||||
it("returns true when autoSync is true and version is CURRENT", () => {
|
it("returns true when autoSync is true", () => {
|
||||||
expect(
|
expect(isLiveVersion({ autoSync: true })).toBe(true)
|
||||||
isLiveVersion({ autoSync: true, version: CURRENT_VERSION_TAG })
|
|
||||||
).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("is case-insensitive for CURRENT tag", () => {
|
it("returns false when autoSync is false", () => {
|
||||||
expect(isLiveVersion({ autoSync: true, version: "current" })).toBe(true)
|
expect(isLiveVersion({ autoSync: false })).toBe(false)
|
||||||
expect(isLiveVersion({ autoSync: true, version: "Current" })).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns true for legacy 1.0.0 version with autoSync", () => {
|
|
||||||
expect(isLiveVersion({ autoSync: true, version: "1.0.0" })).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns false when autoSync is false even if version is CURRENT", () => {
|
|
||||||
expect(
|
|
||||||
isLiveVersion({ autoSync: false, version: CURRENT_VERSION_TAG })
|
|
||||||
).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns false when autoSync is false for legacy 1.0.0", () => {
|
|
||||||
expect(isLiveVersion({ autoSync: false, version: "1.0.0" })).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns false for a snapshot version string", () => {
|
|
||||||
expect(isLiveVersion({ autoSync: true, version: "2.0.0" })).toBe(false)
|
|
||||||
expect(isLiveVersion({ autoSync: false, version: "2.0.0" })).toBe(false)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -100,16 +100,11 @@ export const CURRENT_VERSION_TAG = "CURRENT"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a published doc version is the live (current) version.
|
* Checks whether a published doc version is the live (current) version.
|
||||||
* A live version is auto-synced, has the CURRENT version identifier,
|
* A live version is one that has auto-sync enabled — it stays in sync with
|
||||||
* or has version 1.0.0 (used in older versions of the project).
|
* the collection and will update whenever the collection is updated.
|
||||||
* This version is in sync with the particular collection and will update if the collection is updated.
|
|
||||||
*/
|
*/
|
||||||
export const isLiveVersion = (doc: {
|
export const isLiveVersion = (doc: { autoSync: boolean }): boolean =>
|
||||||
autoSync: boolean
|
doc.autoSync
|
||||||
version: string
|
|
||||||
}): boolean =>
|
|
||||||
doc.autoSync &&
|
|
||||||
(doc.version.toUpperCase() === CURRENT_VERSION_TAG || doc.version === "1.0.0")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service manages edited documentation for collections and requests.
|
* This service manages edited documentation for collections and requests.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue