Skip to content

Commit 642e213

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
AdminForth/1731/security-audit AdminForth/1731/security-audit
2 parents 3598ab2 + ff60f7e commit 642e213

13 files changed

Lines changed: 182 additions & 16 deletions

File tree

adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,26 @@ And `edit` action will be available as quick action:
542542
543543

544544

545+
## Show
546+
547+
### Next record button
548+
549+
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.
550+
551+
To disable the Next button for a resource, set `showNextButton` to `false`:
552+
553+
```typescript title="./resources/apartments.ts"
554+
export default {
555+
resourceId: 'aparts',
556+
options: {
557+
//diff-add
558+
showNextButton: false,
559+
}
560+
}
561+
```
562+
563+
> ☝️ 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.
564+
545565
## Creating
546566

547567
### Fill with default values

adminforth/documentation/docs/tutorial/06-Adapters/02-oauth2-adapters.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Adds support for Twitch authentication, useful for streaming or creator-oriented
5959
## Clerk OAuth Adapter
6060

6161
```bash
62-
pnpm i @adminforth/clerk-oauth-adapter
62+
pnpm i @adminforth/oauth-adapter-clerk
6363
```
6464

6565
Enables sign-in via [Clerk](https://clerk.com/) — a hosted authentication platform with built-in user management, MFA, and social logins.

adminforth/documentation/docs/tutorial/09-Plugins/11-oauth.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ plugins: [
481481
Install Adapter:
482482

483483
```bash
484-
pnpm install @adminforth/clerk-oauth-adapter --save
484+
pnpm install @adminforth/oauth-adapter-clerk --save
485485
```
486486

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

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

507507
// ... existing resource configuration ...
508508
plugins: [

adminforth/modules/restApi.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ const getResourceDataResponseSchema: AnySchemaObject = createErrorOrSuccessSchem
318318
items: genericObjectSchema,
319319
},
320320
total: { type: 'number' },
321+
recordIds: { type: 'array', items: {} },
321322
options: genericObjectSchema,
322323
},
323324
additionalProperties: true,
@@ -1599,6 +1600,11 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
15991600
}
16001601
}
16011602

1603+
if (source === 'list') {
1604+
const pkField = resource.columns.find((col) => col.primaryKey).name;
1605+
(data as any).recordIds = data.data.map((item) => item[pkField]);
1606+
}
1607+
16021608
return data;
16031609
},
16041610
});

adminforth/spa/src/components/CustomDateRangePicker.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ async function initDatepickers() {
208208
'flowbite-datepicker/Datepicker'
209209
);
210210
211+
if (!datepickerStartEl.value || !datepickerEndEl.value) {
212+
return;
213+
}
214+
211215
const LS_LANG_KEY = `afLanguage`;
212216
datepickerStartObject.value = new Datepicker(datepickerStartEl.value, {format: 'dd M yyyy', language: localStorage.getItem(LS_LANG_KEY)});
213217
datepickerEndObject.value = new Datepicker(datepickerEndEl.value, {format: 'dd M yyyy', language: localStorage.getItem(LS_LANG_KEY)});
@@ -225,8 +229,8 @@ function removeChangeDateListener() {
225229
}
226230
227231
function destroyDatepickerElement() {
228-
datepickerStartObject.value.destroy();
229-
datepickerEndObject.value.destroy();
232+
datepickerStartObject.value?.destroy?.();
233+
datepickerEndObject.value?.destroy?.();
230234
}
231235
232236
function setStartDate(event) {

adminforth/spa/src/components/ResourceListTable.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,8 @@ const props = withDefaults(defineProps<{
422422
bufferSize?: number,
423423
customActionIconsThreeDotsMenuItems?: AdminForthComponentDeclaration[]
424424
tableRowReplaceInjection?: AdminForthComponentDeclaration,
425-
isVirtualScrollEnabled: boolean
425+
isVirtualScrollEnabled: boolean,
426+
filters?: any[]
426427
}>(), {
427428
sort: () => []
428429
});
@@ -615,6 +616,12 @@ async function onClick(e: any, row: any) {
615616
// user asked to nothing on click
616617
return;
617618
}
619+
coreStore.listRecordIds = props.rows?.map(r => r._primaryKeyValue) ?? [];
620+
coreStore.listResourceId = props.resource?.resourceId ?? null;
621+
coreStore.listSort = props.sort;
622+
coreStore.listPage = page.value;
623+
coreStore.listPageSize = props.pageSize;
624+
coreStore.listFilters = props.filters ?? [];
618625
if (e.ctrlKey || e.metaKey || row._clickUrl?.includes('target=_blank')) {
619626
620627
if (row._clickUrl) {

adminforth/spa/src/stores/core.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export const useCoreStore = defineStore('core', () => {
2222
const isResourceFetching = ref(false);
2323
const isInternetError = ref(false);
2424
const screenWidth = ref(window.innerWidth);
25+
const listRecordIds: Ref<any[]> = ref([]);
26+
const listResourceId: Ref<string | null> = ref(null);
27+
const listFilters: Ref<any[]> = ref([]);
28+
const listSort: Ref<any[]> = ref([]);
29+
const listPage: Ref<number> = ref(0);
30+
const listPageSize: Ref<number> = ref(0);
2531

2632
onMounted(() => {
2733
window.addEventListener('resize', updateWidth);
@@ -214,6 +220,10 @@ export const useCoreStore = defineStore('core', () => {
214220
resourceId,
215221
}
216222
});
223+
if (!res) {
224+
isResourceFetching.value = false;
225+
return;
226+
}
217227
if (res.error) {
218228
resourceColumnsError.value = res.error;
219229
} else {
@@ -290,5 +300,11 @@ export const useCoreStore = defineStore('core', () => {
290300
isIos,
291301
isInternetError,
292302
isMobile,
303+
listRecordIds,
304+
listResourceId,
305+
listFilters,
306+
listSort,
307+
listPage,
308+
listPageSize,
293309
}
294310
})

adminforth/spa/src/utils/listUtils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@ export async function getList(resource: AdminForthResourceFrontend, isPageLoaded
4747
return row;
4848
});
4949
totalRows = data.total;
50-
50+
const recordIds = data.recordIds || [];
51+
5152
// if checkboxes have items which are not in current data, remove them
5253
checkboxes.value = checkboxes.value.filter((pk: any) => rows.some((r: any) => r._primaryKeyValue === pk));
5354
await nextTick();
54-
return { rows, totalRows };
55+
return { rows, totalRows, recordIds };
5556
}
5657

5758

adminforth/spa/src/utils/utils.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,11 +641,21 @@ export function checkShowIf(c: AdminForthResourceColumnInputCommon, record: Reco
641641
}
642642

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

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

651661
export function compareOldAndNewRecord(oldRecord: Record<string, any>, newRecord: Record<string, any>): {ok: boolean, changedFields: Record<string, {oldValue: any, newValue: any}>} {

adminforth/spa/src/views/CreateView.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
7777
import ResourceForm from '@/components/ResourceForm.vue';
7878
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
7979
import { useCoreStore } from '@/stores/core';
80-
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, onBeforeRouteLeaveCreateEditViewGuard, leaveGuardActiveClass, formatComponent } from '@/utils';
80+
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, onBeforeRouteLeaveCreateEditViewGuard, leaveGuardActiveClass, formatComponent, atob_function } from '@/utils';
8181
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
8282
import { onMounted, onBeforeMount, onBeforeUnmount, ref } from 'vue';
8383
import { useRoute, useRouter } from 'vue-router';
@@ -177,14 +177,14 @@ onMounted(async () => {
177177
if (userUseMultipleEncoding) {
178178
initialValues.value = { ...initialValues.value, ...JSON.parse(decodeURIComponent((route.query.values as string))) };
179179
} else {
180-
initialValues.value = { ...initialValues.value, ...JSON.parse(atob(route.query.values as string)) };
180+
initialValues.value = { ...initialValues.value, ...JSON.parse(atob_function(route.query.values as string)) };
181181
}
182182
}
183183
if (route.query.readonlyColumns) {
184184
if (userUseMultipleEncoding) {
185185
readonlyColumns.value = JSON.parse(decodeURIComponent((route.query.readonlyColumns as string)));
186186
} else {
187-
readonlyColumns.value = JSON.parse(atob(route.query.readonlyColumns as string));
187+
readonlyColumns.value = JSON.parse(atob_function(route.query.readonlyColumns as string));
188188
}
189189
}
190190
record.value = initialValues.value;

0 commit comments

Comments
 (0)