Skip to content

Commit e387e6f

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents e31cfa6 + 02c4026 commit e387e6f

7 files changed

Lines changed: 163 additions & 10 deletions

File tree

adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,12 @@ Instead of defining an `action` handler, you can specify a `url` that the user w
163163
}
164164
```
165165

166+
> ☝️ Note: You cannot specify both `action` and `url` for the same action - only one should be used.
167+
166168
The URL can be:
167169
- A relative path within your admin panel (starting with '/')
168170
- An absolute URL (starting with 'http://' or 'https://')
171+
- function which creates URL based on record fields
169172

170173
To open the URL in a new tab, append `target=_blank` as a query parameter. If the URL already has query parameters, use `&target=_blank`; otherwise use `?target=_blank`:
171174

@@ -181,7 +184,46 @@ To open the URL in a new tab, append `target=_blank` as a query parameter. If th
181184
}
182185
```
183186

184-
> ☝️ Note: You cannot specify both `action` and `url` for the same action - only one should be used.
187+
Example to generate dynamic URL:
188+
189+
```ts
190+
{
191+
name: 'View on Google',
192+
icon: 'flowbite:external-link-solid',
193+
url: async ({record, recordId, adminUser, resource }) => `https://google.com/search?q=Apartment ${record.title}`,
194+
showIn: {
195+
list: true,
196+
showButton: true
197+
}
198+
}
199+
```
200+
201+
> ☝️ Note: Though url function might be async we recommend to omit long awaits, or ideally don't use them at all, cause slow execution of this hook might be a subject of bottleneck for resource pages rendering. For built actions the async functions would be called in parallel to optimize loading speed.
202+
203+
204+
### Deep-level redirects.
205+
206+
Using `url` prop described above is recommended way to implementing URL navigation from actions (internal or external), because URLs are rendered into direct anchour tag and support all anchour features (like Open in new tab).
207+
208+
However, rearely you might also like to decide whether to redirect only after performing some logic (conditionally). This way is not recommended for most of cases, because it is not compatible with action native features (we can't know URL before executing action body):
209+
210+
211+
```ts
212+
{
213+
name: 'View on Google',
214+
icon: 'flowbite:external-link-solid',
215+
action: async ({ recordId }) => {
216+
if (await testSomething(recordId)) {
217+
return { ok: true, redirectUrl: 'https://google.com/search?q=apartment' };
218+
};
219+
return { ok: true, successMessage: 'Done' };
220+
},
221+
showIn: {
222+
list: true,
223+
showButton: true
224+
}
225+
}
226+
```
185227

186228
## Custom Component
187229

@@ -313,4 +355,4 @@ Backend handler: read the payload via `extra`.
313355

314356
Notes:
315357
- If you don’t emit a payload, the default behavior is used by the UI (e.g., in lists the current row context is used). When you do provide a payload, it will be forwarded to the backend as `extra` for your action handler.
316-
- You can combine default context with your own payload by merging before emitting, for example: `emit('callAction', { ...row, asListed: true })` if your component has access to the row object.
358+
- You can combine default context with your own payload by merging before emitting, for example: `emit('callAction', { ...row, asListed: true })` if your component has access to the row object.

adminforth/modules/restApi.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2238,6 +2238,11 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
22382238
if (!resource) {
22392239
return { error: await tr(`Resource {resourceId} not found`, 'errors', { resourceId }) };
22402240
}
2241+
2242+
const record = await this.adminforth.connectors[resource.dataSource].getRecordByPrimaryKey(resource, recordId);
2243+
if (!record){
2244+
return { error: `Record with ${recordId} not found` };
2245+
}
22412246
const { allowedActions } = await interpretResource(
22422247
adminUser,
22432248
resource,
@@ -2257,16 +2262,18 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
22572262
}
22582263

22592264
if (action.url) {
2265+
const redirectUrl = typeof action.url === 'function'
2266+
? await action.url({ record, recordId, adminUser, resource })
2267+
: action.url;
22602268
return {
22612269
actionId,
22622270
recordId,
2271+
record,
22632272
resourceId,
2264-
redirectUrl: action.url
2273+
redirectUrl,
22652274
}
22662275
}
2267-
22682276
const actionResponse = await action.action({ recordId, adminUser, resource, tr, adminforth: this.adminforth, response, extra: {...extra, cookies: cookies, headers: headers} });
2269-
22702277
return {
22712278
actionId,
22722279
recordId,

adminforth/spa/src/afcl/Link.vue

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,37 @@
11
<template>
2+
<a
3+
v-if="isExternal"
4+
v-bind="$attrs"
5+
:href="to"
6+
:target="target"
7+
rel="noopener noreferrer"
8+
:class="linkClasses"
9+
>
10+
<slot></slot>
11+
</a>
12+
213
<router-link
14+
v-else
315
v-bind="$attrs"
416
:to="to"
5-
class="afcl-link text-lightPrimary underline dark:text-darkPrimary hover:no-underline hover:brightness-110
6-
cursor-pointer"
17+
:target="target"
18+
:class="linkClasses"
719
>
820
<slot></slot>
921
</router-link>
1022
</template>
1123

1224
<script setup lang="ts">
25+
import { computed } from 'vue';
1326
14-
defineProps<{
27+
const props = defineProps<{
1528
to: string,
16-
}>()
29+
target?: 'blank' | 'self' | 'parent' | 'top'
30+
}>();
31+
32+
const isExternal = computed(() => {
33+
return typeof props.to === 'string' && props.to.startsWith('http');
34+
});
35+
36+
const linkClasses = "afcl-link text-lightPrimary underline dark:text-darkPrimary hover:no-underline hover:brightness-110 cursor-pointer";
1737
</script>

adminforth/spa/src/utils/listUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export async function startBulkAction(actionId: string, resource: AdminForthReso
6161
if (action?.confirm) {
6262
const confirmed = await confirm({
6363
title: action.confirm,
64+
message: t(`Deleting ${checkboxes.value.length} ${checkboxes.value.length === 1 ? 'item' : 'items'}. This process is irreversible.`),
6465
});
6566
if (!confirmed) {
6667
return;

adminforth/types/Back.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1424,7 +1424,7 @@ export interface AdminForthActionInput {
14241424
adminUser: AdminUser;
14251425
standardAllowedActions: AllowedActions;
14261426
}) => boolean | Promise<boolean>);
1427-
url?: string;
1427+
url?: string | ((params: { adminUser: AdminUser; resource: AdminForthResource; recordId: string, record: any }) => string);
14281428
bulkHandler?: (params: {
14291429
adminforth: IAdminForth;
14301430
resource: AdminForthResource;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
export type SpeechToTextInput = {
2+
buffer: Buffer;
3+
filename: string;
4+
mimeType: string;
5+
language?: string;
6+
prompt?: string;
7+
};
8+
9+
export type SpeechToTextResult = {
10+
text: string;
11+
language?: string;
12+
raw?: unknown;
13+
};
14+
15+
export interface SpeechToTextAdapter {
16+
name: string;
17+
18+
validate(): void;
19+
20+
transcribe(input: SpeechToTextInput): Promise<SpeechToTextResult>;
21+
}
22+
23+
export type TtsAudioFormat =
24+
| "mp3"
25+
| "opus"
26+
| "aac"
27+
| "flac"
28+
| "wav"
29+
| "pcm";
30+
31+
export type TextToSpeechInput<Voice extends string = string> = {
32+
text: string;
33+
voice?: Voice;
34+
format?: TtsAudioFormat;
35+
speed?: number;
36+
instructions?: string;
37+
stream?: false;
38+
};
39+
40+
export type TextToSpeechResult = {
41+
audio: Buffer;
42+
mimeType: string;
43+
format: TtsAudioFormat;
44+
raw?: unknown;
45+
};
46+
47+
export type TtsStreamFormat = "audio" | "sse";
48+
49+
export type TextToSpeechStreamInput<Voice extends string = string> =
50+
Omit<TextToSpeechInput<Voice>, "stream"> & {
51+
stream: true;
52+
streamFormat?: TtsStreamFormat;
53+
};
54+
55+
export type TextToSpeechStreamResult = {
56+
audioStream: ReadableStream<Uint8Array>;
57+
mimeType: string;
58+
format: TtsAudioFormat;
59+
streamFormat: TtsStreamFormat;
60+
raw?: unknown;
61+
};
62+
63+
export interface TextToSpeechAdapter<Voice extends string = string> {
64+
name: string;
65+
66+
validate(): void;
67+
68+
synthesize(input: TextToSpeechStreamInput<Voice>): Promise<TextToSpeechStreamResult>;
69+
synthesize(input: TextToSpeechInput<Voice>): Promise<TextToSpeechResult>;
70+
}
71+
72+
export type AudioAdapter<Voice extends string = string> =
73+
SpeechToTextAdapter & TextToSpeechAdapter<Voice>;

adminforth/types/adapters/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,13 @@ export type { ImageVisionAdapter } from './ImageVisionAdapter.js';
1515
export type { OAuth2Adapter } from './OAuth2Adapter.js';
1616
export type { StorageAdapter } from './StorageAdapter.js';
1717
export type { CaptchaAdapter } from './CaptchaAdapter.js';
18+
export type {
19+
AudioAdapter,
20+
SpeechToTextAdapter,
21+
SpeechToTextInput,
22+
SpeechToTextResult,
23+
TextToSpeechAdapter,
24+
TextToSpeechInput,
25+
TextToSpeechResult,
26+
TtsAudioFormat,
27+
} from './AudioAdapter.js';

0 commit comments

Comments
 (0)