Skip to main content

Standard pages tuning

Fields Grouping

In some cases, you may want to organize data fields into specific groups for better structure and clarity. For example, you could create a "Main Info" group to include columns like title, description, country, and apartment_image. Another group, "Characteristics," could hold attributes such as price, square_meter, number_of_rooms, and listed. Any values without a specified group will be categorized under "Other.

./resources/apartments.ts
export default {
...
options: {
...
fieldGroups: [
{
groupName: 'Main info',
columns: ['id','title', 'description', 'country', 'apartment_image']
},
{
groupName: 'Characteristics',
columns: ['price', 'square_meter', 'number_of_rooms', "listed"]
}
],
}
}

Here is how it looks: alt text

You can hide the group title by setting noTitle to true.

./resources/apartments.ts
export default {
...
options: {
...
fieldGroups: [
{
groupName: 'Main info',
columns: ['id','title', 'description', 'country', 'apartment_image']
noTitle: true,
},
{
groupName: 'Characteristics',
columns: ['price', 'square_meter', 'number_of_rooms', "listed"]
}
],
}
}

You can also specify on which page you want to create or delete groups. If you assign null, the groups will disappear from this page.

./resources/apartments.ts
export default {
...
options: {
createFieldGroups: [
{
groupName: 'Main info',
columns: ['id','title']
},
{
groupName: 'Characteristics',
columns: ['description', 'country', 'price', 'square_meter', 'number_of_rooms', "listed"]
}
],
editFieldGroups: null,
showFieldGroups: null,
}
}

List

Default Sorting

./resources/apartments.ts
import { AdminForthSortDirections } from 'adminforth';

...
export default {
resourceId: 'aparts',
options: {
defaultSort: {
columnName: 'created_at',
direction: AdminForthSortDirections.asc,
}
}
}

Page size

use options.listPageSize to define how many records will be shown on the page

./resources/apartments.ts
export default {
resourceId: 'aparts',
options: {
...
listPageSize: 10,
}
}
]

Custom row click action

By default, when you click on a record in the list view, the show view will be opened.

You can change this behavior by using options.listTableClickUrl.

To disable any action (don't open show) return null:

./resources/apartments.ts
export default {
resourceId: 'aparts',
options: {
...
listTableClickUrl: async (record, adminUser) => null,
}
}
]

To open a custom page, return URL to the custom page (can start with https://, or relative adminforth path):

./resources/apartments.ts
      options: {
...
listTableClickUrl: async (record, adminUser) => {
return `https://google.com/search?q=${record.name}`;
}
}

If you wish to open the page in a new tab, add target=_blank get param to the returned URL:

./resources/apartments.ts
      options: {
...
listTableClickUrl: async (record, adminUser) => {
return `https://google.com/search?q=${record.name}&target=_blank`;
}
}

Auto-refresh records

options.listRowsAutoRefreshSeconds might be used to silently refresh records that are loaded (no new records will be fetched if they appear)

./resources/apartments.ts
export default {
resourceId: 'aparts',
hooks: {
list: {
afterDatasourceResponse: async ({ response }: { response: any }) => {
response.forEach((r: any) => {
// substitute random country on any load
const countries = [ 'US', 'DE', 'FR', 'GB', 'NL', 'IT', 'ES', 'DK', 'PL', 'UA',
'CA', 'AU', 'BR', 'JP', 'CN', 'IN', 'KR', 'TR', 'MX', 'ID']
r.country = countries[Math.floor(Math.random() * countries.length)];
})
return { ok: true, error: "" }
}
}
},
options: {
...
listRowsAutoRefreshSeconds: 1,
}
}
]

alt text

Creating

Fill with default values

Sometimes you want to generate some field value without asking user to fill it. For example createdAt oftenly store time of creation of the record. You can do this by using fillOnCreate:

./resources/apartments.ts

export default {
name: 'apartments',
fields: [
...
{
name: 'created_at',
type: AdminForthDataTypes.DATETIME,
showIn: {
all: true,
create: false, // don't show field in create form
}
fillOnCreate: ({ initialRecord, adminUser }) => (new Date()).toISOString(),
},
],
},
...
],

Also you can assign adminUser ID by adminUser.dbUser.id:

./resources/apartments.ts
export default {
name: 'apartments',
fields: [
...
{
name: 'created_by',
type: AdminForthDataTypes.STRING,
showIn: {
all: true,
create: false, // don't show field in create form
}
fillOnCreate: ({ initialRecord, adminUser }) => adminUser.dbUser.id,
},
],
},
...
],

Same effect can be achieved by using hooks. But fillOnCreate might be shorter and more readable.

Suggest default value in create form

You can suggest a default value for a field in the create form which user can instantly change even before creating record. This might be used to give user some example value or to suggest some default value.

./resources/apartments.ts
export default {
name: 'apartments',
fields: [
...
{
name: 'description',
suggestOnCreate: 'Great apartment in the heart of the city',
},
],
},
...

A difference between fillOnCreate and suggestOnCreate:

  • fillOnCreate is called on the backend when the record is saved to a database. Value returned by fillOnCreate will be saved to the database.
  • suggestOnCreate is just a single value that will be substituted in create form. User can change it before saving the record.
  • fillOnCreate should be used when showIn.create is a false value because if it is true, the input will be shown in the create form but then(during actual save to db) it will be overwritten by the value returned by fillOnCreate.
  • suggestOnCreate should be used with showIn.create set to true because if it is not set, the input will not be shown in the create form and default suggestion will not make sense.

Sometimes you might need to create a link that will open the create form with some fields pre-filled. For example, you might want to create a link that will open the create form with the realtor_id field pre-filled with the current user's ID.

<template>
...
<LinkButton
:to="{
name: 'resource-create',
params: {
resourceId: 'aparts',
},
query: {
values: encodeURIComponent(JSON.stringify({
realtor_id: coreStore?.adminUser.dbUser.id
})),
},
}"
>
{{$t('Create new apartment')}}
</LinkButton>
...
</template>

<script setup lang="ts">
import { LinkButton } from '@afcl';
import { useCoreStore } from '@/stores/core';

const coreStore = useCoreStore();
</script>

Editing

You can set a column editReadonly so it will be shown in the edit form but will be disabled.
This might be useful to better identify the record during editing or to show some additional information that should not be changed but can help to edit the record.

./resources/apartments.ts
export default {
name: 'apartments',
fields: [
...
{
name: 'created_at',
type: AdminForthDataTypes.DATETIME,
editReadonly: true,
},
],
},
...
],

editReadonly is check enforced both on fronted and backend. So it is safe to use it to make sure that data will be never changes.

minValue and maxValue

You can add minValue and maxValue limits to columns, so it will show an error below an input when entered value is out of bounds.

./resources/apartments.ts
export default {
name: 'apartments',
columns: [
...
{
name: 'square_meter',
label: 'Square',
minValue: 3,
maxValue: 1000,
},
],
},
...
],

minValue and maxValue checks are enforced both on frontend and backend.

Validation

In cases when column values must follow certain format, you can add validation to it. validation is an array of rules, each containing regExp that defines a format for a value and message that will be displayed in case when entered value does not pass the check.

./resources/adminuser.ts
export default {
name: 'adminuser',
columns: [
...
{
name: 'email',
required: true,
isUnique: true,
validation: [
{
regExp: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
message: 'Email is not valid, must be in format example@test.com',
},
],
},
],
},
...
],

validation checks are enforced both on frontend and backend.

Input prefix and suffix

You can add prefix or suffix to inputs by adding inputPrefix or inputSuffix fields to a column.

./resources/users.ts
export default {
name: 'users',
columns: [
...
{
name: "price",
inputSuffix: "USD",
allowMinMaxQuery: true,
},
],
},
...
],

These fields can only be used with following AdminForthDataTypes: DECIMAL, FLOAT, INTEGER, STRING and JSON (only if JSON column is an array with appropriate itemType).

Editing note

You can add editingNote to a column to show a note below the input field.

./resources/adminuser.ts
export default {
name: 'adminuser',
columns: [
...
{
name: "password",
editingNote: { edit: "Leave empty to keep password unchanged" },
},
],
},
...
],

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.

./resources/adminuser.ts
export default {
name: 'adminuser',
columns: [
...
{
name: "room_sizes",
type: AdminForthDataTypes.JSON,
isArray: {
enabled: true,
itemType: AdminForthDataTypes.FLOAT,
},
},
],
},
...
],

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:

./resources/adminuser.ts
export default {
name: 'adminuser',
columns: [
...
{
name: "room_sizes",
type: AdminForthDataTypes.JSON,
isArray: {
enabled: true,
itemType: AdminForthDataTypes.FLOAT,
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

When you want to create a connection between two resources, you need to add foreignResource to a column, like so:

./resources/adminuser.ts
export default {
name: 'adminuser',
columns: [
...
{
name: "realtor_id",
foreignResource: {
resourceId: 'adminuser',
},
},
],
},
...
],

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.

Unset label

When foreign resource column is not required, selector will have an 'Unset' option that will set field to null. You can change label for this option using unsetLabel, like so:

./resources/apartments.ts
export default {
name: 'apartments',
columns: [
...
{
name: "realtor_id",
foreignResource: {
resourceId: 'adminuser',
unsetLabel: 'No realtor',
},
},
],
},
...
],

Polymorphic foreign resources

Sometimes it is needed for one column to be a foreign key for multiple tables. For example, given the following schema:

./schema.prisma
...
model apartments {
id String @id
created_at DateTime?
title String
square_meter Float?
price Decimal
number_of_rooms Int?
realtor_id String?
}

model houses {
id String @id
created_at DateTime?
title String
house_square_meter Float?
land_square_meter Float?
price Decimal
realtor_id String?
}

model sold_property {
id String @id
created_at DateTime?
title String
property_id String
realtor_id String?
}

Here, in sold_property table, column property_id can be a foreign key for both apartments and houses tables. If schema is set like this, the is no way to tell to what table exactly property_id links to. Also, if defined like usual, adminforth will link to only one of them. To make sure that property_id works as intended we need add one more column to sold_property and change the way foreign resource is defined in adminforth resource config.

./schema.prisma
...

model sold_property {
id String @id
created_at DateTime?
title String
property_type String
property_id String
realtor_id String?
}

property_type column will be used to store what table id in property_id refers to. And in adminforth config for sold_property table, when describing property_id column, foreign resource field should be defined as follows:

./resources/sold_property.ts
export default {
name: 'sold_property',
columns: [
...
{
name: "property_type",
showIn: { create: false, edit: false },
},
{
name: "property_id",
foreignResource: {
polymorphicResources: [
{
resourceId: 'apartments',
whenValue: 'apartment',
},
{
resourceId: 'houses',
whenValue: 'house',
},
],
polymorphicOn: 'property_type',
},
},
],
},
...
],

When defined like this, adminforth will use value in property_type to figure out to what table does id in property_id refers to and properly link them. When creating or editing a record, adminforth will figure out to what table new property_id links to and fill property_type on its own using corresponding whenValue. Note, that whenValue does not have to be the same as resourceId, it can be any string as long as they do not repeat withing polymorphicResources array. Also, since whenValue is a string, column designated as polymorphicOn must also be string. Another thing to note is that, polymorphicOn column (property_type in our case) must not be editable by user, so it must include both create and edit as false in showIn value. Even though, polymorphicOn column is no editable, it can be beneficial to set is as an enumerator. This will have two benefits: first, columns value displayed in table and show page can be changed to a desired one and second, when filtering on this column, user will only able to choose values provided for him.

If beforeDatasourceRequest or afterDatasourceResponse hooks are set for polymorphic foreign resource, they will be called for each resource in polymorphicResources array.

Filtering

Filter Options

You can specify the delay between filtering requests and filtering operator for a column using filterOptions field.

./resources/adminuser.ts
export default {
name: 'adminuser',
columns: [
...
{
name: "title",
required: true,
maxLength: 255,
minLength: 3,
filterOptions: {
debounceTimeMs: 500,
substringSearch: false,
},
},
],
},
...
],

debounceTimeMs field dictates how long (in milliseconds) to wait between inputs to send updated data request. By increasing this value, you can reduce the amount of requests set to backend. Default value for this field is set to 10ms. substringSearch sets what comparison operator to use for text field. By default this field is set to true, which results in using case-insensitive ILIKE operator, that will look for records that have filter string anywhere inside field value. Setting this substringSearch to false will result in using more strict EQ operator, that will look for exact full-string matches.