Skip to content

Commit 188e887

Browse files
authored
fix(ui): hide awkward empty state for weekly downloads for new packages (#1054)
1 parent 145cc8d commit 188e887

File tree

2 files changed

+122
-40
lines changed

2 files changed

+122
-40
lines changed

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,6 @@ const chartModal = useModal('chart-modal')
1212
const hasChartModalTransitioned = shallowRef(false)
1313
const isChartModalOpen = shallowRef(false)
1414
15-
async function openChartModal() {
16-
isChartModalOpen.value = true
17-
hasChartModalTransitioned.value = false
18-
// ensure the component renders before opening the dialog
19-
await nextTick()
20-
await nextTick()
21-
chartModal.open()
22-
}
23-
2415
function handleModalClose() {
2516
isChartModalOpen.value = false
2617
hasChartModalTransitioned.value = false
@@ -96,10 +87,24 @@ const pulseColor = computed(() => {
9687
})
9788
9889
const weeklyDownloads = shallowRef<WeeklyDownloadPoint[]>([])
90+
const isLoadingWeeklyDownloads = shallowRef(true)
91+
const hasWeeklyDownloads = computed(() => weeklyDownloads.value.length > 0)
92+
93+
async function openChartModal() {
94+
if (!hasWeeklyDownloads.value) return
95+
96+
isChartModalOpen.value = true
97+
hasChartModalTransitioned.value = false
98+
// ensure the component renders before opening the dialog
99+
await nextTick()
100+
await nextTick()
101+
chartModal.open()
102+
}
99103
100104
async function loadWeeklyDownloads() {
101105
if (!import.meta.client) return
102106
107+
isLoadingWeeklyDownloads.value = true
103108
try {
104109
const result = await fetchPackageDownloadEvolution(
105110
() => props.packageName,
@@ -109,6 +114,8 @@ async function loadWeeklyDownloads() {
109114
weeklyDownloads.value = (result as WeeklyDownloadPoint[]) ?? []
110115
} catch {
111116
weeklyDownloads.value = []
117+
} finally {
118+
isLoadingWeeklyDownloads.value = false
112119
}
113120
}
114121
@@ -212,6 +219,7 @@ const config = computed(() => {
212219
<CollapsibleSection id="downloads" :title="$t('package.downloads.title')">
213220
<template #actions>
214221
<ButtonBase
222+
v-if="hasWeeklyDownloads"
215223
type="button"
216224
@click="openChartModal"
217225
class="text-fg-subtle hover:text-fg transition-colors duration-200 inline-flex items-center justify-center min-w-6 min-h-6 -m-1 p-1 focus-visible:outline-accent/70 rounded"
@@ -223,44 +231,53 @@ const config = computed(() => {
223231
</template>
224232

225233
<div class="w-full overflow-hidden">
226-
<ClientOnly>
227-
<VueUiSparkline class="w-full max-w-xs" :dataset :config>
228-
<template #skeleton>
229-
<!-- This empty div overrides the default built-in scanning animation on load -->
230-
<div />
231-
</template>
232-
</VueUiSparkline>
233-
<template #fallback>
234-
<!-- Skeleton matching sparkline layout: title row + chart with data label -->
235-
<div class="min-h-[75.195px]">
236-
<!-- Title row: date range (24px height) -->
237-
<div class="h-6 flex items-center ps-3">
238-
<SkeletonInline class="h-3 w-36" />
239-
</div>
240-
<!-- Chart area: data label left, sparkline right -->
241-
<div class="aspect-[500/80] flex items-center">
242-
<!-- Data label (covers ~42% width) -->
243-
<div class="w-[42%] flex items-center ps-0.5">
244-
<SkeletonInline class="h-7 w-24" />
234+
<template v-if="isLoadingWeeklyDownloads || hasWeeklyDownloads">
235+
<ClientOnly>
236+
<VueUiSparkline class="w-full max-w-xs" :dataset :config>
237+
<template #skeleton>
238+
<!-- This empty div overrides the default built-in scanning animation on load -->
239+
<div />
240+
</template>
241+
</VueUiSparkline>
242+
<template #fallback>
243+
<!-- Skeleton matching sparkline layout: title row + chart with data label -->
244+
<div class="min-h-[75.195px]">
245+
<!-- Title row: date range (24px height) -->
246+
<div class="h-6 flex items-center ps-3">
247+
<SkeletonInline class="h-3 w-36" />
245248
</div>
246-
<!-- Sparkline area (~58% width) -->
247-
<div class="flex-1 flex items-end gap-0.5 h-4/5 pe-3">
248-
<SkeletonInline
249-
v-for="i in 16"
250-
:key="i"
251-
class="flex-1 rounded-sm"
252-
:style="{ height: `${25 + ((i * 7) % 50)}%` }"
253-
/>
249+
<!-- Chart area: data label left, sparkline right -->
250+
<div class="aspect-[500/80] flex items-center">
251+
<!-- Data label (covers ~42% width) -->
252+
<div class="w-[42%] flex items-center ps-0.5">
253+
<SkeletonInline class="h-7 w-24" />
254+
</div>
255+
<!-- Sparkline area (~58% width) -->
256+
<div class="flex-1 flex items-end gap-0.5 h-4/5 pe-3">
257+
<SkeletonInline
258+
v-for="i in 16"
259+
:key="i"
260+
class="flex-1 rounded-sm"
261+
:style="{ height: `${25 + ((i * 7) % 50)}%` }"
262+
/>
263+
</div>
254264
</div>
255265
</div>
256-
</div>
257-
</template>
258-
</ClientOnly>
266+
</template>
267+
</ClientOnly>
268+
</template>
269+
<p v-else class="py-2 text-sm font-mono text-fg-subtle">
270+
{{ $t('package.downloads.no_data') }}
271+
</p>
259272
</div>
260273
</CollapsibleSection>
261274
</div>
262275

263-
<PackageChartModal @close="handleModalClose" @transitioned="handleModalTransitioned">
276+
<PackageChartModal
277+
v-if="isChartModalOpen && hasWeeklyDownloads"
278+
@close="handleModalClose"
279+
@transitioned="handleModalTransitioned"
280+
>
264281
<!-- The Chart is mounted after the dialog has transitioned -->
265282
<!-- This avoids flaky behavior that hides the chart's minimap half of the time -->
266283
<Transition name="opacity" mode="out-in">
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { mockNuxtImport, mountSuspended } from '@nuxt/test-utils/runtime'
2+
import { defineComponent, h } from 'vue'
3+
import { describe, expect, it, vi } from 'vitest'
4+
5+
const { mockFetchPackageDownloadEvolution } = vi.hoisted(() => ({
6+
mockFetchPackageDownloadEvolution: vi.fn(),
7+
}))
8+
9+
mockNuxtImport('useCharts', () => {
10+
return () => ({
11+
fetchPackageDownloadEvolution: (...args: unknown[]) =>
12+
mockFetchPackageDownloadEvolution(...args),
13+
})
14+
})
15+
16+
vi.mock('vue-data-ui/vue-ui-sparkline', () => ({
17+
VueUiSparkline: defineComponent({
18+
name: 'VueUiSparkline',
19+
inheritAttrs: false,
20+
setup(_, { attrs, slots }) {
21+
return () => h('div', { class: attrs.class }, slots.default?.() ?? [])
22+
},
23+
}),
24+
}))
25+
26+
import PackageWeeklyDownloadStats from '~/components/Package/WeeklyDownloadStats.vue'
27+
28+
describe('PackageWeeklyDownloadStats', () => {
29+
const baseProps = {
30+
packageName: 'test-package',
31+
createdIso: '2026-02-05T00:00:00.000Z',
32+
}
33+
34+
it('hides the section when weekly downloads are empty', async () => {
35+
mockFetchPackageDownloadEvolution.mockReset()
36+
mockFetchPackageDownloadEvolution.mockResolvedValue([])
37+
38+
const component = await mountSuspended(PackageWeeklyDownloadStats, {
39+
props: baseProps,
40+
})
41+
42+
expect(component.text()).toContain('Weekly Downloads')
43+
expect(component.text()).toContain('No download data available')
44+
})
45+
46+
it('shows the section when weekly downloads exist', async () => {
47+
mockFetchPackageDownloadEvolution.mockReset()
48+
mockFetchPackageDownloadEvolution.mockResolvedValue([
49+
{
50+
weekStart: '2026-01-01',
51+
weekEnd: '2026-01-07',
52+
timestampStart: 1767225600000,
53+
timestampEnd: 1767744000000,
54+
downloads: 42,
55+
},
56+
])
57+
58+
const component = await mountSuspended(PackageWeeklyDownloadStats, {
59+
props: baseProps,
60+
})
61+
62+
expect(component.text()).toContain('Weekly Downloads')
63+
expect(component.text()).not.toContain('No download data available')
64+
})
65+
})

0 commit comments

Comments
 (0)