Back to Blog

Building a Custom “Settings” Page with Admiral

In Admiral, creating a custom page with a form is just as straightforward as defining a CRUD interface. We’ll start by adding a new menu item — for example, Settings.
This feature is especially useful when you need to implement a standalone section that isn’t directly tied to a typical data entity – such as a user profile settings page, access control configuration, or application-wide preferences.

$menu->addItem(MenuItem::make(MenuKey::SETTINGS, 'Settings', 'FiSettings', '/settings'));
Next, inside the admin panel directory, create a components folder if it doesn’t already exist. Within it, add a new Settings directory and create a file named Settings.tsx.
Now let’s set up the basic structure of our file and walk through its initial implementation.

import React, {useCallback} from 'react'
import {Form, Page} from '@devfamily/admiral'
import api from '../../config/api'


const settingsUri = 'settings'


export default function Settings() {
   const fetchInitialData = useCallback(() => {
       return api.get(settingsUri)
   }, [])


   const _onSubmit = useCallback(async (values) => {
       return api.post(settingsUri, values)
   }, [])


   return (
       <Page title="Settings">
           <Form submitData={_onSubmit} fetchInitialData={fetchInitialData}>
               // Here will be our inputs
  
               <Form.Footer>
                   <Form.Submit>Save</Form.Submit>
               </Form.Footer>
           </Form>
       </Page>
   )
}
Let’s take a look at what our component returns. It renders a Page component along with a Form component from the devfamily/admiral package.
  • In the Page component, we’ve set the title property.
  • In the Form component, we’ve defined:
— the submitData prop, which triggers a callback function to send data to the server when the "Save" button is clicked;
— the fetchInitialData prop, which retrieves data from the server and populates the input fields.
Before we check how the page looks in the browser, let’s register it.
Navigate to the Pages directory and create a new settings folder with an index.tsx file inside.
In that file, we’ll define the following:

import Settings from '@/src/components/Settings/Settings'

export default Settings
Now open the browser where you should see our page rendered like this:
Now we can move on to defining the form inputs. We’ll add two fields:
  • A basic text field:
<TextInput name='min_version' label='Minimum App Version'/>
  • Selector currency, with options to be loaded from the server:
<SelectInput name=’default_currency’ label='Default Currency'/>
The resulting component with both inputs will look like this:

import React, {useCallback} from 'react'
import {Form, Page, SelectInput, TextInput} from '@devfamily/admiral'
import api from '../../config/api'


const settingsUri = 'settings'


export default function Settings() {
   const fetchInitialData = useCallback(() => {
       return api.get(settingsUri)
   }, [])


   const _onSubmit = useCallback(async (values) => {
       return api.post(settingsUri, values)
   }, [])


   return (
       <Page title="Settings">
           <Form submitData={_onSubmit} fetchInitialData={fetchInitialData}>


               <TextInput name='min_version' label='Minimum App Version'/>


               <SelectInput name='default_currency' label='Default currency'/>


               <Form.Footer>
                   <Form.Submit>Save</Form.Submit>
               </Form.Footer>
           </Form>
       </Page>
   )
}
All that’s left is to define the functions for sending API requests to the server – namely api.get() and api.post(). To do this, create a file named api.ts inside the src/config directory with the following content:

import {GetFormDataResult, UpdateResult} from '@devfamily/admiral'
import _ from './request'
import {Any} from '@react-spring/types'


const apiUrl = import.meta.env.VITE_API_URL || '/api'


type apiType = {
   get: (uri: string) => Promise<GetFormDataResult>
   post: (uri: string, data: Any) => Promise<UpdateResult>
}


const api: apiType = {
   get: (uri) => {
       const url = `${apiUrl}/${uri}`
       return _.get(url)({})
   },
   post: (uri, data) => {
       const url = `${apiUrl}/${uri}`
       return _.post(url)({ data })
   },
}


export default api
Here, we defined two methods for sending requests to the server. Each method appends the passed uri (in this case, settings) to the base server URL specified in the environment variables.
The requests themselves are handled using the existing get() and post() methods from the /src/config/request.ts file. In our case, the server URL is defined as: VITE_API_URL=http://localhost:802/admin
Now we can move on to defining the routing and controller logic.  To store the settings, we’ll create a simple table in the database. The migration looks like this:

<?php


use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;


return new class extends Migration
{
   /**
    * Run the migrations.
    */
   public function up(): void
   {
       Schema::create('settings', function (Blueprint $table) {
           $table->string('key')->unique();
           $table->text('value')->nullable();
           $table->timestamps();
       });


       DB::table('settings')->insert([
           [
               'key' => 'min_version',
               'value' => '3.0.0',
           ],
           [
               'key' => 'default_currency',
               'value' => 'RUB',
           ],
       ]);
   }


   /**
    * Reverse the migrations.
    */
   public function down(): void
   {
       Schema::dropIfExists('settings');
   }
};
Let’s create the model.

<?php


namespace App\Models;


use Illuminate\Database\Eloquent\Model;


class Setting extends Model
{
   protected $fillable = [
      'key',
      'value',
   ];
}
Next, let’s create the controller with two methods:
  1. One for returning the current settings;
  2. One for saving new settings values.

<?php


namespace App\Http\Controllers\Admin;


use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;


class SettingsController extends Controller
{
   public function get(): JsonResponse
   {
       $settings = Setting::query()
           ->whereIn('key', ['min_version', 'default_currency'])
           ->get()
           ->keyBy('key');


       return response()->json([
           'data' => [
               'min_version' => $settings->get('min_version')->value,
               'default_currency' => $settings->get('default_currency')->value,
           ],
           'values' => [
               'default_currency' => [
                   [
                       'label' => 'Dollar',
                       'value' => 'USD',
                   ],
                   [
                       'label' => 'Euro',
                       'value' => 'EUR',
                   ],
                   [
                       'label' => 'Russian ruble',
                       'value' => 'RUB',
                   ],
               ],
           ]
       ]);
   }


   public function store(Request $request): JsonResponse
   {
       Setting::query()->where('key', 'min_version')->update(['value' => $request->input('min_version')]);
       Setting::query()->where('key', 'default_currency')->update(['value' => $request->input('default_currency')]);


       return response()->json();
   }
}
In the get() method, the current settings data is returned under the data key. The keys in this object must match the name attributes defined in the form inputs.
The values key is used to pass the necessary options for the SelectInput. In this case, we’ve defined three currency options.
Next, let’s register our routes. In the /routes/admin.php file, add the following:
Route::prefix('settings')->as('settings.')->group(function () {
   Route::get('',[SettingsController::class,'get'])->name('get');
   Route::post('',[SettingsController::class,'store'])->name('store');
});
If we go to the browser and refresh the page, we’ll see that our input fields are now populated with default values.
When the “Save” button is clicked, the form data is sent to the server.
You can use all other input components from the devfamily/admiral package in exactly the same way.
Still have questions about creating custom pages in Admiral? We are here to help!

You may also like: