Skip to content

Commit 83bea9f

Browse files
committed
Merge branch 'next'
2 parents 638caf6 + 318a11c commit 83bea9f

11 files changed

Lines changed: 117 additions & 36 deletions

File tree

adminforth/documentation/src/pages/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import HomepageFeatures from '@site/src/components/HomepageFeatures';
66
import Heading from '@theme/Heading';
77
import styles from './index.module.css';
88

9-
const LIVE_DEMO_IFRAME_URL = `https://demo.adminforth.dev/overview?autologin=${encodeURIComponent('demo@adminfoth.dev:demo')}`;
9+
const LIVE_DEMO_IFRAME_URL = `https://demo.adminforth.dev/overview?autologin=${encodeURIComponent('demo@adminfoth.dev:demo')}&embedZoom=0.7`;
1010

1111

1212
const images = [
@@ -140,7 +140,7 @@ function HomepageHeader() {
140140
</Heading>
141141
<p className="hero__subtitle">{siteConfig.tagline}</p>
142142

143-
<div class="heroRow">
143+
<div className="heroRow">
144144
<div className={styles.buttons}>
145145
<Link
146146
className="button button--secondary button--outline button--lg"

adminforth/modules/configValidator.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {
2-
AdminForthConfig,
3-
AdminForthResource,
4-
IAdminForth, IConfigValidator,
1+
import {
2+
AdminForthConfig,
3+
AdminForthResource,
4+
IAdminForth, IConfigValidator,
55
AdminForthBulkAction,
6+
AdminForthActionInput,
67
AdminForthInputConfig,
78
AdminForthConfigCustomization,
89
AdminForthResourceInput,
@@ -388,7 +389,7 @@ export default class ConfigValidator implements IConfigValidator {
388389
});
389390
}
390391

391-
validateAndNormalizeCustomActions(resInput: AdminForthResourceInput, res: Partial<AdminForthResource>, errors: string[]): any[] {
392+
validateAndNormalizeCustomActions(resInput: AdminForthResourceInput, res: Partial<AdminForthResource>, errors: string[]): AdminForthActionInput[] {
392393
if (!resInput.options?.actions) {
393394
return [];
394395
}
@@ -430,13 +431,18 @@ export default class ConfigValidator implements IConfigValidator {
430431
action.showIn.showThreeDotsMenu = action.showIn.showThreeDotsMenu ?? false;
431432
}
432433

434+
if (typeof action.allowed === 'boolean') {
435+
const val = action.allowed;
436+
action.allowed = () => val;
437+
}
438+
433439
const shownInNonBulk = action.showIn.list || action.showIn.listThreeDotsMenu || action.showIn.showButton || action.showIn.showThreeDotsMenu;
434440
if (shownInNonBulk && !action.action && !action.url) {
435441
errors.push(`Resource "${res.resourceId}" action "${action.name}" has showIn enabled for non-bulk locations (list, listThreeDotsMenu, showButton, showThreeDotsMenu) but has no "action" or "url" handler. Either add an "action" handler or set those showIn flags to false.`);
436442
}
437443
});
438444

439-
return actions;
445+
return actions as AdminForthActionInput[];
440446
}
441447

442448
validateAndNormalizeResources(errors: string[], warnings: string[]): AdminForthResource[] {

adminforth/modules/restApi.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import { afLogger } from "./logger.js";
2323
import { ADMINFORTH_VERSION, listify, md5hash, getLoginPromptHTML, hookResponseError } from './utils.js';
2424

2525
import AdminForthAuth from "../auth.js";
26-
import { ActionCheckSource, AdminForthConfigMenuItem, AdminForthDataTypes, AdminForthFilterOperators, AdminForthResourceColumnInputCommon, AdminForthResourceFrontend, AdminForthResourcePages,
26+
import { ActionCheckSource, AdminForthActionFront, AdminForthConfigMenuItem, AdminForthDataTypes, AdminForthFilterOperators, AdminForthResourceColumnInputCommon, AdminForthResourceFrontend, AdminForthResourcePages,
2727
AdminForthSortDirections,
28-
AdminUser, AllowedActionsEnum, AllowedActionsResolved,
28+
AdminUser, AllowedActionsEnum, AllowedActionsResolved,
2929
AnnouncementBadgeResponse,
3030
GetBaseConfigResponse,
3131
ShowInResolved} from "../types/Common.js";
@@ -1079,6 +1079,22 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
10791079
})
10801080
);
10811081

1082+
const allowedCustomActions = [];
1083+
if (resource.options.actions) {
1084+
await Promise.all(
1085+
resource.options.actions.map(async (action) => {
1086+
if (typeof action.allowed === 'function') {
1087+
const res = await action.allowed({ adminUser, standardAllowedActions: allowedActions });
1088+
if (res) {
1089+
allowedCustomActions.push(action);
1090+
}
1091+
} else {
1092+
allowedCustomActions.push(action);
1093+
}
1094+
})
1095+
);
1096+
}
1097+
10821098
// translate
10831099
const translateRoutines: Record<string, Promise<string>> = {};
10841100
translateRoutines.resLabel = tr(resource.label, `resource.${resource.resourceId}`);
@@ -1191,12 +1207,10 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
11911207
confirm: action.confirm ? translated[`bulkActionConfirm${i}`] : action.confirm,
11921208
})
11931209
),
1194-
actions: resource.options.actions?.map((action) => ({
1195-
...action,
1196-
id: action.id!,
1197-
hasBulkHandler: !!action.bulkHandler,
1198-
bulkHandler: undefined,
1199-
})),
1210+
actions: allowedCustomActions.map(({ bulkHandler, allowed, action: actionFn, ...rest }) => ({
1211+
...rest,
1212+
...(bulkHandler && { bulkHandler: true }),
1213+
})) as AdminForthActionFront[],
12001214
allowedActions,
12011215
}
12021216
}
@@ -2096,7 +2110,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
20962110
if (!action) {
20972111
return { error: await tr(`Action {actionId} not found`, 'errors', { actionId }) };
20982112
}
2099-
if (action.allowed) {
2113+
if (typeof action.allowed === 'function') {
21002114
const execAllowed = await action.allowed({ adminUser, standardAllowedActions: allowedActions });
21012115
if (!execAllowed) {
21022116
return { error: await tr(`Action "{actionId}" not allowed`, 'errors', { actionId: action.name }) };
@@ -2148,7 +2162,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
21482162
if (!action.bulkHandler) {
21492163
return { error: await tr(`Action "{actionId}" has no bulkHandler`, 'errors', { actionId }) };
21502164
}
2151-
if (action.allowed) {
2165+
if (typeof action.allowed === 'function') {
21522166
const execAllowed = await action.allowed({ adminUser, standardAllowedActions: allowedActions });
21532167
if (!execAllowed) {
21542168
return { error: await tr(`Action "{actionId}" not allowed`, 'errors', { actionId: action.name }) };

adminforth/spa/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
<!-- /* IMPORTANT:ADMINFORTH HEAD */ -->
1818
</head>
1919
<body class="min-h-screen flex flex-col">
20+
<script>
21+
const embedZoom = Number(new URLSearchParams(window.location.search).get('embedZoom'));
22+
if (Number.isFinite(embedZoom) && embedZoom > 0) {
23+
document.body.style.zoom = String(embedZoom);
24+
}
25+
</script>
2026
<div id="app" class="grow bg-lightHtml dark:bg-darkHtml w-full"></div>
2127
<script type="module" src="/src/main.ts"></script>
2228
</body>

adminforth/spa/src/components/MenuLink.vue

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
<template>
2-
<RouterLink
3-
:to="item.url || { name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
2+
<component
3+
:is="isExternalUrl(item.url) ? 'a' : RouterLink"
4+
v-bind="isExternalUrl(item.url)
5+
? { href: item.url }
6+
: { to: item.url || { name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {} } }"
47
:target="item.isOpenInNewTab ? '_blank' : '_self'"
5-
class="af-menu-link flex group relative items-center w-full py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default transition-all duration-200 ease-in-out"
6-
:class="{
8+
class="af-menu-link flex group relative items-center w-full py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default transition-all duration-200 ease-in-out"
9+
:class="{
710
'hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover': !['divider', 'gap', 'heading'].includes(item.type),
811
'pl-6 pr-3.5': (isChild && !isSidebarIconOnly && !isSidebarHovering) || (isChild && isSidebarIconOnly && isSidebarHovering),
912
'px-3.5 ': !isChild || (isSidebarIconOnly && !isSidebarHovering),
@@ -51,7 +54,7 @@
5154
<div v-if="(item.badge || item.badge === 0) && isSidebarIconOnly && !isSidebarHovering" class="af-badge absolute right-0.5 bottom-1 -translate-y-1/2 inline-flex items-center justify-center h-2 w-2 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
5255
fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
5356
</div>
54-
</RouterLink>
57+
</component>
5558
</template>
5659

5760
<script setup lang="ts">
@@ -60,13 +63,15 @@ import { Tooltip } from '@/afcl';
6063
import { ref, watch, computed } from 'vue';
6164
import { useCoreStore } from '@/stores/core';
6265
import { IconFileImageOutline } from '@iconify-prerendered/vue-flowbite';
63-
import { useRoute } from 'vue-router';
66+
import { useRoute, RouterLink } from 'vue-router';
67+
68+
const isExternalUrl = (url: string ) => /^https?:\/\//.test(url || '');
6469
6570
const route = useRoute();
6671
6772
const isItemActive = (item: any) => {
6873
if (item.url) {
69-
return route.fullPath === item.url;
74+
return !isExternalUrl(item.url) && route.fullPath === item.url;
7075
}
7176
7277
if (item.resourceId) {

adminforth/spa/src/components/ThreeDotsMenu.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ import { useRoute, useRouter } from 'vue-router';
103103
import CallActionWrapper from '@/components/CallActionWrapper.vue'
104104
import { ref, type ComponentPublicInstance, onMounted, onUnmounted } from 'vue';
105105
import type { AdminForthActionFront, AdminForthBulkActionFront, AdminForthComponentDeclarationFull } from '@/types/Common';
106-
import type { AdminForthActionInput } from '@/types/Back';
107106
import { Spinner } from '@/afcl';
108107
109108
const { list, alert} = useAdminforth();
@@ -137,7 +136,7 @@ function setComponentRef(el: ComponentPublicInstance | null, index: number) {
137136
}
138137
}
139138
140-
async function handleActionClick(action: AdminForthActionInput, payload: any) {
139+
async function handleActionClick(action: AdminForthActionFront, payload: any) {
141140
list.closeThreeDotsDropdown();
142141
await executeCustomAction({
143142
actionId: action.id,

adminforth/spa/src/utils/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,7 @@ export async function executeCustomBulkAction({
891891
try {
892892
const action = resource?.options?.actions?.find((a: any) => a.id === actionId) as AdminForthActionFront | undefined;
893893

894-
if (action?.hasBulkHandler && action?.showIn?.bulkButton) {
894+
if (action?.bulkHandler && action?.showIn?.bulkButton) {
895895
const result = await callAdminForthApi({
896896
path: '/start_custom_bulk_action',
897897
method: 'POST',

adminforth/spa/src/views/ListView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ async function startCustomBulkActionInner(actionId: string | number) {
344344
const successResults = results.filter(r => r?.successMessage);
345345
if (successResults.length > 0) {
346346
alert({
347-
message: action?.bulkSuccessMessage ? action.bulkSuccessMessage : action?.hasBulkHandler ? successResults[0].successMessage : `${successResults.length} out of ${results.length} items processed successfully`,
347+
message: action?.bulkSuccessMessage ? action.bulkSuccessMessage : action?.bulkHandler ? successResults[0].successMessage : `${successResults.length} out of ${results.length} items processed successfully`,
348348
variant: 'success'
349349
});
350350
}

adminforth/types/Back.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,10 +1389,10 @@ export interface AdminForthActionInput {
13891389
showThreeDotsMenu?: boolean,
13901390
bulkButton?: boolean,
13911391
};
1392-
allowed?: (params: {
1392+
allowed?: boolean | ((params: {
13931393
adminUser: AdminUser;
13941394
standardAllowedActions: AllowedActions;
1395-
}) => boolean;
1395+
}) => boolean | Promise<boolean>);
13961396
url?: string;
13971397
bulkHandler?: (params: {
13981398
adminforth: IAdminForth;

adminforth/types/Common.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ export type FieldGroup = {
316316

317317
export interface AdminForthActionFront extends Omit<AdminForthActionInput, 'id' | 'bulkHandler' | 'action' | 'allowed'> {
318318
id: string;
319-
hasBulkHandler?: boolean;
319+
bulkHandler?: boolean;
320320
}
321321

322322
export interface AdminForthBulkActionFront extends Omit<AdminForthBulkActionCommon, 'id'> {
@@ -1177,6 +1177,23 @@ export interface AdminForthConfigMenuItem {
11771177
* Item id will be automatically generated from hashed resourceId+Path+label
11781178
*/
11791179
itemId?: string, // todo move to runtime type
1180+
1181+
1182+
/**
1183+
* If set, menu item will be rendered as external link with this URL. Supported for AdminForthMenuTypes.PAGE and AdminForthMenuTypes.RESOURCE only!
1184+
* If URL starts with `http://` or `https://`, it will be treated as absolute URL. Otherwise, it will be treated as relative to admin panel base URL.
1185+
* Example of absolute URL:
1186+
*
1187+
* ```ts
1188+
* url: 'https://google.com',
1189+
* ```
1190+
*
1191+
* Example of relative URL:
1192+
*
1193+
* ```ts
1194+
* url: '/custom-page',
1195+
* ```
1196+
*/
11801197

11811198
url?: string
11821199

0 commit comments

Comments
 (0)