Skip to content

Commit edce85f

Browse files
committed
Merge branch 'main' of github.com:devforth/adminforth
2 parents 9e05218 + eb8cc19 commit edce85f

File tree

24 files changed

+762
-230
lines changed

24 files changed

+762
-230
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,16 @@ npx adminforth create-app
3939

4040
<a href="https://adminforth.dev/docs/tutorial/Customization/customPages">Custom Dashboard</a>
4141
<br/>
42-
<img src="https://github.com/user-attachments/assets/aa899196-f7f3-4582-839c-2267f2e9e197" alt="AdminForth Dashboard demo" width="500px" />
42+
<img src="https://github.com/user-attachments/assets/aa899196-f7f3-4582-839c-2267f2e9e197" alt="AdminForth Dashboard demo" width="80%" />
43+
4344
<a href="https://adminforth.dev/docs/tutorial/Plugins/chat-gpt">Chat-GPT plugin</a>
4445
<br/>
45-
<img src="https://github.com/user-attachments/assets/cfa17cbd-3a53-4725-ab46-53c7c7666028" alt="AdminForth ChatGPT demo" width="500px" />
46+
47+
<img src="https://github.com/user-attachments/assets/cfa17cbd-3a53-4725-ab46-53c7c7666028" alt="AdminForth ChatGPT demo" width="80%" />
48+
4649
<a href="https://adminforth.dev/docs/tutorial/Plugins/upload/#image-generation">Image DALEE Generation</a>
4750
<br/>
48-
<img src="https://github.com/user-attachments/assets/b923e044-7e29-46ff-ab91-eeca5eee2b0a" alt="AdminForth DALE-E image generator demo" width="500px">
51+
<img src="https://github.com/user-attachments/assets/b923e044-7e29-46ff-ab91-eeca5eee2b0a" alt="AdminForth DALE-E image generator demo" width="80%">
4952

5053

5154

adminforth/documentation/docs/tutorial/03-Customization/04-hooks.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,34 @@ For example we can prevent the user to see Apartments created by other users. Su
155155
This hook will prevent the user to see Apartments created by other users in list, however if user will be able to discover
156156
the apartment id, he will be able to use show page to see the apartment details, that is why separate limiting for show page is required as well. Below we will discover how to limit access to show page.
157157

158+
### Modify record after it is returned from database
159+
160+
You can also change resource data after it was loaded.
161+
162+
For example, you can change the way columns value is displayed by changing the value itself:
163+
164+
```ts title='./resources/apartments.ts'
165+
{
166+
...
167+
hooks: {
168+
list: {
169+
//diff-add
170+
afterDatasourceResponse: async ({ response }: { response: any }) => {
171+
//diff-add
172+
response.forEach((r: any) => {
173+
//diff-add
174+
r.price = `$${r.price}`;
175+
//diff-add
176+
});
177+
//diff-add
178+
return { ok: true, error: "" };
179+
//diff-add
180+
},
181+
},
182+
},
183+
}
184+
```
185+
158186
### Dropdown list of foreignResource
159187

160188
By default if there is `foreignResource` like we use for demo on `realtor_id` column, the filter will suggest a

adminforth/documentation/docs/tutorial/03-Customization/07-alert.md

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,76 @@ For example if fetch to the API fails you might want to show an error message to
66

77
AdminForth has very simple [frontend API](/docs/api/FrontendAPI/interfaces/FrontendAPIInterface) for this.
88

9-
To see an example of alerts, you can call them yourself.
9+
10+
## Alerts
11+
12+
To show an alert use `adminforth.alert` method:
13+
14+
```ts
15+
import adminforth from '@/adminforth';
16+
17+
adminforth.alert({message: 'Hello world', variant: 'success'})
18+
```
19+
20+
Next variants are supported:
21+
22+
* `success`
23+
* `danger`
24+
* `warning`
25+
* `info`
26+
27+
## Confirmations
28+
29+
To show a confirmation dialog use `adminforth.confirm` method:
30+
31+
```ts
32+
import adminforth from '@/adminforth';
33+
34+
const isConfirmed = await adminforth.confirm({message: 'Are you sure?', yes: 'Yes', no: 'No'})
35+
```
36+
37+
## Ussage example
1038

1139
Create a Vue component in the custom directory of your project, e.g. Alerts.vue:
1240

1341
```html title="./custom/Alerts.vue"
1442
<template>
1543
<div class="ml-3 mt-16">
16-
<button @click="callAlert($t('Example success alert'))" class="focus:outline-none text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800">{{$t('Call alert')}}</button>
17-
<button @click="callConfirmation" class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">{{$t('Confirmation')}}</button>
18-
<button @click="callAlert($t('Example danger alert'),'warning')" class="focus:outline-none text-white bg-orange-500 hover:bg-orange-400 focus:ring-4 focus:ring-orange-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-orange-600 dark:hover:bg-orange-700 dark:focus:ring-orange-900">{{$t('Danger alert')}}</button>
44+
<Button @click="successAlert($t('Example success alert'))" >
45+
{{$t('Call success alert')}}
46+
</Button>
47+
48+
<Button @click="warningAlert($t('Example danger alert'))" >
49+
{{$t('Call warning alert')}}
50+
</Button>
51+
52+
<Button @click="doConfirm" >
53+
{{$t('Call confirm dialog')}}
54+
</Button>
1955
</div>
2056
</template>
2157
<script setup>
2258
import adminforth from '@/adminforth';
59+
import { Button } from '@/afcl'
2360
import { useI18n } from 'vue-i18n';
61+
2462
const { t } = useI18n();
2563
26-
function callAlert(message,variant='success'){
27-
adminforth.alert({message: message, variant: variant})
64+
function successAlert(message) {
65+
adminforth.alert({message, variant: 'success'})
66+
};
67+
68+
function warningAlert(message) {
69+
adminforth.alert({message, variant: 'warning'})
2870
};
29-
async function callConfirmation(){
71+
72+
async function doConfirm() {
3073
const isConfirmed = await adminforth.confirm({message: t('Are you sure?'), yes: t('Yes'), no: t('No')})
74+
if (isConfirmed){
75+
adminforth.alert({message: t('Confirmed'), variant: 'success'})
76+
} else {
77+
adminforth.alert({message: t('Not confirmed'), variant: 'warning'})
78+
}
3179
}
3280
</script>
3381
```

adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,4 +430,48 @@ function openCustomPage() {
430430
Also there are:
431431
432432
* `config.customization.globalInjections.header`
433-
* `config.customization.globalInjections.sidebar`
433+
* `config.customization.globalInjections.sidebar`
434+
* `config.customization.globalInjections.everyPageBottom`
435+
436+
Unlike `userMenu`, `header` and `sidebar` injections, `everyPageBottom` will be added to the bottom of every page even when user is not logged in.
437+
You can use it to execute some piece of code when any page is loaded. For example, you can add welcoming pop up when user visits a page.
438+
439+
```ts title="/index.ts"
440+
{
441+
...
442+
customization: {
443+
globalInjections: {
444+
userMenu: [
445+
'@@/CustomUserMenuItem.vue',
446+
//diff-remove
447+
]
448+
//diff-add
449+
],
450+
//diff-add
451+
everyPageBottom: [
452+
//diff-add
453+
'@@/AnyPageWelcome.vue',
454+
//diff-add
455+
]
456+
}
457+
}
458+
...
459+
}
460+
```
461+
462+
Now create file `AnyPageWelcome.vue` in the `custom` folder of your project:
463+
464+
```html title="./custom/AnyPageWelcome.vue"
465+
<template></template>
466+
467+
<script setup>
468+
import { onMounted } from 'vue';
469+
import adminforth from '@/adminforth';
470+
onMounted(() => {
471+
adminforth.alert({
472+
message: 'Welcome!',
473+
variant: 'success',
474+
});
475+
});
476+
</script>
477+
```

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

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,84 @@ export default {
389389
],
390390
```
391391

392+
### Filling an array of values
393+
394+
Whenever you want to have a column to store not a single value but an array of values you have to set column as `AdminForthDataTypes.JSON`. This way when you are creating or editing a record you can type in a JSON array into a textfield. To simplify this process and allow you to create and edit separate items you can add `isArray` to a column.
395+
396+
```typescript title="./resources/users.ts"
397+
export default {
398+
name: 'users',
399+
columns: [
400+
...
401+
{
402+
name: "room_sizes",
403+
type: AdminForthDataTypes.JSON,
404+
//diff-add
405+
isArray: {
406+
//diff-add
407+
enabled: true,
408+
//diff-add
409+
itemType: AdminForthDataTypes.FLOAT,
410+
//diff-add
411+
},
412+
},
413+
],
414+
},
415+
...
416+
],
417+
```
418+
419+
Doing so, will result in UI displaying each item of the array as a separate input corresponding to `isArray.itemType` on create and edit pages.
420+
421+
`itemType` value can be any of `AdminForthDataTypes` except `JSON` and `RICHTEXT`.
422+
423+
By default it is forbidden to store duplicate values in an array column. To change that you can add `allowDuplicateItems: true` to `isArray`, like so:
424+
425+
```typescript title="./resources/users.ts"
426+
export default {
427+
name: 'users',
428+
columns: [
429+
...
430+
{
431+
name: "room_sizes",
432+
type: AdminForthDataTypes.JSON,
433+
isArray: {
434+
enabled: true,
435+
itemType: AdminForthDataTypes.FLOAT,
436+
//diff-add
437+
allowDuplicateItems: true,
438+
},
439+
},
440+
],
441+
},
442+
...
443+
],
444+
```
445+
446+
All validation rules, such as `minValue`, `maxValue`, `minLength`, `maxLength` and `validation` will be applied not to array itself but instead to each item.
447+
448+
Note: array columns can not be marked as `masked`, be a `primaryKey` and at the time can not be linked to a foreign resource.
449+
450+
392451
### Foreign resources
393452

394-
[Documentation in progress]
453+
When you want to create a connection between two resources, you need to add `foreignResource` to a column, like so:
454+
455+
```typescript title="./resources/users.ts"
456+
export default {
457+
name: 'users',
458+
columns: [
459+
...
460+
{
461+
name: "realtor_id",
462+
foreignResource: {
463+
resourceId: 'users',
464+
},
465+
},
466+
],
467+
},
468+
...
469+
],
470+
```
471+
472+
This way, when creating or editing a record you will be able to choose value for this field from a dropdown selector and on list and show pages this field will be displayed as a link to a foreign resource.

adminforth/index.ts

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,55 @@ class AdminForth implements IAdminForth {
230230
});
231231
}
232232

233+
validateRecordValues(resource: AdminForthResource, record: any): any {
234+
// check if record with validation is valid
235+
for (const column of resource.columns.filter((col) => col.name in record && col.validation)) {
236+
let error = null;
237+
if (column.isArray?.enabled) {
238+
error = record[column.name].reduce((err, item) => {
239+
return err || AdminForth.Utils.applyRegexValidation(item, column.validation);
240+
}, null);
241+
} else {
242+
error = AdminForth.Utils.applyRegexValidation(record[column.name], column.validation);
243+
}
244+
if (error) {
245+
return error;
246+
}
247+
}
248+
249+
// check if record with minValue or maxValue is within limits
250+
for (const column of resource.columns.filter((col) => col.name in record
251+
&& ['integer', 'decimal', 'float'].includes(col.isArray?.enabled ? col.isArray.itemType : col.type)
252+
&& (col.minValue !== undefined || col.maxValue !== undefined))) {
253+
if (column.isArray?.enabled) {
254+
const error = record[column.name].reduce((err, item) => {
255+
if (err) return err;
256+
257+
if (column.minValue !== undefined && item < column.minValue) {
258+
return `Value in "${column.name}" must be greater than ${column.minValue}`;
259+
}
260+
if (column.maxValue !== undefined && item > column.maxValue) {
261+
return `Value in "${column.name}" must be less than ${column.maxValue}`;
262+
}
263+
264+
return null;
265+
}, null);
266+
if (error) {
267+
return error;
268+
}
269+
} else {
270+
if (column.minValue !== undefined && record[column.name] < column.minValue) {
271+
return `Value in "${column.name}" must be greater than ${column.minValue}`;
272+
}
273+
if (column.maxValue !== undefined && record[column.name] > column.maxValue) {
274+
return `Value in "${column.name}" must be less than ${column.maxValue}`;
275+
}
276+
}
277+
}
278+
279+
return null;
280+
}
281+
233282

234283
async discoverDatabases() {
235284
this.statuses.dbDiscover = 'running';
@@ -350,24 +399,9 @@ class AdminForth implements IAdminForth {
350399
{ resource: AdminForthResource, record: any, adminUser: AdminUser, extra?: HttpExtra }
351400
): Promise<{ error?: string, createdRecord?: any }> {
352401

353-
// check if record with validation is valid
354-
for (const column of resource.columns.filter((col) => col.name in record && col.validation)) {
355-
const error = AdminForth.Utils.applyRegexValidation(record[column.name], column.validation);
356-
if (error) {
357-
return { error };
358-
}
359-
}
360-
361-
// check if record with minValue or maxValue is within limits
362-
for (const column of resource.columns.filter((col) => col.name in record
363-
&& ['integer', 'decimal', 'float'].includes(col.type)
364-
&& (col.minValue !== undefined || col.maxValue !== undefined))) {
365-
if (column.minValue !== undefined && record[column.name] < column.minValue) {
366-
return { error: `Value in "${column.name}" must be greater than ${column.minValue}` };
367-
}
368-
if (column.maxValue !== undefined && record[column.name] > column.maxValue) {
369-
return { error: `Value in "${column.name}" must be less than ${column.maxValue}` };
370-
}
402+
const err = this.validateRecordValues(resource, record);
403+
if (err) {
404+
return { error: err };
371405
}
372406

373407
// execute hook if needed
@@ -435,24 +469,9 @@ class AdminForth implements IAdminForth {
435469
{ resource: AdminForthResource, recordId: any, record: any, oldRecord: any, adminUser: AdminUser, extra?: HttpExtra }
436470
): Promise<{ error?: string }> {
437471

438-
// check if record with validation is valid
439-
for (const column of resource.columns.filter((col) => col.name in record && col.validation)) {
440-
const error = AdminForth.Utils.applyRegexValidation(record[column.name], column.validation);
441-
if (error) {
442-
return { error };
443-
}
444-
}
445-
446-
// check if record with minValue or maxValue is within limits
447-
for (const column of resource.columns.filter((col) => col.name in record
448-
&& ['integer', 'decimal', 'float'].includes(col.type)
449-
&& (col.minValue !== undefined || col.maxValue !== undefined))) {
450-
if (column.minValue !== undefined && record[column.name] < column.minValue) {
451-
return { error: `Value in "${column.name}" must be greater than ${column.minValue}` };
452-
}
453-
if (column.maxValue !== undefined && record[column.name] > column.maxValue) {
454-
return { error: `Value in "${column.name}" must be less than ${column.maxValue}` };
455-
}
472+
const err = this.validateRecordValues(resource, record);
473+
if (err) {
474+
return { error: err };
456475
}
457476

458477
// remove editReadonly columns from record

0 commit comments

Comments
 (0)