|
10 | 10 | :adminUser="coreStore.adminUser" |
11 | 11 | /> |
12 | 12 | <BreadcrumbsWithButtons> |
| 13 | + <RouterLink |
| 14 | + v-if="hasListNavContext && coreStore.resourceOptions?.showNextButton !== false" |
| 15 | + :to="nextRecordRoute ?? $route" |
| 16 | + @click="handleNextClick()" |
| 17 | + :class="!nextRecordRoute ? 'opacity-50 pointer-events-none cursor-not-allowed' : ''" |
| 18 | + class="af-button-shadow h-[34px] inline-flex items-center gap-1 px-3 py-2 text-sm font-medium transition-all border outline-none bg-lightListViewButtonBackground text-lightListViewButtonText border-lightListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:border-darkListViewButtonBorder hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover rounded-default dark:hover:text-darkListViewButtonTextHover dark:hover:bg-darkListViewButtonBackgroundHover" |
| 19 | + > |
| 20 | + <Spinner v-if="isFetchingNextPage" class="w-4 h-4 text-gray-200 dark:text-gray-500 fill-gray-500 dark:fill-gray-300" /> |
| 21 | + {{ $t('Next') }} |
| 22 | + </RouterLink> |
| 23 | + |
13 | 24 | <template v-if="coreStore.resource?.options?.actions"> |
14 | 25 |
|
15 | 26 | <div class="flex gap-1" v-for="action in coreStore.resource.options.actions.filter(a => a.showIn?.showButton)" :key="action.id"> |
|
60 | 71 | {{ $t('Delete') }} |
61 | 72 | </button> |
62 | 73 |
|
63 | | - <ThreeDotsMenu |
| 74 | + <ThreeDotsMenu |
64 | 75 | :threeDotsDropdownItems="(coreStore.resourceOptions?.pageInjections?.show?.threeDotsDropdownItems as [])" |
65 | 76 | :customActions="customActions" |
66 | 77 | ></ThreeDotsMenu> |
| 78 | + |
67 | 79 | </BreadcrumbsWithButtons> |
68 | 80 |
|
69 | 81 | <component |
@@ -181,7 +193,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue'; |
181 | 193 | import { useCoreStore } from '@/stores/core'; |
182 | 194 | import { getCustomComponent, checkAcessByAllowedActions, initThreeDotsDropdown, formatComponent, executeCustomAction } from '@/utils'; |
183 | 195 | import { IconPenSolid, IconTrashBinSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite'; |
184 | | -import { onMounted, onUnmounted, ref, computed } from 'vue'; |
| 196 | +import { onMounted, onUnmounted, ref, computed, watch } from 'vue'; |
185 | 197 | import { useRoute,useRouter } from 'vue-router'; |
186 | 198 | import {callAdminForthApi} from '@/utils'; |
187 | 199 | import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi'; |
@@ -298,6 +310,87 @@ const allColumns = computed(() => { |
298 | 310 | return coreStore.resource?.columns?.filter(col => col.showIn?.show); |
299 | 311 | }); |
300 | 312 |
|
| 313 | +const isFetchingNextPage = ref(false); |
| 314 | +const nextPageRecords = ref<string[]>([]); |
| 315 | +
|
| 316 | +const hasListNavContext = computed(() => |
| 317 | + coreStore.listResourceId === route.params.resourceId && coreStore.listRecordIds.length > 0 |
| 318 | +); |
| 319 | +
|
| 320 | +const currentRecordIndex = computed(() => { |
| 321 | + const pk = String(route.params.primaryKey); |
| 322 | + return coreStore.listRecordIds.findIndex((id: any) => String(id) === pk); |
| 323 | +}); |
| 324 | +
|
| 325 | +const isLastOnCurrentPage = computed(() => |
| 326 | + currentRecordIndex.value === coreStore.listRecordIds.length - 1 |
| 327 | +); |
| 328 | +
|
| 329 | +const hasNextRecord = computed(() => { |
| 330 | + if (currentRecordIndex.value < 0) return false; |
| 331 | + if (!isLastOnCurrentPage.value) return true; |
| 332 | + return coreStore.listRecordIds.length === coreStore.listPageSize; |
| 333 | +}); |
| 334 | +
|
| 335 | +const nextRecordRoute = computed(() => { |
| 336 | + if (!hasNextRecord.value) return null; |
| 337 | + if (!isLastOnCurrentPage.value) { |
| 338 | + return { |
| 339 | + name: 'resource-show', |
| 340 | + params: { resourceId: route.params.resourceId, primaryKey: coreStore.listRecordIds[currentRecordIndex.value + 1] }, |
| 341 | + }; |
| 342 | + } |
| 343 | + if (nextPageRecords.value.length > 0) { |
| 344 | + return { |
| 345 | + name: 'resource-show', |
| 346 | + params: { resourceId: route.params.resourceId, primaryKey: nextPageRecords.value[0] }, |
| 347 | + }; |
| 348 | + } |
| 349 | + return null; |
| 350 | +}); |
| 351 | +
|
| 352 | +async function prefetchNextPage() { |
| 353 | + if (!hasListNavContext.value || isFetchingNextPage.value) return; |
| 354 | + if (coreStore.listRecordIds.length !== coreStore.listPageSize) return; |
| 355 | +
|
| 356 | + isFetchingNextPage.value = true; |
| 357 | + try { |
| 358 | + const response = await callAdminForthApi({ |
| 359 | + path: '/get_resource_data', |
| 360 | + method: 'POST', |
| 361 | + body: { |
| 362 | + source: 'list', |
| 363 | + resourceId: coreStore.listResourceId, |
| 364 | + limit: coreStore.listPageSize, |
| 365 | + offset: coreStore.listPage * coreStore.listPageSize, |
| 366 | + filters: coreStore.listFilters, |
| 367 | + sort: coreStore.listSort, |
| 368 | + }, |
| 369 | + }); |
| 370 | + if (response?.recordIds?.length) { |
| 371 | + nextPageRecords.value = response.recordIds; |
| 372 | + } |
| 373 | + } finally { |
| 374 | + isFetchingNextPage.value = false; |
| 375 | + } |
| 376 | +} |
| 377 | +
|
| 378 | +watch(isLastOnCurrentPage, (isLast) => { |
| 379 | + if (isLast) { |
| 380 | + prefetchNextPage(); |
| 381 | + } else { |
| 382 | + nextPageRecords.value = []; |
| 383 | + } |
| 384 | +}, { immediate: true }); |
| 385 | +
|
| 386 | +function handleNextClick() { |
| 387 | + if (isLastOnCurrentPage.value && nextPageRecords.value.length > 0) { |
| 388 | + coreStore.listRecordIds = nextPageRecords.value; |
| 389 | + coreStore.listPage += 1; |
| 390 | + nextPageRecords.value = []; |
| 391 | + } |
| 392 | +} |
| 393 | +
|
301 | 394 | const otherColumns = computed(() => { |
302 | 395 | const groupedColumnNames = new Set( |
303 | 396 | groups.value.flatMap(group => group.columns?.map((col: AdminForthResourceColumnCommon) => col.name)) |
|
0 commit comments