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
39 changes: 39 additions & 0 deletions adminforth/modules/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2431,6 +2431,45 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
}
}
});
server.endpoint({
method: 'POST',
path: '/next_filtered_record',
handler: async ({ body, adminUser }) => {
const { resourceId, offset, filters, sort } = body;

const resource = this.adminforth.config.resources.find(
(res) => res.resourceId === resourceId
);
if (!resource) {
return { error: `Resource ${resourceId} not found` };
}

const { allowedActions } = await interpretResource(
adminUser, resource, {}, ActionCheckSource.ShowRequest, this.adminforth
);
const { allowed, error } = checkAccess(AllowedActionsEnum.show, allowedActions);
if (!allowed) {
return { error };
}

const pkColumn = resource.columns.find((col) => col.primaryKey);

const data = await this.adminforth.connectors[resource.dataSource].getData({
resource,
filters: {
operator: AdminForthFilterOperators.AND,
subFilters: filters || [],
},
limit: 1,
offset: offset + 1,
sort: sort || [],
});

const record = data.data?.[0];
return { id: record?.[pkColumn.name] ?? null };
},
});

// setup endpoints for all plugins
this.adminforth.activatedPlugins.forEach((plugin) => {
plugin.setupEndpoints(server);
Expand Down
4 changes: 3 additions & 1 deletion adminforth/spa/src/components/ListActionsThreeDots.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
params: {
resourceId: props.resourceId,
primaryKey: record._primaryKeyValue,
}
},
query: props.rowOffset !== undefined ? { offset: props.rowOffset } : undefined,
}"

>
Expand Down Expand Up @@ -116,6 +117,7 @@ const props = defineProps<{
record: any;
customActionIconsThreeDotsMenuItems: AdminForthComponentDeclaration[];
resourceId: string;
rowOffset?: number;
deleteRecord: (record: any) => void;
updateRecords: () => void;
startCustomAction: (actionId: string, row: any, extraData?: Record<string, any>) => void;
Expand Down
11 changes: 10 additions & 1 deletion adminforth/spa/src/components/ResourceListTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@
params: {
resourceId: resource.resourceId,
primaryKey: row._primaryKeyValue,
}
},
query: { offset: rowGlobalOffset(row) },
}"
>
<IconEyeSolid class="af-show-icon w-5 h-5 me-2"/>
Expand Down Expand Up @@ -238,6 +239,7 @@
:updateRecords="()=>emits('update:records', true)"
:deleteRecord="deleteRecord"
:resourceId="resource.resourceId"
:rowOffset="rowGlobalOffset(row)"
:startCustomAction="startCustomAction"
:customActionIconsThreeDotsMenuItems="customActionIconsThreeDotsMenuItems ?? []"
/>
Expand Down Expand Up @@ -477,6 +479,11 @@ const showListActionsThreeDots = computed(() => {
})

const from = computed(() => ((page.value || 1) - 1) * props.pageSize + 1);

function rowGlobalOffset(row: any): number {
const indexInPage = props.rows?.indexOf(row) ?? -1;
return (page.value - 1) * props.pageSize + indexInPage;
}
const to = computed(() => Math.min((page.value || 1) * props.pageSize, props.totalRows));

watch(() => page.value, (newPage) => {
Expand Down Expand Up @@ -611,6 +618,7 @@ async function onClick(e: any, row: any) {
resourceId: props.resource?.resourceId,
primaryKey: row._primaryKeyValue,
},
query: { offset: rowGlobalOffset(row) },
}).href,
'_blank'
);
Expand All @@ -629,6 +637,7 @@ async function onClick(e: any, row: any) {
resourceId: props.resource?.resourceId,
primaryKey: row._primaryKeyValue,
},
query: { offset: rowGlobalOffset(row) },
});
}
}
Expand Down
54 changes: 53 additions & 1 deletion adminforth/spa/src/views/ShowView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
:adminUser="coreStore.adminUser"
/>
<BreadcrumbsWithButtons>
<button v-if="filtersStore.visibleFiltersCount > 0"
:disabled="nextId === null"
@click="nextId && router.push({ name: 'resource-show', params: { resourceId: $route.params.resourceId, primaryKey: nextId }, query: { offset: currentOffset + 1 } })"
:class="nextId === null ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'"
class="af-button-shadow h-[34px] inline-flex items-center px-3 py-2 text-left
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"
>
{{ $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 @@ -181,7 +194,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
import { useCoreStore } from '@/stores/core';
import { getCustomComponent, checkAcessByAllowedActions, initThreeDotsDropdown, formatComponent, executeCustomAction } from '@/utils';
import { IconPenSolid, IconTrashBinSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
import { onMounted, onUnmounted, ref, computed } from 'vue';
import { onMounted, onUnmounted, ref, computed, watch } from 'vue';
import { useRoute,useRouter } from 'vue-router';
import {callAdminForthApi} from '@/utils';
import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
Expand All @@ -194,9 +207,12 @@ import { type AdminForthComponentDeclarationFull, type AdminForthResourceColumnC
import CallActionWrapper from '@/components/CallActionWrapper.vue'
import { Spinner } from '@/afcl';
import websocket from '@/websocket';
import { useFiltersStore } from '@/stores/filters';

const route = useRoute();
const router = useRouter();
const filtersStore = useFiltersStore();

const loading = ref(true);
const { t } = useI18n();
const { confirm, alert, show } = useAdminforth();
Expand Down Expand Up @@ -226,6 +242,41 @@ const showPageTopic = computed(() => {
return `${SHOW_PAGE_TOPIC_PREFIX}${String(route.params.resourceId)}/${String(route.params.primaryKey)}`;
});

const nextId = ref<string | null>(null);
const currentOffset = computed(() => {
const raw = route.query.offset;
if (raw === undefined || raw === null) return undefined;
const n = Number(raw);
return isNaN(n) ? undefined : n;
});

watch(() => [route.params.primaryKey, route.query.offset], async () => {
await loadNeighbors();
});

async function loadNeighbors() {
if (currentOffset.value === undefined) {
nextId.value = null;
return;
}
const resourceId = route.params.resourceId as string;

const next = await callAdminForthApi({
path: '/next_filtered_record',
method: 'POST',
body: {
resourceId,
filters: filtersStore.filters,
sort: filtersStore.getSort(),
offset: currentOffset.value,
},
});

nextId.value = next.id ?? null;
console.log('nextId.value:', nextId.value, typeof nextId.value);

}

function applyShowPageUpdates(data: { resourceId: string; recordId: string; updates: Record<string, any> }) {
if (String(data.resourceId) !== String(route.params.resourceId)) {
return;
Expand Down Expand Up @@ -264,6 +315,7 @@ onMounted(async () => {
checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'show');
}
loading.value = false;
await loadNeighbors();
});

onUnmounted(() => {
Expand Down