Skip to content
Merged

Next #679

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bffffa6
feat: add next button to show page
SerVitasik Jun 5, 2026
c4ea87c
fix: change next btn style and logic
kulikp1 Jun 9, 2026
419890f
fix: update next btn logic use coreStore
kulikp1 Jun 10, 2026
db60a68
fix: add documentation for next btn
kulikp1 Jun 10, 2026
45d80d8
Merge branch 'next' of github.com:devforth/adminforth into add-next-b…
kulikp1 Jun 17, 2026
0d757ec
fix: chacnge button to router link
kulikp1 Jun 17, 2026
dc24d5a
fix: add page support
kulikp1 Jun 18, 2026
1fcf8c7
fix: fix btoa() to UTF-8 friendly
kulikp1 Jun 18, 2026
39de982
Merge pull request #673 from devforth/feature/AdminForth/1726/image
SerVitasik Jun 19, 2026
2d5ea62
Merge pull request #638 from devforth/add-next-button
SerVitasik Jun 19, 2026
bba8bb2
fix: fix bug in create-app cli
kulikp1 Jun 19, 2026
df03297
Merge pull request #674 from devforth/feature/AdminForth/1691/on-newl…
SerVitasik Jun 19, 2026
8a7cae6
fix: fix race condition
kulikp1 Jun 19, 2026
ab8032e
Merge pull request #675 from devforth/feature/AdminForth/1691/on-newl…
SerVitasik Jun 19, 2026
ff60f7e
fix: update documentation for clerk adapter
kulikp1 Jun 19, 2026
3598ab2
fix: add checkIfFieldIsInsideResourceColumns function in utils
yaroslav8765 Jun 19, 2026
642e213
Merge branch 'next' of github.com:devforth/adminforth into next
yaroslav8765 Jun 19, 2026
2bd7cca
fix: add loader while oauthRedirecting
kulikp1 Jun 19, 2026
9037615
Merge pull request #676 from devforth/feature/AdminForth/1738/before-…
SerVitasik Jun 22, 2026
1c44fd4
fix: delete senseless max-w-[200px]
kulikp1 Jun 22, 2026
dc04df0
Merge branch 'main' of github.com:devforth/adminforth into next
yaroslav8765 Jun 22, 2026
d602c71
dev-demo: remove invalid resource import
yaroslav8765 Jun 22, 2026
205fd2e
fix: remove vite spa link log when HEAVY_DUBUG is on
yaroslav8765 Jun 22, 2026
5553a5e
dev-demo: add missing clerck adapter to the taskfile
yaroslav8765 Jun 22, 2026
5f07632
dev-demo: generate "secret_field" when autofill database
yaroslav8765 Jun 23, 2026
d4c9b85
fix: export applyRegexValidation from adminforth
yaroslav8765 Jun 23, 2026
e3e43f3
fix: export rejectApiRawFilters from restApi module and update index …
yaroslav8765 Jun 23, 2026
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 @@ -542,6 +542,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
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Adds support for Twitch authentication, useful for streaming or creator-oriented
## Clerk OAuth Adapter

```bash
pnpm i @adminforth/clerk-oauth-adapter
pnpm i @adminforth/oauth-adapter-clerk
```

Enables sign-in via [Clerk](https://clerk.com/) — a hosted authentication platform with built-in user management, MFA, and social logins.
4 changes: 2 additions & 2 deletions adminforth/documentation/docs/tutorial/09-Plugins/11-oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ plugins: [
Install Adapter:

```bash
pnpm install @adminforth/clerk-oauth-adapter --save
pnpm install @adminforth/oauth-adapter-clerk --save
```

1. Go to the [Clerk Dashboard](https://dashboard.clerk.com) and open your application.
Expand All @@ -502,7 +502,7 @@ CLERK_DOMAIN=https://your-app.clerk.accounts.dev
Add the adapter to your plugin configuration:

```typescript title="./resources/adminuser.ts"
import AdminForthAdapterClerkOauth2 from '@adminforth/clerk-oauth-adapter';
import AdminForthAdapterClerkOauth2 from '@adminforth/oauth-adapter-clerk';

// ... existing resource configuration ...
plugins: [
Expand Down
34 changes: 4 additions & 30 deletions adminforth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CodeInjector from './modules/codeInjector.js';
import ExpressServer from './servers/express.js';
import OpenApiRegistry from './servers/openapi.js';
// import FastifyServer from './servers/fastify.js';
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn, convertPeriodToSeconds, hookResponseError, md5hash } from './modules/utils.js';
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn, convertPeriodToSeconds, hookResponseError, md5hash, applyRegexValidation } from './modules/utils.js';
import {
type AdminForthConfig,
type IAdminForth,
Expand Down Expand Up @@ -35,7 +35,7 @@ import {

import AdminForthPlugin from './basePlugin.js';
import ConfigValidator from './modules/configValidator.js';
import AdminForthRestAPI, { interpretResource } from './modules/restApi.js';
import AdminForthRestAPI, { interpretResource, rejectApiRawFilters } from './modules/restApi.js';
import OperationalResource from './modules/operationalResource.js';
import SocketBroker from './modules/socketBroker.js';
import { afLogger } from './modules/logger.js';
Expand All @@ -50,7 +50,7 @@ export * from './types/adapters/index.js';
export * from './modules/filtersTools.js';
export * from './modules/requestContext.js';
export * from './modules/utils.js';
export { interpretResource };
export { interpretResource, rejectApiRawFilters };
export { AdminForthPlugin };
export { suggestIfTypo, RateLimiter, RAMLock, getClientIp, convertPeriodToSeconds };
export { default as AdminForthBaseConnector } from './dataConnectors/baseConnector.js';
Expand All @@ -69,33 +69,7 @@ class AdminForth implements IAdminForth {
},

applyRegexValidation(value, validation) {
if (validation?.length) {
const validationArray = validation;
for (let i = 0; i < validationArray.length; i++) {
if (validationArray[i].regExp) {
let flags = '';
if (validationArray[i].caseSensitive) {
flags += 'i';
}
if (validationArray[i].multiline) {
flags += 'm';
}
if (validationArray[i].global) {
flags += 'g';
}

const regExp = new RegExp(validationArray[i].regExp, flags);
if (value === undefined || value === null) {
value = '';
}
let valueS = `${value}`;

if (!regExp.test(valueS)) {
return validationArray[i].message;
}
}
}
}
return applyRegexValidation(value, validation);
},

PASSWORD_VALIDATORS: {
Expand Down
1 change: 0 additions & 1 deletion adminforth/modules/codeInjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,6 @@ class CodeInjector implements ICodeInjector {
// parse port from message " ➜ Local: http://localhost:xyz/"
const s = stripAnsiCodes(data.toString());

process.env.HEAVY_DEBUG && console.log(`🪲 devServer stdout ➜ (port detect): ${s}`);
const portMatch = s.match(/.+?http:\/\/.+?:(\d+).+?/m);
if (portMatch) {
this.devServerPort = parseInt(portMatch[1]);
Expand Down
8 changes: 7 additions & 1 deletion adminforth/modules/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ function hasApiRawFilter(filters: any): boolean {
return Array.isArray(filters.subFilters) && filters.subFilters.some(hasApiRawFilter);
}

function rejectApiRawFilters(filters: any): { error: string } | undefined {
export function rejectApiRawFilters(filters: any): { error: string } | undefined {
if (hasApiRawFilter(filters)) {
return { error: 'insecureRawSQL and insecureRawNoSQL filters are not allowed in API requests' };
}
Expand Down Expand Up @@ -341,6 +341,7 @@ const getResourceDataResponseSchema: AnySchemaObject = createErrorOrSuccessSchem
items: genericObjectSchema,
},
total: { type: 'number' },
recordIds: { type: 'array', items: {} },
options: genericObjectSchema,
},
additionalProperties: true,
Expand Down Expand Up @@ -1636,6 +1637,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
30 changes: 30 additions & 0 deletions adminforth/modules/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,4 +592,34 @@ export function checkIfFieldIsInsideResourceColumns(fieldName: string, resource:
}
}
return false;
}

export function applyRegexValidation(value, validation) {
if (validation?.length) {
const validationArray = validation;
for (let i = 0; i < validationArray.length; i++) {
if (validationArray[i].regExp) {
let flags = '';
if (validationArray[i].caseSensitive) {
flags += 'i';
}
if (validationArray[i].multiline) {
flags += 'm';
}
if (validationArray[i].global) {
flags += 'g';
}

const regExp = new RegExp(validationArray[i].regExp, flags);
if (value === undefined || value === null) {
value = '';
}
let valueS = `${value}`;

if (!regExp.test(valueS)) {
return validationArray[i].message;
}
}
}
}
}
8 changes: 6 additions & 2 deletions adminforth/spa/src/components/CustomDateRangePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ async function initDatepickers() {
'flowbite-datepicker/Datepicker'
);

if (!datepickerStartEl.value || !datepickerEndEl.value) {
return;
}

const LS_LANG_KEY = `afLanguage`;
datepickerStartObject.value = new Datepicker(datepickerStartEl.value, {format: 'dd M yyyy', language: localStorage.getItem(LS_LANG_KEY)});
datepickerEndObject.value = new Datepicker(datepickerEndEl.value, {format: 'dd M yyyy', language: localStorage.getItem(LS_LANG_KEY)});
Expand All @@ -225,8 +229,8 @@ function removeChangeDateListener() {
}

function destroyDatepickerElement() {
datepickerStartObject.value.destroy();
datepickerEndObject.value.destroy();
datepickerStartObject.value?.destroy?.();
datepickerEndObject.value?.destroy?.();
}

function setStartDate(event) {
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 @@ -422,7 +422,8 @@ const props = withDefaults(defineProps<{
bufferSize?: number,
customActionIconsThreeDotsMenuItems?: AdminForthComponentDeclaration[]
tableRowReplaceInjection?: AdminForthComponentDeclaration,
isVirtualScrollEnabled: boolean
isVirtualScrollEnabled: boolean,
filters?: any[]
}>(), {
sort: () => []
});
Expand Down Expand Up @@ -615,6 +616,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
2 changes: 1 addition & 1 deletion adminforth/spa/src/renderers/SensitiveBlurCell.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="inline-flex items-center gap-1">
<div
class="overflow-hidden max-w-[200px] max-h-[20px] rounded-default"
class="overflow-hidden max-h-[20px] rounded-default"
:title="show ? $t('Click to hide') : $t('Click to show')"
@click="toggle"
>
Expand Down
16 changes: 16 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 @@ -214,6 +220,10 @@ export const useCoreStore = defineStore('core', () => {
resourceId,
}
});
if (!res) {
isResourceFetching.value = false;
return;
}
if (res.error) {
resourceColumnsError.value = res.error;
} else {
Expand Down Expand Up @@ -290,5 +300,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
14 changes: 12 additions & 2 deletions adminforth/spa/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,11 +641,21 @@ export function checkShowIf(c: AdminForthResourceColumnInputCommon, record: Reco
}

export function btoa_function(source: string): string {
return btoa(source);
// UTF-8 safe base64 encode: plain btoa() throws on characters outside the
const bytes = new TextEncoder().encode(source);
let binary = '';
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
}
return btoa(binary);
}

export function atob_function(source: string): string {
return atob(source);
// UTF-8 safe base64 decode, the counterpart of btoa_function above.
const binary = atob(source);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}

export function compareOldAndNewRecord(oldRecord: Record<string, any>, newRecord: Record<string, any>): {ok: boolean, changedFields: Record<string, {oldValue: any, newValue: any}>} {
Expand Down
6 changes: 3 additions & 3 deletions adminforth/spa/src/views/CreateView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
import ResourceForm from '@/components/ResourceForm.vue';
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
import { useCoreStore } from '@/stores/core';
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, onBeforeRouteLeaveCreateEditViewGuard, leaveGuardActiveClass, formatComponent } from '@/utils';
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, onBeforeRouteLeaveCreateEditViewGuard, leaveGuardActiveClass, formatComponent, atob_function } from '@/utils';
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
import { onMounted, onBeforeMount, onBeforeUnmount, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
Expand Down Expand Up @@ -177,14 +177,14 @@ onMounted(async () => {
if (userUseMultipleEncoding) {
initialValues.value = { ...initialValues.value, ...JSON.parse(decodeURIComponent((route.query.values as string))) };
} else {
initialValues.value = { ...initialValues.value, ...JSON.parse(atob(route.query.values as string)) };
initialValues.value = { ...initialValues.value, ...JSON.parse(atob_function(route.query.values as string)) };
}
}
if (route.query.readonlyColumns) {
if (userUseMultipleEncoding) {
readonlyColumns.value = JSON.parse(decodeURIComponent((route.query.readonlyColumns as string)));
} else {
readonlyColumns.value = JSON.parse(atob(route.query.readonlyColumns as string));
readonlyColumns.value = JSON.parse(atob_function(route.query.readonlyColumns as string));
}
}
record.value = initialValues.value;
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
Loading