Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,26 @@ And `edit` action will be available as quick action:



## Show

### Next record button

By default, when a user opens a record from the list view, a **Next** button appears on the show page. It allows navigating through records one by one, respecting the current filters and sorting applied in the list. When the user reaches the last record on the current page, AdminForth automatically fetches the next page and continues navigation seamlessly.

To disable the Next button for a resource, set `showNextButton` to `false`:

```typescript title="./resources/apartments.ts"
export default {
resourceId: 'aparts',
options: {
//diff-add
showNextButton: false,
}
}
```

> ☝️ The Next button is only shown when the user navigates to the show page from the list view. Opening a record directly via URL will not display the button.

## Creating

### Fill with default values
Expand Down
6 changes: 6 additions & 0 deletions adminforth/modules/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ const getResourceDataResponseSchema: AnySchemaObject = createErrorOrSuccessSchem
items: genericObjectSchema,
},
total: { type: 'number' },
recordIds: { type: 'array', items: {} },
options: genericObjectSchema,
},
additionalProperties: true,
Expand Down Expand Up @@ -1563,6 +1564,11 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
}
}

if (source === 'list') {
const pkField = resource.columns.find((col) => col.primaryKey).name;
(data as any).recordIds = data.data.map((item) => item[pkField]);
}

return data;
},
});
Expand Down
9 changes: 8 additions & 1 deletion adminforth/spa/src/components/ResourceListTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,8 @@ const props = withDefaults(defineProps<{
bufferSize?: number,
customActionIconsThreeDotsMenuItems?: AdminForthComponentDeclaration[]
tableRowReplaceInjection?: AdminForthComponentDeclaration,
isVirtualScrollEnabled: boolean
isVirtualScrollEnabled: boolean,
filters?: any[]
}>(), {
sort: () => []
});
Expand Down Expand Up @@ -599,6 +600,12 @@ async function onClick(e: any, row: any) {
// user asked to nothing on click
return;
}
coreStore.listRecordIds = props.rows?.map(r => r._primaryKeyValue) ?? [];
coreStore.listResourceId = props.resource?.resourceId ?? null;
coreStore.listSort = props.sort;
coreStore.listPage = page.value;
coreStore.listPageSize = props.pageSize;
coreStore.listFilters = props.filters ?? [];
if (e.ctrlKey || e.metaKey || row._clickUrl?.includes('target=_blank')) {

if (row._clickUrl) {
Expand Down
12 changes: 12 additions & 0 deletions adminforth/spa/src/stores/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const useCoreStore = defineStore('core', () => {
const isResourceFetching = ref(false);
const isInternetError = ref(false);
const screenWidth = ref(window.innerWidth);
const listRecordIds: Ref<any[]> = ref([]);
const listResourceId: Ref<string | null> = ref(null);
const listFilters: Ref<any[]> = ref([]);
const listSort: Ref<any[]> = ref([]);
const listPage: Ref<number> = ref(0);
const listPageSize: Ref<number> = ref(0);

onMounted(() => {
window.addEventListener('resize', updateWidth);
Expand Down Expand Up @@ -290,5 +296,11 @@ export const useCoreStore = defineStore('core', () => {
isIos,
isInternetError,
isMobile,
listRecordIds,
listResourceId,
listFilters,
listSort,
listPage,
listPageSize,
}
})
5 changes: 3 additions & 2 deletions adminforth/spa/src/utils/listUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ export async function getList(resource: AdminForthResourceFrontend, isPageLoaded
return row;
});
totalRows = data.total;

const recordIds = data.recordIds || [];

// if checkboxes have items which are not in current data, remove them
checkboxes.value = checkboxes.value.filter((pk: any) => rows.some((r: any) => r._primaryKeyValue === pk));
await nextTick();
return { rows, totalRows };
return { rows, totalRows, recordIds };
}


Expand Down
1 change: 1 addition & 0 deletions adminforth/spa/src/views/ListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
@update:records="getListInner"
@update:pageSize="(newSize) => { pageSize = newSize; page = 1; }"
:sort="sort"
:filters="filtersStore.filters"
:pageSizeOptions="Array.isArray(coreStore.resource?.options?.listPageSizeOptions) ? coreStore.resource?.options?.listPageSizeOptions : []"
:pageSize="pageSize"
:totalRows="totalRows"
Expand Down
74 changes: 73 additions & 1 deletion adminforth/spa/src/views/ShowView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
:adminUser="coreStore.adminUser"
/>
<BreadcrumbsWithButtons>
<button
v-if="hasListNavContext && coreStore.resourceOptions?.showNextButton !== false"
:disabled="!hasNextRecord || isFetchingNextPage"
@click="goToNextRecord()"
:class="!hasNextRecord ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'"
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"
>
<Spinner v-if="isFetchingNextPage" class="w-4 h-4 text-gray-200 dark:text-gray-500 fill-gray-500 dark:fill-gray-300" />
{{ $t('Next') }}
</button>

<template v-if="coreStore.resource?.options?.actions">

<div class="flex gap-1" v-for="action in coreStore.resource.options.actions.filter(a => a.showIn?.showButton)" :key="action.id">
Expand Down Expand Up @@ -60,10 +71,11 @@
{{ $t('Delete') }}
</button>

<ThreeDotsMenu
<ThreeDotsMenu
:threeDotsDropdownItems="(coreStore.resourceOptions?.pageInjections?.show?.threeDotsDropdownItems as [])"
:customActions="customActions"
></ThreeDotsMenu>

</BreadcrumbsWithButtons>

<component
Expand Down Expand Up @@ -298,6 +310,66 @@ const allColumns = computed(() => {
return coreStore.resource?.columns?.filter(col => col.showIn?.show);
});

const isFetchingNextPage = ref(false);

const hasListNavContext = computed(() =>
coreStore.listResourceId === route.params.resourceId && coreStore.listRecordIds.length > 0
);

const currentRecordIndex = computed(() => {
const pk = String(route.params.primaryKey);
return coreStore.listRecordIds.findIndex((id: any) => String(id) === pk);
});

const isLastOnCurrentPage = computed(() =>
currentRecordIndex.value === coreStore.listRecordIds.length - 1
);

const hasNextRecord = computed(() => {
if (currentRecordIndex.value < 0) return false;
if (!isLastOnCurrentPage.value) return true;
return coreStore.listRecordIds.length === coreStore.listPageSize;
});

async function goToNextRecord() {
if (!hasNextRecord.value) return;

if (!isLastOnCurrentPage.value) {
router.push({
name: 'resource-show',
params: { resourceId: route.params.resourceId, primaryKey: coreStore.listRecordIds[currentRecordIndex.value + 1] },
});
return;
}

isFetchingNextPage.value = true;
try {
const response = await callAdminForthApi({
path: '/get_resource_data',
method: 'POST',
body: {
source: 'list',
resourceId: coreStore.listResourceId,
limit: coreStore.listPageSize,
offset: coreStore.listPage * coreStore.listPageSize,
filters: coreStore.listFilters,
sort: coreStore.listSort,
},
});
if (response?.recordIds?.length) {
console.log('[Next] fetched next page records:', response.recordIds);
coreStore.listRecordIds = response.recordIds;
coreStore.listPage += 1;
router.push({
name: 'resource-show',
params: { resourceId: route.params.resourceId, primaryKey: response.recordIds[0] },
});
}
} finally {
isFetchingNextPage.value = false;
}
}

const otherColumns = computed(() => {
const groupedColumnNames = new Set(
groups.value.flatMap(group => group.columns?.map((col: AdminForthResourceColumnCommon) => col.name))
Expand Down
10 changes: 9 additions & 1 deletion adminforth/types/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ type AdminforthOptionsCommon = NonNullable<AdminForthResourceCommon['options']>;
export interface AdminForthOptionsForFrontend extends Omit<AdminforthOptionsCommon, 'actions' | 'bulkActions'> {
actions?: AdminForthActionFront[],
bulkActions?: AdminForthBulkActionFront[],
showNextButton?: boolean,
}

export interface AdminForthResourceFrontend extends Omit<AdminForthResourceCommon, 'options' | 'table' | 'dataSource'> {
Expand Down Expand Up @@ -520,7 +521,14 @@ export interface AdminForthResourceInputCommon {
/**
* Whether to refresh existing list rows automatically every N seconds.
*/
listRowsAutoRefreshSeconds?: number,
listRowsAutoRefreshSeconds?: number,

/**
* Whether to show the "Next" navigation button on the show page.
* Allows cycling through records respecting current list filters and sorting.
* Defaults to `true`.
*/
showNextButton?: boolean,

/**
* Custom components which can be injected into AdminForth CRUD pages.
Expand Down