Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions app/components/Package/Versions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,7 @@ function versionRoute(version: string): RouteLocationRaw {
}

// Route to the full versions history page
const versionsPageRoute = computed((): RouteLocationRaw => {
const [org, name = ''] = props.packageName.startsWith('@')
? props.packageName.split('/')
: ['', props.packageName]
return { name: 'package-versions', params: { org, name } }
})
const versionsPageRoute = computed(() => packageVersionsRoute(props.packageName))

// Version to tags lookup (supports multiple tags per version)
const versionToTags = computed(() => buildVersionToTagsMap(props.distTags))
Expand Down
107 changes: 80 additions & 27 deletions app/components/VersionSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ const isLoadingAll = shallowRef(false)
/** Cached full version list */
const allVersionsCache = shallowRef<PackageVersionInfo[] | null>(null)

/** Whether non-tagged version groups are visible */
const showAllGroups = shallowRef(false)

// ============================================================================
// Computed
// ============================================================================
Expand All @@ -78,6 +81,18 @@ const latestVersion = computed(() => props.distTags.latest)

const versionToTags = computed(() => buildVersionToTagsMap(props.distTags))

const visibleVersionGroups = computed(() => {
if (!hasLoadedAll.value || showAllGroups.value) {
return versionGroups.value
}

return versionGroups.value.filter(group => group.primaryVersion.tags?.length)
})

const hasAdditionalGroups = computed(() =>
versionGroups.value.some(group => !group.primaryVersion.tags?.length),
)

/** Get URL for a specific version */
function getVersionUrl(version: string): string {
return props.urlPattern.replace('{version}', version)
Expand Down Expand Up @@ -310,30 +325,40 @@ async function toggleGroup(groupId: string) {
const group = versionGroups.value.find(g => g.id === groupId)
if (!group) return

if (group.isExpanded) {
group.isExpanded = false
if (group.isLoading) return

if (hasLoadedAll.value) {
if (hasNestedVersions(group)) {
group.isExpanded = !group.isExpanded
return
}

if (controlsAdditionalGroups(group)) {
showAllGroups.value = !showAllGroups.value
}

return
}

// Load all versions if not yet loaded
if (!hasLoadedAll.value) {
group.isLoading = true
try {
const allVersions = await loadAllVersions()
processLoadedVersions(allVersions)
// Find the group again after processing (it may have moved)
const updatedGroup = versionGroups.value.find(g => g.id === groupId)
if (updatedGroup) {
group.isLoading = true
try {
const allVersions = await loadAllVersions()
processLoadedVersions(allVersions)

// Find the group again after processing (it may have moved)
const updatedGroup = versionGroups.value.find(g => g.id === groupId)
if (updatedGroup) {
if (hasNestedVersions(updatedGroup)) {
updatedGroup.isExpanded = true
} else if (controlsAdditionalGroups(updatedGroup)) {
showAllGroups.value = true
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to load versions:', error)
} finally {
group.isLoading = false
}
} else {
group.isExpanded = true
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to load versions:', error)
} finally {
group.isLoading = false
}
}

Expand All @@ -345,7 +370,7 @@ async function toggleGroup(groupId: string) {
const flatItems = computed(() => {
const items: Array<{ type: 'group' | 'version'; groupId: string; version?: VersionDisplay }> = []

for (const group of versionGroups.value) {
for (const group of visibleVersionGroups.value) {
items.push({ type: 'group', groupId: group.id, version: group.primaryVersion })

if (group.isExpanded && group.versions.length > 1) {
Expand Down Expand Up @@ -401,7 +426,7 @@ function handleListboxKeydown(event: KeyboardEvent) {
const item = items[focusedIndex.value]
if (item?.type === 'group') {
const group = versionGroups.value.find(g => g.id === item.groupId)
if (group && !group.isExpanded && group.versions.length > 1) {
if (group && !isGroupOpen(group) && canToggleGroup(group)) {
toggleGroup(item.groupId)
}
}
Expand All @@ -414,6 +439,8 @@ function handleListboxKeydown(event: KeyboardEvent) {
const group = versionGroups.value.find(g => g.id === item.groupId)
if (group?.isExpanded) {
group.isExpanded = false
} else if (group && controlsAdditionalGroups(group) && showAllGroups.value) {
showAllGroups.value = false
}
} else if (item?.type === 'version') {
// Jump to parent group
Expand Down Expand Up @@ -450,6 +477,31 @@ function navigateToVersion(version: string) {
navigateTo(getVersionUrl(version))
}

function hasNestedVersions(group: VersionGroup): boolean {
return group.versions.length > 1
}

function controlsAdditionalGroups(group: VersionGroup): boolean {
return (
Boolean(group.primaryVersion.tags?.length) &&
!hasNestedVersions(group) &&
hasAdditionalGroups.value
)
}

function isGroupOpen(group: VersionGroup): boolean {
return group.isExpanded || (controlsAdditionalGroups(group) && showAllGroups.value)
}

function canToggleGroup(group: VersionGroup): boolean {
return (
group.isLoading ||
hasNestedVersions(group) ||
!hasLoadedAll.value ||
controlsAdditionalGroups(group)
)
}

// Reset focused index when dropdown opens
watch(isOpen, open => {
if (open) {
Expand All @@ -463,6 +515,7 @@ watch(isOpen, open => {
watch(
() => [props.distTags, props.versions, props.currentVersion],
() => {
showAllGroups.value = false
if (hasLoadedAll.value && allVersionsCache.value) {
processLoadedVersions(allVersionsCache.value)
} else {
Expand Down Expand Up @@ -518,7 +571,7 @@ watch(
@keydown="handleListboxKeydown"
>
<!-- Version groups -->
<div v-for="group in versionGroups" :key="group.id">
<div v-for="group in visibleVersionGroups" :key="group.id">
<!-- Group header (primary version) -->
<div
:id="`version-${group.primaryVersion.version}`"
Expand All @@ -539,11 +592,11 @@ watch(
>
<!-- Expand button -->
<button
v-if="group.versions.length > 1 || !hasLoadedAll"
v-if="canToggleGroup(group)"
type="button"
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors shrink-0"
:aria-expanded="group.isExpanded"
:aria-label="group.isExpanded ? $t('common.collapse') : $t('common.expand')"
:aria-expanded="isGroupOpen(group)"
:aria-label="isGroupOpen(group) ? $t('common.collapse') : $t('common.expand')"
@click.stop="toggleGroup(group.id)"
>
<span
Expand All @@ -554,11 +607,11 @@ watch(
<span
v-else
class="w-3 h-3 transition-transform duration-200 rtl-flip"
:class="group.isExpanded ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'"
:class="isGroupOpen(group) ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'"
aria-hidden="true"
/>
</button>
<span v-else class="w-4" />
<span v-else class="w-4 h-4 shrink-0" />

<!-- Version link -->
<NuxtLink
Expand Down Expand Up @@ -626,7 +679,7 @@ watch(
<!-- Link to package page for full version list -->
<div class="border-t border-border mt-1 pt-1 px-3 py-2">
<NuxtLink
:to="packageRoute(packageName)"
:to="packageVersionsRoute(packageName)"
class="text-xs text-fg-subtle hover:text-fg transition-[color] focus-visible:outline-none focus-visible:text-fg"
@click="isOpen = false"
>
Expand Down
6 changes: 6 additions & 0 deletions app/utils/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export function packageRoute(
}
}

/** Full version history page (`/package/.../versions`) */
export function packageVersionsRoute(packageName: string): RouteLocationRaw {
const [org, name = ''] = packageName.startsWith('@') ? packageName.split('/') : ['', packageName]
return { name: 'package-versions', params: { org, name } }
}

export function diffRoute(
packageName: string,
fromVersion: string,
Expand Down
43 changes: 43 additions & 0 deletions test/nuxt/components/Package/Versions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import type { DOMWrapper } from '@vue/test-utils'
import PackageVersions from '~/components/Package/Versions.vue'
import { packageVersionsRoute } from '~/utils/router'

// Mock the fetchAllPackageVersions function
const mockFetchAllPackageVersions = vi.fn()
Expand Down Expand Up @@ -39,6 +40,12 @@ function isVersionLink(a: DOMWrapper<Element>): boolean {
)
}

function getRouter(
component: Awaited<ReturnType<typeof mountSuspended>>,
): Pick<typeof component.vm.$router, 'resolve'> {
return component.vm.$router
}

describe('PackageVersions', () => {
beforeEach(() => {
mockFetchAllPackageVersions.mockReset()
Expand Down Expand Up @@ -109,6 +116,42 @@ describe('PackageVersions', () => {
expect(versionLinks[0]?.text()).toBe('1.0.0')
})

it('view-all-versions link uses packageVersionsRoute for unscoped packages', async () => {
const component = await mountSuspended(PackageVersions, {
props: {
packageName: 'test-package',
versions: {
'1.0.0': createVersion('1.0.0'),
},
distTags: { latest: '1.0.0' },
time: { '1.0.0': '2024-01-15T12:00:00.000Z' },
},
})

const router = getRouter(component)
const expectedHref = router.resolve(packageVersionsRoute('test-package')).href
const viewAll = component.find('[data-testid="view-all-versions-link"]')
expect(viewAll.attributes('href')).toBe(expectedHref)
})

it('view-all-versions link uses packageVersionsRoute for scoped packages', async () => {
const component = await mountSuspended(PackageVersions, {
props: {
packageName: '@scope/test-package',
versions: {
'1.0.0': createVersion('1.0.0'),
},
distTags: { latest: '1.0.0' },
time: { '1.0.0': '2024-01-15T12:00:00.000Z' },
},
})

const router = getRouter(component)
const expectedHref = router.resolve(packageVersionsRoute('@scope/test-package')).href
const viewAll = component.find('[data-testid="view-all-versions-link"]')
expect(viewAll.attributes('href')).toBe(expectedHref)
})

it('highlights the current version row when selectedVersion prop matches', async () => {
const component = await mountSuspended(PackageVersions, {
props: {
Expand Down
Loading
Loading