New App

Introduction

Assume that you can install the MetaFox site with the the MetaFox Developer package on your local machine or server at http://localhost:8081 (opens in a new tab).

In this acticle, we will create a new app note of company metafox. The Notes app will support following features:

  • Allow user to post/share notes with attachments, privacy.
  • Configure settings including title, description, tags, category, etc.
  • Configure permissions: admin can assign permissions based on user roles.
  • View notes of user's friends on their activity streams.
  • Get notifications when others comment, like notes.

Create New App

Generally, developing a new MetaFox app includes 2 parts: Frontend and Backend. We will create app skeleton for both Frontend and Backend first.

Backend

Enable Code Generator

The Code Generator option will display on the top header in AdminCP Dashboad, next to the User icon.

To enable this option, please do following steps:

  • Update the environment variable APP_ENV to local in the backend/.env file.
  • Clear cache in AdminCP or run the command php artisan cache:clear within the docker container of PHP-FPM

Then, you will see the the Code Generator icon on the top header in AdminCP.

Create New App

Press the Code Generator option on the top header. You will see the popup with many supported options. Let's choose the New App option as we intend to generate code for new app at this time.

On the Create App form, you will need to fill the following app details:

  • App Name: App name must have to 2 parts in the follow format vendor_name/app_name, such as company/note
  • Vendor Name: This info will be used as your app namespace, such as MetaFox
  • Author Name: Fill your company/vendor name, such as phpFox
  • Name: fill the app name, such as Note
  • Home page: Fill the link to the home page of your website

Then, click Create button to generate code skeleton for your new app.

The Skeleton for the Backend of Notes app will be generated as below:

Directory structure

packages/
 metafox/
    note/
      config/
        config.php
      routes/
        api.php
      resources/
        lang/: define supported languages
          drivers.php: define drivers
          en/
            phrase.php: define message translation
            validation.php: define message phrases
          menu/
            items.php: define menu items
            menus.php: define menus
      src/
        Database/
          Factories/
          Migrations/
          Seeders/
        Http/
         Controllers/
         Requests/
         Resources/
        Listeners/
        Models/
        Obsevers/
        Policies/
        Providers/
        Repositories/
      tests/
        Tests/
          Unit/: Unit test source root
          Features/: Feature test source root
      composer.json

Frontend

We assume that you have set up development environment to build React on your machine/server. The Frontend source is located under the frontend folder.

To create app skeleton for the Frontend of Notes app, you can open Terminal, go to the frontend folder mentioned above and run the following commands:

 
cd frontend/
 
yarn && yarn bootstrap
 
npx metafox-cli
 
 What do you need? · Create new app
 What is vendor/company name? · metafox
 What is app name? · note
Generating files ...
Updating workspace ...

You will see that the default skeleton for the Frontend of Notes app will be generated as below:

frontend/
  packages/
    metafox/
      note/
        src/
          reducers/
          sagas/
        package.json
        tsconfig.json
        constants.ts
        types.ts: define typings
        index.tsx: general export
        module.d.ts: integrate typing

Now we should add new routing for '/note'. On this page, we will simply show the text "This is note home page" for now.

Let's create a new file at frontend/packages/company/note/src/pages/HomePage.tsx and add following code:

/**
 * @type: route
 * path: /note
 * name: note.home
 */
 
import React from "react";
 
function HomePage() {
  return <div>This is note home page</div>;
}
 
export default HomePage;

Now, we will open terminal, go to the frontend folder and run following commands to build the frontend

yarn bundle

Once the building process is done, you can copy all build source files to the web folder.

In terminal, go to the top root folder and run the following command

cp -rf frontend/app/dist/* web/

Now, you can open browser and go the URL http://localhost:8081/note (opens in a new tab) . The message "This is note home page" on the screen.

Open browser then see "This is note home page" on the screen.

055d51f8a (fix)

Add Landing Page

Editing ./src/pages/HomePage.tsx

/**
 * @type: route
 * name: note.home
 * path: /note
 */
 
import { createLandingPage } from "@metafox/framework";
 
export default createLandingPage({
  appName: "note",
  pageName: "note.home",
  resourceName: "note",
});

Add new file ./src/assets/pages/note.home.json and paste:

{
  "large": {
    "blocks": [
      {
        "blockId": "ia",
        "component": "core.block.sideAppHeader",
        "slotName": "side",
        "title": "Notes",
        "icon": "ico-compose-alt",
        "blockLayout": "sidebar app header",
        "freeze": true
      },
      {
        "component": "core.block.sidebarAppMenu",
        "slotName": "side",
        "title": "Note",
        "blockId": "i8",
        "menuName": "sidebarMenu",
        "blockLayout": "sidebar app menu"
      }
    ],
    "templateName": "two-column-fixed"
  },
  "info": {
    "bundle": "web",
    "name": "note.home"
  }
}

Add New Menu

Open backend source ./packages/company/note/resources/menu/menus.php, There are 02 menus defines:

<?php
 
 /* this is auto generated file */
 return [
     [
         'name'=> 'note.sidebarMenu',
         'resolution'=> 'web',
     ],
     [
         'name' => 'note.admin',
         'resolution'=> 'admin',
     ],
 ];

note.admin define admincp menu for note, and note.sidebarMenu define app menu.

Update menu items defines for note.sidebarMenu at ./packages/company/note/resources/menu/web.php

<?php
 
 /* this is auto generated file */
 return [
     [
         'module_id' => 'note',
         'menu' => 'core.primaryMenu',
         'name' => '',
         'parent_name' => '',
         'label' => '',
         'ordering' => 4,
         'is_deleted' => 0,
         'version' => 0,
         'is_active' => 1,
         'to' => '/',
         'icon' => 'ico-newspaper-alt',
         'testid' => '',
         'extra' => [
             'subInfo' => 'Browse ',
         ],
     ],
     [
         'tab'      => 'all',
         'menu'     => 'note.sidebarMenu',
         'name'     => 'all',
         'label'    => 'note::phrase.all_note',
         'ordering' => 1,
         'icon'     => 'ico-hashtag',
         'to'       => '/note',
     ],
 ];

Open terminal for backend then upgrade menus:

php artisan package:install company/note --fast

'menu' => 'core.primaryMenu', apply menu item for primary menu to home page, open browser at '/' to see note menu items. Click on note will navigate to /note page.

Checking ./note to see the new menu item note::phrase.all_note menu items.

Add Sample Block

Following guide we create a new layout block for frontend and display a sample block content.

Open layout file ./packages/company/note/src/assets/pages/note.home.json

Update content

{
  "large": {
    "blocks": [
      {
        "blockId": "ia",
        "component": "core.block.sideAppHeader",
        "slotName": "side",
        "title": "Notes",
        "icon": "ico-compose-alt",
        "blockLayout": "sidebar app header",
        "freeze": true
      },
      {
        "component": "core.block.sidebarAppMenu",
        "slotName": "side",
        "title": "Note",
        "blockId": "i8",
        "menuName": "sidebarMenu",
        "blockLayout": "sidebar app menu"
      },
      {
        "blockId": "x8",
        "component": "note.block.SampleBlock",
        "slotName": "main"
      }
    ],
    "templateName": "two-column-fixed"
  },
  "info": {
    "bundle": "web",
    "name": "note.home"
  }
}

We add new layout block to layout:

{
  "blockId": "x8",
  "component": "note.block.SampleBlock",
  "slotName": "main"
}

The second step is create scripts file to handle block, create new file ./packages/company/note/src/blocks/Sample.tsx

/**
 * @type: block
 * name: note.block.SampleBlock
 * title: Sample Block
 * description: This is deveveloper sample guide block
 */
import React from "react";
import { createBlock, ListViewBlockProps } from "@metafox/framework";
 
function SampleBlock() {
  return <div>Sample Block</div>;
}
 
export default createBlock<ListViewBlockProps>({
  name: "SampleBlock",
  extendBlock: SampleBlock,
  defaults: {
    title: "Sample",
  },
});

Open terminal at ./frontend directory then run:

yarn reload

open browser at /note and you'll see new message "Sample Block" on the screen.

The next step we update block scripts to display more complex layout and present data from backend api.

Add New Schema

Press Code Generator on the top header in AdminCP, then choose Migration item and fill schema name with "notes".

A new Migration file is created under backend/package/company/note/src/Database/Migrations folder. Its file name will be prefixed by info of datetime, for example:2022_05_23_101328_migrate_notes_table.php

The datetime prefix helps migration scripts execute Migration files in chronological order.

For more info, you can read Laravel Migration.

By following Laravel Migration, let's update the Migration file to create notes table, including the following columns

id                 : Primary key
module_id          :
user_id            :
user_type          : user_type and user_id is morph columns to this note creator.
owner_id           :
owner_type         : owner_id and owner_type is morph columns to this note owner.
privacy            : Who can see this note.
total_view         :
total_like         :
total_comment      :
total_reply        :
total_share        :
total_attachment   :
title              : Note title
is_approved        :
is_draft           : Is this note post as draft ?
is_sponsor         :
sponsor_in_feed    :
is_featured        : Is this note mark as featured ?
featured_at        :
image_path         :
server_id          : Disk id to storage image
tags               : Contains notes tags
created_at         :
updated_at         :

example source

<?php
 
use MetaFox\Platform\Support\DbTableHelper;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
/*
 * stub: /packages/database/migration.stub
 */
 
/*
 * @ignore
 * @codeCoverageIgnore
 * @link \$PACKAGE_NAMESPACE$\Models
 */
return new class () extends Migration {
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        if (Schema::hasTable('notes')) {
            return;
        }
 
        Schema::create('notes', function (Blueprint $table) {
            $table->bigIncrements('id');
 
            DbTableHelper::setupResourceColumns($table, true, true, true, true);
 
            DbTableHelper::totalColumns($table, ['view', 'like', 'comment', 'reply', 'share', 'attachment']);
 
            $table->string('title', 255);
 
            $table->unsignedTinyInteger('is_draft')->default(0);
 
            DbTableHelper::featuredColumn($table);
            DbTableHelper::sponsorColumn($table);
            DbTableHelper::approvedColumn($table);
            DbTableHelper::imageColumns($table);
            DbTableHelper::tagsColumns($table);
 
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(): void
    {
        Schema::dropIfExists('notes');
    }
};
 

Open terminal at ./backend directory then re-install note package to apply new schema.

php artisan package:install company/note --fast

Add New Model

Press Code Generator on the top header and choose the Model item. Then, fill your App name with company/note, schemas with notes, Model with Note and Entity Name with note.

MetaFox generates somes classes based on what features you have chosen on the previous form.

Has Repository?

Generate files for Repository associated with the model. The pattern is based on l5-repository (opens in a new tab)

Has Model Factory?

Generate files for Model Factory (opens in a new tab).

Has Authorization ?

Add permissions for the model based on laravel-permission (opens in a new tab)

Has Text Data?

Separate text to the second schema. It's helpful to reduce size of the main schema.

Has Category Data

Create a pivot model associated with the main schema to store relationships between the main schema and a category schema.

Has Tags Data

Create a pivot schema associated with the main schema to store relationships between the main schema and a tags data.

Has Activity Feed?

Create a pivot schema to publish a Note item to activity stream.

Has Model Observer?

Create Observer to listen events on the main schema

Register Repository to laravel service provider at ./packages/company/note/src/Providers/PackageServiceProvider.php

<?php
 
namespace Company\Note\Providers;
 
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider;
 
class PackageServiceProvider extends ServiceProvider
{
 
    public array $singletons = [
        \Company\Note\Repositories\NoteRepositoryInterface::class     => \Company\Note\Repositories\Eloquent\NoteRepository::class,
        // .. more dependencies
    ];
}

Code Generator will generate all necessary source files of the model based on the chosen options. It saves you much time.

Generate APIs for Web and Admin

Press Code Generator on the top header in AdminCP and choose the Web API item. On the form, choose company/note package and Note model, then submit.

You can do the same to generate APIs for Admin but choose Admin API.

To update api routes

Next, we will edit backend/packages/company/note/routes/api.php file to add routes:

<?php
 
namespace Company\Note\Http\Controllers\Api;
use Illuminate\Support\Facades\Route;
 
Route::prefix('/note')
    ->resource('resource', NoteController::class);

Open terminal at ./backend directory then clear cache

php artisan cache:reset

Checking api is ready at http://localhost:3000/api/v1/note

you'll see content similar to

{
  "data": [],
  "links": {
    "first": "http://127.0.0.1:8080/api/v1/note?page=1",
    "last": "http://127.0.0.1:8080/api/v1/note?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": null,
    "last_page": 1,
    "links": [
      {
        "url": null,
        "label": "&laquo; Previous",
        "active": false
      },
      {
        "url": "http://127.0.0.1:8080/api/v1/note?page=1",
        "label": "1",
        "active": true
      },
      {
        "url": null,
        "label": "Next &raquo;",
        "active": false
      }
    ],
    "path": "http://127.0.0.1:8080/api/v1/note",
    "per_page": 100,
    "to": null,
    "total": 0
  }
}

Now, you can open the URL http://localhost:8081/note on your browser to view result.

MetaFox framework supports API versioning,

Http requests are forwarded to Company\Note\Http\Controllers\Api\NoteController, validated for versioning and then forwarded to Company\Note\Http\Controllers\Api\v1\NoteController.

Add WebSetting

The Frontend loads all site settings, permissions and all resource settings via the Settings API /api/core/web-settings. This API collects all data defined in the WebSetting classes of all app packages. Follow this guide step by step to register app settings into the Settings API.

Edit backend/packages/company/note/src/Http/Resources/v1/Note/WebSetting.php file

<?php
 
namespace Company\Note\Http\Resources\v1\Note;
 
use MetaFox\Platform\Resource\WebSetting as ResourceSetting;
 
/**
 *--------------------------------------------------------------------------
 * Note Web Resource Setting
 *--------------------------------------------------------------------------
 * stub: /packages/resources/resource_setting.stub
 * Add this class name to resources config gateway.
 */
 
/**
 * Class NoteWebSetting
 * Inject this class into property $resources.
 * @link \Company\Note\Http\Resources\v1\WebSetting::$resources;
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
 */
class WebSetting extends ResourceSetting
{
    /**
     * Defines frontend redux actions
     */
    protected function initActions(): void
    {
        $this->addActions([
            'searchItem'         => [
                'apiUrl'=> '/note',
                'pageUrl'     => '/note/search',
                'placeholder' => 'Search notes',
            ],
            'homePage'           => [
                'pageUrl' => '/note',
            ],
            'viewAll'            => [
                'apiUrl'   => '/note',
                'apiRules' => [],
            ],
            'viewItem'           => [ // view item detail action.
                'apiUrl'  => '/note/:id',
            ],
            'deleteItem'         => [
                'apiUrl'  => '/note/:id',
                'confirm' => [
                    'title'   => 'Confirm',
                    'message' => 'Are you sure you want to delete this item permanently?',
                ],
            ],
            'editItem'           => [
                'pageUrl' => '/note/edit/:id',
            ],
            'addItem'            => [
                'pageUrl' => '/note/add',
                'apiUrl'  => '/note/form',
            ],
        ]);
    }
 
    /**
     * Define forms json should return in web-settings.
     */
    protected function initForms(): void
    {
        $this->addForms([
            'filter' => new SearchNoteForm(),
        ]);
    }
}

Checking backend/packages/metafox/note/resouces/drivers.php contains

    [
        'driver'     => 'MetaFox\\Note\\Http\\Resources\\v1\\Note\\WebSetting',
        'type'       => 'resource-web',
        'name'       => 'note',
        'version'    => 'v1',
        'resolution' => 'web',
        'is_active'  => true,
        'is_preload' => false,
    ],

Home Page

Let's open the frontend/packages/metafox/note/src/pages/HomePage/Page.tsx file and look into some annotations at the top of file

/**
 * @type: route
 * name: note.home
 * path: /note
 */
 
import { createLandingPage } from "@metafox/framework";
 
export default createLandingPage({
  appName: "note",
  pageName: "note.home",
  resourceName: "note",
});

@type: route

Define this source code is a route, MetaFox bundle tool collects this info and separate to bundle.

name: note.home

Define a global unique key for route, it can be overwritten by another page when you want to customize logic.

path: /note

This is a pattern string to define a route path, based on path-to-regexp (opens in a new tab)

Check new file:

App Menu

Visit AdminCP > Appearance > Menus to browse all site menus.

MetaFox framework contains built-in menus

  • core.primaryMenu: Left side menu of home page
  • core.adminSidebarMenu: Left side menu of the AdminCP
  • core.headerSubMenu: Header top-right menu
  • core.accountMenu: Header account menu

Also, each app may contain menus on sidebar, admin. For example, in Notes app can have following menus:

  • note.sidebarMenu: sidebar menu of Notes on home page.
  • note.admin: admin menu of Notes app in AdminCP

When an app is created, its menu is automatically inserted into core.primaryMenu and core.adminSidebarMenu.

Admin can manipulate menus in AdminCP, such as: adding new menu item + Add Note with URL /note/add to the menu note.sidebarMenu

Resource Menu

Each resource has 2 action menus in contexts of listing item and viewing item detail. For example:

  • note.note.itemActionMenu
  • note.note.detailActionMenu

The name of action menus MUST be followed the convention: [appName].[resourceName].[context]

Add Forms

Backend

Visit AdminCP > Code Generator > Forms

Put forms with "store, update, search" to create StoreNoteForm, UpdateNoteForm, SearchNoteForm.

Edit backend/packages/company/note/routes/api.php file to add routes for form requests.

Dive into StoreNoteForm.php

<?php
 
namespace Company\Note\Http\Controllers\Api;
 
use Illuminate\Support\Facades\Route;
 
/**
 * --------------------------------------------------------------------------
 *  API Routes
 * --------------------------------------------------------------------------
 *
 *  This file will be loaded by @link \MetaFox\Platform\ModuleManager::getApiRoutes()
 *
 *  stub: app/Console/Commands/stubs/routes/api.stub
 */
 
Route::group([
    'namespace'  => __NAMESPACE__,
    'middleware' => 'auth:api', // logged in required
], function () {
    // routes to form
    Route::get('note/form', 'NoteController@getStoreForm');
    Route::get('note/form/:id', 'NoteController@getUpdateForm');
    Route::get('note/form/search', 'NoteController@getSearchForm');
 
    // routes for note resource
    Route::get('note', 'NoteController');
 
});

Edit the backend/packages/company/note/src/Http/Controllers/Api/v1/NoteController.php file, add following methods

<?php
namespace Company\Note\Http\Controllers\Api\v1;
 
 
// import use method
 
class NoteController extends ApiController
{
    /**
     * @var NoteRepositoryInterface
     */
    private NoteRepositoryInterface $repository;
 
    // other method and properties
    ....
 
    /**
     * Get updating form
     *
     * @param  int  $id
     *
     * @return UpdateNoteForm
     */
    public function getUpdateForm(int $id): UpdateNoteForm
    {
        $resource = $this->repository->find($id);
 
        return new UpdateNoteForm($resource);
    }
 
    /**
     * Get updating form
     *
     * @param  int  $id
     *
     * @return SearchNoteForm
     */
    public function getSearchForm(int $id): SearchNoteForm
    {
        $resource = $this->repository->find($id);
 
        return new SearchNoteForm($resource);
    }
 
 
    /**
     * Get updating form
     *
     * @param  int  $id
     *
     * @return StoreNoteForm
     */
    public function getDestroyForm(int $id): StoreNoteForm
    {
        $resource = $this->repository->find($id);
 
        return new StoreNoteForm($resource);
    }
}

Edit the backend/packages/pdev/note/src/Http/Resources/v1/Note/StoreNoteForm.php file as below

<?php
 
namespace Company\Note\Http\Resources\v1\Note;
 
use MetaFox\Platform\MetaFoxForm;
use MetaFox\Platform\Support\Form\AbstractForm;
use MetaFox\Platform\Support\Form\Field\CancelButton;
use MetaFox\Platform\Support\Form\Field\Submit;
use MetaFox\Platform\Support\Form\Field\Text;
use Company\Note\Models\Note as Model;
 
class StoreNoteForm extends AbstractForm
{
  /**
   * Prepare title, method, action, value of form object
   */
    protected function prepare(): void
    {
        $this->config([
            'title'  => __p('core.phrase.edit'),
            'action' => '/note',    // target api url
            'method' => 'POST', // use "POST" method
            'value'  => [
                // default value.
            ],
        ]);
    }
 
  /**
   * Define form structure.
   */
    protected function initialize(): void
    {
        $basic = $this->addBasic();
 
        // add form fields.
        $basic->addFields(
            new Text([
                'name'          => 'title',
                'required'      => true,
                'returnKeyType' => 'next',
                'label'         => 'Title',
                'validation'    => [ // add client validation rules
                  'required' => true,
                  'nullable' => false,
                  'errors'=>[
                    'required'=> __p('validation.this_is_required_field'),
                  ]
                ]
            ])
        );
 
 
        // add cancel buttons
        $footer = $this->addFooter();
        $footer->addFields(
            new CancelButton([]),
            new Submit([
                'label' => ($this->resource && $this->resource->id) ?
                    __p('core.phrase.save_changes') :
                    __p('core.phrase.create'),
            ]),
        );
    }
}
FieldNote
AttachmentMultiple file picker to attachment
AutocompleteAutocomplete text field
BirthdayDate picker
ButtonFieldBasic Button
CancelButtonButton for cancel action
CaptchaFieldCaptcha field
CategoryFieldCategory picker
CheckboxFieldMultiple Checkbox field
ChoiceCombobox Field
CountryStateChoose country and state
CustomGendersChoose custom gender
DatetimeDatetime picker
DescriptionFieldTextarea for description
EmailText field with email format
FileSingle file picker
FilterCategoryFieldCategoryField for filter form
FriendPickerFriend picker
HiddenHidden input
LanguageLanguage picker field
LinkButtonFieldButton with href
LocationLocation picker field
PasswordInput password field
PrivacyPrivacy picker field
RadioRadio Field
SearchBoxFieldText field support search
SinglePhotoFieldSingle photo picker field
SingleVideoFieldSingle video picker field
SubmitSubmit button
SwitchFieldAlternate checkbox
TagsFieldMultiple tags input field
TextSingle text input field
TextAreaTextarea input
TimezoneTimezone picker field
TitleFieldSingle title field
TypeCategoryFieldType-category field for 02 level type category

You can check all form fields supported at MetaFox built-in fields support (opens in a new tab)

Frontend

Frontend supports built-in dynamic form builder to transform JSON-based responses into ReactJS Form element.

Below is the sample Form response in JSON format

{
  "status": "success",
  "data": {
    "component": "form",     // define ReactJs render component
    "title": "Add New Note", // form title
    "action": "/note",       // target api for http request when form submit.
    "method": "POST",        // http method for http request when form submit.
    "value": {               // initial values.
      "module_id": "note",
      "privacy": 0,
      "draft": 0,
      "tags": [],
      "owner_id": 0,
      "attachments": []
    },
    "validation": {         // define validation object, based on https://www.npmjs.com/package/yup
      "type": "object",
      "properties": {
        "title": {
          "label": "Title",
          "type": "string",
          "required": true,
          "minLength": 3,
          "maxLength": 255,
          "errors": {
            "maxLength": "Title must be at most 255 characters"
          },
        }
      }
    },
    "elements": {         // define form structure
      "basic": {          // basic form section
        "name": "basic",
        "component": "container",
        "testid": "field basic",
        "elements": {
          "title": {                 // form field
            "component": "text",     // Define react render component to form.element.[component]
            "returnKeyType": "next",
            "maxLength": 255,
            "fullWidth": true,
            "margin": "normal",
            "size": "medium",
            "variant": "outlined",
            "name": "title",
            "required": true,
            "label": "Title",
            "placeholder": "Fill in a title for your note",
            "description": "Maximum 255 of characters",
            "testid": "field title"
          },
          "text": {
            "fullWidth": true,
            "variant": "outlined",
            "returnKeyType": "default",
            "name": "text",
            "required": true,
            "label": "Post",
            "placeholder": "Add some content to your note",
            "component": "RichTextEditor",
            "testid": "field text"
          },
      },
    }
  }
}

Look into frontend/packages/framework/metafox-form/src/elements/TextField.tsx file

/**
 * @type: formElement
 * name: form.element.textarea
 */
import MuiTextField from "@mui/material/TextField";
import { useField } from "formik";
import { camelCase } from "lodash";
import { createElement } from "react";
import { FormFieldProps } from "../types";
 
const TextAreaField = ({
  config,
  disabled: forceDisabled,
  name,
  formik,
}: FormFieldProps) => {
  const [field, meta] = useField(name ?? "TextField");
  const {
    label,
    disabled,
    labelProps,
    placeholder,
    variant,
    margin = "normal",
    fullWidth,
    type = "text",
    rows = 5,
    description,
    autoFocus,
    required,
    maxLength,
  } = config;
 
  // fix: A component is changing an uncontrolled input
  if (!field.value) {
    field.value = config.defaultValue ?? "";
  }
 
  const haveError = Boolean(meta.error && (meta.touched || formik.submitCount));
 
  return createElement(MuiTextField, {
    ...field,
    required,
    multiline: true,
    disabled: disabled || forceDisabled || formik.isSubmitting,
    variant,
    label,
    "data-testid": camelCase(`field ${name}`),
    autoFocus,
    inputProps: { "data-testid": camelCase(`input ${name}`), maxLength },
    rows,
    InputLabelProps: labelProps,
    placeholder,
    margin,
    error: haveError ? meta.error : false,
    fullWidth,
    type,
    helperText: haveError ? meta.error : description,
  });
};
 
export default TextAreaField;

@type: formElement

This annotation determines the file defines a form field component, build tool collects the info to bundle all files into a chunks.

name: form.element.textarea

When the form is returned by a API, form builder will detect and use this component to render elements having "component": "textarea" key-value pair.

Validation

Form supports validation both Frontend and Backend

Backend

Dive into backend/packages/company/note/src/Http/Requests/v1/Note/StoreRequest.php file

<?php
 
namespace Company\Note\Http\Requests\v1\Note;
 
use Illuminate\Foundation\Http\FormRequest;
 
class StoreRequest extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules()
    {
        // Getting validation rules.
        return [
            'title'=> ['string', 'required']
        ];
    }
}

The main method rules returns an array of validation rules (opens in a new tab)

Frontend

Frontend validation is based on yup (opens in a new tab). MetaFox dynamic form builder transforms JSON object to a yup validation object.

Translation

Backend

MetaFox translation feature provides a convenient way to retrieve strings in various languages, allowing you to easily support multiple languages within your application.

Within note package, translations string are stored within the resources/lang directive. With thin this directory, the subdirectory is langue code, and files in contains groups of translations phrase.

note/
  resources/
    lang/
      en/                 : Language code `vi`
        phrase.php        : Phrase groups: `phrase`
        validation.php    : Phrase groups: `validation`
        ...
      fr/                 : Others language code.

Dive deeper into backend/packages/company/note/resources/lang/en/phrase.php, it defines phrase group phrase, contains a list of phrase_name and phrase value.

<?php
return [
  'notes' => 'Notes',         //: phrase_name =  "note", phrase_value = "Notes"
  'label_menu_s' => 'Notes',
  'note_label_saved' => 'Note',
  'specify_how_many_points_the_user_will_receive_when_adding_a_new_note' => 'Specify how many points the user will receive when adding a new note.',
  'specify_how_many_points_the_user_will_receive_when_deleting_a_new_note' => 'Specify how many points the user will receive when deleting a note.',
  'new_note_post' => 'New Note Post',
  'edit_note' => 'Editing Note',
  'add_some_content_to_your_note' => 'Add some content to your note',
  'fill_in_a_title_for_your_note' => 'Fill in a title for your note',
  'control_who_can_see_this_note' => 'Control who can see this note.',
  'note_type' => 'Note Type',
  'added_a_note' => 'added a note',
  'note_notification_type' => 'Note Notification',
  'note_featured_successfully' => 'Note featured successfully.',
];

In order to prevent conflict of phrase name, MetaFox translation feature use namespaced translation key convention {namespace}::{group}.{phrase_name} to identity translation string in the appliation. etc:

<?php
 
/**
 * namespace: "note" is the alias of package `company\note`.
 * group: "phrase"
 * note: "note_length_title"
 */
echo __('note::phrase.note_length_title'); // output "Note Title"

In order to support laravel compatible packages, MetaFox also support laravel-translation (opens in a new tab). Without namespaced translation key convention is {group}.{phrase_name}. Most of theme publish language files into resources directory of project root.

resources/
  lang/
    en/
      auth.php
      pagination.php
      passwords.php
      phrase.php
      validation.php

In this case, the namespace is dropped, and the translation key is {group}.{phrase_name}. etc

<?php
echo __p('auth.failed'); // ouput: These credentials do not match our records.

Add Phrase

The simplest way to add translation phrases is via AdminCP, visit AdminCPLocalizationPhrases+ Add New Phrase

Then, you can follow the Phrase Creation Wizard to create a new phrase.

Frontend

MetaFox frontend provides translations feature in the messages.json file. It contains key/value translations as below:

{
  "toggle_layout_preview": "Toggle Device Preview",
  "total_like": "{ value, plural, =0{No likes} =1{# like} other{# likes} }",
  "total_photo": "{ value, plural, =0{No photos} =1{# photo} other{# photos} }",
  "total_video": "{ value, plural, =0{No videos} =1{# video} other{# videos} }",
  "total_post": "{ value, plural, =0{No posts} =1{# post} other{# posts} }",
  "total_view": "{ value, plural, =0{No views} =1{# view} other{# views} }",
  "total_vote": "{ value, plural, =0{No votes} =1{# vote} other{# votes} }",
  "total_play": "{ value, plural, =0{No plays} =1{# play} other{# plays} }",
  "total_comment": "{ value, plural, =1{# comment} other{# comments} }",
  "total_share": "{ value, plural, =1{# share} other{# shares} }",
  "total_track": "{ value, plural, =1{# track} other{# tracks} }",
  "event_start_date": "start {date}",
  "text_direction": "Text Direction",
  "edit_block_name": "Edit Block {name}",
  "ok": "OK",
  "copy": "Copy",
  "close": "Close",
  "cancel": "Cancel",
  "add_layout_block": "Add New Block"
}

To translate message in the component, use i18n helper.

import { useGlobal } from "@metafox/framework";
import React from "react";
 
function MyComponent() {
  // use i18n helper
  const { i18n } = useGlobal();
 
  return <div>{i18n.formatMessage({ id: "toggle_layout_preview" })}</div>;
  // output: <div>Toggle Device Preview</div>
}

To translate message in the saga function

function * saga(){
  const { i18n } = yield* getGlobalContext();
 
  console.log({i18n.formatMessage({id: 'toggle_layout_preview'})});
  // output: Toggle Device Preview
}

In order to support complex message translation, frontend translation support icu syntax, allows developer formats plurals, number, date, time, select, selectordinal. For more information checkout [icu-syntax](icu syntax (opens in a new tab))

To support multiple language, frontend load custom language translation using api /core/translations/web/{language}. The API responds all messages in the translation group web.

{
  "status": "success",
  "data": {
    "accepted": "The :attribute must be accepted.",
    "active_url": "The :attribute is not a valid URL.",
    "after": "The :attribute must be a date after :date.",
    "after_or_equal": "The :attribute must be a date after or equal to :date.",
    "alpha": "The :attribute may only contain letters.",
    "alpha_dash": "The :attribute may only contain letters, numbers, dashes and underscores.",
    "alpha_num": "The :attribute may only contain letters and numbers.",
    "array": "The :attribute must be an array.",
    "before": "The :attribute must be a date before :date.",
    "before_or_equal": "The :attribute must be a date before or equal to :date."
  },
  "message": null,
  "error": null
}

Page Browsing

Dive deeper into frontend/packages/company/note/src/pages/BrowseNotes/Page.tsx file

/**
 * @type: route
 * name: note.browse
 * path: /note/:tab(friend|all|pending|feature|spam|draft)
 */
import { createBrowseItemPage } from "@metafox/framework";
 
export default createBrowseItemPage({
  appName: "note",
  resourceName: "note",
  pageName: "note.browse",
  categoryName: "note_category",
});

@type: route: Define this file must export default route component.

name: note.browse: Define page name

path: /note/:tab(friend|all|pending|feature|spam|draft)

path-to-regexp (opens in a new tab) pattern to match route.

appName: Define app name

resourceName: Define browsing resource name

pageName: Define layout page name

categoryName: Define link to category resource type

Page Search

Backend

You can define search form and then add to the WebSetting

Global Search

Global search system is centralized search system in MetaFox. In order for your app to integrate with global search system, you must define which content is searchable by implementing MetaFox\Platform\Contracts\HasGlobalSearch interface in your main modal. In this example, we will update the Note model to implement HasGlobalSearch interface

<?php
 
namespace Company\Note\Models;
 
// declares php "uses" directive.
 
class Note extends Model implements HasGlobalSearch // , and other interfaces
{
 
    // others property and method
    public function toSearchable(): ?array
    {
        // A draft blog is not allowed to be searched
        return [
            'title' => $this->title,
            'text'  => 'content of your text',
            'category' => '',
            // others data.
        ];
    }
}

Search system has event listener listening on modification of Note data and update its data in queue worker.

Activity Feed

To support activity feed system, the Note model will need to implement the MetaFox\Platform\Contracts\ActivityFeedSource and MetaFox\Platform\Contracts\HasResourceStream interfaces as below

<?php
 
namespace Company\Note\Models;
use MetaFox\Platform\Support\FeedAction;
 
// declares php "uses" directive.
 
class Note extends Model implements ActivityFeedSource // , and other interfaces
{
    /**
     * Define content of feed action put to activity streams
     *
     * @return FeedAction
     */
    public function toActivityFeed(): ?FeedAction
    {
        if ($this->isDraft()) {
            return null;
        }
 
        return new FeedAction([
            'user_id'    => $this->userId(),
            'user_type'  => $this->userType(),
            'owner_id'   => $this->ownerId(),
            'owner_type' => $this->ownerType(),
            'item_id'    => $this->entityId(),
            'item_type'  => $this->entityType(),
            'type_id'    => $this->entityType(),
            'privacy'    => $this->privacy,
        ]);
    }
 
    /**
     * Define morph map to privacy streams.
     */
    public function privacyStreams(): HasMany
    {
        return $this->hasMany(PrivacyStream::class, 'item_id', 'id');
    }
}

Event Listeners

To track model modification, you can use Event Listener and build-in event list.

To list full events your site, you can open terminal and run the command php artisan event:list

Model Observer

In order to track model modification, checkout Eloquent Observer

Export App

Following instruction /export-package to export language pack.