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",
|
||||
"create_new_version": "Create New Version",
|
||||
"invalid_version": "Version must only contain alphanumeric characters, dots, and hyphens",
|
||||
"snapshot_description": "This will snapshot the current documentation as this version",
|
||||
"live": "Live",
|
||||
"snapshot": "Snapshot",
|
||||
"version_immutable": "Published versions are read-only snapshots",
|
||||
|
|
@ -556,8 +555,9 @@
|
|||
"snapshot_empty": "No requests or folders in this snapshot",
|
||||
"snapshot_item_count": "{count} items",
|
||||
"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",
|
||||
"first_publish_hint": "Your documentation will be published as a live version that automatically stays in sync with your collection",
|
||||
"environment": "Environment",
|
||||
"no_environment": "No environment",
|
||||
"environment_description": "Attach an environment to resolve variables in the published documentation"
|
||||
|
|
|
|||
|
|
@ -1,55 +1,32 @@
|
|||
<template>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<div>
|
||||
<HoppSmartInput
|
||||
v-model="titleModel"
|
||||
:label="t('documentation.publish.doc_title')"
|
||||
type="text"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="titleModel"
|
||||
:label="t('documentation.publish.doc_title')"
|
||||
type="text"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="isFirstPublish"
|
||||
class="flex items-start space-x-2 px-3 py-2.5 rounded-md bg-green-500/5 border border-green-500/15"
|
||||
<!-- Version Input -->
|
||||
<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
|
||||
v-if="!isValidVersion && versionModel.length > 0"
|
||||
class="text-xs text-red-500 mt-1 block"
|
||||
>
|
||||
<icon-lucide-info
|
||||
class="w-3.5 h-3.5 text-green-600 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<span class="text-xs text-green-600 leading-relaxed">
|
||||
{{ t("documentation.publish.first_publish_hint") }}
|
||||
</span>
|
||||
</div>
|
||||
{{ t("documentation.publish.invalid_version") }}
|
||||
</span>
|
||||
|
||||
<!-- Version Input (hidden for first publish) -->
|
||||
<div v-if="!isFirstPublish">
|
||||
<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">
|
||||
<!-- Auto-sync Toggle -->
|
||||
<div class="flex items-start">
|
||||
<HoppSmartCheckbox
|
||||
:on="autoSyncModel"
|
||||
@change="autoSyncModel = !autoSyncModel"
|
||||
|
|
@ -65,6 +42,19 @@
|
|||
</HoppSmartCheckbox>
|
||||
</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 -->
|
||||
<div class="space-y-2">
|
||||
<span class="block text-sm font-medium text-secondaryDark">
|
||||
|
|
@ -130,8 +120,6 @@ const props = defineProps<{
|
|||
autoSync: boolean
|
||||
selectedEnvironmentID: string | null
|
||||
publishedUrl: string | null
|
||||
isFirstPublish: boolean
|
||||
isAutoSyncLocked: boolean
|
||||
isValidVersion: boolean
|
||||
workspaceType: WorkspaceType
|
||||
workspaceID: string
|
||||
|
|
|
|||
|
|
@ -3,15 +3,26 @@
|
|||
v-if="show"
|
||||
dialog
|
||||
: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"
|
||||
>
|
||||
<template #body>
|
||||
<CollectionsDocumentationPublishDocSnapshotPreview
|
||||
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"
|
||||
:published-url="publishedUrl"
|
||||
:show="show && mode === 'view'"
|
||||
:is-valid-version="isValidVersion"
|
||||
:workspace-type="workspaceType"
|
||||
:workspace-i-d="workspaceID"
|
||||
@copy-url="copyUrl"
|
||||
@view-published="viewPublished"
|
||||
/>
|
||||
|
|
@ -24,8 +35,6 @@
|
|||
v-model:auto-sync="autoSync"
|
||||
v-model:selected-environment-i-d="selectedEnvironmentID"
|
||||
:published-url="publishedUrl"
|
||||
:is-first-publish="isFirstPublish ?? false"
|
||||
:is-auto-sync-locked="isAutoSyncLocked ?? false"
|
||||
:is-valid-version="isValidVersion"
|
||||
:workspace-type="workspaceType"
|
||||
:workspace-i-d="workspaceID"
|
||||
|
|
@ -46,7 +55,7 @@
|
|||
@click="handlePublish"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
v-else-if="mode === 'update'"
|
||||
v-else-if="mode === 'update' || mode === 'view'"
|
||||
:label="t('documentation.publish.update_button')"
|
||||
:disabled="!canPublish || loading || !hasChanges"
|
||||
:loading="loading"
|
||||
|
|
@ -112,9 +121,9 @@ const props = defineProps<{
|
|||
workspaceID: string
|
||||
mode?: "create" | "update" | "view"
|
||||
isFirstPublish?: boolean
|
||||
isAutoSyncLocked?: boolean
|
||||
publishedDocId?: string
|
||||
existingData?: {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
autoSync: boolean
|
||||
|
|
@ -168,17 +177,30 @@ const initializeFormData = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Watch for modal open/close
|
||||
watch(
|
||||
[() => props.existingData, () => props.show],
|
||||
([, isOpen]) => {
|
||||
if (isOpen) {
|
||||
initializeFormData()
|
||||
}
|
||||
() => props.show,
|
||||
(isOpen) => {
|
||||
if (isOpen) initializeFormData()
|
||||
},
|
||||
{ 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(() => {
|
||||
if (props.mode === "update") return t("documentation.publish.update_title")
|
||||
if (props.mode === "view") return t("documentation.publish.view_title")
|
||||
|
|
|
|||
|
|
@ -1,37 +1,67 @@
|
|||
<template>
|
||||
<div class="flex flex-col lg:flex-row gap-4 flex-1">
|
||||
<!-- Left Metadata Panel -->
|
||||
<div class="lg:w-72 flex-shrink-0 flex flex-col space-y-3">
|
||||
<!-- Title & version header -->
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-sm font-semibold text-secondaryDark truncate">
|
||||
{{ existingData?.title }}
|
||||
</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div
|
||||
class="lg:w-96 flex-shrink-0 flex flex-col divide-y divide-divider space-y-4"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<HoppSmartInput
|
||||
v-model="titleModel"
|
||||
: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
|
||||
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>
|
||||
</div>
|
||||
<!-- Environment badge -->
|
||||
<div
|
||||
v-if="existingData?.environmentName"
|
||||
class="flex items-center space-x-1.5"
|
||||
>
|
||||
<icon-lucide-layers
|
||||
class="w-3 h-3 text-secondaryLight flex-shrink-0"
|
||||
/>
|
||||
<span class="text-xs text-secondaryLight">
|
||||
{{ existingData.environmentName }}
|
||||
|
||||
<div class="flex items-start">
|
||||
<HoppSmartCheckbox
|
||||
:on="autoSyncModel"
|
||||
@change="autoSyncModel = !autoSyncModel"
|
||||
>
|
||||
<div>
|
||||
<span class="text-sm text-secondaryDark">
|
||||
{{ t("documentation.publish.auto_sync") }}
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<hr class="border-divider" />
|
||||
|
||||
<!-- Published URL -->
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-3 py-4">
|
||||
<div v-if="publishedUrl" class="space-y-1">
|
||||
<label
|
||||
class="text-[10px] font-semibold uppercase tracking-wider text-secondaryLight"
|
||||
|
|
@ -66,20 +96,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status notice -->
|
||||
<!-- Status notice: version is already live -->
|
||||
<div
|
||||
v-if="existingData && !isLive"
|
||||
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"
|
||||
v-if="existingData?.autoSync && autoSyncModel"
|
||||
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
|
||||
|
|
@ -89,6 +108,32 @@
|
|||
{{ t("documentation.publish.auto_sync_live_notice") }}
|
||||
</span>
|
||||
</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>
|
||||
|
||||
<!-- Right: Snapshot Preview -->
|
||||
|
|
@ -141,7 +186,10 @@
|
|||
</div>
|
||||
|
||||
<!-- 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
|
||||
:collection-data="snapshotCollectionData"
|
||||
:all-items="snapshotItems"
|
||||
|
|
@ -176,7 +224,7 @@ import IconCopy from "~icons/lucide/copy"
|
|||
import IconCheck from "~icons/lucide/check"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
import IconRefreshCw from "~icons/lucide/refresh-cw"
|
||||
import { isLiveVersion } from "~/services/documentation.service"
|
||||
import { WorkspaceType } from "~/helpers/backend/graphql"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
|
|
@ -193,13 +241,44 @@ const props = defineProps<{
|
|||
existingData?: ExistingData
|
||||
publishedUrl: string | null
|
||||
show: boolean
|
||||
publishTitle: string
|
||||
publishVersion: string
|
||||
autoSync: boolean
|
||||
selectedEnvironmentID: string | null
|
||||
isValidVersion: boolean
|
||||
workspaceType: WorkspaceType
|
||||
workspaceID: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "copyUrl"): 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 handleCopyUrl = () => {
|
||||
|
|
@ -221,14 +300,6 @@ const snapshotCollectionData = ref<HoppCollection | null>(null)
|
|||
const snapshotItems = ref<SnapshotDocumentationItem[]>([])
|
||||
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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -102,8 +102,13 @@
|
|||
<div
|
||||
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
|
||||
:icon="IconCheveronDown"
|
||||
reverse
|
||||
|
|
@ -237,11 +242,6 @@
|
|||
:workspace-i-d="isTeamCollection ? teamID || '' : ''"
|
||||
:mode="publishModalMode"
|
||||
:is-first-publish="!isCollectionPublished && !isCreatingNewVersion"
|
||||
:is-auto-sync-locked="
|
||||
!!selectedVersionDoc &&
|
||||
isLiveVersion(selectedVersionDoc) &&
|
||||
!isCreatingNewVersion
|
||||
"
|
||||
:published-doc-id="publishedDocId"
|
||||
:existing-data="existingPublishedData"
|
||||
:loading="isProcessingPublish"
|
||||
|
|
@ -383,9 +383,12 @@ const publishedDocs = computed(() => {
|
|||
|
||||
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.
|
||||
* The CURRENT version is the initial publish — identified by version string "CURRENT" (case-insensitive).
|
||||
* Finds the live (auto-synced) version from the published docs list.
|
||||
* Falls back to the last doc (oldest, since the list is in descending order).
|
||||
*/
|
||||
const findCurrentVersion = (docs: PublishedDocInfo[]): PublishedDocInfo => {
|
||||
|
|
@ -395,6 +398,14 @@ const findCurrentVersion = (docs: PublishedDocInfo[]): PublishedDocInfo => {
|
|||
watch(
|
||||
publishedDocs,
|
||||
(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 we already have a selected version, try to keep it (by ID)
|
||||
if (selectedVersionDoc.value) {
|
||||
|
|
@ -414,17 +425,25 @@ watch(
|
|||
)
|
||||
|
||||
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(() => {
|
||||
if (isCreatingNewVersion.value) return undefined
|
||||
if (!selectedVersionDoc.value) return undefined
|
||||
if (!activeModalDoc.value) return undefined
|
||||
return {
|
||||
title: selectedVersionDoc.value.title,
|
||||
version: selectedVersionDoc.value.version,
|
||||
autoSync: selectedVersionDoc.value.autoSync,
|
||||
url: selectedVersionDoc.value.url,
|
||||
environmentName: selectedVersionDoc.value.environmentName ?? null,
|
||||
environmentID: selectedVersionDoc.value.environmentID ?? null,
|
||||
id: activeModalDoc.value.id,
|
||||
title: activeModalDoc.value.title,
|
||||
version: activeModalDoc.value.version,
|
||||
autoSync: activeModalDoc.value.autoSync,
|
||||
url: activeModalDoc.value.url,
|
||||
environmentName: activeModalDoc.value.environmentName ?? null,
|
||||
environmentID: activeModalDoc.value.environmentID ?? null,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -592,18 +611,22 @@ const openPublishModalForView = () => {
|
|||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
const handleVersionSelect = (
|
||||
doc: PublishedDocInfo,
|
||||
hideDropdown: () => void
|
||||
) => {
|
||||
selectedVersionDoc.value = doc
|
||||
if (!isLiveVersion(doc)) {
|
||||
viewingSnapshotDoc.value = doc
|
||||
hideDropdown()
|
||||
openPublishModalForView()
|
||||
return
|
||||
}
|
||||
selectedVersionDoc.value = doc
|
||||
hideDropdown()
|
||||
}
|
||||
|
||||
const createNewVersion = () => {
|
||||
|
|
@ -616,11 +639,10 @@ watch(showPublishModal, (isOpen) => {
|
|||
if (!isOpen) {
|
||||
// Reset selection back to the CURRENT version so the dropdown
|
||||
// label matches what the editor is actually showing
|
||||
if (isViewingSnapshot.value || isCreatingNewVersion.value) {
|
||||
if (publishedDocs.value.length > 0) {
|
||||
selectedVersionDoc.value = findCurrentVersion(publishedDocs.value)
|
||||
}
|
||||
if (isCreatingNewVersion.value && publishedDocs.value.length > 0) {
|
||||
selectedVersionDoc.value = findCurrentVersion(publishedDocs.value)
|
||||
}
|
||||
viewingSnapshotDoc.value = null
|
||||
isCreatingNewVersion.value = false
|
||||
isViewingSnapshot.value = false
|
||||
}
|
||||
|
|
@ -1005,9 +1027,17 @@ const handlePublish = async (
|
|||
)
|
||||
}
|
||||
|
||||
// Select the new version and exit create mode
|
||||
selectedVersionDoc.value = newDocInfo
|
||||
isCreatingNewVersion.value = false
|
||||
// Only select the new version if it's live; for non-live (snapshot)
|
||||
// versions, keep the previously selected live version in the dropdown
|
||||
// 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 class="flex items-center gap-4">
|
||||
<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")
|
||||
}}
|
||||
</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) -->
|
||||
<tippy
|
||||
v-if="versions.length"
|
||||
|
|
@ -28,17 +46,11 @@
|
|||
:on-shown="() => versionDropdownRef?.focus()"
|
||||
>
|
||||
<button
|
||||
class="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md cursor-pointer transition-colors"
|
||||
:class="
|
||||
isCurrentDocLive
|
||||
? 'bg-green-500/10 text-green-600 hover:bg-green-500/20'
|
||||
: 'bg-accent/10 text-accent hover:bg-accent/20'
|
||||
"
|
||||
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"
|
||||
>
|
||||
<icon-lucide-globe class="w-3.5 h-3.5" />
|
||||
{{
|
||||
isCurrentDocLive
|
||||
? t("documentation.publish.live")
|
||||
: `${publishedDoc?.version}`
|
||||
publishedDoc?.version || t("documentation.publish.published")
|
||||
}}
|
||||
<icon-lucide-chevron-down class="w-3 h-3" />
|
||||
</button>
|
||||
|
|
@ -51,40 +63,30 @@
|
|||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
<div
|
||||
v-for="ver in versions"
|
||||
:key="ver.id"
|
||||
:label="getVersionLabel(ver)"
|
||||
:info-icon="
|
||||
ver.version === publishedDoc?.version
|
||||
? IconCheck
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="ver.version === publishedDoc?.version"
|
||||
@click="
|
||||
() => {
|
||||
navigateToVersion(ver)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
:class="{ 'version-item--live': isLiveVersion(ver) }"
|
||||
class="flex-1"
|
||||
>
|
||||
<template #prefix>
|
||||
<span
|
||||
class="px-1.5 py-0.5 text-[10px] font-semibold uppercase rounded mr-2"
|
||||
:class="
|
||||
isLiveVersion(ver)
|
||||
? 'bg-green-500/10 text-green-600'
|
||||
: 'bg-accent/10 text-accent'
|
||||
"
|
||||
>
|
||||
{{
|
||||
isLiveVersion(ver)
|
||||
? t("documentation.publish.live")
|
||||
: t("documentation.publish.snapshot")
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartItem>
|
||||
<HoppSmartItem
|
||||
:icon="IconGlobe"
|
||||
:label="ver.version"
|
||||
:info-icon="
|
||||
ver.version === publishedDoc?.version
|
||||
? IconCheck
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="ver.version === publishedDoc?.version"
|
||||
class="w-full"
|
||||
@click="
|
||||
() => {
|
||||
navigateToVersion(ver)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
|
|
@ -157,6 +159,7 @@ import { useRouter } from "vue-router"
|
|||
import { computed, PropType, ref } from "vue"
|
||||
import { PublishedDocs } from "~/helpers/backend/graphql"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconGlobe from "~icons/lucide/globe"
|
||||
import IconLayers from "~icons/lucide/layers"
|
||||
import { isLiveVersion } from "~/services/documentation.service"
|
||||
|
||||
|
|
@ -203,22 +206,16 @@ const versionDropdownRef = ref<HTMLElement | null>(null)
|
|||
const envDropdownRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/**
|
||||
* Checks whether the currently displayed published doc is the live (current) version.
|
||||
* This is true if the doc is auto-synced, has the CURRENT version identifier, or has version 1.0.0 (legacy).
|
||||
* Checks whether the currently displayed published doc is the live version —
|
||||
* this is purely based on the auto-sync flag.
|
||||
*/
|
||||
const isCurrentDocLive = computed(() => {
|
||||
if (!props.publishedDoc?.version) return true
|
||||
if (!props.publishedDoc) return false
|
||||
return isLiveVersion({
|
||||
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) => {
|
||||
if (ver.version === props.publishedDoc?.version) return
|
||||
|
||||
|
|
@ -235,3 +232,10 @@ const navigateToVersion = (ver: PublishedDocVersion) => {
|
|||
}
|
||||
}
|
||||
</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,
|
||||
SetRequestDocumentationOptions,
|
||||
isLiveVersion,
|
||||
CURRENT_VERSION_TAG,
|
||||
} from "../documentation.service"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
|
|
@ -750,33 +749,11 @@ describe("DocumentationService", () => {
|
|||
})
|
||||
|
||||
describe("isLiveVersion", () => {
|
||||
it("returns true when autoSync is true and version is CURRENT", () => {
|
||||
expect(
|
||||
isLiveVersion({ autoSync: true, version: CURRENT_VERSION_TAG })
|
||||
).toBe(true)
|
||||
it("returns true when autoSync is true", () => {
|
||||
expect(isLiveVersion({ autoSync: true })).toBe(true)
|
||||
})
|
||||
|
||||
it("is case-insensitive for CURRENT tag", () => {
|
||||
expect(isLiveVersion({ autoSync: true, version: "current" })).toBe(true)
|
||||
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)
|
||||
it("returns false when autoSync is false", () => {
|
||||
expect(isLiveVersion({ autoSync: false })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -100,16 +100,11 @@ export const CURRENT_VERSION_TAG = "CURRENT"
|
|||
|
||||
/**
|
||||
* Checks whether a published doc version is the live (current) version.
|
||||
* A live version is auto-synced, has the CURRENT version identifier,
|
||||
* or has version 1.0.0 (used in older versions of the project).
|
||||
* This version is in sync with the particular collection and will update if the collection is updated.
|
||||
* A live version is one that has auto-sync enabled — it stays in sync with
|
||||
* the collection and will update whenever the collection is updated.
|
||||
*/
|
||||
export const isLiveVersion = (doc: {
|
||||
autoSync: boolean
|
||||
version: string
|
||||
}): boolean =>
|
||||
doc.autoSync &&
|
||||
(doc.version.toUpperCase() === CURRENT_VERSION_TAG || doc.version === "1.0.0")
|
||||
export const isLiveVersion = (doc: { autoSync: boolean }): boolean =>
|
||||
doc.autoSync
|
||||
|
||||
/**
|
||||
* This service manages edited documentation for collections and requests.
|
||||
|
|
|
|||
Loading…
Reference in a new issue