From 5a988086dbb31d0791987fe8491b52f01b7ac252 Mon Sep 17 00:00:00 2001 From: jacky158 Date: Tue, 26 Dec 2023 10:58:18 +0700 Subject: [PATCH] Updates --- 404.html | 2 +- _next/static/chunks/nextra-data-en-US.json | 2 +- .../_buildManifest.js | 0 .../_ssgManifest.js | 0 backend/category.html | 4 ++-- backend/commands.html | 4 ++-- backend/content.html | 4 ++-- backend/controller.html | 4 ++-- backend/datagrid.html | 4 ++-- backend/eloquent.html | 4 ++-- backend/event-list.html | 4 ++-- backend/event.html | 4 ++-- backend/form.html | 4 ++-- backend/installation.html | 4 ++-- backend/interfaces.html | 4 ++-- backend/load-reduce.html | 4 ++-- backend/mailer.html | 4 ++-- backend/octane.html | 4 ++-- backend/package.html | 4 ++-- backend/routing.html | 4 ++-- backend/structure.html | 4 ++-- export-package.html | 4 ++-- frontend/concepts.html | 4 ++-- frontend/cookie.html | 4 ++-- frontend/dialog.html | 4 ++-- frontend/form.html | 4 ++-- frontend/gridview.html | 4 ++-- frontend/home.html | 4 ++-- frontend/layout.html | 4 ++-- frontend/local-store.html | 4 ++-- frontend/routing.html | 4 ++-- frontend/sagas.html | 4 ++-- frontend/service.html | 4 ++-- frontend/theme.html | 4 ++-- frontend/translation.html | 4 ++-- frontend/typings.html | 4 ++-- frontend/validation.html | 4 ++-- frontend/when-lib.html | 4 ++-- index.html | 4 ++-- mobile/flatlist.html | 2 +- mobile/layout.html | 4 ++-- mobile/reducers.html | 2 +- mobile/router.html | 4 ++-- mobile/sagas.html | 2 +- mobile/service.html | 2 +- mobile/translations.html | 2 +- new-app.html | 4 ++-- new-language.html | 4 ++-- new-theme.html | 4 ++-- 49 files changed, 87 insertions(+), 87 deletions(-) rename _next/static/{JE7mgC1QUgHyxpz87oK9Y => vETGZwN9166EGdki_sRgj}/_buildManifest.js (100%) rename _next/static/{JE7mgC1QUgHyxpz87oK9Y => vETGZwN9166EGdki_sRgj}/_ssgManifest.js (100%) diff --git a/404.html b/404.html index ad0e826e5..5577bffae 100644 --- a/404.html +++ b/404.html @@ -1 +1 @@ -404: This page could not be found

404

This page could not be found.

\ No newline at end of file +404: This page could not be found

404

This page could not be found.

\ No newline at end of file diff --git a/_next/static/chunks/nextra-data-en-US.json b/_next/static/chunks/nextra-data-en-US.json index ead48319b..315885d0f 100644 --- a/_next/static/chunks/nextra-data-en-US.json +++ b/_next/static/chunks/nextra-data-en-US.json @@ -1 +1 @@ -{"/backend/category":{"title":"Category","data":{"":"App items such as blogs, events, etc., can be organized into categories. In this topic, we are going to walk you through steps to support Categories in the sample Notes app.In the Notes app, we are going to define 2 database schemas note_categories and note_category_data to keep many-to-many relationship between notes and categories","build-schema#Build Schema":"We will define a Migration class with 2 methods up and down\n\nclass NoteMigration extends Migration{\n\npublic function up (){\nDbTableHelper::categoryTable('note_categories', true);\nDbTableHelper::categoryDataTable('note_category_data');\n}\n\npublic function down(){\nSchema::dropIfExists('note_categories');\nSchema::dropIfExists('note_category_data');\n}\n}\n\nIn result, the database schemes will be generated automatically as below","category-ddl#Category DDL":"-- auto-generated definition\nCREATE TABLE note_categories\n(\nid serial CONSTRAINT note_categories_pkey PRIMARY KEY,\nparent_id integer,\nname varchar(255) NOT NULL,\nname_url varchar(255),\nis_active smallint DEFAULT '1'::smallint NOT NULL,\nordering integer DEFAULT 0 NOT NULL,\ntotal_item integer DEFAULT 0 NOT NULL,\ncreated_at timestamp(0),\nupdated_at timestamp(0)\n);","category-data-ddl#Category Data DDL":"-- auto-generated definition\nCREATE TABLE note_category_data\n(\nid bigserial CONSTRAINT blog_category_data_pkey PRIMARY KEY,\nitem_id bigint NOT NULL,\ncategory_id integer NOT NULL\n);"}},"/backend/commands":{"title":"Commands","data":{"":"In this topic, we are going to discuss about most frequently commands which can be executed on Linux terminal.We'll assume that you are familiar with Linux terminal. Firstly, please open the terminal, log in SSH to the Backend server, and go to the source folder of MetaFox Backend. If the MetaFox Backend is set up with a Docker container, you can run the docker exec command to log in to the running container.Here is a list of Linux commands:\n\n# List all supported commands\nphp artisan list\n\n# Install fresh MetaFox site\nbash ./scripts/install.sh\n\n# Update MetaFox site\nphp artisan metafox:update"}},"/backend/content":{"title":"Content","data":{"":"This is an interface of a Contract content. Example: Blog, Photo v.v...A Contract content always has user_id, user_type, owner_id, and owner_type.\n\nnamespace MetaFox\\Platform\\Contracts;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\nuse Illuminate\\Support\\Collection;\n\ninterface Content extends Entity\n{\n/**\n* @return int\n*/\npublic function userId(): int;\n\n/**\n* @return string\n*/\npublic function userType(): string;\n\n/**\n* @return int\n*/\npublic function ownerId(): int;\n\n/**\n* @return string\n*/\npublic function ownerType(): string;\n\n/**\n* @return User|MorphTo\n*/\npublic function user();\n\n/**\n* @return UserEntity|BelongsTo\n*/\npublic function userEntity();\n\n/**\n* @return User|MorphTo\n*/\npublic function owner();\n\n/**\n* @return UserEntity|BelongsTo\n*/\npublic function ownerEntity();\n}","category#Category":"For example, you may want to organize Note items into categories, you need to have 2 schemas note_categories and note_category_data to keep many-to-many relationship of notes and categories","schema#Schema":"class NoteMigration extends Migration{\n\npublic function up (){\nDbTableHelper::categoryTable('note_categories', true);\nDbTableHelper::categoryDataTable('blog_category_data');\n}\n\npublic function down(){\nSchema::dropIfExists('note_categories');\nSchema::dropIfExists('note_category_data');\n}\n}\n\nCategory DDL\n-- auto-generated definition\nCREATE TABLE note_categories\n(\nid serial CONSTRAINT note_categories_pkey PRIMARY KEY,\nparent_id integer,\nname varchar(255) NOT NULL,\nname_url varchar(255),\nis_active smallint DEFAULT '1'::smallint NOT NULL,\nordering integer DEFAULT 0 NOT NULL,\ntotal_item integer DEFAULT 0 NOT NULL,\ncreated_at timestamp(0),\nupdated_at timestamp(0)\n);\nCategory Data DDL\n-- auto-generated definition\nCREATE TABLE note_category_data\n(\nid bigserial CONSTRAINT blog_category_data_pkey PRIMARY KEY,\nitem_id bigint NOT NULL,\ncategory_id integer NOT NULL\n);","tags#Tags":"Each content can support hashtag and tags\nhashtag is tagged words starting by # in description of content.\ntag (sometimes named as topics) is separated word/label attached to a content\n\nIn order to support tags and search in MetaFox app","step-1#Step 1:":"Add *_tag_data relation to migration\nDbTableHelper::createTagDataTable('blog_tag_data');","step-2#Step 2:":"Modify content database table to have tags\nclass CreateBlogTables extends Migration{\n\nfunction up(){\n// add this line to up() method\nDbTableHelper::tagsColumns($table);\n}\n}\nAdd *TagData table class\nbelongsToMany(\nTag::class,\n'blog_tag_data',\n'item_id',\n'tag_id'\n)->using(BlogTagData::class);\n}\n// ... other method\n}","step-4#Step 4":"Add tag scope to associated query to be able to filter content by tag.\nif ($searchTag != '') {\n$query = $query->addScope(new TagScope($searchTag));\n}","step-5#Step 5":"On the Search form, there are no tag fields, users can type \"#\" in search field to search with tags.\n\nclass IndexRequest{\n\n/**\n* @return array\n*/\npublic function validated(): array\n{\n$data = parent::validated();\n\n// .. other process\n\nif (Str::startsWith($data['q'], '#')) {\n$data['tag'] = Str::substr($data['q'], 1);\n$data['q'] = MetaFoxConstant::EMPTY_STRING;\n}\n\nreturn $data;\n}\n}","policy#Policy":"This is the main interface for all Policies.\nnamespace MetaFox\\Platform\\Contracts\\Policy;\n\nuse MetaFox\\Platform\\Contracts\\User;\nuse MetaFox\\Platform\\Contracts\\Content;\n\ninterface ResourcePolicyInterface\n{\npublic function viewAny(User $user, ?User $owner = null): bool;\n\npublic function view(User $user, Content $resource): bool;\n\npublic function viewOwner(User $user, User $owner): bool;\n\npublic function create(User $user, ?User $owner = null): bool;\n\npublic function update(User $user, ?Content $resource = null): bool;\n\npublic function delete(User $user, ?Content $resource = null): bool;\n\npublic function deleteOwn(User $user, ?Content $resource = null): bool;\n}","global-policy#Global Policy":"When you want to set a global policy to every Resource, you should create classes in packages/[company]/[app_name]/src/Policies/Handlers folderIn the below example, CanComment class will add a comment policy to all Policy classes\n\nnamespace MetaFox\\Comment\\Policies\\Handlers;\n\nuse MetaFox\\Platform\\Contracts\\Content;\nuse MetaFox\\Platform\\Contracts\\HasTotalComment;\nuse MetaFox\\Platform\\Contracts\\User;\nuse MetaFox\\Platform\\Support\\Facades\\PrivacyPolicy;\n\nclass CanComment\n{\npublic function check(string $entityType, User $user, Content $resource): bool\n{\n// Code here\n}\n}\n\nIf your policy has its own comment method, it will override the global policy method."}},"/backend/controller":{"title":"Controller","data":{"":"Controllers can help group related request handling logic into a single class. Controller classes are located under src\\Http\\Controllers directory within your package directory.Here is the directory structure of the metafox\\blog app\npackages/\nmetafox/\nblog/\nsrc/\nHttp/\nControllers/\nApi/ // Contains API Gateway classes\nBlogController.php\nCategoryController.php\nv1/ // Contains Controller classes for certain version\nBlogController.php\nCategoryController.php\nv2/ // Contains Controller classes for certain version\nBlogController.php\n\nNext step, we are going to take a deeper look into the Gateway class, the BlogController at src/Http/Controllers/Api/BlogController.php, which is extended from the abstract class GatewayController,\n v1\\BlogController::class,\n'v2' => v2\\BlogController::class,\n];\n\n// DO NOT IMPLEMENT ACTION HERE.\n}\nThe BlogController class defines its property controllers to associate 2 versions with 2 different BlogController classes.GatewayController class supports parsing the ver parameter from routing and invoking the respective Controller class associated with that versionFor example, with the route /api/v1/blog/18, routing resolves to MetaFox\\Blog\\Http\\Controllers\\Api\\BlogController::show with parameters ver=v1 and id=18, GatewayController class is dispatched with the same show method, then forward the dispatched call to v1/BlogController::show method.","api-fallback#API Fallback":"If a method isn't defined in the Controller class of certain version, the call will be dispatched to the class method of the previous version.For example with the route /api/v2/blog/18, routing resolves MetaFox\\Blog\\Http\\Controllers\\Api\\BlogController::show with parameters ver=v2 and id=18.GatewayController class will try to dispatch the call with the same show method with parameters of ver=v2 and id=18 in v2\\BlogController class as normal. But if there are no show methods in v2\\BlogController class, the call will be forwarded to v1\\BlogController::show method.","api-controller#API Controller":"In this section, we will see how the Controller classes of certain API version are defined for main action controllers.Below is the sample BlogController class of version v1. This class is located under src/Http/Controllers/Api/v1/BlogController.php\nrepository->viewBlog(user(), $id);\n\nreturn new BlogDetail($blog);\n}\n\n// define other methods.\n}","http-request#HTTP Request":"HTTP Request classes of an app are located under src/Http/Requests directory. Here is the directory structure of the metafox\\blog app\npackages/\nmetafox/\nblog/\nsrc/\nHttp/\nRequests/\nv1/ // contain Requests class for Api version `v1`\nBlog/ // For blog content\nIndexRequest.php // for browsing blog request\nEditFormRequest.php // for edit form request\nYou can read more about Laravel Docs about HTTP Request and HTTP Responses."}},"/backend/datagrid":{"title":"Introduction","data":{"":"MetaFox provides feature data grid builder to configure a JSON structure to render data grid in the frontend.","define-datagrid#Define DataGrid":"enableCheckboxSelection(true);\n\n$this->setSearchForm(new SearchPhraseForm());\n$this->setDataSource('/admincp/phrase', ['q' => ':q','limit'=>50]);\n\n$this->addColumn('id')\n->header('ID')\n->width(80);\n\n$this->addColumn('key')\n->header(__p('core::locale.key_name'))\n->width(200);\n\n$this->addColumn('namespace')\n->header(__p('core::locale.namespace'))\n->width(200);\n\n$this->addColumn('group')\n->header(__p('core::locale.group'))\n->width(120);\n\n$this->addColumn('package_id')\n->header(__p('core::locale.module_name'))\n->width(120);\n\n$this->addColumn('text')\n->header(__p('core::locale.translation'))\n->flex(1);\n\n/*\n* Add default actions\n*/\n$this->withActions(function (Actions $actions) {\n$actions->addDefaults('admincp/phrase');\n});\n\n/*\n* with batch menu actions\n*/\n$this->withBatchMenu(function (BatchActionMenu $menu) {\n$menu->asButton();\n$menu->withDelete();\n$menu->withCreate(__p('core::phrase.add_new_phrase'));\n});\n\n/*\n* with item action menus\n*/\n$this->withItemMenu(function (ItemActionMenu $menu) {\n$menu->withEdit();\n$menu->withDelete();\n});\n}\n}\n\nExample Reponse from data grid api.\n{\n\"status\": \"success\",\n\"data\": {\n\"checkboxSelection\": true,\n\"dataSource\": {\n\"apiUrl\": \"/admincp/phrase\",\n\"apiParams\": { \"q\": \":q\", \"limit\": 50 }\n},\n\"itemActionMenu\": {\n\"variant\": null,\n\"items\": [\n{\n\"name\": \"editItem\",\n\"icon\": \"ico-pencil-o\",\n\"value\": \"row/edit\",\n\"label\": \"Edit\",\n\"params\": { \"action\": \"editItem\" }\n},\n{\n\"name\": \"deleteItem\",\n\"icon\": \"ico-trash\",\n\"value\": \"row/remove\",\n\"label\": \"Delete\",\n\"params\": { \"action\": \"deleteItem\" },\n\"showWhen\": []\n}\n]\n},\n\"batchActionMenu\": {\n\"variant\": \"IconLabel\",\n\"items\": [\n{\n\"name\": \"deleteItem\",\n\"icon\": \"ico-trash\",\n\"value\": \"row/batchRemove\",\n\"label\": \"Delete\",\n\"style\": \"danger\",\n\"params\": { \"action\": \"deleteItems\" }\n},\n{\n\"name\": \"addItem\",\n\"icon\": \"ico-plus\",\n\"value\": \"row/add\",\n\"label\": \"Add New Phrase\",\n\"disabled\": false,\n\"params\": { \"action\": \"addItem\" }\n}\n]\n},\n\"actions\": {\n\"deleteItems\": { \"apiUrl\": \"admincp/phrase\" },\n\"deleteItem\": { \"apiUrl\": \"admincp/phrase/:id\" },\n\"addItem\": { \"apiUrl\": \"admincp/phrase/form\" },\n\"editItem\": { \"apiUrl\": \"admincp/phrase/form/:id\" },\n\"activeItem\": { \"apiUrl\": \"admincp/phrase/active/:id\" }\n},\n\"columns\": [\n{ \"field\": \"id\", \"headerName\": \"ID\", \"width\": 80 },\n{ \"field\": \"key\", \"headerName\": \"Translation Key\", \"width\": 200 },\n{ \"field\": \"namespace\", \"headerName\": \"Namespace\", \"width\": 200 },\n{ \"field\": \"group\", \"headerName\": \"Group\", \"width\": 120 },\n{\n\"field\": \"package_id\",\n\"headerName\": \"Package\",\n\"width\": 120\n},\n{ \"field\": \"text\", \"headerName\": \"Translation\", \"flex\": 1 }\n]\n},\n\"message\": null,\n\"error\": null\n}","configuration#Configuration":"","enablecheckboxselection#enableCheckboxSelection":"Enable checkbox for the first item data grid."}},"/backend/eloquent":{"title":"Eloquent","data":{"":"Eloquent is an object-relational mapper (ORM) supported by default in Laravel framework to make it enjoyable when interacting with your database. For more info, you may like to read Eloquent ORM first.","migrations#Migrations":"Migrations, like version control for your database, allow your team to define and share the application's database schema definitions. With database migrations, you no longer have to tell teammates to manually update required changes in their local database schema after pulling certain commits from source control.Migrations classes are under src/Database/Migrations directory.\npackages/metafox/blog/src/: Package source root\nDatabase/\nMigrations/\n2021_02_04_034457_CreateBlogTables.php : Migrations for blog schema\n2021_02_05_034457_CreateCategoryTables.php : Migrations for category schema\nEach Migrations class is child of Illuminate\\Database\\Migrations\\Migration class, and contains 2 up and down methods. Let's see the Migration CreateBlogTables class below\nbigIncrements('id');\n\n// other columns\n});\n}\n\n// setup other table\n}\n\n/**\n* Reverse the migrations.\n*\n* @return void\n*/\npublic function down()\n{\nDbTableHelper::dropStreamTables('blog');\n}\n}\nMetaFox platform is shipped with DbTableHelper, to support creating platform schemas easily and quickly.For more info, please read Laravel migrations","seeders#Seeders":"Each package includes the ability to seed your database with default data using Seeder classes.\nAll package Seeder classes are stored in the src/Database/Seeders directory. By default, a DatabaseSeeder class is defined for you. With this class, you may use the call method to run other Seeder classes. It will allow you to control the seeding order.\ndispatch('search.updated', [$model]);","listeners#Listeners":"Define Event Listener\n [\nSearchUpdatedListener::class,\n],\n];\n}\n}","response#Response":"TBDFor futher info, please read Laravel Event","bail#Bail":"Sometimes, you may need dispatcher to stop running other listeners whenever a listener returns a not-null value.\ndispatch('search.updated', [$model], $bail=true);\n\nIn the above sample code, with $bail=true parameter, at the first time a listener returns not-null value, the dispatcher will stop running remaining registered Listener and returns to $response value."}},"/backend/event-list":{"title":"Event Details","data":{"":"Here is the list of Events supported on MetaFox\n+---------------------------------------------------------+------------------------------------------------------------------------+\n| Event | Listeners |\n+---------------------------------------------------------+------------------------------------------------------------------------+\n| activity.count_feed_pending_on_owner | MetaFox\\Activity\\Listeners\\CountFeedPendingOnOwnerListener |\n| activity.delete_feed | MetaFox\\Activity\\Listeners\\DeleteFeedListener |\n| activity.get_feed | MetaFox\\Activity\\Listeners\\GetFeedListener |\n| activity.get_feed_by_item_id | MetaFox\\Activity\\Listeners\\GetFeedByItemIdListener |\n| activity.get_feed_id | MetaFox\\Activity\\Listeners\\GetFeedIdListener |\n| activity.push_feed_on_top | MetaFox\\Activity\\Listeners\\PushFeedOnTopListener |\n| activity.redundant | MetaFox\\Activity\\Listeners\\FeedRedundantListener |\n| activity.sponsor_in_feed | MetaFox\\Activity\\Listeners\\SponsorInFeedListener |\n| activity.update_feed_item_privacy | MetaFox\\Activity\\Listeners\\UpdateFeedItemPrivacy |\n| album.get_album_item | MetaFox\\Photo\\Listeners\\GetAlbumItemListener |\n| background-status.get_bg_status_image | MetaFox\\BackgroundStatus\\Listeners\\GetBgStatusImageListener |\n| comment.related_comments | MetaFox\\Comment\\Listeners\\RelatedCommentsListener |\n| comment.related_comments.item_detail | MetaFox\\Comment\\Listeners\\RelatedCommentsItemDetailListener |\n| core.check_privacy_list | MetaFox\\Core\\Listeners\\CheckPrivacyListListener |\n| core.get_privacy_id | MetaFox\\Core\\Listeners\\GetPrivacyIdListener |\n| core.parse_content | MetaFox\\Friend\\Listeners\\ParseFeedContentListener |\n| core.privacy.check_privacy_member | MetaFox\\Core\\Listeners\\CheckPrivacyMember |\n| core.total_view | MetaFox\\Activity\\Listeners\\FeedRedundantListener |\n| core.user_privacy.get_privacy_id | MetaFox\\Core\\Listeners\\GetPrivacyIdForUserPrivacyListener |\n| feed.composer | MetaFox\\Core\\Listeners\\FeedComposerListener |\n| feed.composer.edit | MetaFox\\Core\\Listeners\\FeedComposerEditListener |\n| friend.can_add_friend | MetaFox\\Friend\\Listeners\\CanAddFriendListener |\n| friend.count_total_friend | MetaFox\\Friend\\Listeners\\CountTotalFriendListener |\n| friend.count_total_mutual_friend | MetaFox\\Friend\\Listeners\\CountTotalMutualFriendListener |\n| friend.create_tag_friends | MetaFox\\Friend\\Listeners\\CreateTagFriendsListener |\n| friend.delete_tag_friend | MetaFox\\Friend\\Listeners\\DeleteTagFriendListener |\n| friend.friend_ids | MetaFox\\Friend\\Listeners\\GetFriendIdsListener |\n| friend.get_count_new_friend_request | MetaFox\\Friend\\Listeners\\CountNewFriendRequestListener |\n| friend.get_friend_ship | MetaFox\\Friend\\Listeners\\GetFriendShipListener |\n| friend.get_photo_tag_friends | MetaFox\\Friend\\Listeners\\GetPhotoTagFriendsListener |\n| friend.get_suggestion | MetaFox\\Friend\\Listeners\\GetSuggestionListener |\n| friend.get_tag_friend_by_id | MetaFox\\Friend\\Listeners\\GetTagFriendByIdListener |\n| friend.get_tag_friends | MetaFox\\Friend\\Listeners\\GetTagFriendsListener |\n| friend.is_friend | MetaFox\\Friend\\Listeners\\IsFriendListener |\n| friend.is_friend_of_friend | MetaFox\\Friend\\Listeners\\IsFriendOfFriendListener |\n| friend.update_tag_friends | MetaFox\\Friend\\Listeners\\UpdateTagFriendsListener |\n| friendList.check_privacy_list | MetaFox\\Friend\\Listeners\\CheckPrivacyListListener |\n| group.get_privacy_for_setting | MetaFox\\Group\\Listeners\\PrivacyForSetting |\n| group.get_search_resource | MetaFox\\Group\\Listeners\\GetSearchResourceListener |\n| group.get_user_preview | MetaFox\\Group\\Listeners\\UserPreviewListener |\n| group.update_cover | MetaFox\\Group\\Listeners\\UpdateGroupCover |\n| hashtag.create_hashtag | MetaFox\\Hashtag\\Listeners\\ItemTagAwareListener |\n| hashtag.update_total_item | MetaFox\\Hashtag\\Listeners\\UpdateTotalItemListener |\n| like.is_liked | MetaFox\\Like\\Listeners\\IsLikedListener |\n| like.most_reactions | MetaFox\\Like\\Listeners\\MostReactionsListener |\n| like.user_reacted | MetaFox\\Like\\Listeners\\UserReactedListener |\n| models.notify.created | MetaFox\\Core\\Listeners\\ModelCreatedListener |\n| models.notify.creating | MetaFox\\Core\\Listeners\\ModelCreatingListener |\n| models.notify.deleted | MetaFox\\Core\\Listeners\\ModelDeletedListener |\n| models.notify.updated | MetaFox\\Core\\Listeners\\ModelUpdatedListener |\n| models.notify.updating | MetaFox\\Core\\Listeners\\ModelUpdatingListener |\n| packages.installed | MetaFox\\Sticker\\Listeners\\PackageInstalledListener |\n| packages.scan | MetaFox\\Core\\Listeners\\PackageScanListener |\n| packages.updated | MetaFox\\User\\Listeners\\PackageUpdatedListener |\n| notification.delete_notification_by_type_and_notifiable | MetaFox\\Notification\\Listeners\\DeleteNotifyByTypeAndNotifiableListener |\n| notification.getEmailNotificationSettings | MetaFox\\Notification\\Listeners\\GetEmailNotificationSettingsListener |\n| notification.get_count_new_notification | MetaFox\\Notification\\Listeners\\GetNewNotificationCount |\n| notification.updateEmailNotificationSettings | MetaFox\\Notification\\Listeners\\UpdateEmailNotificationSettingsListener |\n| packages.deleting | MetaFox\\Core\\Listeners\\PackageDeletingListener |\n| page.get_privacy_for_setting | MetaFox\\Page\\Listeners\\PrivacyForSetting |\n| page.get_search_resource | MetaFox\\Page\\Listeners\\GetSearchResourceListener |\n| page.get_user_preview | MetaFox\\Page\\Listeners\\UserPreviewListener |\n| page.update_cover | MetaFox\\Page\\Listeners\\UpdatePageCover |\n| photo.create | MetaFox\\Photo\\Listeners\\PhotoCreateListener |\n| photo.media_remove | MetaFox\\Photo\\Listeners\\MediaRemoveListener |\n| photo.media_update | MetaFox\\Photo\\Listeners\\MediaUpdateListener |\n| photo.media_upload | MetaFox\\Photo\\Listeners\\MediaUploadListener |\n| photo.update_avatar_path | MetaFox\\Photo\\Listeners\\UpdateAvatarPathListener |\n| search.created | MetaFox\\Search\\Listeners\\ModelCreatedListener |\n| search.deleted | MetaFox\\Search\\Listeners\\ModelDeletedListener |\n| search.updated | MetaFox\\Search\\Listeners\\ModelUpdatedListener |\n| site_settings.updated | MetaFox\\ChatPlus\\Listeners\\SiteSettingUpdated |\n| sticker.get_sticker_image | MetaFox\\Sticker\\Listeners\\GetStickerImageListener |\n| tag.get_search_resource | MetaFox\\Hashtag\\Listeners\\GetSearchResourceListener |\n| user.blocked | MetaFox\\Friend\\Listeners\\UserBlockedListener |\n| user.get_mentions | MetaFox\\User\\Listeners\\UserGetMentions |\n| user.get_privacy_for_setting | MetaFox\\User\\Listeners\\PrivacyForSetting |\n| user.get_search_resource | MetaFox\\User\\Listeners\\GetSearchResourceListener |\n| user.get_user_preview | MetaFox\\User\\Listeners\\UserPreviewListener |\n| user.social_account.callback | MetaFox\\Facebook\\Listeners\\UserSocialAccountCallbackListener |\n| user.social_account.request | MetaFox\\Facebook\\Listeners\\UserSocialAccountRequestListener |\n| video.mux.webhook_callback | MetaFox\\Video\\Listeners\\MuxWebhookCallback |\n+---------------------------------------------------------+------------------------------------------------------------------------+\nNext, we are going to dive into deeper about all supported Events","model-events#Model Events":"These are events that were called in EloquentModelObserver class located at packages/platform/src/Support/EloquentModelObserver.php","model-creating-event#Model Creating Event":"Event name: models.notify.creating\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.creating', [$model]);","model-created-event#Model Created Event":"Event name: models.notify.created\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.created', [$model]);","model-updating-event#Model Updating Event":"Event name: models.notify.updating\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.updating', [$model]);","model-updated-event#Model Updated Event":"Event name: models.notify.updated\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.updated', [$model]);","model-deleted-event#Model Deleted Event":"Event name: models.notify.deleted\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.deleted', [$model]);","package-events#Package Events":"","package-installed-event#Package installed Event":"This Event will be called after running the install command\nEvent name: packages.installed","package-updated-event#Package updated Event":"This Event will be called after running the updated command\nEvent name: packages.updated","package-scan-event#Package scan Event":"Scan all package directories and MetaFox config files to update database\nEvent name: packages.scan","package-export-menu-event#Package Export Menu Event":"This event will be called when exporting package\nEvent name: packages.export_menu","package-export-phrase-event#Package Export Phrase Event":"This event will be called when exporting phrase\nEvent name: modules.export_phrase","package-deleting-event#Package Deleting Event":"This event will be called when uninstalling package\nEvent name: packages.deleting","core-package-events#Core Package Events":"","check-privacy-list-event#Check Privacy List Event":"Called when checking if user has owner privacy list\nEvent name: core.check_privacy_list","parse-content-event#Parse content Event":"Called when parsing Content. Can be use when converting mention, tag,.. in text\nEvent name: core.parse_content","check-privacy-member-event#Check Privacy member Event":"Called when checking user has privacy member\nEvent name: core.privacy.check_privacy_member","total-view-event#Total View Event":"Called when getting total view on a Content to feed\nEvent name: core.total_view","get-privacy-id-event#Get Privacy Id Event":"Privacy id form user_id and privacy\nEvent name: core.user_privacy.get_privacy_id","activity-package-events#Activity Package Events":"","count-pending-feed-on-owner-event#Count Pending Feed On Owner Event":"Get count pending feed\nEvent name: activity.count_feed_pending_on_owner","delete-feed-event#Delete Feed Event":"Event name: activity.delete_feed","get-feed-event#Get Feed Event":"Event name: activity.get_feed","get-feed-by-item-id-event#Get Feed By Item Id Event":"Event name: activity.get_feed_by_item_id","get-feed-by-id-event#Get Feed By Id Event":"Event name: activity.get_feed_id","push-feed-on-top-event#Push Feed On Top Event":"Event name: activity.push_feed_on_top","push-feed-on-top-event-1#Push Feed On Top Event":"Event name: activity.push_feed_on_top","redundant-data-event#Redundant Data Event":"Called when updating info of total comment, total like, total view,... from resource Content to feed\nEvent name: activity.redundant","sponsor-feed-event#Sponsor Feed Event":"Event name: activity.sponsor_in_feed","update-feed-item-privacy-event#Update feed item privacy Event":"Called when update privacy on feed, also update on item\nEvent name: activity.update_feed_item_privacy","feed-composer-event#Feed Composer Event":"Called when create feed by feed form\nEvent name: feed.composer","edit-feed-composer-event#Edit Feed Composer Event":"Event name: feed.composer.edit","friend-package-events#Friend Package Events":"","check-can-add-friend-event#Check Can Add Friend Event":"Event name: friend.can_add_friend","count-total-friend-event#Count Total Friend Event":"Event name: friend.count_total_friend","count-total-mutual-friend-event#Count Total Mutual Friend Event":"Event name: friend.count_total_mutual_friend","create-tag-friend-event#Create Tag Friend Event":"Event name: friend.create_tag_friends","delete-tag-friend-event#Delete Tag Friend Event":"Event name: friend.delete_tag_friend","get-friend-ids-event#Get Friend Ids Event":"Event name: friend.friend_ids","get-friendship-event#Get Friendship Event":"Event name: friend.get_friend_ship","count-new-friend-request-event#Count New Friend Request Event":"Event name: friend.get_count_new_friend_request","get-tagged-friend-in-photo-event#Get Tagged Friend In Photo Event":"Event name: friend.get_photo_tag_friends","get-friend-suggestion-event#Get Friend Suggestion Event":"Event name: friend.get_suggestion","get-tagged-friend-by-tag-id-event#Get Tagged Friend By Tag Id Event":"Event name: friend.get_tag_friend_by_id","get-tagged-friend-by-tag-id-event-1#Get Tagged Friend By Tag Id Event":"Event name: friend.get_tag_friend_by_id","get-tagged-friends-event#Get Tagged Friends Event":"Event name: friend.get_tag_friends","check-is-friend-of-friend-event#Check Is Friend Of Friend Event":"Event name: friend.is_friend_of_friend","update-tagged-friend-event#Update Tagged Friend Event":"Event name: friend.update_tag_friends","comment-package-events#Comment Package Events":"","get-related-comments-event#Get Related Comments Event":"Event name: comment.related_comments","get-related-comments-for-item-detail-event#Get Related Comments For Item Detail Event":"Event name: comment.related_comments.item_detail","background-status-package-events#Background Status Package Events":"","get-background-status-by-id-event#Get Background Status By Id Event":"Event name: background-status.get_bg_status_image","group-package-events#Group Package Events":"","get-privacy-for-setting-event#Get Privacy For Setting Event":"Event name: group.get_privacy_for_setting","get-search-resource-event#Get Search Resource Event":"Event name: group.get_search_resource","get-user-preview-event#Get User Preview Event":"Event name: group.get_user_preview","update-cover-event#Update Cover Event":"Event name: group.update_cover","hashtag-package-events#HashTag Package Events":"","create-hashtag-event#Create HashTag Event":"Event name: hashtag.create_hashtag","update-total-item-event#Update Total Item Event":"Event name: hashtag.update_total_item","get-search-resource-event-1#Get Search Resource Event":"Event name: tag.get_search_resource","like-package-events#Like Package Events":"","check-is-liked-event#Check Is Liked Event":"Event name: like.is_liked","get-most-reactions-event#Get Most Reactions Event":"Event name: like.most_reactions","get-user-reacted-event#Get User Reacted Event":"Event name: like.user_reacted","notification-package-events#Notification Package Events":"","delete-notification-event#Delete Notification Event":"When deleting an item, also delete notification of an item\nEvent name: notification.delete_notification_by_type_and_notifiable","get-email-notification-settings-event#Get Email Notification Settings Event":"Event name: notification.getEmailNotificationSettings","count-new-notification-settings-event#Count New Notification Settings Event":"Event name: notification.get_count_new_notification","update-notification-settings-event#Update Notification Settings Event":"Event name: notification.updateEmailNotificationSettings","page-package-events#Page Package Events":"","get-privacy-for-setting-event-1#Get Privacy For Setting Event":"Event name: page.get_privacy_for_setting","get-search-resource-event-2#Get Search Resource Event":"Event name: page.get_search_resource","get-user-preview-event-1#Get User Preview Event":"Event name: page.get_user_preview","update-cover-event-1#Update Cover Event":"Event name: page.update_cover","photo-package-events#Photo Package Events":"","create-photo-event#Create Photo Event":"Event name: photo.create","remove-media-event#Remove Media Event":"Event name: photo.media_remove","update-media-event#Update Media Event":"Event name: photo.media_update","upload-media-event#Upload Media Event":"Event name: photo.media_upload","update-avatar-path-event#Update Avatar Path Event":"Event name: photo.update_avatar_path","search-package-events#Search Package Events":"","create-search-record-event#Create Search Record Event":"This event will be called after Content was created\nEvent name: search.created","update-search-record-event#Update Search Record Event":"This event will be called after content was updated\nEvent name: search.updated","delete-search-record-event#Delete Search Record Event":"This event will be called after content was deleted\nEvent name: search.deleted","sticker-package-events#Sticker Package Events":"","get-sticker-image-event#Get Sticker Image Event":"Event name: sticker.get_sticker_image","user-package-events#User Package Events":"","block-user-event#Block User Event":"Event name: user.blocked","get-mentions-event#Get Mentions Event":"Event name: user.get_mentions","get-privacy-for-setting-event-2#Get Privacy For Setting Event":"Event name: user.get_privacy_for_setting","get-search-resource-event-3#Get Search Resource Event":"Event name: user.get_search_resource","get-user-preview-event-2#Get User Preview Event":"Event name: user.get_user_preview","social-account-request-event#Social Account Request Event":"Event name: user.social_account.request","social-account-callback-event#Social Account Callback Event":"Event name: user.social_account.callback","video-package-events#Video Package Events":"","mux-webhook-callback-event#Mux Webhook Callback Event":"Event name: video.mux.webhook_callback"}},"/backend/installation":{"title":"MetaFox Developer Installation","data":{"":"This MetaFox Developer package includes the MetaFox Core and some basic apps such as Feed, Photos, Members, Blogs, Polls.","system-requirements#System Requirements":"On the Development environment: since we have to build MetaFox Frontend, your server will need to have at least 4G RAM as the Recommendation for ReactThis article will guide you how to set up Local Development with docker. Thus, the server will need to have docker and docker-compose installed first.","download-source#Download Source":"You can download Developer source from Client Area, under MetaFox Dev license. Then, extract the package on your server.For example, all source files will be uploaded to your server under home/metafox folder","local-installation-steps#Local Installation Steps":"Open Terminal and log in SSH to your server. It is necessary to note that you will need to log in with SSH user having permissions to run dockerThen, run the following command\ncd /home/metafox\nbash install.sh\nAfter starting successfully, you can check the MetaFox site at http://your_server_IP:8081 or http://your_domain:8081","issues#Issues":"If you got issues with MetaFox Installation, please open new tickets in Client Area with department of 3rd Party Developer for further support.","references#References":"Laravel\nAuthentication\nAuthorization\nImage Processing\nDebug Tool\nStorage System\nPackage Management\nGlobalization\nMessage Queue","unit-test#Unit Test:":"Testing\nMockery\nFaker"}},"/backend/form":{"title":"Form","data":{"":"","create-form#Create Form":"MetaFox provide form configuration feature to define JSON structure that frontend can be rendered via RestFul api.\n\nnamespace MetaFox\\Core\\Http\\Resources\\v1\\Admin;\n\nuse MetaFox\\Platform\\Support\\Form\\AbstractForm;\n\nclass GeneralSiteSettingForm extends AbstractForm\n{\nprotected function prepare():void\n{\n\n// add form configuration\n}\n\nprotected funciton initialize():void\n{\n// add fields structure\n}\n}\nShow form api in controller.\n\n\nuse MetaFox\\Core\\Http\\Resources\\v1\\Admin\\GeneralSiteSettingForm;\n\nclass ApiController{\n\npublic function showForm()\n{\nreturn new GeneralSiteSettingForm();\n}\n}","form-fields#Form Fields":"AbstractForm group related fields in sections, each section contains FormFields, AbstractForm add form fields in initialize() method. There are 02 sections\nbasic and footer are support.Supported fields configuration is located at ./config/form.php.Create text field directory\n\n$text = new TextField(['name'=>'firstName']);\n\nCreate via builder service\nuse MetaFox\\Platform\\Form\\Builder;\n\n$text = Builder::text('firstName'); // get TextField instance\n\n\nuse MetaFox\\Platform\\Form\\Builder;\n\nclass GeneralSiteSettingForm extends AbstractForm\n{\n\nprotected funciton initialize():void\n{\n// section basic\n$basic = $this->addBasic();\n\n// add field\n$basic->addField(\nBuilder::text('general.site_name') // Create Text field with name \"general.site_name\"\n->label('Name Of Site') // Add label\n->description('Name of your site.') // Add description\n);\n\n// add field\n$basic->addField(\nBuilder::text('general.site_title') // Create Text field with name \"general.site_title\"\n->label('Name Title') // Add label\n->description('Fill your site title') // Add description\n);\n\n// add footer structure\n$this->addFooter()\n->addField(\nBuilder::submit() // Add \"submit\", field use default name=\"_submit\"\n->label(__p('core::phrase.save_changes'))\n);\n}\n}\nMetaFox comes with a list of build-in suppor form fields. Fore more information checkout","form-validation#Form Validation":"Validation configuration defines JSON structure to\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::text('general.site_title') // Create Text field with name \"general.site_title\"\n->label('Name Title') // Add label\n->yup( // add validation\nYup::string() // Add yup string at https://dev-docs.metafox.com/frontend/validation#string\n->required(__p('core::validation.site_title_could_not_be_blank')) // set required\n)\nMetaFox comes with a list of build-in suppor form validation. Fore more information checkout","yupstring#Yup::string()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#string\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuiler::text('general.site_title')\n->yup( // add validation\nYup::string()\n->required(__p('core::validation.site_title_could_not_be_blank')) // set required\n->minLength(5) // Set min length rule\n->maxLength(64) // Set max length\n->matches('^\\w+$') // set regex match pattern\n->lowercase() // Require lowercase format\n// ->uppercase() // Require uppercase format\n->email() // Require email format\n// ->url() // Require full url format\n)","yupnumber#Yup::number()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#number\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::text('general.site_title')\n->yup( // add validation\nYup::string()\n->required(__p('core::validation.site_title_could_not_be_blank')) // set required\n->min(5) // Set \">=\" compare\n->max(64) // Set \"<=\" compare\n// ->lessThan(100) // Set \"<\" compare\n// ->moreThan() // Set \">\" compare\n// ->int()\n->unint() // Require unsigned int\n// ->positive() // Require positive number\n// ->negative() // Require negative number\n)","yupdate#Yup::date()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#date\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::text('event.start_date')\n->yup( // add validation\nYup::string()\n->required(__p('core::validation.this_is_required_field')) // set required\n->min('2022-10-10') // Set min length rule\n->max('2026-10-10') // Set max length\n)","yupboolean#Yup::boolean()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#boolean\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::checkbox('event.enable_close')\n->yup( // add validation\nYup::boolean()\n->required(__p('core::validation.this_is_required_field')) // set required\n)","yuparray#Yup::array()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#array\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::questions('questions')\n->yup( // add validation\nYup::array()\n->of(Yup::string()) // define validator for each array\n->min(2)\n->max(5)\n)","yupobject#Yup::object()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#object\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::jobs('person')\n->yup( // add validation\nYup::object()\n->addProperty( // add property\n'firstName',\nYup:string()->required()\n)\n->addProperty(\n'lastName',\nYup:string()->required()\n)\n->addProperty(\n'email',\nYup::string()->required()->email()\n);\n)"}},"/backend/interfaces":{"title":"Interfaces","data":{"":"","contract-user#Contract User":"This is an Interface of an User. Contract User is a child of Contract Content, and has more abilities.\n\nnamespace MetaFox\\Platform\\Contracts;\n\ninterface User extends Content, BigNumberId\n{\n/**\n* Result must contain entity_type, user_name, user_image, name.\n*\n* @return array\n*/\npublic function toUserResource(): array;\n\n/**\n* Determine if resource can be blocked.\n*\n* @return bool\n*/\npublic function canBeBlocked(): bool;\n}\n\nA special ability of a Contract User is that it uses BigNumberId to set ID (with setEntityId method) for entities tables ( to support global and unique ID for all resources).\n\nnamespace MetaFox\\Platform\\Contracts;\n\ninterface BigNumberId\n{\npublic function entityId();\n\npublic function setEntityId(int $id);\n\npublic function entityType();\n}\n\nTo define which details are stored in entities table, we can use the toUserResource() method. For example: in User, you can define this method as below:\n\npublic function toUserResource()\n{\nreturn [\n'entity_type' => $this->entityType(),\n'user_name' => $this->user_name,\n'user_image' => $this->profile != null ? $this->profile->user_image : null,\n'name' => $this->full_name,\n];\n}"}},"/backend/mailer":{"title":"Mailer","data":{"":"","add-custom-mail-transport#Add Custom Mail Transport":"In other to add custom storage, add new driver type=\"mail-transport\" to drivers.phpexample\n 'MetaFox\\\\Core\\\\Http\\\\Resources\\\\v1\\\\Admin\\\\MailerSmtpSettingForm',\n'type' => 'form-mailer',\n'name' => 'smtp',\n'version' => 'v1',\n'package_id' => 'metafox/core',\n'alias' => null,\n'is_admin' => 1,\n'is_active' => 1,\n'is_preload' => 0,\n'title' => 'SMTP Mail Transport',\n'url' => '',\n'description' => '',\n];\n]"}},"/backend/routing":{"title":"Basic Routing","data":{"":"MetaFox platform supports RESTful API routes.Routing defines the way how platform resolve URI request info into Controller action,\nGenerally, you can define routes of your app package in the routes/api.php for frontend api and routes/api-amin.php for admincp apis.This file re-uses Laravel Routing system at package-level, for futher information read out laravel routingroutes/api.php\ngroup(function () {\nRoute::patch('blog/sponsor/{id}', 'sponsor');\nRoute::patch('blog/feature/{id}', 'feature');\nRoute::patch('blog/approve/{id}', 'approve');\nRoute::put('blog/publish/{id}', 'publish');\n});\n\n\nRoute::resource('blog', BlogController::class);\nroutes/api-admin.php\nas('admin') // add this prefix to prevent dulicated route names with blog resource in `api.php`\n->resource('category', CategoryAdminController::class);\n\nRoute::as('admin')\n->resource('blog',BlogAdminController::class);","route-method#Route Method":" __NAMESPACE__, // define all of controller\n'middleware' => 'auth:api',\n'prefix'=> 'blog'\n], function () {\n// all sub route have namespace\n});","route-resource#Route Resource":"Routing for RESTful API requests has a short method Route::resouce($uri, $callback)\nget('/user/{id}', 'show');\n\nRoute::controller(BlogController::class)\n->get('/user/info/{id?}', 'info');\n\n// UserController.php\n\nget('/user/{postId}/comments/{comemntId?}', 'view');","api-versioning#API Versioning":"In real world, when you have a long-time project with multiple versions released, the platform wraps all routes defined in api.php within a prefix /api/{ver}. Parameter {ver} will be passed to Controller action to help us define correct response base on the given version."}},"/backend/package":{"title":"App Package","data":{"":"MetaFox Backend utilizes Laravel Service Providers to provide a robust, re-useable, and flexible way to extend platform features.","app-directory-structure#App Directory Structure":"The packages directory contains all packages and is organized as below:\npackages/\n[vendor_name]/\npackage-1/\npackage-2/\nvendor_name should be your company name to avoid duplication with others.In the next section, we will look into details of app directory structure.\ncomposer.json : Package infomration and composer dependencies\nconfig/ :\nconfig.php : Contain package configuration\nresources/ :\nlangs/\nen/ : Language file for `en` locale\nphrase.php : Define phrases for group `phrase`\nvalidator.php : Define phrases for group `validator`\nmenu/ :\nmenus.php :\nmenuitems.php :\nroutes/ :\napi.php : Define RESTful API routes\nweb.php : Define web routes\nsrc/\nContracts/ : Contains basic interface defination\nDatabase/ :\nFactories : Contain database model [factories](https://laravel.com/docs/9.x/database-testing#defining-model-factories)\nMigrations : Contain database [migrations](https://laravel.com/docs/9.x/migrations#main-content)\nSeeders : Contain database [seeders](https://laravel.com/docs/9.x/seeding#introduction)\nPackageSeeder.php : Entry point for database seeder\nHttp/\nControllers/ :\nRequests/ :\nResources/ :\nJobs/ :\nListeners/ :\nMail/ :\nNotification/ :\nModels/ :\nObservers/ :\nPolicies/ :\nProviders/ :\nRepositories/ :\nRules/ :\ntests/ :\nFeatures/ :\nUnit/ :","composerjson#composer.json":"{\n\"name\": \"metafox/video\",\n\"version\": \"5.0.7\",\n\"description\": \"\",\n\"authors\": [\n{\n\"name\": \"phpFox\",\n\"email\": \"dev@phpfox.com\",\n\"homepage\": \"https://www.phpfox.com\"\n}\n],\n\"extra\": {\n\"metafox\": {\n\"core\": false,\n\"alias\": \"video\",\n\"asset\": \"video\",\n\"namespace\": \"MetaFox\\\\Video\",\n\"path\": \"packages/metafox/video\",\n\"title\": \"Video\",\n\"internalAdminUrl\": \"/video/setting\",\n\"providers\": [\"MetaFox\\\\Video\\\\Providers\\\\PackageServiceProvider\"],\n\"frontend\": {\n\"@metafox/video\": \"*\"\n},\n\"frontendPaths\": [\"packages/metafox/video\"],\n\"require\": {\n\"metafox/core\": \"5.1.3 - 5.1.4\"\n},\n\"aliases\": {}\n}\n},\n\"autoload\": {\n\"psr-4\": {\n\"MetaFox\\\\Video\\\\\": \"src/\"\n}\n},\n\"require\": {\n\"php-ffmpeg/php-ffmpeg\": \"^1.0\"\n},\n\"autoload-dev\": {\n\"psr-4\": {\n\"MetaFox\\\\Video\\\\\": \"\",\n\"MetaFox\\\\Video\\\\Tests\\\\\": \"tests/\"\n}\n}\n}","base-information#Base Information":"","name#Name":"name: The name of the app package. It consists of vendor name and project name, separated by a slash (/)For examples:\nmetafox/platform\nmetafox/blog\n\nThe app name MUST be lowercase and consist of words separated by -, . or _. The complete name should match the regular expression ^[a-z0-9]([_.-]?[a-z0-9]+)_/[a-z0-9](<([_.]?|-{0,2})[a-z0-9]+>)\\_\\$.","description#Description":"description: A short description of the app package. Should be a one-line message.","version#Version":"version: a string specifing the version of the app package. In most cases, this field is not required and can be omitted (see below).Here are some examples of valid values for versions\n1.0.0\n1.0.2\n1.1.0\n0.2.5\n1.0.0-dev\n1.0.0-alpha3\n1.0.0-beta2","authors#Authors":"authors: Info of the authors of the package. You can specify multiple author objects here. An author object can have following properties:\nname: The author's name. Usually their real name.\nemail: The author's email address.\nhomepage: URL to the author's website.","require#Require":"require: Map of packages required by this package. The app package will not be installed unless all requirements are met.","require-dev#Require-Dev":"require-dev: Map of packages required for developing this package, or running tests, etc. The dev requirements of the root package are installed by default. Both install and update commands support the --no-dev option to prevent dev dependencies from being installed.","autoload#Autoload":"autoload.psr-4: Under the psr-4 key you can define a mapping from namespaces to paths relative to the package root. When autoloading a class like Foo\\\\Bar\\\\Baz, and a namespace prefixed Foo\\\\ are pointed to a directory src/, the autoloader will look for a file named src/Bar/Baz.php and include it if existing. Note that as opposed to the older PSR-0 style, the prefix (Foo\\\\) is not present in the file path.Namespace prefixes must end in \\\\ to avoid conflicts between similar prefixes. For example: Prefix Foo would match classes in the FooBar namespace. Thus, we use the trailing backslashes to solve the problem: Foo\\\\ and FooBar\\\\ are distinct.The PSR-4 references are all combined, during installation or update, into a single key which may be found in the generated file vendor/composer/autoload_psr4.php.autoload.files: If you want to require certain files explicitly on every request then you can use the file autoloading mechanism. This is very useful if your app package needs to include PHP functions that cannot be autoloaded by PHP\n{\n\"autoload\": {\n\"files\": [\"src/MyLibrary/functions.php\"]\n}\n}\nautoload-dev: This section allows to define autoload rules for development purposes.Classes only running for testing purposes should not be included in the main autoload rules to optimize the autoloader in production or for others to use your app package as a dependency.Therefore, it is a good solution to rely on a dedicated path for your unittests and add it within the autoload-dev section.For example:\n{\n\"autoload\": {\n\"psr-4\": { \"MyLibrary\\\\\": \"src/\" }\n},\n\"autoload-dev\": {\n\"psr-4\": { \"MyLibrary\\\\Tests\\\\\": \"tests/\" }\n}\n}","metafox#MetaFox":"MetaFox framework picks extra.metafox section for installation, exporting, upgrading, etc ...Example of extra.metafox\n{\n\"extra\": {\n\"metafox\": {\n\"core\": false,\n\"alias\": \"video\",\n\"asset\": \"video\",\n\"namespace\": \"MetaFox\\\\Video\",\n\"path\": \"packages/metafox/video\",\n\"title\": \"Video\",\n\"internalAdminUrl\": \"/video/setting\",\n\"providers\": [\"MetaFox\\\\Video\\\\Providers\\\\PackageServiceProvider\"],\n\"frontend\": {\n\"@metafox/video\": \"*\"\n},\n\"frontendPaths\": [\"packages/metafox/video\"],\n\"require\": {\n\"metafox/core\": \"5.1.3 - 5.1.4\"\n},\n\"aliases\": {}\n}\n}\n}","metafoxpath#metafox.path":"extra.metafox.path is required. Used to indicate backend package source under root application. For example: packages/metafox/blog","metafoxalias#metafox.alias":"extra.metafox.alias is required. Used to indicate the alias name of your app package. For example, you may prefer to use an alias name blog rather than fully name metafox/blog.","metafoxnamespace#metafox.namespace":"extra.metafox.namespace is required. Used to indicate the root namespace of the package. It must use trailing backslashes with \\\\. For example: MetaFox\\\\Blog","metafoxinternaladminurl#metafox.internalAdminUrl":"extra.metafox.internalAdminUrl is required. Used to indicate where to navigate when administrator click on the app in the AdminCP area.","metafoxfrontend#metafox.frontend":"extra.metafox.frontend is optional. Used to indicate frontend end packages.","metafoxfrontendpaths#metafox.frontendPaths":"extra.metafox.frontendPaths is optional. Used to indicate frontend paths when developer export packages","metafoxpeerdependencies#metafox.peerDependencies":"extra.metafox.peerDependencies is optional. Used to indicate peer dependencies backend packages when developer export packages.The packages listed in peerDependencies comes with single exported file. It's helpful for developer release a product with multiple packages dependencies.\n{\n\"peerDependencies\": [\"metafox/payment-helpers\"]\n}","metafoxproviders#metafox.providers":"extra.metafox.providers is optional. The array of fully Laravel provider classes, for futher information read out Service Provider"}},"/backend/load-reduce":{"title":"Load Reduce","data":{"":"Reducing number of database queries using array cache (cache lifetime within request lifecycle).","register#Register":"Register reducer\nclass PackageServiceProvider extends ServiceProvider\n{\n\n/**\n* Register the service provider.\n*\n* @return void\n*/\npublic function register()\n{\n$this->callAfterResolving('reducer', function ($reducer) {\n$reducer->register([\n\\MetaFox\\Saved\\Support\\LoadMissingIsSaved::class,\n]);\n});\n}\n}","sample#Sample":"Using union to pick data\nuserId();\n$items = $reducer->entities()\n->filter(fn ($x) => $x instanceof HasSavedItem && $x->entityType() !== 'feed')\n->map(fn ($x) => [$x->entityType(), $x->entityId()]);\n\nif ($items->isEmpty()) {\nreturn null;\n}\n\n$key = fn ($type, $id) => sprintf('saved::exists(user:%s,%s:%s)', $userId, $type, $id);\n\n$data = $items->reduce(function ($carry, $x) use ($key) {\n$carry[$key($x[0], $x[1])] = false;\n\nreturn $carry;\n}, []);\n\n/** @var Builder $query */\n$query = $items->map(function ($x) use ($userId) {\nreturn DB::table('saved_items')\n->select(['item_id', 'item_type'])\n->where([\n'user_id' => $userId,\n'item_type' => $x[0],\n'item_id' => $x[1],\n])->limit(1);\n})->reduce(function ($carry, $x) {\nreturn $carry ? $carry->union($x) : $x;\n});\n\nreturn $query->get()\n->reduce(function ($carry, $x) use ($key) {\n$carry[$key($x->item_type, $x->item_id)] = true;\n\nreturn $carry;\n}, $data);\n}\n}\nCheck is saved\nclass IsSavedItem implements PolicyRuleInterface\n{\npublic function check(string $entityType, User $user, $resource, $newValue = null): ?bool\n{\nif (!$resource instanceof HasSavedItem) {\nreturn false;\n}\n\nreturn LoadReduce::remember(\nsprintf('saved::exists(user:%s,%s:%s)', $user->userId(), $resource->entityType(), $resource->entityId()),\nfn () => resolve(SavedRepositoryInterface::class)->checkIsSaved(\n$user->userId(),\n$resource->entityId(),\n$resource->entityType()\n)\n);\n}\n}","best-practise#Best practise":"// DON'T use eager loading for finding one. It's can duplicate query.\n$blog = $this\n->with(['user', 'userEntity', 'categories', 'activeCategories', 'attachments'])\n->find($id);","core_item_statstitic#core_item_statstitic":"Sample migration total pending comments\nINSERT INTO core_item_statistics (item_id, item_type)\nSELECT A.item_id, A.item_type\nFROM comments as A\nLEFT JOIN core_item_statistics as B\nON A.item_id = B.item_id AND A.item_type = B.item_type\nWHERE B.item_type is null\nGROUP BY A.item_id, A.item_type;\npostgres\nWITH B as (SELECT item_id, item_type, count(*) as aggregate\nFROM comments\nWHERE is_approved = 0\nAND parent_id =0\nGROUP BY item_id, item_type)\nUPDATE core_item_statistics as A\nSET total_pending_comment = B.aggregate\nFROM B\nWHERE B.item_id = A.item_id\nAND B.item_type = A.item_type;\nMySql\nUPDATE core_item_statistics A\nINNER JOIN (\nselect item_id, item_type, count(*) as aggregate\nfrom comments\nwhere is_approved=0 and parent_id = 0\ngroup by item_id, item_type\n) as B ON(B.item_id = A.item_id and B.item_type=A.item_type)\nSET A.total_pending_comment = B.aggregate\nDepends on database driver to run migration scripts\nuse \\Illuminate\\Support\\Facades\\DB;\n\n$driver = DB::getDriverName();\n\nif ($driver == 'pgsql') {\nDB::statement($sql)\n} elseif ($driver == 'mysql') {\n$sql;\n}"}},"/backend/structure":{"title":"Structure","data":{"":"","structure#Structure":"The MetaFox directory structure extends the Laravel framework directory structure. Please read laravel directory structure for more details.\napp/ : Contains the core code of laravel framework\nbootstrap/ : Laravel default, don't change\napp.php :\ncache/ : Contains framework generated files for performance optimization such as the route and services cache files.\nconfig/ : Contains root of your application's configuration files.\ndatabase/ : Contains root database migrations, model factories, and seeds.\npackages/ : All MetaFox packages\nmetafox/ : Contains MetaFox core packages\nplatform/ : Contains MetaFox framework code.\npublic/ : Laravel defaults\n.htaccess\nrobot.txt\nindex.php : Entry point for all requests\nresources/ : Laravel default\nroutes/ : Laravel default\nstorage/ : Laravel default\nlogs/ : Contains your file based logs files\napp/\npublic/ : Contains users uploaded files\ntests/ : Contains root automated tests\nvendor/ : Contains your Composer dependencies.\nNow, we'll look into directories deeper","app-directory#app Directory":"The app directory contains the core code of Laravel framework. You should not change this source code to keep your application is upgradable.","bootstrap-directory#bootstrap Directory":"The bootstrap directory contains the app.php file which bootstraps the framework. This directory also has a cache directory which contains generated files for performance optimization such as the route and services cache files. You don't need to modify any files within this directory.","config-directory#config Directory":"The config directory contains root of your application's configuration files. It's a great idea to read through all of configuration files and familiarize yourself with all of the available options.","database-directory#database Directory":"The database directory contains root database migrations, model factories, and seeds. Also, you can use this directory to hold an SQLite database.","public-directory#public Directory":"The public directory contains the index.php file, which is the entrypoint for all requests entering your application and configures autoloading. This directory also contains your assets such as images, JavaScript, and CSS.","resources-directory#resources Directory":"The resources directory contains your views as well as your raw, un-compiled assets such as CSS or JavaScript.","routes-directory#routes Directory":"The routes directory contains root of the route definitions for your application. By default, several route files are included with Laravel, such as: web.php, api.php, console.php, and channels.php.The web.php file contains routes that the RouteServiceProvider places in the web middleware group, which provides session state, CSRF protection, and cookie encryption. If your application does not support stateless RESTful API then it is likely that all of your routes will most likely be defined in the web.php file.The api.php file contains routes that the RouteServiceProvider places in the API middleware group. These routes are intended to be stateless, so requests entering the application through these routes need to be authenticated via tokens and will not have access to session state.The console.php file is where you may define all of your closure based console commands. Each closure is bounded to a command instance allowing a simple approach to interact with each command's IO methods. Even though this file does not define HTTP routes, it defines console based entrypoints (routes) into your application.The channels.php file is where you may register all of the event broadcasting channels that your application supports.","storage-directory#storage Directory":"The /storage directory contains your logs, file based sessions, file caches, and other files generated by the framework. This directory is segregated into app, framework, and logs directories. The app directory may be used to store any files generated by your application. The framework directory is used to store framework generated files and caches. Finally, the logs directory contains your application's log files.The storage/app/public directory may be used to store user-generated files, such as profile avatars, that should be publicly accessible. You should create a symbolic link at public/storage which points to this directory. You may create the link using the php artisan storage:link Artisan command.","tests-directory#tests Directory":"The /tests directory contains root automated tests. Example PHPUnit unit tests and feature tests are provided out of the box. Each test class should be suffixed with Test. You may run your tests using the phpunit or php vendor/bin/phpunit commands. Or, if you would like a more detailed and beautiful representation of your test results, you may run your tests using the php artisan test Artisan command.","vendor-directory#vendor Directory":"The vendor directory contains your Composer dependencies."}},"/frontend/cookie":{"title":"Cookie","data":{"":"","cookie#Cookie":"cookieBackend is the wrapper of js-cookie","hooks#Hooks":"const ComponentWithCookieBackend = () => {\nconst { cookieBackend } = useGlobal();\n\n// ... your code\n};","saga#Saga":"import { getContext } from 'redux-saga/effects';\n\nexport function* functionWithCookieBackend() {\nconst { cookieBackend } = yield getContext('useGlobal');\n\n// ... your code\n}","apis#Apis":"# Get a cookie value\ncookieBackend.get(\"name\");\n\n// Set a string to name\ncookieBackend.set(\"name\", \"string value\");\n\n// return number or undefined\ncookieBackend.getInt(\"name\");","best-practices#Best practices":"Cookie will send data it contains back to server in every request, so do not store more data than you need in cookie."}},"/export-package":{"title":"Export Package","data":{"":"After development complete, you need export package to upload to appstore.phpfox.com.\nMetaFox's clients can buy/install it directly from store.appstore.phpfox.com require a zip file with file contents structure.","environment#Environment":"WARN: if your package contains frontend source code, backend server have to access to frontend source code before run export.\nAPP_ENV=\"local\"\nMFOX_FRONTEND_ROOT=\"/path/to/frontend root\"","filesystem#Filesystem":"|- backend\n| |- packages\n| |- [vendor]\n| |- [app name]\n| |- composer.json\n| |- config\n| |- src\n|- frontend\n|- packages\n|- [vendor]\n|- [app name]\n|- package.json\n|- tsconfig.json\n|- src\nExample","manual-export#Manual Export":"Export package if you can access frontend source from backend server directly.\nCreate a new empty directory export\nCopy frontend package to `export/frontend/packages/[vendor]/[app name]\nCopy frontend package to `export/backend/packages/[vendor]/[app name]\ncd export && zip export.zip -r .","admincp-export#AdminCP Export":"Clean cache then access /admincp/app/package/browse/installed","command-export#Command Export":"Require if export from command lineUpdate backend .env\nMFOX_STORE_API_TOKEN=\"Copy from https://appstore.phpfox.com/account-settings/\"\nExport package command\nphp artisan package:publish foxdev/theme-chocolate\nRelease for development channel to testing\nphp artisan package:publish foxdev/theme-chocolate --release\nRelease for production to testing\nphp artisan package:publish foxdev/theme-chocolate --release --production"}},"/backend/octane":{"title":"Performance","data":{"":"MetaFox is based on laravel framework, take a look over request lifecycle. By default laravel bootstrap process\nrequires two many files & services, in default packages there are ~ 1300 files and 130 services need to register and bootstrap.","request-lifecycle#Request Lifecycle":"","fpm-server#FPM Server":"","octane-server#Octane Server":"Compare FPM Vs Octanewrk -c100 -t4 \"htttp://api.metafox.test/me\"","best-practise#Best Practise":"","dependency-injection#Dependency Injection":"https://laravel.com/docs/10.x/octane#configuration-repository-injection","register_shutdown_function#register_shutdown_function":"register_shutdown_function => Lifecycle::onRequestTerminated","memory-leak#Memory leak":"/**\n* Handle an incoming request.\n*/\npublic function index(Request $request): array\n{\nService::$data[] = Str::random(10);\n\nreturn [\n// ...\n];\n}\nphp"}},"/frontend/dialog":{"title":"Dialog","data":{"":"MetaFox Dialog is based on Material-UI Dialog,\nWe wrap it in a Controller and dialogBackend services so you can work with Dialog Component more conveniently by using dialogBackend service and useDialog hooks.","create-dialog#Create Dialog":"Here is the sample declaration of a simple dialog. Note that annotations MUST be at the beginning of the source file.\n/**\n* @type: dialog\n* name: ExampleDialog\n*\n*/\n\nimport {\nButton,\nDialog,\nDialogActions,\nDialogContent,\nDialogTitle\n} from '@metafox/dialog';\nimport React from 'react';\n\nexport default function ExampleDialog({\ntitle,\nmessage\n}}) {\nconst { useDialog } = useGlobal();\nconst { setDialogValue, dialogProps } = useDialog();\n\nconst onSubmit = () => setDialogValue(true);\n\nconst onCancel = () => setDialogValue();\n\nreturn (\n\n{title}\n{message}\n\n\n\n\n\n);\n}","export-dialog#Export Dialog":"We are going to use MetaFox annotations syntax for the created dialog.For example: this source defines a dialog named sampleModalDialog\n/**\n* @type: dialog\n* name: sampleModalDialog\n*/","present#Present":"const value = await dialogBackend.present({\ncomponent: \"sampleModalDialog\",\nprops: {\ntitle: \"Simple Dialog Title\",\nmessage:\n\"This is simple dialog demo, you can extends with others function laters.\",\n},\n});\nIn order to present dialog dialogBackend, assign component with value of 'sampleModalDialog'.\ndialogBackend\n.present({\ncomponent: \"sampleModalDialog\",\nprops: {\ntitle: \"Simple Dialog Title\",\nmessage:\n\"This is simple dialog demo, you can extends with others function laters.\",\n},\n})\n.then((value) => {\n// your code\n});","alert#Alert":"Create a Alert Dialog with a single line message\ndialogBackend.alert({\ntitle: \"Alert\",\nmesssage: \"You can not delete this content!\",\n});\nCreate a Alert Dialog with multiple line messages\ndialogBackend.alert({\nmessage:\n\"- Checking the network cables, modem, and router\\n- Reconnecting to Wi-Fi\",\n});","message-component#Message Component":"Content of message should be wrapped in DialogContentText to keep a consistent look and feel.\nimport { DialogContentText } from \"@mui/material\";\n\ndialogBackend.alert({\nmessage: (\n\nChecking the network cables, modem, and router\n\n),\n});","confirm#Confirm":"Create a Confirm Dialog\nconst ok: boolean = await dialogBackend.confirm({\ntitle: \"Confirm\",\nmessage: \"Are you sure?\",\n});\nThe message content is similar to Alert.Add a Custom Button to Confirm Dialog\nconst ok: boolean = await dialogBackend.confirm({\ntitle: \"Confirm\",\nmesssage: \"Are you sure?\",\n});","dismiss#Dismiss":"Dismiss presenting dialog\ndialogBackend.dimiss();\nDimiss all dialog\ndialogBackend.dismiss(true);","dialog-form#Dialog Form":"Use setDialogValue to set value for the current promise call."}},"/frontend/concepts":{"title":"Concepts","data":{"":"","annotation#Annotation":"Since MetaFox comes with Modular structure, Build tool helps solve issues of dependencies without using import directly. Build tool will scan all packages declared in settings.json file, find all components, librarys to build the bundle.For example:\n/**\n* @type: route\n* name: user.profile\n* path: /user/:user_id(\\d+)/:tab?\n*/","best-practices#Best practices":"Keep anotation at the begin of source file\nEach file should declare only one component.","annotation-1#Annotation":"Type\tGroup\tui\tgeneric UI components\tblock\tLayout block component\titemView\tItem view component\tembedView\tembedView view component\tdialog\tdialog component\tformElement\tForm Elements\tpopover\tpopover component\troute\tpage route\tmodalRoute\tmodal route\tsaga\tsaga function(s)\treducer\treducer functions\tservice\tservice provider","using-testid#Using testid":"For automation testing, please follow below rules:\nFor item view, embed view component, detail view component, all clickable/focusable items MUST have data-tid (link , ....)\nFor menu component, all menu items MUST have data-tid","ui#ui":"Type ui declares generic React component.\n/**\n* @type: ui\n* name: CategoryTitle\n*/\n\nexport default function CategoryTitle(): React.FC<{}> {\n// your code\n}","itemview#itemView":"Naming convention: resource_name.itemView.suffixWhen rendering, declaration of React component is included in a listing/grid component.In some specific cases, platform will base on the naming convention to find components having the best match for rendering.resource_name : blog, photo, video, .... etc.suffix: mainCard, smallCard, flatCard, etc ...\n/**\n* @type: itemView\n* name: photo_album.itemView.mainCard\n*/\nexport default function PhotoAlbumMainCard() {\n// your code\n}","embedview#embedView":"Naming Convention: resource_name.embedView.parent_resourceDeclaration of React component is used as the embed content of other component.For example:\n/**\n* @type: itemView\n* name: photo_album.embedView.feedItem\n*/\nexport default function EmbedPhotoAlbum() {\n// your code\n}","dialog#Dialog":"Naming Convention: dialog.purposeSee dialog\n/**\n* @type: dialog\n*\n* name: dialog.\n*/\n\nexport default function MyDialog() {\n// dialog code\n}","formelement#formElement":"Here are list of supported form elements\nbutton\ncancel\ncaptcha\ncheckbox\nhidden\nlinkButton\npassword\neditor\nsubmit\ntext\nattachment\ndate\ntime\ndatetime\nfriendPicker\nitemPhoto\nitemPhotoGallery\nlocation\nsearchBox\nselect\nTags\nTypeCategory\nvideoUrl\naddAlbumn","popover#popover":"","route#route":"","modalroute#modalRoute":"","saga#saga":"","reducer#reducer":"","service#service":""}},"/frontend/form":{"title":"Form Builder","data":{"":"Metafox includes the most popular React Form library formik and validation with yup, wraps theme in FormBuilder in order to help build, configure flexible logic from AdminCP without modifying source code.The Form mechanism on MetaFox is based on formik + material-ui. It can build a Form with certain fields & structure with JSON schema. Let's take a look at the below example.","schema#Schema":"import { Form } from \"@metafox/form\";\n\nconst ExampleLoginForm = () => {\nconst loginFormSchema = {\ncontainer: true,\ncomponent: \"form\",\nelements: {\nemail: {\nlabel: \"Email\",\ncomponent: \"text\",\ntype: \"email\",\nvariant: \"outlined\",\n},\npassword: {\nlabel: \"Password\",\ncomponent: \"Email\",\ntype: \"password\",\nvariant: \"outlined\",\n},\nbuttons: {\ncontainer: true,\ncomponent: \"html\",\nelements: {\nsubmit: {\nlabel: \"Login\",\ntype: \"submit\",\n},\n},\n},\n},\n};\nconst initialValues = {\nemail: \"jack@MetaFox.com\",\npassword: \"\",\n};\n\nreturn
;\n};","elements#Elements":"Here is the list of Supported Form elements\ntext\nswitch\ncheckbox\ncheckboxGroup\nradio\nradioGroup\nselect\ndate\ndatetime\ntime\nslider\nmarkSlider\ncategory\ntags\nprivacy\nbutton\nbuttonGroup\nhidden\neditor\nlink\ntypo\ncontainer\ndialogHeader\ndialogFooter\ndialogContent","text#Text":"Definition\nexport type TextFieldProps = {\nname: string;\ncomponent: \"text\";\nlabel: string;\nplaceholder: string;\nreadOnly?: boolean;\ndisabled?: boolean;\nrequired?: boolean;\nmaxLength?: number;\nvariant?: \"outlined\" | \"filled\";\ntype?: \"text\" | \"email\" | \"password\" | \"number\" | \"date\" | \"time\";\n};\nBlow is the example to create a Text element\nconst Field = {\nname: \"title\",\ncomponent: \"text\",\nlabel: \"Title\",\nplaceholder: \"Enter an title for this blog\",\nreadOnly: false, // optional\ndisabled: false, // optional\nrequired: false, // optional\nmaxLength: 250, // optional\nautoComplete: true, //optional,\nvariant: \"outlined\",\n};","switch#Switch":"TBD","checkbox#Checkbox":"TBD","checkboxgroup#CheckboxGroup":"TBD","radio#Radio":"TBD","radiogroup#RadioGroup":"TBD","date#Date":"TBD","datetime#Datetime":"TBD","time#Time":"TBD","slider#Slider":"TBD","markslider#MarkSlider":"TBD","tags#Tags":"TBD","privacyselect#PrivacySelect":"TBD","button#Button":"TBD","buttongroup#ButtonGroup":"TBD","hidden#Hidden":"TBD","editor#Editor":"TBD","link#Link":"TBD","slider-1#Slider":"TBD","container#Container":"TBD","dialogheader#DialogHeader":"TBD","dialogfooter#DialogFooter":"TBD","dialogcontent#DialogContent":"TBD","custom-elements#Custom Elements":"TBD"}},"/frontend/gridview":{"title":"Grid View","data":{"":"Naming for grid layouts per resource.\nFriend - Cards: Main Card\nFriend - Lists: Main Flat List\nFriend - Small Lists: Using in side"}},"/frontend/layout":{"title":"Layouts","data":{"":"","layout-organization#Layout Organization":"In order to support customization without modify source base, MetaFox layout system organized in to template, block\nTemplate separates a page into multiple named slot: header, aside, content, subside\nBlock is a React component you can assign to a participant location in template\n\nExample template structure\n-------------------------------------------------------\nheader |\n-------------------------------------------------------\nTop |\n-------------------------------------------------------\n| | |\naside | content | subside |\n| | |\n| | |\n-------------------------------------------------------\nbottom |\n-------------------------------------------------------\nAfter that you can assign block and template using layouts.json file, example:\n{\n\"home.member\": {\n\"info\": {\n\"title\": \"Home Page for logged in users\",\n\"description\": \"Home Page for Logged in users\"\n},\n\"large\": {\n\"templateName\": \"three-column-fixed\",\n\"blocks\": [\n{\n\"component\": \"feed.block.statusComposer\",\n\"slotName\": \"main\",\n\"title\": \"Status Composer\",\n\"key\": \"15t1m\",\n\"blockId\": \"15t1m\",\n\"variant\": \"default\",\n\"blockStyle\": \"Contained\",\n\"blockLayout\": \"Blocker\"\n},\n{\n\"component\": \"feed.block.homeFeeds\",\n\"emptyPage\": \"core.block.no_content_with_description\",\n\"slotName\": \"main\",\n\"title\": \"Activity Feed\",\n\"key\": \"mngh\",\n\"blockId\": \"mngh\",\n\"itemView\": \"feed.itemView.card\",\n\"contentType\": \"feed\",\n\"dataSource\": {\n\"apiUrl\": \"/feed\",\n\"apiParams\": \"view=latest\",\n\"pagingId\": \"/feed\"\n},\n\"canLoadMore\": true,\n\"canLoadSmooth\": true,\n\"blockStyle\": \"Main Listings\",\n\"gridStyle\": \"Feeds\",\n\"blockLayout\": \"Main Listings\",\n\"gridLayout\": \"Feeds\"\n},\n{\n\"component\": \"core.block.sidebarPrimaryMenu\",\n\"slotName\": \"side\",\n\"key\": \"8r659\",\n\"blockId\": \"8r659\",\n\"title\": \"\",\n\"variant\": 8,\n\"displayLimit\": 8,\n\"headerActions\": {\n\"ml\": 0,\n\"mr\": 0\n},\n\"blockLayout\": \"sidebar primary menu\"\n},\n{\n\"component\": \"core.dividerBlock\",\n\"slotName\": \"side\",\n\"title\": \"\",\n\"blockProps\": {\n\"blockStyle\": {}\n},\n\"dividerVariant\": \"middle\",\n\"key\": \"qlfp\",\n\"blockId\": \"qlfp\"\n},\n{\n\"component\": \"core.block.sidebarShortcutMenu\",\n\"slotName\": \"side\",\n\"key\": \"2c7hu\",\n\"blockId\": \"2c7hu\",\n\"title\": \"Shortcuts\",\n\"itemView\": \"shortcut.itemView.smallCard\",\n\"gridLayout\": \"Shortcut - Menu Items\",\n\"blockLayout\": \"sidebar shortcut\",\n\"canLoadMore\": 0,\n\"canLoadSmooth\": true,\n\"authRequired\": true,\n\"dataSource\": {\n\"apiUrl\": \"/user/shortcut\"\n},\n\"pagingId\": \"/user/shortcut\",\n\"headerActions\": [\n{\n\"as\": \"user.ManageShortcutButton\"\n}\n],\n\"emptyPageProps\": {\n\"noBlock\": 1\n},\n\"emptyPage\": \"hide\"\n},\n{\n\"component\": \"announcement.block.announcementListing\",\n\"slotName\": \"subside\",\n\"title\": \"Announcements\",\n\"key\": \"qea3s\",\n\"blockId\": \"qea3s\",\n\"itemProps\": {\n\"mediaPlacement\": \"none\"\n},\n\"blockStyle\": \"Profile - Side Contained\",\n\"blockLayout\": \"Side Contained\"\n},\n{\n\"component\": \"chatplus.block.contactsNewFeed\",\n\"slotName\": \"subside\",\n\"title\": \"Contacts\",\n\"key\": \"2312ewqe\",\n\"blockId\": \"2312ewqe\",\n\"showWhen\": [\"truthy\", \"setting.chatplus.server\"]\n}\n]\n},\n\"small\": {}\n}\n}\nThen you use layout named \"home.member\" in to your page source to define layout\n/**\n* @type: route\n* name: core.home\n* path: /, /home\n*/\nimport { APP_FEED } from \"@metafox/feed\";\nimport { useGlobal, useLoggedIn } from \"@metafox/framework\";\nimport { Page } from \"@metafox/layout\";\nimport * as React from \"react\";\n\nexport default function HomePage(props) {\nconst loggedIn = useLoggedIn();\nconst { createPageParams } = useGlobal();\n\nconst pageParams = createPageParams(props, (prev) => ({\nmodule_name: APP_FEED,\nitem_type: APP_FEED,\n}));\n\nif (loggedIn) {\nreturn ;\n}\n\nreturn ;\n}","pageparams#pageParams":"Page params is a React context sharing wrapped all component in layout, its simple way to passing value without direct pass props.\nThen you get props of any block, component via useParams hooks\nexport default function AdminAppStoreShowDetail() {\nconst { useParams } = useGlobal();\n\nconst { module_name, item_type } = useParams();\n}","custom-block#Custom block":"MetaFox will scan the comment code for layout declaration when building. Below is the sample layout declaration\n/**\n* @type: itemView\n* name: blog.itemView.mainCard\n* keywords: blog\n* description:\n* previewImage:\n* deps:\n* priority: sort orthers.\n*/\n@type\nitemView: Declare grid/list item view component.\nembedView: Declare embedView in feed/notification/search embed view component.\nblock: Declare configurable block view component.\nsaga: Declare redux saga effects.\nreducer: Declare redux reducer function.","filename#Filename":"File types:\nmessages.json: Declare translation objects.\nlayouts.json: Declare layout configuration objects.","naming#Naming":"Export Component Naming\nItem view: [resource_type].itemView.*, etc: blog.itemView.mainCard, blog.itemView.smallFlat\nEmbed item view: [resource_type].embedView.*, etc: blog.embedItem.basic\nForm Field: form.field.[name]: etc: form.field.text, form.field.upload\nBlock Component: [module_name].block.[name], etc: core.block.listview","how-to-detect-layout-variants#How to detect layout variants":"Imagine that user is viewing the user.profile page in medium viewport size, but administrator hasn't defined the layout configuration for such viewport size yet.\nIn this case, the Layout service will check size variants and apply layout configuration in the order of up-to-down sizes: medium, small, xsmall, xxsmall, large, xlarge.Check detail of selection:\nxxsmall: xxsmall, xsmall, small, medium, large, xlarge\nxsmall: xsmall, xxsmall, small, medium, large, xlarge\nsmall: small, xsmall, xxsmall, medium, large, xlarge\nmedium: medium, small, xsmall, xxsmall, large, xlarge\nlarge: large, medium, small, xsmall, xxsmall, xlarge\nxlarge: xlarge, large, medium, small, xsmall, xxsmall","when-to-detect-layout-size-variants#When to detect layout size variants":"For performance reasons, layout size variants will be checked and applied when page is started rendering. It means that layout won't be updated when user resizes viewport.","layout-elements#Layout Elements":"","block#Block":"A Block is a React component that Administrator can add, remove, configure, and drag-and-drop to layout slot","slot#Slot:":"A slot is an area where Administrators can put blocks into. Slot can contain other slots.","actions#Actions:":"Let's take a look at available actions on Slot","resize#Resize":"Resize a slot for responsive viewport size as small, medium, and more.","split#Split":"Split a slot into vertical direction. It's not usually but sometimes required to build complex layouts.","add-slot#Add Slot":"Add other siblings slot at the start/end of the current slot.","section#Section":"Section similar to material-ui Container component, it defines rows in layouts.","actions-1#Actions:":"Let's take a look at available actions on Section","settings#Settings":"maxWidthValues: lg| md| sm| xl| xs| falseDetermine the max-width of the container. The container width grows relatively with the size of the screen. Set to false to disable maxWidth.disableGutters: If true, the left and right paddings are removed.","remove#Remove":"TBD","add-new-slot#Add new slot":"TBD","item-view#Item View":"Item View is a component that implements UI for a single item in a listing block, etc. For example: featured members, activity feeds:\n// file blog/src/components/BlogItemView.tsx\nexport type BlogItemShape = {\ntitle: string;\ndescription: string;\n} & ItemShape;\n\nconst BlogItemView = ({ item, itemProps }: ItemViewProps) => {\nconst to = `/blog/view/${item.id}`;\nconst classes = useStyles();\nconst cover = /string/i.test(typeof item.image)\n? item.image\n: item.image[\"500\"];\n\nconst [control, state] = useActionControl<{}>(item, {\nmenuOpened: false,\n});\n\nreturn (\n
\n
\n\n\n\n
\n
\n\n{item.title}\n\n\n
\n

{item.description}

\n\n
\n
\n
\n);\n};\n\nexport default BlogItemView;\n\n// file blog/src/views.tsx\nimport BlogItemView from \"./components/BlogItemView\";\nexport default {\n\"blog.itemView.card\": BlogItemView,\n};","loading-skeleton#Loading Skeleton":"// file blog/src/components/BlogItemView.tsx\n\nconst LoadingSkeleton = ({ itemProps }) => {\nconst classes = useStyles();\nreturn (\n
\n
\n
\n\n
\n
\n\n\n\n
\n
\n
\n);\n};\n\nBlogItemView.LoadingSkeleton = LoadingSkeleton;\n\nexport default BlogItemView;","listing-block#Listing Block":"You can create new block components by extending other blocks\nimport createBlock from \"@metafox/framework/createBlock\";\nimport { ListViewBlockProps } from \"@metafox/framework/types\";\n\nconst BlogListingBlock = createBlock({\nextendBlock: \"core.block.listview\", // extend from block\nname: \"BlogListingBlock\", // based Block compoinent\noverrides: {\n// override properties automacally merged to targed component\ncontentType: \"blog\", // layout editor will load view prefix by `contentType.itemView.*` to select itemView.\ndataSource: { apiUrl: \"/blog\" },\n},\ndefaults: {\n// default properties show in layout editor,\n// only property show in defaults AND NOT in overrides will be show in editor.\ntitle: \"Blogs\",\nblockProps: { variant: \"contained\" },\nitemView: \"blog.itemView.mainCard\",\ngridContainerProps: { spacing: 2 },\ngridItemProps: { xs: 12, sm: 12, md: 12, lg: 12, xl: 12 },\n},\n});\n\nexport default BlogListingBlock;","side-menu-block#Side Menu Block":"Create a Side Menu block\nimport createBlock from \"@metafox/framework/createBlock\";\nimport { SideMenuBlockProps as Props } from \"@metafox/framework/types\";\n\nconst BlogSideMenuBlock = createBlock({\nextendBlock: \"core.block.sideNavigation\",\nname: \"BlogSideMenuBlock\",\ndisplayName: \"Blog Menu\",\nkeywords: \"blogs, navigation, menu\",\ndescription: \"\",\npreviewImage: \"\",\noverrides: {\nmenuItems: [\n{\nto: \"/blog\",\nlabel: \"All Blogs\",\nactive: true,\n},\n{\nto: \"/blog?view=my\",\nlabel: \"My Blogs\",\n},\n{\nto: \"/blog?view=friend\",\nlabel: \"Friend's Blogs\",\n},\n],\n},\ndefaults: {\ntitle: \"Blogs\",\nblockProps: { variant: \"plained\", noHeader: true, noFooter: false },\n},\n});\n\nexport default BlogSideMenuBlock;","side-category#Side Category":"import createBlock from \"@metafox/framework/createBlock\";\nimport { CategoryBlockProps } from \"@metafox/framework/types\";\n\nconst SideCategoryBlock = createBlock({\nextendBlock: \"core.categoryBlock\", // based Block compoinent\nname: \"BlogCategoryBlock\", // React component name\ndisplayName: \"Blog Categories\", // display in layout editor\nkeywords: \"blogs, category\", // keyword to search on layout editor\ndescription: \"\", // description in layout editor\npreviewImage: \"\", // preview image in layout editor 200x200\noverrides: {\n// overrides properties will apply to derived blog automacally.\ndataSource: { apiUrl: \"/blog-category\", apiParams: \"\" },\nhref: \"/blog/category\",\n},\ndefaults: {\n// properties will show in edit block modal.\ntitle: \"Categories\",\nblockProps: { variant: \"plained\" },\n},\n});\n\nexport default SideCategoryBlock;"}},"/frontend/sagas":{"title":"Sagas","data":{"":"Example saga files\n/**\n* @type: saga\n* name: updatedVideo\n*/\n\nimport { LocalAction, viewItem } from \"@metafox/framework\";\nimport { takeEvery } from \"redux-saga/effects\";\n\nfunction* updatedVideo({ payload: { id } }: LocalAction<{ id: string }>) {\nyield* viewItem(\"video\", \"video\", id);\n}\n\nconst sagas = [takeEvery(\"@updatedItem/video\", updatedVideo)];\n\nexport default sagas;\nmetafox build/start command collect all sagas file using annotation @type: saga saga root patternWhenever a new saga file be added or removed, run yarn metafox reload to bundle saga file again.All sagas bundled at file ./app/src/bundle/produce.tsx, example files\n\nimport coreSagaHandleActionFeedbackSaga from\n'@metafox/framework/sagas/handleActionFeedback';\nimport CoreRequestSaga from\n'@metafox/framework/sagas/handleRequest';\nimport sagaReloadEntitySaga from\n'@metafox/core/reducers/reloadEntity';\nimport abortControllerSaga from\n'@metafox/core/sagas/abortController';\nimport coreChooseThemeSaga from\n'@metafox/core/sagas/chooseTheme';\nimport sagaCoreCloseDialogSaga from\n'@metafox/core/sagas/closeDialog';\n\nconst sagas = [\ncoreSagaHandleActionFeedbackSaga,\nCoreRequestSaga,\nsagaReloadEntitySaga,\nabortControllerSaga,\ncoreChooseThemeSaga,\nsagaCoreCloseDialogSaga\n]\n\nexport default function injector(config: any) {\nconfig.sagas=sagas;\n}"}},"/frontend/local-store":{"title":"Local Storage","data":{"":"","get#get":"Get the item with the specific name in localStore\n// get string value\nlocalStore.get('dump name');\n\n// get int value\nlocalStore.getInt('dump name');\n\n// get JSON object\nlocalStore.getJSON('dump name');","set#set":"Set an item in localStore\nlocalStore.set('dump name', 'dump value');","remove#remove":"Remove the item with specific name in localStore.\nlocalStore.remove('dump name');","clear#clear":"Remove all items in localStore\nlocalStore.clear();"}},"/frontend/routing":{"title":"Router","data":{"":"","table-of-contents#Table of Contents":"In this document, we will go through the following topics:\nDynamic/Async Routing\nRouter and Code Splitting\nRouter and Transition\nRouter and Loading progress\nRouter and Modal\n\nFirstly, let's take a look at MetaFox's URL structure\n/: Display home page or user.home page based on the user authentication state.\n/blog: Display user.browse page.\n/blog/my: Display user.browse.my page.\n/user: Display user.browse page.\n/user/profile/1: Display profile of user\n/pages/profile/14: Display profile of pages\n/groups/profile/12: Display profile of groups\n/jack: Display user.profile page, alias of /user/profile/1\n/jack/photo: Display user.profile.photo page, alias of /user/profile/1/photo\n/barca: Display pages.profile as alias of /pages/profile/14\n/barca/groups: Display pages.profile.groups as alias of /pages/profile/14/photo\n/nancy-club: Display private groups.profile as alias of /group/profile/12\n\nBy using react-router-dom and React.lazy, we can structure React components:\n\n\n\n\n\n\n\n\n\n\n... Others Routes\n\n\n\nAs you see /, blog,blog/my are static regex URLs, they can be solved easily by using regex or literal strings.While /jack, jack/photo, /barca, /barca/groups is more difficult, we have to query database to check if terms of jack, barca are for usernames or page slugs.\nRouter gets a request with the pathname /jack/photo\nCheck router path but no matched\nKeep previous matched Route, showing loading progress bar.\nSend async request to get canonical pathname of url /jack/photo\nIf getting the canonical pathname /user/profile/1/photo, replace current location state, keep location pathname.\nRouter tries to re-run with /user/profile/1/photo pathname.","best-practices#Best Practices:":"Application can be configured to use either Loading progress bar or Empty waiting screen\nShould use nested route in user/pages/event profile page. TBD.\nDo not base on the browser location. Instead, when developing UI, use $PageContext to ask the context to query content instead of browser location.","code-splitting#Code Splitting":"Because of app sizing, we have to split code of every Page components. read moreAt the first time visiting /user page, the associated chunk file is not ready. Thus, if the network speed is around 1 second, end users may see a flash animation.","modal-route#Modal Route":"Here is the sample code for a Modal\n{\nsampleModalDialog: {\nmodal: true,\npath: '/m/example/simple-modal',\ncomponent: loadable(() => import('./pages/ExampleModal')),\n},\nsampleModalPage: {\npath: '/m/example/simple-modal',\ncomponent: loadable(() => import('./pages/ExampleModalPage')),\n},\n}\nYou have to create two routes, the first route is for Modal Route, and the seconds is Page Route. You can declare them in routes.ts file, with the same path but add property modal.","how-to-disable-modal-dialog-on-mobile-device#How to disable modal dialog on mobile device?":"TBD"}},"/frontend/service":{"title":"Service Manager","data":{"":"Service can be a class instance, a function or a React component to handle some pieces of your app logic.MetaFox platform has a Service Manager called globalContext to manage all services. You can not access the global manager directly but via hooks or saga context.After passed to the global service manager, a service can be accessible in React components or Saga functions.In React component:\nimport {useGlobal} from '@metafox/framework'\n\nconst MyComponent (){\nconst manager = useGlobal();\nconst {apiClient} = manager\n\n// or simplier\n// const {apiClient} = useGlobal();\n}\nIn saga functions:\nimport { getContext } from 'redux-saga/effects';\n\nexport function mySagas() {\nconst { apiClient } = yield * getGlobalContext();\n}\n\nglobalContext is mutable, changeble. Its service members will be affected by others without being changed or updated.","class-services#Class Services":"A standard Service class must have bootstrap method, DO NOT define bootstrap as an arrow function.Let's create the LogService by adding the src/manager/LogService.ts with the following content\n/**\n* @type: service\n* name: logService\n**/\n\nimport {Manager} from '@metafox/manager'\n\ntype LogServiceProps {\n// any property here\n}\n\nclass LogService {\noptions: LogServiceProps;\nconstructor(config: LogServiceProps){\nthis.config = LogServiceProps\n}\n\nbootstrap(manager: Manager){\n// to do something here.\n// return this or void\n}\ninfo(message: string){\nconsole.log(message)\n}\n}\nRestart dev server, then you can use the LogService with useGlobal hook as the below example:\nimport useGlobal from '@metafox/framework';\n\nconst LoginForm (){\nconst { logService } = useGlobal();\nlogService.info('This message is passed by logService')\n}","configuration#Configuration":"In the above example, you have created a service named logService. You can pass configuration from root app by adding a section with the same name in manifest.json as below\n{\n\"logService\": {\n\"level\": \"warn\"\n}\n}\nService Manager will pass this configuration to LogService constructor.If you want to access global configuration in LogService, just use manager.options accesstor.","functional-service#Functional Service":"Sometimes, you would like to declare general functions which can be used anywhere in React components and Sagas. And, you don't want to declare import dependencies everywhere. The good solution is to inject them into Service Manager.In the next example, we will define a function to generate random string\nexport default function randomId(): string {\nreturn Math.random()\n.toString(36)\n.replace(/[^a-z]+/g, '')\n.substr(0, 5);\n}\nNow, pass the function to Service Manager\n# file src/manager.tsx\n\nconst manager = {\nlogService: LogService,\nrandomId,\n// other manager here\n}\n\nexport default manager\n\nimport {useGlobal} from '@metafox/framework';\n\nconst LoginForm (){\nconst { logService, randomId } = useGlobal();\nlogService.info('This message is passed by logService')\nlogService.info('test message id:', randomId());\n}","react-component-service#React Component Service":"In order to inject a React Component across apps without declaration for dependencies, you just need to inject components into the global Service Manager by using inject method\n/**\n* @type: service\n* name: CommentList\n*/\n\nexport default function CommentList() {}\nNext, we will declare typingsCreate ./src/module.d.ts with the following content\nimport '@metafox/framework/Manager';\ndeclare module '@metafox/framework/Manager' {\ninterface Manager {\nCommentList?: React.FC<{}>;\n}\n}\nNow, you can get CommentList from other components. For example:\nimport { useGlobal } from '@metafox/framework';\n\nfunction MyComponent() {\nconst { CommentList } = useGlobal();\n\nreturn ;\n}","core-services#Core Services":"apiClient\nusePopover\ncookieBackend\nnormalization\npreferenceBackend\ndialogBackend\nintl\ncreatePageParams\nlocalStore\ncompactUrl\nslugify\ncopyToClipboard\nuseActionControl\nuseSession\nuseGetItem\nuseLoggedIn\nuseIsMobile\nusePageParams"}},"/frontend/translation":{"title":"Translation","data":{"":"MetaFox translation based on react-intlWithin component"}},"/frontend/home":{"title":"Get Started","data":{"":"This documentation is mainly for Frontend and ReactJs developer to set up the development environment.\nIf you are looking for testers or backend development, reading this docs is not neccessary.","prerequisites#Prerequisites":"The MetaFox Frontend is developed with ReactJS. Thus, in this documentation, we assume that you are already familiar with Frontend Development with ReactJS. If you are still not confident with Frontend Development with ReactJS, you may want to take a look at the following docs before continuing:\nReactJs\nTypescript\nReact redux\nRedux saga\nRedux Toolkit\nMaterial UI\nReact Testing Library\nJest","system-requirements-for-development-environment#System Requirements for Development Environment":"","software#Software":"Node 16+\nIf you are using Linux/MacOS, You should install node via Node Version Manager.\n\n\nPHP 7.x or later\nDatabase: MySQL, Postgres\nOS: Linux is recommended","hardware#Hardware:":"RAM at least 4GB","helpful-commands#Helpful Commands":"# Check current default node version\nnode --version\n\n# Check nvm is installed\nnvm --version\n\n## install missing nvm\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash\n\n# Install specify node version\nnvm install 16.14.0\n\n# Set 12.18.0 as default node excutable.\nnvm alias default 16.14.0","environment#Environment":"Location: app/.env\n;--------------------------------------------------------------------\n; location: /app/.env\n; Configuration\n;--------------------------------------------------------------------\n\n; run multiple site on the same server\nMFOX_SITE_ID=1\n\n; A properly formatted must have a leading slash but no ending slash.\nPUBLIC_URL=\nMFOX_API_KEY=2\nMFOX_API_SECRET=738ab5b83c902a7b81860e05811fd5cd65e95f72\n\n; A properly formatted must have a leading slash but no ending slash.\nMFOX_BASE_URL = https://dev-MetaFox.MetaFox.us\n\n; A properly formatted must have a leading slash but no ending slash.\nMFOX_API_URL=https://dev-MetaFox.MetaFox.us/v5-backend/api/v1\n\nMFOX_GOOGLE_MAP_API_KEY=AIzaSyBiQutexpRrU8p0wg7-H8CM9kfKeOfk9HE\n\n# loading background color\nMFOX_LOADING_BG=#2d2d2d","start-dev-server#Start Dev Server":"# to workbox dir\ncd workbox\n\n# install yarn\nnpm install -g yarn\n\n# install dependencies\nyarn && yarn bootstrap\n\n# start dev server\nyarn start\n\n# start dev server for admin site\nyarn start:admin\nBy default, the development server will start at http://localhost:3000 after starting successfully","directory-structure#Directory Structure":"Workbox contains all libraries, tools and extensions of MetaFox Frontend. We use lerna to organize many packages into a single repository. Let's look overall to the top level of directory structure.\nworkbox/\napp/\npackages/\napps/\nframework/\nyourcompany/\ntests/\njest/\ntools\npackage.json\npackages/framework/: This directory contains metafox core libraries.packages/apps/: It contains modules, themes, and other extensions.packages/app: It contains configuration.Let's look inside to the app/ directory\napp/\ndist/\npublic/\nfavicon.ico\nindex.html\nlogo192.png\nlogo512.png\nsrc/\nindex.tsx\n.env\nsettings.json\npackage.json\napp/dist/: It contains bundled code for the production deployment.app/public/: It contains static resources to build React app.app/src/: It contains source code of the frontend app.","site-configurations#Site Configurations":"Many site configurations are declared in app/settings.json file\nPackages are listed in packages\n\n\n# file /workbox/app/settings.json\n\n{\n\"siteUrl\": \"http://localhost:3000\",\n\"api\": {\n\"baseUrl\": \"http://localhost:3000/api\",\n},\n\"i18n\": {\n\"locale\": \"en\",\n\"supports\": [\"en\", \"ar\", \"zh-cn\", \"fr\"]\n},\n\"packages\": [\n\"@metafox/framework\",\n\"@metafox/ui\",\n\"@metafox/form\",\n\"@metafox/html-viewer\",\n\"@metafox/core\",\n\"@metafox/user\",\n\"@metafox/blog\",\n\"@metafox/feed\",\n\"@metafox/video\",\n\"@metafox/event\",\n\"@metafox/forum\",\n\"@metafox/quiz\",\n\"@metafox/saved\",\n\"@metafox/ad\",\n\"@metafox/group\",\n\"@metafox/pages\",\n\"@metafox/poll\",\n\"@metafox/photo\",\n\"@metafox/friend\",\n\"@metafox/music\",\n\"@metafox/marketplace\",\n\"@metafox/forum\",\n\"@metafox/ad\"\n],\n}","ide-tools#IDE Tools":"Many IDE tools are supporting React Development and you can choose one IDE that you are familiar with. If you haven't got an idea about IDE for developing React, one of the most popular and powerful IDE tools that we would like to recommend to you is VSCode since it has well-support for React Development with many good extensions.","recommended-extensions#Recommended Extensions":"Prettier - Code formatter\nCode Spell Checker\nESLint"}},"/frontend/typings":{"title":"Typescript","data":{"":"","declaration-merging#Declaration Merging":"Please read the Declaration Merging","typests#types.ts":"// file blog/src/types.ts\n\ninterface BlogItemShape {}\n\nexport interface AppState {\nentities: {\nblog: Record;\n};\n}","moduledts#module.d.ts":"The module.d.ts file in all apps is loaded automatically by TypescriptExtend global service manager\n// file blog/src/module.d.ts\nimport '@metafox/framework/Manager';\nimport { BlogList } from './types';\n\ndeclare module '@metafox/framework/Manager' {\ninterface Manager {\nBlogList?: React.FC<{}>;\n}\n}\nExtend global state\n// file blog/src/module.d.ts\nimport '@metafox/framework/Manager';\nimport { AppState } from './types';\n\ndeclare module '@metafox/framework/Manager' {\ninterface GlobalState {\nblog?: AppState;\n}\n}"}},"/frontend/when-lib":{"title":"When Lib","data":{"":"When condition apply for filter menu, block, to check disabled, shown state of an component.","syntax#Syntax":"//\nconst when = [\"truthy\", \"item.can_edit\"];\n** Nested Condtion **\nconst nestedWhen = [\n\"and\",\n[\"truthy\", \"item.can_edit\"],\n[\"falsy\", \"item.can_delete\"],\n];\n\nconst nestedWhenOr = [\n\"and\",\n[\"truthy\", \"item.can_edit\"],\n[\"falsy\", \"item.can_delete\"],\n];","support-rules#Support Rules":"truthy\nfalsy\nequals\nnotEquals\nstrictEquals\nnotStrictEquals\nlessThan\nlessOrEquals\ngreater\ngreaterOrEquals\nlengthEquals\nlengthGreater\nlengthLess\nlengthGreaterOrEquals\nlengthLessOrEquals\noneOf\nexists\nnotExists","examples#Examples":"// true\nwhen({ a: 1, b: 2, c: 3 }, [\n\"and\",\n[\"equals\", \"a\", 1],\n[\"equals\", \"b\", \"2\"],\n[\"equals\", \"c\", \"3\"],\n]);\n\n// true\nwhen({ a: 1, b: 2, c: [1, 2, 3] }, [\"lengthGreaterOrEquals\", \"c\", 3]);\n\n// false\nwhen({ a: 1, b: 2, c: [1, 2, 3] }, [\"lengthGreaterOrEquals\", \"c\", 2]);\n** Menu Items **// file web.menu\nreturn [\n[\n'showWhen' => [\n'and',\n['truthy', 'item.is_pending'],\n['truthy', 'item.extra.can_approve'],\n],\n'menu' => 'blog.blog.detailActionMenu',\n'name' => 'approve',\n'label' => 'blog::phrase.approve',\n'ordering' => 4,\n'value' => 'approveItem',\n'icon' => 'ico-check-circle-o',\n],\n]"}},"/":{"title":"Getting Started","data":{"":"Welcome to the MetaFox Developer Docs. This is a repository for all things of development on MetaFox platform so you may be able to find the answer to your questions right here.Generally, the MetaFox architecture includes 2 main parts: Frontend and Backend. Backend part supports APIs to manipulate data and process business activity. Frontend part will interact with Backend by calling APIs, get returned data and display to end-users. Frontend part includes Web UI and mobile apps. This documentation will walk you through development for both Frontend and Backend","table-of-contents#Table of Contents":"Backend Development\nWeb Frontend Development\nRESTful API\nClass References"}},"/mobile/flatlist":{"title":"Flatlist","data":{"":""}},"/frontend/validation":{"title":"Form & Validation","data":{"":"Form Validation is usually used when getting the response data from server API and before submitting to validate data typings.For example:\n{\n\"type\": \"object\",\n\"properties\": {\n\"question\": {\n\"type\": \"string\",\n\"required\": true,\n\"minLength\": 3,\n\"maxLength\": 255,\n\"label\": \"Question\"\n},\n\"attachments\": {\n\"type\": \"array\",\n\"of\": {\n\"type\": \"object\",\n\"properties\": {\n\"id\": {\n\"type\": \"number\",\n\"required\": true\n},\n\"file_name\": {\n\"type\": \"string\",\n\"required\": true\n}\n}\n},\n\"label\": \"Attachments\"\n}\n}\n}","boolean#Boolean":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.boolean() with all of the additional validation configurations.\nimport { toYup, BooleanTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: BooleanTypeSchema = {\ntype: \"boolean\",\nstrict: true,\nrequired: true,\nerrors: {\nrequired: \"MY custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync(true)); //true\nconsole.log(yupSchema.isValidSync(false)); //true\nconsole.log(yupSchema.isValidSync(\"true\")); //false\nconsole.log(yupSchema.isValidSync(\"false\")); //false\n\n// Equivalent to\n\nconst yupBooleanSchema = yup\n.boolean()\n.required(\"My custom required message\")\n.strict(true);","type#Type":"type BooleanTypeSchema = YupTypeSchema & {\ntype: \"boolean\";\noneOf?: boolean[];\nnotOneOf?: boolean[];\nnullable?: boolean;\nerrors?: YupTypeErrors & {\noneOf?: string;\nnotOneOf?: string;\n};\nwhen?: WhenSchema[];\n};","array#Array":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.array() with all of the additional validation configurations.\nimport { toYup, ArrayTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: ArrayTypeSchema = {\ntype: \"array\",\nstrict: true,\nrequired: true,\nmin: 2,\nerrors: {\nmin: \"My custom min length message\",\nrequired: \"My custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync([\"Good\", \"Morning\"])); //true\nconsole.log(yupSchema.isValidSync(\"Hello\")); //false\n\n// Equivalent to\n\nconst yupArraySchema = yup\n.array()\n.min(2, \"My custom min length message\")\n.required(\"My custom required message\")\n.strict(true);","type-1#Type":"type ArrayTypeSchema = YupTypeSchema & {\ntype: \"array\";\nof?: TypeSchemas;\nmin?: number;\nmax?: number;\nnullable?: boolean;\nunique?: boolean; // to compare string[], int[]\nuniqueBy?: string; // to compare complex object\nerrors?: YupTypeErrors & {\nmin?: string;\nmax?: string;\nunique?: boolean;\nuniqueBy?: string;\n};\nwhen?: WhenSchema[];\n};","date#Date":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.date() with all of the additional validation configurations.\nimport { toYup, DateTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: DateTypeSchema = {\ntype: \"date\",\nstrict: true,\nrequired: true,\nmin: \"2020-01-01\",\nerrors: {\nmin: \"MY custom min date message\",\nrequired: \"MY custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync(\"2020-01-02\")); //true\nconsole.log(yupSchema.isValidSync(\"2019-12-31\")); //false\n\n// Equivalent to\n\nconst yupDateSchema = yup\n.date()\n.min(2, \"My custom min date message\")\n.required(\"My custom required message\")\n.strict(true);","type-2#Type":"type DateTypeSchema = YupTypeSchema & {\ntype: \"date\";\n\n/**\n* number: as a unix timestamp in seconds\n* string: anything parsable by `new Date(string)` e.g. '2020-12-01'\n*/\nmin?: number | string | Reference;\n\n/**\n* number: as a unix timestamp in seconds\n* string: anything parsable by `new Date(string)` e.g. '2020-12-01'\n*/\nmax?: number | string | Reference;\n\nnullable?: boolean;\nerrors?: YupTypeErrors & { min?: string; max?: string };\nwhen?: WhenSchema[];\n};","number#Number":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.number() with all of the additional validation configurations.For more advanced usage, check out the number type test suite.\nimport { toYup, NumberTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: NumberTypeSchema = {\ntype: \"number\",\nstrict: true,\nrequired: true,\nmin: 5,\nerrors: {\nmin: \"My custom min value message\",\nrequired: \"My custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync(5)); //true\nconsole.log(yupSchema.isValidSync(1)); //false\n\n// Equivalent to\n\nconst yupNumberSchema = yup\n.number()\n.min(5, \"My custom min value message\")\n.required(\"My custom required message\")\n.strict(true);","type-3#Type":"type NumberTypeSchema = YupTypeSchema & {\ntype: \"number\";\nmin?: number | Reference;\nmax?: number | Reference;\nlessThan?: number | Reference;\nmoreThan?: number | Reference;\nsign?: \"positive\" | \"negative\";\ninteger?: boolean;\noneOf?: number[];\nnotOneOf?: number[];\nround?: \"floor\" | \"ceil\" | \"trunc\" | \"round\";\nnullable?: boolean;\nerrors?: YupTypeErrors & {\nmin?: string;\nmax?: string;\nlessThan?: string;\nmoreThan?: string;\npositive?: string;\nnegative?: string;\ninteger?: string;\noneOf?: string;\nnotOneOf?: string;\n};\nwhen?: WhenSchema[];\n};","ref#Ref":"ref is very helpful in case you want to compare values of dependent fields. Currently, ref supports min, max, lessThan, moreThanFor example:\nconst json = {\ntype: \"object\",\nproperties: {\nmin_length: {\ntype: \"number\",\nmin: 1,\nmax: 255,\n},\nmax_length: {\ntype: \"number\",\nmin: { ref: \"min_length\" },\n},\n},\n};","object#Object":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.object() with all of the additional validation configurations.\nimport { toYup, ObjectTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: ObjectTypeSchema = {\ntype: \"object\",\nstrict: true,\nproperties: {\nfirstName: {\ntype: \"string\",\nminLength: 2,\nstrict: true,\nrequired: true,\nerrors: {\nminLength: \"first name too short\",\nrequired: \"first name required\",\n},\n},\nlastName: {\ntype: \"string\",\nminLength: 2,\nstrict: true,\nrequired: true,\nerrors: {\nminLength: \"last name too short\",\nrequired: \"last name required\",\n},\n},\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(\nyupSchema.isValidSync({\nfirstName: \"Bob\",\nlastName: \"Jones\",\n})\n); //true\n\nconsole.log(\nyupSchema.isValidSync({\nfirstName: \"Bobby\",\nlastName: \"W\",\n})\n); //false\n\n// Equivalent to\n\nconst yupBooleanSchema = yup\n.object({\nfirstName: yup\n.string()\n.min(2, \"first name too short\")\n.required(\"first name required\")\n.strict(true),\nlastName: yup\n.string()\n.min(2, \"last name too short\")\n.required(\"last name required\")\n.strict(true),\n})\n.strict(true);","type-4#Type":"type ObjectTypeSchema = Omit & {\ntype: \"object\";\nproperties: Record;\n};","keypath-conversion#Keypath Conversion":"Object property keys containing dots (.) will be automatically converted and nested into child object validation types\nBasic Keypath Example.\nAdvanced Keypath Example.\n\nThe following example demonstrates how an object definition will be validated once it is converted to a yup object. It's important to note that this dot notation can be done at any level of an object type validation schema.\nimport { ObjectTypeSchema } from \"@metafox/json2yup\";\n\n// Property names with dot notation keypaths\n\nconst objectSchema: ObjectTypeSchema = {\ntype: \"object\",\nstrict: true,\nproperties: {\n\"user.details.firstName\": {\ntype: \"string\",\nrequired: true,\n},\n},\n};\n\n// Will actually be converted into this object before being 'YUP-ified'\n\nconst actualObjectSchema: ObjectTypeSchema = {\ntype: \"object\",\nstrict: true,\nproperties: {\nuser: {\ntype: \"object\",\nproperties: {\ndetails: {\ntype: \"object\",\nproperties: {\nfirstName: {\ntype: \"string\",\nrequired: true,\n},\n},\n},\n},\n},\n},\n};\nMetaFox supports object uniqueBy when an object is a direct child of Array schema.\nconst schema: ObjectTypeSchema = {\ntype: 'array',\nstrict: true,\nof: {\ntype: 'object',\nuniqueBy: 'name',\nerror: {\nuniqueBy: 'name must be unique in list',\n}\nproperties: {\nname: {\ntype: 'string'\n},\nemail: {\ntype: 'string'\n}\n}\n}\n};","string#String":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.string() with all of the additional validation configuration.\nimport { toYup, StringTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: StringTypeSchema = {\ntype: \"string\",\nstrict: true,\nrequired: true,\nminLength: 5,\nerrors: {\nminLength: \"My custom min length message\",\nrequired: \"My custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync(\"Hello\")); //true\nconsole.log(yupSchema.isValidSync(\"Hi\")); //false\n\n// Equivalent to\n\nconst yupStringSchema = yup\n.string()\n.min(5, \"My custom min length message\")\n.required(\"My custom required message\")\n.strict(true);\nSupport ref to other fields\nconst schema = {\ntype: \"object\",\nproperties: {\nmin_password_length: {\ntype: \"number\",\nrequired: true,\n},\npassword: {\ntype: \"string\",\nminLength: { ref: \"min_password_length\" },\n},\n},\n};","type-5#Type":"type StringTypeSchema = YupTypeSchema & {\ntype: \"string\";\nminLength?: number;\nmaxLength?: number;\ncase?: \"lowercase\" | \"uppercase\";\nuppercase?: number;\nmatches?: { regex: string; excludeEmptyString?: boolean };\nformat?: \"email\" | \"url\";\noneOf?: string[];\nnotOneOf?: string[];\nnullable?: boolean;\nerrors?: YupTypeErrors & {\nminLength?: string;\nmaxLength?: string;\nlowercase?: string;\nuppercase?: string;\nmatches?: string;\nemail?: string;\nurl?: string;\noneOf?: string;\nnotOneOf?: string;\n};\nwhen?: WhenSchema[];\n};","when#When":"Yup allows you to alter the validation on your data based on other values within the validated data payload by using the when() method.The test suite contains examples of how when validation can be used with all the different data types.\nconst schema: ObjectTypeSchema = {\ntype: \"object\",\nstrict: true,\nproperties: {\nshareName: {\ntype: \"boolean\",\nstrict: true,\nrequired: true,\n},\nname: {\ntype: \"string\",\nstrict: true,\nwhen: [\n{\nfields: \"shareName\",\nis: true,\nthen: {\ntype: \"string\",\nminLength: 1,\nerrors: {\nminLength: \"Must fill name in when shareName is true\",\n},\n},\notherwise: {\ntype: \"string\",\nmaxLength: 0,\nerrors: {\nmaxLength: \"Must not fill name in when shareName is true\",\n},\n},\n},\n],\n},\n},\n};","type-6#Type":"type WhenSchema = {\nfields: string | string[];\nis: unknown;\nthen: T;\notherwise?: T;\n};","custom-errors#Custom Errors":"Every schema type has an optional Error objects that allow you to override the default YUP error messages for specific failure reasons.For example, these are the StringTypeSchema error message options:\nYupTypeErrors\nExample\n\n\nerrors?: YupTypeErrors & {\nminLength?: string;\nmaxLength?: string;\nlowercase?: string;\nuppercase?: string;\nmatches?: string;\nemail?: string;\nurl?: string;\noneOf?: string;\nnotOneOf?: string;\n}","example#Example":"In this example, we will set and retrieve our custom Yup error messages, for the minLength and required rules. This can be applied for all schema types and all schema type rules. We will use console.log() to check the schema type's custom error messages.\nimport { toYup, StringTypeSchema } from \"@metafox/json2yup\";\nimport to from \"await-to-js\";\n\nconst schema: StringTypeSchema = {\ntype: \"string\",\nstrict: true,\nrequired: true,\nminLength: 5,\nerrors: {\nminLength: \"My custom min length message\",\nrequired: \"My custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconst [error] = await to(yupSchema.validate(\"Hi\"));\nconsole.log(error.errors); //[\"My custom min length message\"]\n\nconst [error2] = await to(yupSchema.validate(undefined));\nconsole.log(error2.errors); //[\"My custom required message\"]"}},"/mobile/router":{"title":"Page Screen","data":{"":"Router Structure\nRootStack.Navigator\nRootStack.Screen group = auth, mode = modal\nBottomTabNavigator name = MainTab\nTabStack.Navigator name = HomeStack\nTabStack.Navigator name = FriendStack\nTabStack.Navigator name = ...\nRootStack.Group mode=modal\nIn order to create a new screens using file annotation\n/**\n* @type: route\n* name: blog.home\n* path: /blog, /blog/:tab(all|my|pending|draft|friend)\n*/\nimport { createModuleHomeScreen } from \"@metafox/layout\";\n\nexport default createModuleHomeScreen({\nappName: \"blog\",\nresource: \"blog\",\n});\nSupport hocs to create screens\ncreateModuleHomeScreen\ncreateShowDetailScreen\n\nMetafox build service bundle this file and create a screen named 'blog.home' has path map /blog, screen name and page path can be used later to navigating between screens.\nnavigation.navigate(\"blog.home\", {});\n\n// or\n\nnavigation.openLink(\"/blog\");"}},"/mobile/reducers":{"title":"Reducers","data":{"":"MetaFox reducer organization is the same frontend reducers"}},"/mobile/layout":{"title":"Layout","data":{"":"Layout Organization support mobile and tablet.A layout contain a structured location to put react component.In mobile layout, we support 5 location header, top, left, right and content.\n---------------------------------------------------------------------\n| header |\n---------------------------------------------------------------------\n| top |\n---------------------------------------------------------------------\n| | | |\n| left | content | right |\n| | | |\n| | | |\n| | | |\n---------------------------------------------------------------------\n\nLayout file contain json structure of React component for header, and content of layout.Custom your screens\n/**\n* @type: route\n* name: mymodule.home\n* path: /blog, /blog/:tab(all|my|pending|draft|friend)\n*/\n\nimport { useGlobal } from \"@metafox/framework\";\ninterface Props {}\n\nexport default function MyModuleHomeScreen(props: Props) {\nconst { createParams } = useGlobal();\n\nconst params = createParams<{ tab: string }>(props, ({ tab }) => ({\nmodule_name: \"my module\",\nresource_name: \"my block\",\ntab,\n}));\n\nreturn ;\n}\nThen create custom layout.json filefile name: layouts.json\n{\n\"mymodule.home\": {\n\"header\": [\n{\n\"component\": \"module_header\",\n\"props\": {\n\"key\": \"module_header\",\n\"title\": \"notifications\",\n\"back\": false,\n\"rightButtons\": [\n{\n\"component\": \"Header.RightButton\",\n\"props\": {\n\"key\": \"listItem\",\n\"icon\": \"list-bullet-o\"\n}\n},\n{\n\"component\": \"Header.RightButton\",\n\"props\": {\n\"key\": \"addItem\",\n\"icon\": \"plus\",\n\"value\": \"addItem\",\n\"params\": {\n\"module_name\": \"blog\",\n\"resource_name\": \"blog\"\n}\n}\n}\n]\n}\n}\n],\n\"content\": {}\n}\n}\nThen developer can put your custom component in location area, using annotation block\n/**\n* @type: block\n* name: MyCustomBlock\n*/\n\nexport default function () {\nreturn (\n\nyour custom header\n\n);\n}\nOr Update Navigation Header\n/**\n* @type: ui\n* name: MyNavigationHeader\n*/\nimport React from \"react\";\nimport { FormStack, RenderBaseItem, useGlobal } from \"@metafox/framework\";\nimport { View } from \"react-native\";\nimport { IconName } from \"@metafox/icons\";\nimport { useNavigation } from \"@react-navigation/native\";\nimport { StackScreenParamsList } from \"@metafox/framework/types\";\nimport { StackNavigationProp } from \"@react-navigation/stack\";\nimport { useParams } from \"@metafox/layout\";\n\ntype Props = {\nrightButtons?: RenderBaseItem[];\n};\n\nexport function ModuleHomeHeader(props: Props) {\nconst { rightButtons } = props;\nconst { jsxBackend } = useGlobal();\nconst { module_name, resource_name, headerTitle } = useParams();\nconst navigation =\nuseNavigation>();\n\nconst [TitleMenu] = jsxBackend.all([\"Header.TitleMenu\"]);\n\nReact.useLayoutEffect(() => {\nnavigation.setOptions({\n// @ts-ignore\nheaderTitle: () => (\n\n),\n});\n// eslint-disable-next-line react-hooks/exhaustive-deps\n}, [handleAddItem, headerTitle, navigation]);\n\nreturn null;\n}\n\nexport default ModuleHomeHeader;\nYou can collect multiple layouts configuration in to a single file.files blog/layouts.json\n{\n\"blog.home\": {\n\"header\": [\n{\n\"component\": \"module_header\",\n\"props\": {\n\"key\": \"module_header\",\n\"title\": \"notifications\",\n\"back\": false,\n\"rightButtons\": [\n{\n\"component\": \"Header.RightButton\",\n\"props\": {\n\"key\": \"listItem\",\n\"icon\": \"list-bullet-o\"\n}\n},\n{\n\"component\": \"Header.RightButton\",\n\"props\": {\n\"key\": \"addItem\",\n\"icon\": \"plus\",\n\"value\": \"addItem\",\n\"params\": {\n\"module_name\": \"blog\",\n\"resource_name\": \"blog\"\n}\n}\n}\n]\n}\n}\n]\n},\n\"blog.my\": {}\n}"}},"/mobile/translations":{"title":"Translations","data":{"":"MetaFox reducer translations is the same frontend translations"}},"/mobile/sagas":{"title":"Sagas","data":{"":"Mobile sagas development use the same skeleton as frontend sagas"}},"/mobile/service":{"title":"Service","data":{"":""}},"/new-theme":{"title":"Theme","data":{"":"MetaFox theme is based on Material UI, a beautiful by design and features a suite of\ncustomization options that make it easy to implement your own custom design system.MetaFox theme defines styles and layouts, in which styles indicate color palette, typography","create-a-new-theme#Create A New Theme":"Using metafox-cli tool to create a new theme and install to front end workbox.Open terminal and navigate to frontend project root.\nnpx metafox-cli\nExample to create new theme chocolate and associate package @foxtheme/chocolate.\n✔ What do you need? · Create new app\n✔ What is vendor/company name? · foxdev\n✔ What is theme id? · chocolate\nGenerating theme files ...\nUpdating workspace ...\nReloading workspace ...\nGenerated theme chocolate located at ./packages/foxdev/theme-chocolate\nRestart frontend terminal to apply changes.\nRestart frontend terminal then refresh the browser to see changes.","customize-styling#Customize Styling":"Styling is defined src/styles.json, includes\npalette - used to modify colors\ntypography - used to modify css font properties\nshape - used to modify border radius system","palette#Palette":"palette.primary\n{\n\"primary\": {\n\"main\": \"#2682d5\",\n\"light\": \"#4a97dc\",\n\"dark\": \"#0a71cd\"\n}\n}\nUsed to represent primary interface elements for a user. Modify palette primary affect to submit buttons, continue buttons, upload file button, links, selected menu items.palette.errorUsed to represent interface elements that the user should be made aware of. Modify palette error affect to error messagepalette.warningUsed to represent potentially dangerous actions or important messages.palette.successUsed to indicate the successful completion of an action that user triggered.palette.border\npalette.border - border color of components\npalette.divider - used to present color of divider component.\n\npalette.background\n{\n\"background\": {\n\"default\": \"#ededed\",\n\"paper\": \"#fff\",\n\"auto\": \"transparent\"\n}\n}\nbackground.default Used to present background of your site & paper. background.default usedpalette.actionpalette.text","typography#Typography":"Typography indicates css font properties.\nTypography supported variant h1, h2, h3, h4, h5, h6, subtitle1, subtitle2, body1, body2, button, caption, overline.\nThe following code styling for typography h1.\n{\n\"h1\": {\n\"fontWeight\": 700,\n\"fontSize\": \"40px\",\n\"lineHeight\": 1.2,\n\"letterSpacing\": \"0\"\n}\n}","shape#Shape":"Shape indicates border radius base for styling system.\n{\n\"shape\": {\n\"borderRadius\": 0\n}\n}","layoutslot#LayoutSlot":"Layout slots define sizes for layout slot.\n{\n\"layoutSlot\": {\n\"background\": {\n\"paper\": \"#f5f5f5\"\n},\n\"points\": {\n\"xs1\": 306,\n\"xs2\": 322,\n\"xs3\": 400,\n\"xs\": 400,\n\"sm1\": 600,\n\"sm\": 720,\n\"md\": 1024,\n\"lg\": 1200,\n\"xl\": 1920\n}\n}\n}","custom-components#Custom Components":"MetaFox theme systems allow frontend developer overrides material components by processors.","componentsts#Components.ts":"In order to customize themed components. Edit src/processors/Component.tsThe following example explain how to customize button styles\nimport { Theme,ThemedProps } from '@mui/material';\n\nexport default function overridesComponents(theme: Theme): void {\ntheme.components.MuiButton = {\ndefaultProps: {\nvariant: 'contained'\n},\nstyleOverrides: {\nroot: {\nborderRadius: 4,\ntextTransform: 'none',\nboxShadow: 'none',\n}\n}\n};\n// other components\n}","cssbaselinets#CssBaseLine.ts":"In order to overrides css baseline, edit processors/CssBaseLine.ts\nimport { Theme } from '@mui/material';\n\nexport default function overridesGlobalStyles(theme: Theme) {\nif (!theme.components) {\ntheme.components = {};\n}\n\ntheme.components.MuiCssBaseline = {\nstyleOverrides: {\nhtml: {\nWebkitFontSmoothing: 'auto',\nfontSize: '16px'\n},\nbody: {\nfontFamily: theme.fontFamily,\noverflowX: 'hidden',\n},\na: {\ncolor: theme.palette.primary.main\n}\n}\n}\n}","custom-site-blocks#Custom Site Blocks":"Site blocks allows developers add blocks to any templates match screen size and slot name in layout. Put block you need affected to all templates\nin layout.siteBlocks.origin.json.\n{\n[screenSize]: [\n{\n\"blockId\": [global unique id],\n\"component\": [componentName],\n\"slotName\": [slotName],\n\"blockOrigin\": \"site\",\n\"blockLayout\": \"none\"\n}\n]\n}\nExample\n{\n\"small\": [\n{\n\"blockId\": \"appbar0\",\n\"component\": \"core.siteBarMobileBlock\",\n\"slotName\": \"header\",\n\"blockOrigin\": \"site\",\n\"blockLayout\": \"none\"\n}\n],\n\"large\": [\n{\n\"blockOrder\": -1,\n\"component\": \"core.block.appbar\",\n\"slotName\": \"header\",\n\"blockId\": \"appbar0\",\n\"blockOrigin\": \"site\",\n\"blockLayout\": \"none\"\n}\n]\n}","theme-backend#Theme Backend":"In order to allow site administrator control theme, metafox theme system require a backend php package associate with the theme.\nOpen your admincp, access /admincp/layout/theme/create","export-theme#Export Theme":"Export metafox theme as order metafox packages, follow export metafox package to export theme."}},"/new-language":{"title":"Import Phrases","data":{"":"","add-language-pack#Add Language pack":"In this article, we will guide you step by step to create a new language pack.Firstly, please go to AdminCP > Localize and click on the Add Language button. The, follow wizard and fill the info of your language pack. For example, to create a French language pack, you can use the language code as fr , Vendor name as foxdev and App Name as French Pack.Fill the form as following\n\nOn the server, the packages/foxdev/lang-fr directory will be generated automatically.","edit-phrases#Edit Phrases":"Go to \"Language\" then choose \"Export Phrase\" from \"French\" as following image. Export phrases give you a translation file in csv format.","translate-phrase#Translate Phrase":"Open csv file by editor tool which support csv format Etc: Excel, Google Sheets, ...\nkey: the phrase id\nlocale: the locale of phrase\npackage: the package associate with phrase.\norigin_text: Original phrase in english\ntext: actual translation you have to translate\n\nNote that you have not to change key, locale, package, and original_text. Add your translation to text and keep others.\n\nAfter translation, export the result to csv file then import to the server again.","export-language#Export Language":"After updating and saving translation phrases on .php files, you can export language pack in AdminCP.Following instruction /export-package to export language pack."}},"/new-app":{"title":"New App","data":{"":"","introduction#Introduction":"Assume that you can install the MetaFox site on your local machine or server.In this acticle, we will create a new app note of company company. The Notes app will support following features:\nAllow user to post/share notes with attachments, privacy.\nConfigure settings including title, description, tags, category, etc.\nConfigure permissions: admin can assign permissions based on user roles.\nView notes of user's friends on their activity streams.\nGet notifications when others comment, like notes.","create-new-app#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#Backend":"Go to AdminCP > Installed > Apps or use the direct URL /admincp/apps on your browserPress Create New App button. Then, fill info of company and app name to generate new app skeleton.In app options, press Code Generator, and fill \"note\" in others to generate item type note skeleton.The Skeleton for the Backend of Notes app will be generated as below:Directory structure\npackages/\ncompany/\nexample/\nroutes/\napi.php\nresources/\nlang/: define supported languages\ndrivers.php: define drivers\nen/\nphrase.php: define message translation\nvalidation.php: define message phrases\nmenu/\nitems.php: define menu items\nmenus.php: define menus\nsrc/\nDatabase/\nFactories/\nMigrations/\nSeeders/\nHttp/\nControllers/\nRequests/\nResources/\nListeners/\nModels/\nObsevers/\nPolicies/\nProviders/\nRepositories/\ntests/\nUnit/: Unit test source root\nFeatures/: Feature test source root","frontend#Frontend":"We assume that you have downloaded the MetaFox source package and extract it on your local machine or server. The MetaFox package includes 2 main folders: backend and frontend.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:\nyarn metafox create-app company/note\n\n# Reload project settings\nyarn bootstrap\n\n#Restart dev server again\nyarn start\n\nYou will see that the default skeleton for the Frontend of Notes app will be generated as below:\npackages/\ncompany/\nnote/\npackage.json\ntsconfig.json\ntypes.ts: define typings\nindex.tsx: general export\nmodule.d.ts: integrate typing\ncomponents/: define components\npages/: define pages\nHomePage/\nPage.tsx\nlayouts.json","add-new-schema#Add New Schema":"Go to AdminCP > Installed > Apps or use the direct URL /admincp/apps on your browser\nPress Code Generator -> Migration. Fill schema name with \"notes\"\nA new Migration file is created under src/Database/Migrations folder. Its file name will be prefixed by info of datetime, for example:2022_05_23_101328_migrate_notes_table.phpThe datetime prefix helps migration scripts execute Migration files in chronological order.For more info, you can read Laravel Migration.In the Notes app, data is stored in notes table, including the following columns\nid : Primary key\nmodule_id :\nuser_id :\nuser_type : user_type and user_id is morph columns to this note creator.\nowner_id :\nowner_type : owner_id and owner_type is morph columns to this note owner.\nprivacy : Who can see this note.\ntotal_view :\ntotal_like :\ntotal_comment :\ntotal_reply :\ntotal_share :\ntotal_attachment :\ntitle : Note title\nis_approved :\nis_draft : Is this note post as draft ?\nis_sponsor :\nsponsor_in_feed :\nis_featured : Is this note mark as featured ?\nfeatured_at :\nimage_path :\nserver_id : Disk id to storage image\ntags : Contains notes tags\ncreated_at :\nupdated_at :","add-new-model#Add New Model":"","backend-1#Backend":"Go to AdminCP > Installed > Apps or use the direct URL /admincp/apps on your browser\nPress Code Generator -> Model. Fill your package name and previous schemas with \"notes\".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-repositoryHas Model Factory?Generate files for Model Factory.Has Authorization ?Add permissions for the model based on laravel-permissionHas Text Data?Separate text to the second schema. It's helpful to reduce size of the main schema.Has Category DataCreate a pivot model associated with the main schema to store relationships between the main schema and a category schema.Has Tags DataCreate 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\nCode Generator will generate all necessary source files of the model based on the chosen options. It saves you much time.","add-apis#Add APIs":"Go to AdminCP > Installed > Apps or use the direct URL /admincp/apps on your browser\nPress Code Generator -> APIs, choose package and model name then submit.To create skeleton of Frontend resource.\nOpen Terminal.\nGo to the frontend folder\nRun command:\n\n\n\nyarn metafox create-resource company/hello Note\n\nYou can use the command yarn metafox --help to list commands and options.To update api routesNext, we will edit packages/company/note/routes/api.php file to add routes:\n __NAMESPACE__,\n'middleware' => 'auth:api', // logged in required\n], function () {\n// put your routes\nRoute::resource('note', 'BlogController');\n});\nNow, you can open the URL http://localhost:3000/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#Add WebSetting":"Then 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.After adding a new API, edit packages/company/hello/src/Http/Resources/v1/WebSetting.php file as below\n\n*/\nprotected $resources = [\n'note' => Note\\WebSetting::class, // <- add this line to define WebSetting.\n];\n}\nEdit packages/company/note/src/Http/Resources/v1/Note/WebSetting.php file\naddActions([\n'searchItem' => [\n'apiUrl'=> '/note',\n'pageUrl' => '/note/search',\n'placeholder' => 'Search blogs',\n],\n'homePage' => [\n'pageUrl' => '/note',\n],\n'viewAll' => [\n'apiUrl' => '/note',\n'apiRules' => [],\n],\n'viewItem' => [ // view item detail action.\n'apiUrl' => '/note/:id',\n],\n'deleteItem' => [\n'apiUrl' => '/note/:id',\n'confirm' => [\n'title' => 'Confirm',\n'message' => 'Are you sure you want to delete this item permanently?',\n],\n],\n'editItem' => [\n'pageUrl' => '/note/edit/:id',\n],\n'addItem' => [\n'pageUrl' => '/note/add',\n'apiUrl' => '/note/form',\n],\n]);\n}\n\n/**\n* Define forms json should return in web-settings.\n*/\nprotected function initForms(): void\n{\n$this->addForms([\n'filter' => new SearchBlogForm(),\n]);\n}\n}","home-page#Home Page":"Let's open the note/src/pages/HomePage/Page.tsx file and look into some annotations at the top of file\n/**\n* @type: route\n* name: note.home\n* path: /note\n*/\n\nimport { createLandingPage } from \"@metafox/framework\";\n\nexport default createLandingPage({\nappName: \"note\",\npageName: \"note.home\",\nresourceName: \"note\",\n});\n@type: routeDefine this source code is a route, MetaFox bundle tool collects this info and separate to bundle.name: note.homeDefine a global unique key for route, it can be overwritten by another page when you want to customize logic.path: /noteThis is a pattern string to define a route path, based on path-to-regexp","add-new-menu#Add New Menu":"","app-menu#App Menu":"Visit AdminCP > Appearance > Menus to browse all site menus.MetaFox framework contains built-in menus\ncore.primaryMenu: Left side menu of home page\ncore.adminSidebarMenu: Left side menu of the AdminCP\ncore.headerSubMenu: Header top-right menu\ncore.accountMenu: Header account menu\n\nAlso, each app may contain menus on sidebar, admin. For example, in Notes app can have following menus:\nnote.sidebarMenu: sidebar menu of Notes on home page.\nnote.admin: admin menu of Notes app in AdminCP\n\nWhen 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#Resource Menu":"Each resource has 2 action menus in contexts of listing item and viewing item detail.\nFor example:\nnote.note.itemActionMenu\nnote.note.detailActionMenu\n\nThe name of action menus MUST be followed the convention: [appName].[resourceName].[context]","add-forms#Add Forms":"","backend-2#Backend":"Visit AdminCP > Code Generator > FormsPut forms with \"store, update, search\" to create StoreNoteForm, UpdateNoteForm, SearchNoteForm.Edit packages/company/note/routes/api.php file to add routes for form requests.Dive into StoreNoteForm.php\n __NAMESPACE__,\n'middleware' => 'auth:api', // logged in required\n], function () {\n// routes to form\nRoute::get('note/form', 'NoteController@getStoreForm');\nRoute::get('note/form/:id', 'NoteController@getUpdateForm');\nRoute::get('note/form/search', 'NoteController@getSearchForm');\n\n// routes for note resource\nRoute::get('note', 'NoteController');\n\n});\nEdit Company\\Note\\Http\\Controllers\\Api\\v1\\NoteController file, add following methods\nrepository->find($id);\n\nreturn new UpdateNoteForm($resource);\n}\n\n/**\n* Get updating form\n*\n* @param int $id\n*\n* @return SearchNoteForm\n*/\npublic function getSearchForm(int $id): SearchNoteForm\n{\n$resource = $this->repository->find($id);\n\nreturn new SearchNoteForm($resource);\n}\n\n\n/**\n* Get updating form\n*\n* @param int $id\n*\n* @return DestroyNoteForm\n*/\npublic function getDestroyForm(int $id): DestroyNoteForm\n{\n$resource = $this->repository->find($id);\n\nreturn new DestroyNoteForm($resource);\n}\n}\n\nconfig([\n'title' => __p('core.phrase.edit'),\n'action' => '/note', // target api url\n'method' => 'POST', // use \"POST\" method\n'value' => [\n// default value.\n],\n]);\n}\n\n/**\n* Define form structure.\n*/\nprotected function initialize(): void\n{\n$basic = $this->addBasic();\n\n// add form fields.\n$basic->addFields(\nnew Text([\n'name' => 'title',\n'required' => true,\n'returnKeyType' => 'next',\n'label' => 'Title',\n'validation' => [ // add client validation rules\n'required' => true,\n'nullable' => false,\n'errors'=>[\n'required'=> __p('validation.this_is_required_field'),\n]\n]\n])\n);\n\n\n// add cancel buttons\n$footer = $this->addFooter();\n$footer->addFields(\nnew CancelButton([]),\nnew Submit([\n'label' => ($this->resource && $this->resource->id) ?\n__p('core.phrase.save_changes') :\n__p('core.phrase.create'),\n]),\n);\n}\n}\n\nField\tNote\t\tAttachment\tMultiple file picker to attachment\t\tAutocomplete\tAutocomplete text field\t\tBirthday\tDate picker\t\tButtonField\tBasic Button\t\tCancelButton\tButton for cancel action\t\tCaptchaField\tCaptcha field\t\tCategoryField\tCategory picker\t\tCheckboxField\tMultiple Checkbox field\t\tChoice\tCombobox Field\t\tCountryState\tChoose country and state\t\tCustomGenders\tChoose custom gender\t\tDatetime\tDatetime picker\t\tDescriptionField\tTextarea for description\t\tEmail\tText field with email format\t\tFile\tSingle file picker\t\tFilterCategoryField\tCategoryField for filter form\t\tFriendPicker\tFriend picker\t\tHidden\tHidden input\t\tLanguage\tLanguage picker field\t\tLinkButtonField\tButton with href\t\tLocation\tLocation picker field\t\tPassword\tInput password field\t\tPrivacy\tPrivacy picker field\t\tRadio\tRadio Field\t\tSearchBoxField\tText field support search\t\tSinglePhotoField\tSingle photo picker field\t\tSingleVideoField\tSingle video picker field\t\tSubmit\tSubmit button\t\tSwitchField\tAlternate checkbox\t\tTagsField\tMultiple tags input field\t\tText\tSingle text input field\t\tTextArea\tTextarea input\t\tTimezone\tTimezone picker field\t\tTitleField\tSingle title field\t\tTypeCategoryField\tType-category field for 02 level type category\nYou can check all form fields supported at MetaFox built-in fields support","frontend-1#Frontend":"MetaFox Frontend supports built-in dynamic form builder to transform JSON-based responses into ReactJS Form element, For exampleBelow is the sample Form response in JSON format\n{\n\"status\": \"success\",\n\"data\": {\n\"component\": \"form\", // define ReactJs render component\n\"title\": \"Add New Note\", // form title\n\"action\": \"/note\", // target api for http request when form submit.\n\"method\": \"POST\", // http method for http request when form submit.\n\"value\": { // initial values.\n\"module_id\": \"note\",\n\"privacy\": 0,\n\"draft\": 0,\n\"tags\": [],\n\"owner_id\": 0,\n\"attachments\": []\n},\n\"validation\": { // define validation object, based on https://www.npmjs.com/package/yup\n\"type\": \"object\",\n\"properties\": {\n\"title\": {\n\"label\": \"Title\",\n\"type\": \"string\",\n\"required\": true,\n\"minLength\": 3,\n\"maxLength\": 255,\n\"errors\": {\n\"maxLength\": \"Title must be at most 255 characters\"\n},\n}\n}\n},\n\"elements\": { // define form structure\n\"basic\": { // basic form section\n\"name\": \"basic\",\n\"component\": \"container\",\n\"testid\": \"field basic\",\n\"elements\": {\n\"title\": { // form field\n\"component\": \"text\", // Define react render component to form.element.[component]\n\"returnKeyType\": \"next\",\n\"maxLength\": 255,\n\"fullWidth\": true,\n\"margin\": \"normal\",\n\"size\": \"medium\",\n\"variant\": \"outlined\",\n\"name\": \"title\",\n\"required\": true,\n\"label\": \"Title\",\n\"placeholder\": \"Fill in a title for your note\",\n\"description\": \"Maximum 255 of characters\",\n\"testid\": \"field title\"\n},\n\"text\": {\n\"fullWidth\": true,\n\"variant\": \"outlined\",\n\"returnKeyType\": \"default\",\n\"name\": \"text\",\n\"required\": true,\n\"label\": \"Post\",\n\"placeholder\": \"Add some content to your note\",\n\"component\": \"RichTextEditor\",\n\"testid\": \"field text\"\n},\n},\n}\n},\n}\nLook into packages/framework/metafox-form/src/elements/TextField.tsx file\n/**\n* @type: formElement\n* name: form.element.textarea\n*/\nimport MuiTextField from \"@mui/material/TextField\";\nimport { useField } from \"formik\";\nimport { camelCase } from \"lodash\";\nimport { createElement } from \"react\";\nimport { FormFieldProps } from \"../types\";\n\nconst TextAreaField = ({\nconfig,\ndisabled: forceDisabled,\nname,\nformik,\n}: FormFieldProps) => {\nconst [field, meta] = useField(name ?? \"TextField\");\nconst {\nlabel,\ndisabled,\nlabelProps,\nplaceholder,\nvariant,\nmargin = \"normal\",\nfullWidth,\ntype = \"text\",\nrows = 5,\ndescription,\nautoFocus,\nrequired,\nmaxLength,\n} = config;\n\n// fix: A component is changing an uncontrolled input\nif (!field.value) {\nfield.value = config.defaultValue ?? \"\";\n}\n\nconst haveError = Boolean(meta.error && (meta.touched || formik.submitCount));\n\nreturn createElement(MuiTextField, {\n...field,\nrequired,\nmultiline: true,\ndisabled: disabled || forceDisabled || formik.isSubmitting,\nvariant,\nlabel,\n\"data-testid\": camelCase(`field ${name}`),\nautoFocus,\ninputProps: { \"data-testid\": camelCase(`input ${name}`), maxLength },\nrows,\nInputLabelProps: labelProps,\nplaceholder,\nmargin,\nerror: haveError ? meta.error : false,\nfullWidth,\ntype,\nhelperText: haveError ? meta.error : description,\n});\n};\n\nexport default TextAreaField;\n@type: formElementThis annotation determines the file defines a form field component, build tool collects the info to bundle all files into a chunks.name: form.element.textareaWhen 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#Validation":"Form supports validation both Frontend and Backend","backend-3#Backend":"Dive into packages/company/hello/src/Http/Requests/v1/Note/StoreRequest.php file\n\n*/\npublic function rules()\n{\n// Getting validation rules.\nreturn [\n'title'=> ['string', 'required']\n];\n}\n}\nThe main method rules returns an array of validation rules","frontend-2#Frontend":"Frontend validation is based on yup. MetaFox dynamic form builder transforms JSON object to a yup validation object.","translation#Translation":"","backend-4#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.\nnote/\nresources/\nlang/\nen/ : Language code `vi`\nphrase.php : Phrase groups: `phrase`\nvalidation.php : Phrase groups: `validation`\n...\nfr/ : Others language code.\nDive deeper into ./note/resources/lang/en/phrase.php, it defines phrase group phrase, contains a list of phrase_name and phrase value.\n 'Notes', //: phrase_name = \"note\", phrase_value = \"Notes\"\n'label_menu_s' => 'Notes',\n'note_label_saved' => 'Note',\n'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.',\n'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.',\n'new_note_post' => 'New Note Post',\n'edit_note' => 'Editing Note',\n'add_some_content_to_your_note' => 'Add some content to your note',\n'fill_in_a_title_for_your_note' => 'Fill in a title for your note',\n'control_who_can_see_this_note' => 'Control who can see this note.',\n'note_type' => 'Note Type',\n'added_a_note' => 'added a note',\n'note_notification_type' => 'Note Notification',\n'note_featured_successfully' => 'Note featured successfully.',\n];\nIn order to prevent conflict of phrase name, MetaFox translation feature use namespaced translation key convention {namespace}::{group}.{phrase_name}\nto identity translation string in the appliation. etc:\n{i18n.formatMessage({ id: \"toggle_layout_preview\" })};\n// output:
Toggle Device Preview
\n}\nTo translate message in the saga function\nfunction * saga(){\nconst { i18n } = yield* getGlobalContext();\n\nconsole.log({i18n.formatMessage({id: 'toggle_layout_preview'})});\n// output: Toggle Device Preview\n}\nIn 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)To support multiple language, frontend load custom language translation using api /core/translations/web/{language}. The api reponse all messages in the translation group web.\n{\n\"status\": \"success\",\n\"data\": {\n\"accepted\": \"The :attribute must be accepted.\",\n\"active_url\": \"The :attribute is not a valid URL.\",\n\"after\": \"The :attribute must be a date after :date.\",\n\"after_or_equal\": \"The :attribute must be a date after or equal to :date.\",\n\"alpha\": \"The :attribute may only contain letters.\",\n\"alpha_dash\": \"The :attribute may only contain letters, numbers, dashes and underscores.\",\n\"alpha_num\": \"The :attribute may only contain letters and numbers.\",\n\"array\": \"The :attribute must be an array.\",\n\"before\": \"The :attribute must be a date before :date.\",\n\"before_or_equal\": \"The :attribute must be a date before or equal to :date.\"\n},\n\"message\": null,\n\"error\": null\n}","page-browsing#Page Browsing":"Dive deeper into packages/company/note/src/pages/BrowseNotes/Page.tsx file\n/**\n* @type: route\n* name: note.browse\n* path: /note/:tab(friend|all|pending|feature|spam|draft)\n*/\nimport { createBrowseItemPage } from \"@metafox/framework\";\n\nexport default createBrowseItemPage({\nappName: \"note\",\nresourceName: \"note\",\npageName: \"note.browse\",\ncategoryName: \"note_category\",\n});\n@type: route: Define this file must export default route component.name: note.browse: Define page namepath: /note/:tab(friend|all|pending|feature|spam|draft)path-to-regexp pattern to match route.appName: Define app nameresourceName: Define browsing resource namepageName: Define layout page namecategoryName: Define link to category resource type","page-search#Page Search":"","backend-5#Backend":"You can define search form and then add to the WebSetting","global-search#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\n $this->title,\n'text' => 'content of your text',\n'category' => '',\n// others data.\n];\n}\n}\nSearch system has event listener listening on modification of Note data and update its data in queue worker.","activity-feed#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\nisDraft()) {\nreturn null;\n}\n\nreturn new FeedAction([\n'user_id' => $this->userId(),\n'user_type' => $this->userType(),\n'owner_id' => $this->ownerId(),\n'owner_type' => $this->ownerType(),\n'item_id' => $this->entityId(),\n'item_type' => $this->entityType(),\n'type_id' => $this->entityType(),\n'privacy' => $this->privacy,\n]);\n}\n\n/**\n* Define morph map to privacy streams.\n*/\npublic function privacyStreams(): HasMany\n{\nreturn $this->hasMany(PrivacyStream::class, 'item_id', 'id');\n}\n}","event-listeners#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#Model Observer":"In order to track model modification, checkout Eloquent Observer","export-app#Export App":"Following instruction /export-package to export language pack."}},"/frontend/theme":{"title":"Theming","data":{"":"MetaFox Theme system is based on Material-UI. A ThemeProvider component is created and you are able to access all standard Material-UI features in MetaFox platform.","theme-themeprovider#Theme ThemeProvider":"You can pass a custom CSS rules to global, by modifying themeOptions:\n// file packages/sites/example/src/themeOptions.tsx\n\nconst themeOptions = {\noverrides: {\nMuiCssBaseline: {\n\"your-css-selector\": {\n// pass css object here\n},\n},\n},\n};\nFor more info, please refer to Material UI","styled#styled":"You can write custom styles by using makeStyles, creatStyles features, look like the below example:\n// file MyComponent.styles.tsx\nimport { makeStyles, createStyles, Theme } from \"@metafox/ui/styles\";\n\nconst useStyles = makeStyles(\n(theme: Theme) =>\ncreateStyles({\nroot: {},\nheader: {\ndisplay: \"flex\",\nalignItems: \"center\",\ncolor: theme.palette.primary.main,\nmarginBottom: theme.spacing(2),\n},\n}),\n{ name: \"MuiSideMenu\" }\n);\n\nexport default useStyles;\nThen, use useStyles function in a component\n// file MyComponent.tsx\nimport useStyles from './MyComponent.styles'\n\nconst MyComponent = ()=>{\nconst classes = useStyles();\n\nreturn (\n
\n... others class access here.\n
);\n}","control-classname-logic#Control className logic":"You can control React className properties by classnames or clsx tools. clsx is a choice of Material-UI. Thus, we should not add others tools for the same features.For more info, you can check trends and clsx","variables#Variables":"TBD","add-mixins#Add mixins":"TBD","dark-mode-vs-light-mode#Dark mode vs Light mode":"TBD","theme-editor#Theme Editor":"TBD","best-practices#Best Practices":"Separate styling to *.styles.tsx\nDo not pass objects to useStyles","typescript#Typescript":"In other to extend declarations, please read Customization of theme and Package AugmentationTheming is the ability to systematically customize site look & feel to reflect better your product's brand such as color, spacing, round corner, shadow, background and typography.MetaFox provides MaterialUI ThemeProvider at RootContainer, you can access to add new variables to Theme Provider.","variables-1#Variables":"// file /src/themes.tsx\nexport default {\nstatus: {\ncolor: \"#dadada\",\nbackground: \"linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)\",\n},\n};","hook#Hook":"You can use useTheme in pure function to get theme variables as the below example:\nimport { useTheme } from \"@metafox/framework/theme\";\n\nexport const MyComponent = () => {\nconst theme = useTheme();\nreturn (\n\nUsing Theme Variable\n\n);\n};","hoc#HOC":"Example:\nimport { withTheme } from \"@metafox/framework/theme\";\n\nfunction DeepChildRaw(props) {\nconst theme = useTheme();\nreturn (\n\nUsing Theme Variable\n\n);\n}\n\nconst DeepChild = withTheme(DeepChildRaw);","how-to-add-new-custom-fonts-#How to add new custom fonts ?":"TBD","how-to-add-more-icons-#How to add more icons ?":"TBD","should-we-use-font-icon-or-svg-icon-#Should we use font icon or svg icon ?":"TBD"}}} \ No newline at end of file +{"/backend/category":{"title":"Category","data":{"":"App items such as blogs, events, etc., can be organized into categories. In this topic, we are going to walk you through steps to support Categories in the sample Notes app.In the Notes app, we are going to define 2 database schemas note_categories and note_category_data to keep many-to-many relationship between notes and categories","build-schema#Build Schema":"We will define a Migration class with 2 methods up and down\n\nclass NoteMigration extends Migration{\n\npublic function up (){\nDbTableHelper::categoryTable('note_categories', true);\nDbTableHelper::categoryDataTable('note_category_data');\n}\n\npublic function down(){\nSchema::dropIfExists('note_categories');\nSchema::dropIfExists('note_category_data');\n}\n}\n\nIn result, the database schemes will be generated automatically as below","category-ddl#Category DDL":"-- auto-generated definition\nCREATE TABLE note_categories\n(\nid serial CONSTRAINT note_categories_pkey PRIMARY KEY,\nparent_id integer,\nname varchar(255) NOT NULL,\nname_url varchar(255),\nis_active smallint DEFAULT '1'::smallint NOT NULL,\nordering integer DEFAULT 0 NOT NULL,\ntotal_item integer DEFAULT 0 NOT NULL,\ncreated_at timestamp(0),\nupdated_at timestamp(0)\n);","category-data-ddl#Category Data DDL":"-- auto-generated definition\nCREATE TABLE note_category_data\n(\nid bigserial CONSTRAINT blog_category_data_pkey PRIMARY KEY,\nitem_id bigint NOT NULL,\ncategory_id integer NOT NULL\n);"}},"/backend/commands":{"title":"Commands","data":{"":"In this topic, we are going to discuss about most frequently commands which can be executed on Linux terminal.We'll assume that you are familiar with Linux terminal. Firstly, please open the terminal, log in SSH to the Backend server, and go to the source folder of MetaFox Backend. If the MetaFox Backend is set up with a Docker container, you can run the docker exec command to log in to the running container.Here is a list of Linux commands:\n\n# List all supported commands\nphp artisan list\n\n# Install fresh MetaFox site\nbash ./scripts/install.sh\n\n# Update MetaFox site\nphp artisan metafox:update"}},"/backend/eloquent":{"title":"Eloquent","data":{"":"Eloquent is an object-relational mapper (ORM) supported by default in Laravel framework to make it enjoyable when interacting with your database. For more info, you may like to read Eloquent ORM first.","migrations#Migrations":"Migrations, like version control for your database, allow your team to define and share the application's database schema definitions. With database migrations, you no longer have to tell teammates to manually update required changes in their local database schema after pulling certain commits from source control.Migrations classes are under src/Database/Migrations directory.\npackages/metafox/blog/src/: Package source root\nDatabase/\nMigrations/\n2021_02_04_034457_CreateBlogTables.php : Migrations for blog schema\n2021_02_05_034457_CreateCategoryTables.php : Migrations for category schema\nEach Migrations class is child of Illuminate\\Database\\Migrations\\Migration class, and contains 2 up and down methods. Let's see the Migration CreateBlogTables class below\nbigIncrements('id');\n\n// other columns\n});\n}\n\n// setup other table\n}\n\n/**\n* Reverse the migrations.\n*\n* @return void\n*/\npublic function down()\n{\nDbTableHelper::dropStreamTables('blog');\n}\n}\nMetaFox platform is shipped with DbTableHelper, to support creating platform schemas easily and quickly.For more info, please read Laravel migrations","seeders#Seeders":"Each package includes the ability to seed your database with default data using Seeder classes.\nAll package Seeder classes are stored in the src/Database/Seeders directory. By default, a DatabaseSeeder class is defined for you. With this class, you may use the call method to run other Seeder classes. It will allow you to control the seeding order.\n v1\\BlogController::class,\n'v2' => v2\\BlogController::class,\n];\n\n// DO NOT IMPLEMENT ACTION HERE.\n}\nThe BlogController class defines its property controllers to associate 2 versions with 2 different BlogController classes.GatewayController class supports parsing the ver parameter from routing and invoking the respective Controller class associated with that versionFor example, with the route /api/v1/blog/18, routing resolves to MetaFox\\Blog\\Http\\Controllers\\Api\\BlogController::show with parameters ver=v1 and id=18, GatewayController class is dispatched with the same show method, then forward the dispatched call to v1/BlogController::show method.","api-fallback#API Fallback":"If a method isn't defined in the Controller class of certain version, the call will be dispatched to the class method of the previous version.For example with the route /api/v2/blog/18, routing resolves MetaFox\\Blog\\Http\\Controllers\\Api\\BlogController::show with parameters ver=v2 and id=18.GatewayController class will try to dispatch the call with the same show method with parameters of ver=v2 and id=18 in v2\\BlogController class as normal. But if there are no show methods in v2\\BlogController class, the call will be forwarded to v1\\BlogController::show method.","api-controller#API Controller":"In this section, we will see how the Controller classes of certain API version are defined for main action controllers.Below is the sample BlogController class of version v1. This class is located under src/Http/Controllers/Api/v1/BlogController.php\nrepository->viewBlog(user(), $id);\n\nreturn new BlogDetail($blog);\n}\n\n// define other methods.\n}","http-request#HTTP Request":"HTTP Request classes of an app are located under src/Http/Requests directory. Here is the directory structure of the metafox\\blog app\npackages/\nmetafox/\nblog/\nsrc/\nHttp/\nRequests/\nv1/ // contain Requests class for Api version `v1`\nBlog/ // For blog content\nIndexRequest.php // for browsing blog request\nEditFormRequest.php // for edit form request\nYou can read more about Laravel Docs about HTTP Request and HTTP Responses."}},"/backend/datagrid":{"title":"Introduction","data":{"":"MetaFox provides feature data grid builder to configure a JSON structure to render data grid in the frontend.","define-datagrid#Define DataGrid":"enableCheckboxSelection(true);\n\n$this->setSearchForm(new SearchPhraseForm());\n$this->setDataSource('/admincp/phrase', ['q' => ':q','limit'=>50]);\n\n$this->addColumn('id')\n->header('ID')\n->width(80);\n\n$this->addColumn('key')\n->header(__p('core::locale.key_name'))\n->width(200);\n\n$this->addColumn('namespace')\n->header(__p('core::locale.namespace'))\n->width(200);\n\n$this->addColumn('group')\n->header(__p('core::locale.group'))\n->width(120);\n\n$this->addColumn('package_id')\n->header(__p('core::locale.module_name'))\n->width(120);\n\n$this->addColumn('text')\n->header(__p('core::locale.translation'))\n->flex(1);\n\n/*\n* Add default actions\n*/\n$this->withActions(function (Actions $actions) {\n$actions->addDefaults('admincp/phrase');\n});\n\n/*\n* with batch menu actions\n*/\n$this->withBatchMenu(function (BatchActionMenu $menu) {\n$menu->asButton();\n$menu->withDelete();\n$menu->withCreate(__p('core::phrase.add_new_phrase'));\n});\n\n/*\n* with item action menus\n*/\n$this->withItemMenu(function (ItemActionMenu $menu) {\n$menu->withEdit();\n$menu->withDelete();\n});\n}\n}\n\nExample Reponse from data grid api.\n{\n\"status\": \"success\",\n\"data\": {\n\"checkboxSelection\": true,\n\"dataSource\": {\n\"apiUrl\": \"/admincp/phrase\",\n\"apiParams\": { \"q\": \":q\", \"limit\": 50 }\n},\n\"itemActionMenu\": {\n\"variant\": null,\n\"items\": [\n{\n\"name\": \"editItem\",\n\"icon\": \"ico-pencil-o\",\n\"value\": \"row/edit\",\n\"label\": \"Edit\",\n\"params\": { \"action\": \"editItem\" }\n},\n{\n\"name\": \"deleteItem\",\n\"icon\": \"ico-trash\",\n\"value\": \"row/remove\",\n\"label\": \"Delete\",\n\"params\": { \"action\": \"deleteItem\" },\n\"showWhen\": []\n}\n]\n},\n\"batchActionMenu\": {\n\"variant\": \"IconLabel\",\n\"items\": [\n{\n\"name\": \"deleteItem\",\n\"icon\": \"ico-trash\",\n\"value\": \"row/batchRemove\",\n\"label\": \"Delete\",\n\"style\": \"danger\",\n\"params\": { \"action\": \"deleteItems\" }\n},\n{\n\"name\": \"addItem\",\n\"icon\": \"ico-plus\",\n\"value\": \"row/add\",\n\"label\": \"Add New Phrase\",\n\"disabled\": false,\n\"params\": { \"action\": \"addItem\" }\n}\n]\n},\n\"actions\": {\n\"deleteItems\": { \"apiUrl\": \"admincp/phrase\" },\n\"deleteItem\": { \"apiUrl\": \"admincp/phrase/:id\" },\n\"addItem\": { \"apiUrl\": \"admincp/phrase/form\" },\n\"editItem\": { \"apiUrl\": \"admincp/phrase/form/:id\" },\n\"activeItem\": { \"apiUrl\": \"admincp/phrase/active/:id\" }\n},\n\"columns\": [\n{ \"field\": \"id\", \"headerName\": \"ID\", \"width\": 80 },\n{ \"field\": \"key\", \"headerName\": \"Translation Key\", \"width\": 200 },\n{ \"field\": \"namespace\", \"headerName\": \"Namespace\", \"width\": 200 },\n{ \"field\": \"group\", \"headerName\": \"Group\", \"width\": 120 },\n{\n\"field\": \"package_id\",\n\"headerName\": \"Package\",\n\"width\": 120\n},\n{ \"field\": \"text\", \"headerName\": \"Translation\", \"flex\": 1 }\n]\n},\n\"message\": null,\n\"error\": null\n}","configuration#Configuration":"","enablecheckboxselection#enableCheckboxSelection":"Enable checkbox for the first item data grid."}},"/backend/content":{"title":"Content","data":{"":"This is an interface of a Contract content. Example: Blog, Photo v.v...A Contract content always has user_id, user_type, owner_id, and owner_type.\n\nnamespace MetaFox\\Platform\\Contracts;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo;\nuse Illuminate\\Support\\Collection;\n\ninterface Content extends Entity\n{\n/**\n* @return int\n*/\npublic function userId(): int;\n\n/**\n* @return string\n*/\npublic function userType(): string;\n\n/**\n* @return int\n*/\npublic function ownerId(): int;\n\n/**\n* @return string\n*/\npublic function ownerType(): string;\n\n/**\n* @return User|MorphTo\n*/\npublic function user();\n\n/**\n* @return UserEntity|BelongsTo\n*/\npublic function userEntity();\n\n/**\n* @return User|MorphTo\n*/\npublic function owner();\n\n/**\n* @return UserEntity|BelongsTo\n*/\npublic function ownerEntity();\n}","category#Category":"For example, you may want to organize Note items into categories, you need to have 2 schemas note_categories and note_category_data to keep many-to-many relationship of notes and categories","schema#Schema":"class NoteMigration extends Migration{\n\npublic function up (){\nDbTableHelper::categoryTable('note_categories', true);\nDbTableHelper::categoryDataTable('blog_category_data');\n}\n\npublic function down(){\nSchema::dropIfExists('note_categories');\nSchema::dropIfExists('note_category_data');\n}\n}\n\nCategory DDL\n-- auto-generated definition\nCREATE TABLE note_categories\n(\nid serial CONSTRAINT note_categories_pkey PRIMARY KEY,\nparent_id integer,\nname varchar(255) NOT NULL,\nname_url varchar(255),\nis_active smallint DEFAULT '1'::smallint NOT NULL,\nordering integer DEFAULT 0 NOT NULL,\ntotal_item integer DEFAULT 0 NOT NULL,\ncreated_at timestamp(0),\nupdated_at timestamp(0)\n);\nCategory Data DDL\n-- auto-generated definition\nCREATE TABLE note_category_data\n(\nid bigserial CONSTRAINT blog_category_data_pkey PRIMARY KEY,\nitem_id bigint NOT NULL,\ncategory_id integer NOT NULL\n);","tags#Tags":"Each content can support hashtag and tags\nhashtag is tagged words starting by # in description of content.\ntag (sometimes named as topics) is separated word/label attached to a content\n\nIn order to support tags and search in MetaFox app","step-1#Step 1:":"Add *_tag_data relation to migration\nDbTableHelper::createTagDataTable('blog_tag_data');","step-2#Step 2:":"Modify content database table to have tags\nclass CreateBlogTables extends Migration{\n\nfunction up(){\n// add this line to up() method\nDbTableHelper::tagsColumns($table);\n}\n}\nAdd *TagData table class\nbelongsToMany(\nTag::class,\n'blog_tag_data',\n'item_id',\n'tag_id'\n)->using(BlogTagData::class);\n}\n// ... other method\n}","step-4#Step 4":"Add tag scope to associated query to be able to filter content by tag.\nif ($searchTag != '') {\n$query = $query->addScope(new TagScope($searchTag));\n}","step-5#Step 5":"On the Search form, there are no tag fields, users can type \"#\" in search field to search with tags.\n\nclass IndexRequest{\n\n/**\n* @return array\n*/\npublic function validated(): array\n{\n$data = parent::validated();\n\n// .. other process\n\nif (Str::startsWith($data['q'], '#')) {\n$data['tag'] = Str::substr($data['q'], 1);\n$data['q'] = MetaFoxConstant::EMPTY_STRING;\n}\n\nreturn $data;\n}\n}","policy#Policy":"This is the main interface for all Policies.\nnamespace MetaFox\\Platform\\Contracts\\Policy;\n\nuse MetaFox\\Platform\\Contracts\\User;\nuse MetaFox\\Platform\\Contracts\\Content;\n\ninterface ResourcePolicyInterface\n{\npublic function viewAny(User $user, ?User $owner = null): bool;\n\npublic function view(User $user, Content $resource): bool;\n\npublic function viewOwner(User $user, User $owner): bool;\n\npublic function create(User $user, ?User $owner = null): bool;\n\npublic function update(User $user, ?Content $resource = null): bool;\n\npublic function delete(User $user, ?Content $resource = null): bool;\n\npublic function deleteOwn(User $user, ?Content $resource = null): bool;\n}","global-policy#Global Policy":"When you want to set a global policy to every Resource, you should create classes in packages/[company]/[app_name]/src/Policies/Handlers folderIn the below example, CanComment class will add a comment policy to all Policy classes\n\nnamespace MetaFox\\Comment\\Policies\\Handlers;\n\nuse MetaFox\\Platform\\Contracts\\Content;\nuse MetaFox\\Platform\\Contracts\\HasTotalComment;\nuse MetaFox\\Platform\\Contracts\\User;\nuse MetaFox\\Platform\\Support\\Facades\\PrivacyPolicy;\n\nclass CanComment\n{\npublic function check(string $entityType, User $user, Content $resource): bool\n{\n// Code here\n}\n}\n\nIf your policy has its own comment method, it will override the global policy method."}},"/backend/event-list":{"title":"Event Details","data":{"":"Here is the list of Events supported on MetaFox\n+---------------------------------------------------------+------------------------------------------------------------------------+\n| Event | Listeners |\n+---------------------------------------------------------+------------------------------------------------------------------------+\n| activity.count_feed_pending_on_owner | MetaFox\\Activity\\Listeners\\CountFeedPendingOnOwnerListener |\n| activity.delete_feed | MetaFox\\Activity\\Listeners\\DeleteFeedListener |\n| activity.get_feed | MetaFox\\Activity\\Listeners\\GetFeedListener |\n| activity.get_feed_by_item_id | MetaFox\\Activity\\Listeners\\GetFeedByItemIdListener |\n| activity.get_feed_id | MetaFox\\Activity\\Listeners\\GetFeedIdListener |\n| activity.push_feed_on_top | MetaFox\\Activity\\Listeners\\PushFeedOnTopListener |\n| activity.redundant | MetaFox\\Activity\\Listeners\\FeedRedundantListener |\n| activity.sponsor_in_feed | MetaFox\\Activity\\Listeners\\SponsorInFeedListener |\n| activity.update_feed_item_privacy | MetaFox\\Activity\\Listeners\\UpdateFeedItemPrivacy |\n| album.get_album_item | MetaFox\\Photo\\Listeners\\GetAlbumItemListener |\n| background-status.get_bg_status_image | MetaFox\\BackgroundStatus\\Listeners\\GetBgStatusImageListener |\n| comment.related_comments | MetaFox\\Comment\\Listeners\\RelatedCommentsListener |\n| comment.related_comments.item_detail | MetaFox\\Comment\\Listeners\\RelatedCommentsItemDetailListener |\n| core.check_privacy_list | MetaFox\\Core\\Listeners\\CheckPrivacyListListener |\n| core.get_privacy_id | MetaFox\\Core\\Listeners\\GetPrivacyIdListener |\n| core.parse_content | MetaFox\\Friend\\Listeners\\ParseFeedContentListener |\n| core.privacy.check_privacy_member | MetaFox\\Core\\Listeners\\CheckPrivacyMember |\n| core.total_view | MetaFox\\Activity\\Listeners\\FeedRedundantListener |\n| core.user_privacy.get_privacy_id | MetaFox\\Core\\Listeners\\GetPrivacyIdForUserPrivacyListener |\n| feed.composer | MetaFox\\Core\\Listeners\\FeedComposerListener |\n| feed.composer.edit | MetaFox\\Core\\Listeners\\FeedComposerEditListener |\n| friend.can_add_friend | MetaFox\\Friend\\Listeners\\CanAddFriendListener |\n| friend.count_total_friend | MetaFox\\Friend\\Listeners\\CountTotalFriendListener |\n| friend.count_total_mutual_friend | MetaFox\\Friend\\Listeners\\CountTotalMutualFriendListener |\n| friend.create_tag_friends | MetaFox\\Friend\\Listeners\\CreateTagFriendsListener |\n| friend.delete_tag_friend | MetaFox\\Friend\\Listeners\\DeleteTagFriendListener |\n| friend.friend_ids | MetaFox\\Friend\\Listeners\\GetFriendIdsListener |\n| friend.get_count_new_friend_request | MetaFox\\Friend\\Listeners\\CountNewFriendRequestListener |\n| friend.get_friend_ship | MetaFox\\Friend\\Listeners\\GetFriendShipListener |\n| friend.get_photo_tag_friends | MetaFox\\Friend\\Listeners\\GetPhotoTagFriendsListener |\n| friend.get_suggestion | MetaFox\\Friend\\Listeners\\GetSuggestionListener |\n| friend.get_tag_friend_by_id | MetaFox\\Friend\\Listeners\\GetTagFriendByIdListener |\n| friend.get_tag_friends | MetaFox\\Friend\\Listeners\\GetTagFriendsListener |\n| friend.is_friend | MetaFox\\Friend\\Listeners\\IsFriendListener |\n| friend.is_friend_of_friend | MetaFox\\Friend\\Listeners\\IsFriendOfFriendListener |\n| friend.update_tag_friends | MetaFox\\Friend\\Listeners\\UpdateTagFriendsListener |\n| friendList.check_privacy_list | MetaFox\\Friend\\Listeners\\CheckPrivacyListListener |\n| group.get_privacy_for_setting | MetaFox\\Group\\Listeners\\PrivacyForSetting |\n| group.get_search_resource | MetaFox\\Group\\Listeners\\GetSearchResourceListener |\n| group.get_user_preview | MetaFox\\Group\\Listeners\\UserPreviewListener |\n| group.update_cover | MetaFox\\Group\\Listeners\\UpdateGroupCover |\n| hashtag.create_hashtag | MetaFox\\Hashtag\\Listeners\\ItemTagAwareListener |\n| hashtag.update_total_item | MetaFox\\Hashtag\\Listeners\\UpdateTotalItemListener |\n| like.is_liked | MetaFox\\Like\\Listeners\\IsLikedListener |\n| like.most_reactions | MetaFox\\Like\\Listeners\\MostReactionsListener |\n| like.user_reacted | MetaFox\\Like\\Listeners\\UserReactedListener |\n| models.notify.created | MetaFox\\Core\\Listeners\\ModelCreatedListener |\n| models.notify.creating | MetaFox\\Core\\Listeners\\ModelCreatingListener |\n| models.notify.deleted | MetaFox\\Core\\Listeners\\ModelDeletedListener |\n| models.notify.updated | MetaFox\\Core\\Listeners\\ModelUpdatedListener |\n| models.notify.updating | MetaFox\\Core\\Listeners\\ModelUpdatingListener |\n| packages.installed | MetaFox\\Sticker\\Listeners\\PackageInstalledListener |\n| packages.scan | MetaFox\\Core\\Listeners\\PackageScanListener |\n| packages.updated | MetaFox\\User\\Listeners\\PackageUpdatedListener |\n| notification.delete_notification_by_type_and_notifiable | MetaFox\\Notification\\Listeners\\DeleteNotifyByTypeAndNotifiableListener |\n| notification.getEmailNotificationSettings | MetaFox\\Notification\\Listeners\\GetEmailNotificationSettingsListener |\n| notification.get_count_new_notification | MetaFox\\Notification\\Listeners\\GetNewNotificationCount |\n| notification.updateEmailNotificationSettings | MetaFox\\Notification\\Listeners\\UpdateEmailNotificationSettingsListener |\n| packages.deleting | MetaFox\\Core\\Listeners\\PackageDeletingListener |\n| page.get_privacy_for_setting | MetaFox\\Page\\Listeners\\PrivacyForSetting |\n| page.get_search_resource | MetaFox\\Page\\Listeners\\GetSearchResourceListener |\n| page.get_user_preview | MetaFox\\Page\\Listeners\\UserPreviewListener |\n| page.update_cover | MetaFox\\Page\\Listeners\\UpdatePageCover |\n| photo.create | MetaFox\\Photo\\Listeners\\PhotoCreateListener |\n| photo.media_remove | MetaFox\\Photo\\Listeners\\MediaRemoveListener |\n| photo.media_update | MetaFox\\Photo\\Listeners\\MediaUpdateListener |\n| photo.media_upload | MetaFox\\Photo\\Listeners\\MediaUploadListener |\n| photo.update_avatar_path | MetaFox\\Photo\\Listeners\\UpdateAvatarPathListener |\n| search.created | MetaFox\\Search\\Listeners\\ModelCreatedListener |\n| search.deleted | MetaFox\\Search\\Listeners\\ModelDeletedListener |\n| search.updated | MetaFox\\Search\\Listeners\\ModelUpdatedListener |\n| site_settings.updated | MetaFox\\ChatPlus\\Listeners\\SiteSettingUpdated |\n| sticker.get_sticker_image | MetaFox\\Sticker\\Listeners\\GetStickerImageListener |\n| tag.get_search_resource | MetaFox\\Hashtag\\Listeners\\GetSearchResourceListener |\n| user.blocked | MetaFox\\Friend\\Listeners\\UserBlockedListener |\n| user.get_mentions | MetaFox\\User\\Listeners\\UserGetMentions |\n| user.get_privacy_for_setting | MetaFox\\User\\Listeners\\PrivacyForSetting |\n| user.get_search_resource | MetaFox\\User\\Listeners\\GetSearchResourceListener |\n| user.get_user_preview | MetaFox\\User\\Listeners\\UserPreviewListener |\n| user.social_account.callback | MetaFox\\Facebook\\Listeners\\UserSocialAccountCallbackListener |\n| user.social_account.request | MetaFox\\Facebook\\Listeners\\UserSocialAccountRequestListener |\n| video.mux.webhook_callback | MetaFox\\Video\\Listeners\\MuxWebhookCallback |\n+---------------------------------------------------------+------------------------------------------------------------------------+\nNext, we are going to dive into deeper about all supported Events","model-events#Model Events":"These are events that were called in EloquentModelObserver class located at packages/platform/src/Support/EloquentModelObserver.php","model-creating-event#Model Creating Event":"Event name: models.notify.creating\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.creating', [$model]);","model-created-event#Model Created Event":"Event name: models.notify.created\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.created', [$model]);","model-updating-event#Model Updating Event":"Event name: models.notify.updating\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.updating', [$model]);","model-updated-event#Model Updated Event":"Event name: models.notify.updated\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.updated', [$model]);","model-deleted-event#Model Deleted Event":"Event name: models.notify.deleted\nExample:\n\n\n/**\n* @var Model $model\n*/\napp('events')->dispatch('models.notify.deleted', [$model]);","package-events#Package Events":"","package-installed-event#Package installed Event":"This Event will be called after running the install command\nEvent name: packages.installed","package-updated-event#Package updated Event":"This Event will be called after running the updated command\nEvent name: packages.updated","package-scan-event#Package scan Event":"Scan all package directories and MetaFox config files to update database\nEvent name: packages.scan","package-export-menu-event#Package Export Menu Event":"This event will be called when exporting package\nEvent name: packages.export_menu","package-export-phrase-event#Package Export Phrase Event":"This event will be called when exporting phrase\nEvent name: modules.export_phrase","package-deleting-event#Package Deleting Event":"This event will be called when uninstalling package\nEvent name: packages.deleting","core-package-events#Core Package Events":"","check-privacy-list-event#Check Privacy List Event":"Called when checking if user has owner privacy list\nEvent name: core.check_privacy_list","parse-content-event#Parse content Event":"Called when parsing Content. Can be use when converting mention, tag,.. in text\nEvent name: core.parse_content","check-privacy-member-event#Check Privacy member Event":"Called when checking user has privacy member\nEvent name: core.privacy.check_privacy_member","total-view-event#Total View Event":"Called when getting total view on a Content to feed\nEvent name: core.total_view","get-privacy-id-event#Get Privacy Id Event":"Privacy id form user_id and privacy\nEvent name: core.user_privacy.get_privacy_id","activity-package-events#Activity Package Events":"","count-pending-feed-on-owner-event#Count Pending Feed On Owner Event":"Get count pending feed\nEvent name: activity.count_feed_pending_on_owner","delete-feed-event#Delete Feed Event":"Event name: activity.delete_feed","get-feed-event#Get Feed Event":"Event name: activity.get_feed","get-feed-by-item-id-event#Get Feed By Item Id Event":"Event name: activity.get_feed_by_item_id","get-feed-by-id-event#Get Feed By Id Event":"Event name: activity.get_feed_id","push-feed-on-top-event#Push Feed On Top Event":"Event name: activity.push_feed_on_top","push-feed-on-top-event-1#Push Feed On Top Event":"Event name: activity.push_feed_on_top","redundant-data-event#Redundant Data Event":"Called when updating info of total comment, total like, total view,... from resource Content to feed\nEvent name: activity.redundant","sponsor-feed-event#Sponsor Feed Event":"Event name: activity.sponsor_in_feed","update-feed-item-privacy-event#Update feed item privacy Event":"Called when update privacy on feed, also update on item\nEvent name: activity.update_feed_item_privacy","feed-composer-event#Feed Composer Event":"Called when create feed by feed form\nEvent name: feed.composer","edit-feed-composer-event#Edit Feed Composer Event":"Event name: feed.composer.edit","friend-package-events#Friend Package Events":"","check-can-add-friend-event#Check Can Add Friend Event":"Event name: friend.can_add_friend","count-total-friend-event#Count Total Friend Event":"Event name: friend.count_total_friend","count-total-mutual-friend-event#Count Total Mutual Friend Event":"Event name: friend.count_total_mutual_friend","create-tag-friend-event#Create Tag Friend Event":"Event name: friend.create_tag_friends","delete-tag-friend-event#Delete Tag Friend Event":"Event name: friend.delete_tag_friend","get-friend-ids-event#Get Friend Ids Event":"Event name: friend.friend_ids","get-friendship-event#Get Friendship Event":"Event name: friend.get_friend_ship","count-new-friend-request-event#Count New Friend Request Event":"Event name: friend.get_count_new_friend_request","get-tagged-friend-in-photo-event#Get Tagged Friend In Photo Event":"Event name: friend.get_photo_tag_friends","get-friend-suggestion-event#Get Friend Suggestion Event":"Event name: friend.get_suggestion","get-tagged-friend-by-tag-id-event#Get Tagged Friend By Tag Id Event":"Event name: friend.get_tag_friend_by_id","get-tagged-friend-by-tag-id-event-1#Get Tagged Friend By Tag Id Event":"Event name: friend.get_tag_friend_by_id","get-tagged-friends-event#Get Tagged Friends Event":"Event name: friend.get_tag_friends","check-is-friend-of-friend-event#Check Is Friend Of Friend Event":"Event name: friend.is_friend_of_friend","update-tagged-friend-event#Update Tagged Friend Event":"Event name: friend.update_tag_friends","comment-package-events#Comment Package Events":"","get-related-comments-event#Get Related Comments Event":"Event name: comment.related_comments","get-related-comments-for-item-detail-event#Get Related Comments For Item Detail Event":"Event name: comment.related_comments.item_detail","background-status-package-events#Background Status Package Events":"","get-background-status-by-id-event#Get Background Status By Id Event":"Event name: background-status.get_bg_status_image","group-package-events#Group Package Events":"","get-privacy-for-setting-event#Get Privacy For Setting Event":"Event name: group.get_privacy_for_setting","get-search-resource-event#Get Search Resource Event":"Event name: group.get_search_resource","get-user-preview-event#Get User Preview Event":"Event name: group.get_user_preview","update-cover-event#Update Cover Event":"Event name: group.update_cover","hashtag-package-events#HashTag Package Events":"","create-hashtag-event#Create HashTag Event":"Event name: hashtag.create_hashtag","update-total-item-event#Update Total Item Event":"Event name: hashtag.update_total_item","get-search-resource-event-1#Get Search Resource Event":"Event name: tag.get_search_resource","like-package-events#Like Package Events":"","check-is-liked-event#Check Is Liked Event":"Event name: like.is_liked","get-most-reactions-event#Get Most Reactions Event":"Event name: like.most_reactions","get-user-reacted-event#Get User Reacted Event":"Event name: like.user_reacted","notification-package-events#Notification Package Events":"","delete-notification-event#Delete Notification Event":"When deleting an item, also delete notification of an item\nEvent name: notification.delete_notification_by_type_and_notifiable","get-email-notification-settings-event#Get Email Notification Settings Event":"Event name: notification.getEmailNotificationSettings","count-new-notification-settings-event#Count New Notification Settings Event":"Event name: notification.get_count_new_notification","update-notification-settings-event#Update Notification Settings Event":"Event name: notification.updateEmailNotificationSettings","page-package-events#Page Package Events":"","get-privacy-for-setting-event-1#Get Privacy For Setting Event":"Event name: page.get_privacy_for_setting","get-search-resource-event-2#Get Search Resource Event":"Event name: page.get_search_resource","get-user-preview-event-1#Get User Preview Event":"Event name: page.get_user_preview","update-cover-event-1#Update Cover Event":"Event name: page.update_cover","photo-package-events#Photo Package Events":"","create-photo-event#Create Photo Event":"Event name: photo.create","remove-media-event#Remove Media Event":"Event name: photo.media_remove","update-media-event#Update Media Event":"Event name: photo.media_update","upload-media-event#Upload Media Event":"Event name: photo.media_upload","update-avatar-path-event#Update Avatar Path Event":"Event name: photo.update_avatar_path","search-package-events#Search Package Events":"","create-search-record-event#Create Search Record Event":"This event will be called after Content was created\nEvent name: search.created","update-search-record-event#Update Search Record Event":"This event will be called after content was updated\nEvent name: search.updated","delete-search-record-event#Delete Search Record Event":"This event will be called after content was deleted\nEvent name: search.deleted","sticker-package-events#Sticker Package Events":"","get-sticker-image-event#Get Sticker Image Event":"Event name: sticker.get_sticker_image","user-package-events#User Package Events":"","block-user-event#Block User Event":"Event name: user.blocked","get-mentions-event#Get Mentions Event":"Event name: user.get_mentions","get-privacy-for-setting-event-2#Get Privacy For Setting Event":"Event name: user.get_privacy_for_setting","get-search-resource-event-3#Get Search Resource Event":"Event name: user.get_search_resource","get-user-preview-event-2#Get User Preview Event":"Event name: user.get_user_preview","social-account-request-event#Social Account Request Event":"Event name: user.social_account.request","social-account-callback-event#Social Account Callback Event":"Event name: user.social_account.callback","video-package-events#Video Package Events":"","mux-webhook-callback-event#Mux Webhook Callback Event":"Event name: video.mux.webhook_callback"}},"/backend/event":{"title":"Event","data":{"":"Laravel's Events provides a simple Observer pattern implementation, allowing you to subscribe and listen to various events that occur within your application. Event classes are typically stored in the app/Events directory, while their Listeners are stored in app/Listeners directory. Don't worry if you don't see these directories in your application as they will be created automatically as you generate events and listeners using Artisan console commands.\n\nEvents serve as a great way to decouple various aspects of your application, since a single event can have multiple listeners that do not depend on each other. For example, you may wish to send a Slack notification to your user each time an order has shipped. Instead of coupling your order processing code to your Slack notification code, you can raise an App\\Events\\OrderShipped event which a listener can receive and use to dispatch a Slack notification.","dispatch-event#Dispatch Event":"For example, dispatch an event named search.updated and pass $model argurment. All Listener classes registered with the search.updated event will be processed.\ndispatch('search.updated', [$model]);","listeners#Listeners":"Define Event Listener\n [\nSearchUpdatedListener::class,\n],\n];\n}\n}","response#Response":"TBDFor futher info, please read Laravel Event","bail#Bail":"Sometimes, you may need dispatcher to stop running other listeners whenever a listener returns a not-null value.\ndispatch('search.updated', [$model], $bail=true);\n\nIn the above sample code, with $bail=true parameter, at the first time a listener returns not-null value, the dispatcher will stop running remaining registered Listener and returns to $response value."}},"/backend/interfaces":{"title":"Interfaces","data":{"":"","contract-user#Contract User":"This is an Interface of an User. Contract User is a child of Contract Content, and has more abilities.\n\nnamespace MetaFox\\Platform\\Contracts;\n\ninterface User extends Content, BigNumberId\n{\n/**\n* Result must contain entity_type, user_name, user_image, name.\n*\n* @return array\n*/\npublic function toUserResource(): array;\n\n/**\n* Determine if resource can be blocked.\n*\n* @return bool\n*/\npublic function canBeBlocked(): bool;\n}\n\nA special ability of a Contract User is that it uses BigNumberId to set ID (with setEntityId method) for entities tables ( to support global and unique ID for all resources).\n\nnamespace MetaFox\\Platform\\Contracts;\n\ninterface BigNumberId\n{\npublic function entityId();\n\npublic function setEntityId(int $id);\n\npublic function entityType();\n}\n\nTo define which details are stored in entities table, we can use the toUserResource() method. For example: in User, you can define this method as below:\n\npublic function toUserResource()\n{\nreturn [\n'entity_type' => $this->entityType(),\n'user_name' => $this->user_name,\n'user_image' => $this->profile != null ? $this->profile->user_image : null,\n'name' => $this->full_name,\n];\n}"}},"/backend/installation":{"title":"MetaFox Developer Installation","data":{"":"This MetaFox Developer package includes the MetaFox Core and some basic apps such as Feed, Photos, Members, Blogs, Polls.","system-requirements#System Requirements":"On the Development environment: since we have to build MetaFox Frontend, your server will need to have at least 4G RAM as the Recommendation for ReactThis article will guide you how to set up Local Development with docker. Thus, the server will need to have docker and docker-compose installed first.","download-source#Download Source":"You can download Developer source from Client Area, under MetaFox Dev license. Then, extract the package on your server.For example, all source files will be uploaded to your server under home/metafox folder","local-installation-steps#Local Installation Steps":"Open Terminal and log in SSH to your server. It is necessary to note that you will need to log in with SSH user having permissions to run dockerThen, run the following command\ncd /home/metafox\nbash install.sh\nAfter starting successfully, you can check the MetaFox site at http://your_server_IP:8081 or http://your_domain:8081","issues#Issues":"If you got issues with MetaFox Installation, please open new tickets in Client Area with department of 3rd Party Developer for further support.","references#References":"Laravel\nAuthentication\nAuthorization\nImage Processing\nDebug Tool\nStorage System\nPackage Management\nGlobalization\nMessage Queue","unit-test#Unit Test:":"Testing\nMockery\nFaker"}},"/backend/form":{"title":"Form","data":{"":"","create-form#Create Form":"MetaFox provide form configuration feature to define JSON structure that frontend can be rendered via RestFul api.\n\nnamespace MetaFox\\Core\\Http\\Resources\\v1\\Admin;\n\nuse MetaFox\\Platform\\Support\\Form\\AbstractForm;\n\nclass GeneralSiteSettingForm extends AbstractForm\n{\nprotected function prepare():void\n{\n\n// add form configuration\n}\n\nprotected funciton initialize():void\n{\n// add fields structure\n}\n}\nShow form api in controller.\n\n\nuse MetaFox\\Core\\Http\\Resources\\v1\\Admin\\GeneralSiteSettingForm;\n\nclass ApiController{\n\npublic function showForm()\n{\nreturn new GeneralSiteSettingForm();\n}\n}","form-fields#Form Fields":"AbstractForm group related fields in sections, each section contains FormFields, AbstractForm add form fields in initialize() method. There are 02 sections\nbasic and footer are support.Supported fields configuration is located at ./config/form.php.Create text field directory\n\n$text = new TextField(['name'=>'firstName']);\n\nCreate via builder service\nuse MetaFox\\Platform\\Form\\Builder;\n\n$text = Builder::text('firstName'); // get TextField instance\n\n\nuse MetaFox\\Platform\\Form\\Builder;\n\nclass GeneralSiteSettingForm extends AbstractForm\n{\n\nprotected funciton initialize():void\n{\n// section basic\n$basic = $this->addBasic();\n\n// add field\n$basic->addField(\nBuilder::text('general.site_name') // Create Text field with name \"general.site_name\"\n->label('Name Of Site') // Add label\n->description('Name of your site.') // Add description\n);\n\n// add field\n$basic->addField(\nBuilder::text('general.site_title') // Create Text field with name \"general.site_title\"\n->label('Name Title') // Add label\n->description('Fill your site title') // Add description\n);\n\n// add footer structure\n$this->addFooter()\n->addField(\nBuilder::submit() // Add \"submit\", field use default name=\"_submit\"\n->label(__p('core::phrase.save_changes'))\n);\n}\n}\nMetaFox comes with a list of build-in suppor form fields. Fore more information checkout","form-validation#Form Validation":"Validation configuration defines JSON structure to\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::text('general.site_title') // Create Text field with name \"general.site_title\"\n->label('Name Title') // Add label\n->yup( // add validation\nYup::string() // Add yup string at https://dev-docs.metafox.com/frontend/validation#string\n->required(__p('core::validation.site_title_could_not_be_blank')) // set required\n)\nMetaFox comes with a list of build-in suppor form validation. Fore more information checkout","yupstring#Yup::string()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#string\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuiler::text('general.site_title')\n->yup( // add validation\nYup::string()\n->required(__p('core::validation.site_title_could_not_be_blank')) // set required\n->minLength(5) // Set min length rule\n->maxLength(64) // Set max length\n->matches('^\\w+$') // set regex match pattern\n->lowercase() // Require lowercase format\n// ->uppercase() // Require uppercase format\n->email() // Require email format\n// ->url() // Require full url format\n)","yupnumber#Yup::number()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#number\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::text('general.site_title')\n->yup( // add validation\nYup::string()\n->required(__p('core::validation.site_title_could_not_be_blank')) // set required\n->min(5) // Set \">=\" compare\n->max(64) // Set \"<=\" compare\n// ->lessThan(100) // Set \"<\" compare\n// ->moreThan() // Set \">\" compare\n// ->int()\n->unint() // Require unsigned int\n// ->positive() // Require positive number\n// ->negative() // Require negative number\n)","yupdate#Yup::date()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#date\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::text('event.start_date')\n->yup( // add validation\nYup::string()\n->required(__p('core::validation.this_is_required_field')) // set required\n->min('2022-10-10') // Set min length rule\n->max('2026-10-10') // Set max length\n)","yupboolean#Yup::boolean()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#boolean\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::checkbox('event.enable_close')\n->yup( // add validation\nYup::boolean()\n->required(__p('core::validation.this_is_required_field')) // set required\n)","yuparray#Yup::array()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#array\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::questions('questions')\n->yup( // add validation\nYup::array()\n->of(Yup::string()) // define validator for each array\n->min(2)\n->max(5)\n)","yupobject#Yup::object()":"Add yup string at https://dev-docs.metafox.com/frontend/validation#object\n\nuse MetaFox\\Yup\\Yup;\nuse MetaFox\\Form\\Builder;\n\nBuilder::jobs('person')\n->yup( // add validation\nYup::object()\n->addProperty( // add property\n'firstName',\nYup:string()->required()\n)\n->addProperty(\n'lastName',\nYup:string()->required()\n)\n->addProperty(\n'email',\nYup::string()->required()->email()\n);\n)"}},"/backend/mailer":{"title":"Mailer","data":{"":"","add-custom-mail-transport#Add Custom Mail Transport":"In other to add custom storage, add new driver type=\"mail-transport\" to drivers.phpexample\n 'MetaFox\\\\Core\\\\Http\\\\Resources\\\\v1\\\\Admin\\\\MailerSmtpSettingForm',\n'type' => 'form-mailer',\n'name' => 'smtp',\n'version' => 'v1',\n'package_id' => 'metafox/core',\n'alias' => null,\n'is_admin' => 1,\n'is_active' => 1,\n'is_preload' => 0,\n'title' => 'SMTP Mail Transport',\n'url' => '',\n'description' => '',\n];\n]"}},"/backend/load-reduce":{"title":"Load Reduce","data":{"":"Reducing number of database queries using array cache (cache lifetime within request lifecycle).","register#Register":"Register reducer\nclass PackageServiceProvider extends ServiceProvider\n{\n\n/**\n* Register the service provider.\n*\n* @return void\n*/\npublic function register()\n{\n$this->callAfterResolving('reducer', function ($reducer) {\n$reducer->register([\n\\MetaFox\\Saved\\Support\\LoadMissingIsSaved::class,\n]);\n});\n}\n}","sample#Sample":"Using union to pick data\nuserId();\n$items = $reducer->entities()\n->filter(fn ($x) => $x instanceof HasSavedItem && $x->entityType() !== 'feed')\n->map(fn ($x) => [$x->entityType(), $x->entityId()]);\n\nif ($items->isEmpty()) {\nreturn null;\n}\n\n$key = fn ($type, $id) => sprintf('saved::exists(user:%s,%s:%s)', $userId, $type, $id);\n\n$data = $items->reduce(function ($carry, $x) use ($key) {\n$carry[$key($x[0], $x[1])] = false;\n\nreturn $carry;\n}, []);\n\n/** @var Builder $query */\n$query = $items->map(function ($x) use ($userId) {\nreturn DB::table('saved_items')\n->select(['item_id', 'item_type'])\n->where([\n'user_id' => $userId,\n'item_type' => $x[0],\n'item_id' => $x[1],\n])->limit(1);\n})->reduce(function ($carry, $x) {\nreturn $carry ? $carry->union($x) : $x;\n});\n\nreturn $query->get()\n->reduce(function ($carry, $x) use ($key) {\n$carry[$key($x->item_type, $x->item_id)] = true;\n\nreturn $carry;\n}, $data);\n}\n}\nCheck is saved\nclass IsSavedItem implements PolicyRuleInterface\n{\npublic function check(string $entityType, User $user, $resource, $newValue = null): ?bool\n{\nif (!$resource instanceof HasSavedItem) {\nreturn false;\n}\n\nreturn LoadReduce::remember(\nsprintf('saved::exists(user:%s,%s:%s)', $user->userId(), $resource->entityType(), $resource->entityId()),\nfn () => resolve(SavedRepositoryInterface::class)->checkIsSaved(\n$user->userId(),\n$resource->entityId(),\n$resource->entityType()\n)\n);\n}\n}","best-practise#Best practise":"// DON'T use eager loading for finding one. It's can duplicate query.\n$blog = $this\n->with(['user', 'userEntity', 'categories', 'activeCategories', 'attachments'])\n->find($id);","core_item_statstitic#core_item_statstitic":"Sample migration total pending comments\nINSERT INTO core_item_statistics (item_id, item_type)\nSELECT A.item_id, A.item_type\nFROM comments as A\nLEFT JOIN core_item_statistics as B\nON A.item_id = B.item_id AND A.item_type = B.item_type\nWHERE B.item_type is null\nGROUP BY A.item_id, A.item_type;\npostgres\nWITH B as (SELECT item_id, item_type, count(*) as aggregate\nFROM comments\nWHERE is_approved = 0\nAND parent_id =0\nGROUP BY item_id, item_type)\nUPDATE core_item_statistics as A\nSET total_pending_comment = B.aggregate\nFROM B\nWHERE B.item_id = A.item_id\nAND B.item_type = A.item_type;\nMySql\nUPDATE core_item_statistics A\nINNER JOIN (\nselect item_id, item_type, count(*) as aggregate\nfrom comments\nwhere is_approved=0 and parent_id = 0\ngroup by item_id, item_type\n) as B ON(B.item_id = A.item_id and B.item_type=A.item_type)\nSET A.total_pending_comment = B.aggregate\nDepends on database driver to run migration scripts\nuse \\Illuminate\\Support\\Facades\\DB;\n\n$driver = DB::getDriverName();\n\nif ($driver == 'pgsql') {\nDB::statement($sql)\n} elseif ($driver == 'mysql') {\n$sql;\n}"}},"/export-package":{"title":"Export Package","data":{"":"After development complete, you need export package to upload to appstore.phpfox.com.\nMetaFox's clients can buy/install it directly from store.appstore.phpfox.com require a zip file with file contents structure.","environment#Environment":"WARN: if your package contains frontend source code, backend server have to access to frontend source code before run export.\nAPP_ENV=\"local\"\nMFOX_FRONTEND_ROOT=\"/path/to/frontend root\"","filesystem#Filesystem":"|- backend\n| |- packages\n| |- [vendor]\n| |- [app name]\n| |- composer.json\n| |- config\n| |- src\n|- frontend\n|- packages\n|- [vendor]\n|- [app name]\n|- package.json\n|- tsconfig.json\n|- src\nExample","manual-export#Manual Export":"Export package if you can access frontend source from backend server directly.\nCreate a new empty directory export\nCopy frontend package to `export/frontend/packages/[vendor]/[app name]\nCopy frontend package to `export/backend/packages/[vendor]/[app name]\ncd export && zip export.zip -r .","admincp-export#AdminCP Export":"Clean cache then access /admincp/app/package/browse/installed","command-export#Command Export":"Require if export from command lineUpdate backend .env\nMFOX_STORE_API_TOKEN=\"Copy from https://appstore.phpfox.com/account-settings/\"\nExport package command\nphp artisan package:publish foxdev/theme-chocolate\nRelease for development channel to testing\nphp artisan package:publish foxdev/theme-chocolate --release\nRelease for production to testing\nphp artisan package:publish foxdev/theme-chocolate --release --production"}},"/backend/routing":{"title":"Basic Routing","data":{"":"MetaFox platform supports RESTful API routes.Routing defines the way how platform resolve URI request info into Controller action,\nGenerally, you can define routes of your app package in the routes/api.php for frontend api and routes/api-amin.php for admincp apis.This file re-uses Laravel Routing system at package-level, for futher information read out laravel routingroutes/api.php\ngroup(function () {\nRoute::patch('blog/sponsor/{id}', 'sponsor');\nRoute::patch('blog/feature/{id}', 'feature');\nRoute::patch('blog/approve/{id}', 'approve');\nRoute::put('blog/publish/{id}', 'publish');\n});\n\n\nRoute::resource('blog', BlogController::class);\nroutes/api-admin.php\nas('admin') // add this prefix to prevent dulicated route names with blog resource in `api.php`\n->resource('category', CategoryAdminController::class);\n\nRoute::as('admin')\n->resource('blog',BlogAdminController::class);","route-method#Route Method":" __NAMESPACE__, // define all of controller\n'middleware' => 'auth:api',\n'prefix'=> 'blog'\n], function () {\n// all sub route have namespace\n});","route-resource#Route Resource":"Routing for RESTful API requests has a short method Route::resouce($uri, $callback)\nget('/user/{id}', 'show');\n\nRoute::controller(BlogController::class)\n->get('/user/info/{id?}', 'info');\n\n// UserController.php\n\nget('/user/{postId}/comments/{comemntId?}', 'view');","api-versioning#API Versioning":"In real world, when you have a long-time project with multiple versions released, the platform wraps all routes defined in api.php within a prefix /api/{ver}. Parameter {ver} will be passed to Controller action to help us define correct response base on the given version."}},"/backend/octane":{"title":"Performance","data":{"":"MetaFox is based on laravel framework, take a look over request lifecycle. By default laravel bootstrap process\nrequires two many files & services, in default packages there are ~ 1300 files and 130 services need to register and bootstrap.","request-lifecycle#Request Lifecycle":"","fpm-server#FPM Server":"","octane-server#Octane Server":"Compare FPM Vs Octanewrk -c100 -t4 \"htttp://api.metafox.test/me\"","best-practise#Best Practise":"","dependency-injection#Dependency Injection":"https://laravel.com/docs/10.x/octane#configuration-repository-injection","register_shutdown_function#register_shutdown_function":"register_shutdown_function => Lifecycle::onRequestTerminated","memory-leak#Memory leak":"/**\n* Handle an incoming request.\n*/\npublic function index(Request $request): array\n{\nService::$data[] = Str::random(10);\n\nreturn [\n// ...\n];\n}\nphp"}},"/backend/package":{"title":"App Package","data":{"":"MetaFox Backend utilizes Laravel Service Providers to provide a robust, re-useable, and flexible way to extend platform features.","app-directory-structure#App Directory Structure":"The packages directory contains all packages and is organized as below:\npackages/\n[vendor_name]/\npackage-1/\npackage-2/\nvendor_name should be your company name to avoid duplication with others.In the next section, we will look into details of app directory structure.\ncomposer.json : Package infomration and composer dependencies\nconfig/ :\nconfig.php : Contain package configuration\nresources/ :\nlangs/\nen/ : Language file for `en` locale\nphrase.php : Define phrases for group `phrase`\nvalidator.php : Define phrases for group `validator`\nmenu/ :\nmenus.php :\nmenuitems.php :\nroutes/ :\napi.php : Define RESTful API routes\nweb.php : Define web routes\nsrc/\nContracts/ : Contains basic interface defination\nDatabase/ :\nFactories : Contain database model [factories](https://laravel.com/docs/9.x/database-testing#defining-model-factories)\nMigrations : Contain database [migrations](https://laravel.com/docs/9.x/migrations#main-content)\nSeeders : Contain database [seeders](https://laravel.com/docs/9.x/seeding#introduction)\nPackageSeeder.php : Entry point for database seeder\nHttp/\nControllers/ :\nRequests/ :\nResources/ :\nJobs/ :\nListeners/ :\nMail/ :\nNotification/ :\nModels/ :\nObservers/ :\nPolicies/ :\nProviders/ :\nRepositories/ :\nRules/ :\ntests/ :\nFeatures/ :\nUnit/ :","composerjson#composer.json":"{\n\"name\": \"metafox/video\",\n\"version\": \"5.0.7\",\n\"description\": \"\",\n\"authors\": [\n{\n\"name\": \"phpFox\",\n\"email\": \"dev@phpfox.com\",\n\"homepage\": \"https://www.phpfox.com\"\n}\n],\n\"extra\": {\n\"metafox\": {\n\"core\": false,\n\"alias\": \"video\",\n\"asset\": \"video\",\n\"namespace\": \"MetaFox\\\\Video\",\n\"path\": \"packages/metafox/video\",\n\"title\": \"Video\",\n\"internalAdminUrl\": \"/video/setting\",\n\"providers\": [\"MetaFox\\\\Video\\\\Providers\\\\PackageServiceProvider\"],\n\"frontend\": {\n\"@metafox/video\": \"*\"\n},\n\"frontendPaths\": [\"packages/metafox/video\"],\n\"require\": {\n\"metafox/core\": \"5.1.3 - 5.1.4\"\n},\n\"aliases\": {}\n}\n},\n\"autoload\": {\n\"psr-4\": {\n\"MetaFox\\\\Video\\\\\": \"src/\"\n}\n},\n\"require\": {\n\"php-ffmpeg/php-ffmpeg\": \"^1.0\"\n},\n\"autoload-dev\": {\n\"psr-4\": {\n\"MetaFox\\\\Video\\\\\": \"\",\n\"MetaFox\\\\Video\\\\Tests\\\\\": \"tests/\"\n}\n}\n}","base-information#Base Information":"","name#Name":"name: The name of the app package. It consists of vendor name and project name, separated by a slash (/)For examples:\nmetafox/platform\nmetafox/blog\n\nThe app name MUST be lowercase and consist of words separated by -, . or _. The complete name should match the regular expression ^[a-z0-9]([_.-]?[a-z0-9]+)_/[a-z0-9](<([_.]?|-{0,2})[a-z0-9]+>)\\_\\$.","description#Description":"description: A short description of the app package. Should be a one-line message.","version#Version":"version: a string specifing the version of the app package. In most cases, this field is not required and can be omitted (see below).Here are some examples of valid values for versions\n1.0.0\n1.0.2\n1.1.0\n0.2.5\n1.0.0-dev\n1.0.0-alpha3\n1.0.0-beta2","authors#Authors":"authors: Info of the authors of the package. You can specify multiple author objects here. An author object can have following properties:\nname: The author's name. Usually their real name.\nemail: The author's email address.\nhomepage: URL to the author's website.","require#Require":"require: Map of packages required by this package. The app package will not be installed unless all requirements are met.","require-dev#Require-Dev":"require-dev: Map of packages required for developing this package, or running tests, etc. The dev requirements of the root package are installed by default. Both install and update commands support the --no-dev option to prevent dev dependencies from being installed.","autoload#Autoload":"autoload.psr-4: Under the psr-4 key you can define a mapping from namespaces to paths relative to the package root. When autoloading a class like Foo\\\\Bar\\\\Baz, and a namespace prefixed Foo\\\\ are pointed to a directory src/, the autoloader will look for a file named src/Bar/Baz.php and include it if existing. Note that as opposed to the older PSR-0 style, the prefix (Foo\\\\) is not present in the file path.Namespace prefixes must end in \\\\ to avoid conflicts between similar prefixes. For example: Prefix Foo would match classes in the FooBar namespace. Thus, we use the trailing backslashes to solve the problem: Foo\\\\ and FooBar\\\\ are distinct.The PSR-4 references are all combined, during installation or update, into a single key which may be found in the generated file vendor/composer/autoload_psr4.php.autoload.files: If you want to require certain files explicitly on every request then you can use the file autoloading mechanism. This is very useful if your app package needs to include PHP functions that cannot be autoloaded by PHP\n{\n\"autoload\": {\n\"files\": [\"src/MyLibrary/functions.php\"]\n}\n}\nautoload-dev: This section allows to define autoload rules for development purposes.Classes only running for testing purposes should not be included in the main autoload rules to optimize the autoloader in production or for others to use your app package as a dependency.Therefore, it is a good solution to rely on a dedicated path for your unittests and add it within the autoload-dev section.For example:\n{\n\"autoload\": {\n\"psr-4\": { \"MyLibrary\\\\\": \"src/\" }\n},\n\"autoload-dev\": {\n\"psr-4\": { \"MyLibrary\\\\Tests\\\\\": \"tests/\" }\n}\n}","metafox#MetaFox":"MetaFox framework picks extra.metafox section for installation, exporting, upgrading, etc ...Example of extra.metafox\n{\n\"extra\": {\n\"metafox\": {\n\"core\": false,\n\"alias\": \"video\",\n\"asset\": \"video\",\n\"namespace\": \"MetaFox\\\\Video\",\n\"path\": \"packages/metafox/video\",\n\"title\": \"Video\",\n\"internalAdminUrl\": \"/video/setting\",\n\"providers\": [\"MetaFox\\\\Video\\\\Providers\\\\PackageServiceProvider\"],\n\"frontend\": {\n\"@metafox/video\": \"*\"\n},\n\"frontendPaths\": [\"packages/metafox/video\"],\n\"require\": {\n\"metafox/core\": \"5.1.3 - 5.1.4\"\n},\n\"aliases\": {}\n}\n}\n}","metafoxpath#metafox.path":"extra.metafox.path is required. Used to indicate backend package source under root application. For example: packages/metafox/blog","metafoxalias#metafox.alias":"extra.metafox.alias is required. Used to indicate the alias name of your app package. For example, you may prefer to use an alias name blog rather than fully name metafox/blog.","metafoxnamespace#metafox.namespace":"extra.metafox.namespace is required. Used to indicate the root namespace of the package. It must use trailing backslashes with \\\\. For example: MetaFox\\\\Blog","metafoxinternaladminurl#metafox.internalAdminUrl":"extra.metafox.internalAdminUrl is required. Used to indicate where to navigate when administrator click on the app in the AdminCP area.","metafoxfrontend#metafox.frontend":"extra.metafox.frontend is optional. Used to indicate frontend end packages.","metafoxfrontendpaths#metafox.frontendPaths":"extra.metafox.frontendPaths is optional. Used to indicate frontend paths when developer export packages","metafoxpeerdependencies#metafox.peerDependencies":"extra.metafox.peerDependencies is optional. Used to indicate peer dependencies backend packages when developer export packages.The packages listed in peerDependencies comes with single exported file. It's helpful for developer release a product with multiple packages dependencies.\n{\n\"peerDependencies\": [\"metafox/payment-helpers\"]\n}","metafoxproviders#metafox.providers":"extra.metafox.providers is optional. The array of fully Laravel provider classes, for futher information read out Service Provider"}},"/frontend/concepts":{"title":"Concepts","data":{"":"","annotation#Annotation":"Since MetaFox comes with Modular structure, Build tool helps solve issues of dependencies without using import directly. Build tool will scan all packages declared in settings.json file, find all components, librarys to build the bundle.For example:\n/**\n* @type: route\n* name: user.profile\n* path: /user/:user_id(\\d+)/:tab?\n*/","best-practices#Best practices":"Keep anotation at the begin of source file\nEach file should declare only one component.","annotation-1#Annotation":"Type\tGroup\tui\tgeneric UI components\tblock\tLayout block component\titemView\tItem view component\tembedView\tembedView view component\tdialog\tdialog component\tformElement\tForm Elements\tpopover\tpopover component\troute\tpage route\tmodalRoute\tmodal route\tsaga\tsaga function(s)\treducer\treducer functions\tservice\tservice provider","using-testid#Using testid":"For automation testing, please follow below rules:\nFor item view, embed view component, detail view component, all clickable/focusable items MUST have data-tid (link , ....)\nFor menu component, all menu items MUST have data-tid","ui#ui":"Type ui declares generic React component.\n/**\n* @type: ui\n* name: CategoryTitle\n*/\n\nexport default function CategoryTitle(): React.FC<{}> {\n// your code\n}","itemview#itemView":"Naming convention: resource_name.itemView.suffixWhen rendering, declaration of React component is included in a listing/grid component.In some specific cases, platform will base on the naming convention to find components having the best match for rendering.resource_name : blog, photo, video, .... etc.suffix: mainCard, smallCard, flatCard, etc ...\n/**\n* @type: itemView\n* name: photo_album.itemView.mainCard\n*/\nexport default function PhotoAlbumMainCard() {\n// your code\n}","embedview#embedView":"Naming Convention: resource_name.embedView.parent_resourceDeclaration of React component is used as the embed content of other component.For example:\n/**\n* @type: itemView\n* name: photo_album.embedView.feedItem\n*/\nexport default function EmbedPhotoAlbum() {\n// your code\n}","dialog#Dialog":"Naming Convention: dialog.purposeSee dialog\n/**\n* @type: dialog\n*\n* name: dialog.\n*/\n\nexport default function MyDialog() {\n// dialog code\n}","formelement#formElement":"Here are list of supported form elements\nbutton\ncancel\ncaptcha\ncheckbox\nhidden\nlinkButton\npassword\neditor\nsubmit\ntext\nattachment\ndate\ntime\ndatetime\nfriendPicker\nitemPhoto\nitemPhotoGallery\nlocation\nsearchBox\nselect\nTags\nTypeCategory\nvideoUrl\naddAlbumn","popover#popover":"","route#route":"","modalroute#modalRoute":"","saga#saga":"","reducer#reducer":"","service#service":""}},"/frontend/gridview":{"title":"Grid View","data":{"":"Naming for grid layouts per resource.\nFriend - Cards: Main Card\nFriend - Lists: Main Flat List\nFriend - Small Lists: Using in side"}},"/backend/structure":{"title":"Structure","data":{"":"","structure#Structure":"The MetaFox directory structure extends the Laravel framework directory structure. Please read laravel directory structure for more details.\napp/ : Contains the core code of laravel framework\nbootstrap/ : Laravel default, don't change\napp.php :\ncache/ : Contains framework generated files for performance optimization such as the route and services cache files.\nconfig/ : Contains root of your application's configuration files.\ndatabase/ : Contains root database migrations, model factories, and seeds.\npackages/ : All MetaFox packages\nmetafox/ : Contains MetaFox core packages\nplatform/ : Contains MetaFox framework code.\npublic/ : Laravel defaults\n.htaccess\nrobot.txt\nindex.php : Entry point for all requests\nresources/ : Laravel default\nroutes/ : Laravel default\nstorage/ : Laravel default\nlogs/ : Contains your file based logs files\napp/\npublic/ : Contains users uploaded files\ntests/ : Contains root automated tests\nvendor/ : Contains your Composer dependencies.\nNow, we'll look into directories deeper","app-directory#app Directory":"The app directory contains the core code of Laravel framework. You should not change this source code to keep your application is upgradable.","bootstrap-directory#bootstrap Directory":"The bootstrap directory contains the app.php file which bootstraps the framework. This directory also has a cache directory which contains generated files for performance optimization such as the route and services cache files. You don't need to modify any files within this directory.","config-directory#config Directory":"The config directory contains root of your application's configuration files. It's a great idea to read through all of configuration files and familiarize yourself with all of the available options.","database-directory#database Directory":"The database directory contains root database migrations, model factories, and seeds. Also, you can use this directory to hold an SQLite database.","public-directory#public Directory":"The public directory contains the index.php file, which is the entrypoint for all requests entering your application and configures autoloading. This directory also contains your assets such as images, JavaScript, and CSS.","resources-directory#resources Directory":"The resources directory contains your views as well as your raw, un-compiled assets such as CSS or JavaScript.","routes-directory#routes Directory":"The routes directory contains root of the route definitions for your application. By default, several route files are included with Laravel, such as: web.php, api.php, console.php, and channels.php.The web.php file contains routes that the RouteServiceProvider places in the web middleware group, which provides session state, CSRF protection, and cookie encryption. If your application does not support stateless RESTful API then it is likely that all of your routes will most likely be defined in the web.php file.The api.php file contains routes that the RouteServiceProvider places in the API middleware group. These routes are intended to be stateless, so requests entering the application through these routes need to be authenticated via tokens and will not have access to session state.The console.php file is where you may define all of your closure based console commands. Each closure is bounded to a command instance allowing a simple approach to interact with each command's IO methods. Even though this file does not define HTTP routes, it defines console based entrypoints (routes) into your application.The channels.php file is where you may register all of the event broadcasting channels that your application supports.","storage-directory#storage Directory":"The /storage directory contains your logs, file based sessions, file caches, and other files generated by the framework. This directory is segregated into app, framework, and logs directories. The app directory may be used to store any files generated by your application. The framework directory is used to store framework generated files and caches. Finally, the logs directory contains your application's log files.The storage/app/public directory may be used to store user-generated files, such as profile avatars, that should be publicly accessible. You should create a symbolic link at public/storage which points to this directory. You may create the link using the php artisan storage:link Artisan command.","tests-directory#tests Directory":"The /tests directory contains root automated tests. Example PHPUnit unit tests and feature tests are provided out of the box. Each test class should be suffixed with Test. You may run your tests using the phpunit or php vendor/bin/phpunit commands. Or, if you would like a more detailed and beautiful representation of your test results, you may run your tests using the php artisan test Artisan command.","vendor-directory#vendor Directory":"The vendor directory contains your Composer dependencies."}},"/frontend/cookie":{"title":"Cookie","data":{"":"","cookie#Cookie":"cookieBackend is the wrapper of js-cookie","hooks#Hooks":"const ComponentWithCookieBackend = () => {\nconst { cookieBackend } = useGlobal();\n\n// ... your code\n};","saga#Saga":"import { getContext } from 'redux-saga/effects';\n\nexport function* functionWithCookieBackend() {\nconst { cookieBackend } = yield getContext('useGlobal');\n\n// ... your code\n}","apis#Apis":"# Get a cookie value\ncookieBackend.get(\"name\");\n\n// Set a string to name\ncookieBackend.set(\"name\", \"string value\");\n\n// return number or undefined\ncookieBackend.getInt(\"name\");","best-practices#Best practices":"Cookie will send data it contains back to server in every request, so do not store more data than you need in cookie."}},"/frontend/home":{"title":"Get Started","data":{"":"This documentation is mainly for Frontend and ReactJs developer to set up the development environment.\nIf you are looking for testers or backend development, reading this docs is not neccessary.","prerequisites#Prerequisites":"The MetaFox Frontend is developed with ReactJS. Thus, in this documentation, we assume that you are already familiar with Frontend Development with ReactJS. If you are still not confident with Frontend Development with ReactJS, you may want to take a look at the following docs before continuing:\nReactJs\nTypescript\nReact redux\nRedux saga\nRedux Toolkit\nMaterial UI\nReact Testing Library\nJest","system-requirements-for-development-environment#System Requirements for Development Environment":"","software#Software":"Node 16+\nIf you are using Linux/MacOS, You should install node via Node Version Manager.\n\n\nPHP 7.x or later\nDatabase: MySQL, Postgres\nOS: Linux is recommended","hardware#Hardware:":"RAM at least 4GB","helpful-commands#Helpful Commands":"# Check current default node version\nnode --version\n\n# Check nvm is installed\nnvm --version\n\n## install missing nvm\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash\n\n# Install specify node version\nnvm install 16.14.0\n\n# Set 12.18.0 as default node excutable.\nnvm alias default 16.14.0","environment#Environment":"Location: app/.env\n;--------------------------------------------------------------------\n; location: /app/.env\n; Configuration\n;--------------------------------------------------------------------\n\n; run multiple site on the same server\nMFOX_SITE_ID=1\n\n; A properly formatted must have a leading slash but no ending slash.\nPUBLIC_URL=\nMFOX_API_KEY=2\nMFOX_API_SECRET=738ab5b83c902a7b81860e05811fd5cd65e95f72\n\n; A properly formatted must have a leading slash but no ending slash.\nMFOX_BASE_URL = https://dev-MetaFox.MetaFox.us\n\n; A properly formatted must have a leading slash but no ending slash.\nMFOX_API_URL=https://dev-MetaFox.MetaFox.us/v5-backend/api/v1\n\nMFOX_GOOGLE_MAP_API_KEY=AIzaSyBiQutexpRrU8p0wg7-H8CM9kfKeOfk9HE\n\n# loading background color\nMFOX_LOADING_BG=#2d2d2d","start-dev-server#Start Dev Server":"# to workbox dir\ncd workbox\n\n# install yarn\nnpm install -g yarn\n\n# install dependencies\nyarn && yarn bootstrap\n\n# start dev server\nyarn start\n\n# start dev server for admin site\nyarn start:admin\nBy default, the development server will start at http://localhost:3000 after starting successfully","directory-structure#Directory Structure":"Workbox contains all libraries, tools and extensions of MetaFox Frontend. We use lerna to organize many packages into a single repository. Let's look overall to the top level of directory structure.\nworkbox/\napp/\npackages/\napps/\nframework/\nyourcompany/\ntests/\njest/\ntools\npackage.json\npackages/framework/: This directory contains metafox core libraries.packages/apps/: It contains modules, themes, and other extensions.packages/app: It contains configuration.Let's look inside to the app/ directory\napp/\ndist/\npublic/\nfavicon.ico\nindex.html\nlogo192.png\nlogo512.png\nsrc/\nindex.tsx\n.env\nsettings.json\npackage.json\napp/dist/: It contains bundled code for the production deployment.app/public/: It contains static resources to build React app.app/src/: It contains source code of the frontend app.","site-configurations#Site Configurations":"Many site configurations are declared in app/settings.json file\nPackages are listed in packages\n\n\n# file /workbox/app/settings.json\n\n{\n\"siteUrl\": \"http://localhost:3000\",\n\"api\": {\n\"baseUrl\": \"http://localhost:3000/api\",\n},\n\"i18n\": {\n\"locale\": \"en\",\n\"supports\": [\"en\", \"ar\", \"zh-cn\", \"fr\"]\n},\n\"packages\": [\n\"@metafox/framework\",\n\"@metafox/ui\",\n\"@metafox/form\",\n\"@metafox/html-viewer\",\n\"@metafox/core\",\n\"@metafox/user\",\n\"@metafox/blog\",\n\"@metafox/feed\",\n\"@metafox/video\",\n\"@metafox/event\",\n\"@metafox/forum\",\n\"@metafox/quiz\",\n\"@metafox/saved\",\n\"@metafox/ad\",\n\"@metafox/group\",\n\"@metafox/pages\",\n\"@metafox/poll\",\n\"@metafox/photo\",\n\"@metafox/friend\",\n\"@metafox/music\",\n\"@metafox/marketplace\",\n\"@metafox/forum\",\n\"@metafox/ad\"\n],\n}","ide-tools#IDE Tools":"Many IDE tools are supporting React Development and you can choose one IDE that you are familiar with. If you haven't got an idea about IDE for developing React, one of the most popular and powerful IDE tools that we would like to recommend to you is VSCode since it has well-support for React Development with many good extensions.","recommended-extensions#Recommended Extensions":"Prettier - Code formatter\nCode Spell Checker\nESLint"}},"/frontend/routing":{"title":"Router","data":{"":"","table-of-contents#Table of Contents":"In this document, we will go through the following topics:\nDynamic/Async Routing\nRouter and Code Splitting\nRouter and Transition\nRouter and Loading progress\nRouter and Modal\n\nFirstly, let's take a look at MetaFox's URL structure\n/: Display home page or user.home page based on the user authentication state.\n/blog: Display user.browse page.\n/blog/my: Display user.browse.my page.\n/user: Display user.browse page.\n/user/profile/1: Display profile of user\n/pages/profile/14: Display profile of pages\n/groups/profile/12: Display profile of groups\n/jack: Display user.profile page, alias of /user/profile/1\n/jack/photo: Display user.profile.photo page, alias of /user/profile/1/photo\n/barca: Display pages.profile as alias of /pages/profile/14\n/barca/groups: Display pages.profile.groups as alias of /pages/profile/14/photo\n/nancy-club: Display private groups.profile as alias of /group/profile/12\n\nBy using react-router-dom and React.lazy, we can structure React components:\n\n\n\n\n\n\n\n\n\n\n... Others Routes\n\n\n\nAs you see /, blog,blog/my are static regex URLs, they can be solved easily by using regex or literal strings.While /jack, jack/photo, /barca, /barca/groups is more difficult, we have to query database to check if terms of jack, barca are for usernames or page slugs.\nRouter gets a request with the pathname /jack/photo\nCheck router path but no matched\nKeep previous matched Route, showing loading progress bar.\nSend async request to get canonical pathname of url /jack/photo\nIf getting the canonical pathname /user/profile/1/photo, replace current location state, keep location pathname.\nRouter tries to re-run with /user/profile/1/photo pathname.","best-practices#Best Practices:":"Application can be configured to use either Loading progress bar or Empty waiting screen\nShould use nested route in user/pages/event profile page. TBD.\nDo not base on the browser location. Instead, when developing UI, use $PageContext to ask the context to query content instead of browser location.","code-splitting#Code Splitting":"Because of app sizing, we have to split code of every Page components. read moreAt the first time visiting /user page, the associated chunk file is not ready. Thus, if the network speed is around 1 second, end users may see a flash animation.","modal-route#Modal Route":"Here is the sample code for a Modal\n{\nsampleModalDialog: {\nmodal: true,\npath: '/m/example/simple-modal',\ncomponent: loadable(() => import('./pages/ExampleModal')),\n},\nsampleModalPage: {\npath: '/m/example/simple-modal',\ncomponent: loadable(() => import('./pages/ExampleModalPage')),\n},\n}\nYou have to create two routes, the first route is for Modal Route, and the seconds is Page Route. You can declare them in routes.ts file, with the same path but add property modal.","how-to-disable-modal-dialog-on-mobile-device#How to disable modal dialog on mobile device?":"TBD"}},"/frontend/layout":{"title":"Layouts","data":{"":"","layout-organization#Layout Organization":"In order to support customization without modify source base, MetaFox layout system organized in to template, block\nTemplate separates a page into multiple named slot: header, aside, content, subside\nBlock is a React component you can assign to a participant location in template\n\nExample template structure\n-------------------------------------------------------\nheader |\n-------------------------------------------------------\nTop |\n-------------------------------------------------------\n| | |\naside | content | subside |\n| | |\n| | |\n-------------------------------------------------------\nbottom |\n-------------------------------------------------------\nAfter that you can assign block and template using layouts.json file, example:\n{\n\"home.member\": {\n\"info\": {\n\"title\": \"Home Page for logged in users\",\n\"description\": \"Home Page for Logged in users\"\n},\n\"large\": {\n\"templateName\": \"three-column-fixed\",\n\"blocks\": [\n{\n\"component\": \"feed.block.statusComposer\",\n\"slotName\": \"main\",\n\"title\": \"Status Composer\",\n\"key\": \"15t1m\",\n\"blockId\": \"15t1m\",\n\"variant\": \"default\",\n\"blockStyle\": \"Contained\",\n\"blockLayout\": \"Blocker\"\n},\n{\n\"component\": \"feed.block.homeFeeds\",\n\"emptyPage\": \"core.block.no_content_with_description\",\n\"slotName\": \"main\",\n\"title\": \"Activity Feed\",\n\"key\": \"mngh\",\n\"blockId\": \"mngh\",\n\"itemView\": \"feed.itemView.card\",\n\"contentType\": \"feed\",\n\"dataSource\": {\n\"apiUrl\": \"/feed\",\n\"apiParams\": \"view=latest\",\n\"pagingId\": \"/feed\"\n},\n\"canLoadMore\": true,\n\"canLoadSmooth\": true,\n\"blockStyle\": \"Main Listings\",\n\"gridStyle\": \"Feeds\",\n\"blockLayout\": \"Main Listings\",\n\"gridLayout\": \"Feeds\"\n},\n{\n\"component\": \"core.block.sidebarPrimaryMenu\",\n\"slotName\": \"side\",\n\"key\": \"8r659\",\n\"blockId\": \"8r659\",\n\"title\": \"\",\n\"variant\": 8,\n\"displayLimit\": 8,\n\"headerActions\": {\n\"ml\": 0,\n\"mr\": 0\n},\n\"blockLayout\": \"sidebar primary menu\"\n},\n{\n\"component\": \"core.dividerBlock\",\n\"slotName\": \"side\",\n\"title\": \"\",\n\"blockProps\": {\n\"blockStyle\": {}\n},\n\"dividerVariant\": \"middle\",\n\"key\": \"qlfp\",\n\"blockId\": \"qlfp\"\n},\n{\n\"component\": \"core.block.sidebarShortcutMenu\",\n\"slotName\": \"side\",\n\"key\": \"2c7hu\",\n\"blockId\": \"2c7hu\",\n\"title\": \"Shortcuts\",\n\"itemView\": \"shortcut.itemView.smallCard\",\n\"gridLayout\": \"Shortcut - Menu Items\",\n\"blockLayout\": \"sidebar shortcut\",\n\"canLoadMore\": 0,\n\"canLoadSmooth\": true,\n\"authRequired\": true,\n\"dataSource\": {\n\"apiUrl\": \"/user/shortcut\"\n},\n\"pagingId\": \"/user/shortcut\",\n\"headerActions\": [\n{\n\"as\": \"user.ManageShortcutButton\"\n}\n],\n\"emptyPageProps\": {\n\"noBlock\": 1\n},\n\"emptyPage\": \"hide\"\n},\n{\n\"component\": \"announcement.block.announcementListing\",\n\"slotName\": \"subside\",\n\"title\": \"Announcements\",\n\"key\": \"qea3s\",\n\"blockId\": \"qea3s\",\n\"itemProps\": {\n\"mediaPlacement\": \"none\"\n},\n\"blockStyle\": \"Profile - Side Contained\",\n\"blockLayout\": \"Side Contained\"\n},\n{\n\"component\": \"chatplus.block.contactsNewFeed\",\n\"slotName\": \"subside\",\n\"title\": \"Contacts\",\n\"key\": \"2312ewqe\",\n\"blockId\": \"2312ewqe\",\n\"showWhen\": [\"truthy\", \"setting.chatplus.server\"]\n}\n]\n},\n\"small\": {}\n}\n}\nThen you use layout named \"home.member\" in to your page source to define layout\n/**\n* @type: route\n* name: core.home\n* path: /, /home\n*/\nimport { APP_FEED } from \"@metafox/feed\";\nimport { useGlobal, useLoggedIn } from \"@metafox/framework\";\nimport { Page } from \"@metafox/layout\";\nimport * as React from \"react\";\n\nexport default function HomePage(props) {\nconst loggedIn = useLoggedIn();\nconst { createPageParams } = useGlobal();\n\nconst pageParams = createPageParams(props, (prev) => ({\nmodule_name: APP_FEED,\nitem_type: APP_FEED,\n}));\n\nif (loggedIn) {\nreturn ;\n}\n\nreturn ;\n}","pageparams#pageParams":"Page params is a React context sharing wrapped all component in layout, its simple way to passing value without direct pass props.\nThen you get props of any block, component via useParams hooks\nexport default function AdminAppStoreShowDetail() {\nconst { useParams } = useGlobal();\n\nconst { module_name, item_type } = useParams();\n}","custom-block#Custom block":"MetaFox will scan the comment code for layout declaration when building. Below is the sample layout declaration\n/**\n* @type: itemView\n* name: blog.itemView.mainCard\n* keywords: blog\n* description:\n* previewImage:\n* deps:\n* priority: sort orthers.\n*/\n@type\nitemView: Declare grid/list item view component.\nembedView: Declare embedView in feed/notification/search embed view component.\nblock: Declare configurable block view component.\nsaga: Declare redux saga effects.\nreducer: Declare redux reducer function.","filename#Filename":"File types:\nmessages.json: Declare translation objects.\nlayouts.json: Declare layout configuration objects.","naming#Naming":"Export Component Naming\nItem view: [resource_type].itemView.*, etc: blog.itemView.mainCard, blog.itemView.smallFlat\nEmbed item view: [resource_type].embedView.*, etc: blog.embedItem.basic\nForm Field: form.field.[name]: etc: form.field.text, form.field.upload\nBlock Component: [module_name].block.[name], etc: core.block.listview","how-to-detect-layout-variants#How to detect layout variants":"Imagine that user is viewing the user.profile page in medium viewport size, but administrator hasn't defined the layout configuration for such viewport size yet.\nIn this case, the Layout service will check size variants and apply layout configuration in the order of up-to-down sizes: medium, small, xsmall, xxsmall, large, xlarge.Check detail of selection:\nxxsmall: xxsmall, xsmall, small, medium, large, xlarge\nxsmall: xsmall, xxsmall, small, medium, large, xlarge\nsmall: small, xsmall, xxsmall, medium, large, xlarge\nmedium: medium, small, xsmall, xxsmall, large, xlarge\nlarge: large, medium, small, xsmall, xxsmall, xlarge\nxlarge: xlarge, large, medium, small, xsmall, xxsmall","when-to-detect-layout-size-variants#When to detect layout size variants":"For performance reasons, layout size variants will be checked and applied when page is started rendering. It means that layout won't be updated when user resizes viewport.","layout-elements#Layout Elements":"","block#Block":"A Block is a React component that Administrator can add, remove, configure, and drag-and-drop to layout slot","slot#Slot:":"A slot is an area where Administrators can put blocks into. Slot can contain other slots.","actions#Actions:":"Let's take a look at available actions on Slot","resize#Resize":"Resize a slot for responsive viewport size as small, medium, and more.","split#Split":"Split a slot into vertical direction. It's not usually but sometimes required to build complex layouts.","add-slot#Add Slot":"Add other siblings slot at the start/end of the current slot.","section#Section":"Section similar to material-ui Container component, it defines rows in layouts.","actions-1#Actions:":"Let's take a look at available actions on Section","settings#Settings":"maxWidthValues: lg| md| sm| xl| xs| falseDetermine the max-width of the container. The container width grows relatively with the size of the screen. Set to false to disable maxWidth.disableGutters: If true, the left and right paddings are removed.","remove#Remove":"TBD","add-new-slot#Add new slot":"TBD","item-view#Item View":"Item View is a component that implements UI for a single item in a listing block, etc. For example: featured members, activity feeds:\n// file blog/src/components/BlogItemView.tsx\nexport type BlogItemShape = {\ntitle: string;\ndescription: string;\n} & ItemShape;\n\nconst BlogItemView = ({ item, itemProps }: ItemViewProps) => {\nconst to = `/blog/view/${item.id}`;\nconst classes = useStyles();\nconst cover = /string/i.test(typeof item.image)\n? item.image\n: item.image[\"500\"];\n\nconst [control, state] = useActionControl<{}>(item, {\nmenuOpened: false,\n});\n\nreturn (\n
\n
\n\n\n\n
\n
\n\n{item.title}\n\n\n
\n

{item.description}

\n\n
\n
\n
\n);\n};\n\nexport default BlogItemView;\n\n// file blog/src/views.tsx\nimport BlogItemView from \"./components/BlogItemView\";\nexport default {\n\"blog.itemView.card\": BlogItemView,\n};","loading-skeleton#Loading Skeleton":"// file blog/src/components/BlogItemView.tsx\n\nconst LoadingSkeleton = ({ itemProps }) => {\nconst classes = useStyles();\nreturn (\n
\n
\n
\n\n
\n
\n\n\n\n
\n
\n
\n);\n};\n\nBlogItemView.LoadingSkeleton = LoadingSkeleton;\n\nexport default BlogItemView;","listing-block#Listing Block":"You can create new block components by extending other blocks\nimport createBlock from \"@metafox/framework/createBlock\";\nimport { ListViewBlockProps } from \"@metafox/framework/types\";\n\nconst BlogListingBlock = createBlock({\nextendBlock: \"core.block.listview\", // extend from block\nname: \"BlogListingBlock\", // based Block compoinent\noverrides: {\n// override properties automacally merged to targed component\ncontentType: \"blog\", // layout editor will load view prefix by `contentType.itemView.*` to select itemView.\ndataSource: { apiUrl: \"/blog\" },\n},\ndefaults: {\n// default properties show in layout editor,\n// only property show in defaults AND NOT in overrides will be show in editor.\ntitle: \"Blogs\",\nblockProps: { variant: \"contained\" },\nitemView: \"blog.itemView.mainCard\",\ngridContainerProps: { spacing: 2 },\ngridItemProps: { xs: 12, sm: 12, md: 12, lg: 12, xl: 12 },\n},\n});\n\nexport default BlogListingBlock;","side-menu-block#Side Menu Block":"Create a Side Menu block\nimport createBlock from \"@metafox/framework/createBlock\";\nimport { SideMenuBlockProps as Props } from \"@metafox/framework/types\";\n\nconst BlogSideMenuBlock = createBlock({\nextendBlock: \"core.block.sideNavigation\",\nname: \"BlogSideMenuBlock\",\ndisplayName: \"Blog Menu\",\nkeywords: \"blogs, navigation, menu\",\ndescription: \"\",\npreviewImage: \"\",\noverrides: {\nmenuItems: [\n{\nto: \"/blog\",\nlabel: \"All Blogs\",\nactive: true,\n},\n{\nto: \"/blog?view=my\",\nlabel: \"My Blogs\",\n},\n{\nto: \"/blog?view=friend\",\nlabel: \"Friend's Blogs\",\n},\n],\n},\ndefaults: {\ntitle: \"Blogs\",\nblockProps: { variant: \"plained\", noHeader: true, noFooter: false },\n},\n});\n\nexport default BlogSideMenuBlock;","side-category#Side Category":"import createBlock from \"@metafox/framework/createBlock\";\nimport { CategoryBlockProps } from \"@metafox/framework/types\";\n\nconst SideCategoryBlock = createBlock({\nextendBlock: \"core.categoryBlock\", // based Block compoinent\nname: \"BlogCategoryBlock\", // React component name\ndisplayName: \"Blog Categories\", // display in layout editor\nkeywords: \"blogs, category\", // keyword to search on layout editor\ndescription: \"\", // description in layout editor\npreviewImage: \"\", // preview image in layout editor 200x200\noverrides: {\n// overrides properties will apply to derived blog automacally.\ndataSource: { apiUrl: \"/blog-category\", apiParams: \"\" },\nhref: \"/blog/category\",\n},\ndefaults: {\n// properties will show in edit block modal.\ntitle: \"Categories\",\nblockProps: { variant: \"plained\" },\n},\n});\n\nexport default SideCategoryBlock;"}},"/frontend/local-store":{"title":"Local Storage","data":{"":"","get#get":"Get the item with the specific name in localStore\n// get string value\nlocalStore.get('dump name');\n\n// get int value\nlocalStore.getInt('dump name');\n\n// get JSON object\nlocalStore.getJSON('dump name');","set#set":"Set an item in localStore\nlocalStore.set('dump name', 'dump value');","remove#remove":"Remove the item with specific name in localStore.\nlocalStore.remove('dump name');","clear#clear":"Remove all items in localStore\nlocalStore.clear();"}},"/frontend/service":{"title":"Service Manager","data":{"":"Service can be a class instance, a function or a React component to handle some pieces of your app logic.MetaFox platform has a Service Manager called globalContext to manage all services. You can not access the global manager directly but via hooks or saga context.After passed to the global service manager, a service can be accessible in React components or Saga functions.In React component:\nimport {useGlobal} from '@metafox/framework'\n\nconst MyComponent (){\nconst manager = useGlobal();\nconst {apiClient} = manager\n\n// or simplier\n// const {apiClient} = useGlobal();\n}\nIn saga functions:\nimport { getContext } from 'redux-saga/effects';\n\nexport function mySagas() {\nconst { apiClient } = yield * getGlobalContext();\n}\n\nglobalContext is mutable, changeble. Its service members will be affected by others without being changed or updated.","class-services#Class Services":"A standard Service class must have bootstrap method, DO NOT define bootstrap as an arrow function.Let's create the LogService by adding the src/manager/LogService.ts with the following content\n/**\n* @type: service\n* name: logService\n**/\n\nimport {Manager} from '@metafox/manager'\n\ntype LogServiceProps {\n// any property here\n}\n\nclass LogService {\noptions: LogServiceProps;\nconstructor(config: LogServiceProps){\nthis.config = LogServiceProps\n}\n\nbootstrap(manager: Manager){\n// to do something here.\n// return this or void\n}\ninfo(message: string){\nconsole.log(message)\n}\n}\nRestart dev server, then you can use the LogService with useGlobal hook as the below example:\nimport useGlobal from '@metafox/framework';\n\nconst LoginForm (){\nconst { logService } = useGlobal();\nlogService.info('This message is passed by logService')\n}","configuration#Configuration":"In the above example, you have created a service named logService. You can pass configuration from root app by adding a section with the same name in manifest.json as below\n{\n\"logService\": {\n\"level\": \"warn\"\n}\n}\nService Manager will pass this configuration to LogService constructor.If you want to access global configuration in LogService, just use manager.options accesstor.","functional-service#Functional Service":"Sometimes, you would like to declare general functions which can be used anywhere in React components and Sagas. And, you don't want to declare import dependencies everywhere. The good solution is to inject them into Service Manager.In the next example, we will define a function to generate random string\nexport default function randomId(): string {\nreturn Math.random()\n.toString(36)\n.replace(/[^a-z]+/g, '')\n.substr(0, 5);\n}\nNow, pass the function to Service Manager\n# file src/manager.tsx\n\nconst manager = {\nlogService: LogService,\nrandomId,\n// other manager here\n}\n\nexport default manager\n\nimport {useGlobal} from '@metafox/framework';\n\nconst LoginForm (){\nconst { logService, randomId } = useGlobal();\nlogService.info('This message is passed by logService')\nlogService.info('test message id:', randomId());\n}","react-component-service#React Component Service":"In order to inject a React Component across apps without declaration for dependencies, you just need to inject components into the global Service Manager by using inject method\n/**\n* @type: service\n* name: CommentList\n*/\n\nexport default function CommentList() {}\nNext, we will declare typingsCreate ./src/module.d.ts with the following content\nimport '@metafox/framework/Manager';\ndeclare module '@metafox/framework/Manager' {\ninterface Manager {\nCommentList?: React.FC<{}>;\n}\n}\nNow, you can get CommentList from other components. For example:\nimport { useGlobal } from '@metafox/framework';\n\nfunction MyComponent() {\nconst { CommentList } = useGlobal();\n\nreturn ;\n}","core-services#Core Services":"apiClient\nusePopover\ncookieBackend\nnormalization\npreferenceBackend\ndialogBackend\nintl\ncreatePageParams\nlocalStore\ncompactUrl\nslugify\ncopyToClipboard\nuseActionControl\nuseSession\nuseGetItem\nuseLoggedIn\nuseIsMobile\nusePageParams"}},"/frontend/dialog":{"title":"Dialog","data":{"":"MetaFox Dialog is based on Material-UI Dialog,\nWe wrap it in a Controller and dialogBackend services so you can work with Dialog Component more conveniently by using dialogBackend service and useDialog hooks.","create-dialog#Create Dialog":"Here is the sample declaration of a simple dialog. Note that annotations MUST be at the beginning of the source file.\n/**\n* @type: dialog\n* name: ExampleDialog\n*\n*/\n\nimport {\nButton,\nDialog,\nDialogActions,\nDialogContent,\nDialogTitle\n} from '@metafox/dialog';\nimport React from 'react';\n\nexport default function ExampleDialog({\ntitle,\nmessage\n}}) {\nconst { useDialog } = useGlobal();\nconst { setDialogValue, dialogProps } = useDialog();\n\nconst onSubmit = () => setDialogValue(true);\n\nconst onCancel = () => setDialogValue();\n\nreturn (\n\n{title}\n{message}\n\n\n\n\n\n);\n}","export-dialog#Export Dialog":"We are going to use MetaFox annotations syntax for the created dialog.For example: this source defines a dialog named sampleModalDialog\n/**\n* @type: dialog\n* name: sampleModalDialog\n*/","present#Present":"const value = await dialogBackend.present({\ncomponent: \"sampleModalDialog\",\nprops: {\ntitle: \"Simple Dialog Title\",\nmessage:\n\"This is simple dialog demo, you can extends with others function laters.\",\n},\n});\nIn order to present dialog dialogBackend, assign component with value of 'sampleModalDialog'.\ndialogBackend\n.present({\ncomponent: \"sampleModalDialog\",\nprops: {\ntitle: \"Simple Dialog Title\",\nmessage:\n\"This is simple dialog demo, you can extends with others function laters.\",\n},\n})\n.then((value) => {\n// your code\n});","alert#Alert":"Create a Alert Dialog with a single line message\ndialogBackend.alert({\ntitle: \"Alert\",\nmesssage: \"You can not delete this content!\",\n});\nCreate a Alert Dialog with multiple line messages\ndialogBackend.alert({\nmessage:\n\"- Checking the network cables, modem, and router\\n- Reconnecting to Wi-Fi\",\n});","message-component#Message Component":"Content of message should be wrapped in DialogContentText to keep a consistent look and feel.\nimport { DialogContentText } from \"@mui/material\";\n\ndialogBackend.alert({\nmessage: (\n\nChecking the network cables, modem, and router\n\n),\n});","confirm#Confirm":"Create a Confirm Dialog\nconst ok: boolean = await dialogBackend.confirm({\ntitle: \"Confirm\",\nmessage: \"Are you sure?\",\n});\nThe message content is similar to Alert.Add a Custom Button to Confirm Dialog\nconst ok: boolean = await dialogBackend.confirm({\ntitle: \"Confirm\",\nmesssage: \"Are you sure?\",\n});","dismiss#Dismiss":"Dismiss presenting dialog\ndialogBackend.dimiss();\nDimiss all dialog\ndialogBackend.dismiss(true);","dialog-form#Dialog Form":"Use setDialogValue to set value for the current promise call."}},"/frontend/sagas":{"title":"Sagas","data":{"":"Example saga files\n/**\n* @type: saga\n* name: updatedVideo\n*/\n\nimport { LocalAction, viewItem } from \"@metafox/framework\";\nimport { takeEvery } from \"redux-saga/effects\";\n\nfunction* updatedVideo({ payload: { id } }: LocalAction<{ id: string }>) {\nyield* viewItem(\"video\", \"video\", id);\n}\n\nconst sagas = [takeEvery(\"@updatedItem/video\", updatedVideo)];\n\nexport default sagas;\nmetafox build/start command collect all sagas file using annotation @type: saga saga root patternWhenever a new saga file be added or removed, run yarn metafox reload to bundle saga file again.All sagas bundled at file ./app/src/bundle/produce.tsx, example files\n\nimport coreSagaHandleActionFeedbackSaga from\n'@metafox/framework/sagas/handleActionFeedback';\nimport CoreRequestSaga from\n'@metafox/framework/sagas/handleRequest';\nimport sagaReloadEntitySaga from\n'@metafox/core/reducers/reloadEntity';\nimport abortControllerSaga from\n'@metafox/core/sagas/abortController';\nimport coreChooseThemeSaga from\n'@metafox/core/sagas/chooseTheme';\nimport sagaCoreCloseDialogSaga from\n'@metafox/core/sagas/closeDialog';\n\nconst sagas = [\ncoreSagaHandleActionFeedbackSaga,\nCoreRequestSaga,\nsagaReloadEntitySaga,\nabortControllerSaga,\ncoreChooseThemeSaga,\nsagaCoreCloseDialogSaga\n]\n\nexport default function injector(config: any) {\nconfig.sagas=sagas;\n}"}},"/frontend/form":{"title":"Form Builder","data":{"":"Metafox includes the most popular React Form library formik and validation with yup, wraps theme in FormBuilder in order to help build, configure flexible logic from AdminCP without modifying source code.The Form mechanism on MetaFox is based on formik + material-ui. It can build a Form with certain fields & structure with JSON schema. Let's take a look at the below example.","schema#Schema":"import { Form } from \"@metafox/form\";\n\nconst ExampleLoginForm = () => {\nconst loginFormSchema = {\ncontainer: true,\ncomponent: \"form\",\nelements: {\nemail: {\nlabel: \"Email\",\ncomponent: \"text\",\ntype: \"email\",\nvariant: \"outlined\",\n},\npassword: {\nlabel: \"Password\",\ncomponent: \"Email\",\ntype: \"password\",\nvariant: \"outlined\",\n},\nbuttons: {\ncontainer: true,\ncomponent: \"html\",\nelements: {\nsubmit: {\nlabel: \"Login\",\ntype: \"submit\",\n},\n},\n},\n},\n};\nconst initialValues = {\nemail: \"jack@MetaFox.com\",\npassword: \"\",\n};\n\nreturn ;\n};","elements#Elements":"Here is the list of Supported Form elements\ntext\nswitch\ncheckbox\ncheckboxGroup\nradio\nradioGroup\nselect\ndate\ndatetime\ntime\nslider\nmarkSlider\ncategory\ntags\nprivacy\nbutton\nbuttonGroup\nhidden\neditor\nlink\ntypo\ncontainer\ndialogHeader\ndialogFooter\ndialogContent","text#Text":"Definition\nexport type TextFieldProps = {\nname: string;\ncomponent: \"text\";\nlabel: string;\nplaceholder: string;\nreadOnly?: boolean;\ndisabled?: boolean;\nrequired?: boolean;\nmaxLength?: number;\nvariant?: \"outlined\" | \"filled\";\ntype?: \"text\" | \"email\" | \"password\" | \"number\" | \"date\" | \"time\";\n};\nBlow is the example to create a Text element\nconst Field = {\nname: \"title\",\ncomponent: \"text\",\nlabel: \"Title\",\nplaceholder: \"Enter an title for this blog\",\nreadOnly: false, // optional\ndisabled: false, // optional\nrequired: false, // optional\nmaxLength: 250, // optional\nautoComplete: true, //optional,\nvariant: \"outlined\",\n};","switch#Switch":"TBD","checkbox#Checkbox":"TBD","checkboxgroup#CheckboxGroup":"TBD","radio#Radio":"TBD","radiogroup#RadioGroup":"TBD","date#Date":"TBD","datetime#Datetime":"TBD","time#Time":"TBD","slider#Slider":"TBD","markslider#MarkSlider":"TBD","tags#Tags":"TBD","privacyselect#PrivacySelect":"TBD","button#Button":"TBD","buttongroup#ButtonGroup":"TBD","hidden#Hidden":"TBD","editor#Editor":"TBD","link#Link":"TBD","slider-1#Slider":"TBD","container#Container":"TBD","dialogheader#DialogHeader":"TBD","dialogfooter#DialogFooter":"TBD","dialogcontent#DialogContent":"TBD","custom-elements#Custom Elements":"TBD"}},"/frontend/translation":{"title":"Translation","data":{"":"MetaFox translation based on react-intlWithin component"}},"/frontend/typings":{"title":"Typescript","data":{"":"","declaration-merging#Declaration Merging":"Please read the Declaration Merging","typests#types.ts":"// file blog/src/types.ts\n\ninterface BlogItemShape {}\n\nexport interface AppState {\nentities: {\nblog: Record;\n};\n}","moduledts#module.d.ts":"The module.d.ts file in all apps is loaded automatically by TypescriptExtend global service manager\n// file blog/src/module.d.ts\nimport '@metafox/framework/Manager';\nimport { BlogList } from './types';\n\ndeclare module '@metafox/framework/Manager' {\ninterface Manager {\nBlogList?: React.FC<{}>;\n}\n}\nExtend global state\n// file blog/src/module.d.ts\nimport '@metafox/framework/Manager';\nimport { AppState } from './types';\n\ndeclare module '@metafox/framework/Manager' {\ninterface GlobalState {\nblog?: AppState;\n}\n}"}},"/mobile/flatlist":{"title":"Flatlist","data":{"":""}},"/":{"title":"Getting Started","data":{"":"Welcome to the MetaFox Developer Docs. This is a repository for all things of development on MetaFox platform so you may be able to find the answer to your questions right here.Generally, the MetaFox architecture includes 2 main parts: Frontend and Backend. Backend part supports APIs to manipulate data and process business activity. Frontend part will interact with Backend by calling APIs, get returned data and display to end-users. Frontend part includes Web UI and mobile apps. This documentation will walk you through development for both Frontend and Backend","table-of-contents#Table of Contents":"Backend Development\nWeb Frontend Development\nRESTful API\nClass References"}},"/frontend/validation":{"title":"Form & Validation","data":{"":"Form Validation is usually used when getting the response data from server API and before submitting to validate data typings.For example:\n{\n\"type\": \"object\",\n\"properties\": {\n\"question\": {\n\"type\": \"string\",\n\"required\": true,\n\"minLength\": 3,\n\"maxLength\": 255,\n\"label\": \"Question\"\n},\n\"attachments\": {\n\"type\": \"array\",\n\"of\": {\n\"type\": \"object\",\n\"properties\": {\n\"id\": {\n\"type\": \"number\",\n\"required\": true\n},\n\"file_name\": {\n\"type\": \"string\",\n\"required\": true\n}\n}\n},\n\"label\": \"Attachments\"\n}\n}\n}","boolean#Boolean":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.boolean() with all of the additional validation configurations.\nimport { toYup, BooleanTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: BooleanTypeSchema = {\ntype: \"boolean\",\nstrict: true,\nrequired: true,\nerrors: {\nrequired: \"MY custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync(true)); //true\nconsole.log(yupSchema.isValidSync(false)); //true\nconsole.log(yupSchema.isValidSync(\"true\")); //false\nconsole.log(yupSchema.isValidSync(\"false\")); //false\n\n// Equivalent to\n\nconst yupBooleanSchema = yup\n.boolean()\n.required(\"My custom required message\")\n.strict(true);","type#Type":"type BooleanTypeSchema = YupTypeSchema & {\ntype: \"boolean\";\noneOf?: boolean[];\nnotOneOf?: boolean[];\nnullable?: boolean;\nerrors?: YupTypeErrors & {\noneOf?: string;\nnotOneOf?: string;\n};\nwhen?: WhenSchema[];\n};","array#Array":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.array() with all of the additional validation configurations.\nimport { toYup, ArrayTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: ArrayTypeSchema = {\ntype: \"array\",\nstrict: true,\nrequired: true,\nmin: 2,\nerrors: {\nmin: \"My custom min length message\",\nrequired: \"My custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync([\"Good\", \"Morning\"])); //true\nconsole.log(yupSchema.isValidSync(\"Hello\")); //false\n\n// Equivalent to\n\nconst yupArraySchema = yup\n.array()\n.min(2, \"My custom min length message\")\n.required(\"My custom required message\")\n.strict(true);","type-1#Type":"type ArrayTypeSchema = YupTypeSchema & {\ntype: \"array\";\nof?: TypeSchemas;\nmin?: number;\nmax?: number;\nnullable?: boolean;\nunique?: boolean; // to compare string[], int[]\nuniqueBy?: string; // to compare complex object\nerrors?: YupTypeErrors & {\nmin?: string;\nmax?: string;\nunique?: boolean;\nuniqueBy?: string;\n};\nwhen?: WhenSchema[];\n};","date#Date":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.date() with all of the additional validation configurations.\nimport { toYup, DateTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: DateTypeSchema = {\ntype: \"date\",\nstrict: true,\nrequired: true,\nmin: \"2020-01-01\",\nerrors: {\nmin: \"MY custom min date message\",\nrequired: \"MY custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync(\"2020-01-02\")); //true\nconsole.log(yupSchema.isValidSync(\"2019-12-31\")); //false\n\n// Equivalent to\n\nconst yupDateSchema = yup\n.date()\n.min(2, \"My custom min date message\")\n.required(\"My custom required message\")\n.strict(true);","type-2#Type":"type DateTypeSchema = YupTypeSchema & {\ntype: \"date\";\n\n/**\n* number: as a unix timestamp in seconds\n* string: anything parsable by `new Date(string)` e.g. '2020-12-01'\n*/\nmin?: number | string | Reference;\n\n/**\n* number: as a unix timestamp in seconds\n* string: anything parsable by `new Date(string)` e.g. '2020-12-01'\n*/\nmax?: number | string | Reference;\n\nnullable?: boolean;\nerrors?: YupTypeErrors & { min?: string; max?: string };\nwhen?: WhenSchema[];\n};","number#Number":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.number() with all of the additional validation configurations.For more advanced usage, check out the number type test suite.\nimport { toYup, NumberTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: NumberTypeSchema = {\ntype: \"number\",\nstrict: true,\nrequired: true,\nmin: 5,\nerrors: {\nmin: \"My custom min value message\",\nrequired: \"My custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync(5)); //true\nconsole.log(yupSchema.isValidSync(1)); //false\n\n// Equivalent to\n\nconst yupNumberSchema = yup\n.number()\n.min(5, \"My custom min value message\")\n.required(\"My custom required message\")\n.strict(true);","type-3#Type":"type NumberTypeSchema = YupTypeSchema & {\ntype: \"number\";\nmin?: number | Reference;\nmax?: number | Reference;\nlessThan?: number | Reference;\nmoreThan?: number | Reference;\nsign?: \"positive\" | \"negative\";\ninteger?: boolean;\noneOf?: number[];\nnotOneOf?: number[];\nround?: \"floor\" | \"ceil\" | \"trunc\" | \"round\";\nnullable?: boolean;\nerrors?: YupTypeErrors & {\nmin?: string;\nmax?: string;\nlessThan?: string;\nmoreThan?: string;\npositive?: string;\nnegative?: string;\ninteger?: string;\noneOf?: string;\nnotOneOf?: string;\n};\nwhen?: WhenSchema[];\n};","ref#Ref":"ref is very helpful in case you want to compare values of dependent fields. Currently, ref supports min, max, lessThan, moreThanFor example:\nconst json = {\ntype: \"object\",\nproperties: {\nmin_length: {\ntype: \"number\",\nmin: 1,\nmax: 255,\n},\nmax_length: {\ntype: \"number\",\nmin: { ref: \"min_length\" },\n},\n},\n};","object#Object":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.object() with all of the additional validation configurations.\nimport { toYup, ObjectTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: ObjectTypeSchema = {\ntype: \"object\",\nstrict: true,\nproperties: {\nfirstName: {\ntype: \"string\",\nminLength: 2,\nstrict: true,\nrequired: true,\nerrors: {\nminLength: \"first name too short\",\nrequired: \"first name required\",\n},\n},\nlastName: {\ntype: \"string\",\nminLength: 2,\nstrict: true,\nrequired: true,\nerrors: {\nminLength: \"last name too short\",\nrequired: \"last name required\",\n},\n},\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(\nyupSchema.isValidSync({\nfirstName: \"Bob\",\nlastName: \"Jones\",\n})\n); //true\n\nconsole.log(\nyupSchema.isValidSync({\nfirstName: \"Bobby\",\nlastName: \"W\",\n})\n); //false\n\n// Equivalent to\n\nconst yupBooleanSchema = yup\n.object({\nfirstName: yup\n.string()\n.min(2, \"first name too short\")\n.required(\"first name required\")\n.strict(true),\nlastName: yup\n.string()\n.min(2, \"last name too short\")\n.required(\"last name required\")\n.strict(true),\n})\n.strict(true);","type-4#Type":"type ObjectTypeSchema = Omit & {\ntype: \"object\";\nproperties: Record;\n};","keypath-conversion#Keypath Conversion":"Object property keys containing dots (.) will be automatically converted and nested into child object validation types\nBasic Keypath Example.\nAdvanced Keypath Example.\n\nThe following example demonstrates how an object definition will be validated once it is converted to a yup object. It's important to note that this dot notation can be done at any level of an object type validation schema.\nimport { ObjectTypeSchema } from \"@metafox/json2yup\";\n\n// Property names with dot notation keypaths\n\nconst objectSchema: ObjectTypeSchema = {\ntype: \"object\",\nstrict: true,\nproperties: {\n\"user.details.firstName\": {\ntype: \"string\",\nrequired: true,\n},\n},\n};\n\n// Will actually be converted into this object before being 'YUP-ified'\n\nconst actualObjectSchema: ObjectTypeSchema = {\ntype: \"object\",\nstrict: true,\nproperties: {\nuser: {\ntype: \"object\",\nproperties: {\ndetails: {\ntype: \"object\",\nproperties: {\nfirstName: {\ntype: \"string\",\nrequired: true,\n},\n},\n},\n},\n},\n},\n};\nMetaFox supports object uniqueBy when an object is a direct child of Array schema.\nconst schema: ObjectTypeSchema = {\ntype: 'array',\nstrict: true,\nof: {\ntype: 'object',\nuniqueBy: 'name',\nerror: {\nuniqueBy: 'name must be unique in list',\n}\nproperties: {\nname: {\ntype: 'string'\n},\nemail: {\ntype: 'string'\n}\n}\n}\n};","string#String":"Converting a string type JSON schema to a yup object will return an equivalent object of yup.string() with all of the additional validation configuration.\nimport { toYup, StringTypeSchema } from \"@metafox/json2yup\";\nimport * as yup from \"yup\";\n\nconst schema: StringTypeSchema = {\ntype: \"string\",\nstrict: true,\nrequired: true,\nminLength: 5,\nerrors: {\nminLength: \"My custom min length message\",\nrequired: \"My custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconsole.log(yupSchema.isValidSync(\"Hello\")); //true\nconsole.log(yupSchema.isValidSync(\"Hi\")); //false\n\n// Equivalent to\n\nconst yupStringSchema = yup\n.string()\n.min(5, \"My custom min length message\")\n.required(\"My custom required message\")\n.strict(true);\nSupport ref to other fields\nconst schema = {\ntype: \"object\",\nproperties: {\nmin_password_length: {\ntype: \"number\",\nrequired: true,\n},\npassword: {\ntype: \"string\",\nminLength: { ref: \"min_password_length\" },\n},\n},\n};","type-5#Type":"type StringTypeSchema = YupTypeSchema & {\ntype: \"string\";\nminLength?: number;\nmaxLength?: number;\ncase?: \"lowercase\" | \"uppercase\";\nuppercase?: number;\nmatches?: { regex: string; excludeEmptyString?: boolean };\nformat?: \"email\" | \"url\";\noneOf?: string[];\nnotOneOf?: string[];\nnullable?: boolean;\nerrors?: YupTypeErrors & {\nminLength?: string;\nmaxLength?: string;\nlowercase?: string;\nuppercase?: string;\nmatches?: string;\nemail?: string;\nurl?: string;\noneOf?: string;\nnotOneOf?: string;\n};\nwhen?: WhenSchema[];\n};","when#When":"Yup allows you to alter the validation on your data based on other values within the validated data payload by using the when() method.The test suite contains examples of how when validation can be used with all the different data types.\nconst schema: ObjectTypeSchema = {\ntype: \"object\",\nstrict: true,\nproperties: {\nshareName: {\ntype: \"boolean\",\nstrict: true,\nrequired: true,\n},\nname: {\ntype: \"string\",\nstrict: true,\nwhen: [\n{\nfields: \"shareName\",\nis: true,\nthen: {\ntype: \"string\",\nminLength: 1,\nerrors: {\nminLength: \"Must fill name in when shareName is true\",\n},\n},\notherwise: {\ntype: \"string\",\nmaxLength: 0,\nerrors: {\nmaxLength: \"Must not fill name in when shareName is true\",\n},\n},\n},\n],\n},\n},\n};","type-6#Type":"type WhenSchema = {\nfields: string | string[];\nis: unknown;\nthen: T;\notherwise?: T;\n};","custom-errors#Custom Errors":"Every schema type has an optional Error objects that allow you to override the default YUP error messages for specific failure reasons.For example, these are the StringTypeSchema error message options:\nYupTypeErrors\nExample\n\n\nerrors?: YupTypeErrors & {\nminLength?: string;\nmaxLength?: string;\nlowercase?: string;\nuppercase?: string;\nmatches?: string;\nemail?: string;\nurl?: string;\noneOf?: string;\nnotOneOf?: string;\n}","example#Example":"In this example, we will set and retrieve our custom Yup error messages, for the minLength and required rules. This can be applied for all schema types and all schema type rules. We will use console.log() to check the schema type's custom error messages.\nimport { toYup, StringTypeSchema } from \"@metafox/json2yup\";\nimport to from \"await-to-js\";\n\nconst schema: StringTypeSchema = {\ntype: \"string\",\nstrict: true,\nrequired: true,\nminLength: 5,\nerrors: {\nminLength: \"My custom min length message\",\nrequired: \"My custom required message\",\n},\n};\n\nconst yupSchema = toYup(schema);\n\nconst [error] = await to(yupSchema.validate(\"Hi\"));\nconsole.log(error.errors); //[\"My custom min length message\"]\n\nconst [error2] = await to(yupSchema.validate(undefined));\nconsole.log(error2.errors); //[\"My custom required message\"]"}},"/frontend/when-lib":{"title":"When Lib","data":{"":"When condition apply for filter menu, block, to check disabled, shown state of an component.","syntax#Syntax":"//\nconst when = [\"truthy\", \"item.can_edit\"];\n** Nested Condtion **\nconst nestedWhen = [\n\"and\",\n[\"truthy\", \"item.can_edit\"],\n[\"falsy\", \"item.can_delete\"],\n];\n\nconst nestedWhenOr = [\n\"and\",\n[\"truthy\", \"item.can_edit\"],\n[\"falsy\", \"item.can_delete\"],\n];","support-rules#Support Rules":"truthy\nfalsy\nequals\nnotEquals\nstrictEquals\nnotStrictEquals\nlessThan\nlessOrEquals\ngreater\ngreaterOrEquals\nlengthEquals\nlengthGreater\nlengthLess\nlengthGreaterOrEquals\nlengthLessOrEquals\noneOf\nexists\nnotExists","examples#Examples":"// true\nwhen({ a: 1, b: 2, c: 3 }, [\n\"and\",\n[\"equals\", \"a\", 1],\n[\"equals\", \"b\", \"2\"],\n[\"equals\", \"c\", \"3\"],\n]);\n\n// true\nwhen({ a: 1, b: 2, c: [1, 2, 3] }, [\"lengthGreaterOrEquals\", \"c\", 3]);\n\n// false\nwhen({ a: 1, b: 2, c: [1, 2, 3] }, [\"lengthGreaterOrEquals\", \"c\", 2]);\n** Menu Items **// file web.menu\nreturn [\n[\n'showWhen' => [\n'and',\n['truthy', 'item.is_pending'],\n['truthy', 'item.extra.can_approve'],\n],\n'menu' => 'blog.blog.detailActionMenu',\n'name' => 'approve',\n'label' => 'blog::phrase.approve',\n'ordering' => 4,\n'value' => 'approveItem',\n'icon' => 'ico-check-circle-o',\n],\n]"}},"/mobile/layout":{"title":"Layout","data":{"":"Layout Organization support mobile and tablet.A layout contain a structured location to put react component.In mobile layout, we support 5 location header, top, left, right and content.\n---------------------------------------------------------------------\n| header |\n---------------------------------------------------------------------\n| top |\n---------------------------------------------------------------------\n| | | |\n| left | content | right |\n| | | |\n| | | |\n| | | |\n---------------------------------------------------------------------\n\nLayout file contain json structure of React component for header, and content of layout.Custom your screens\n/**\n* @type: route\n* name: mymodule.home\n* path: /blog, /blog/:tab(all|my|pending|draft|friend)\n*/\n\nimport { useGlobal } from \"@metafox/framework\";\ninterface Props {}\n\nexport default function MyModuleHomeScreen(props: Props) {\nconst { createParams } = useGlobal();\n\nconst params = createParams<{ tab: string }>(props, ({ tab }) => ({\nmodule_name: \"my module\",\nresource_name: \"my block\",\ntab,\n}));\n\nreturn ;\n}\nThen create custom layout.json filefile name: layouts.json\n{\n\"mymodule.home\": {\n\"header\": [\n{\n\"component\": \"module_header\",\n\"props\": {\n\"key\": \"module_header\",\n\"title\": \"notifications\",\n\"back\": false,\n\"rightButtons\": [\n{\n\"component\": \"Header.RightButton\",\n\"props\": {\n\"key\": \"listItem\",\n\"icon\": \"list-bullet-o\"\n}\n},\n{\n\"component\": \"Header.RightButton\",\n\"props\": {\n\"key\": \"addItem\",\n\"icon\": \"plus\",\n\"value\": \"addItem\",\n\"params\": {\n\"module_name\": \"blog\",\n\"resource_name\": \"blog\"\n}\n}\n}\n]\n}\n}\n],\n\"content\": {}\n}\n}\nThen developer can put your custom component in location area, using annotation block\n/**\n* @type: block\n* name: MyCustomBlock\n*/\n\nexport default function () {\nreturn (\n\nyour custom header\n\n);\n}\nOr Update Navigation Header\n/**\n* @type: ui\n* name: MyNavigationHeader\n*/\nimport React from \"react\";\nimport { FormStack, RenderBaseItem, useGlobal } from \"@metafox/framework\";\nimport { View } from \"react-native\";\nimport { IconName } from \"@metafox/icons\";\nimport { useNavigation } from \"@react-navigation/native\";\nimport { StackScreenParamsList } from \"@metafox/framework/types\";\nimport { StackNavigationProp } from \"@react-navigation/stack\";\nimport { useParams } from \"@metafox/layout\";\n\ntype Props = {\nrightButtons?: RenderBaseItem[];\n};\n\nexport function ModuleHomeHeader(props: Props) {\nconst { rightButtons } = props;\nconst { jsxBackend } = useGlobal();\nconst { module_name, resource_name, headerTitle } = useParams();\nconst navigation =\nuseNavigation>();\n\nconst [TitleMenu] = jsxBackend.all([\"Header.TitleMenu\"]);\n\nReact.useLayoutEffect(() => {\nnavigation.setOptions({\n// @ts-ignore\nheaderTitle: () => (\n\n),\n});\n// eslint-disable-next-line react-hooks/exhaustive-deps\n}, [handleAddItem, headerTitle, navigation]);\n\nreturn null;\n}\n\nexport default ModuleHomeHeader;\nYou can collect multiple layouts configuration in to a single file.files blog/layouts.json\n{\n\"blog.home\": {\n\"header\": [\n{\n\"component\": \"module_header\",\n\"props\": {\n\"key\": \"module_header\",\n\"title\": \"notifications\",\n\"back\": false,\n\"rightButtons\": [\n{\n\"component\": \"Header.RightButton\",\n\"props\": {\n\"key\": \"listItem\",\n\"icon\": \"list-bullet-o\"\n}\n},\n{\n\"component\": \"Header.RightButton\",\n\"props\": {\n\"key\": \"addItem\",\n\"icon\": \"plus\",\n\"value\": \"addItem\",\n\"params\": {\n\"module_name\": \"blog\",\n\"resource_name\": \"blog\"\n}\n}\n}\n]\n}\n}\n]\n},\n\"blog.my\": {}\n}"}},"/mobile/reducers":{"title":"Reducers","data":{"":"MetaFox reducer organization is the same frontend reducers"}},"/mobile/router":{"title":"Page Screen","data":{"":"Router Structure\nRootStack.Navigator\nRootStack.Screen group = auth, mode = modal\nBottomTabNavigator name = MainTab\nTabStack.Navigator name = HomeStack\nTabStack.Navigator name = FriendStack\nTabStack.Navigator name = ...\nRootStack.Group mode=modal\nIn order to create a new screens using file annotation\n/**\n* @type: route\n* name: blog.home\n* path: /blog, /blog/:tab(all|my|pending|draft|friend)\n*/\nimport { createModuleHomeScreen } from \"@metafox/layout\";\n\nexport default createModuleHomeScreen({\nappName: \"blog\",\nresource: \"blog\",\n});\nSupport hocs to create screens\ncreateModuleHomeScreen\ncreateShowDetailScreen\n\nMetafox build service bundle this file and create a screen named 'blog.home' has path map /blog, screen name and page path can be used later to navigating between screens.\nnavigation.navigate(\"blog.home\", {});\n\n// or\n\nnavigation.openLink(\"/blog\");"}},"/mobile/service":{"title":"Service","data":{"":""}},"/mobile/sagas":{"title":"Sagas","data":{"":"Mobile sagas development use the same skeleton as frontend sagas"}},"/mobile/translations":{"title":"Translations","data":{"":"MetaFox reducer translations is the same frontend translations"}},"/new-language":{"title":"Import Phrases","data":{"":"","add-language-pack#Add Language pack":"In this article, we will guide you step by step to create a new language pack.Firstly, please go to AdminCP > Localize and click on the Add Language button. The, follow wizard and fill the info of your language pack. For example, to create a French language pack, you can use the language code as fr , Vendor name as foxdev and App Name as French Pack.Fill the form as following\n\nOn the server, the packages/foxdev/lang-fr directory will be generated automatically.","edit-phrases#Edit Phrases":"Go to \"Language\" then choose \"Export Phrase\" from \"French\" as following image. Export phrases give you a translation file in csv format.","translate-phrase#Translate Phrase":"Open csv file by editor tool which support csv format Etc: Excel, Google Sheets, ...\nkey: the phrase id\nlocale: the locale of phrase\npackage: the package associate with phrase.\norigin_text: Original phrase in english\ntext: actual translation you have to translate\n\nNote that you have not to change key, locale, package, and original_text. Add your translation to text and keep others.\n\nAfter translation, export the result to csv file then import to the server again.","export-language#Export Language":"After updating and saving translation phrases on .php files, you can export language pack in AdminCP.Following instruction /export-package to export language pack."}},"/new-app":{"title":"New App","data":{"":"","introduction#Introduction":"Assume that you can install the MetaFox site on your local machine or server.In this acticle, we will create a new app note of company company. The Notes app will support following features:\nAllow user to post/share notes with attachments, privacy.\nConfigure settings including title, description, tags, category, etc.\nConfigure permissions: admin can assign permissions based on user roles.\nView notes of user's friends on their activity streams.\nGet notifications when others comment, like notes.","create-new-app#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#Backend":"Go to AdminCP > Installed > Apps or use the direct URL /admincp/apps on your browserPress Create New App button. Then, fill info of company and app name to generate new app skeleton.In app options, press Code Generator, and fill \"note\" in others to generate item type note skeleton.The Skeleton for the Backend of Notes app will be generated as below:Directory structure\npackages/\ncompany/\nexample/\nroutes/\napi.php\nresources/\nlang/: define supported languages\ndrivers.php: define drivers\nen/\nphrase.php: define message translation\nvalidation.php: define message phrases\nmenu/\nitems.php: define menu items\nmenus.php: define menus\nsrc/\nDatabase/\nFactories/\nMigrations/\nSeeders/\nHttp/\nControllers/\nRequests/\nResources/\nListeners/\nModels/\nObsevers/\nPolicies/\nProviders/\nRepositories/\ntests/\nUnit/: Unit test source root\nFeatures/: Feature test source root","frontend#Frontend":"We assume that you have downloaded the MetaFox source package and extract it on your local machine or server. The MetaFox package includes 2 main folders: backend and frontend.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:\nyarn metafox create-app company/note\n\n# Reload project settings\nyarn bootstrap\n\n#Restart dev server again\nyarn start\n\nYou will see that the default skeleton for the Frontend of Notes app will be generated as below:\npackages/\ncompany/\nnote/\npackage.json\ntsconfig.json\ntypes.ts: define typings\nindex.tsx: general export\nmodule.d.ts: integrate typing\ncomponents/: define components\npages/: define pages\nHomePage/\nPage.tsx\nlayouts.json","add-new-schema#Add New Schema":"Go to AdminCP > Installed > Apps or use the direct URL /admincp/apps on your browser\nPress Code Generator -> Migration. Fill schema name with \"notes\"\nA new Migration file is created under src/Database/Migrations folder. Its file name will be prefixed by info of datetime, for example:2022_05_23_101328_migrate_notes_table.phpThe datetime prefix helps migration scripts execute Migration files in chronological order.For more info, you can read Laravel Migration.In the Notes app, data is stored in notes table, including the following columns\nid : Primary key\nmodule_id :\nuser_id :\nuser_type : user_type and user_id is morph columns to this note creator.\nowner_id :\nowner_type : owner_id and owner_type is morph columns to this note owner.\nprivacy : Who can see this note.\ntotal_view :\ntotal_like :\ntotal_comment :\ntotal_reply :\ntotal_share :\ntotal_attachment :\ntitle : Note title\nis_approved :\nis_draft : Is this note post as draft ?\nis_sponsor :\nsponsor_in_feed :\nis_featured : Is this note mark as featured ?\nfeatured_at :\nimage_path :\nserver_id : Disk id to storage image\ntags : Contains notes tags\ncreated_at :\nupdated_at :","add-new-model#Add New Model":"","backend-1#Backend":"Go to AdminCP > Installed > Apps or use the direct URL /admincp/apps on your browser\nPress Code Generator -> Model. Fill your package name and previous schemas with \"notes\".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-repositoryHas Model Factory?Generate files for Model Factory.Has Authorization ?Add permissions for the model based on laravel-permissionHas Text Data?Separate text to the second schema. It's helpful to reduce size of the main schema.Has Category DataCreate a pivot model associated with the main schema to store relationships between the main schema and a category schema.Has Tags DataCreate 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\nCode Generator will generate all necessary source files of the model based on the chosen options. It saves you much time.","add-apis#Add APIs":"Go to AdminCP > Installed > Apps or use the direct URL /admincp/apps on your browser\nPress Code Generator -> APIs, choose package and model name then submit.To create skeleton of Frontend resource.\nOpen Terminal.\nGo to the frontend folder\nRun command:\n\n\n\nyarn metafox create-resource company/hello Note\n\nYou can use the command yarn metafox --help to list commands and options.To update api routesNext, we will edit packages/company/note/routes/api.php file to add routes:\n __NAMESPACE__,\n'middleware' => 'auth:api', // logged in required\n], function () {\n// put your routes\nRoute::resource('note', 'BlogController');\n});\nNow, you can open the URL http://localhost:3000/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#Add WebSetting":"Then 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.After adding a new API, edit packages/company/hello/src/Http/Resources/v1/WebSetting.php file as below\n\n*/\nprotected $resources = [\n'note' => Note\\WebSetting::class, // <- add this line to define WebSetting.\n];\n}\nEdit packages/company/note/src/Http/Resources/v1/Note/WebSetting.php file\naddActions([\n'searchItem' => [\n'apiUrl'=> '/note',\n'pageUrl' => '/note/search',\n'placeholder' => 'Search blogs',\n],\n'homePage' => [\n'pageUrl' => '/note',\n],\n'viewAll' => [\n'apiUrl' => '/note',\n'apiRules' => [],\n],\n'viewItem' => [ // view item detail action.\n'apiUrl' => '/note/:id',\n],\n'deleteItem' => [\n'apiUrl' => '/note/:id',\n'confirm' => [\n'title' => 'Confirm',\n'message' => 'Are you sure you want to delete this item permanently?',\n],\n],\n'editItem' => [\n'pageUrl' => '/note/edit/:id',\n],\n'addItem' => [\n'pageUrl' => '/note/add',\n'apiUrl' => '/note/form',\n],\n]);\n}\n\n/**\n* Define forms json should return in web-settings.\n*/\nprotected function initForms(): void\n{\n$this->addForms([\n'filter' => new SearchBlogForm(),\n]);\n}\n}","home-page#Home Page":"Let's open the note/src/pages/HomePage/Page.tsx file and look into some annotations at the top of file\n/**\n* @type: route\n* name: note.home\n* path: /note\n*/\n\nimport { createLandingPage } from \"@metafox/framework\";\n\nexport default createLandingPage({\nappName: \"note\",\npageName: \"note.home\",\nresourceName: \"note\",\n});\n@type: routeDefine this source code is a route, MetaFox bundle tool collects this info and separate to bundle.name: note.homeDefine a global unique key for route, it can be overwritten by another page when you want to customize logic.path: /noteThis is a pattern string to define a route path, based on path-to-regexp","add-new-menu#Add New Menu":"","app-menu#App Menu":"Visit AdminCP > Appearance > Menus to browse all site menus.MetaFox framework contains built-in menus\ncore.primaryMenu: Left side menu of home page\ncore.adminSidebarMenu: Left side menu of the AdminCP\ncore.headerSubMenu: Header top-right menu\ncore.accountMenu: Header account menu\n\nAlso, each app may contain menus on sidebar, admin. For example, in Notes app can have following menus:\nnote.sidebarMenu: sidebar menu of Notes on home page.\nnote.admin: admin menu of Notes app in AdminCP\n\nWhen 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#Resource Menu":"Each resource has 2 action menus in contexts of listing item and viewing item detail.\nFor example:\nnote.note.itemActionMenu\nnote.note.detailActionMenu\n\nThe name of action menus MUST be followed the convention: [appName].[resourceName].[context]","add-forms#Add Forms":"","backend-2#Backend":"Visit AdminCP > Code Generator > FormsPut forms with \"store, update, search\" to create StoreNoteForm, UpdateNoteForm, SearchNoteForm.Edit packages/company/note/routes/api.php file to add routes for form requests.Dive into StoreNoteForm.php\n __NAMESPACE__,\n'middleware' => 'auth:api', // logged in required\n], function () {\n// routes to form\nRoute::get('note/form', 'NoteController@getStoreForm');\nRoute::get('note/form/:id', 'NoteController@getUpdateForm');\nRoute::get('note/form/search', 'NoteController@getSearchForm');\n\n// routes for note resource\nRoute::get('note', 'NoteController');\n\n});\nEdit Company\\Note\\Http\\Controllers\\Api\\v1\\NoteController file, add following methods\nrepository->find($id);\n\nreturn new UpdateNoteForm($resource);\n}\n\n/**\n* Get updating form\n*\n* @param int $id\n*\n* @return SearchNoteForm\n*/\npublic function getSearchForm(int $id): SearchNoteForm\n{\n$resource = $this->repository->find($id);\n\nreturn new SearchNoteForm($resource);\n}\n\n\n/**\n* Get updating form\n*\n* @param int $id\n*\n* @return DestroyNoteForm\n*/\npublic function getDestroyForm(int $id): DestroyNoteForm\n{\n$resource = $this->repository->find($id);\n\nreturn new DestroyNoteForm($resource);\n}\n}\n\nconfig([\n'title' => __p('core.phrase.edit'),\n'action' => '/note', // target api url\n'method' => 'POST', // use \"POST\" method\n'value' => [\n// default value.\n],\n]);\n}\n\n/**\n* Define form structure.\n*/\nprotected function initialize(): void\n{\n$basic = $this->addBasic();\n\n// add form fields.\n$basic->addFields(\nnew Text([\n'name' => 'title',\n'required' => true,\n'returnKeyType' => 'next',\n'label' => 'Title',\n'validation' => [ // add client validation rules\n'required' => true,\n'nullable' => false,\n'errors'=>[\n'required'=> __p('validation.this_is_required_field'),\n]\n]\n])\n);\n\n\n// add cancel buttons\n$footer = $this->addFooter();\n$footer->addFields(\nnew CancelButton([]),\nnew Submit([\n'label' => ($this->resource && $this->resource->id) ?\n__p('core.phrase.save_changes') :\n__p('core.phrase.create'),\n]),\n);\n}\n}\n\nField\tNote\t\tAttachment\tMultiple file picker to attachment\t\tAutocomplete\tAutocomplete text field\t\tBirthday\tDate picker\t\tButtonField\tBasic Button\t\tCancelButton\tButton for cancel action\t\tCaptchaField\tCaptcha field\t\tCategoryField\tCategory picker\t\tCheckboxField\tMultiple Checkbox field\t\tChoice\tCombobox Field\t\tCountryState\tChoose country and state\t\tCustomGenders\tChoose custom gender\t\tDatetime\tDatetime picker\t\tDescriptionField\tTextarea for description\t\tEmail\tText field with email format\t\tFile\tSingle file picker\t\tFilterCategoryField\tCategoryField for filter form\t\tFriendPicker\tFriend picker\t\tHidden\tHidden input\t\tLanguage\tLanguage picker field\t\tLinkButtonField\tButton with href\t\tLocation\tLocation picker field\t\tPassword\tInput password field\t\tPrivacy\tPrivacy picker field\t\tRadio\tRadio Field\t\tSearchBoxField\tText field support search\t\tSinglePhotoField\tSingle photo picker field\t\tSingleVideoField\tSingle video picker field\t\tSubmit\tSubmit button\t\tSwitchField\tAlternate checkbox\t\tTagsField\tMultiple tags input field\t\tText\tSingle text input field\t\tTextArea\tTextarea input\t\tTimezone\tTimezone picker field\t\tTitleField\tSingle title field\t\tTypeCategoryField\tType-category field for 02 level type category\nYou can check all form fields supported at MetaFox built-in fields support","frontend-1#Frontend":"MetaFox Frontend supports built-in dynamic form builder to transform JSON-based responses into ReactJS Form element, For exampleBelow is the sample Form response in JSON format\n{\n\"status\": \"success\",\n\"data\": {\n\"component\": \"form\", // define ReactJs render component\n\"title\": \"Add New Note\", // form title\n\"action\": \"/note\", // target api for http request when form submit.\n\"method\": \"POST\", // http method for http request when form submit.\n\"value\": { // initial values.\n\"module_id\": \"note\",\n\"privacy\": 0,\n\"draft\": 0,\n\"tags\": [],\n\"owner_id\": 0,\n\"attachments\": []\n},\n\"validation\": { // define validation object, based on https://www.npmjs.com/package/yup\n\"type\": \"object\",\n\"properties\": {\n\"title\": {\n\"label\": \"Title\",\n\"type\": \"string\",\n\"required\": true,\n\"minLength\": 3,\n\"maxLength\": 255,\n\"errors\": {\n\"maxLength\": \"Title must be at most 255 characters\"\n},\n}\n}\n},\n\"elements\": { // define form structure\n\"basic\": { // basic form section\n\"name\": \"basic\",\n\"component\": \"container\",\n\"testid\": \"field basic\",\n\"elements\": {\n\"title\": { // form field\n\"component\": \"text\", // Define react render component to form.element.[component]\n\"returnKeyType\": \"next\",\n\"maxLength\": 255,\n\"fullWidth\": true,\n\"margin\": \"normal\",\n\"size\": \"medium\",\n\"variant\": \"outlined\",\n\"name\": \"title\",\n\"required\": true,\n\"label\": \"Title\",\n\"placeholder\": \"Fill in a title for your note\",\n\"description\": \"Maximum 255 of characters\",\n\"testid\": \"field title\"\n},\n\"text\": {\n\"fullWidth\": true,\n\"variant\": \"outlined\",\n\"returnKeyType\": \"default\",\n\"name\": \"text\",\n\"required\": true,\n\"label\": \"Post\",\n\"placeholder\": \"Add some content to your note\",\n\"component\": \"RichTextEditor\",\n\"testid\": \"field text\"\n},\n},\n}\n},\n}\nLook into packages/framework/metafox-form/src/elements/TextField.tsx file\n/**\n* @type: formElement\n* name: form.element.textarea\n*/\nimport MuiTextField from \"@mui/material/TextField\";\nimport { useField } from \"formik\";\nimport { camelCase } from \"lodash\";\nimport { createElement } from \"react\";\nimport { FormFieldProps } from \"../types\";\n\nconst TextAreaField = ({\nconfig,\ndisabled: forceDisabled,\nname,\nformik,\n}: FormFieldProps) => {\nconst [field, meta] = useField(name ?? \"TextField\");\nconst {\nlabel,\ndisabled,\nlabelProps,\nplaceholder,\nvariant,\nmargin = \"normal\",\nfullWidth,\ntype = \"text\",\nrows = 5,\ndescription,\nautoFocus,\nrequired,\nmaxLength,\n} = config;\n\n// fix: A component is changing an uncontrolled input\nif (!field.value) {\nfield.value = config.defaultValue ?? \"\";\n}\n\nconst haveError = Boolean(meta.error && (meta.touched || formik.submitCount));\n\nreturn createElement(MuiTextField, {\n...field,\nrequired,\nmultiline: true,\ndisabled: disabled || forceDisabled || formik.isSubmitting,\nvariant,\nlabel,\n\"data-testid\": camelCase(`field ${name}`),\nautoFocus,\ninputProps: { \"data-testid\": camelCase(`input ${name}`), maxLength },\nrows,\nInputLabelProps: labelProps,\nplaceholder,\nmargin,\nerror: haveError ? meta.error : false,\nfullWidth,\ntype,\nhelperText: haveError ? meta.error : description,\n});\n};\n\nexport default TextAreaField;\n@type: formElementThis annotation determines the file defines a form field component, build tool collects the info to bundle all files into a chunks.name: form.element.textareaWhen 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#Validation":"Form supports validation both Frontend and Backend","backend-3#Backend":"Dive into packages/company/hello/src/Http/Requests/v1/Note/StoreRequest.php file\n\n*/\npublic function rules()\n{\n// Getting validation rules.\nreturn [\n'title'=> ['string', 'required']\n];\n}\n}\nThe main method rules returns an array of validation rules","frontend-2#Frontend":"Frontend validation is based on yup. MetaFox dynamic form builder transforms JSON object to a yup validation object.","translation#Translation":"","backend-4#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.\nnote/\nresources/\nlang/\nen/ : Language code `vi`\nphrase.php : Phrase groups: `phrase`\nvalidation.php : Phrase groups: `validation`\n...\nfr/ : Others language code.\nDive deeper into ./note/resources/lang/en/phrase.php, it defines phrase group phrase, contains a list of phrase_name and phrase value.\n 'Notes', //: phrase_name = \"note\", phrase_value = \"Notes\"\n'label_menu_s' => 'Notes',\n'note_label_saved' => 'Note',\n'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.',\n'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.',\n'new_note_post' => 'New Note Post',\n'edit_note' => 'Editing Note',\n'add_some_content_to_your_note' => 'Add some content to your note',\n'fill_in_a_title_for_your_note' => 'Fill in a title for your note',\n'control_who_can_see_this_note' => 'Control who can see this note.',\n'note_type' => 'Note Type',\n'added_a_note' => 'added a note',\n'note_notification_type' => 'Note Notification',\n'note_featured_successfully' => 'Note featured successfully.',\n];\nIn order to prevent conflict of phrase name, MetaFox translation feature use namespaced translation key convention {namespace}::{group}.{phrase_name}\nto identity translation string in the appliation. etc:\n{i18n.formatMessage({ id: \"toggle_layout_preview\" })};\n// output:
Toggle Device Preview
\n}\nTo translate message in the saga function\nfunction * saga(){\nconst { i18n } = yield* getGlobalContext();\n\nconsole.log({i18n.formatMessage({id: 'toggle_layout_preview'})});\n// output: Toggle Device Preview\n}\nIn 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)To support multiple language, frontend load custom language translation using api /core/translations/web/{language}. The api reponse all messages in the translation group web.\n{\n\"status\": \"success\",\n\"data\": {\n\"accepted\": \"The :attribute must be accepted.\",\n\"active_url\": \"The :attribute is not a valid URL.\",\n\"after\": \"The :attribute must be a date after :date.\",\n\"after_or_equal\": \"The :attribute must be a date after or equal to :date.\",\n\"alpha\": \"The :attribute may only contain letters.\",\n\"alpha_dash\": \"The :attribute may only contain letters, numbers, dashes and underscores.\",\n\"alpha_num\": \"The :attribute may only contain letters and numbers.\",\n\"array\": \"The :attribute must be an array.\",\n\"before\": \"The :attribute must be a date before :date.\",\n\"before_or_equal\": \"The :attribute must be a date before or equal to :date.\"\n},\n\"message\": null,\n\"error\": null\n}","page-browsing#Page Browsing":"Dive deeper into packages/company/note/src/pages/BrowseNotes/Page.tsx file\n/**\n* @type: route\n* name: note.browse\n* path: /note/:tab(friend|all|pending|feature|spam|draft)\n*/\nimport { createBrowseItemPage } from \"@metafox/framework\";\n\nexport default createBrowseItemPage({\nappName: \"note\",\nresourceName: \"note\",\npageName: \"note.browse\",\ncategoryName: \"note_category\",\n});\n@type: route: Define this file must export default route component.name: note.browse: Define page namepath: /note/:tab(friend|all|pending|feature|spam|draft)path-to-regexp pattern to match route.appName: Define app nameresourceName: Define browsing resource namepageName: Define layout page namecategoryName: Define link to category resource type","page-search#Page Search":"","backend-5#Backend":"You can define search form and then add to the WebSetting","global-search#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\n $this->title,\n'text' => 'content of your text',\n'category' => '',\n// others data.\n];\n}\n}\nSearch system has event listener listening on modification of Note data and update its data in queue worker.","activity-feed#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\nisDraft()) {\nreturn null;\n}\n\nreturn new FeedAction([\n'user_id' => $this->userId(),\n'user_type' => $this->userType(),\n'owner_id' => $this->ownerId(),\n'owner_type' => $this->ownerType(),\n'item_id' => $this->entityId(),\n'item_type' => $this->entityType(),\n'type_id' => $this->entityType(),\n'privacy' => $this->privacy,\n]);\n}\n\n/**\n* Define morph map to privacy streams.\n*/\npublic function privacyStreams(): HasMany\n{\nreturn $this->hasMany(PrivacyStream::class, 'item_id', 'id');\n}\n}","event-listeners#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#Model Observer":"In order to track model modification, checkout Eloquent Observer","export-app#Export App":"Following instruction /export-package to export language pack."}},"/new-theme":{"title":"Theme","data":{"":"MetaFox theme is based on Material UI, a beautiful by design and features a suite of\ncustomization options that make it easy to implement your own custom design system.MetaFox theme defines styles and layouts, in which styles indicate color palette, typography","create-a-new-theme#Create A New Theme":"Using metafox-cli tool to create a new theme and install to front end workbox.Open terminal and navigate to frontend project root.\nnpx metafox-cli\nExample to create new theme chocolate and associate package @foxtheme/chocolate.\n✔ What do you need? · Create new app\n✔ What is vendor/company name? · foxdev\n✔ What is theme id? · chocolate\nGenerating theme files ...\nUpdating workspace ...\nReloading workspace ...\nGenerated theme chocolate located at ./packages/foxdev/theme-chocolate\nRestart frontend terminal to apply changes.\nRestart frontend terminal then refresh the browser to see changes.","customize-styling#Customize Styling":"Styling is defined src/styles.json, includes\npalette - used to modify colors\ntypography - used to modify css font properties\nshape - used to modify border radius system","palette#Palette":"palette.primary\n{\n\"primary\": {\n\"main\": \"#2682d5\",\n\"light\": \"#4a97dc\",\n\"dark\": \"#0a71cd\"\n}\n}\nUsed to represent primary interface elements for a user. Modify palette primary affect to submit buttons, continue buttons, upload file button, links, selected menu items.palette.errorUsed to represent interface elements that the user should be made aware of. Modify palette error affect to error messagepalette.warningUsed to represent potentially dangerous actions or important messages.palette.successUsed to indicate the successful completion of an action that user triggered.palette.border\npalette.border - border color of components\npalette.divider - used to present color of divider component.\n\npalette.background\n{\n\"background\": {\n\"default\": \"#ededed\",\n\"paper\": \"#fff\",\n\"auto\": \"transparent\"\n}\n}\nbackground.default Used to present background of your site & paper. background.default usedpalette.actionpalette.text","typography#Typography":"Typography indicates css font properties.\nTypography supported variant h1, h2, h3, h4, h5, h6, subtitle1, subtitle2, body1, body2, button, caption, overline.\nThe following code styling for typography h1.\n{\n\"h1\": {\n\"fontWeight\": 700,\n\"fontSize\": \"40px\",\n\"lineHeight\": 1.2,\n\"letterSpacing\": \"0\"\n}\n}","shape#Shape":"Shape indicates border radius base for styling system.\n{\n\"shape\": {\n\"borderRadius\": 0\n}\n}","layoutslot#LayoutSlot":"Layout slots define sizes for layout slot.\n{\n\"layoutSlot\": {\n\"background\": {\n\"paper\": \"#f5f5f5\"\n},\n\"points\": {\n\"xs1\": 306,\n\"xs2\": 322,\n\"xs3\": 400,\n\"xs\": 400,\n\"sm1\": 600,\n\"sm\": 720,\n\"md\": 1024,\n\"lg\": 1200,\n\"xl\": 1920\n}\n}\n}","custom-components#Custom Components":"MetaFox theme systems allow frontend developer overrides material components by processors.","componentsts#Components.ts":"In order to customize themed components. Edit src/processors/Component.tsThe following example explain how to customize button styles\nimport { Theme,ThemedProps } from '@mui/material';\n\nexport default function overridesComponents(theme: Theme): void {\ntheme.components.MuiButton = {\ndefaultProps: {\nvariant: 'contained'\n},\nstyleOverrides: {\nroot: {\nborderRadius: 4,\ntextTransform: 'none',\nboxShadow: 'none',\n}\n}\n};\n// other components\n}","cssbaselinets#CssBaseLine.ts":"In order to overrides css baseline, edit processors/CssBaseLine.ts\nimport { Theme } from '@mui/material';\n\nexport default function overridesGlobalStyles(theme: Theme) {\nif (!theme.components) {\ntheme.components = {};\n}\n\ntheme.components.MuiCssBaseline = {\nstyleOverrides: {\nhtml: {\nWebkitFontSmoothing: 'auto',\nfontSize: '16px'\n},\nbody: {\nfontFamily: theme.fontFamily,\noverflowX: 'hidden',\n},\na: {\ncolor: theme.palette.primary.main\n}\n}\n}\n}","custom-site-blocks#Custom Site Blocks":"Site blocks allows developers add blocks to any templates match screen size and slot name in layout. Put block you need affected to all templates\nin layout.siteBlocks.origin.json.\n{\n[screenSize]: [\n{\n\"blockId\": [global unique id],\n\"component\": [componentName],\n\"slotName\": [slotName],\n\"blockOrigin\": \"site\",\n\"blockLayout\": \"none\"\n}\n]\n}\nExample\n{\n\"small\": [\n{\n\"blockId\": \"appbar0\",\n\"component\": \"core.siteBarMobileBlock\",\n\"slotName\": \"header\",\n\"blockOrigin\": \"site\",\n\"blockLayout\": \"none\"\n}\n],\n\"large\": [\n{\n\"blockOrder\": -1,\n\"component\": \"core.block.appbar\",\n\"slotName\": \"header\",\n\"blockId\": \"appbar0\",\n\"blockOrigin\": \"site\",\n\"blockLayout\": \"none\"\n}\n]\n}","theme-backend#Theme Backend":"In order to allow site administrator control theme, metafox theme system require a backend php package associate with the theme.\nOpen your admincp, access /admincp/layout/theme/create","export-theme#Export Theme":"Export metafox theme as order metafox packages, follow export metafox package to export theme."}},"/frontend/theme":{"title":"Theming","data":{"":"MetaFox Theme system is based on Material-UI. A ThemeProvider component is created and you are able to access all standard Material-UI features in MetaFox platform.","theme-themeprovider#Theme ThemeProvider":"You can pass a custom CSS rules to global, by modifying themeOptions:\n// file packages/sites/example/src/themeOptions.tsx\n\nconst themeOptions = {\noverrides: {\nMuiCssBaseline: {\n\"your-css-selector\": {\n// pass css object here\n},\n},\n},\n};\nFor more info, please refer to Material UI","styled#styled":"You can write custom styles by using makeStyles, creatStyles features, look like the below example:\n// file MyComponent.styles.tsx\nimport { makeStyles, createStyles, Theme } from \"@metafox/ui/styles\";\n\nconst useStyles = makeStyles(\n(theme: Theme) =>\ncreateStyles({\nroot: {},\nheader: {\ndisplay: \"flex\",\nalignItems: \"center\",\ncolor: theme.palette.primary.main,\nmarginBottom: theme.spacing(2),\n},\n}),\n{ name: \"MuiSideMenu\" }\n);\n\nexport default useStyles;\nThen, use useStyles function in a component\n// file MyComponent.tsx\nimport useStyles from './MyComponent.styles'\n\nconst MyComponent = ()=>{\nconst classes = useStyles();\n\nreturn (\n
\n... others class access here.\n
);\n}","control-classname-logic#Control className logic":"You can control React className properties by classnames or clsx tools. clsx is a choice of Material-UI. Thus, we should not add others tools for the same features.For more info, you can check trends and clsx","variables#Variables":"TBD","add-mixins#Add mixins":"TBD","dark-mode-vs-light-mode#Dark mode vs Light mode":"TBD","theme-editor#Theme Editor":"TBD","best-practices#Best Practices":"Separate styling to *.styles.tsx\nDo not pass objects to useStyles","typescript#Typescript":"In other to extend declarations, please read Customization of theme and Package AugmentationTheming is the ability to systematically customize site look & feel to reflect better your product's brand such as color, spacing, round corner, shadow, background and typography.MetaFox provides MaterialUI ThemeProvider at RootContainer, you can access to add new variables to Theme Provider.","variables-1#Variables":"// file /src/themes.tsx\nexport default {\nstatus: {\ncolor: \"#dadada\",\nbackground: \"linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)\",\n},\n};","hook#Hook":"You can use useTheme in pure function to get theme variables as the below example:\nimport { useTheme } from \"@metafox/framework/theme\";\n\nexport const MyComponent = () => {\nconst theme = useTheme();\nreturn (\n\nUsing Theme Variable\n\n);\n};","hoc#HOC":"Example:\nimport { withTheme } from \"@metafox/framework/theme\";\n\nfunction DeepChildRaw(props) {\nconst theme = useTheme();\nreturn (\n\nUsing Theme Variable\n\n);\n}\n\nconst DeepChild = withTheme(DeepChildRaw);","how-to-add-new-custom-fonts-#How to add new custom fonts ?":"TBD","how-to-add-more-icons-#How to add more icons ?":"TBD","should-we-use-font-icon-or-svg-icon-#Should we use font icon or svg icon ?":"TBD"}}} \ No newline at end of file diff --git a/_next/static/JE7mgC1QUgHyxpz87oK9Y/_buildManifest.js b/_next/static/vETGZwN9166EGdki_sRgj/_buildManifest.js similarity index 100% rename from _next/static/JE7mgC1QUgHyxpz87oK9Y/_buildManifest.js rename to _next/static/vETGZwN9166EGdki_sRgj/_buildManifest.js diff --git a/_next/static/JE7mgC1QUgHyxpz87oK9Y/_ssgManifest.js b/_next/static/vETGZwN9166EGdki_sRgj/_ssgManifest.js similarity index 100% rename from _next/static/JE7mgC1QUgHyxpz87oK9Y/_ssgManifest.js rename to _next/static/vETGZwN9166EGdki_sRgj/_ssgManifest.js diff --git a/backend/category.html b/backend/category.html index 81001bf17..e2596b864 100644 --- a/backend/category.html +++ b/backend/category.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Category

Category

+
Backend
Category

Category

App items such as blogs, events, etc., can be organized into categories. In this topic, we are going to walk you through steps to support Categories in the sample Notes app.

In the Notes app, we are going to define 2 database schemas note_categories and note_category_data to keep many-to-many relationship between notes and categories

Build Schema

@@ -50,4 +50,4 @@

id bigserial CONSTRAINT blog_category_data_pkey PRIMARY KEY, item_id bigint NOT NULL, category_id integer NOT NULL -);


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +);

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/commands.html b/backend/commands.html index 8e8bddef6..4d3dc5d17 100644 --- a/backend/commands.html +++ b/backend/commands.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Commands

Commands

+
Backend
Commands

Commands

In this topic, we are going to discuss about most frequently commands which can be executed on Linux terminal.

We'll assume that you are familiar with Linux terminal. Firstly, please open the terminal, log in SSH to the Backend server, and go to the source folder of MetaFox Backend. If the MetaFox Backend is set up with a Docker container, you can run the docker exec command to log in to the running container.

Here is a list of Linux commands:

@@ -21,4 +21,4 @@ bash ./scripts/install.sh # Update MetaFox site -php artisan metafox:update

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +php artisan metafox:update

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/content.html b/backend/content.html index 8aca76b75..eb1226707 100644 --- a/backend/content.html +++ b/backend/content.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Content

Content

+
Backend
Content

Content

This is an interface of a Contract content. Example: Blog, Photo v.v...

A Contract content always has user_id, user_type, owner_id, and owner_type.

 
@@ -246,4 +246,4 @@ 

} }

-

If your policy has its own comment method, it will override the global policy method.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

If your policy has its own comment method, it will override the global policy method.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/controller.html b/backend/controller.html index 5c267144c..4cdffcd96 100644 --- a/backend/controller.html +++ b/backend/controller.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Controller

Controller

+
Backend
Controller

Controller

Controllers can help group related request handling logic into a single class. Controller classes are located under src\Http\Controllers directory within your package directory.

Here is the directory structure of the metafox\blog app

packages/
@@ -110,4 +110,4 @@ 

Blog/ // For blog content IndexRequest.php // for browsing blog request EditFormRequest.php // for edit form request

-

You can read more about Laravel Docs about HTTP Request (opens in a new tab) and HTTP Responses (opens in a new tab).


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

You can read more about Laravel Docs about HTTP Request (opens in a new tab) and HTTP Responses (opens in a new tab).


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/datagrid.html b/backend/datagrid.html index fc955a3e9..326f237d3 100644 --- a/backend/datagrid.html +++ b/backend/datagrid.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Datagrid

Introduction

+
Backend
Datagrid

Introduction

MetaFox provides feature data grid builder to configure a JSON structure to render data grid in the frontend.

Mange Phrase Data Grid

Define DataGrid

@@ -163,4 +163,4 @@

}

Configuration

enableCheckboxSelection

-

Enable checkbox for the first item data grid.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

Enable checkbox for the first item data grid.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/eloquent.html b/backend/eloquent.html index c0184eb69..2b4b87c03 100644 --- a/backend/eloquent.html +++ b/backend/eloquent.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Eloquent

Eloquent

+
Backend
Eloquent

Eloquent

Eloquent is an object-relational mapper (ORM) supported by default in Laravel framework to make it enjoyable when interacting with your database. For more info, you may like to read Eloquent ORM (opens in a new tab) first.

Migrations

Migrations, like version control for your database, allow your team to define and share the application's database schema definitions. With database migrations, you no longer have to tell teammates to manually update required changes in their local database schema after pulling certain commits from source control.

@@ -180,4 +180,4 @@

For more info about Observers, please visit Laravel Model Observers (opens in a new tab)

For Events list, please read Appendix - Event list

Repository

-

The main idea to use Repository Pattern in a Laravel is to create a bridge between models and controllers.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

The main idea to use Repository Pattern in a Laravel is to create a bridge between models and controllers.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/event-list.html b/backend/event-list.html index 7809ed141..93e99ca62 100644 --- a/backend/event-list.html +++ b/backend/event-list.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Support Events

Event List

+
Backend
Support Events

Event List

Here is the list of Events supported on MetaFox

+---------------------------------------------------------+------------------------------------------------------------------------+
 | Event                                                   | Listeners                                                              |
@@ -496,4 +496,4 @@ 

Mux Webhook Callback Event

  • Event name: video.mux.webhook_callback
  • -

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/event.html b/backend/event.html index 6696db607..435eb2f1c 100644 --- a/backend/event.html +++ b/backend/event.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Event

Event

+
Backend
Event

Event

Laravel's Events provides a simple Observer pattern implementation, allowing you to subscribe and listen to various events that occur within your application. Event classes are typically stored in the app/Events directory, while their Listeners are stored in app/Listeners directory. Don't worry if you don't see these directories in your application as they will be created automatically as you generate events and listeners using Artisan console commands.

@@ -67,4 +67,4 @@

$response = app('events')->dispatch('search.updated', [$model], $bail=true);

-

In the above sample code, with $bail=true parameter, at the first time a listener returns not-null value, the dispatcher will stop running remaining registered Listener and returns to $response value.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

In the above sample code, with $bail=true parameter, at the first time a listener returns not-null value, the dispatcher will stop running remaining registered Listener and returns to $response value.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/form.html b/backend/form.html index 4458110f6..6c2f15865 100644 --- a/backend/form.html +++ b/backend/form.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Form Builder

Create Form

+
Backend
Form Builder

Create Form

MetaFox provide form configuration feature to define JSON structure that frontend can be rendered via RestFul api.

 
 namespace MetaFox\Core\Http\Resources\v1\Admin;
@@ -194,4 +194,4 @@ 

'email', Yup::string()->required()->email() ); - )


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file + )

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/installation.html b/backend/installation.html index 39ac7e5a6..c1fac6688 100644 --- a/backend/installation.html +++ b/backend/installation.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Installation

MetaFox Developer Installation

+

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/interfaces.html b/backend/interfaces.html index 95943f593..48664e1b2 100644 --- a/backend/interfaces.html +++ b/backend/interfaces.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Interfaces

Interfaces

+
Backend
Interfaces

Interfaces

Contract User

This is an Interface of an User. Contract User is a child of Contract Content, and has more abilities.

 
@@ -56,4 +56,4 @@ 

'name' => $this->full_name, ]; } -


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/load-reduce.html b/backend/load-reduce.html index 00f2fabbe..b629e8a16 100644 --- a/backend/load-reduce.html +++ b/backend/load-reduce.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Load Reduce

Load Reduce

+
Backend
Load Reduce

Load Reduce

Reducing number of database queries using array cache (cache lifetime within request lifecycle).

Register

Register reducer

@@ -154,4 +154,4 @@

DB::statement($sql) } elseif ($driver == 'mysql') { $sql; -}


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +}

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/mailer.html b/backend/mailer.html index 564c969a8..cfce426a3 100644 --- a/backend/mailer.html +++ b/backend/mailer.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Mailer

Add Custom Mail Transport

+
Backend
Mailer

Add Custom Mail Transport

In other to add custom storage, add new driver type="mail-transport" to drivers.php

example

<?php
@@ -29,4 +29,4 @@
         'url'         => '',
         'description' => '',
     ];
-]

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +]

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/octane.html b/backend/octane.html index a2a477b98..dd075956b 100644 --- a/backend/octane.html +++ b/backend/octane.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Octane

Performance

+
Backend
Octane

Performance

MetaFox is based on laravel framework, take a look over request lifecycle. By default laravel bootstrap process requires two many files & services, in default packages there are ~ 1300 files and 130 services need to register and bootstrap.

Request Lifecycle

@@ -42,4 +42,4 @@

]; } php -


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/package.html b/backend/package.html index c0246f2b2..9a90b0e20 100644 --- a/backend/package.html +++ b/backend/package.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Package

App Package

+
Backend
Package

App Package

MetaFox Backend utilizes Laravel Service Providers (opens in a new tab) to provide a robust, re-useable, and flexible way to extend platform features.

App Directory Structure

The packages directory contains all packages and is organized as below:

@@ -204,4 +204,4 @@

"peerDependencies": ["metafox/payment-helpers"] }

metafox.providers

-

extra.metafox.providers is optional. The array of fully Laravel provider classes, for futher information read out Service Provider (opens in a new tab)


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

extra.metafox.providers is optional. The array of fully Laravel provider classes, for futher information read out Service Provider (opens in a new tab)


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/routing.html b/backend/routing.html index 2dd42ec8e..d4117b8ea 100644 --- a/backend/routing.html +++ b/backend/routing.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Routing

Basic Routing

+
Backend
Routing

Basic Routing

MetaFox platform supports RESTful API routes.

Routing defines the way how platform resolve URI request info into Controller action, Generally, you can define routes of your app package in the routes/api.php for frontend api and routes/api-amin.php for admincp apis.

@@ -141,4 +141,4 @@

Route::controller(CommentController::class) ->get('/user/{postId}/comments/{comemntId?}', 'view');

API Versioning

-

In real world, when you have a long-time project with multiple versions released, the platform wraps all routes defined in api.php within a prefix /api/{ver}. Parameter {ver} will be passed to Controller action to help us define correct response base on the given version.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

In real world, when you have a long-time project with multiple versions released, the platform wraps all routes defined in api.php within a prefix /api/{ver}. Parameter {ver} will be passed to Controller action to help us define correct response base on the given version.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/backend/structure.html b/backend/structure.html index c8c0bb611..c05d6debb 100644 --- a/backend/structure.html +++ b/backend/structure.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Backend
Directory Struture

Structure

+
Backend
Directory Struture

Structure

The MetaFox directory structure extends the Laravel framework directory structure. Please read laravel directory structure (opens in a new tab) for more details.

app/            : Contains the core code of laravel framework
 bootstrap/      : Laravel default, don't change
@@ -57,4 +57,4 @@ 

tests Directory

The /tests directory contains root automated tests. Example PHPUnit unit tests and feature tests are provided out of the box. Each test class should be suffixed with Test. You may run your tests using the phpunit or php vendor/bin/phpunit commands. Or, if you would like a more detailed and beautiful representation of your test results, you may run your tests using the php artisan test Artisan command.

vendor Directory

-

The vendor directory contains your Composer dependencies.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

The vendor directory contains your Composer dependencies.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/export-package.html b/export-package.html index 2c6f5ef6b..373c9689e 100644 --- a/export-package.html +++ b/export-package.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Export Package

Export Package

+
Export Package

Export Package

After development complete, you need export package to upload to appstore.phpfox.com (opens in a new tab). MetaFox's clients can buy/install it directly from store.

appstore.phpfox.com require a zip file with file contents structure.

@@ -54,4 +54,4 @@

Release for development channel to testing

php artisan package:publish foxdev/theme-chocolate --release

Release for production to testing

-
php artisan package:publish foxdev/theme-chocolate --release --production


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +
php artisan package:publish foxdev/theme-chocolate --release --production

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/concepts.html b/frontend/concepts.html index 8e5654f2e..7fb10db0e 100644 --- a/frontend/concepts.html +++ b/frontend/concepts.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Concepts

Concepts

+
Frontend
Concepts

Concepts

Annotation

Since MetaFox comes with Modular structure, Build tool helps solve issues of dependencies without using import directly. Build tool will scan all packages declared in settings.json file, find all components, librarys to build the bundle.

For example:

@@ -110,4 +110,4 @@

modalRoute

saga

reducer

-

service


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

service


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/cookie.html b/frontend/cookie.html index 110dc1044..a2dc59dc9 100644 --- a/frontend/cookie.html +++ b/frontend/cookie.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Cookie

Cookie

+
Frontend
Cookie

Cookie

cookieBackend is the wrapper of js-cookie (opens in a new tab)

Hooks

const ComponentWithCookieBackend = () => {
@@ -37,4 +37,4 @@ 

cookieBackend.getInt("name");

Best practices

-

Cookie will send data it contains back to server in every request, so do not store more data than you need in cookie.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

Cookie will send data it contains back to server in every request, so do not store more data than you need in cookie.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/dialog.html b/frontend/dialog.html index 1c47c46d1..c4fc166e3 100644 --- a/frontend/dialog.html +++ b/frontend/dialog.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Dialog

MetaFox Dialog is based on Material-UI Dialog (opens in a new tab), +

Frontend
Dialog

MetaFox Dialog is based on Material-UI Dialog (opens in a new tab), We wrap it in a Controller and dialogBackend services so you can work with Dialog Component more conveniently by using dialogBackend service and useDialog hooks.

Create Dialog

Here is the sample declaration of a simple dialog. Note that annotations MUST be at the beginning of the source file.

@@ -119,4 +119,4 @@

Dimiss all dialog

dialogBackend.dismiss(true);

Dialog Form

-

Use setDialogValue to set value for the current promise call.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

Use setDialogValue to set value for the current promise call.


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/form.html b/frontend/form.html index dd313a8b1..80bdf71bb 100644 --- a/frontend/form.html +++ b/frontend/form.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Form Builder

Form Builder

+
Frontend
Form Builder

Form Builder

Metafox includes the most popular React Form library formik and validation with yup, wraps theme in FormBuilder in order to help build, configure flexible logic from AdminCP without modifying source code.

The Form mechanism on MetaFox is based on formik + material-ui. It can build a Form with certain fields & structure with JSON schema. Let's take a look at the below example.

Schema

@@ -152,4 +152,4 @@

DialogContent

TBD

Custom Elements

-

TBD


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

TBD


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/gridview.html b/frontend/gridview.html index 8def0d250..f7b13b9bb 100644 --- a/frontend/gridview.html +++ b/frontend/gridview.html @@ -9,10 +9,10 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Grid View

Grid View

+
Frontend
Grid View

Grid View

Naming for grid layouts per resource.

  1. Friend - Cards: Main Card
  2. Friend - Lists: Main Flat List
  3. Friend - Small Lists: Using in side
  4. -

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/home.html b/frontend/home.html index c995b987f..e7bf900cb 100644 --- a/frontend/home.html +++ b/frontend/home.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Get Started

Get Started

+

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/layout.html b/frontend/layout.html index 01a2902b1..ea5154502 100644 --- a/frontend/layout.html +++ b/frontend/layout.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Layouts

Layouts

+
Frontend
Layouts

Layouts

Layout Organization

In order to support customization without modify source base, MetaFox layout system organized in to template, block

    @@ -413,4 +413,4 @@

    }, }); -export default SideCategoryBlock;


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +export default SideCategoryBlock;

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/local-store.html b/frontend/local-store.html index f1b8ff3af..39faf7551 100644 --- a/frontend/local-store.html +++ b/frontend/local-store.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Local Store

Local Storage

+
Frontend
Local Store

Local Storage

get

Get the item with the specific name in localStore

// get string value
@@ -28,4 +28,4 @@ 

localStore.remove('dump name');

clear

Remove all items in localStore

-
localStore.clear();

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +
localStore.clear();

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/routing.html b/frontend/routing.html index 957e2174c..73f612aa4 100644 --- a/frontend/routing.html +++ b/frontend/routing.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Routing

Router

+
Frontend
Routing

Router

Table of Contents

In this document, we will go through the following topics:

    @@ -89,4 +89,4 @@

    }

You have to create two routes, the first route is for Modal Route, and the seconds is Page Route. You can declare them in routes.ts file, with the same path but add property modal.

How to disable modal dialog on mobile device?

-

TBD


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +

TBD


Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/sagas.html b/frontend/sagas.html index a41215475..9f5e69b49 100644 --- a/frontend/sagas.html +++ b/frontend/sagas.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Sagas

Example saga files

+
Frontend
Sagas

Example saga files

/**
  * @type: saga
  * name: updatedVideo
@@ -53,4 +53,4 @@
  
 export default function injector(config: any) {
     config.sagas=sagas;
-}

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file +}

Copyright © 2023  phpFox LLC. All rights reserved.
\ No newline at end of file diff --git a/frontend/service.html b/frontend/service.html index 57370cc15..392649d16 100644 --- a/frontend/service.html +++ b/frontend/service.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
Frontend
Service

Service Manager

+
Frontend
Service

Service Manager

Service can be a class instance, a function or a React component to handle some pieces of your app logic.

MetaFox platform has a Service Manager called globalContext to manage all services. You can not access the global manager directly but via hooks or saga context.

After passed to the global service manager, a service can be accessible in React components or Saga functions.

@@ -146,4 +146,4 @@

useLoggedIn
  • useIsMobile
  • usePageParams
  • -


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/frontend/theme.html b/frontend/theme.html index b6e8aa1ef..c33d90e97 100644 --- a/frontend/theme.html +++ b/frontend/theme.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    Frontend
    Theme

    Basic

    +
    Frontend
    Theme

    Basic

    MetaFox Theme system is based on Material-UI (opens in a new tab). A ThemeProvider component is created and you are able to access all standard Material-UI features in MetaFox platform.

    Theme ThemeProvider

    You can pass a custom CSS rules to global, by modifying themeOptions:

    @@ -121,4 +121,4 @@

    How to add more icons ?

    TBD

    Should we use font icon or svg icon ?

    -

    TBD


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    TBD


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/frontend/translation.html b/frontend/translation.html index 71cf1c9a2..d91398c42 100644 --- a/frontend/translation.html +++ b/frontend/translation.html @@ -9,6 +9,6 @@ .dark { --nextra-primary-hue: 204deg; } -
    Frontend
    Translation

    MetaFox translation based on react-intl (opens in a new tab)

    +

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +
     

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/frontend/typings.html b/frontend/typings.html index 6d91a174f..1abcf3fec 100644 --- a/frontend/typings.html +++ b/frontend/typings.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    Frontend
    Typings

    Typescript

    +

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +}

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/frontend/validation.html b/frontend/validation.html index 0bb54afbf..2ba87509b 100644 --- a/frontend/validation.html +++ b/frontend/validation.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    Frontend
    Validation

    Form & Validation

    +
    Frontend
    Validation

    Form & Validation

    Form Validation is usually used when getting the response data from server API and before submitting to validate data typings.

    For example:

    {
    @@ -530,4 +530,4 @@ 

    console.log(error.errors); //["My custom min length message"] const [error2] = await to(yupSchema.validate(undefined)); -console.log(error2.errors); //["My custom required message"]


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +console.log(error2.errors); //["My custom required message"]

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/frontend/when-lib.html b/frontend/when-lib.html index 2d6f007d2..04dfa266e 100644 --- a/frontend/when-lib.html +++ b/frontend/when-lib.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    Frontend
    When Lib

    When condition apply for filter menu, block, to check disabled, shown state of an component.

    +
    Frontend
    When Lib

    When condition apply for filter menu, block, to check disabled, shown state of an component.

    Syntax

    //
     const when = ["truthy", "item.can_edit"];
    @@ -76,4 +76,4 @@

    'value' => 'approveItem', 'icon' => 'ico-check-circle-o', ], -]


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +]

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/index.html b/index.html index 3ad96791e..8bc5abbf5 100644 --- a/index.html +++ b/index.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    Getting Started

    Getting Started

    +
    Getting Started

    Getting Started

    Welcome to the MetaFox Developer Docs. This is a repository for all things of development on MetaFox platform so you may be able to find the answer to your questions right here.

    Generally, the MetaFox architecture includes 2 main parts: Frontend and Backend. Backend part supports APIs to manipulate data and process business activity. Frontend part will interact with Backend by calling APIs, get returned data and display to end-users. Frontend part includes Web UI and mobile apps. This documentation will walk you through development for both Frontend and Backend

    Table of Contents

    @@ -18,4 +18,4 @@

    Web Frontend Development
  • RESTful API
  • Class References
  • -


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/mobile/flatlist.html b/mobile/flatlist.html index 31a75447c..f108a0482 100644 --- a/mobile/flatlist.html +++ b/mobile/flatlist.html @@ -9,4 +9,4 @@ .dark { --nextra-primary-hue: 204deg; } -

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/mobile/layout.html b/mobile/layout.html index d4f766dff..99ea50e28 100644 --- a/mobile/layout.html +++ b/mobile/layout.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    Mobile
    Layouts

    Layout Organization support mobile and tablet.

    +
    Mobile
    Layouts

    Layout Organization support mobile and tablet.

    A layout contain a structured location to put react component.

    In mobile layout, we support 5 location header, top, left, right and content.

    ---------------------------------------------------------------------
    @@ -175,4 +175,4 @@
         ]
       },
       "blog.my": {}
    -}

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +}

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/mobile/reducers.html b/mobile/reducers.html index 263707021..ec4c501ff 100644 --- a/mobile/reducers.html +++ b/mobile/reducers.html @@ -9,4 +9,4 @@ .dark { --nextra-primary-hue: 204deg; } -

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/mobile/router.html b/mobile/router.html index 93db0a2b3..5850407d5 100644 --- a/mobile/router.html +++ b/mobile/router.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    Mobile
    Router

    Router

    +
    Mobile
    Router

    Router

    Router Structure

     RootStack.Navigator
         RootStack.Screen group = auth, mode = modal
    @@ -41,4 +41,4 @@ 

    // or -navigation.openLink("/blog");


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +navigation.openLink("/blog");

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/mobile/sagas.html b/mobile/sagas.html index 7a62c678f..66d21c529 100644 --- a/mobile/sagas.html +++ b/mobile/sagas.html @@ -9,4 +9,4 @@ .dark { --nextra-primary-hue: 204deg; } -

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/mobile/service.html b/mobile/service.html index baa2596a9..553c52fa2 100644 --- a/mobile/service.html +++ b/mobile/service.html @@ -9,4 +9,4 @@ .dark { --nextra-primary-hue: 204deg; } -

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/mobile/translations.html b/mobile/translations.html index f4119bac9..0a89ce1fc 100644 --- a/mobile/translations.html +++ b/mobile/translations.html @@ -9,4 +9,4 @@ .dark { --nextra-primary-hue: 204deg; } -

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/new-app.html b/new-app.html index 51f6a718c..dfaef9666 100644 --- a/new-app.html +++ b/new-app.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    New App

    Introduction

    +
    New App

    Introduction

    Assume that you can install the MetaFox site on your local machine or server.

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


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Following instruction /export-package to export language pack.


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/new-language.html b/new-language.html index c46f06680..e337c987c 100644 --- a/new-language.html +++ b/new-language.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    New Language

    Language

    +
    New Language

    Language

    Add Language pack

    In this article, we will guide you step by step to create a new language pack.

    Firstly, please go to AdminCP > Localize and click on the Add Language button. The, follow wizard and fill the info of your language pack. For example, to create a French language pack, you can use the language code as fr , Vendor name as foxdev and App Name as French Pack.

    @@ -35,4 +35,4 @@

    Import Phrases

    Export Language

    After updating and saving translation phrases on .php files, you can export language pack in AdminCP.

    -

    Following instruction /export-package to export language pack.


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Following instruction /export-package to export language pack.


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file diff --git a/new-theme.html b/new-theme.html index 346e032e8..a5197ea14 100644 --- a/new-theme.html +++ b/new-theme.html @@ -9,7 +9,7 @@ .dark { --nextra-primary-hue: 204deg; } -
    New Theme

    Theme

    +
    New Theme

    Theme

    MetaFox theme is based on Material UI (opens in a new tab), a beautiful by design and features a suite of customization options that make it easy to implement your own custom design system.

    MetaFox theme defines styles and layouts, in which styles indicate color palette, typography

    @@ -193,4 +193,4 @@

    /admincp/layout/theme/create

    Creat Theme

    Export Theme

    -

    Export metafox theme as order metafox packages, follow export metafox package to export theme.


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file +

    Export metafox theme as order metafox packages, follow export metafox package to export theme.


    Copyright © 2023  phpFox LLC. All rights reserved.
    \ No newline at end of file