Skip to content
Merged

Next #80

Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -155,6 +155,34 @@ For example we can prevent the user to see Apartments created by other users. Su
This hook will prevent the user to see Apartments created by other users in list, however if user will be able to discover
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.

### Modify record after it is returned from database

You can also change resource data after it was loaded.

For example, you can change the way columns value is displayed by changing the value itself:

```ts title='./resources/apartments.ts'
{
...
hooks: {
list: {
//diff-add
afterDatasourceResponse: async ({ response }: { response: any }) => {
//diff-add
response.forEach((r: any) => {
//diff-add
r.price = `$${r.price}`;
//diff-add
});
//diff-add
return { ok: true, error: "" };
//diff-add
},
},
},
}
```

### Dropdown list of foreignResource

By default if there is `foreignResource` like we use for demo on `realtor_id` column, the filter will suggest a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,76 @@ For example if fetch to the API fails you might want to show an error message to

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

To see an example of alerts, you can call them yourself.

## Alerts

To show an alert use `adminforth.alert` method:

```ts
import adminforth from '@/adminforth';

adminforth.alert({message: 'Hello world', variant: 'success'})
```

Next variants are supported:

* `success`
* `danger`
* `warning`
* `info`

## Confirmations

To show a confirmation dialog use `adminforth.confirm` method:

```ts
import adminforth from '@/adminforth';

const isConfirmed = await adminforth.confirm({message: 'Are you sure?', yes: 'Yes', no: 'No'})
```

## Ussage example

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

```html title="./custom/Alerts.vue"
<template>
<div class="ml-3 mt-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>
<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>
<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>
<Button @click="successAlert($t('Example success alert'))" >
{{$t('Call success alert')}}
</Button>

<Button @click="warningAlert($t('Example danger alert'))" >
{{$t('Call warning alert')}}
</Button>

<Button @click="doConfirm" >
{{$t('Call confirm dialog')}}
</Button>
</div>
</template>
<script setup>
import adminforth from '@/adminforth';
import { Button } from '@/afcl'
import { useI18n } from 'vue-i18n';

const { t } = useI18n();

function callAlert(message,variant='success'){
adminforth.alert({message: message, variant: variant})
function successAlert(message) {
adminforth.alert({message, variant: 'success'})
};

function warningAlert(message) {
adminforth.alert({message, variant: 'warning'})
};
async function callConfirmation(){

async function doConfirm() {
const isConfirmed = await adminforth.confirm({message: t('Are you sure?'), yes: t('Yes'), no: t('No')})
if (isConfirmed){
adminforth.alert({message: t('Confirmed'), variant: 'success'})
} else {
adminforth.alert({message: t('Not confirmed'), variant: 'warning'})
}
}
</script>
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,48 @@ function openCustomPage() {
Also there are:

* `config.customization.globalInjections.header`
* `config.customization.globalInjections.sidebar`
* `config.customization.globalInjections.sidebar`
* `config.customization.globalInjections.everyPageBottom`

Unlike `userMenu`, `header` and `sidebar` injections, `everyPageBottom` will be added to the bottom of every page even when user is not logged in.
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.

```ts title="/index.ts"
{
...
customization: {
globalInjections: {
userMenu: [
'@@/CustomUserMenuItem.vue',
//diff-remove
]
//diff-add
],
//diff-add
everyPageBottom: [
//diff-add
'@@/AnyPageWelcome.vue',
//diff-add
]
}
}
...
}
```

Now create file `AnyPageWelcome.vue` in the `custom` folder of your project:

```html title="./custom/AnyPageWelcome.vue"
<template></template>

<script setup>
import { onMounted } from 'vue';
import adminforth from '@/adminforth';
onMounted(() => {
adminforth.alert({
message: 'Welcome!',
variant: 'success',
});
});
</script>
```
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,84 @@ export default {
],
```

### Filling an array of values

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.

```typescript title="./resources/users.ts"
export default {
name: 'users',
columns: [
...
{
name: "room_sizes",
type: AdminForthDataTypes.JSON,
//diff-add
isArray: {
//diff-add
enabled: true,
//diff-add
itemType: AdminForthDataTypes.FLOAT,
//diff-add
},
},
],
},
...
],
```

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.

`itemType` value can be any of `AdminForthDataTypes` except `JSON` and `RICHTEXT`.

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:

```typescript title="./resources/users.ts"
export default {
name: 'users',
columns: [
...
{
name: "room_sizes",
type: AdminForthDataTypes.JSON,
isArray: {
enabled: true,
itemType: AdminForthDataTypes.FLOAT,
//diff-add
allowDuplicateItems: true,
},
},
],
},
...
],
```

All validation rules, such as `minValue`, `maxValue`, `minLength`, `maxLength` and `validation` will be applied not to array itself but instead to each item.

Note: array columns can not be marked as `masked`, be a `primaryKey` and at the time can not be linked to a foreign resource.


### Foreign resources

[Documentation in progress]
When you want to create a connection between two resources, you need to add `foreignResource` to a column, like so:

```typescript title="./resources/users.ts"
export default {
name: 'users',
columns: [
...
{
name: "realtor_id",
foreignResource: {
resourceId: 'users',
},
},
],
},
...
],
```

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.
91 changes: 55 additions & 36 deletions adminforth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,55 @@ class AdminForth implements IAdminForth {
});
}

validateRecordValues(resource: AdminForthResource, record: any): any {
// check if record with validation is valid
for (const column of resource.columns.filter((col) => col.name in record && col.validation)) {
let error = null;
if (column.isArray?.enabled) {
error = record[column.name].reduce((err, item) => {
return err || AdminForth.Utils.applyRegexValidation(item, column.validation);
}, null);
} else {
error = AdminForth.Utils.applyRegexValidation(record[column.name], column.validation);
}
if (error) {
return error;
}
}

// check if record with minValue or maxValue is within limits
for (const column of resource.columns.filter((col) => col.name in record
&& ['integer', 'decimal', 'float'].includes(col.isArray?.enabled ? col.isArray.itemType : col.type)
&& (col.minValue !== undefined || col.maxValue !== undefined))) {
if (column.isArray?.enabled) {
const error = record[column.name].reduce((err, item) => {
if (err) return err;

if (column.minValue !== undefined && item < column.minValue) {
return `Value in "${column.name}" must be greater than ${column.minValue}`;
}
if (column.maxValue !== undefined && item > column.maxValue) {
return `Value in "${column.name}" must be less than ${column.maxValue}`;
}

return null;
}, null);
if (error) {
return error;
}
} else {
if (column.minValue !== undefined && record[column.name] < column.minValue) {
return `Value in "${column.name}" must be greater than ${column.minValue}`;
}
if (column.maxValue !== undefined && record[column.name] > column.maxValue) {
return `Value in "${column.name}" must be less than ${column.maxValue}`;
}
}
}

return null;
}


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

// check if record with validation is valid
for (const column of resource.columns.filter((col) => col.name in record && col.validation)) {
const error = AdminForth.Utils.applyRegexValidation(record[column.name], column.validation);
if (error) {
return { error };
}
}

// check if record with minValue or maxValue is within limits
for (const column of resource.columns.filter((col) => col.name in record
&& ['integer', 'decimal', 'float'].includes(col.type)
&& (col.minValue !== undefined || col.maxValue !== undefined))) {
if (column.minValue !== undefined && record[column.name] < column.minValue) {
return { error: `Value in "${column.name}" must be greater than ${column.minValue}` };
}
if (column.maxValue !== undefined && record[column.name] > column.maxValue) {
return { error: `Value in "${column.name}" must be less than ${column.maxValue}` };
}
const err = this.validateRecordValues(resource, record);
if (err) {
return { error: err };
}

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

// check if record with validation is valid
for (const column of resource.columns.filter((col) => col.name in record && col.validation)) {
const error = AdminForth.Utils.applyRegexValidation(record[column.name], column.validation);
if (error) {
return { error };
}
}

// check if record with minValue or maxValue is within limits
for (const column of resource.columns.filter((col) => col.name in record
&& ['integer', 'decimal', 'float'].includes(col.type)
&& (col.minValue !== undefined || col.maxValue !== undefined))) {
if (column.minValue !== undefined && record[column.name] < column.minValue) {
return { error: `Value in "${column.name}" must be greater than ${column.minValue}` };
}
if (column.maxValue !== undefined && record[column.name] > column.maxValue) {
return { error: `Value in "${column.name}" must be less than ${column.maxValue}` };
}
const err = this.validateRecordValues(resource, record);
if (err) {
return { error: err };
}

// remove editReadonly columns from record
Expand Down
Loading