Skip to content

Commit 5530873

Browse files
authored
fix(ui): load all versions when semver range filter is entered (#1912)
1 parent 71116e8 commit 5530873

File tree

2 files changed

+133
-9
lines changed

2 files changed

+133
-9
lines changed

app/components/Package/Versions.vue

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,22 @@ const effectiveCurrentVersion = computed(
115115
116116
// Semver range filter
117117
const semverFilter = ref('')
118+
119+
// Load all versions when a valid semver filter is entered
120+
watch(semverFilter, async newFilter => {
121+
const trimmed = newFilter.trim()
122+
if (trimmed === '' || hasLoadedAll.value) return
123+
if (!validRange(trimmed)) return
124+
125+
try {
126+
const allVersions = await loadAllVersions()
127+
processLoadedVersions(allVersions)
128+
// Auto-expand "Other versions" so filtered results are visible
129+
otherVersionsExpanded.value = true
130+
} catch {
131+
// Silently fail — user can still use the filter with already-known versions
132+
}
133+
})
118134
// Collect all known versions: initial props + dynamically loaded ones
119135
const allKnownVersions = computed(() => {
120136
const versions = new Set(Object.keys(props.versions))
@@ -196,8 +212,10 @@ const visibleTagRows = computed(() => {
196212
? allTagRows.value
197213
: allTagRows.value.filter(row => !row.primaryVersion.deprecated)
198214
const rows = isFilterActive.value
199-
? rowsMaybeFilteredForDeprecation.filter(row =>
200-
filteredVersionSet.value.has(row.primaryVersion.version),
215+
? rowsMaybeFilteredForDeprecation.filter(
216+
row =>
217+
filteredVersionSet.value.has(row.primaryVersion.version) ||
218+
getTagVersions(row.tag).some(v => filteredVersionSet.value.has(v.version)),
201219
)
202220
: rowsMaybeFilteredForDeprecation
203221
const first = rows.slice(0, MAX_VISIBLE_TAGS)
@@ -215,7 +233,11 @@ const visibleTagRows = computed(() => {
215233
const hiddenTagRows = computed(() => {
216234
const hiddenRows = allTagRows.value.filter(row => !visibleTagRows.value.includes(row))
217235
const rows = isFilterActive.value
218-
? hiddenRows.filter(row => filteredVersionSet.value.has(row.primaryVersion.version))
236+
? hiddenRows.filter(
237+
row =>
238+
filteredVersionSet.value.has(row.primaryVersion.version) ||
239+
getTagVersions(row.tag).some(v => filteredVersionSet.value.has(v.version)),
240+
)
219241
: hiddenRows
220242
return rows
221243
})
@@ -435,6 +457,14 @@ function getExpandedTagVersions(tag: string, primaryVersion: string): VersionDis
435457
return versions.filter(v => filteredVersionSet.value.has(v.version))
436458
}
437459
460+
// Check if a tag row's children are expanded (manually or via active filter)
461+
function isTagExpanded(tag: string, primaryVersion: string): boolean {
462+
return (
463+
expandedTags.value.has(tag) ||
464+
(isFilterActive.value && getExpandedTagVersions(tag, primaryVersion).length > 0)
465+
)
466+
}
467+
438468
function findClaimingTag(version: string): string | null {
439469
const versionChannel = getPrereleaseChannel(version)
440470
@@ -583,7 +613,7 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
583613
v-if="getTagVersions(row.tag).length > 1 || !hasLoadedAll"
584614
type="button"
585615
class="size-5 -me-1 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors rounded-sm relative z-10"
586-
:aria-expanded="expandedTags.has(row.tag)"
616+
:aria-expanded="isTagExpanded(row.tag, row.primaryVersion.version)"
587617
:aria-label="
588618
expandedTags.has(row.tag)
589619
? $t('package.versions.collapse', { tag: row.tag })
@@ -602,7 +632,9 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
602632
v-else
603633
class="size-3 transition-transform duration-200 rtl-flip"
604634
:class="
605-
expandedTags.has(row.tag) ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'
635+
isTagExpanded(row.tag, row.primaryVersion.version)
636+
? 'i-lucide:chevron-down'
637+
: 'i-lucide:chevron-right'
606638
"
607639
aria-hidden="true"
608640
/>
@@ -671,10 +703,7 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
671703

672704
<!-- Expanded versions -->
673705
<div
674-
v-if="
675-
expandedTags.has(row.tag) &&
676-
getExpandedTagVersions(row.tag, row.primaryVersion.version).length
677-
"
706+
v-if="isTagExpanded(row.tag, row.primaryVersion.version)"
678707
class="ms-4 ps-2 border-is border-border space-y-0.5 pe-2"
679708
>
680709
<div

test/nuxt/components/PackageVersions.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,101 @@ describe('PackageVersions', () => {
11271127
})
11281128
})
11291129

1130+
it('loads all versions when a valid semver filter is entered', async () => {
1131+
mockFetchAllPackageVersions.mockResolvedValue([
1132+
{ version: '3.5.0', time: '2024-04-01T00:00:00.000Z', hasProvenance: false },
1133+
{ version: '3.4.0', time: '2024-03-01T00:00:00.000Z', hasProvenance: false },
1134+
{ version: '3.3.0', time: '2024-02-01T00:00:00.000Z', hasProvenance: false },
1135+
{ version: '2.0.0', time: '2024-01-15T00:00:00.000Z', hasProvenance: false },
1136+
{ version: '1.0.0', time: '2024-01-01T00:00:00.000Z', hasProvenance: false },
1137+
])
1138+
1139+
// Only provide latest in props (simulating initial SSR payload)
1140+
const component = await mountSuspended(PackageVersions, {
1141+
props: {
1142+
packageName: 'test-package',
1143+
versions: {
1144+
'3.5.0': createVersion('3.5.0'),
1145+
},
1146+
distTags: { latest: '3.5.0' },
1147+
time: { '3.5.0': '2024-04-01T00:00:00.000Z' },
1148+
},
1149+
})
1150+
1151+
// Filter for a version in the SAME major group as latest (claimed by the tag)
1152+
const input = component.find('input[type="text"]')
1153+
await input.setValue('~3.4.0')
1154+
1155+
// Should trigger loading all versions
1156+
await vi.waitFor(() => {
1157+
expect(mockFetchAllPackageVersions).toHaveBeenCalledWith('test-package')
1158+
})
1159+
1160+
// After loading, 3.4.0 should appear as an auto-expanded child of the latest tag
1161+
await vi.waitFor(() => {
1162+
const versionLinks = component
1163+
.findAll('a')
1164+
.filter(
1165+
a => !a.attributes('href')?.startsWith('#') && a.attributes('target') !== '_blank',
1166+
)
1167+
const versions = versionLinks.map(l => l.text())
1168+
expect(versions).toContain('3.4.0')
1169+
})
1170+
})
1171+
1172+
it('does not load all versions for invalid semver filter', async () => {
1173+
const component = await mountSuspended(PackageVersions, {
1174+
props: {
1175+
packageName: 'test-package',
1176+
versions: {
1177+
'3.0.0': createVersion('3.0.0'),
1178+
},
1179+
distTags: { latest: '3.0.0' },
1180+
time: { '3.0.0': '2024-04-01T00:00:00.000Z' },
1181+
},
1182+
})
1183+
1184+
const input = component.find('input[type="text"]')
1185+
await input.setValue('not-a-range!!!')
1186+
1187+
// Should NOT trigger loading
1188+
expect(mockFetchAllPackageVersions).not.toHaveBeenCalled()
1189+
})
1190+
1191+
it('only fetches versions once across multiple filter changes', async () => {
1192+
mockFetchAllPackageVersions.mockResolvedValue([
1193+
{ version: '3.0.0', time: '2024-04-01T00:00:00.000Z', hasProvenance: false },
1194+
{ version: '2.0.0', time: '2024-02-01T00:00:00.000Z', hasProvenance: false },
1195+
{ version: '1.0.0', time: '2024-01-01T00:00:00.000Z', hasProvenance: false },
1196+
])
1197+
1198+
const component = await mountSuspended(PackageVersions, {
1199+
props: {
1200+
packageName: 'test-package',
1201+
versions: {
1202+
'3.0.0': createVersion('3.0.0'),
1203+
},
1204+
distTags: { latest: '3.0.0' },
1205+
time: { '3.0.0': '2024-04-01T00:00:00.000Z' },
1206+
},
1207+
})
1208+
1209+
const input = component.find('input[type="text"]')
1210+
1211+
// First valid filter triggers fetch
1212+
await input.setValue('^1.0.0')
1213+
await vi.waitFor(() => {
1214+
expect(mockFetchAllPackageVersions).toHaveBeenCalledTimes(1)
1215+
})
1216+
1217+
// Subsequent filter changes should NOT fetch again
1218+
await input.setValue('^2.0.0')
1219+
await input.setValue('~3.0.0')
1220+
await input.setValue('>=1.0.0')
1221+
1222+
expect(mockFetchAllPackageVersions).toHaveBeenCalledTimes(1)
1223+
})
1224+
11301225
it('filters other major version groups', async () => {
11311226
mockFetchAllPackageVersions.mockResolvedValue([
11321227
{ version: '3.0.0', time: '2024-04-01T00:00:00.000Z', hasProvenance: false },

0 commit comments

Comments
 (0)