From 5f5d67137e1afbfbb4dd9e6b1489dacfbbc472f1 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Fri, 8 May 2026 13:30:41 +0300 Subject: [PATCH 01/14] feat: add dropdown for selecting page size options in list view --- .../13-standardPagesTuning.md | 29 ++++++++++++++ .../spa/src/components/ResourceListTable.vue | 35 +++++++++++++++++ adminforth/spa/src/views/ListView.vue | 39 +++++++++++++++++-- adminforth/types/Common.ts | 1 + 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md b/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md index 169fcb967..441ee8177 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md @@ -319,6 +319,35 @@ export default { ] ``` +### List Page Size Options +You can define available pagination sizes using options.listPageSizeOptions. This allows users to choose how many records they want to see per page in the list view. +```typescript title="./resources/apartments.ts" +export default { + resourceId: 'aparts', + options: { + ... + listPageSize: 10, + //diff-add + listPageSizeOptions: [10, 20, 50], + } + } + ] +``` + +#### How it works +- listPageSize defines the default number of records per page when the list is opened. +- listPageSizeOptions defines the available page size options shown to the user. + +For example: listPageSizeOptions: [10, 20, 50] will allow switching between 10 / 20 / 50 records per page. + +#### UI behavior +Page size switching is implemented via a select dropdown (select input) in the table pagination controls. +- User opens the select +- Chooses a value (e.g. 20) +- Table reloads with the new page size +> ☝️Notes +If listPageSizeOptions is not provided, a default set of page sizes will be used. The selected value updates the table immediately and triggers a data refetch. This option works together with listPageSize, which defines both the default value and the available options at the same time. + ### Virtual scroll Set `options.listVirtualScrollEnabled` to true to enable virtual scrolling in the table. The default value is false. Enable this option if you need to display a large number of records on a single page. diff --git a/adminforth/spa/src/components/ResourceListTable.vue b/adminforth/spa/src/components/ResourceListTable.vue index df90cafc2..d3b3b91a3 100644 --- a/adminforth/spa/src/components/ResourceListTable.vue +++ b/adminforth/spa/src/components/ResourceListTable.vue @@ -340,6 +340,20 @@ +
+ + {{ $t('Rows per page') }} + + { } }); +const selectDynamicWidth = computed(() => { + const length = pageSizeInternal.value?.toString().length || 2; + const calculatedWidth = length * 9 + 50; + return `${Math.max(68, calculatedWidth)}px`; +}); + // emits, update page const emits = defineEmits([ 'update:page', From a610e96907cc233ae10c2c88c6cf432498bba6b0 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 11 May 2026 14:11:09 +0300 Subject: [PATCH 06/14] feat: update placeholder logic in Select component and add dynamic placeholder to ResourceListTable --- adminforth/spa/src/afcl/Select.vue | 2 +- adminforth/spa/src/components/ResourceListTable.vue | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/adminforth/spa/src/afcl/Select.vue b/adminforth/spa/src/afcl/Select.vue index 54bf3f7ac..46114a68e 100644 --- a/adminforth/spa/src/afcl/Select.vue +++ b/adminforth/spa/src/afcl/Select.vue @@ -16,7 +16,7 @@ :class="[{'cursor-pointer': searchDisabled}, classesForInput]" autocomplete="off" data-custom="no-autofill" :placeholder=" - selectedItems.length && !multiple ? '' : (showDropdown ? $t('Search') : placeholder || $t('Select...')) + selectedItems.length && !multiple ? '' : (showDropdown && !searchDisabled ? $t('Search') : placeholder || $t('Select...')) " /> diff --git a/adminforth/spa/src/components/ResourceListTable.vue b/adminforth/spa/src/components/ResourceListTable.vue index 521fc9417..28847b028 100644 --- a/adminforth/spa/src/components/ResourceListTable.vue +++ b/adminforth/spa/src/components/ResourceListTable.vue @@ -349,7 +349,8 @@ v-model="pageSizeInternal" :options="pageSizeOptionsComputed" :searchDisabled="true" - :style="{ width: selectDynamicWidth }" + :style="{ width: selectDynamicWidth }" + :placeholder="pageSizeInternal?.toString()" class="text-sm " classesForInput="h-[34px] min-h-0 py-1 pl-2 pr-6 text-sm rounded-md cursor-pointer af-button-shadow bg-lightDropdownButtonsBackground text-lightDropdownButtonsText border-lightDropdownButtonsBorder From c03a3a731dc149704e952c616ea5180423285347 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 11 May 2026 14:39:05 +0300 Subject: [PATCH 07/14] feat: refine listPageSizeOptions type for improved type safety and clarity --- .../docs/tutorial/03-Customization/13-standardPagesTuning.md | 4 ++-- adminforth/types/Common.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md b/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md index e59ab7796..9199203be 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md @@ -320,7 +320,7 @@ export default { ``` ### List Page Size Options -You can define available pagination sizes using options.listPageSizeOptions. This allows users to choose how many records they want to see per page in the list view. +You can define available pagination sizes using options.listPageSizeOptions. This allows users to choose how many records they want to see per page in the list view. ```typescript title="./resources/apartments.ts" export default { resourceId: 'aparts', @@ -360,7 +360,7 @@ Page size switching is implemented via a select dropdown (select input) in the t - Chooses a value (e.g. 20) - Table reloads with the new page size > ☝️Notes -If listPageSizeOptions is not provided, a default set of page sizes will be used. The selected value updates the table immediately and triggers a data refetch. This option works together with listPageSize, which defines both the default value and the available options at the same time. +If `listPageSizeOptions` is not provided (or resolves to an empty array), the page size select is not shown. The selected value updates the table immediately and triggers a data refetch. Use `listPageSize` to define the initial number of records per page, and `listPageSizeOptions` to define which page sizes the user can switch between. ### Virtual scroll diff --git a/adminforth/types/Common.ts b/adminforth/types/Common.ts index e6120804c..cdcbdc487 100644 --- a/adminforth/types/Common.ts +++ b/adminforth/types/Common.ts @@ -472,7 +472,7 @@ export interface AdminForthResourceInputCommon { * Page size for list view */ listPageSize?: number, - listPageSizeOptions?: number[] | ((args: { adminUser: any, adminforth: any }) => number[] | Promise); + listPageSizeOptions?: number[] | ((args: { adminUser: AdminUser, adminforth: IAdminForth }) => number[] | Promise); /** * Whether to use virtual scroll in list view. From 8cd6d720996654eacbf8aa59ce665c4b49186a60 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 11 May 2026 14:51:09 +0300 Subject: [PATCH 08/14] feat: enhance PAGE_SIZE_OPTIONS computation for better type handling and clarity --- adminforth/spa/src/views/ListView.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/adminforth/spa/src/views/ListView.vue b/adminforth/spa/src/views/ListView.vue index cc737940b..6206c6099 100644 --- a/adminforth/spa/src/views/ListView.vue +++ b/adminforth/spa/src/views/ListView.vue @@ -273,9 +273,12 @@ watch(() => coreStore.resource?.resourceId, () => { const PAGE_SIZE_OPTIONS = computed(() => { const array = coreStore.resource?.options?.listPageSizeOptions; - if (!array || array.length === 0) return undefined; + if (!Array.isArray(array)) return undefined; - return array.map(size => ({ label: size.toString(), value: size })); + return array.map((size: number) => ({ + label: size.toString(), + value: size + })); }); const pageSize = ref(DEFAULT_PAGE_SIZE); @@ -450,8 +453,7 @@ async function init() { } if (route.query.pageSize) { const parsedPageSize = parseInt(route.query.pageSize as string); - // Перевіряємо наявність опцій перед використанням .includes - if (PAGE_SIZE_OPTIONS.value && PAGE_SIZE_OPTIONS.value.some(o => o.value === parsedPageSize)) { + if (PAGE_SIZE_OPTIONS.value && PAGE_SIZE_OPTIONS.value.some((o: { value: number }) => o.value === parsedPageSize)) { pageSize.value = parsedPageSize; } } else if (coreStore.resource?.options?.listPageSize) { From 13c22f4a8c76fa4142e2fcbdd03b10e119e8b4c8 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 11 May 2026 14:54:17 +0300 Subject: [PATCH 09/14] fix: rebuild --- adminforth/spa/src/afcl/Select.vue | 2 +- adminforth/spa/src/views/ListView.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adminforth/spa/src/afcl/Select.vue b/adminforth/spa/src/afcl/Select.vue index 46114a68e..f5aa40a25 100644 --- a/adminforth/spa/src/afcl/Select.vue +++ b/adminforth/spa/src/afcl/Select.vue @@ -16,7 +16,7 @@ :class="[{'cursor-pointer': searchDisabled}, classesForInput]" autocomplete="off" data-custom="no-autofill" :placeholder=" - selectedItems.length && !multiple ? '' : (showDropdown && !searchDisabled ? $t('Search') : placeholder || $t('Select...')) + selectedItems.length && !multiple ? '' : (showDropdown && !searchDisabled ? $t('Search') : placeholder || $t('Select...')) " /> diff --git a/adminforth/spa/src/views/ListView.vue b/adminforth/spa/src/views/ListView.vue index 6206c6099..1420bb145 100644 --- a/adminforth/spa/src/views/ListView.vue +++ b/adminforth/spa/src/views/ListView.vue @@ -449,7 +449,7 @@ async function init() { } // page init should be also in same tick if (route.query.page) { - page.value = parseInt(route.query.page as string); + page.value = parseInt(route.query.page as string); } if (route.query.pageSize) { const parsedPageSize = parseInt(route.query.pageSize as string); From b3fe21c1e088f7e308791452421f5a99dad30a9a Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 11 May 2026 15:07:09 +0300 Subject: [PATCH 10/14] feat: add option to disable toggle of selected item in page size selector --- adminforth/spa/src/components/ResourceListTable.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/adminforth/spa/src/components/ResourceListTable.vue b/adminforth/spa/src/components/ResourceListTable.vue index 28847b028..2243601db 100644 --- a/adminforth/spa/src/components/ResourceListTable.vue +++ b/adminforth/spa/src/components/ResourceListTable.vue @@ -349,6 +349,7 @@ v-model="pageSizeInternal" :options="pageSizeOptionsComputed" :searchDisabled="true" + :disableTogleOfSelectedItem="true" :style="{ width: selectDynamicWidth }" :placeholder="pageSizeInternal?.toString()" class="text-sm " From 93aba31498c5d83aa9a932f3d03c537e86889024 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 11 May 2026 15:41:27 +0300 Subject: [PATCH 11/14] feat: enhance pageSize watcher to prevent unnecessary updates --- adminforth/spa/src/views/ListView.vue | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/adminforth/spa/src/views/ListView.vue b/adminforth/spa/src/views/ListView.vue index 1420bb145..ba78fca1e 100644 --- a/adminforth/spa/src/views/ListView.vue +++ b/adminforth/spa/src/views/ListView.vue @@ -544,12 +544,15 @@ watch([page], async () => { setQuery({ page: page.value }); }); -watch(pageSize, async () => { +watch(pageSize, async (newSize, oldSize) => { + if (newSize === oldSize) return; + if (initInProcess) return; + page.value = 1; setQuery({ page: 1, - pageSize: pageSize.value, + pageSize: newSize, }); }); From e3b985f37f01c5bd6220133c9d5287942cd2b4ae Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 11 May 2026 15:44:45 +0300 Subject: [PATCH 12/14] rebuild --- adminforth/spa/src/views/ListView.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/adminforth/spa/src/views/ListView.vue b/adminforth/spa/src/views/ListView.vue index ba78fca1e..0a0c51b09 100644 --- a/adminforth/spa/src/views/ListView.vue +++ b/adminforth/spa/src/views/ListView.vue @@ -283,9 +283,7 @@ const PAGE_SIZE_OPTIONS = computed(() => { const pageSize = ref(DEFAULT_PAGE_SIZE); -const isVirtualScrollEnabled = computed(() => - coreStore.resource?.options?.listVirtualScrollEnabled || false -); +const isVirtualScrollEnabled = computed(() => coreStore.resource?.options?.listVirtualScrollEnabled || false); const listBufferSize = computed(() => coreStore.resource?.options?.listBufferSize || 30); const isPageLoaded = ref(false); From 1d6a2f49c1258e88fbb8ec22f0790a8362eeaafc Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 11 May 2026 15:54:19 +0300 Subject: [PATCH 13/14] feat: add listPageSizeOptions description --- adminforth/types/Common.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/adminforth/types/Common.ts b/adminforth/types/Common.ts index cdcbdc487..d4156ac80 100644 --- a/adminforth/types/Common.ts +++ b/adminforth/types/Common.ts @@ -472,6 +472,12 @@ export interface AdminForthResourceInputCommon { * Page size for list view */ listPageSize?: number, + + /** + * Available page size options for list view, provided as an array of page sizes + * or a function returning them. When set together with `listPageSize`, the page + * size should be one of the values returned here. + */ listPageSizeOptions?: number[] | ((args: { adminUser: AdminUser, adminforth: IAdminForth }) => number[] | Promise); /** From 22d1c070262972c9a05f0ca1e42dc47b04f4fa09 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 11 May 2026 16:06:16 +0300 Subject: [PATCH 14/14] feat: refactor pageSize handling to support query parameter and improve default logic --- adminforth/spa/src/views/ListView.vue | 57 +++++++++++++-------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/adminforth/spa/src/views/ListView.vue b/adminforth/spa/src/views/ListView.vue index 0a0c51b09..ed3e051b5 100644 --- a/adminforth/spa/src/views/ListView.vue +++ b/adminforth/spa/src/views/ListView.vue @@ -262,14 +262,6 @@ const customActionLoadingStates = ref<{[key: string]: boolean}>({}); const DEFAULT_PAGE_SIZE = 10; -watch(() => coreStore.resource?.resourceId, () => { - if (coreStore.resource?.options?.listPageSize) { - pageSize.value = coreStore.resource.options.listPageSize; - } else { - pageSize.value = DEFAULT_PAGE_SIZE; - } -}); - const PAGE_SIZE_OPTIONS = computed(() => { const array = coreStore.resource?.options?.listPageSizeOptions; @@ -281,7 +273,34 @@ const PAGE_SIZE_OPTIONS = computed(() => { })); }); -const pageSize = ref(DEFAULT_PAGE_SIZE); +const pageSize = computed({ + get() { + if (route.query.pageSize) { + const parsed = parseInt(route.query.pageSize as string); + if (PAGE_SIZE_OPTIONS.value?.some(o => o.value === parsed)) { + return parsed; + } + } + + if (coreStore.resource?.options?.listPageSize) { + return coreStore.resource.options.listPageSize; + } + + return DEFAULT_PAGE_SIZE; + }, + + set(newSize) { + if (initInProcess) return; + + page.value = 1; + + setQuery({ + page: 1, + pageSize: newSize, + }); + } +}); + const isVirtualScrollEnabled = computed(() => coreStore.resource?.options?.listVirtualScrollEnabled || false); const listBufferSize = computed(() => coreStore.resource?.options?.listBufferSize || 30); @@ -449,14 +468,6 @@ async function init() { if (route.query.page) { page.value = parseInt(route.query.page as string); } - if (route.query.pageSize) { - const parsedPageSize = parseInt(route.query.pageSize as string); - if (PAGE_SIZE_OPTIONS.value && PAGE_SIZE_OPTIONS.value.some((o: { value: number }) => o.value === parsedPageSize)) { - pageSize.value = parsedPageSize; - } - } else if (coreStore.resource?.options?.listPageSize) { - pageSize.value = coreStore.resource.options.listPageSize; - } // getList(); - Not needed here, watch will trigger it columnsMinMax.value = await callAdminForthApi({ @@ -542,18 +553,6 @@ watch([page], async () => { setQuery({ page: page.value }); }); -watch(pageSize, async (newSize, oldSize) => { - if (newSize === oldSize) return; - if (initInProcess) return; - - page.value = 1; - - setQuery({ - page: 1, - pageSize: newSize, - }); -}); - watch([sort], async () => { if (!sort.value.length) { setQuery({ sort: undefined });