Skip to content

Commit 1106764

Browse files
tinseverautofix-ci[bot]ghostdevv
authored
fix(ui): version selector older groups and versions history link (#2276)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Willow (GHOST) <git@willow.sh>
1 parent 064cf97 commit 1106764

File tree

5 files changed

+314
-37
lines changed

5 files changed

+314
-37
lines changed

app/components/Package/Versions.vue

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,7 @@ function versionRoute(version: string): RouteLocationRaw {
9696
}
9797
9898
// Route to the full versions history page
99-
const versionsPageRoute = computed((): RouteLocationRaw => {
100-
const [org, name = ''] = props.packageName.startsWith('@')
101-
? props.packageName.split('/')
102-
: ['', props.packageName]
103-
return { name: 'package-versions', params: { org, name } }
104-
})
99+
const versionsPageRoute = computed(() => packageVersionsRoute(props.packageName))
105100
106101
// Version to tags lookup (supports multiple tags per version)
107102
const versionToTags = computed(() => buildVersionToTagsMap(props.distTags))

app/components/VersionSelector.vue

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ const isLoadingAll = shallowRef(false)
7070
/** Cached full version list */
7171
const allVersionsCache = shallowRef<PackageVersionInfo[] | null>(null)
7272
73+
/** Whether non-tagged version groups are visible */
74+
const showAllGroups = shallowRef(false)
75+
7376
// ============================================================================
7477
// Computed
7578
// ============================================================================
@@ -78,6 +81,18 @@ const latestVersion = computed(() => props.distTags.latest)
7881
7982
const versionToTags = computed(() => buildVersionToTagsMap(props.distTags))
8083
84+
const visibleVersionGroups = computed(() => {
85+
if (!hasLoadedAll.value || showAllGroups.value) {
86+
return versionGroups.value
87+
}
88+
89+
return versionGroups.value.filter(group => group.primaryVersion.tags?.length)
90+
})
91+
92+
const hasAdditionalGroups = computed(() =>
93+
versionGroups.value.some(group => !group.primaryVersion.tags?.length),
94+
)
95+
8196
/** Get URL for a specific version */
8297
function getVersionUrl(version: string): string {
8398
return props.urlPattern.replace('{version}', version)
@@ -310,30 +325,40 @@ async function toggleGroup(groupId: string) {
310325
const group = versionGroups.value.find(g => g.id === groupId)
311326
if (!group) return
312327
313-
if (group.isExpanded) {
314-
group.isExpanded = false
328+
if (group.isLoading) return
329+
330+
if (hasLoadedAll.value) {
331+
if (hasNestedVersions(group)) {
332+
group.isExpanded = !group.isExpanded
333+
return
334+
}
335+
336+
if (controlsAdditionalGroups(group)) {
337+
showAllGroups.value = !showAllGroups.value
338+
}
339+
315340
return
316341
}
317342
318-
// Load all versions if not yet loaded
319-
if (!hasLoadedAll.value) {
320-
group.isLoading = true
321-
try {
322-
const allVersions = await loadAllVersions()
323-
processLoadedVersions(allVersions)
324-
// Find the group again after processing (it may have moved)
325-
const updatedGroup = versionGroups.value.find(g => g.id === groupId)
326-
if (updatedGroup) {
343+
group.isLoading = true
344+
try {
345+
const allVersions = await loadAllVersions()
346+
processLoadedVersions(allVersions)
347+
348+
// Find the group again after processing (it may have moved)
349+
const updatedGroup = versionGroups.value.find(g => g.id === groupId)
350+
if (updatedGroup) {
351+
if (hasNestedVersions(updatedGroup)) {
327352
updatedGroup.isExpanded = true
353+
} else if (controlsAdditionalGroups(updatedGroup)) {
354+
showAllGroups.value = true
328355
}
329-
} catch (error) {
330-
// eslint-disable-next-line no-console
331-
console.error('Failed to load versions:', error)
332-
} finally {
333-
group.isLoading = false
334356
}
335-
} else {
336-
group.isExpanded = true
357+
} catch (error) {
358+
// eslint-disable-next-line no-console
359+
console.error('Failed to load versions:', error)
360+
} finally {
361+
group.isLoading = false
337362
}
338363
}
339364
@@ -345,7 +370,7 @@ async function toggleGroup(groupId: string) {
345370
const flatItems = computed(() => {
346371
const items: Array<{ type: 'group' | 'version'; groupId: string; version?: VersionDisplay }> = []
347372
348-
for (const group of versionGroups.value) {
373+
for (const group of visibleVersionGroups.value) {
349374
items.push({ type: 'group', groupId: group.id, version: group.primaryVersion })
350375
351376
if (group.isExpanded && group.versions.length > 1) {
@@ -401,7 +426,7 @@ function handleListboxKeydown(event: KeyboardEvent) {
401426
const item = items[focusedIndex.value]
402427
if (item?.type === 'group') {
403428
const group = versionGroups.value.find(g => g.id === item.groupId)
404-
if (group && !group.isExpanded && group.versions.length > 1) {
429+
if (group && !isGroupOpen(group) && canToggleGroup(group)) {
405430
toggleGroup(item.groupId)
406431
}
407432
}
@@ -414,6 +439,8 @@ function handleListboxKeydown(event: KeyboardEvent) {
414439
const group = versionGroups.value.find(g => g.id === item.groupId)
415440
if (group?.isExpanded) {
416441
group.isExpanded = false
442+
} else if (group && controlsAdditionalGroups(group) && showAllGroups.value) {
443+
showAllGroups.value = false
417444
}
418445
} else if (item?.type === 'version') {
419446
// Jump to parent group
@@ -450,6 +477,31 @@ function navigateToVersion(version: string) {
450477
navigateTo(getVersionUrl(version))
451478
}
452479
480+
function hasNestedVersions(group: VersionGroup): boolean {
481+
return group.versions.length > 1
482+
}
483+
484+
function controlsAdditionalGroups(group: VersionGroup): boolean {
485+
return (
486+
Boolean(group.primaryVersion.tags?.length) &&
487+
!hasNestedVersions(group) &&
488+
hasAdditionalGroups.value
489+
)
490+
}
491+
492+
function isGroupOpen(group: VersionGroup): boolean {
493+
return group.isExpanded || (controlsAdditionalGroups(group) && showAllGroups.value)
494+
}
495+
496+
function canToggleGroup(group: VersionGroup): boolean {
497+
return (
498+
group.isLoading ||
499+
hasNestedVersions(group) ||
500+
!hasLoadedAll.value ||
501+
controlsAdditionalGroups(group)
502+
)
503+
}
504+
453505
// Reset focused index when dropdown opens
454506
watch(isOpen, open => {
455507
if (open) {
@@ -463,6 +515,7 @@ watch(isOpen, open => {
463515
watch(
464516
() => [props.distTags, props.versions, props.currentVersion],
465517
() => {
518+
showAllGroups.value = false
466519
if (hasLoadedAll.value && allVersionsCache.value) {
467520
processLoadedVersions(allVersionsCache.value)
468521
} else {
@@ -518,7 +571,7 @@ watch(
518571
@keydown="handleListboxKeydown"
519572
>
520573
<!-- Version groups -->
521-
<div v-for="group in versionGroups" :key="group.id">
574+
<div v-for="group in visibleVersionGroups" :key="group.id">
522575
<!-- Group header (primary version) -->
523576
<div
524577
:id="`version-${group.primaryVersion.version}`"
@@ -539,11 +592,11 @@ watch(
539592
>
540593
<!-- Expand button -->
541594
<button
542-
v-if="group.versions.length > 1 || !hasLoadedAll"
595+
v-if="canToggleGroup(group)"
543596
type="button"
544597
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors shrink-0"
545-
:aria-expanded="group.isExpanded"
546-
:aria-label="group.isExpanded ? $t('common.collapse') : $t('common.expand')"
598+
:aria-expanded="isGroupOpen(group)"
599+
:aria-label="isGroupOpen(group) ? $t('common.collapse') : $t('common.expand')"
547600
@click.stop="toggleGroup(group.id)"
548601
>
549602
<span
@@ -554,11 +607,11 @@ watch(
554607
<span
555608
v-else
556609
class="w-3 h-3 transition-transform duration-200 rtl-flip"
557-
:class="group.isExpanded ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'"
610+
:class="isGroupOpen(group) ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'"
558611
aria-hidden="true"
559612
/>
560613
</button>
561-
<span v-else class="w-4" />
614+
<span v-else class="w-4 h-4 shrink-0" />
562615

563616
<!-- Version link -->
564617
<NuxtLink
@@ -626,7 +679,7 @@ watch(
626679
<!-- Link to package page for full version list -->
627680
<div class="border-t border-border mt-1 pt-1 px-3 py-2">
628681
<NuxtLink
629-
:to="packageRoute(packageName)"
682+
:to="packageVersionsRoute(packageName)"
630683
class="text-xs text-fg-subtle hover:text-fg transition-[color] focus-visible:outline-none focus-visible:text-fg"
631684
@click="isOpen = false"
632685
>

app/utils/router.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export function packageRoute(
2929
}
3030
}
3131

32+
/** Full version history page (`/package/.../versions`) */
33+
export function packageVersionsRoute(packageName: string): RouteLocationRaw {
34+
const [org, name = ''] = packageName.startsWith('@') ? packageName.split('/') : ['', packageName]
35+
return { name: 'package-versions', params: { org, name } }
36+
}
37+
3238
export function diffRoute(
3339
packageName: string,
3440
fromVersion: string,

test/nuxt/components/Package/Versions.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
22
import { mountSuspended } from '@nuxt/test-utils/runtime'
33
import type { DOMWrapper } from '@vue/test-utils'
44
import PackageVersions from '~/components/Package/Versions.vue'
5+
import { packageVersionsRoute } from '~/utils/router'
56

67
// Mock the fetchAllPackageVersions function
78
const mockFetchAllPackageVersions = vi.fn()
@@ -39,6 +40,12 @@ function isVersionLink(a: DOMWrapper<Element>): boolean {
3940
)
4041
}
4142

43+
function getRouter(
44+
component: Awaited<ReturnType<typeof mountSuspended>>,
45+
): Pick<typeof component.vm.$router, 'resolve'> {
46+
return component.vm.$router
47+
}
48+
4249
describe('PackageVersions', () => {
4350
beforeEach(() => {
4451
mockFetchAllPackageVersions.mockReset()
@@ -109,6 +116,42 @@ describe('PackageVersions', () => {
109116
expect(versionLinks[0]?.text()).toBe('1.0.0')
110117
})
111118

119+
it('view-all-versions link uses packageVersionsRoute for unscoped packages', async () => {
120+
const component = await mountSuspended(PackageVersions, {
121+
props: {
122+
packageName: 'test-package',
123+
versions: {
124+
'1.0.0': createVersion('1.0.0'),
125+
},
126+
distTags: { latest: '1.0.0' },
127+
time: { '1.0.0': '2024-01-15T12:00:00.000Z' },
128+
},
129+
})
130+
131+
const router = getRouter(component)
132+
const expectedHref = router.resolve(packageVersionsRoute('test-package')).href
133+
const viewAll = component.find('[data-testid="view-all-versions-link"]')
134+
expect(viewAll.attributes('href')).toBe(expectedHref)
135+
})
136+
137+
it('view-all-versions link uses packageVersionsRoute for scoped packages', async () => {
138+
const component = await mountSuspended(PackageVersions, {
139+
props: {
140+
packageName: '@scope/test-package',
141+
versions: {
142+
'1.0.0': createVersion('1.0.0'),
143+
},
144+
distTags: { latest: '1.0.0' },
145+
time: { '1.0.0': '2024-01-15T12:00:00.000Z' },
146+
},
147+
})
148+
149+
const router = getRouter(component)
150+
const expectedHref = router.resolve(packageVersionsRoute('@scope/test-package')).href
151+
const viewAll = component.find('[data-testid="view-all-versions-link"]')
152+
expect(viewAll.attributes('href')).toBe(expectedHref)
153+
})
154+
112155
it('highlights the current version row when selectedVersion prop matches', async () => {
113156
const component = await mountSuspended(PackageVersions, {
114157
props: {

0 commit comments

Comments
 (0)