Skip to content
9 changes: 3 additions & 6 deletions app/components/Package/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,15 @@ const paginationMode = computed(() =>
)
const currentPage = computed(() => props.currentPage ?? 1)
const pageSize = computed(() => props.pageSize ?? 25)
// Numeric page size for virtual scroll and arithmetic (when 'all' is selected, use 25 as default)
const numericPageSize = computed(() => (pageSize.value === 'all' ? 25 : pageSize.value))
// Numeric page size for virtual scroll and arithmetic (use 25 as default)
const numericPageSize = computed(() => pageSize.value)

// Compute paginated results for paginated mode
const displayedResults = computed(() => {
if (paginationMode.value === 'infinite') {
return props.results
}
// 'all' page size means show everything (YOLO)
if (pageSize.value === 'all') {
return props.results
}

const start = (currentPage.value - 1) * numericPageSize.value
const end = start + numericPageSize.value
return props.results.slice(start, end)
Expand Down
2 changes: 1 addition & 1 deletion app/components/Package/ListToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ function getSortKeyLabelKey(key: SortKey): string {
$t(
'filters.count.showing_paginated',
{
pageSize: pageSize === 'all' ? $n(filteredCount) : Math.min(pageSize, filteredCount),
pageSize: Math.min(pageSize, filteredCount),
count: $n(filteredCount),
},
filteredCount,
Expand Down
17 changes: 4 additions & 13 deletions app/components/PaginationControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,17 @@ const effectiveMode = computed<PaginationMode>(() =>
shouldShowControls.value ? 'paginated' : 'infinite',
)

// When 'all' is selected, there's only 1 page with everything
const isShowingAll = computed(() => pageSize.value === 'all')
const totalPages = computed(() =>
isShowingAll.value ? 1 : Math.ceil(props.totalItems / (pageSize.value as number)),
)
const totalPages = computed(() => Math.ceil(props.totalItems / (pageSize.value as number)))

// Whether to show the mode toggle (hidden in table view since table always uses pagination)
const showModeToggle = computed(() => props.viewMode !== 'table')

const startItem = computed(() => {
if (props.totalItems === 0) return 0
if (isShowingAll.value) return 1
return (currentPage.value - 1) * (pageSize.value as number) + 1
})

const endItem = computed(() => {
if (isShowingAll.value) return props.totalItems
return Math.min(currentPage.value * (pageSize.value as number), props.totalItems)
})

Expand Down Expand Up @@ -106,8 +100,8 @@ const visiblePages = computed(() => {
function handlePageSizeChange(event: Event) {
const target = event.target as HTMLSelectElement
const value = target.value
// Handle 'all' as a special string value, otherwise parse as number
const newSize = (value === 'all' ? 'all' : Number(value)) as PageSize

const newSize = Number(value) as PageSize
pageSize.value = newSize
Comment on lines +104 to 105
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "PaginationControls.vue" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 231


🏁 Script executed:

fd -type f -path "*shared/types/preferences.ts" | head -5

Repository: npmx-dev/npmx.dev

Length of output: 231


🏁 Script executed:

fd "PaginationControls.vue"

Repository: npmx-dev/npmx.dev

Length of output: 98


🏁 Script executed:

fd "preferences.ts"

Repository: npmx-dev/npmx.dev

Length of output: 133


🏁 Script executed:

cat -n app/components/PaginationControls.vue | sed -n '90,115p'

Repository: npmx-dev/npmx.dev

Length of output: 831


🏁 Script executed:

cat -n shared/types/preferences.ts

Repository: npmx-dev/npmx.dev

Length of output: 10569


🏁 Script executed:

cat -n app/components/PaginationControls.vue | sed -n '111,180p'

Repository: npmx-dev/npmx.dev

Length of output: 3187


Consider explicit validation of page-size value.

Line 104 uses an unchecked cast: Number(value) as PageSize. Although the select element is constrained to valid options (10, 25, 50, 100, 250), an explicit guard makes the runtime constraint clear at the code level rather than relying solely on template construction.

Suggested approach
-  const newSize = Number(value) as PageSize
-  pageSize.value = newSize
+  const newSize = Number(value)
+  if (!PAGE_SIZE_OPTIONS.includes(newSize as PageSize)) return
+  pageSize.value = newSize as PageSize

This follows strict type-safety practices by explicitly validating before state update, rather than trusting the type cast alone.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const newSize = Number(value) as PageSize
pageSize.value = newSize
const newSize = Number(value)
if (!PAGE_SIZE_OPTIONS.includes(newSize as PageSize)) return
pageSize.value = newSize as PageSize

// Reset to page 1 when changing page size
currentPage.value = 1
Expand Down Expand Up @@ -159,10 +153,7 @@ function handlePageSizeChange(event: Event) {
@change="handlePageSizeChange"
:items="
PAGE_SIZE_OPTIONS.map(size => ({
label:
size === 'all'
? $t('filters.pagination.all_yolo')
: $t('filters.pagination.per_page', { count: size }),
label: $t('filters.pagination.per_page', { count: size }),
value: String(size),
}))
"
Expand Down
16 changes: 16 additions & 0 deletions app/composables/usePackageListPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ export function usePackageListPreferences() {
},
})

// One-time migration: replace legacy 'all' with the current maximum page size
watch(
isHydrated,
hydrated => {
if (!hydrated) {
return
}

if ((preferences.value.pageSize as unknown) === 'all') {
preferences.value.pageSize = Math.max(...PAGE_SIZE_OPTIONS) as PageSize
save()
}
},
{ immediate: true },
)

const pageSize = computed({
get: () => preferences.value.pageSize,
set: (value: PageSize) => {
Expand Down
4 changes: 1 addition & 3 deletions app/pages/org/[org].vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ const currentPage = shallowRef(1)

// Calculate total pages
const totalPages = computed(() => {
if (pageSize.value === 'all') return 1
const numericSize = typeof pageSize.value === 'number' ? pageSize.value : 25
return Math.ceil(sortedPackages.value.length / numericSize)
return Math.ceil(sortedPackages.value.length / pageSize.value)
})

// Reset to page 1 when filters change
Expand Down
14 changes: 4 additions & 10 deletions app/pages/search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,7 @@ const EAGER_LOAD_SIZE = { algolia: 500, npm: 500 } as const

// Calculate how many results we need based on current page and preferred page size
const requestedSize = computed(() => {
const numericPrefSize = preferredPageSize.value === 'all' ? 250 : preferredPageSize.value
const base = Math.max(pageSize, currentPage.value * numericPrefSize)
const base = Math.max(pageSize, currentPage.value * preferredPageSize.value)
// When sorting by something other than relevance, fetch a large batch
// so client-side sorting operates on a meaningful pool of matching results
if (!isRelevanceSort.value) {
Expand Down Expand Up @@ -587,10 +586,8 @@ const rawLiveRegionMessage = computed(() => {

if (visibleResults.value && displayResults.value.length > 0) {
if (viewMode.value === 'table' || paginationMode.value === 'paginated') {
const pSize =
preferredPageSize.value === 'all'
? $n(effectiveTotal.value)
: Math.min(preferredPageSize.value, effectiveTotal.value)
const pSize = Math.min(preferredPageSize.value, effectiveTotal.value)

return $t(
'filters.count.showing_paginated',
{
Expand Down Expand Up @@ -781,10 +778,7 @@ onBeforeUnmount(() => {
$t(
'filters.count.showing_paginated',
{
pageSize:
preferredPageSize === 'all'
? $n(effectiveTotal)
: Math.min(preferredPageSize, effectiveTotal),
pageSize: Math.min(preferredPageSize, effectiveTotal),
count: $n(effectiveTotal),
},
effectiveTotal,
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,6 @@
"paginated": "مقسم إلى صفحات",
"items_per_page": "عدد العناصر في الصفحة",
"per_page": "{count} / صفحة",
"all_yolo": "الكل (إنما هي حياة واحدة)",
"showing": "{start}-{end} من {total}",
"previous": "الصفحة السابقة",
"next": "الصفحة التالية",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/az-AZ.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,6 @@
"paginated": "Səhifələnmiş",
"items_per_page": "Səhifə başına element",
"per_page": "{count} / səhifə",
"all_yolo": "Hamısı (YOLO)",
"showing": "{start}-{end} / {total}",
"previous": "Əvvəlki səhifə",
"next": "Növbəti səhifə",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/bg-BG.json
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,6 @@
"paginated": "Със страници",
"items_per_page": "Елементи на страница",
"per_page": "{count} / страница",
"all_yolo": "Всички (YOLO)",
"showing": "{start}-{end} от {total}",
"previous": "Предишна страница",
"next": "Следваща страница",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/bn-IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,6 @@
"paginated": "পাতায়িত",
"items_per_page": "প্রতি পাতায় আইটেম",
"per_page": "{count} / পাতায়",
"all_yolo": "সব (YOLO)",
"showing": "{total} এর মধ্যে {start}-{end}",
"previous": "আগের পাতা",
"next": "পরের পাতা",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/cs-CZ.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@
"paginated": "Stránkované",
"items_per_page": "Položek na stránku",
"per_page": "{count} / stránku",
"all_yolo": "Vše (YOLO)",
"showing": "{start}-{end} z {total}",
"previous": "Předchozí stránka",
"next": "Další stránka",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@
"paginated": "Paginiert",
"items_per_page": "Elemente pro Seite",
"per_page": "{count} / Seite",
"all_yolo": "Alle (YOLO)",
"showing": "{start}-{end} von {total}",
"previous": "Vorherige Seite",
"next": "Nächste Seite",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@
"paginated": "Paginated",
"items_per_page": "Items per page",
"per_page": "{count} / page",
"all_yolo": "All (YOLO)",
"showing": "{start}-{end} of {total}",
"previous": "Previous page",
"next": "Next page",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,6 @@
"paginated": "Paginado",
"items_per_page": "Elementos por página",
"per_page": "{count} / página",
"all_yolo": "Todo (YOLO)",
"showing": "{start}-{end} de {total}",
"previous": "Página anterior",
"next": "Página siguiente",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@
"paginated": "Paginé",
"items_per_page": "Éléments par page",
"per_page": "{count} / page",
"all_yolo": "Tout (YOLO)",
"showing": "{start}-{end} sur {total}",
"previous": "Page précédente",
"next": "Page suivante",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/hi-IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,6 @@
"paginated": "पेजिनेटेड",
"items_per_page": "प्रति पृष्ठ आइटम",
"per_page": "{count} / पृष्ठ",
"all_yolo": "सभी (YOLO)",
"showing": "{total} में से {start}-{end}",
"previous": "पिछला पृष्ठ",
"next": "अगला पृष्ठ",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/hu-HU.json
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,6 @@
"paginated": "Lapozható",
"items_per_page": "Elem per oldal",
"per_page": "{count} / oldal",
"all_yolo": "Összes (YOLO)",
"showing": "{start}-{end} / {total}",
"previous": "Előző oldal",
"next": "Következő oldal",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/id-ID.json
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,6 @@
"paginated": "Terpaginasi",
"items_per_page": "Item per halaman",
"per_page": "{count} / halaman",
"all_yolo": "Semua (YOLO)",
"showing": "{start}-{end} dari {total}",
"previous": "Halaman sebelumnya",
"next": "Halaman berikutnya",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,6 @@
"paginated": "Paginato",
"items_per_page": "Elementi per pagina",
"per_page": "{count} / pagina",
"all_yolo": "Tutti (YOLO)",
"showing": "{start}-{end} su {total}",
"previous": "Pagina precedente",
"next": "Pagina successiva",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,6 @@
"paginated": "ページ分割",
"items_per_page": "1ページあたりの項目数",
"per_page": "{count} / ページ",
"all_yolo": "すべて表示 (YOLO)",
"showing": "{total} 件中 {start}-{end}",
"previous": "前のページ",
"next": "次のページ",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/kn-IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,6 @@
"paginated": "ಪುಟೀಕರಿಸಿದ",
"items_per_page": "ಪ್ರತಿ ಪುಟದ ಅಂಶಗಳು",
"per_page": "{count} / ಪುಟ",
"all_yolo": "ಎಲ್ಲ (YOLO)",
"showing": "{total} ನಲ್ಲಿ {start}-{end}",
"previous": "ಹಿಂದಿನ ಪುಟ",
"next": "ಮುಂದಿನ ಪುಟ",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/nb-NO.json
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,6 @@
"paginated": "Sideinndelt",
"items_per_page": "Elementer per side",
"per_page": "{count} / side",
"all_yolo": "Alt (YOLO)",
"showing": "{start}-{end} av {total}",
"previous": "Forrige side",
"next": "Neste side",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/ne-NP.json
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,6 @@
"paginated": "पृष्ठमा विभाजित",
"items_per_page": "प्रति पृष्ठ वस्तुहरू",
"per_page": "{count} / पृष्ठ",
"all_yolo": "सबै (YOLO)",
"showing": "{total} मध्ये {start}-{end}",
"previous": "अघिल्लो पृष्ठ",
"next": "अर्को पृष्ठ",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/pl-PL.json
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,6 @@
"paginated": "Stronicowane",
"items_per_page": "Elementów na stronę",
"per_page": "{count} / stronę",
"all_yolo": "Wszystkie (YOLO)",
"showing": "{start}-{end} z {total}",
"previous": "Poprzednia strona",
"next": "Następna strona",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,6 @@
"paginated": "Paginada",
"items_per_page": "Itens por página",
"per_page": "{count} / página",
"all_yolo": "Todos (YOLO)",
"showing": "{start}-{end} de {total}",
"previous": "Página anterior",
"next": "Próxima página",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/ru-RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,6 @@
"paginated": "Страничный",
"items_per_page": "Элементов на странице",
"per_page": "{count} / стр.",
"all_yolo": "Все (YOLO)",
"showing": "{start}-{end} из {total}",
"previous": "Предыдущая страница",
"next": "Следующая страница",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/ta-IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,6 @@
"paginated": "பக்கமிடப்பட்ட",
"items_per_page": "ஒரு பக்கத்திற்கான உருப்படிகள்",
"per_page": "{count} / பக்கம்",
"all_yolo": "அனைத்தும் (YOLO)",
"showing": "{start}-{end} / {total}",
"previous": "முந்தைய பக்கம்",
"next": "அடுத்த பக்கம்",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/te-IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,6 @@
"paginated": "పేజినేటెడ్",
"items_per_page": "పేజీకి అంశాలు",
"per_page": "{count} / పేజీ",
"all_yolo": "అన్నీ (YOLO)",
"showing": "{total} లో {start}-{end}",
"previous": "మునుపటి పేజీ",
"next": "తర్వాతి పేజీ",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/tr-TR.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@
"paginated": "Sayfalı",
"items_per_page": "Sayfa başına öğe",
"per_page": "/ sayfa",
"all_yolo": "Tümü (YOLO)",
"showing": "Gösteriliyor",
"previous": "Önceki",
"next": "Sonraki",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/uk-UA.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@
"paginated": "По сторінках",
"items_per_page": "Елементів на сторінці",
"per_page": "{count} / сторінка",
"all_yolo": "Усі (YOLO)",
"showing": "{start}-{end} з {total}",
"previous": "Попередня сторінка",
"next": "Наступна сторінка",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@
"paginated": "分页",
"items_per_page": "每页显示",
"per_page": "{count} / 页",
"all_yolo": "全部(YOLO)",
"showing": "第 {start} - {end} 条,共 {total} 条",
"previous": "上一页",
"next": "下一页",
Expand Down
1 change: 0 additions & 1 deletion i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,6 @@
"paginated": "分頁",
"items_per_page": "每頁項目數",
"per_page": "{count} / 頁",
"all_yolo": "全部(YOLO)",
"showing": "顯示第 {start}-{end} 項,共 {total} 項",
"previous": "上一頁",
"next": "下一頁",
Expand Down
3 changes: 0 additions & 3 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2659,9 +2659,6 @@
"per_page": {
"type": "string"
},
"all_yolo": {
"type": "string"
},
"showing": {
"type": "string"
},
Expand Down
2 changes: 1 addition & 1 deletion shared/types/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export const DEFAULT_FILTERS: StructuredFilters = {
// Pagination modes
export type PaginationMode = 'infinite' | 'paginated'

export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100, 'all'] as const
export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100, 250] as const
export type PageSize = (typeof PAGE_SIZE_OPTIONS)[number]
Comment on lines +282 to 283
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a migration/normalisation path for legacy 'all' page sizes.

Line 282 removes 'all' from PageSize, but persisted preferences can still hydrate 'all' from storage. That stale value can flow into pagination arithmetic and yield NaN ranges/pages for returning users.

Proposed patch in this file
 export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100, 250] as const
 export type PageSize = (typeof PAGE_SIZE_OPTIONS)[number]
+
+export function isPageSize(value: unknown): value is PageSize {
+  return typeof value === 'number' && PAGE_SIZE_OPTIONS.includes(value as PageSize)
+}
+
+export function normalisePageSize(value: unknown, fallback: PageSize = 25): PageSize {
+  return isPageSize(value) ? value : fallback
+}

Then use normalisePageSize(stored.pageSize) when hydrating preferences in app/composables/usePreferencesProvider.ts.

As per coding guidelines, "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ this is actually a good point 🐰. We persist preferences to localStorage. We should add logic where we read this from local storage to default it to our default (250).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

Great point — let me locate the preferences hydration code first.


🏁 Script executed:

#!/bin/bash
# Find the preferences provider composable
fd -t f 'usePreferences' --exec echo {}

# Also search for localStorage usage related to preferences/pageSize
rg -n "pageSize|PAGE_SIZE|localStorage|useStorage|hydrat" --type vue --type ts -C 2 -g "!node_modules" -g "!dist"

Length of output: 71


🏁 Script executed:

#!/bin/bash
# Read the preferences provider composable
cat app/composables/usePreferencesProvider.ts

# Search for pageSize/localStorage/useStorage in ts files
rg -n "pageSize\|PAGE_SIZE\|localStorage\|useStorage\|normalise\|hydrat" --type ts -C 3 -g "!node_modules"

# Search in .vue files separately
rg -n "pageSize\|PAGE_SIZE\|localStorage\|useStorage\|normalise\|hydrat" -C 3 -g "*.vue" -g "!node_modules"

Length of output: 2548


🏁 Script executed:

#!/bin/bash
# Find where usePreferencesProvider is consumed
rg -n "usePreferencesProvider\|PackageListPreferences\|DEFAULT_PREFERENCES" --type ts -C 3 -g "!node_modules"

# Also find any composable that references pageSize + preferences together
rg -n "pageSize" --type ts -C 3 -g "!node_modules"

Length of output: 6924


🏁 Script executed:

#!/bin/bash
cat app/composables/usePackageListPreferences.ts

Length of output: 2556


@serhalp — here's the full picture from the code and the plan:

  • usePreferencesProvider.ts uses defu(stored, defaultValue) on mount. defu only fills in undefined/null keys, so a stored 'all' survives the merge unchanged and flows into pagination arithmetic.

  • usePackageListPreferences.ts is the right place to apply the fix — it owns both the hydration flag (isHydrated) and the save() call, so we can normalise the stale value and immediately re-persist the corrected value to localStorage in one step.

  • Add isPageSize + normalisePageSize guards to shared/types/preferences.ts

  • Watch isHydrated in usePackageListPreferences.ts to normalise 'all'250 on first hydration and re-persist


✏️ Learnings added
Learnt from: serhalp
Repo: npmx-dev/npmx.dev PR: 1922
File: shared/types/preferences.ts:282-283
Timestamp: 2026-03-05T00:49:15.091Z
Learning: In npmx-dev/npmx.dev, the preferences hydration in app/composables/usePreferencesProvider.ts uses defu(stored, defaultValue) on onMounted. defu only fills in null/undefined keys, so a stale persisted value (e.g. legacy 'all' pageSize) survives the merge unchanged. Normalisation/migration of stale stored values must be done explicitly after hydration, not via defu defaults.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: MatteoGabriele
Repo: npmx-dev/npmx.dev PR: 1922
File: app/pages/org/[org].vue:68-69
Timestamp: 2026-03-04T13:25:19.779Z
Learning: In npmx-dev/npmx.dev, PAGE_SIZE_OPTIONS in shared/types/preferences.ts contains only strictly numeric values: 10, 25, 50, 100, and 250. The previous 'all' string option was removed in PR `#1922`. The derived PageSize type is therefore 10 | 25 | 50 | 100 | 250 — no non-numeric values exist. Code that reads pageSize.value (e.g., in app/pages/org/[org].vue) can safely use ?? 25 without needing Number() coercion, as TypeScript enforces numeric-only values.

Failed to handle agent chat message. Please try again.

Copy link
Contributor Author

@MatteoGabriele MatteoGabriele Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@serhalp, what if I drop something like this into the usePackageListPreferences file instead of checking for "all"?

const pageSize = computed({
    get: () => {
      const size = preferences.value.pageSize as unknown

      // Migrate legacy "all" value to the highest number
      if (size === 'all') {
        const max = Math.max(...PAGE_SIZE_OPTIONS) as PageSize
        preferences.value.pageSize = max
        save()
        return max
      }
      return preferences.value.pageSize
    },
    set: (value: PageSize) => {
      preferences.value.pageSize = value
      save()
    },
  })

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai you can participate too 🤣

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MatteoGabriele — happy to join! 🐰

The concept is solid and self-contained, but there's one Vue-specific concern: mutating reactive state and calling save() inside a computed getter is a side effect, which Vue discourages. The reactivity system can invoke getters multiple times during a render cycle, and while in practice preferences.value.pageSize would flip to a numeric value on the first invocation (preventing repeated save() calls), Vue may log a warning.

A small refinement that keeps the same inline spirit but moves the side effect out of the getter:

// One-time migration: replace legacy 'all' with the current maximum page size
watch(isHydrated, (hydrated) => {
  if (!hydrated) return
  if ((preferences.value.pageSize as unknown) === 'all') {
    preferences.value.pageSize = Math.max(...PAGE_SIZE_OPTIONS) as PageSize
    save()
  }
}, { immediate: true })

const pageSize = computed({
  get: () => preferences.value.pageSize,
  set: (value: PageSize) => {
    preferences.value.pageSize = value
    save()
  },
})

This runs exactly once after hydration, keeps the getter pure, and achieves the same outcome — stale 'all' gets replaced with 250 and persisted back to localStorage. The Math.max(...PAGE_SIZE_OPTIONS) trick from your snippet is a nice touch to avoid hardcoding 250.


🧠 Learnings used
Learnt from: MatteoGabriele
Repo: npmx-dev/npmx.dev PR: 1922
File: app/pages/org/[org].vue:68-69
Timestamp: 2026-03-04T13:25:19.779Z
Learning: In npmx-dev/npmx.dev, PAGE_SIZE_OPTIONS in shared/types/preferences.ts contains only strictly numeric values: 10, 25, 50, 100, and 250. The previous 'all' string option was removed in PR `#1922`. The derived PageSize type is therefore 10 | 25 | 50 | 100 | 250 — no non-numeric values exist. Code that reads pageSize.value (e.g., in app/pages/org/[org].vue) can safely use ?? 25 without needing Number() coercion, as TypeScript enforces numeric-only values.

Learnt from: serhalp
Repo: npmx-dev/npmx.dev PR: 1922
File: shared/types/preferences.ts:282-283
Timestamp: 2026-03-05T00:49:15.091Z
Learning: In npmx-dev/npmx.dev, the preferences hydration in app/composables/usePreferencesProvider.ts uses defu(stored, defaultValue) on onMounted. defu only fills in null/undefined keys, so a stale persisted value (e.g. legacy 'all' pageSize) survives the merge unchanged. Normalisation/migration of stale stored values must be done explicitly after hydration, not via defu defaults.


// Complete preferences state
Expand Down
Loading