diff --git a/docs/.gitignore b/docs/.gitignore index ad5c5ce65..dd5e01bd3 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,8 +1,8 @@ -.vuepress/dist -.vuepress/docs .idea node_modules -!.vuepress/public +docs +src/.vuepress/dist +!src/.vuepress/public # generated using npm scripts -README.md \ No newline at end of file +/README.md diff --git a/docs/.sections/block-editor.md b/docs/.sections/block-editor.md deleted file mode 100644 index 7cb4480ac..000000000 --- a/docs/.sections/block-editor.md +++ /dev/null @@ -1,625 +0,0 @@ -## Block editor - -### Overview - -The block editor is a dynamic, drag and drop interface giving users a lot of flexibility in adding and changing content for a given entry. -For instance, if you have a module for creating work case studies (as we do in [our demo](https://demo.twill.io/)), you can use the block editor to create, arrange, and edit blocks of images and text, or anything else you can think of really, as they would appear in a page. -You can create any number of different block types, each with a unique form that can be accessed directly within the block editor. - -Below, we describe the process of creating a block editor and connecting it to your module. - -Here is an overview of the process, each of which is detailed below. - -1. Include the block editor form field in your module's form -2. Create and define blocks -3. Make sure you use blocks traits in your Model and Repository - -### Creating a block editor - -#### Include the block editor in your module's form - -In order to add a block editor to your module, add the `block_editor` field to your module form. e.g.: - -```php -@extends('twill::layouts.form') - -@section('contentFields') - @formField('input', [ - 'name' => 'description', - 'label' => 'Description', - ]) -... - @formField('block_editor') -@stop -``` - -By default, adding the `@formField('block_editor')` directive enables all available *blocks* for use in your module. To scope only certain *blocks* to be available in a given module, you can add a second parameter to the `@formField()` directive with the *blocks* key. e.g.: - -```php -@formField('block_editor', [ - 'blocks' => ['quote', 'image'] -]) -``` - -#### Create and define blocks - -Blocks and Repeaters are built on the same Block model and are created and defined in their respective folders. By default, Twill will look for Blade templates in `views/admin/blocks` for blocks and `views/admin/repeaters` for repeaters. - -Note: Prior to Twill version 2.2, Blocks (and Repeaters) needed to be defined in the configuration file – this is no longer necessary and not recommended. This change is backward compatible, so your existing configuration should work as it used to. Defining blocks in the configuration file will be deprecated in a future release (see the section below [Legacy configuration](#legacy-configuration-2-2)). - -Blocks (and Repeaters) are exactly like a regular form, without any Blade layout or section. The templates take special annotations to add further customization. The title annotation is mandatory and Twill will throw an error if it is not defined. - -Available annotations: - - Provide a title with `@twillPropTitle` or `@twillBlockTitle` or `@twillRepeaterTitle` (mandatory) - - Provide a dynamic title with `@twillPropTitleField` or `@twillBlockTitleField` or `@twillRepeaterTitleField` - - Provide an icon with `@twillPropIcon` or `@twillBlockIcon` or `@twillRepeaterIcon` - - Provide a group with `@twillPropGroup` or `@twillBlockGroup` or `@twillRepeaterGroup` (defaults to `app`) - - Provide a repeater trigger label with `@twillPropTrigger` or `@twillRepeaterTrigger` - - Provide a repeater max items with `@twillPropMax` or `@twillRepeaterMax` - - Define a block or repeater as compiled with `@twillPropCompiled` or `@twillBlockCompiled` or `@twillRepeaterCompiled` - - Define a block or repeater component with `@twillPropComponent` or `@twillBlockComponent` or `@twillRepeaterComponent` - -e.g.: - -filename: ```views/admin/blocks/quote.blade.php``` -```php -@twillBlockTitle('Quote') -@twillBlockIcon('text') - -@formField('input', [ - 'name' => 'quote', - 'type' => 'textarea', - 'label' => 'Quote text', - 'maxlength' => 250, - 'rows' => 4 -]) -``` - -A more complex example would look like this: - -filename: ```views/admin/blocks/media.blade.php``` -```php -@twillBlockTitle('Media') -@twillBlockIcon('image') - -@formField('medias', [ - 'name' => 'image', - 'label' => 'Images', - 'withVideoUrl' => false, - 'max' => 20, -]) - -@formField('files', [ - 'name' => 'video', - 'label' => 'Video', - 'note' => 'Video will overwrite previously selected images', - 'max' => 1 -]) - -@formField('input', [ - 'name' => 'caption', - 'label' => 'Caption', - 'maxlength' => 250, - 'translated' => true, -]) - -@formField('select', [ - 'name' => 'effect', - 'label' => 'Transition Effect', - 'placeholder' => 'Select Transition Effect', - 'default' => 'cut', - 'options' => [ - [ - 'value' => 'cut', - 'label' => 'Cut' - ], - [ - 'value' => 'fade', - 'label' => 'Fade In/Out' - ] - ] -]) - -@formField('color', [ - 'name' => 'bg', - 'label' => 'Background color', - 'note' => 'Default is light grey (#E6E6E6)', -]) - -@formField('input', [ - 'name' => 'timing', - 'label' => 'Timing', - 'maxlength' => 250, - 'note' => 'Timing in ms (default is 4000ms)', -]) -``` - -With that, the *block* is ready to be used on the form! - -##### Dynamic block titles - -In Twill >= 2.5, you can use the `@twillBlockTitleField` directive to include the value of a given field in the title area of the blocks. This directive also accepts a `hidePrefix` option to hide the generic block title: - -```php -@twillBlockTitle('Section') -@twillBlockTitleField('title', ['hidePrefix' => true]) -@twillBlockIcon('text') -@twillBlockGroup('app') - -@formField('input', [ - 'name' => 'title', - 'label' => 'Title', - 'required' => true, -]) - -... -``` - -##### Create a block from an existing block template - -Using `php artisan twill:make:block {name} {baseBlock} {icon}`, you can generate a new block based on a provided block as a base. - -This example would create `views/admin/blocks/exceptional-media.blade.php` from `views/admin/blocks/media.blade.php`: - -``` -$ php artisan twill:make:block ExceptionalMedia media image -``` - -##### List existing blocks and repeaters - -Using `php artisan twill:list:blocks` will list all blocks and repeaters. There are a few options: - - `-s|--shorter` for a shorter table, - - `-b|--blocks` for blocks only, - - `-r|--repeaters` for repeaters only, - - `-a|--app` for app blocks/repeaters only, - - `-c|--custom` for app blocks/repeaters overriding Twill blocks/repeaters only, - - `-t|--twill` for Twill blocks/repeaters only - -##### List existing icons - -`php artisan twill:list:icons` will list all icons available. - -#### Use Block traits in your Model and Repository - -Now, to handle the block data you must integrate it with your module. *Use* the *Blocks* traits in the Model and Repository associated with your module. -If you generated that module from the CLI and did respond yes to the question asking you about using blocks, this should already be the case for you. - -In your model, use `HasBlocks`: - -filename: ```app/Models/Article.php``` -```php - 'accordion_item']) -``` -You can add other fields before or after your repeater, or even multiple repeaters to the same block. - -- Create an *item* block, the one that will be reapeated inside the *container* block - -filename: ```views/admin/repeaters/accordion_item.blade.php``` -```php - @twillRepeaterTitle('Accordion item') - @twillRepeaterMax('10') - - @formField('input', [ - 'name' => 'header', - 'label' => 'Header' - ]) - - @formField('input', [ - 'type' => 'textarea', - 'name' => 'description', - 'label' => 'Description', - 'rows' => 4 - ]) -``` - -### Adding browser fields to a block - -To attach other records inside of a block, it is possible to use the `browser` field. - -- In a block, use the `browser` field: - -filename: ```views/admin/blocks/products.blade.php``` -```php - @twillBlockTitle('Products') - - @formField('browser', [ - 'routePrefix' => 'shop', - 'moduleName' => 'products', - 'name' => 'products', - 'label' => 'Products', - 'max' => 10 - ]) -``` - -- If the module you are browsing is not at the root of your admin, you should use the `browser_route_prefixes` array in the configuration in addition to `routePrefix` in the form field declaration: - -```php - 'block_editor' => [ - ... - 'browser_route_prefixes' => [ - 'products' => 'shop', - ], - ... - ], -``` - -- When rendering the blocks on the frontend you can get the browser items selected in the block, by using the `browserIds` helper to retrieve the selected items' ids, and then you may use Eloquent method like `find` to get the actual records. Example in a blade template: - -filename: ```views/site/blocks/blockWithBrowser.blade.php``` -```php - @php - $selected_items_ids = $block->browserIds('browserFieldName'); - $items = Item::find($selected_items_ids); - @endphp -``` - -- When the browser field allows multiple modules/endpoints, you can also use the `getRelated` function on the block: - -filename: ```views/site/blocks/blockWithBrowser.blade.php``` -```php - @php - $selected_items = $block->getRelated('browserFieldName'); - @endphp -``` - -### Rendering blocks - -When it is time to build a frontend, you will want to render a designed set of blocks, with all blocks in their proper order. When working with a model instance that uses the HasBlocks trait in a view, you can call the `renderBlocks` helper on it. This will render the list of blocks that were created from the CMS. By default, this function will loop over all the blocks and their child blocks. In each case, the function will look for a Blade view to render for a given block. - -Create views for your blocks in the `resources/views/site/blocks` directory. Their filenames should match the block key specified in your Twill configuration and module form. - -For the `products` block example above, a corresponding view would be `resources/views/site/blocks/products.blade.php`. - -You can call the `renderBlocks` helper within a *Blade* file. Such a call would look like this: - -```php -{!! $item->renderBlocks() !!} -``` - -If you want to render child blocks (when using repeaters) inside the parent block, you can do the following: - -```php -{!! $work->renderBlocks(false) !!} -``` - -You can also specify alternate blade views for blocks. This can be helpful if you use the same block in 2 different modules of the CMS, but you want to have design flexibility in how each is rendered. To do that, specify the block view file in your call to the renderBlocks helper like this - -```php -{!! $work->renderBlocks(true, [ - 'block-type' => 'view.path', - 'block-type-2' => 'another.view.path' -]) !!} -``` - -Within these Blade views, you will have access to a `$block` variable with helper functions available to retrieve the block content: - -```php -{{ $block->input('inputNameYouSpecifiedInTheBlockFormField') }} -{{ $block->translatedinput('inputNameYouSpecifiedInATranslatedBlockFormField') }} -``` - -If the block has a media field, you can refer to the Media Library documentation below to learn about the `HasMedias` trait helpers. Here's an example of how a media field could be rendered: - -```php -{{ $block->image('mediaFieldName', 'cropNameFromBlocksConfig') }} -{{ $block->images('mediaFieldName', 'cropNameFromBlocksConfig')}} -``` - -### Previewing blocks - -At the top right of a form where you enabled a block editor, you will find a blue button labeled "Editor". The idea is to provide a better user experience when working with blocks, where the frontend preview is being immediately rendered next to the form, in a full-screen experience. - -You can enable the content editor individual block previews by providing a `resources/views/site/layouts/block.blade.php` blade layout file. This file will be treated as a _layout_, so it will need to yield a `content` section: `@yield('content')`. It will also need to include any frontend CSS/JS necessary to give the block the look and feel of the corresponding frontend layout. Here's a simple example: - -```php - - - - #madewithtwill website - - - -
- @yield('content') -
- - - -``` - -If you would like to specify a custom layout view path, you can do so in `config/twill.php` at `twill.block_editor.block_single_layout`. A good way to share assets and structure from the frontend with these individual block previews is to create a parent layout and extend it from your block layout. - -### Development workflow - -As of verison 2.2, it is not necessary to rebuild Twill's frontend when working with blocks anymore. Their templates are now dynamically rendered in Blade and loaded at runtime by Vue. (For <2.1.x users, it means you do not need to run `php artisan twill:blocks` and `npm run twill-build` after creating or updating a block. Just reload the page to see your changes after saving your Blade file!) - -This is possible because Twill's blocks Vue components are simple single file components that only have a template and a mixin registration. Blocks components are now dynamically registered by Vue using `x-template` scripts that are inlined by Blade. - -#### Custom blocks and repeaters - -To define a block as being `compiled` (ie. using a custom Vue component), you can do this with the annotations `@twillPropCompiled('true')`, `@twillBlockCompiled('true')` or `@twillRepeaterCompiled('true')`. The imported Vue file will be prefered at runtime over the inline, template only, version. - -You can bootstrap your custom Vue blocks by generating them from their Blade counterpart using `php artisan twill:blocks`. It will ask you to confirm before overriding any existing custom Vue block. To start a custom Vue block from scratch, use the following template: - -```vue - - - - -``` - -Note: For legacy 2.1.x users, in the `twill.block_editor.blocks` configuration array, set 'compiled' to `true` on the individual blocks. - -If you are using custom Vue blocks (as in, you edited the `template`, `script` or `style` section of a generated block Vue file), you need to rebuild Twill assets. - -There are two artisan commands to help you and we recommend using them instead of our previous versions' npm scripts: - - - `php artisan twill:build`, which will build Twill's assets with your custom blocks, located in the `twill.block_editor.custom_vue_blocks_resource_path` new configurable path (with defaults to `assets/js/blocks`, like in previous versions). - - - `php artisan twill:dev`, which will start a local server that watches for changes in Twill's frontend directory. You need to set `'dev_mode' => true` in your `config/twill.php` file when using this command. This is especially helpful for Twill's contributors, but can also be useful if you use a lot of custom components in your application. - -Both commands take a `--noInstall` option to avoid running `npm ci` before every build. - -#### Naming convention of custom Vue components - -The naming convention for custom blocks Vue component is deferred from the block's component name. For example, if your block's component name is `a17-block-quote`, the custom blocks should be `assets/js/blocks/BlockQuote.vue`. For component name with underscores, for example `a17-amazing_quote`, it would be `assets/js/blocks/BlockAmazing_quote.vue`. - -#### Disabling inline blocks' templates - -It is also possible to completely disable this feature by setting the `twill.block_editor.inline_blocks_templates` config flag to `false`. - -If you do disable this feature, you could continue using previous versions' npm scripts, but we recommend you stop rebuilding Twill assets entirely unless you are using custom code in your generated Vue blocks. If you do keep using our npm scripts instead of our new Artisan commands, you will need to update `twill-build` from: - -``` - "twill-build": "rm -f public/hot && npm run twill-copy-blocks && cd vendor/area17/twill && npm ci && npm run prod && cp -R public/* ${INIT_CWD}/public", -``` - -to: - -``` - "twill-build": "npm run twill-copy-blocks && cd vendor/area17/twill && npm ci && npm run prod && cp -R dist/* ${INIT_CWD}/public", -``` - -#### A bit further: extending Twill with custom components and custom workflows - -On top of custom Vue blocks, It is possible to rebuild Twill with custom Vue components. This can be used to override Twill's own Vue components or create new form fields, for example. The `twill.custom_components_resource_path` configuration can be used to provide a path under Laravel `resources` folder that will be used as a source of Vue components to include in your form js build when running `php artisan twill:build`. - -You have to run `php artisan twill:build` for your custom Vue components to be included in the frontend build. - -For a more in depth tutorial, check out this [Spectrum post](https://spectrum.chat/twill/tips-and-tricks/adding-a-custom-block-to-twill-admin-view-with-vuejs~028d79b1-b3cd-4fb7-a89c-ce64af7be4af). - -### Default configuration - -```php -return [ - /* - |-------------------------------------------------------------------------- - | Twill Block Editor configuration - |-------------------------------------------------------------------------- - | - | This array allows you to provide the package with your configuration - | for the Block editor field and Editor features. - | - */ - 'block_single_layout' => 'site.layouts.block', // layout to use when rendering a single block in the editor - 'block_views_path' => 'site.blocks', // path where a view file per block type is stored - 'block_views_mappings' => [], // custom mapping of block types and views - 'block_preview_render_childs' => true, // indicates if childs should be rendered when using repeater in blocks - 'block_presenter_path' => null, // allow to set a custom presenter to a block model - // Indicates if blocks templates should be inlined in HTML. - // When setting to false, make sure to build Twill with your all your custom blocks. - 'inline_blocks_templates' => true, - 'custom_vue_blocks_resource_path' => 'assets/js/blocks', - 'use_twill_blocks' => ['text', 'image'], - 'crops' => [ - 'image' => [ - 'desktop' => [ - [ - 'name' => 'desktop', - 'ratio' => 16 / 9, - 'minValues' => [ - 'width' => 100, - 'height' => 100, - ], - ], - ], - 'tablet' => [ - [ - 'name' => 'tablet', - 'ratio' => 4 / 3, - 'minValues' => [ - 'width' => 100, - 'height' => 100, - ], - ], - ], - 'mobile' => [ - [ - 'name' => 'mobile', - 'ratio' => 1, - 'minValues' => [ - 'width' => 100, - 'height' => 100, - ], - ], - ], - ], - ], - 'directories' => [ - 'source' => [ - 'blocks' => [ - [ - 'path' => base_path('vendor/area17/twill/src/Commands/stubs/blocks'), - 'source' => A17\Twill\Services\Blocks\Block::SOURCE_TWILL, - ], - [ - 'path' => resource_path('views/admin/blocks'), - 'source' => A17\Twill\Services\Blocks\Block::SOURCE_APP, - ], - ], - 'repeaters' => [ - [ - 'path' => resource_path('views/admin/repeaters'), - 'source' => A17\Twill\Services\Blocks\Block::SOURCE_APP, - ], - [ - 'path' => base_path('vendor/area17/twill/src/Commands/stubs/repeaters'), - 'source' => A17\Twill\Services\Blocks\Block::SOURCE_TWILL, - ], - ], - 'icons' => [ - base_path('vendor/area17/twill/frontend/icons'), - resource_path('views/admin/icons'), - ], - ], - 'destination' => [ - 'make_dir' => true, - 'blocks' => resource_path('views/admin/blocks'), - 'repeaters' => resource_path('views/admin/repeaters'), - ], - ], -]; -``` - -### Legacy configuration (< 2.2) - -#### Twill prior to version 2.2 - -For Twill version 2.1.x and below, in the `config/twill.php` `block_editor` array, define all *blocks* and *repeaters* available in your project, including the block title, the icon used when displaying it in the block editor form and the associated component name. It would look like this: - -filename: ```config/twill.php``` -```php - 'block_editor' => [ - 'blocks' => [ - ... - 'quote' => [ - 'title' => 'Quote', - 'icon' => 'text', - 'component' => 'a17-block-quote', - ], - 'media' => [ - 'title' => 'Media', - 'icon' => 'image', - 'component' => 'a17-block-media', - ], - 'accordion' => [ - 'title' => 'Accordion', - 'icon' => 'text', - 'component' => 'a17-block-accordion', - ], - ... - ] - 'repeaters' => [ - 'accordion_item' => [ - 'title' => 'Accordion item', - 'icon' => 'text', - 'component' => 'a17-block-accordion_item', - ], - ... - ], - ], -``` - -**Please note the naming convention. If the *block* added is `quote` then the component should be prefixed with `a17-block-`.** - -If you added a block named *awesome_block*, your configuration would look like this: - -```php - 'block_editor' => [ - 'blocks' => [ - ... - 'awesome_block' => [ - 'title' => 'Title for the awesome block', - 'icon' => 'text', - 'component' => 'a17-block-awesome_block', - ], - .. - ] -``` - -##### Common errors -- If you add the *container* block to the _repeaters_ section inside the config, it won't work, e.g.: -```php - 'repeaters' => [ - ... - 'accordion' => [ - 'title' => 'Accordion', - 'trigger' => 'Add accordion', - 'component' => 'a17-block-accordion', - 'max' => 10, - ], - ... - ] -``` - -- If you use a different name for the block inside the _repeaters_ section, it also won't work, e. g.: -```php - 'repeaters' => [ - ... - 'accordion-item' => [ - 'title' => 'Accordion', - 'trigger' => 'Add accordion', - 'component' => 'a17-block-accordion_item', - 'max' => 10, - ], - ... - ] -``` - -- Not adding the *item* block to the _repeaters_ section will also result in failure. diff --git a/docs/.sections/crud-modules.md b/docs/.sections/crud-modules.md deleted file mode 100644 index e7f0a8b87..000000000 --- a/docs/.sections/crud-modules.md +++ /dev/null @@ -1,978 +0,0 @@ -## CRUD modules -Twill core functionality is the ability to setup what we call modules. A module is a set of files that define a content model and its associated business logic in your application. Modules can be configured to enable several features for publishers, from the ability to translate content, to the ability to attach images and create a more complex data structure in your records. - -### CLI Generator -You can generate all the files needed in your application to create a new CRUD module using Twill's Artisan generator: - -```bash -php artisan twill:module moduleName -``` - -The command accepts several options: -- `--hasBlocks (-B)`, to use the block editor on your module form -- `--hasTranslation (-T)`, to add content in multiple languages -- `--hasSlug (-S)`, to generate slugs based on one or multiple fields in your model -- `--hasMedias (-M)`, to attach images to your records -- `--hasFiles (-F)`, to attach files to your records -- `--hasPosition (-P)`, to allow manually reordering of records in the listing screen -- `--hasRevisions(-R)`, to allow comparing and restoring past revisions of records -- `--hasNesting(-N)`, to enable nested items in the module listing (see [Nested Module](#nested-module)) - -The `twill:module` command will generate a migration file, a model, a repository, a controller, a form request object and a form view. - -Add the route to your admin routes file(`routes/admin.php`). - -```php - [ - 'title' => 'Module name', - 'module' => true - ] - ... -] -``` - -With that in place, after migrating the database using `php artisan migrate`, you should be able to start creating content. By default, a module only have a title and a description, the ability to be published, and any other feature you added through the CLI generator. - -If you provided the `hasBlocks` option, you will be able to use the `block_editor` form field in the form of that module. - -If you provided the `hasTranslation` option, and have multiple languages specified in your `translatable.php` configuration file, the UI will react automatically and allow publishers to translate content and manage publication at the language level. - -If you provided the `hasSlug` option, slugs will automatically be generated from the title field. - -If you provided the `hasMedias` or `hasFiles` option, you will be able to respectively add several `medias` or `files` form fields to the form of that module. - -If you provided the `hasPosition` option, publishers will be able to manually order records from the module's listing screen (after enabling the `reorder` option in the module's controller `indexOptions` array). - -If you provided the `hasRevisions` option, each form submission will create a new revision in your database so that publishers can compare and restore them in the CMS UI. - -Depending on the depth of your module in your navigation, you'll need to wrap your route declaration in one or multiple nested route groups. - -You can setup your index options and columns in the generated controller if needed. - -### Migrations -Twill's generated migrations are standard Laravel migrations, enhanced with helpers to create the default fields any CRUD module will use: -```php -increments('id'); - // $table->softDeletes(); - // $table->timestamps(); - // $table->boolean('published'); -}); - -// translation table, holds translated fields -Schema::create('table_name_singular_translations', function (Blueprint $table) { - createDefaultTranslationsTableFields($table, 'tableNameSingular'); - // will add the following inscructions to your migration file - // createDefaultTableFields($table); - // $table->string('locale', 6)->index(); - // $table->boolean('active'); - // $table->integer("{$tableNameSingular}_id")->unsigned(); - // $table->foreign("{$tableNameSingular}_id", "fk_{$tableNameSingular}_translations_{$tableNameSingular}_id")->references('id')->on($table)->onDelete('CASCADE'); - // $table->unique(["{$tableNameSingular}_id", 'locale']); -}); - -// slugs table, holds slugs history -Schema::create('table_name_singular_slugs', function (Blueprint $table) { - createDefaultSlugsTableFields($table, 'tableNameSingular'); - // will add the following inscructions to your migration file - // createDefaultTableFields($table); - // $table->string('slug'); - // $table->string('locale', 6)->index(); - // $table->boolean('active'); - // $table->integer("{$tableNameSingular}_id")->unsigned(); - // $table->foreign("{$tableNameSingular}_id", "fk_{$tableNameSingular}_translations_{$tableNameSingular}_id")->references('id')->on($table)->onDelete('CASCADE')->onUpdate('NO ACTION'); -}); - -// revisions table, holds revision history -Schema::create('table_name_singular_revisions', function (Blueprint $table) { - createDefaultRevisionTableFields($table, 'tableNameSingular'); - // will add the following inscructions to your migration file - // $table->increments('id'); - // $table->timestamps(); - // $table->json('payload'); - // $table->integer("{$tableNameSingular}_id")->unsigned()->index(); - // $table->integer('user_id')->unsigned()->nullable(); - // $table->foreign("{$tableNameSingular}_id")->references('id')->on("{$tableNamePlural}")->onDelete('cascade'); - // $table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); -}); - -// related content table, holds many to many association between 2 tables -Schema::create('table_name_singular1_table_name_singular2', function (Blueprint $table) { - createDefaultRelationshipTableFields($table, $table1NameSingular, $table2NameSingular); - // will add the following inscructions to your migration file - // $table->integer("{$table1NameSingular}_id")->unsigned(); - // $table->foreign("{$table1NameSingular}_id")->references('id')->on($table1NamePlural)->onDelete('cascade'); - // $table->integer("{$table2NameSingular}_id")->unsigned(); - // $table->foreign("{$table2NameSingular}_id")->references('id')->on($table2NamePlural)->onDelete('cascade'); - // $table->index(["{$table2NameSingular}_id", "{$table1NameSingular}_id"]); -}); -``` - -A few CRUD controllers require that your model have a field in the database with a specific name: `published`, `publish_start_date`, `publish_end_date`, `public`, and `position`, so stick with those column names if you are going to use publication status, timeframe and reorderable listings. - -### Models - -Set your fillables to prevent mass-assignement. This is very important, as we use `request()->all()` in the module controller. - -For fields that should default as null in the database when not sent by the form, use the `nullable` array. - -For fields that should default to false in the database when not sent by the form, use the `checkboxes` array. - -Depending upon the Twill features you need on your model, include the related traits and configure their respective options: - -- HasPosition: implement the `A17\Twill\Models\Behaviors\Sortable` interface and add a position field to your fillables. - -- HasTranslation: add translated fields in the `translatedAttributes` array. - -Twill's `HasTranslation` trait is a wrapper around the popular `astronomic/laravel-translatable` package. A default configuration will be automatically published to your `config` directory when you run the `twill:install` command. - -To setup your list of available languages for translated fields, modify the `locales` array in `config/translatable.php`, using ISO 639-1 two-letter languages codes as in the following example: - -```php - [ - 'en', - 'fr', - ], - ... -]; -``` - -- HasSlug: specify the field(s) used to create the slug in the `slugAttributes` array - -- HasMedias: add the `mediasParams` configuration array: - -```php - [ // role name - 'default' => [ // crop name - [ - 'name' => 'default', // ratio name, same as crop name if single - 'ratio' => 16 / 9, // ratio as a fraction or number - ], - ], - 'mobile' => [ - [ - 'name' => 'landscape', // ratio name, multiple allowed - 'ratio' => 16 / 9, - ], - [ - 'name' => 'portrait', // ratio name, multiple allowed - 'ratio' => 3 / 4, - ], - ], - ], - '...' => [ // another role - ... // with crops - ] -]; -``` - -- HasFiles: add the `filesParams` configuration array - -```php -addLikeFilterScope($query, $scopes, 'field_in_scope'); - - // add orWhereHas clauses - $this->searchIn($query, $scopes, 'field_in_scope', ['field1', 'field2', 'field3']); - - // add a whereHas clause - $this->addRelationFilterScope($query, $scopes, 'field_in_scope', 'relationName'); - - // or just go manually with the $query object - if (isset($scopes['field_in_scope'])) { - $query->orWhereHas('relationName', function ($query) use ($scopes) { - $query->where('field', 'like', '%' . $scopes['field_in_scope'] . '%'); - }); - } - - // don't forget to call the parent filter function - return parent::filter($query, $scopes); -} -``` - -- for custom ordering: - -```php -getFormFieldsForBrowser($object, 'relationName'); - - // get fields for a repeater - $fields = $this->getFormFieldsForRepeater($object, $fields, 'relationName', 'ModelName', 'repeaterItemName'); - - // return fields - return $fields -} - -``` - -- for custom field preparation before create action - - -```php -updateMultiSelect($object, $fields, 'relationName'); - - // which will simply run the following for you - $object->relationName()->sync($fields['relationName'] ?? []); - - // or, to save a oneToMany relationship - $this->updateOneToMany($object, $fields, 'relationName', 'formFieldName', 'relationAttribute') - - // or, to save a belongToMany relationship used with the browser field - $this->updateBrowser($object, $fields, 'relationName'); - - // or, to save a hasMany relationship used with the repeater field - $this->updateRepeater($object, $fields, 'relationName', 'ModelName', 'repeaterItemName'); - - // or, to save a belongToMany relationship used with the repeater field - $this->updateRepeaterMany($object, $fields, 'relationName', false); - - parent::afterSave($object, $fields); -} - -``` - -- for hydrating the model for preview of revisions - -```php -hydrateBrowser($object, $fields, 'relationName'); - - // or a multiselect - $this->hydrateMultiSelect($object, $fields, 'relationName'); - - // or a repeater - $this->hydrateRepeater($object, $fields, 'relationName'); - - return parent::hydrate($object, $fields); -} -``` - -### Controllers - -```php - true, - 'edit' => true, - 'publish' => true, - 'bulkPublish' => true, - 'feature' => false, - 'bulkFeature' => false, - 'restore' => true, - 'bulkRestore' => true, - 'forceDelete' => true, - 'bulkForceDelete' => true, - 'delete' => true, - 'duplicate' => false, - 'bulkDelete' => true, - 'reorder' => false, - 'permalink' => true, - 'bulkEdit' => true, - 'editInModal' => false, - 'skipCreateModal' => false, - ]; - - /* - * Key of the index column to use as title/name/anythingelse column - * This will be the first column in the listing and will have a link to the form - */ - protected $titleColumnKey = 'title'; - - /* - * Available columns of the index view - */ - protected $indexColumns = [ - 'image' => [ - 'thumb' => true, // image column - 'variant' => [ - 'role' => 'cover', - 'crop' => 'default', - ], - ], - 'title' => [ // field column - 'title' => 'Title', - 'field' => 'title', - ], - 'subtitle' => [ - 'title' => 'Subtitle', - 'field' => 'subtitle', - 'sort' => true, // column is sortable - 'visible' => false, // will be available from the columns settings dropdown - ], - 'relationName' => [ // relation column - // Take a look at the example in the next section fot the implementation of the sort - 'title' => 'Relation name', - 'sort' => true, - 'relationship' => 'relationName', - 'field' => 'relationFieldToDisplay' - ], - 'presenterMethodField' => [ // presenter column - 'title' => 'Field title', - 'field' => 'presenterMethod', - 'present' => true, - ], - 'relatedBrowserFieldName' => [ // related browser column - 'title' => 'Field title', - 'field' => 'relatedFieldToDisplay', - 'relatedBrowser' => 'browserName', - ] - ]; - - /* - * Columns of the browser view for this module when browsed from another module - * using a browser form field - */ - protected $browserColumns = [ - 'title' => [ - 'title' => 'Title', - 'field' => 'title', - ], - ]; - - /* - * Relations to eager load for the index view - */ - protected $indexWith = []; - - /* - * Relations to eager load for the form view - * Add relationship used in multiselect and resource form fields - */ - protected $formWith = []; - - /* - * Relation count to eager load for the form view - */ - protected $formWithCount = []; - - /* - * Filters mapping ('filterName' => 'filterColumn') - * You can associate items list to filters by having a filterNameList key in the indexData array - * For example, 'category' => 'category_id' and 'categoryList' => app(CategoryRepository::class)->listAll() - */ - protected $filters = []; - - /* - * Add anything you would like to have available in your module's index view - */ - protected function indexData($request) - { - return []; - } - - /* - * Add anything you would like to have available in your module's form view - * For example, relationship lists for multiselect form fields - */ - protected function formData($request) - { - return []; - } - - // Optional, if the automatic way is not working for you (default is ucfirst(str_singular($moduleName))) - protected $modelName = 'model'; - - // Optional, to specify a different feature field name than the default 'featured' - protected $featureField = 'featured'; - - // Optional, specify number of items per page in the listing view (-1 to disable pagination) - // If you are implementing Sortable, this parameter is ignored given reordering is not implemented - // along with pagination. - protected $perPage = 20; - - // Optional, specify the default listing order - protected $defaultOrders = ['title' => 'asc']; - - // Optional, specify the default listing filters - protected $defaultFilters = ['search' => 'title|search']; -``` - -You can also override all actions and internal functions, checkout the ModuleController source in `A17\Twill\Http\Controllers\Admin\ModuleController`. - -#### Example: sorting by a relationship field - -Let's say we have a controller with certain fields displayed: - -File: `app/Http/Controllers/Admin/PlayController.php` -```php - protected $indexColumns = [ - 'image' => [ - 'thumb' => true, // image column - 'variant' => [ - 'role' => 'featured', - 'crop' => 'default', - ], - ], - 'title' => [ // field column - 'title' => 'Title', - 'field' => 'title', - ], - 'festivals' => [ // relation column - 'title' => 'Festival', - 'sort' => true, - 'relationship' => 'festivals', - 'field' => 'title' - ], - ]; -``` - -To order by the relationship we need to overwrite the order method in the module's repository. - -File: `app/Repositories/PlayRepository.php` -```php - ... - public function order($query, array $orders = []) { - - if (array_key_exists('festivalsTitle', $orders)){ - $sort_method = $orders['festivalsTitle']; - // remove the unexisting column from the orders array - unset($orders['festivalsTitle']); - $query = $query->orderByFestival($sort_method); - } - // don't forget to call the parent order function - return parent::order($query, $orders); - } - ... -``` - -Then, add a custom `sort` scope to your model, it could be something like this: - -File: `app/Models/Play.php` -```php - public function scopeOrderByFestival($query, $sort_method = 'ASC') { - return $query - ->leftJoin('festivals', 'plays.section_id', '=', 'festivals.id') - ->select('plays.*', 'festivals.id', 'festivals.title') - ->orderBy('festivals.title', $sort_method); - } -``` - -#### Additional table actions - -You can override the `additionalTableActions()` method to add custom actions in your module's listing view: - -File: `app/Http/Controllers/Admin/NewsletterController.php` -```php - public function additionalTableActions() - { - return [ - 'exportAction' => [ // Action name. - 'name' => 'Export Newsletter List', // Button action title. - 'variant' => 'primary', // Button style variant. Available variants; primary, secondary, action, editor, validate, aslink, aslink-grey, warning, ghost, outline, tertiary - 'size' => 'small', // Button size. Available sizes; small - 'link' => route('newsletter.export'), // Button action link. - 'target' => '', // Leave it blank for self. - 'type' => 'a', // Leave it blank for "button". - ] - ]; - } -``` - -### Form Requests -Classic Laravel 5 [form request validation](https://laravel.com/docs/5.5/validation#form-request-validation). - -Once you generated the module using Twill's CLI module generator, it will also prepare the `App/Http/Requests/Admin/ModuleNameRequest.php` for you to use. -You can choose to use different rules for creation and update by implementing the following 2 functions instead of the classic `rules` one: - -```php -rulesForTranslatedFields([ - // regular rules -], [ - // translated fields rules with just the field name like regular rules -]); -``` - -There is also an helper to define validation messages for translated fields: - -```php -messagesForTranslatedFields([ - // regular messages -], [ - // translated fields messages -]); -``` - -Once you defined the rules in this file, the UI will show the corresponding validation error state or message next to the corresponding form field. - -### Routes - -A router macro is available to create module routes quicker: -```php - ['reorder', 'feature', 'bucket', 'browser']]); - -// You can add an array of only/except action names for the resource controller as a third parameter -// By default, the following routes are created : 'index', 'store', 'show', 'edit', 'update', 'destroy' -Route::module('yourModulePluralName', [], ['only' => ['index', 'edit', 'store', 'destroy']]); - -// The last optional parameter disable the resource controller actions on the module -Route::module('yourPluralModuleName', [], [], false); -``` - -### Revisions and previewing - -When using the `HasRevisions` trait, Twill's UI gives publishers the ability to preview their changes without saving, as well as to preview and compare old revisions. - -If you are implementing your site using Laravel routing and Blade templating (ie. traditional server side rendering), you can follow Twill's convention of creating frontend views at `resources/views/site` and naming them according to their corresponding CRUD module name. When publishers try to preview their changes, Twill will render your frontend view within an iframe, passing the previewed record with it's unsaved changes to your view in the `$item` variable. - -If you want to provide Twill with a custom frontend views path, use the `frontend` configuration array of your `config/twill.php` file: - -```php -return [ - 'frontend' => [ - 'views_path' => 'site', - ], - ... -]; -``` - -If you named your frontend view differently than the name of its corresponding module, you can use the $previewView class property of your module's controller: - -```php - $item, - 'setting_name' => $settingRepository->byKey('setting_name') - ]; -} -``` - -### Nested Modules - -Out of the box, Twill supports 2 kinds of nested modules: [self-nested](#self-nested-modules) and [parent-child](#parent-child-modules). - -#### Self-nested modules - -Self-nested modules allow items to be nested within other items of the same module (e.g. Pages can contain other Pages): - -![self-nested module](/docs/_media/nested-module.png) - -#### Creating self-nested modules - -You can enable nesting when creating a new module with the `--hasNesting` or `-N` option: - -``` -php artisan twill:module:make -N pages -``` - -This will prefill some options and methods in your module's controller and use the supporting traits on your model and repository. - -This feature requires the `laravel-nestedset` package, which can be installed via composer: - -``` -composer require kalnoy/nestedset -``` - -#### Working with self-nested items - -A few accessors and methods are available to work with nested item slugs: - -```php -// Get the combined slug for all ancestors of an item in the current locale: -$slug = $item->ancestorsSlug; - -// for a specific locale: -$slug = $item->getAncestorsSlug($lang); - -// Get the combined slug for an item including all ancestors: -$slug = $item->nestedSlug; - -// for a specific locale: -$slug = $item->getNestedSlug($lang); -``` - -To include all ancestor slugs in the permalink of an item in the CMS, you can dynamically set the `$permalinkBase` property from the `form()` method of your module controller: - -```php -class PageController extends ModuleController -{ - //... - - protected function form($id, $item = null) - { - $item = $this->repository->getById($id, $this->formWith, $this->formWithCount); - - $this->permalinkBase = $item->ancestorsSlug; - - return parent::form($id, $item); - } -} -``` - -To implement routing for nested items, you can combine the `forNestedSlug()` method from `HandleNesting` with a wildcard route parameter: - -```php -// file: routes/web.php - -Route::get('{slug}', function ($slug) { - $page = app(PageRepository::class)->forNestedSlug($slug); - - abort_unless($page, 404); - - return view('site.page', ['page' => $page]); -})->where('slug', '.*'); -``` - -For more information on how to work with nested items in your application, you can refer to the -[laravel-nestedset package documentation](https://github.com/lazychaser/laravel-nestedset#retrieving-nodes). - -#### Parent-child modules - -Parent-child modules are 2 distinct modules, where items of the child module are attached to items of the parent module (e.g. Issues can contain Articles): - -![parent-child modules](/docs/_media/nested-parent-index.png) - -Items of the child module can't be created independently. - -#### Creating parent-child modules - -We'll use the `slug` and `position` features in this example but you can customize as needed: - -``` -php artisan twill:module issues -SP -php artisan twill:module issueArticles -SP -``` - -Add the `issue_id` foreign key to the child module's migration: - -```php -class CreateIssueArticlesTables extends Migration -{ - public function up() - { - Schema::create('issue_articles', function (Blueprint $table) { - // ... - $table->unsignedBigInteger('issue_id')->nullable(); - $table->foreign('issue_id')->references('id')->on('issues'); - }); - - // ... - } -} -``` - -Run the migrations: - -``` -php artisan migrate -``` - -Update the child model. Add the `issue_id` fillable and the relationship to the parent model: - -```php -class IssueArticle extends Model implements Sortable -{ - // ... - - protected $fillable = [ - // ... - 'issue_id', - ]; - - public function issue() - { - return $this->belongsTo(Issue::class); - } -} -``` - -Update the parent model. Add the relationship to the child model: - -```php -class Issue extends Model implements Sortable -{ - // ... - - public function articles() - { - return $this->hasMany(IssueArticle::class); - } -} -``` - -Update the child controller. Set the `$moduleName` and `$modelName` properties, then override the `getParentModuleForeignKey()` method: - -```php -class IssueArticleController extends BaseModuleController -{ - protected $moduleName = 'issues.articles'; - - protected $modelName = 'IssueArticle'; - - protected function getParentModuleForeignKey() - { - return 'issue_id'; - } -} -``` - -Update the parent controller. Set the `$indexColumns` property to include a new `Articles` column. This will be a link to the child module items, for each parent. - -```php -class IssueController extends BaseModuleController -{ - protected $moduleName = 'issues'; - - protected $indexColumns = [ - 'title' => [ - 'title' => 'Title', - 'field' => 'title', - ], - 'articles' => [ - 'title' => 'Articles', - 'nested' => 'articles', - ], - ]; -} -``` - -Add both modules to `routes/admin.php`: - -```php -Route::module('issues'); -Route::module('issues.articles'); -``` - -Add the parent module to `config/twill-navigation.php`: - -```php -return [ - 'issues' => [ - 'title' => 'Issues', - 'module' => true, - ], -]; -``` - -Then, rename and move the `articles/` views folder inside of the parent `issues/` folder: -``` -resources/views/admin/ -└── issues - ├── articles - │   └── form.blade.php - └── form.blade.php -... -``` - -#### Using breadcrumbs for easier navigation - -In the child module controller, override the `indexData()` method to add the breadcrumbs to the index view: - -```php -class IssueArticleController extends BaseModuleController -{ - // ... - - protected function indexData($request) - { - $issue = app(IssueRepository::class)->getById(request('issue')); - - return [ - 'breadcrumb' => [ - [ - 'label' => 'Issues', - 'url' => moduleRoute('issues', '', 'index'), - ], - [ - 'label' => $issue->title, - 'url' => moduleRoute('issues', '', 'edit', $issue->id), - ], - [ - 'label' => 'Articles', - ], - ], - ]; - } -} -``` - -![child module index](/docs/_media/nested-child-index.png) - -
- -Then, override the `formData()` method to do the same in the form view: - -```php - protected function formData($request) - { - $issue = app(IssueRepository::class)->getById(request('issue')); - - return [ - 'breadcrumb' => [ - [ - 'label' => 'Issues', - 'url' => moduleRoute('issues', '', 'index'), - ], - [ - 'label' => $issue->title, - 'url' => moduleRoute('issues', '', 'edit', $issue->id), - ], - [ - 'label' => 'Articles', - 'url' => moduleRoute('issues.articles', '', 'index'), - ], - [ - 'label' => 'Edit', - ], - ], - ]; - } -``` - -![nested child form](/docs/_media/nested-child-form.png) diff --git a/docs/.sections/form-fields.md b/docs/.sections/form-fields.md deleted file mode 100644 index 1d7f86ec3..000000000 --- a/docs/.sections/form-fields.md +++ /dev/null @@ -1,1333 +0,0 @@ -## Form fields - -Your module `form` view should look something like this (`resources/views/admin/moduleName/form.blade.php`): - -```php -@extends('twill::layouts.form') -@section('contentFields') - @formField('...', [...]) - ... -@stop -``` - -The idea of the `contentFields` section is to contain your most important fields and, if applicable, the block editor as the last field. - -If you have other fields, like attributes, relationships, extra images, file attachments or repeaters, you'll want to add a `fieldsets` section after the `contentFields` section and use the `@formFieldset` directive to create new ones like in the following example: - -```php -@extends('twill::layouts.form', [ - 'additionalFieldsets' => [ - ['fieldset' => 'attributes', 'label' => 'Attributes'], - ] -]) - -@section('contentFields') - @formField('...', [...]) - ... -@stop - -@section('fieldsets') - @formFieldset(['id' => 'attributes', 'title' => 'Attributes']) - @formField('...', [...]) - ... - @endformFieldset -@stop -``` - -The additional fieldsets array passed to the form layout will display a sticky navigation of your fieldset on scroll. -You can also rename the content section by passing a `contentFieldsetLabel` property to the layout, or disable it entirely using -`'disableContentFieldset' => true`. - -### Input -![screenshot](/docs/_media/input.png) - -```php -@formField('input', [ - 'name' => 'subtitle', - 'label' => 'Subtitle', - 'maxlength' => 100, - 'required' => true, - 'note' => 'Hint message goes here', - 'placeholder' => 'Placeholder goes here', -]) - -@formField('input', [ - 'translated' => true, - 'name' => 'subtitle_translated', - 'label' => 'Subtitle (translated)', - 'maxlength' => 250, - 'required' => true, - 'note' => 'Hint message goes here', - 'placeholder' => 'Placeholder goes here', - 'type' => 'textarea', - 'rows' => 3 -]) -``` - -| Option | Description | Type/values | Default value | -| :---------- | :------------------------------------------------------------------------------------------------------------------------| :------------------------------------------------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| type | Type of input field | text
texarea
email
number
password | text | -| translated | Defines if the field is translatable | true
false | false | -| maxlength | Max character count of the field | integer | | -| note | Hint message displayed above the field | string | | -| placeholder | Text displayed as a placeholder in the field | string | | -| prefix | Text displayed as a prefix in the field | string | | -| rows | Sets the number of rows in a textarea | integer | 5 | -| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | -| disabled | Disables the field | true
false | false | -| readonly | Sets the field as readonly | true
false | false | -| default | Sets a default value if empty | string | | - - -A migration to save an `input` field would be: - -```php -Schema::table('articles', function (Blueprint $table) { - ... - $table->string('subtitle', 100)->nullable(); - ... - -}); -// OR -Schema::table('article_translations', function (Blueprint $table) { - ... - $table->string('subtitle', 250)->nullable(); - ... -}); -``` - -If this `input` field is used for longer strings then the migration would be: - -```php -Schema::table('articles', function (Blueprint $table) { - ... - $table->text('subtitle')->nullable(); - ... -}); -``` - -When used in a [block](https://twill.io/docs/#adding-blocks), no migration is needed. - - -### WYSIWYG -![screenshot](/docs/_media/wysiwyg.png) - -```php -@formField('wysiwyg', [ - 'name' => 'case_study', - 'label' => 'Case study text', - 'toolbarOptions' => ['list-ordered', 'list-unordered'], - 'placeholder' => 'Case study text', - 'maxlength' => 200, - 'note' => 'Hint message', -]) - -@formField('wysiwyg', [ - 'name' => 'case_study', - 'label' => 'Case study text', - 'toolbarOptions' => [ [ 'header' => [1, 2, false] ], 'list-ordered', 'list-unordered', [ 'indent' => '-1'], [ 'indent' => '+1' ] ], - 'placeholder' => 'Case study text', - 'maxlength' => 200, - 'editSource' => true, - 'note' => 'Hint message', -]) -``` -By default, the WYSIWYG field is based on [Quill](https://quilljs.com/). - -You can add all [toolbar options](https://quilljs.com/docs/modules/toolbar/) from Quill with the `toolbarOptions` key. - -For example, this configuration will render a `wysiwyg` field with almost all features from Quill and Snow theme. - -```php - @formField('wysiwyg', [ - 'name' => 'case_study', - 'label' => 'Case study text', - 'toolbarOptions' => [ - ['header' => [2, 3, 4, 5, 6, false]], - 'bold', - 'italic', - 'underline', - 'strike', - ["script" => "super"], - ["script" => "sub"], - "blockquote", - "code-block", - ['list' => 'ordered'], - ['list' => 'bullet'], - ['indent' => '-1'], - ['indent' => '+1'], - ["align" => []], - ["direction" => "rtl"], - 'link', - "clean", - ], - 'placeholder' => 'Case study text', - 'maxlength' => 200, - 'editSource' => true, - 'note' => 'Hint message`', - ]) -``` - -Note that Quill outputs CSS classes in the HTML for certain toolbar modules (indent, font, align, etc.), and that the image module is not integrated with Twill's media library. It outputs the base64 representation of the uploaded image. It is not a recommended way of using and storing images, prefer using one or multiple `medias` form fields or blocks fields for flexible content. This will give you greater control over your frontend output. - - -| Option | Description | Type/values | Default value | -| :------------- | :----------------------------------------------------------- | :--------------------------------------------------------- | :-------------------------------------- | -| name | Name of the field | string | | -| label | Label of the field | string | | -| type | Type of wysiwyg field | quill
tiptap | quill | -| toolbarOptions | Array of options/tools that will be displayed in the editor | [Quill options](https://quilljs.com/docs/modules/toolbar/) | bold
italic
underline
link | -| editSource | Displays a button to view source code | true
false | false | -| hideCounter | Hide the character counter displayed at the bottom | true
false | false | -| limitHeight | Limit the editor height from growing beyond the viewport | true
false | false | -| translated | Defines if the field is translatable | true
false | false | -| maxlength | Max character count of the field | integer | 255 | -| note | Hint message displayed above the field | string | | -| placeholder | Text displayed as a placeholder in the field | string | | -| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | - - -A migration to save a `wysiwyg` field would be: - -```php -Schema::table('articles', function (Blueprint $table) { - ... - $table->text('case_study')->nullable(); - ... - -}); -// OR -Schema::table('article_translations', function (Blueprint $table) { - ... - $table->text('case_study')->nullable(); - ... -}); -``` -When used in a [block](https://twill.io/docs/#adding-blocks), no migration is needed. - -### Medias -![screenshot](/docs/_media/medias.png) - -```php -@formField('medias', [ - 'name' => 'cover', - 'label' => 'Cover image', - 'note' => 'Also used in listings', - 'fieldNote' => 'Minimum image width: 1500px' -]) - -@formField('medias', [ - 'name' => 'slideshow', - 'label' => 'Slideshow', - 'max' => 5, - 'fieldNote' => 'Minimum image width: 1500px' -]) -``` - -| Option | Description | Type/values | Default value | -| :------------- | :--------------------------------------------------- | :------------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| translated | Defines if the field is translatable | true
false | false | -| max | Max number of attached items | integer | 1 | -| fieldNote | Hint message displayed above the field | string | | -| note | Hint message displayed in the field | string | | -| buttonOnTop | Displays the `Attach images` button above the images | true
false | false | - - -Right after declaring the `medias` formField in the blade template file, you still need to do a few things to make it work properly. - -If the formField is in a static content form, you have to include the `HasMedias` Trait in your module's [Model](https://twill.io/docs/#models) and inlcude `HandleMedias` in your module's [Repository](https://twill.io/docs/#repositories). In addition, you have to uncomment the `$mediasParams` section in your Model file to let the model know about fields you'd like to save from the form. - -Learn more about how Twill's media configurations work at [Model](https://twill.io/docs/#models), [Repository](https://twill.io/docs/#repositories), [Media Library Role & Crop Params](https://twill.io/docs/#image-rendering-service) - -If the formField is used inside a block, you need to define the `mediasParams` at `config/twill.php` under `crops` key, and you are good to go. You could checkout [Twill Default Configuration](https://twill.io/docs/#default-configuration) and [Rendering Blocks](https://twill.io/docs/#rendering-blocks) for references. - -If the formField is used inside a repeater, you need to define the `mediasParams` at `config/twill.php` under `block_editor.crops`. - -If you need medias fields to be translatable (ie. publishers can select different images for each locale), set the `twill.media_library.translated_form_fields` configuration value to `true`. - -##### Example: -To add a `medias` form field in a form, first add `$mediaParams` to the model. - -```php - [ - 'default' => [ - [ - 'name' => 'default', - 'ratio' => 16 / 9, - ], - ], - 'mobile' => [ - [ - 'name' => 'mobile', - 'ratio' => 1, - ], - ], - ], - ]; - - ... -} -``` - -Then, add the form field to the `form.blade.php` file. - -```php -@extends('twill::layouts.form') - -@section('contentFields') - - ... - - @formField('medias', [ - 'name' => 'cover', - 'label' => 'Cover image', - ]) - - ... -@stop -``` - -No migration is needed to save `medias` form fields. - - -### Files -![screenshot](/docs/_media/files.png) - -```php -@formField('files', [ - 'name' => 'single_file', - 'label' => 'Single file', - 'note' => 'Add one file (per language)' -]) - -@formField('files', [ - 'name' => 'files', - 'label' => 'Files', - 'max' => 4, -]) -``` - -| Option | Description | Type/values | Default value | -| :------------- | :---------------------------------------- | :------------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| itemLabel | Label used for the `Add` button | string | | -| max | Max number of attached items | integer | 1 | -| fieldNote | Hint message displayed above the field | string | | -| note | Hint message displayed in the field | string | | -| buttonOnTop | Displays the `Add` button above the files | true
false | false | - - -Similar to the media formField, to make the file field work, you have to include the `HasFiles` trait in your module's [Model](https://twill.io/docs/#models), and include `HandleFiles` trait in your module's [Repository](https://twill.io/docs/#repositories). At last, add the `filesParams` configuration array in your model. -```php -public $filesParams = ['file_role', ...]; // a list of file roles -``` - -Learn more at [Model](https://twill.io/docs/#models), [Repository](https://twill.io/docs/#repositories). - -If you are using the file formField in a block, you have to define the `files` key in `config/twill.php`. Add it under `block_editor` key and at the same level as `crops` key: -```php -return [ - 'block_editor' => [ - 'crops' => [ - ... - ], - 'files' => ['file_role1', 'file_role2', ...] - ] -``` - -No migration is needed to save `files` form fields. - - -### Datepicker -![screenshot](/docs/_media/datepicker.png) - -```php -@formField('date_picker', [ - 'name' => 'event_date', - 'label' => 'Event date', - 'minDate' => '2017-09-10 12:00', - 'maxDate' => '2017-12-10 12:00' -]) -``` - -| Option | Description | Type/values | Default value | -| :---------- | :----------------------------------------------------------- | :-------------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| minDate | Minimum selectable date | string | | -| maxDate | Maximum selectable date | string | | -| withTime | Define if the field will display the time selector | true
false | true | -| time24Hr | Pick time with a 24h picker instead of AM/PM | true
false | false | -| allowClear | Adds a button to clear the field | true
false | false | -| allowInput | Allow manually editing the selected date in the field | true
false | false | -| altFormat | Format used by [flatpickr](https://flatpickr.js.org/formatting/) | string | F j, Y | -| hourIncrement | Time picker hours increment | number | 1 | -| minuteIncrement | Time picker minutes increment | number | 30 | -| note | Hint message displayed above the field | string | | -| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | - - -A migration to save a `date_picker` field would be: - -```php -Schema::table('posts', function (Blueprint $table) { - ... - $table->date('event_date')->nullable(); - ... -}); -// OR -Schema::table('posts', function (Blueprint $table) { - ... - $table->dateTime('event_date')->nullable(); - ... -}); -``` - -When used in a [block](https://twill.io/docs/#adding-blocks), no migration is needed. - -### Timepicker - -```php -@formField('time_picker', [ - 'name' => 'event_time', - 'label' => 'Event time', -]) -``` - -| Option | Description | Type/values | Default value | -| :---------- | :----------------------------------------------------------- | :-------------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| time24Hr | Pick time with a 24h picker instead of AM/PM | true
false | false | -| allowClear | Adds a button to clear the field | true
false | false | -| allowInput | Allow manually editing the selected date in the field | true
false | false | -| hourIncrement | Time picker hours increment | number | 1 | -| minuteIncrement | Time picker minutes increment | number | 30 | -| altFormat | Format used by [flatpickr](https://flatpickr.js.org/formatting/) | string | h:i | -| note | Hint message displayed above the field | string | | -| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | - - -A migration to save a `time_picker` field would be: - -```php -Schema::table('posts', function (Blueprint $table) { - ... - $table->time('event_time')->nullable(); - ... -}); -// OR, if you are merging with a date field -Schema::table('posts', function (Blueprint $table) { - ... - $table->dateTime('event_date')->nullable(); - ... -}); -``` - -When used in a [block](https://twill.io/docs/#adding-blocks), no migration is needed. - - -### Select -![screenshot](/docs/_media/select.png) - -```php -@formField('select', [ - 'name' => 'office', - 'label' => 'Office', - 'placeholder' => 'Select an office', - 'options' => [ - [ - 'value' => 1, - 'label' => 'New York' - ], - [ - 'value' => 2, - 'label' => 'London' - ], - [ - 'value' => 3, - 'label' => 'Berlin' - ] - ] -]) -``` - -| Option | Description | Type/values | Default value | -| :---------- | :----------------------------------------------------------- | :-------------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| options | Array of options for the dropdown, must include _value_ and _label_ | array | | -| unpack | Defines if the select will be displayed as an open list of options | true
false | false | -| columns | Aligns the options on a grid with a given number of columns | integer | 0 (off) | -| searchable | Filter the field values while typing | true
false | false | -| note | Hint message displayed above the field | string | | -| placeholder | Text displayed as a placeholder in the field | string | | -| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | - - -A migration to save a `select` field would be: - -```php -Schema::table('posts', function (Blueprint $table) { - ... - $table->integer('office')->nullable(); - ... -}); -``` - -When used in a [block](https://twill.io/docs/#adding-blocks), no migration is needed. - -### Select unpacked -![screenshot](/docs/_media/selectunpacked.png) - -```php -@formField('select', [ - 'name' => 'discipline', - 'label' => 'Discipline', - 'unpack' => true, - 'options' => [ - [ - 'value' => 'arts', - 'label' => 'Arts & Culture' - ], - [ - 'value' => 'finance', - 'label' => 'Banking & Finance' - ], - [ - 'value' => 'civic', - 'label' => 'Civic & Public' - ], - [ - 'value' => 'design', - 'label' => 'Design & Architecture' - ], - [ - 'value' => 'education', - 'label' => 'Education' - ], - [ - 'value' => 'entertainment', - 'label' => 'Entertainment' - ], - ] -]) -``` - -A migration to save the above `select` field would be: - -```php -Schema::table('posts', function (Blueprint $table) { - ... - $table->string('discipline')->nullable(); - ... -}); -``` - -When used in a [block](https://twill.io/docs/#adding-blocks), no migration is needed. - - -### Multi select -![screenshot](/docs/_media/multiselectunpacked.png) - -```php -@formField('multi_select', [ - 'name' => 'sectors', - 'label' => 'Sectors', - 'min' => 1, - 'max' => 2, - 'options' => [ - [ - 'value' => 'arts', - 'label' => 'Arts & Culture' - ], - [ - 'value' => 'finance', - 'label' => 'Banking & Finance' - ], - [ - 'value' => 'civic', - 'label' => 'Civic & Public' - ], - [ - 'value' => 'design', - 'label' => 'Design & Architecture' - ], - [ - 'value' => 'education', - 'label' => 'Education' - ] - ] -]) -``` - -| Option | Description | Type/values | Default value | -| :---------- | :----------------------------------------------------------- | :-------------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| min | Minimum number of selectable options | integer | | -| max | Maximum number of selectable options | integer | | -| options | Array of options for the dropdown, must include _value_ and _label_ | array | | -| unpack | Defines if the multi select will be displayed as an open list of options | true
false | true | -| columns | Aligns the options on a grid with a given number of columns | integer | 0 (off) | -| searchable | Filter the field values while typing | true
false | false | -| note | Hint message displayed above the field | string | | -| placeholder | Text displayed as a placeholder in the field | string | | -| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | -| disabled | Disables the field | true
false | false | - - -There are several ways to implement a `multi_select` form field. - -##### Multi select with static values -Sometimes you just have a set of values that are static. - -In this case that it can be implemented as follows: - -- Create the database migration to store a JSON or LONGTEXT: -```php -Schema::table('posts', function (Blueprint $table) { - ... - $table->json('sectors')->nullable(); - ... -}); - -// OR -Schema::table('posts', function (Blueprint $table) { - ... - $table->longtext('sectors')->nullable(); - ... -}); -``` - -- In your model add an accessor and a mutator: -```php -public function getSectorsAttribute($value) -{ - return collect(json_decode($value))->map(function($item) { - return ['id' => $item]; - })->all(); -} - -public function setSectorsAttribute($value) -{ - $this->attributes['sectors'] = collect($value)->filter()->values(); -} -``` - -- Cast the field to `array`: -```php -protected $casts = [ - 'sectors' => 'array' -] -``` - -##### Multi select with dynamic values - -Sometimes the content for the `multi_select` is coming from another model. - -In this case that it can be implemented as follows: - -- Create a Sectors [module](https://twill.io/docs/#cli-generator) - -``` -php artisan twill:module sectors -``` - -- Create a migration for a pivot table. - -``` -php artisan make:migration create_post_sector_table -``` - -- Use Twill's `createDefaultRelationshipTableFields` to set it up: - -```php -public function up() -{ - Schema::create('post_sector', function (Blueprint $table) { - createDefaultRelationshipTableFields($table, 'sector', 'post'); - $table->integer('position')->unsigned()->index(); - }); -} -``` - -- In your model, add a `belongsToMany` relationship: - -```php -public function sectors() { - return $this->belongsToMany('App\Models\Sector'); -} -``` - -- In your repository, make sure to sync the association when saving: - -```php -public function afterSave($object, $fields) -{ - $object->sectors()->sync($fields['sectors'] ?? []); - - parent::afterSave($object, $fields); -} -``` - -- In your controller, add to the formData the collection of options: -```php -protected function formData($request) -{ - return [ - 'sectors' => app()->make(SectorRepository::class)->listAll() - ]; -} -``` - -- In the form, we can now add the field: -```php -@formField('multi_select', [ - 'name' => 'sectors', - 'label' => 'Sectors', - 'options' => $sectors -]) -``` - -When used in a [block](https://twill.io/docs/#adding-blocks), no migration is needed. - - -### Multi select inline -![screenshot](/docs/_media/multiselectinline.png) - -```php -@formField('multi_select', [ - 'name' => 'sectors', - 'label' => 'Sectors', - 'unpack' => false, - 'options' => [ - [ - 'value' => 'arts', - 'label' => 'Arts & Culture' - ], - [ - 'value' => 'finance', - 'label' => 'Banking & Finance' - ], - [ - 'value' => 'civic', - 'label' => 'Civic & Public' - ], - [ - 'value' => 'design', - 'label' => 'Design & Architecture' - ], - [ - 'value' => 'education', - 'label' => 'Education' - ] - ] -]) -``` - -See [Multi select](https://twill.io/docs/#multi-select) for more information on how to implement the field with static and dynamic values. - - -### Checkbox -![screenshot](/docs/_media/checkbox.png) - -```php -@formField('checkbox', [ - 'name' => 'featured', - 'label' => 'Featured' -]) -``` - -| Option | Description | Type | Default value | -| :------------------ | :------------------------------------------------------ | :-------------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| note | Hint message displayed above the field | string | | -| default | Sets a default value | boolean | false | -| disabled | Disables the field | boolean | false | -| requireConfirmation | Displays a confirmation dialog when modifying the field | boolean | false | -| confirmTitleText | The title of the confirmation dialog | string | 'Confirm selection' | -| confirmMessageText | The text of the confirmation dialog | string | 'Are you sure you want to change this option ?' | -| border | Draws a border around the field | boolean | false | - - -### Multiple checkboxes -![screenshot](/docs/_media/checkboxes.png) - -```php -@formField('checkboxes', [ - 'name' => 'sectors', - 'label' => 'Sectors', - 'note' => '3 sectors max & at least 1 sector', - 'min' => 1, - 'max' => 3, - 'inline' => true, - 'options' => [ - [ - 'value' => 'arts', - 'label' => 'Arts & Culture' - ], - [ - 'value' => 'finance', - 'label' => 'Banking & Finance' - ], - [ - 'value' => 'civic', - 'label' => 'Civic & Public' - ], - ] -]) -``` - -| Option | Description | Type | Default value | -| :------ | :------------------------------------------------------------------ | :-------| :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| min | Minimum number of selectable options | integer | | -| max | Maximum number of selectable options | integer | | -| options | Array of options for the dropdown, must include _value_ and _label_ | array | | -| inline | Defines if the options are displayed on one or multiple lines | boolean | false | -| note | Hint message displayed above the field | string | | -| border | Draws a border around the field | boolean | false | -| columns | Aligns the options on a grid with a given number of columns | integer | 0 (off) | - - -### Radios -![screenshot](/docs/_media/radios.png) - -```php -@formField('radios', [ - 'name' => 'discipline', - 'label' => 'Discipline', - 'default' => 'civic', - 'inline' => true, - 'options' => [ - [ - 'value' => 'arts', - 'label' => 'Arts & Culture' - ], - [ - 'value' => 'finance', - 'label' => 'Banking & Finance' - ], - [ - 'value' => 'civic', - 'label' => 'Civic & Public' - ], - ] -]) -``` - -| Option | Description | Type | Default value | -| :------------------ | :------------------------------------------------------------------ | :------ | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| note | Hint message displayed above the field | string | | -| options | Array of options for the dropdown, must include _value_ and _label_ | array | | -| inline | Defines if the options are displayed on one or multiple lines | boolean | false | -| default | Sets a default value | string | | -| requireConfirmation | Displays a confirmation dialog when modifying the field | boolean | false | -| confirmTitleText | The title of the confirmation dialog | string | 'Confirm selection' | -| confirmMessageText | The text of the confirmation dialog | string | 'Are you sure you want to change this option ?' | -| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | boolean | false | -| border | Draws a border around the field | boolean | false | -| columns | Aligns the options on a grid with a given number of columns | integer | 0 (off) | - - -### Block editor -![screenshot](/docs/_media/blockeditor.png) - -```php -@formField('block_editor', [ - 'blocks' => ['title', 'quote', 'text', 'image', 'grid', 'test', 'publications', 'news'] -]) -``` - -See [Block editor](https://twill.io/docs/#block-editor-3) - - -| Option | Description | Type/values | Default value | -| :--------------- | :----------------------------------------------------------- | :------------- | :------------ | -| blocks | Array of blocks | array | | -| label | Label used for the button | string | 'Add Content' | -| withoutSeparator | Defines if a separator before the block editor container should be rendered | true
false | false | - - -### Browser -![screenshot](/docs/_media/browser.png) - -```php -@formField('browser', [ - 'moduleName' => 'publications', - 'name' => 'publications', - 'label' => 'Publications', - 'max' => 4, -]) -``` - -| Option | Description | Type | Default value | -| :---------- | :------------------------------------------------------------------------------ | :-------| :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| moduleName | Name of the module (single related module) | string | | -| modules | Array of modules (multiple related modules), must include _name_ | array | | -| endpoints | Array of endpoints (multiple related modules), must include _value_ and _label_ | array | | -| max | Max number of attached items | integer | 1 | -| note | Hint message displayed in the field | string | | -| fieldNote | Hint message displayed above the field | string | | -| browserNote | Hint message displayed inside the browser modal | string | | -| itemLabel | Label used for the `Add` button | string | | -| buttonOnTop | Displays the `Add` button above the items | boolean | false | -| wide | Expands the browser modal to fill the viewport | boolean | false | -| sortable | Allows manually sorting the attached items | boolean | true | - -
- -Browser fields can be used inside as well as outside the block editor. - -Inside the block editor, no migration is needed when using browsers. Refer to the section titled [Adding browser fields to a block](#adding-browser-fields-to-a-block) for a detailed explanation. - -Outside the block editor, browser fields are used to save `belongsToMany` relationships. The relationships can be stored in Twill's own `related` table or in a custom pivot table. - -#### Using browser fields as related items - -The following example demonstrates how to use a browser field to attach `Authors` to `Articles`. - -- Update the `Article` model to add the `HasRelated` trait: - -```php -use A17\Twill\Models\Behaviors\HasRelated; - -class Article extends Model -{ - use HasRelated; - - /* ... */ -} -``` - -- Update `ArticleRepository` to add the browser field to the `$relatedBrowsers` property: - -```php -class ArticleRepository extends ModuleRepository -{ - protected $relatedBrowsers = ['authors']; -} -``` - -- Add the browser field to `resources/views/admin/articles/form.blade.php`: - -```php -@extends('twill::layouts.form') - -@section('contentFields') - ... - - @formField('browser', [ - 'moduleName' => 'authors', - 'name' => 'authors', - 'label' => 'Authors', - 'max' => 4, - ]) -@stop -``` - -#### Multiple modules as related items - -You can use the same approach to handle polymorphic relationships through Twill's `related` table. - -- Update `ArticleRepository`: - -```php -class ArticleRepository extends ModuleRepository -{ - protected $relatedBrowsers = ['collaborators']; -} -``` - -- Add the browser field to `resources/views/admin/articles/form.blade.php`: - -```php -@extends('twill::layouts.form') - -@section('contentFields') - ... - - @formField('browser', [ - 'modules' => [ - [ - 'label' => 'Authors', - 'name' => 'authors', - ], - [ - 'label' => 'Editors', - 'name' => 'editors', - ], - ], - 'name' => 'collaborators', - 'label' => 'Collaborators', - 'max' => 4, - ]) -@stop -``` - -- Alternatively, you can use manual endpoints instead of module names: - -```php - @formField('browser', [ - 'endpoints' => [ - [ - 'label' => 'Authors', - 'value' => '/authors/browser', - ], - [ - 'label' => 'Editors', - 'value' => '/editors/browser', - ], - ], - 'name' => 'collaborators', - 'label' => 'Collaborators', - 'max' => 4, - ]) -``` - -#### Working with related items - -To retrieve the items in the frontend, you can use the `getRelated` method on models and blocks. It will return of collection of related models in the correct order: - -```php - $item->getRelated('collaborators'); - - // or, in a block: - - $block->getRelated('collaborators'); -``` - -#### Using browser fields and custom pivot tables - -Checkout this [Spectrum tutorial](https://spectrum.chat/twill/tips-and-tricks/step-by-step-ii-creating-a-twill-app~37c36601-1198-4c53-857a-a2b47c6d11aa) that walks through the entire process of using browser fields with custom pivot tables. - -### Repeater -![screenshot](/docs/_media/repeater.png) - -```php -@formField('repeater', ['type' => 'video']) -``` - -| Option | Description | Type | Default value | -| :----------- | :-------------------------------------------- | :-------| :------------- | -| type | Type of repeater items | string | | -| name | Name of the field | string | same as `type` | -| buttonAsLink | Displays the `Add` button as a centered link | boolean | false | - -
- -Repeater fields can be used inside as well as outside the block editor. - -Inside the block editor, repeater blocks share the same model as regular blocks. By reading the section on the [block editor](#block-editor-3) first, you will get a good overview of how to create and define repeater blocks for your project. No migration is needed when using repeater blocks. Refer to the section titled [Adding repeater fields to a block](#adding-repeater-fields-to-a-block) for a detailed explanation. - -Outside the block editor, repeater fields are used to save `hasMany` or `morphMany` relationships. - -#### Using repeater fields - -The following example demonstrates how to define a relationship between `Team` and `TeamMember` modules to implement a `team-member` repeater. - -- Create the modules. Make sure to enable the `position` feature on the `TeamMember` module: - -``` -php artisan twill:make:module Team -php artisan twill:make:module TeamMember -P -``` - -- Update the `create_team_members_tables` migration. Add the `team_id` foreign key used for the `TeamMember—Team` relationship: - -```php -class CreateTeamMembersTables extends Migration -{ - public function up() - { - Schema::create('team_members', function (Blueprint $table) { - /* ... */ - - $table->foreignId('team_id') - ->constrained() - ->onUpdate('cascade') - ->onDelete('cascade'); - }); - } -} -``` - -- Run the migrations: - -``` -php artisan migrate -``` - -- Update the `Team` model. Define the `members` relationship. The results should be ordered by position: - -```php -class Team extends Model -{ - /* ... */ - - public function members() - { - return $this->hasMany(TeamMember::class)->orderBy('position'); - } -} -``` - -- Update the `TeamMember` model. Add `team_id` to the `fillable` array: - -```php -class TeamMember extends Model -{ - protected $fillable = [ - /* ... */ - 'team_id', - ]; -} -``` - -- Update `TeamRepository`. Override the `afterSave` and `getFormFields` methods to process the repeater field: - -```php -class TeamRepository extends ModuleRepository -{ - /* ... */ - - public function afterSave($object, $fields) - { - $this->updateRepeater($object, $fields, 'members', 'TeamMember', 'team-member'); - parent::afterSave($object, $fields); - } - - public function getFormFields($object) - { - $fields = parent::getFormFields($object); - $fields = $this->getFormFieldsForRepeater($object, $fields, 'members', 'TeamMember', 'team-member'); - return $fields; - } -} -``` - -- Add the repeater Blade template: - -Create file `resources/views/admin/repeaters/team-member.blade.php`: - -```php -@twillRepeaterTitle('Team Member') -@twillRepeaterTrigger('Add member') -@twillRepeaterGroup('app') - -@formField('input', [ - 'name' => 'title', - 'label' => 'Title', - 'required' => true, -]) - -... -``` - -- Add the repeater field to the form: - -Update file `resources/views/admin/teams/form.blade.php`: - -```php -@extends('twill::layouts.form') - -@section('contentFields') - ... - - @formField('repeater', ['type' => 'team-member']) -@stop -``` - -- Finishing up: - -Add both modules to your `admin.php` routes. Add the `Team` module to your `twill-navigation.php` config and you are done! - -#### Dynamic repeater titles - -In Twill >= 2.5, you can use the `@twillRepeaterTitleField` directive to include the value of a given field in the title of the repeater items. This directive also accepts a `hidePrefix` option to hide the generic repeater title: - -```php -@twillRepeaterTitle('Person') -@twillRepeaterTitleField('name', ['hidePrefix' => true]) -@twillRepeaterTrigger('Add person') -@twillRepeaterGroup('app') - -@formField('input', [ - 'name' => 'name', - 'label' => 'Name', - 'required' => true, -]) -``` - - -### Map -![screenshot](/docs/_media/map.png) - -```php -@formField('map', [ - 'name' => 'location', - 'label' => 'Location', - 'showMap' => true, -]) -``` - -| Option | Description | Type/values | Default value | -| :--------------- | :---------------------------------------------------------- | :-------------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | -| showMap | Adds a button to toggle the map visibility | true
false | true | -| openMap | Used with `showMap`, initialize the field with the map open | true
false | false | -| saveExtendedData | Enables saving Bounding Box Coordinates and Location types | true
false | false | - -This field requires that you provide a `GOOGLE_MAPS_API_KEY` variable in your .env file. - -A migration to save a `map` field would be: - -```php -Schema::table('posts', function (Blueprint $table) { - ... - $table->json('location')->nullable(); - ... -}); -``` - -The field used should also be casted as an array in your model: - -```php -public $casts = [ - 'location' => 'array', -]; -``` - -When used in a [block](https://twill.io/docs/#adding-blocks), no migration is needed. - -#### Example of data stored in the Database: -Default data: - -```javascript -{ - "latlng": "48.85661400000001|2.3522219", - "address": "Paris, France" -} -``` - -Extended data: - -```javascript -{ - "latlng": "51.1808302|-2.256022799999999", - "address": "Warminster BA12 7LG, United Kingdom", - "types": ["point_of_interest", "establishment"], - "boundingBox": { - "east": -2.25289275, - "west": -2.257066149999999, - "north": 51.18158853029149, - "south": 51.17889056970849 - } -} -``` - -### Color -![screenshot](/docs/_media/color.png) - -```php -@formField('color', [ - 'name' => 'main_color', - 'label' => 'Main color' -]) -``` - -| Option | Description | Type | Default value | -| :------ | :------------------ | :------- | :------------ | -| name | Name of the field | string | | -| label | Label of the field | string | | - - -A migration to save a `color` field would be: - -```php -Schema::table('posts', function (Blueprint $table) { - ... - $table->string('main_color', 10)->nullable(); - ... -}); -``` -### Conditional fields - -You can conditionally display fields based on the values of other fields in your form. For example, if you wanted to display a video embed text field only if the type of article, a radio field, is "video" you'd do something like the following: - -```php -@formField('radios', [ - 'name' => 'type', - 'label' => 'Article type', - 'default' => 'long_form', - 'inline' => true, - 'options' => [ - [ - 'value' => 'long_form', - 'label' => 'Long form article' - ], - [ - 'value' => 'video', - 'label' => 'Video article' - ] - ] -]) - -@formConnectedFields([ - 'fieldName' => 'type', - 'fieldValues' => 'video', - 'renderForBlocks' => true/false # (depending on regular form vs block form) -]) - @formField('input', [ - 'name' => 'video_embed', - 'label' => 'Video embed' - ]) -@endformConnectedFields -``` -Here's an example based on a checkbox field where the value is either true or false: - -```php -@formField('checkbox', [ - 'name' => 'vertical_article', - 'label' => 'Vertical Story' -]) - -@formConnectedFields([ - 'fieldName' => 'vertical_article', - 'fieldValues' => true, - 'renderForBlocks' => true/false # (depending on regular form vs block form) -]) - @formField('medias', [ - 'name' => 'vertical_image', - 'label' => 'Vertical Image', - ]) -@endformConnectedFields -``` diff --git a/docs/.sections/getting-started/environment-requirements.md b/docs/.sections/getting-started/environment-requirements.md deleted file mode 100644 index 1a8521eec..000000000 --- a/docs/.sections/getting-started/environment-requirements.md +++ /dev/null @@ -1,20 +0,0 @@ -## Getting started - -### Environment requirements -Twill is compatible with Laravel versions `5.6` to `8`, running on PHP 7.1 and above. - -As a dependency to your own application, Twill shares Laravel's [server requirements](https://laravel.com/docs/6.x/installation#server-requirements), which are satisfied by both [Homestead](https://laravel.com/docs/7.x/homestead) and [Valet](https://laravel.com/docs/7.x/valet) during development, and easily deployed to production using [Forge](https://forge.laravel.com) and [Envoyer](https://envoyer.io) or [Envoy](https://laravel.com/docs/7.x/envoy), as well as any other Laravel compatible server configuration and deployment strategy. - -Twill uses Vue CLI to build the frontend assets of its UI. To ensure reproducible builds, npm scripts provided by Twill use the [npm `ci`](https://blog.npmjs.org/post/171556855892/introducing-npm-ci-for-faster-more-reliable) command, which is available since npm `5.7`. - -Twill's database migrations create `json` columns. Your database should support the `json` type. Twill has been developed and tested against MySQL (`>=5.7`) and PostgreSQL(`>=9.3`). - -In summary: - -| | Supported versions | Recommended version | -|:-----------|:------------------:|:-------------------:| -| PHP | >= 7.1 | 7.4 | -| Laravel | >= 5.6 | 8 | -| npm | >= 5.7 | 6.13 | -| MySQL | >= 5.7 | 5.7 | -| PostgreSQL | >= 9.3 | 10 | diff --git a/docs/.sections/media-library.md b/docs/.sections/media-library.md deleted file mode 100644 index 23c865fc3..000000000 --- a/docs/.sections/media-library.md +++ /dev/null @@ -1,124 +0,0 @@ -## Media library -![screenshot](/docs/_media/medialibrary.png) - -### Storage provider -The media and files libraries currently support S3, Azure and local storage. Head over to the `twill` configuration file to setup your storage disk and configurations. Also check out the direct upload section of this documentation to setup your IAM users and bucket / container if you want to use S3 or Azure as a storage provider. - -### Image rendering service -This package currently ships with 3 rendering services, [Imgix](https://www.imgix.com/), [Glide](http://glide.thephpleague.com/) and a local minimalistic rendering service. It is very simple to implement another one like [Cloudinary](http://cloudinary.com/) or even another local service like or [Croppa](https://github.com/BKWLD/croppa). -Changing the image rendering service can be done by changing the `MEDIA_LIBRARY_IMAGE_SERVICE` environment variable to one of the following options: -- `A17\Twill\Services\MediaLibrary\Glide` -- `A17\Twill\Services\MediaLibrary\Imgix` -- `A17\Twill\Services\MediaLibrary\Local` - -For a custom image service you would have to implement the `ImageServiceInterface` and modify your `twill` configuration value `media_library.image_service` with your implementation class. -Here are the methods you would have to implement: - -```php - - - - https://YOUR_ADMIN_DOMAIN - http://YOUR_ADMIN_DOMAIN - POST - PUT - DELETE - 3000 - ETag - * - - -``` - -### Imgix and local uploads - -When setting up an Imgix source for local uploads, choose the `Web Folder` source type and specify your domain in the `Base URL` settings. - -![screenshot](/docs/_media/imgix_source.png) diff --git a/docs/.sections/other-cms-features.md b/docs/.sections/other-cms-features.md deleted file mode 100644 index 2449aada7..000000000 --- a/docs/.sections/other-cms-features.md +++ /dev/null @@ -1,469 +0,0 @@ -## Dashboard - -Once you have created and configured multiple CRUD modules in your Twill's admin console, you can configure Twill's dashboard in `config/twill.php`. - -For each module that you want to enable in a part or all parts of the dashboad, add an entry to the `dashboard.modules` array, like in the following example: - -```php -return [ - 'dashboard' => [ - 'modules' => [ - 'projects' => [ // module name if you added a morph map entry for it, otherwise FQCN of the model (eg. App\Models\Project) - 'name' => 'projects', // module name - 'label' => 'projects', // optional, if the name of your module above does not work as a label - 'label_singular' => 'project', // optional, if the automated singular version of your name/label above does not work as a label - 'routePrefix' => 'work', // optional, if the module is living under a specific routes group - 'count' => true, // show total count with link to index of this module - 'create' => true, // show link in create new dropdown - 'activity' => true, // show activities on this module in actities list - 'draft' => true, // show drafts of this module for current user - 'search' => true, // show results for this module in global search - ], - ... - ], - ... - ], - ... -]; -``` - -You can also enable a Google Analytics module: - -```php -return [ - 'dashboard' => [ - ..., - 'analytics' => [ - 'enabled' => true, - 'service_account_credentials_json' => storage_path('app/analytics/service-account-credentials.json'), - ], - ], - ... -]; -``` - -It is using Spatie's [Laravel Analytics](https://github.com/spatie/laravel-analytics) package. - -Follow [Spatie's documentation](https://github.com/spatie/laravel-analytics#how-to-obtain-the-credentials-to-communicate-with-google-analytics) to setup a Google service account and download a json file containing your credentials, and provide your Analytics view ID using the `ANALYTICS_VIEW_ID` environment variable. - -## Global search - -By default, Twill's global search input is always available in the dashboard and behind the top-right search icon on other Twill's screens. By default, the search input performs a LIKE query on the title attribute only. If you like, you can specify a custom list of attributes to search for in each dashboard enabled module: - -```php -return [ - 'dashboard' => [ - 'modules' => [ - 'projects' => [ - 'name' => 'projects', - 'routePrefix' => 'work', - 'count' => true, - 'create' => true, - 'activity' => true, - 'draft' => true, - 'search' => true, - 'search_fields' => ['name', 'description'] - ], - ... - ], - ... - ], - ... -]; -``` - -You can also customize the endpoint to handle search queries yourself: - -```php -return [ - 'dashboard' => [ - ..., - 'search_endpoint' => 'your.custom.search.endpoint.route.name', - ], - ... -]; -``` - -You will need to return a collection of values, like in the following example: - -```php -return $searchResults->map(function ($item) use ($module) { - try { - $author = $item->revisions()->latest()->first()->user->name ?? 'Admin'; - } catch (\Exception $e) { - $author = 'Admin'; - } - - return [ - 'id' => $item->id, - 'href' => moduleRoute($moduleName['name'], $moduleName['routePrefix'], 'edit', $item->id), - 'thumbnail' => $item->defaultCmsImage(['w' => 100, 'h' => 100]), - 'published' => $item->published, - 'activity' => 'Last edited', - 'date' => $item->updated_at->toIso8601String(), - 'title' => $item->title, - 'author' => $author, - 'type' => Str::singular($module['name']), - ]; -})->values(); - -``` - -## Featuring content -Twill's buckets allow you to provide publishers with featured content management screens. You can add multiple pages of buckets anywhere you'd like in your CMS navigation and, in each page, multiple buckets with different rules and accepted modules. In the following example, we will assume that our application has a Guide model and that we want to feature guides on the homepage of our site. Our site's homepage has multiple zones for featured guides: a primary zone, that shows only one featured guide, and a secondary zone, that shows guides in a carousel of maximum 10 items. - -First, you will need to enable the buckets feature. In `config/twill.php`: -```php -'enabled' => [ - 'buckets' => true, -], -``` - -Then, define your buckets configuration: - -```php -'buckets' => [ - 'homepage' => [ - 'name' => 'Home', - 'buckets' => [ - 'home_primary_feature' => [ - 'name' => 'Home primary feature', - 'bucketables' => [ - [ - 'module' => 'guides', - 'name' => 'Guides', - 'scopes' => ['published' => true], - ], - ], - 'max_items' => 1, - ], - 'home_secondary_features' => [ - 'name' => 'Home secondary features', - 'bucketables' => [ - [ - 'module' => 'guides', - 'name' => 'Guides', - 'scopes' => ['published' => true], - ], - ], - 'max_items' => 10, - ], - ], - ], -], -``` - -You can allow mixing modules in a single bucket by adding more modules to the `bucketables` array. -Each `bucketable` should have its [model morph map](https://laravel.com/docs/5.5/eloquent-relationships#polymorphic-relations) defined because features are stored in a polymorphic table. -In your AppServiceProvider, you can do it like the following: - -```php -use Illuminate\Database\Eloquent\Relations\Relation; -... -public function boot() -{ - Relation::morphMap([ - 'guides' => 'App\Models\Guide', - ]); -} -``` - -Finally, add a link to your buckets page in your CMS navigation: - -```php -return [ - 'featured' => [ - 'title' => 'Features', - 'route' => 'admin.featured.homepage', - 'primary_navigation' => [ - 'homepage' => [ - 'title' => 'Homepage', - 'route' => 'admin.featured.homepage', - ], - ], - ], - ... -]; -``` - -By default, the buckets page (in our example, only homepage) will live under the /featured prefix. -But you might need to split your buckets page between sections of your CMS. For example if you want to have the homepage bucket page of our example under the /pages prefix in your navigation, you can use another configuration property: - -```php -'bucketsRoutes' => [ - 'homepage' => 'pages' -] -``` - -## Settings sections -Settings sections are standalone forms that you can add to your Twill's navigation to give publishers the ability to manage simple key/value records for you to then use anywhere in your application codebase. - -Start by enabling the `settings` feature in your `config/twill.php` configuration file `enabled` array. See [Twill's configuration documentation](#enabled-features) for more information. - -If you did not enable this feature before running the `twill:install` command, you need to copy the migration in `vendor/area17/twill/migrations/create_settings_table.php` to your own `database/migrations` directory and migrate your database before continuing. - -To create a new settings section, add a blade file to your `resources/views/admin/settings` folder. The name of this file is the name of your new settings section. - -In this file, you can use `@formField('input')` Blade directives to add new settings. The name attribute of each form field is the name of a setting. Wrap them like in the following example: - -```php -@extends('twill::layouts.settings') - -@section('contentFields') - @formField('input', [ - 'label' => 'Site title', - 'name' => 'site_title', - 'textLimit' => '80' - ]) -@stop -``` - -If your `translatable.locales` configuration array contains multiple language codes, you can enable the `translated` option on your settings input form fields to make them translatable. - -At this point, you want to add an entry in your `config/twill-navigation.php` configuration file to show the settings section link: - -```php -return [ - ... - 'settings' => [ - 'title' => 'Settings', - 'route' => 'admin.settings', - 'params' => ['section' => 'section_name'], - 'primary_navigation' => [ - 'section_name' => [ - 'title' => 'Section name', - 'route' => 'admin.settings', - 'params' => ['section' => 'section_name'] - ], - ... - ] - ], -]; -``` - -Each Blade file you create in `resources/views/admin/settings` creates a new section available for you to add in the `primary_navigation` array of your `config/twill-navigation.php` file. - -You can then retrieve the value of a specific setting by its key, which is the name of the form field you defined in your settings form, either by directly using the `A17\Twill\Models\Setting` Eloquent model or by using the provided `byKey` helper in `A17\Twill\Repositories\SettingRepository`: - -```php -byKey('site_title'); -app(SettingRepository::class)->byKey('site_title', 'section_name'); -``` - -## Custom CMS pages -Twill includes the ability to create fully custom pages that includes your navigation, by extending the `twill::layouts.free` layout in a view located in your `resources/views/admin` folder. - -#### Example -- Create a route in `routes/admin.php` - -```php - Route::name('customPage')->get('/customPage', 'MockController@show'); -``` - -- Add a link to your page in `config/twill-navigation.php` - -```php -return [ - ... - 'customPage' => [ - 'title' => 'Custom page', - 'route' => 'admin.customPage', - ], - ... -]; -``` - -- Add a controller to handle the request - -```php -namespace App\Http\Controllers\Admin; - -class MockController -{ - public function show() - { - return view('admin.customPage'); - } -} -``` - -- And create the view - -```php -@extends('twill::layouts.free') - -@section('customPageContent') - CUSTOM CONTENT GOES HERE -@stop -``` - -You can use Twill's Vue components if you need on those custom pages, for example: - -```php -@extends('twill::layouts.free') - -@section('customPageContent') - - - - -
-
- -
-
- -
-
- - - - - - -
- Button variant: validate -@stop -``` - - -## User management -Authentication and authorization are provided by default in Laravel. This package simply leverages what Laravel provides and configures the views for you. By default, users can login at `/login` and can also reset their password through that same screen. New users have to reset their password before they can gain access to the admin application. By using the twill configuration file, you can change the default redirect path (`auth_login_redirect_path`) and send users to anywhere in your application following login. - -#### Roles -The package currently provides three different roles: -- view only -- publisher -- admin - -#### Permissions -Default permissions are as follows. To learn how permissions can be modified or extended, see the next section. - -View only users are able to: -- login -- view CRUD listings -- filter CRUD listings -- view media/file library -- download original files from the media/file library -- edit their own profile - -Publishers have the same permissions as view only users plus: -- full CRUD permissions -- publish -- sort -- feature -- upload new images/files to the media/file library - -Admin users have the same permissions as publisher users plus: -- full permissions on users - -There is also a super admin user that can impersonate other users at `/users/impersonate/{id}`. The super admin can be a useful tool for testing features with different user roles without having to logout/login manually, as well as for debugging issues reported by specific users. You can stop impersonating by going to `/users/impersonate/stop`. - -#### Extending user roles and permissions -You can create or modify new permissions for existing roles by using the Gate façade in your `AuthServiceProvider`. The `can` middleware, provided by default in Laravel, is very easy to use, either through route definition or controller constructor. - -To create new user roles, you could extend the default enum UserRole by overriding it using Composer autoloading. In `composer.json`: - -```json - "autoload": { - "classmap": [ - "database/seeds", - "database/factories" - ], - "psr-4": { - "App\\": "app/" - }, - "files": ["app/Models/Enums/UserRole.php"], - "exclude-from-classmap": ["vendor/area17/twill/src/Models/Enums/UserRole.php"] - } -``` - -In `app/Models/Enums/UserRole.php` (or anywhere else you'd like actually, only the namespace needs to be the same): - -```php - role_value, [ - UserRole::CUSTOM1, - UserRole::CUSTOM2, - UserRole::ADMIN, - ]); - }); - - Gate::define('edit', function ($user) { - return in_array($user->role_value, [ - UserRole::CUSTOM3, - UserRole::ADMIN, - ]); - }); - - Gate::define('custom-permission', function ($user) { - return in_array($user->role_value, [ - UserRole::CUSTOM2, - UserRole::ADMIN, - ]); - }); - } - } -``` - -You can use your new permission and existing ones in many places like the `twill-navigation` configuration using `can`: - -```php - 'projects' => [ - 'can' => 'custom-permission', - 'title' => 'Projects', - 'module' => true, - ], -``` - -Also in forms blade files using `@can`, as well as in middleware definitions in routes or controllers, see [Laravel's documentation](https://laravel.com/docs/5.7/authorization#via-middleware) for more info. - -You should follow the Laravel documentation regarding [authorization](https://laravel.com/docs/5.3/authorization). It's pretty good. Also if you would like to bring administration of roles and permissions to the admin application, [spatie/laravel-permission](https://github.com/spatie/laravel-permission) would probably be your best friend. - -## OAuth login - -You can enable the `twill.enabled.users-oauth` feature to let your users login to the CMS using a third party service supported by Laravel Socialite. -By default, `twill.oauth.providers` only has `google`, but you are free to change it or add more services to it. -In the case of using Google, you would of course need to provide the following environment variables: - -``` -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= -GOOGLE_CALLBACK_URL=https://admin.twill-based-cms.com/login/oauth/callback/google -``` - diff --git a/docs/.sections/preface.md b/docs/.sections/preface.md deleted file mode 100644 index cb897a591..000000000 --- a/docs/.sections/preface.md +++ /dev/null @@ -1,202 +0,0 @@ -## Preface - -### About Twill - -Twill is an open source Laravel package that helps developers rapidly create a custom CMS that is beautiful, powerful, and flexible. By standardizing common functions without compromising developer control, Twill makes it easy to deliver a feature-rich admin console that focuses on modern publishing needs. - -Twill is an [AREA 17](https://area17.com) product. It was crafted with the belief that content management should be a creative, productive, and enjoyable experience for both publishers and developers. - -### Benefits overview - -With Twill's vast number of pre-built features and associated library of Vue.js UI components, developers can focus their efforts on the unique aspects of their applications instead of rebuilding standard ones. - -Built to get out of your way, Twill offers: -- No lock-in, create your own data models or hook existing ones -- No front-end assumptions, use it within your Laravel app or as a headless CMS -- No bloat, turn off features you don’t need -- No need to write/adapt HTML for the admin UI -- No limits, extend as you see fit - - -### Feature list - -#### CRUD modules -* Enhanced Laravel “resources” models -* Command line generator and conventions to speed up creating new ones -* Based on PHP traits and regular Laravel concepts (migrations, models, controllers, form requests, repositories, Blade views) -* Fully custom forms per content type -* Slug management, including the ability to automatically redirect old urls -* Configurable content listings with searching, filtering, sorting, publishing, featuring, reordering and more -* Support for all Eloquent ORM relationships (1-1, 1-n, n-n, polymorphic) -* Content versioning - -#### UI Components -* Large library of plugged-in Vue.js form components with tons of options for maximum flexibility and composition -* Completely abstracted HTML markup. You’ll never have to deal with Bootstrap HTML again, which means you won’t ever have to maintain frontend-related code for your CMS -* Input, text area, rich text area form fields with option to set SEO optimized limits -* Configurable WYSIWYG built with Quill.js -* Inline translated fields with independent publication status (no duplication) -* Select, multi-select, content type browsers for related content and tags -* Form repeaters -* Date and color pickers -* Flexible content block editor (dynamically composable from all form components) -* Custom content blocks per content type - -#### Media library -* Media/files library with S3 and imgix integration (3rd party services are swappable) -* Image selector with smart cropping -* Ability to set custom image requirements and cropping parameters per content type -* Multiple crops possible per image for art directed responsive -* Batch uploading and tagging -* Metadata editing (alternative text, caption) -* Multi fields search (filename, alternative text, tags, dimensions…) - -#### Configuration based features -* User authentication, authorization and management -* Fully configurable CMS navigation, with three levels of hierarchy and breadcrumbs for limitless content structure -* Configurable CMS dashboard with quick access links, activity log and Google Analytics integration -* Configurable CMS global search -* Intuitive content featuring, using a bucket UI. Put any of your content types in "buckets" to manage any layout of featured content or other concepts like localization - -#### Developer experience -* Maintain a Laravel application, not a Twill application -* Support for Laravel 5.6 and up – Twill will be updated to support all future versions -* Support for both MySQL and PostgreSQL databases -* No conflict with other Laravel packages – keep building with your tools of choice -* No specific server requirements, if you can deploy a Laravel application, you can deploy Twill -* Development and production ready toolset (debug bar, inspector, exceptions handler) -* No data lock in – all Twill content types are proper relational database tables, so it’s easy to move to Twill from other solutions and to expose content created with your Twill CMS to other applications -* Previewing and side by side comparison of fully rendered frontend site that you’ll get up and running very quickly no matter how you built your frontend (fully headed Laravel app, hybrid Laravel app with your own custom API endpoints or even full SPA with frameworks like React or Vue) -* Scales to very large amount of content without performance drawbacks, even on minimal resources servers (for what it’s worth, it’s running perfectly fine on a $5/month VPS, and you can cache frontend pages if you’d like through packages like laravel-response-cache or a CDN like Cloudfront) - -### Architecture concepts - -#### CRUD modules - -A Twill [CRUD module](#crud-modules-3) is a set of classes and configurations in your Laravel application that enable your publishers to manage a certain type of content. The structure of a CRUD module is completely up to you. - -Another way to think of a CRUD module is as a feature rich Laravel resource. In other words (and for the non-Laravel developer), a CRUD module is basically a content type (or post type, as sometimes called by other CMS solutions) with CRUD operations (Create, Read, Update, Delete), as well as custom Twill-provided operations like: Publish, Feature, Tag, Preview, Restore, Restore revision, Reorder or Bulk edit. Using Twill's media library, images and files can be attached to modules records. Also, using Twill's block editor, a rich editing experience of a module's record can be offered to publishers. - -In Twill's UI, a CRUD module most often consists of a listing page and a form page or modal. Records created under a module can then be associated with other modules' records to create relationships between your content. Records of your Twill modules and any associations are stored in a traditional relational database schema, following Laravel's migrations and Eloquent model conventions. - -Twill's CRUD modules features are enabled using PHP traits you include in your Eloquent models and Twill repositories, as well as various configuration variables, and a bunch of conventions to follow. Further guidance is documented in the [CRUD modules](#crud-modules-3) section. - -A Twill module can be modified however you like – you can include countless types of content fields, and change the organization and structure according to the needs of the module and your product. Setup is simple: you just need to compose a form using all of Twill's available form fields. - -#### Recommended CRUD content types - -While possibilities for composition are endless, we’ve identified four standard content types: - -- Entities: Entities are your primary data models, usually represented on your frontend as listing and detail views. Generally speaking, entity listings are displayed programmatically (e.g., by date, price, etc.) but also can be manually ordered. For example, if you’re building an editorial site, your primary entity might be articles. If you’re building a site to showcase your company’s work, you might have entities for projects, case studies, people, etc. This is the default behavior of a Twill module. - -- Attributes: Attributes are secondary data models most often used to add structured details to an entity (for search, filtering, and/or display). Example attributes include: categories, types, sectors, industries, etc. In a Twill CMS, each attribute needs a listing screen and, within that screen, quick creation and editing ability. As attributes tend to be relatively simple (few content fields, etc), their form screen can often fit within a modal. This modal can be made available from other parts of the CMS rather than only from their own listing screen. In Twill, the `editInModal` index option of your module's controllers can be used to enable that behavior. - -- Pages: Pages are unstructured data models most often used for static content, such as an About page. Rather than being separated into listing and detail screens, pages are manually organized into parent/child relationships. Combined with the [kalnoy/nestedset](https://github.com/lazychaser/laravel-nestedset) package, a Twill module can be configured to show and make parent/child relationships manageable on a module's records. - -- Elements: Elements are modules or snippets of content that are added to an entity, page, or screen. Examples include the ability to manage footer text or create a global alert that can be turned on/off, etc. Twill offers developers the ability to quickly create [settings sections](#settings-sections) to manage elements. A Twill module could also be configured to manage any sort of standalone element or content composition. There's nothing wrong with having a database table with a single record if that is what your product require, so you should feel free to create a Twill module to have a custom form for a single record. You can use a Laravel seeder or migration to initialize your admin console with those records. - -#### CRUD listings - -One of the benefits of Twill is the ability to fully customize CRUD listing views. At minimum, you’ll want to include the key information for each data record so that publishers can have an at-a-glance view without having to click into a record. You can also set up a default view and give each publisher the ability to customize the columns and the number of records per pagination page. - -In certain cases, you may require nested CRUD modules. For example, if you are building a handbook website, the parent CRUD would be the handbooks and then within each handbook there are pages (child CRUD). In this case, the listing will be the parent CRUD and for each record, you’d include a column to access the child CRUDs for each. - -#### CMS navigation - -One of the benefits of Twill is the ability to fully customize the navigation as needed to make it easy and intuitive for publishers to navigate through the CMS and perform their regular production duties. Twill has three levels of navigation: - -- Main navigation: we recommend that the main navigation reflects the frontend organization, in that way, it is intuitive for publishers. Additionally, the main navigation includes transversal items such as media library and global settings. - -- Secondary navigation: we recommend that you group all entities, attributes, pages, and possibly buckets (see below) under each main navigation item. For example, if you have a section called “Our work” then the secondary navigation will include: case studies (entity), sectors (attribute), how we work (page), featured (buckets), etc. - -- Tertiary navigation: in certain cases, you will need a third level of navigation, however we recommend that you only use it when absolutely necessary, otherwise content may be too buried. You also have the option to turn the tertiary navigation into a breadcrumb. - - -#### Block editor - -Central to the Twill experience is the block editor, giving publishers full control of how they construct the content of a record. A block is a composition of form fields made available to publshers in Twill's block editor form field. - -Generally speaking, with a standard CMS, all content is managed through fixed forms. While in a Twill CMS some of the content may be fixed (such as title, subtitle, intro, required content, etc.), when using the block editor, the content is constructed by adding and reordering blocks of content. This gives you maximum flexibility to build narrative experiences on the front end. - -For example, let’s say you’re building a blog. Your blog post form may require fixed content such as the title, short description, author, etc. But then you can use the block editor for the body of the post, allowing the publisher to add standardized blocks for text, images, quotes, slideshows, videos, related content, embeds, etc. and reorder them as needed. - -A block can include any combination of fields, including repeater fields and even data pulled from a third party service. Each block also can contain additional options so that a single block can be displayed according to different variations. This obviates the need to create a new block every time you need a different display of your content, and allows you to match the build of the page to the content, context or design required. For example, you can have a media block that may alternatively include a video or an image, be displayed at small, medium or large, or displayed inline with content or full screen. - -To keep page-building as simple as possible, we recommend that you keep blocks to a minimum – ideally no more than 8 blocks, if possible. When adding a new block, consider: is this a unique block or simply block options? Publishers will prefer switching an option using existing content rather than having to create another block and copy and paste. - - -It is also important that you work with a designer early on to discuss the block strategy and make sure your content works well no matter how your publishers arrange it. Can all the blocks work in any combination or are there restrictions? If the latter, you can create form validations to block publishers from arranging blocks in certain contexts. - -#### Buckets - -Buckets are used to feature content. While the name might be boring, your publishers will love them! - -The functionality is made up of two parts: an entity navigator and buckets. The entity navigator gives access to the entities, including search and filters. Buckets represent your feature areas. For example, let’s say you have a homepage with main features (such as a hero display pointing your users to 2-3 pages), secondary features (such as a grid of content), and tertiary features. You would create three buckets for each of these feature sections. Then, your publishers can simply drag the desired entity to the bucket they want it featured in. - -You can also associate rules for your buckets. For example, let’s say you only want three main features and five secondary features – but unlimited tertiary features. You can add those restrictions and when the publishers try to add more than the limit, they will be informed they need to remove an entity before they can add another. - - -While buckets are primarily used for featuring, they can also be used for any purpose. For example, if you have a website that has different navigation for different market locations (e.g. USA, Europe, Asia), you can use buckets to manage this. - - - -### Credits - -Over the last 15 years, nearly every engineer at AREA 17 has contributed to Twill in some capacity. The current iteration of Twill as an open source initiative was created by: - -- [Quentin Renard](https://area17.com/about/quentin-renard), lead application engineer -- [Antoine Doury](https://area17.com/about/antoine-doury), lead interface engineer -- [Antonin Caudron](https://area17.com/about/antonin-caudron), interface engineer -- [Martin Rettenbacher](https://area17.com/about/martin-rettenbacher), product designer -- [Jesse Golomb](https://area17.com/about/jesse-golomb), product owner -- [George Eid](https://area17.com/about/george-eid), product manager - -Additional contributors include [Laurens van Heems](https://area17.com/about/laurens-van-heems), [Fernando Petrelli](https://area17.com/about/fernando-petrelli), [Gilbert Moufflet](https://area17.com/about/gilbert-moufflet), [Mubashar Iqbal](https://area17.com/about/mubashar-iqbal), [Pablo Barrios](https://area17.com/about/pablo-barrios), [Luis Lavena](https://area17.com/about/luis-lavena), and [Mike Byrne](https://area17.com/about/mike-byrne). - -### Contribution guide - -#### Code of Conduct -Twill is dedicated to building a welcoming, diverse, safe community. We expect everyone participating in the Twill community to abide by our [Code of Conduct](https://github.com/area17/twill/blob/main/CODE_OF_CONDUCT.md). Please read it. Please follow it. - -#### Bug reports and features submission -To submit an issue or request a feature, please do so on [Github](https://github.com/area17/twill/issues). - -If you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a bug report is to make it easy for yourself - and others - to replicate the bug and develop a fix. - -Remember, bug reports are created in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the bug report will automatically see any activity or that others will jump to fix it. Creating a bug report serves to help yourself and others start on the path of fixing the problem. - -#### Security vulnerabilities -If you discover a security vulnerability within Twill, please email us at [security@twill.io](mailto:security@twill.io). All security vulnerabilities will be promptly addressed. - -#### Versioning scheme - -Twill follows [Semantic Versioning](https://semver.org/). Major releases are released only when breaking changes are necessary, while minor and patch releases may be released as often as every week. Minor and patch releases should never contain breaking changes. - -When referencing Twill from your application, you should always use a version constraint such as `^2.0`, since major releases of Twill do include breaking changes. - -#### Which branch? -All bug fixes should be sent to the latest stable branch (`2.x`). Bug fixes should never be sent to the `main` branch unless they fix features that exist only in the upcoming release. - -Minor features that are fully backwards compatible with the current Twill release may be sent to the latest stable branch (`2.x`). - -Major new features should always be sent to the `main` branch, which contains the upcoming Twill release. - -Please send coherent history — make sure each individual commit in your pull request is meaningful. If you had to make a lot of intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. - -#### Coding style -- PHP: [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). - -- Javascript: [Standard](https://standardjs.com/), [Vue ESLint Essentials](https://github.com/vuejs/eslint-plugin-vue). - -### Licensing -#### Software -The Twill software is licensed under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). - -#### User interface -The Twill UI, including but not limited to images, icons, patterns, and derivatives thereof are licensed under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). - -#### Attribution -By using the Twill UI, you agree that any application which incorporates it shall prominently display the message “Made with Twill” in a legible manner in the footer of the admin console. This message must open a link to Twill.io when clicked or touched. For permission to remove the attribution, contact us at [hello@twill.io](mailto:hello@twill.io). - -### 1.x documentation -Documentation for Twill versions below 2.0 is available for reference [here](/docs/1.x). diff --git a/docs/generate_readme.js b/docs/generate_readme.js deleted file mode 100644 index d3d0205db..000000000 --- a/docs/generate_readme.js +++ /dev/null @@ -1,29 +0,0 @@ -const fs = require('fs'); - -const sections = [ - 'preface', - 'getting-started/environment-requirements', - 'getting-started/installation', - 'getting-started/configuration', - 'getting-started/navigation', - 'crud-modules', - 'form-fields', - 'block-editor', - 'media-library', - 'artisan', - 'other-cms-features', - 'resources', - 'api-documentation' -]; - -const settings = `--- -sidebar: auto -pageClass: twill-doc -title: Documentation ----`; - -const content = settings + sections.map((section) => { - return '\n\n' + fs.readFileSync('./.sections/' + section + '.md', 'utf8') + '\n\n'; -}).join(''); - -fs.writeFileSync('README.md', content); diff --git a/docs/package-lock.json b/docs/package-lock.json index 399bde520..1a9557c9a 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -2908,135 +2908,6 @@ "typedarray": "^0.0.6" } }, - "concurrently": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.1.0.tgz", - "integrity": "sha512-9ViZMu3OOCID3rBgU31mjBftro2chOop0G2u1olq1OuwRBVRw/GxHTg80TVJBUTJfoswMmEUeuOg1g1yu1X2dA==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "date-fns": "^2.0.1", - "lodash": "^4.17.15", - "read-pkg": "^4.0.1", - "rxjs": "^6.5.2", - "spawn-command": "^0.0.2-1", - "supports-color": "^6.1.0", - "tree-kill": "^1.2.2", - "yargs": "^13.3.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, "configstore": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", @@ -3583,12 +3454,6 @@ "assert-plus": "^1.0.0" } }, - "date-fns": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.10.0.tgz", - "integrity": "sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA==", - "dev": true - }, "de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -4160,15 +4025,6 @@ "safe-buffer": "^5.1.1" } }, - "exec-sh": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", - "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", - "dev": true, - "requires": { - "merge": "^1.2.0" - } - }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -4936,12 +4792,6 @@ } } }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -5922,12 +5772,6 @@ "readable-stream": "^2.0.1" } }, - "merge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", - "dev": true - }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -6249,18 +6093,6 @@ "abbrev": "1" } }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7499,17 +7331,6 @@ "strip-json-comments": "~2.0.1" } }, - "read-pkg": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", - "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", - "dev": true, - "requires": { - "normalize-package-data": "^2.3.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0" - } - }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -7795,15 +7616,6 @@ "aproba": "^1.1.1" } }, - "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -8300,44 +8112,6 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, - "spawn-command": { - "version": "0.0.2-1", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", - "dev": true - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, "spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -8894,12 +8668,6 @@ "punycode": "^2.1.1" } }, - "tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true - }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", @@ -9302,16 +9070,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -9519,16 +9277,6 @@ "smoothscroll-polyfill": "^0.4.3" } }, - "watch": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz", - "integrity": "sha1-NApxe952Vyb6CqB9ch4BR6VR3ww=", - "dev": true, - "requires": { - "exec-sh": "^0.2.0", - "minimist": "^1.2.0" - } - }, "watchpack": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", diff --git a/docs/package.json b/docs/package.json index 8b30e6e14..6313fa2c4 100755 --- a/docs/package.json +++ b/docs/package.json @@ -1,13 +1,9 @@ { "name": "twill-docs", "scripts": { - "dev": "concurrently \"watch 'node generate_readme.js' .sections --wait=2 --interval=0.1\" \"vuepress dev\"", - "build": "node generate_readme.js && vuepress build", - "prod": "npm run build && mkdir -p .vuepress/docs/api && cp -R ../docs-api/build/* .vuepress/docs/api" - }, - "devDependencies": { - "concurrently": "5.1.0", - "watch": "^1.0.2" + "dev": "vuepress dev src", + "build": "vuepress build src", + "prod": "npm run build && mkdir -p docs/api && cp -R ../docs-api/build/* docs/api" }, "dependencies": { "@vuepress/plugin-google-analytics": "1.3.1", diff --git a/docs/.vuepress/config.js b/docs/src/.vuepress/config.js similarity index 63% rename from docs/.vuepress/config.js rename to docs/src/.vuepress/config.js index 4ce65f7ec..5dfccf2dd 100644 --- a/docs/.vuepress/config.js +++ b/docs/src/.vuepress/config.js @@ -2,18 +2,18 @@ module.exports = { title: 'Twill', description: 'Twill — An open source CMS toolkit for Laravel', base: "/docs/", - dest: ".vuepress/docs", + dest: "docs", head: [ - ['link', { rel: 'shortcut icon', href: 'favicon.ico' }], - ['link', { rel: 'apple-touch-icon', href: 'favicon-192.png' }], + ['link', { rel: 'shortcut icon', href: '/favicon.ico' }], + ['link', { rel: 'apple-touch-icon', href: '/favicon-192.png' }], ['meta', { name: 'theme-color', content: '#000000' }], ['meta', { property: 'og:url', content: 'https://twill.io/' }], ['meta', { name: 'twitter:url', content: 'https://twill.io/' }], - ['meta', { property: 'og:image', content: 'social_share.png' }], + ['meta', { property: 'og:image', content: '/social_share.png' }], ['meta', { property: 'og:image:width', content: '1200' }], ['meta', { property: 'og:image:height', content: '630' }], - ['meta', { name: 'twitter:image', content: 'social_share.png' }], - ['meta', { itemprop: 'image', content: 'social_share.png' }], + ['meta', { name: 'twitter:image', content: '/social_share.png' }], + ['meta', { itemprop: 'image', content: '/social_share.png' }], ['meta', { property: 'og:site_name', content: 'Twill' }], ['meta', { property: 'og:author', content: 'https://www.facebook.com/twillcms/' }], ['meta', { name: 'twitter:card', content: 'summary_large_image' }], @@ -22,13 +22,20 @@ module.exports = { ['meta', { name: 'twitter:creator', content: '@twillcms' }] ], themeConfig: { + docsRepo: 'area17/twill', + docsDir: 'docs/src', + docsBranch: '2.x', + editLinks: true, + editLinkText: 'Edit this page on GitHub', algolia: { - apiKey: 'ef9b333ded0418b96b32e189b183a94e', - indexName: 'twill' + apiKey: '9360cb12e45076d95f77b63549021a6d', + indexName: 'twill', + appId: '89HNJPXALF', }, nav: [ { text: 'GitHub', link: 'https://github.com/area17/twill' }, - ] + ], + sidebar: require('./sidebar.js'), }, plugins: [ ['@vuepress/google-analytics', { diff --git a/docs/.vuepress/enhanceApp.js b/docs/src/.vuepress/enhanceApp.js similarity index 67% rename from docs/.vuepress/enhanceApp.js rename to docs/src/.vuepress/enhanceApp.js index 325ccf5d7..1189c1d42 100644 --- a/docs/.vuepress/enhanceApp.js +++ b/docs/src/.vuepress/enhanceApp.js @@ -2,7 +2,7 @@ export default ({ Vue }) => { Vue.mixin({ computed: { $title () { - return 'Documentation – Twill' + return this.$page.title + ' – Twill' } } }) diff --git a/docs/.vuepress/public/Inter-Medium.woff b/docs/src/.vuepress/public/Inter-Medium.woff similarity index 100% rename from docs/.vuepress/public/Inter-Medium.woff rename to docs/src/.vuepress/public/Inter-Medium.woff diff --git a/docs/.vuepress/public/Inter-Medium.woff2 b/docs/src/.vuepress/public/Inter-Medium.woff2 similarity index 100% rename from docs/.vuepress/public/Inter-Medium.woff2 rename to docs/src/.vuepress/public/Inter-Medium.woff2 diff --git a/docs/.vuepress/public/Inter-Regular.woff b/docs/src/.vuepress/public/Inter-Regular.woff similarity index 100% rename from docs/.vuepress/public/Inter-Regular.woff rename to docs/src/.vuepress/public/Inter-Regular.woff diff --git a/docs/.vuepress/public/Inter-Regular.woff2 b/docs/src/.vuepress/public/Inter-Regular.woff2 similarity index 100% rename from docs/.vuepress/public/Inter-Regular.woff2 rename to docs/src/.vuepress/public/Inter-Regular.woff2 diff --git a/docs/.vuepress/public/_media/a17_black.svg b/docs/src/.vuepress/public/_media/a17_black.svg similarity index 100% rename from docs/.vuepress/public/_media/a17_black.svg rename to docs/src/.vuepress/public/_media/a17_black.svg diff --git a/docs/.vuepress/public/_media/a17_white.svg b/docs/src/.vuepress/public/_media/a17_white.svg similarity index 100% rename from docs/.vuepress/public/_media/a17_white.svg rename to docs/src/.vuepress/public/_media/a17_white.svg diff --git a/docs/.vuepress/public/_media/blockeditor.png b/docs/src/.vuepress/public/_media/blockeditor.png similarity index 100% rename from docs/.vuepress/public/_media/blockeditor.png rename to docs/src/.vuepress/public/_media/blockeditor.png diff --git a/docs/.vuepress/public/_media/browser.png b/docs/src/.vuepress/public/_media/browser.png similarity index 100% rename from docs/.vuepress/public/_media/browser.png rename to docs/src/.vuepress/public/_media/browser.png diff --git a/docs/.vuepress/public/_media/checkbox.png b/docs/src/.vuepress/public/_media/checkbox.png similarity index 100% rename from docs/.vuepress/public/_media/checkbox.png rename to docs/src/.vuepress/public/_media/checkbox.png diff --git a/docs/.vuepress/public/_media/checkboxes.png b/docs/src/.vuepress/public/_media/checkboxes.png similarity index 100% rename from docs/.vuepress/public/_media/checkboxes.png rename to docs/src/.vuepress/public/_media/checkboxes.png diff --git a/docs/.vuepress/public/_media/color.png b/docs/src/.vuepress/public/_media/color.png similarity index 100% rename from docs/.vuepress/public/_media/color.png rename to docs/src/.vuepress/public/_media/color.png diff --git a/docs/.vuepress/public/_media/datepicker.png b/docs/src/.vuepress/public/_media/datepicker.png similarity index 100% rename from docs/.vuepress/public/_media/datepicker.png rename to docs/src/.vuepress/public/_media/datepicker.png diff --git a/docs/.vuepress/public/_media/files.png b/docs/src/.vuepress/public/_media/files.png similarity index 100% rename from docs/.vuepress/public/_media/files.png rename to docs/src/.vuepress/public/_media/files.png diff --git a/docs/.vuepress/public/_media/imgix_source.png b/docs/src/.vuepress/public/_media/imgix_source.png similarity index 100% rename from docs/.vuepress/public/_media/imgix_source.png rename to docs/src/.vuepress/public/_media/imgix_source.png diff --git a/docs/.vuepress/public/_media/input.png b/docs/src/.vuepress/public/_media/input.png similarity index 100% rename from docs/.vuepress/public/_media/input.png rename to docs/src/.vuepress/public/_media/input.png diff --git a/docs/.vuepress/public/_media/map.png b/docs/src/.vuepress/public/_media/map.png similarity index 100% rename from docs/.vuepress/public/_media/map.png rename to docs/src/.vuepress/public/_media/map.png diff --git a/docs/.vuepress/public/_media/medialibrary.png b/docs/src/.vuepress/public/_media/medialibrary.png similarity index 100% rename from docs/.vuepress/public/_media/medialibrary.png rename to docs/src/.vuepress/public/_media/medialibrary.png diff --git a/docs/.vuepress/public/_media/medias.png b/docs/src/.vuepress/public/_media/medias.png similarity index 100% rename from docs/.vuepress/public/_media/medias.png rename to docs/src/.vuepress/public/_media/medias.png diff --git a/docs/.vuepress/public/_media/multiselectinline.png b/docs/src/.vuepress/public/_media/multiselectinline.png similarity index 100% rename from docs/.vuepress/public/_media/multiselectinline.png rename to docs/src/.vuepress/public/_media/multiselectinline.png diff --git a/docs/.vuepress/public/_media/multiselectunpacked.png b/docs/src/.vuepress/public/_media/multiselectunpacked.png similarity index 100% rename from docs/.vuepress/public/_media/multiselectunpacked.png rename to docs/src/.vuepress/public/_media/multiselectunpacked.png diff --git a/docs/.vuepress/public/_media/nested-child-form.png b/docs/src/.vuepress/public/_media/nested-child-form.png similarity index 100% rename from docs/.vuepress/public/_media/nested-child-form.png rename to docs/src/.vuepress/public/_media/nested-child-form.png diff --git a/docs/.vuepress/public/_media/nested-child-index.png b/docs/src/.vuepress/public/_media/nested-child-index.png similarity index 100% rename from docs/.vuepress/public/_media/nested-child-index.png rename to docs/src/.vuepress/public/_media/nested-child-index.png diff --git a/docs/.vuepress/public/_media/nested-module.png b/docs/src/.vuepress/public/_media/nested-module.png similarity index 100% rename from docs/.vuepress/public/_media/nested-module.png rename to docs/src/.vuepress/public/_media/nested-module.png diff --git a/docs/.vuepress/public/_media/nested-parent-index.png b/docs/src/.vuepress/public/_media/nested-parent-index.png similarity index 100% rename from docs/.vuepress/public/_media/nested-parent-index.png rename to docs/src/.vuepress/public/_media/nested-parent-index.png diff --git a/docs/.vuepress/public/_media/radios.png b/docs/src/.vuepress/public/_media/radios.png similarity index 100% rename from docs/.vuepress/public/_media/radios.png rename to docs/src/.vuepress/public/_media/radios.png diff --git a/docs/.vuepress/public/_media/repeater.png b/docs/src/.vuepress/public/_media/repeater.png similarity index 100% rename from docs/.vuepress/public/_media/repeater.png rename to docs/src/.vuepress/public/_media/repeater.png diff --git a/docs/.vuepress/public/_media/select.png b/docs/src/.vuepress/public/_media/select.png similarity index 100% rename from docs/.vuepress/public/_media/select.png rename to docs/src/.vuepress/public/_media/select.png diff --git a/docs/.vuepress/public/_media/selectunpacked.png b/docs/src/.vuepress/public/_media/selectunpacked.png similarity index 100% rename from docs/.vuepress/public/_media/selectunpacked.png rename to docs/src/.vuepress/public/_media/selectunpacked.png diff --git a/docs/src/.vuepress/public/_media/twill-dashboard.jpg b/docs/src/.vuepress/public/_media/twill-dashboard.jpg new file mode 100644 index 000000000..16a0fce0c Binary files /dev/null and b/docs/src/.vuepress/public/_media/twill-dashboard.jpg differ diff --git a/docs/.vuepress/public/_media/video.png b/docs/src/.vuepress/public/_media/video.png similarity index 100% rename from docs/.vuepress/public/_media/video.png rename to docs/src/.vuepress/public/_media/video.png diff --git a/docs/.vuepress/public/_media/wysiwyg.png b/docs/src/.vuepress/public/_media/wysiwyg.png similarity index 100% rename from docs/.vuepress/public/_media/wysiwyg.png rename to docs/src/.vuepress/public/_media/wysiwyg.png diff --git a/docs/.vuepress/public/favicon-16.png b/docs/src/.vuepress/public/favicon-16.png similarity index 100% rename from docs/.vuepress/public/favicon-16.png rename to docs/src/.vuepress/public/favicon-16.png diff --git a/docs/.vuepress/public/favicon-180.png b/docs/src/.vuepress/public/favicon-180.png similarity index 100% rename from docs/.vuepress/public/favicon-180.png rename to docs/src/.vuepress/public/favicon-180.png diff --git a/docs/.vuepress/public/favicon-192.png b/docs/src/.vuepress/public/favicon-192.png similarity index 100% rename from docs/.vuepress/public/favicon-192.png rename to docs/src/.vuepress/public/favicon-192.png diff --git a/docs/.vuepress/public/favicon-32.png b/docs/src/.vuepress/public/favicon-32.png similarity index 100% rename from docs/.vuepress/public/favicon-32.png rename to docs/src/.vuepress/public/favicon-32.png diff --git a/docs/.vuepress/public/favicon-512.png b/docs/src/.vuepress/public/favicon-512.png similarity index 100% rename from docs/.vuepress/public/favicon-512.png rename to docs/src/.vuepress/public/favicon-512.png diff --git a/docs/.vuepress/public/favicon.ico b/docs/src/.vuepress/public/favicon.ico similarity index 100% rename from docs/.vuepress/public/favicon.ico rename to docs/src/.vuepress/public/favicon.ico diff --git a/docs/.vuepress/public/social_share.png b/docs/src/.vuepress/public/social_share.png similarity index 100% rename from docs/.vuepress/public/social_share.png rename to docs/src/.vuepress/public/social_share.png diff --git a/docs/src/.vuepress/sidebar.js b/docs/src/.vuepress/sidebar.js new file mode 100644 index 000000000..aa5b7e307 --- /dev/null +++ b/docs/src/.vuepress/sidebar.js @@ -0,0 +1,328 @@ +module.exports = [ + { + "title": "Documentation", + "children": [ + { + "title": "Preface", + "path": "/preface/", + "children": [ + { + "title": "Benefits Overview", + "path": "/preface/", + }, + { + "title": "Feature List", + "path": "/preface/feature-list.html", + }, + { + "title": "Architecture Concepts", + "path": "/preface/architecture-concepts.html", + }, + { + "title": "Credits", + "path": "/preface/credits.html", + }, + { + "title": "Contribution Guide", + "path": "/preface/contribution-guide.html", + }, + { + "title": "Licensing", + "path": "/preface/licensing.html", + }, + { + "title": "1.x Documentation", + "path": "/preface/1-x-documentation.html", + } + ], + "collapsable": true + }, + { + "title": "Getting Started", + "path": "/getting-started/", + "children": [ + { + "title": "Environment Requirements", + "path": "/getting-started/", + }, + { + "title": "Installation", + "path": "/getting-started/installation.html", + }, + { + "title": "Configuration", + "path": "/getting-started/configuration.html", + }, + { + "title": "Navigation", + "path": "/getting-started/navigation.html", + } + ], + "collapsable": true + }, + { + "title": "Modules", + "path": "/crud-modules/", + "children": [ + { + "title": "CRUD Modules", + "path": "/crud-modules/", + }, + { + "title": "CLI Generator", + "path": "/crud-modules/cli-generator.html", + }, + { + "title": "Migrations", + "path": "/crud-modules/migrations.html", + }, + { + "title": "Models", + "path": "/crud-modules/models.html", + }, + { + "title": "Repositories", + "path": "/crud-modules/repositories.html", + }, + { + "title": "Controllers", + "path": "/crud-modules/controllers.html", + }, + { + "title": "Form Requests", + "path": "/crud-modules/form-requests.html", + }, + { + "title": "Routes", + "path": "/crud-modules/routes.html", + }, + { + "title": "Revisions and Previewing", + "path": "/crud-modules/revisions-and-previewing.html", + }, + { + "title": "Nested Modules", + "path": "/crud-modules/nested-modules.html", + } + ], + "collapsable": true + }, + { + "title": "Form Fields", + "path": "/form-fields/", + "children": [ + { + "title": "Form Fields", + "path": "/form-fields/", + }, + { + "title": "Input", + "path": "/form-fields/input.html", + }, + { + "title": "WYSIWYG", + "path": "/form-fields/wysiwyg.html", + }, + { + "title": "Medias", + "path": "/form-fields/medias.html", + }, + { + "title": "Files", + "path": "/form-fields/files.html", + }, + { + "title": "Datepicker", + "path": "/form-fields/datepicker.html", + }, + { + "title": "Timepicker", + "path": "/form-fields/timepicker.html", + }, + { + "title": "Select", + "path": "/form-fields/select.html", + }, + { + "title": "Select Unpacked", + "path": "/form-fields/select-unpacked.html", + }, + { + "title": "Multi Select", + "path": "/form-fields/multi-select.html", + }, + { + "title": "Multi Select Inline", + "path": "/form-fields/multi-select-inline.html", + }, + { + "title": "Checkbox", + "path": "/form-fields/checkbox.html", + }, + { + "title": "Multiple Checkboxes", + "path": "/form-fields/multiple-checkboxes.html", + }, + { + "title": "Radios", + "path": "/form-fields/radios.html", + }, + { + "title": "Block Editor", + "path": "/form-fields/block-editor.html", + }, + { + "title": "Browser", + "path": "/form-fields/browser.html", + }, + { + "title": "Repeater", + "path": "/form-fields/repeater.html", + }, + { + "title": "Map", + "path": "/form-fields/map.html", + }, + { + "title": "Color", + "path": "/form-fields/color.html", + }, + { + "title": "Conditional Fields", + "path": "/form-fields/conditional-fields.html", + } + ], + "collapsable": true + }, + { + "title": "Block Editor", + "path": "/block-editor/", + "children": [ + { + "title": "Overview", + "path": "/block-editor/", + }, + { + "title": "Creating a Block Editor", + "path": "/block-editor/creating-a-block-editor.html", + }, + { + "title": "Adding Repeater Fields to a Block", + "path": "/block-editor/adding-repeater-fields-to-a-block.html", + }, + { + "title": "Adding Browser Fields to a Block", + "path": "/block-editor/adding-browser-fields-to-a-block.html", + }, + { + "title": "Rendering Blocks", + "path": "/block-editor/rendering-blocks.html", + }, + { + "title": "Previewing Blocks", + "path": "/block-editor/previewing-blocks.html", + }, + { + "title": "Development Workflow", + "path": "/block-editor/development-workflow.html", + }, + { + "title": "Default Configuration", + "path": "/block-editor/default-configuration.html", + }, + { + "title": "Legacy Configuration (< 2.2)", + "path": "/block-editor/legacy-configuration-2-2.html", + } + ], + "collapsable": true + }, + { + "title": "Media Library", + "path": "/media-library/", + "children": [ + { + "title": "Media Library", + "path": "/media-library/", + }, + { + "title": "Storage Provider", + "path": "/media-library/storage-provider.html", + }, + { + "title": "Image Rendering Service", + "path": "/media-library/image-rendering-service.html", + }, + { + "title": "Role & Crop Params", + "path": "/media-library/role-crop-params.html", + }, + { + "title": "File Library", + "path": "/media-library/file-library.html", + }, + { + "title": "Imgix and S3 Direct Uploads", + "path": "/media-library/imgix-and-s3-direct-uploads.html", + }, + { + "title": "Imgix and Local Uploads", + "path": "/media-library/imgix-and-local-uploads.html", + } + ], + "collapsable": true + }, + { + "title": "Artisan Commands", + "path": "/artisan-commands/", + "children": [], + }, + { + "title": "Dashboard", + "path": "/dashboard/", + "children": [], + }, + { + "title": "Global Search", + "path": "/global-search/", + "children": [], + }, + { + "title": "Featuring Content", + "path": "/featuring-content/", + "children": [], + }, + { + "title": "Settings Sections", + "path": "/settings-sections/", + "children": [], + }, + { + "title": "Custom CMS Pages", + "path": "/custom-cms-pages/", + "children": [], + }, + { + "title": "User Management", + "path": "/user-management/", + "children": [], + }, + { + "title": "OAuth Login", + "path": "/oauth-login/", + "children": [], + }, + { + "title": "Resources", + "path": "/resources/", + "children": [], + }, + { + "title": "API Documentation", + "path": "/api-documentation/", + "children": [], + } + ], + "collapsable": false + } +]; diff --git a/docs/.vuepress/styles/index.styl b/docs/src/.vuepress/styles/index.styl similarity index 94% rename from docs/.vuepress/styles/index.styl rename to docs/src/.vuepress/styles/index.styl index 289508dc5..7b50c6402 100644 --- a/docs/.vuepress/styles/index.styl +++ b/docs/src/.vuepress/styles/index.styl @@ -86,33 +86,45 @@ top: 0; } -.twill-doc h1, -.twill-doc h2 { +.twill-doc h1 { font-size: 2.5rem; } .twill-doc h2 { + font-size: 1.4375rem; border-bottom: 0 none; } .twill-doc h3 { - font-size: 1.4375rem; + font-size: 1.25rem; } -.twill-doc h4, -.twill-doc h5 { +.twill-doc h4 { font-size: 1rem; } +.twill-doc h5 { + font-size: 0.875rem; +} + +.twill-doc h6 { + font-size: 0.75rem; +} + .twill-doc .theme-default-content:not(.custom) { padding-top: 6.25rem; - padding-bottom: 6.25rem; + padding-bottom: 3.25rem; } .twill-doc .sidebar .sidebar-links:not(.sidebar-group-items) { padding: 2rem 0; } +.twill-doc .sidebar-group.is-sub-group > .sidebar-heading { + font-size: 1em; +} + +.twill-doc a.sidebar-heading.active, .twill-doc a.sidebar-link.active { border-left-color: transparent } diff --git a/docs/.vuepress/styles/palette.styl b/docs/src/.vuepress/styles/palette.styl similarity index 100% rename from docs/.vuepress/styles/palette.styl rename to docs/src/.vuepress/styles/palette.styl diff --git a/docs/.sections/api-documentation.md b/docs/src/api-documentation/index.md similarity index 87% rename from docs/.sections/api-documentation.md rename to docs/src/api-documentation/index.md index 62083da08..fc35d6b5a 100644 --- a/docs/.sections/api-documentation.md +++ b/docs/src/api-documentation/index.md @@ -1,4 +1,8 @@ -## API Documentation +--- +pageClass: twill-doc +--- + +# API Documentation [Doctum](https://github.com/code-lts/doctum) generated API documentation of the framework is available [here](https://twill.io/docs/api/2.x) for `2.x` and previous versions. diff --git a/docs/.sections/artisan.md b/docs/src/artisan-commands/index.md similarity index 65% rename from docs/.sections/artisan.md rename to docs/src/artisan-commands/index.md index b6191167c..33e87f3b4 100644 --- a/docs/.sections/artisan.md +++ b/docs/src/artisan-commands/index.md @@ -1,13 +1,17 @@ -## Artisan commands +--- +pageClass: twill-doc +--- + +# Artisan Commands Twill includes a few Artisan commands to facilitate the development process. They are all maintained under the `twill:` namespace. Here is an overview: -* `php artisan twill:install` - Detailed in the [installation section](#installation) of the documentation, this command will generate and run core migrations for a starter Twill installation. Running this command after it has already been run can lead to errors and conflicts with your changes. After running database migrations, it will then automatically run the `twill:superadmin` command detailed below, in order to create a superadmin user so you can log into your CMS. +* `php artisan twill:install` - Detailed in the [installation section](/getting-started/installation.html) of the documentation, this command will generate and run core migrations for a starter Twill installation. Running this command after it has already been run can lead to errors and conflicts with your changes. After running database migrations, it will then automatically run the `twill:superadmin` command detailed below, in order to create a superadmin user so you can log into your CMS. * `php artisan twill:superadmin` - As noted above, this command will prompt you to create a new superadmin user, requesting a user email address and then a password. Run this command on its own if you need to quickly generate a new superadmin user. -* `php artisan twill:module {moduleName}` - This command is extremely helpful in bootstrapping the files you will need to manage new models. It is detailed extensively in the [CRUD Modules section](#cli-generator) of the documentation. +* `php artisan twill:module {moduleName}` - This command is extremely helpful in bootstrapping the files you will need to manage new models. It is detailed extensively in the [CRUD Modules section](/crud-modules/cli-generator.html) of the documentation. -* `php artisan twill:lqip` - This command generates low-quality image placeholders (LQIP) of your media files as base64 encoded strings that you can inline in your HTML response to avoid an extra image request. This is a strategy deployed in media management to improve initial page load times. The default behavior of this command is to generate LQIP for any media files that do not already have an LQIP alternative. Use the `--all` flag to generate new LQIP for all media files. To learn more about media management, check out the [media library section](#media-library-3) of the documentation. +* `php artisan twill:lqip` - This command generates low-quality image placeholders (LQIP) of your media files as base64 encoded strings that you can inline in your HTML response to avoid an extra image request. This is a strategy deployed in media management to improve initial page load times. The default behavior of this command is to generate LQIP for any media files that do not already have an LQIP alternative. Use the `--all` flag to generate new LQIP for all media files. To learn more about media management, check out the [media library section](/media-library/) of the documentation. diff --git a/docs/src/block-editor/adding-browser-fields-to-a-block.md b/docs/src/block-editor/adding-browser-fields-to-a-block.md new file mode 100644 index 000000000..80b7a4af4 --- /dev/null +++ b/docs/src/block-editor/adding-browser-fields-to-a-block.md @@ -0,0 +1,53 @@ +--- +pageClass: twill-doc +--- + +# Adding Browser Fields to a Block + +To attach other records inside of a block, it is possible to use the `browser` field. + +- In a block, use the `browser` field: + +filename: ```views/admin/blocks/products.blade.php``` +```php + @twillBlockTitle('Products') + + @formField('browser', [ + 'routePrefix' => 'shop', + 'moduleName' => 'products', + 'name' => 'products', + 'label' => 'Products', + 'max' => 10 + ]) +``` + +- If the module you are browsing is not at the root of your admin, you should use the `browser_route_prefixes` array in the configuration in addition to `routePrefix` in the form field declaration: + +```php + 'block_editor' => [ + ... + 'browser_route_prefixes' => [ + 'products' => 'shop', + ], + ... + ], +``` + +- When rendering the blocks on the frontend you can get the browser items selected in the block, by using the `browserIds` helper to retrieve the selected items' ids, and then you may use Eloquent method like `find` to get the actual records. Example in a blade template: + +filename: ```views/site/blocks/blockWithBrowser.blade.php``` +```php + @php + $selected_items_ids = $block->browserIds('browserFieldName'); + $items = Item::find($selected_items_ids); + @endphp +``` + +- When the browser field allows multiple modules/endpoints, you can also use the `getRelated` function on the block: + +filename: ```views/site/blocks/blockWithBrowser.blade.php``` +```php + @php + $selected_items = $block->getRelated('browserFieldName'); + @endphp +``` diff --git a/docs/src/block-editor/adding-repeater-fields-to-a-block.md b/docs/src/block-editor/adding-repeater-fields-to-a-block.md new file mode 100644 index 000000000..4a9caeaa1 --- /dev/null +++ b/docs/src/block-editor/adding-repeater-fields-to-a-block.md @@ -0,0 +1,37 @@ +--- +pageClass: twill-doc +--- + +# Adding Repeater Fields to a Block + +Inside a block, repeaters can be used too. + +- Create a *container* block file, using a repeater form field: + + filename: ```views/admin/blocks/accordion.blade.php``` +```php + @twillBlockTitle('Accordion') + ... + @formField('repeater', ['type' => 'accordion_item']) +``` +You can add other fields before or after your repeater, or even multiple repeaters to the same block. + +- Create an *item* block, the one that will be reapeated inside the *container* block + +filename: ```views/admin/repeaters/accordion_item.blade.php``` +```php + @twillRepeaterTitle('Accordion item') + @twillRepeaterMax('10') + + @formField('input', [ + 'name' => 'header', + 'label' => 'Header' + ]) + + @formField('input', [ + 'type' => 'textarea', + 'name' => 'description', + 'label' => 'Description', + 'rows' => 4 + ]) +``` diff --git a/docs/src/block-editor/creating-a-block-editor.md b/docs/src/block-editor/creating-a-block-editor.md new file mode 100644 index 000000000..22b1e27c3 --- /dev/null +++ b/docs/src/block-editor/creating-a-block-editor.md @@ -0,0 +1,212 @@ +--- +pageClass: twill-doc +--- + +# Creating a Block Editor + +#### Include the block editor in your module's form + +In order to add a block editor to your module, add the `block_editor` field to your module form. e.g.: + +```php +@extends('twill::layouts.form') + +@section('contentFields') + @formField('input', [ + 'name' => 'description', + 'label' => 'Description', + ]) +... + @formField('block_editor') +@stop +``` + +By default, adding the `@formField('block_editor')` directive enables all available *blocks* for use in your module. To scope only certain *blocks* to be available in a given module, you can add a second parameter to the `@formField()` directive with the *blocks* key. e.g.: + +```php +@formField('block_editor', [ + 'blocks' => ['quote', 'image'] +]) +``` + +#### Create and define blocks + +Blocks and Repeaters are built on the same Block model and are created and defined in their respective folders. By default, Twill will look for Blade templates in `views/admin/blocks` for blocks and `views/admin/repeaters` for repeaters. + +Note: Prior to Twill version 2.2, Blocks (and Repeaters) needed to be defined in the configuration file – this is no longer necessary and not recommended. This change is backward compatible, so your existing configuration should work as it used to. Defining blocks in the configuration file will be deprecated in a future release (see the section below [Legacy configuration](/block-editor/legacy-configuration-2-2.html). + +Blocks (and Repeaters) are exactly like a regular form, without any Blade layout or section. The templates take special annotations to add further customization. The title annotation is mandatory and Twill will throw an error if it is not defined. + +Available annotations: + - Provide a title with `@twillPropTitle` or `@twillBlockTitle` or `@twillRepeaterTitle` (mandatory) + - Provide a dynamic title with `@twillPropTitleField` or `@twillBlockTitleField` or `@twillRepeaterTitleField` + - Provide an icon with `@twillPropIcon` or `@twillBlockIcon` or `@twillRepeaterIcon` + - Provide a group with `@twillPropGroup` or `@twillBlockGroup` or `@twillRepeaterGroup` (defaults to `app`) + - Provide a repeater trigger label with `@twillPropTrigger` or `@twillRepeaterTrigger` + - Provide a repeater max items with `@twillPropMax` or `@twillRepeaterMax` + - Define a block or repeater as compiled with `@twillPropCompiled` or `@twillBlockCompiled` or `@twillRepeaterCompiled` + - Define a block or repeater component with `@twillPropComponent` or `@twillBlockComponent` or `@twillRepeaterComponent` + +e.g.: + +filename: ```views/admin/blocks/quote.blade.php``` +```php +@twillBlockTitle('Quote') +@twillBlockIcon('text') + +@formField('input', [ + 'name' => 'quote', + 'type' => 'textarea', + 'label' => 'Quote text', + 'maxlength' => 250, + 'rows' => 4 +]) +``` + +A more complex example would look like this: + +filename: ```views/admin/blocks/media.blade.php``` +```php +@twillBlockTitle('Media') +@twillBlockIcon('image') + +@formField('medias', [ + 'name' => 'image', + 'label' => 'Images', + 'withVideoUrl' => false, + 'max' => 20, +]) + +@formField('files', [ + 'name' => 'video', + 'label' => 'Video', + 'note' => 'Video will overwrite previously selected images', + 'max' => 1 +]) + +@formField('input', [ + 'name' => 'caption', + 'label' => 'Caption', + 'maxlength' => 250, + 'translated' => true, +]) + +@formField('select', [ + 'name' => 'effect', + 'label' => 'Transition Effect', + 'placeholder' => 'Select Transition Effect', + 'default' => 'cut', + 'options' => [ + [ + 'value' => 'cut', + 'label' => 'Cut' + ], + [ + 'value' => 'fade', + 'label' => 'Fade In/Out' + ] + ] +]) + +@formField('color', [ + 'name' => 'bg', + 'label' => 'Background color', + 'note' => 'Default is light grey (#E6E6E6)', +]) + +@formField('input', [ + 'name' => 'timing', + 'label' => 'Timing', + 'maxlength' => 250, + 'note' => 'Timing in ms (default is 4000ms)', +]) +``` + +With that, the *block* is ready to be used on the form! + +##### Dynamic block titles + +In Twill >= 2.5, you can use the `@twillBlockTitleField` directive to include the value of a given field in the title area of the blocks. This directive also accepts a `hidePrefix` option to hide the generic block title: + +```php +@twillBlockTitle('Section') +@twillBlockTitleField('title', ['hidePrefix' => true]) +@twillBlockIcon('text') +@twillBlockGroup('app') + +@formField('input', [ + 'name' => 'title', + 'label' => 'Title', + 'required' => true, +]) + +... +``` + +##### Create a block from an existing block template + +Using `php artisan twill:make:block {name} {baseBlock} {icon}`, you can generate a new block based on a provided block as a base. + +This example would create `views/admin/blocks/exceptional-media.blade.php` from `views/admin/blocks/media.blade.php`: + +``` +$ php artisan twill:make:block ExceptionalMedia media image +``` + +##### List existing blocks and repeaters + +Using `php artisan twill:list:blocks` will list all blocks and repeaters. There are a few options: + - `-s|--shorter` for a shorter table, + - `-b|--blocks` for blocks only, + - `-r|--repeaters` for repeaters only, + - `-a|--app` for app blocks/repeaters only, + - `-c|--custom` for app blocks/repeaters overriding Twill blocks/repeaters only, + - `-t|--twill` for Twill blocks/repeaters only + +##### List existing icons + +`php artisan twill:list:icons` will list all icons available. + +#### Use Block traits in your Model and Repository + +Now, to handle the block data you must integrate it with your module. *Use* the *Blocks* traits in the Model and Repository associated with your module. +If you generated that module from the CLI and did respond yes to the question asking you about using blocks, this should already be the case for you. + +In your model, use `HasBlocks`: + +filename: ```app/Models/Article.php``` +```php + 'site.layouts.block', // layout to use when rendering a single block in the editor + 'block_views_path' => 'site.blocks', // path where a view file per block type is stored + 'block_views_mappings' => [], // custom mapping of block types and views + 'block_preview_render_childs' => true, // indicates if childs should be rendered when using repeater in blocks + 'block_presenter_path' => null, // allow to set a custom presenter to a block model + // Indicates if blocks templates should be inlined in HTML. + // When setting to false, make sure to build Twill with your all your custom blocks. + 'inline_blocks_templates' => true, + 'custom_vue_blocks_resource_path' => 'assets/js/blocks', + 'use_twill_blocks' => ['text', 'image'], + 'crops' => [ + 'image' => [ + 'desktop' => [ + [ + 'name' => 'desktop', + 'ratio' => 16 / 9, + 'minValues' => [ + 'width' => 100, + 'height' => 100, + ], + ], + ], + 'tablet' => [ + [ + 'name' => 'tablet', + 'ratio' => 4 / 3, + 'minValues' => [ + 'width' => 100, + 'height' => 100, + ], + ], + ], + 'mobile' => [ + [ + 'name' => 'mobile', + 'ratio' => 1, + 'minValues' => [ + 'width' => 100, + 'height' => 100, + ], + ], + ], + ], + ], + 'directories' => [ + 'source' => [ + 'blocks' => [ + [ + 'path' => base_path('vendor/area17/twill/src/Commands/stubs/blocks'), + 'source' => A17\Twill\Services\Blocks\Block::SOURCE_TWILL, + ], + [ + 'path' => resource_path('views/admin/blocks'), + 'source' => A17\Twill\Services\Blocks\Block::SOURCE_APP, + ], + ], + 'repeaters' => [ + [ + 'path' => resource_path('views/admin/repeaters'), + 'source' => A17\Twill\Services\Blocks\Block::SOURCE_APP, + ], + [ + 'path' => base_path('vendor/area17/twill/src/Commands/stubs/repeaters'), + 'source' => A17\Twill\Services\Blocks\Block::SOURCE_TWILL, + ], + ], + 'icons' => [ + base_path('vendor/area17/twill/frontend/icons'), + resource_path('views/admin/icons'), + ], + ], + 'destination' => [ + 'make_dir' => true, + 'blocks' => resource_path('views/admin/blocks'), + 'repeaters' => resource_path('views/admin/repeaters'), + ], + ], +]; +``` diff --git a/docs/src/block-editor/development-workflow.md b/docs/src/block-editor/development-workflow.md new file mode 100644 index 000000000..cd76533bf --- /dev/null +++ b/docs/src/block-editor/development-workflow.md @@ -0,0 +1,73 @@ +--- +pageClass: twill-doc +--- + +# Development Workflow + +As of verison 2.2, it is not necessary to rebuild Twill's frontend when working with blocks anymore. Their templates are now dynamically rendered in Blade and loaded at runtime by Vue. (For <2.1.x users, it means you do not need to run `php artisan twill:blocks` and `npm run twill-build` after creating or updating a block. Just reload the page to see your changes after saving your Blade file!) + +This is possible because Twill's blocks Vue components are simple single file components that only have a template and a mixin registration. Blocks components are now dynamically registered by Vue using `x-template` scripts that are inlined by Blade. + +#### Custom blocks and repeaters + +To define a block as being `compiled` (ie. using a custom Vue component), you can do this with the annotations `@twillPropCompiled('true')`, `@twillBlockCompiled('true')` or `@twillRepeaterCompiled('true')`. The imported Vue file will be prefered at runtime over the inline, template only, version. + +You can bootstrap your custom Vue blocks by generating them from their Blade counterpart using `php artisan twill:blocks`. It will ask you to confirm before overriding any existing custom Vue block. To start a custom Vue block from scratch, use the following template: + +```vue + + + + +``` + +Note: For legacy 2.1.x users, in the `twill.block_editor.blocks` configuration array, set 'compiled' to `true` on the individual blocks. + +If you are using custom Vue blocks (as in, you edited the `template`, `script` or `style` section of a generated block Vue file), you need to rebuild Twill assets. + +There are two artisan commands to help you and we recommend using them instead of our previous versions' npm scripts: + + - `php artisan twill:build`, which will build Twill's assets with your custom blocks, located in the `twill.block_editor.custom_vue_blocks_resource_path` new configurable path (with defaults to `assets/js/blocks`, like in previous versions). + + - `php artisan twill:dev`, which will start a local server that watches for changes in Twill's frontend directory. You need to set `'dev_mode' => true` in your `config/twill.php` file when using this command. This is especially helpful for Twill's contributors, but can also be useful if you use a lot of custom components in your application. + +Both commands take a `--noInstall` option to avoid running `npm ci` before every build. + +#### Naming convention of custom Vue components + +The naming convention for custom blocks Vue component is deferred from the block's component name. For example, if your block's component name is `a17-block-quote`, the custom blocks should be `assets/js/blocks/BlockQuote.vue`. For component name with underscores, for example `a17-amazing_quote`, it would be `assets/js/blocks/BlockAmazing_quote.vue`. + +#### Disabling inline blocks' templates + +It is also possible to completely disable this feature by setting the `twill.block_editor.inline_blocks_templates` config flag to `false`. + +If you do disable this feature, you could continue using previous versions' npm scripts, but we recommend you stop rebuilding Twill assets entirely unless you are using custom code in your generated Vue blocks. If you do keep using our npm scripts instead of our new Artisan commands, you will need to update `twill-build` from: + +``` + "twill-build": "rm -f public/hot && npm run twill-copy-blocks && cd vendor/area17/twill && npm ci && npm run prod && cp -R public/* ${INIT_CWD}/public", +``` + +to: + +``` + "twill-build": "npm run twill-copy-blocks && cd vendor/area17/twill && npm ci && npm run prod && cp -R dist/* ${INIT_CWD}/public", +``` + +#### A bit further: extending Twill with custom components and custom workflows + +On top of custom Vue blocks, It is possible to rebuild Twill with custom Vue components. This can be used to override Twill's own Vue components or create new form fields, for example. The `twill.custom_components_resource_path` configuration can be used to provide a path under Laravel `resources` folder that will be used as a source of Vue components to include in your form js build when running `php artisan twill:build`. + +You have to run `php artisan twill:build` for your custom Vue components to be included in the frontend build. + +For a more in depth tutorial, check out this [Spectrum post](https://spectrum.chat/twill/tips-and-tricks/adding-a-custom-block-to-twill-admin-view-with-vuejs~028d79b1-b3cd-4fb7-a89c-ce64af7be4af). diff --git a/docs/src/block-editor/index.md b/docs/src/block-editor/index.md new file mode 100644 index 000000000..ce903d440 --- /dev/null +++ b/docs/src/block-editor/index.md @@ -0,0 +1,17 @@ +--- +pageClass: twill-doc +--- + +# Block Editor Overview + +The block editor is a dynamic, drag and drop interface giving users a lot of flexibility in adding and changing content for a given entry. +For instance, if you have a module for creating work case studies (as we do in [our demo](https://demo.twill.io/)), you can use the block editor to create, arrange, and edit blocks of images and text, or anything else you can think of really, as they would appear in a page. +You can create any number of different block types, each with a unique form that can be accessed directly within the block editor. + +Below, we describe the process of creating a block editor and connecting it to your module. + +Here is an overview of the process, each of which is detailed below. + +1. Include the block editor form field in your module's form +2. Create and define blocks +3. Make sure you use blocks traits in your Model and Repository diff --git a/docs/src/block-editor/legacy-configuration-2-2.md b/docs/src/block-editor/legacy-configuration-2-2.md new file mode 100644 index 000000000..552a65db9 --- /dev/null +++ b/docs/src/block-editor/legacy-configuration-2-2.md @@ -0,0 +1,90 @@ +--- +pageClass: twill-doc +--- + +# Legacy Configuration (< 2.2) + +#### Twill prior to version 2.2 + +For Twill version 2.1.x and below, in the `config/twill.php` `block_editor` array, define all *blocks* and *repeaters* available in your project, including the block title, the icon used when displaying it in the block editor form and the associated component name. It would look like this: + +filename: ```config/twill.php``` +```php + 'block_editor' => [ + 'blocks' => [ + ... + 'quote' => [ + 'title' => 'Quote', + 'icon' => 'text', + 'component' => 'a17-block-quote', + ], + 'media' => [ + 'title' => 'Media', + 'icon' => 'image', + 'component' => 'a17-block-media', + ], + 'accordion' => [ + 'title' => 'Accordion', + 'icon' => 'text', + 'component' => 'a17-block-accordion', + ], + ... + ] + 'repeaters' => [ + 'accordion_item' => [ + 'title' => 'Accordion item', + 'icon' => 'text', + 'component' => 'a17-block-accordion_item', + ], + ... + ], + ], +``` + +**Please note the naming convention. If the *block* added is `quote` then the component should be prefixed with `a17-block-`.** + +If you added a block named *awesome_block*, your configuration would look like this: + +```php + 'block_editor' => [ + 'blocks' => [ + ... + 'awesome_block' => [ + 'title' => 'Title for the awesome block', + 'icon' => 'text', + 'component' => 'a17-block-awesome_block', + ], + .. + ] +``` + +##### Common errors +- If you add the *container* block to the _repeaters_ section inside the config, it won't work, e.g.: +```php + 'repeaters' => [ + ... + 'accordion' => [ + 'title' => 'Accordion', + 'trigger' => 'Add accordion', + 'component' => 'a17-block-accordion', + 'max' => 10, + ], + ... + ] +``` + +- If you use a different name for the block inside the _repeaters_ section, it also won't work, e. g.: +```php + 'repeaters' => [ + ... + 'accordion-item' => [ + 'title' => 'Accordion', + 'trigger' => 'Add accordion', + 'component' => 'a17-block-accordion_item', + 'max' => 10, + ], + ... + ] +``` + +- Not adding the *item* block to the _repeaters_ section will also result in failure. diff --git a/docs/src/block-editor/previewing-blocks.md b/docs/src/block-editor/previewing-blocks.md new file mode 100644 index 000000000..76c391b39 --- /dev/null +++ b/docs/src/block-editor/previewing-blocks.md @@ -0,0 +1,27 @@ +--- +pageClass: twill-doc +--- + +# Previewing Blocks + +At the top right of a form where you enabled a block editor, you will find a blue button labeled "Editor". The idea is to provide a better user experience when working with blocks, where the frontend preview is being immediately rendered next to the form, in a full-screen experience. + +You can enable the content editor individual block previews by providing a `resources/views/site/layouts/block.blade.php` blade layout file. This file will be treated as a _layout_, so it will need to yield a `content` section: `@yield('content')`. It will also need to include any frontend CSS/JS necessary to give the block the look and feel of the corresponding frontend layout. Here's a simple example: + +```php + + + + #madewithtwill website + + + +
+ @yield('content') +
+ + + +``` + +If you would like to specify a custom layout view path, you can do so in `config/twill.php` at `twill.block_editor.block_single_layout`. A good way to share assets and structure from the frontend with these individual block previews is to create a parent layout and extend it from your block layout. diff --git a/docs/src/block-editor/rendering-blocks.md b/docs/src/block-editor/rendering-blocks.md new file mode 100644 index 000000000..64c1deee1 --- /dev/null +++ b/docs/src/block-editor/rendering-blocks.md @@ -0,0 +1,46 @@ +--- +pageClass: twill-doc +--- + +# Rendering Blocks + +When it is time to build a frontend, you will want to render a designed set of blocks, with all blocks in their proper order. When working with a model instance that uses the HasBlocks trait in a view, you can call the `renderBlocks` helper on it. This will render the list of blocks that were created from the CMS. By default, this function will loop over all the blocks and their child blocks. In each case, the function will look for a Blade view to render for a given block. + +Create views for your blocks in the `resources/views/site/blocks` directory. Their filenames should match the block key specified in your Twill configuration and module form. + +For the `products` block example above, a corresponding view would be `resources/views/site/blocks/products.blade.php`. + +You can call the `renderBlocks` helper within a *Blade* file. Such a call would look like this: + +```php +{!! $item->renderBlocks() !!} +``` + +If you want to render child blocks (when using repeaters) inside the parent block, you can do the following: + +```php +{!! $work->renderBlocks(false) !!} +``` + +You can also specify alternate blade views for blocks. This can be helpful if you use the same block in 2 different modules of the CMS, but you want to have design flexibility in how each is rendered. To do that, specify the block view file in your call to the renderBlocks helper like this + +```php +{!! $work->renderBlocks(true, [ + 'block-type' => 'view.path', + 'block-type-2' => 'another.view.path' +]) !!} +``` + +Within these Blade views, you will have access to a `$block` variable with helper functions available to retrieve the block content: + +```php +{{ $block->input('inputNameYouSpecifiedInTheBlockFormField') }} +{{ $block->translatedinput('inputNameYouSpecifiedInATranslatedBlockFormField') }} +``` + +If the block has a media field, you can refer to the Media Library documentation below to learn about the `HasMedias` trait helpers. Here's an example of how a media field could be rendered: + +```php +{{ $block->image('mediaFieldName', 'cropNameFromBlocksConfig') }} +{{ $block->images('mediaFieldName', 'cropNameFromBlocksConfig') }} +``` diff --git a/docs/src/crud-modules/cli-generator.md b/docs/src/crud-modules/cli-generator.md new file mode 100644 index 000000000..0419a7481 --- /dev/null +++ b/docs/src/crud-modules/cli-generator.md @@ -0,0 +1,62 @@ +--- +pageClass: twill-doc +--- + +# CLI Generator + +You can generate all the files needed in your application to create a new CRUD module using Twill's Artisan generator: + +```bash +php artisan twill:module moduleName +``` + +The command accepts several options: +- `--hasBlocks (-B)`, to use the block editor on your module form +- `--hasTranslation (-T)`, to add content in multiple languages +- `--hasSlug (-S)`, to generate slugs based on one or multiple fields in your model +- `--hasMedias (-M)`, to attach images to your records +- `--hasFiles (-F)`, to attach files to your records +- `--hasPosition (-P)`, to allow manually reordering of records in the listing screen +- `--hasRevisions(-R)`, to allow comparing and restoring past revisions of records +- `--hasNesting(-N)`, to enable nested items in the module listing (see [Nested Module](/crud-modules/nested-modules.html)) + +The `twill:module` command will generate a migration file, a model, a repository, a controller, a form request object and a form view. + +Add the route to your admin routes file(`routes/admin.php`). + +```php + [ + 'title' => 'Module name', + 'module' => true + ] + ... +] +``` + +With that in place, after migrating the database using `php artisan migrate`, you should be able to start creating content. By default, a module only have a title and a description, the ability to be published, and any other feature you added through the CLI generator. + +If you provided the `hasBlocks` option, you will be able to use the `block_editor` form field in the form of that module. + +If you provided the `hasTranslation` option, and have multiple languages specified in your `translatable.php` configuration file, the UI will react automatically and allow publishers to translate content and manage publication at the language level. + +If you provided the `hasSlug` option, slugs will automatically be generated from the title field. + +If you provided the `hasMedias` or `hasFiles` option, you will be able to respectively add several `medias` or `files` form fields to the form of that module. + +If you provided the `hasPosition` option, publishers will be able to manually order records from the module's listing screen (after enabling the `reorder` option in the module's controller `indexOptions` array). + +If you provided the `hasRevisions` option, each form submission will create a new revision in your database so that publishers can compare and restore them in the CMS UI. + +Depending on the depth of your module in your navigation, you'll need to wrap your route declaration in one or multiple nested route groups. + +You can setup your index options and columns in the generated controller if needed. diff --git a/docs/src/crud-modules/controllers.md b/docs/src/crud-modules/controllers.md new file mode 100644 index 000000000..9824a3e8f --- /dev/null +++ b/docs/src/crud-modules/controllers.md @@ -0,0 +1,236 @@ +--- +pageClass: twill-doc +--- + +# Controllers + +```php + true, + 'edit' => true, + 'publish' => true, + 'bulkPublish' => true, + 'feature' => false, + 'bulkFeature' => false, + 'restore' => true, + 'bulkRestore' => true, + 'forceDelete' => true, + 'bulkForceDelete' => true, + 'delete' => true, + 'duplicate' => false, + 'bulkDelete' => true, + 'reorder' => false, + 'permalink' => true, + 'bulkEdit' => true, + 'editInModal' => false, + 'skipCreateModal' => false, + ]; + + /* + * Key of the index column to use as title/name/anythingelse column + * This will be the first column in the listing and will have a link to the form + */ + protected $titleColumnKey = 'title'; + + /* + * Available columns of the index view + */ + protected $indexColumns = [ + 'image' => [ + 'thumb' => true, // image column + 'variant' => [ + 'role' => 'cover', + 'crop' => 'default', + ], + ], + 'title' => [ // field column + 'title' => 'Title', + 'field' => 'title', + ], + 'subtitle' => [ + 'title' => 'Subtitle', + 'field' => 'subtitle', + 'sort' => true, // column is sortable + 'visible' => false, // will be available from the columns settings dropdown + ], + 'relationName' => [ // relation column + // Take a look at the example in the next section fot the implementation of the sort + 'title' => 'Relation name', + 'sort' => true, + 'relationship' => 'relationName', + 'field' => 'relationFieldToDisplay' + ], + 'presenterMethodField' => [ // presenter column + 'title' => 'Field title', + 'field' => 'presenterMethod', + 'present' => true, + ], + 'relatedBrowserFieldName' => [ // related browser column + 'title' => 'Field title', + 'field' => 'relatedFieldToDisplay', + 'relatedBrowser' => 'browserName', + ] + ]; + + /* + * Columns of the browser view for this module when browsed from another module + * using a browser form field + */ + protected $browserColumns = [ + 'title' => [ + 'title' => 'Title', + 'field' => 'title', + ], + ]; + + /* + * Relations to eager load for the index view + */ + protected $indexWith = []; + + /* + * Relations to eager load for the form view + * Add relationship used in multiselect and resource form fields + */ + protected $formWith = []; + + /* + * Relation count to eager load for the form view + */ + protected $formWithCount = []; + + /* + * Filters mapping ('filterName' => 'filterColumn') + * You can associate items list to filters by having a filterNameList key in the indexData array + * For example, 'category' => 'category_id' and 'categoryList' => app(CategoryRepository::class)->listAll() + */ + protected $filters = []; + + /* + * Add anything you would like to have available in your module's index view + */ + protected function indexData($request) + { + return []; + } + + /* + * Add anything you would like to have available in your module's form view + * For example, relationship lists for multiselect form fields + */ + protected function formData($request) + { + return []; + } + + // Optional, if the automatic way is not working for you (default is ucfirst(str_singular($moduleName))) + protected $modelName = 'model'; + + // Optional, to specify a different feature field name than the default 'featured' + protected $featureField = 'featured'; + + // Optional, specify number of items per page in the listing view (-1 to disable pagination) + // If you are implementing Sortable, this parameter is ignored given reordering is not implemented + // along with pagination. + protected $perPage = 20; + + // Optional, specify the default listing order + protected $defaultOrders = ['title' => 'asc']; + + // Optional, specify the default listing filters + protected $defaultFilters = ['search' => 'title|search']; +``` + +You can also override all actions and internal functions, checkout the ModuleController source in `A17\Twill\Http\Controllers\Admin\ModuleController`. + +#### Example: sorting by a relationship field + +Let's say we have a controller with certain fields displayed: + +File: `app/Http/Controllers/Admin/PlayController.php` +```php + protected $indexColumns = [ + 'image' => [ + 'thumb' => true, // image column + 'variant' => [ + 'role' => 'featured', + 'crop' => 'default', + ], + ], + 'title' => [ // field column + 'title' => 'Title', + 'field' => 'title', + ], + 'festivals' => [ // relation column + 'title' => 'Festival', + 'sort' => true, + 'relationship' => 'festivals', + 'field' => 'title' + ], + ]; +``` + +To order by the relationship we need to overwrite the order method in the module's repository. + +File: `app/Repositories/PlayRepository.php` +```php + ... + public function order($query, array $orders = []) { + + if (array_key_exists('festivalsTitle', $orders)){ + $sort_method = $orders['festivalsTitle']; + // remove the unexisting column from the orders array + unset($orders['festivalsTitle']); + $query = $query->orderByFestival($sort_method); + } + // don't forget to call the parent order function + return parent::order($query, $orders); + } + ... +``` + +Then, add a custom `sort` scope to your model, it could be something like this: + +File: `app/Models/Play.php` +```php + public function scopeOrderByFestival($query, $sort_method = 'ASC') { + return $query + ->leftJoin('festivals', 'plays.section_id', '=', 'festivals.id') + ->select('plays.*', 'festivals.id', 'festivals.title') + ->orderBy('festivals.title', $sort_method); + } +``` + +#### Additional table actions + +You can override the `additionalTableActions()` method to add custom actions in your module's listing view: + +File: `app/Http/Controllers/Admin/NewsletterController.php` +```php + public function additionalTableActions() + { + return [ + 'exportAction' => [ // Action name. + 'name' => 'Export Newsletter List', // Button action title. + 'variant' => 'primary', // Button style variant. Available variants; primary, secondary, action, editor, validate, aslink, aslink-grey, warning, ghost, outline, tertiary + 'size' => 'small', // Button size. Available sizes; small + 'link' => route('newsletter.export'), // Button action link. + 'target' => '', // Leave it blank for self. + 'type' => 'a', // Leave it blank for "button". + ] + ]; + } +``` diff --git a/docs/src/crud-modules/form-requests.md b/docs/src/crud-modules/form-requests.md new file mode 100644 index 000000000..49df51117 --- /dev/null +++ b/docs/src/crud-modules/form-requests.md @@ -0,0 +1,50 @@ +--- +pageClass: twill-doc +--- + +# Form Requests + +Classic Laravel 5 [form request validation](https://laravel.com/docs/5.5/validation#form-request-validation). + +Once you generated the module using Twill's CLI module generator, it will also prepare the `App/Http/Requests/Admin/ModuleNameRequest.php` for you to use. +You can choose to use different rules for creation and update by implementing the following 2 functions instead of the classic `rules` one: + +```php +rulesForTranslatedFields([ + // regular rules +], [ + // translated fields rules with just the field name like regular rules +]); +``` + +There is also an helper to define validation messages for translated fields: + +```php +messagesForTranslatedFields([ + // regular messages +], [ + // translated fields messages +]); +``` + +Once you defined the rules in this file, the UI will show the corresponding validation error state or message next to the corresponding form field. diff --git a/docs/src/crud-modules/index.md b/docs/src/crud-modules/index.md new file mode 100644 index 000000000..4b5131eea --- /dev/null +++ b/docs/src/crud-modules/index.md @@ -0,0 +1,7 @@ +--- +pageClass: twill-doc +--- + +# CRUD Modules + +Twill core functionality is the ability to setup what we call modules. A module is a set of files that define a content model and its associated business logic in your application. Modules can be configured to enable several features for publishers, from the ability to translate content, to the ability to attach images and create a more complex data structure in your records. diff --git a/docs/src/crud-modules/migrations.md b/docs/src/crud-modules/migrations.md new file mode 100644 index 000000000..3e9bc6b0a --- /dev/null +++ b/docs/src/crud-modules/migrations.md @@ -0,0 +1,70 @@ +--- +pageClass: twill-doc +--- + +# Migrations + +Twill's generated migrations are standard Laravel migrations, enhanced with helpers to create the default fields any CRUD module will use: +```php +increments('id'); + // $table->softDeletes(); + // $table->timestamps(); + // $table->boolean('published'); +}); + +// translation table, holds translated fields +Schema::create('table_name_singular_translations', function (Blueprint $table) { + createDefaultTranslationsTableFields($table, 'tableNameSingular'); + // will add the following inscructions to your migration file + // createDefaultTableFields($table); + // $table->string('locale', 6)->index(); + // $table->boolean('active'); + // $table->integer("{$tableNameSingular}_id")->unsigned(); + // $table->foreign("{$tableNameSingular}_id", "fk_{$tableNameSingular}_translations_{$tableNameSingular}_id")->references('id')->on($table)->onDelete('CASCADE'); + // $table->unique(["{$tableNameSingular}_id", 'locale']); +}); + +// slugs table, holds slugs history +Schema::create('table_name_singular_slugs', function (Blueprint $table) { + createDefaultSlugsTableFields($table, 'tableNameSingular'); + // will add the following inscructions to your migration file + // createDefaultTableFields($table); + // $table->string('slug'); + // $table->string('locale', 6)->index(); + // $table->boolean('active'); + // $table->integer("{$tableNameSingular}_id")->unsigned(); + // $table->foreign("{$tableNameSingular}_id", "fk_{$tableNameSingular}_translations_{$tableNameSingular}_id")->references('id')->on($table)->onDelete('CASCADE')->onUpdate('NO ACTION'); +}); + +// revisions table, holds revision history +Schema::create('table_name_singular_revisions', function (Blueprint $table) { + createDefaultRevisionTableFields($table, 'tableNameSingular'); + // will add the following inscructions to your migration file + // $table->increments('id'); + // $table->timestamps(); + // $table->json('payload'); + // $table->integer("{$tableNameSingular}_id")->unsigned()->index(); + // $table->integer('user_id')->unsigned()->nullable(); + // $table->foreign("{$tableNameSingular}_id")->references('id')->on("{$tableNamePlural}")->onDelete('cascade'); + // $table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); +}); + +// related content table, holds many to many association between 2 tables +Schema::create('table_name_singular1_table_name_singular2', function (Blueprint $table) { + createDefaultRelationshipTableFields($table, $table1NameSingular, $table2NameSingular); + // will add the following inscructions to your migration file + // $table->integer("{$table1NameSingular}_id")->unsigned(); + // $table->foreign("{$table1NameSingular}_id")->references('id')->on($table1NamePlural)->onDelete('cascade'); + // $table->integer("{$table2NameSingular}_id")->unsigned(); + // $table->foreign("{$table2NameSingular}_id")->references('id')->on($table2NamePlural)->onDelete('cascade'); + // $table->index(["{$table2NameSingular}_id", "{$table1NameSingular}_id"]); +}); +``` + +A few CRUD controllers require that your model have a field in the database with a specific name: `published`, `publish_start_date`, `publish_end_date`, `public`, and `position`, so stick with those column names if you are going to use publication status, timeframe and reorderable listings. diff --git a/docs/src/crud-modules/models.md b/docs/src/crud-modules/models.md new file mode 100644 index 000000000..bc54050c2 --- /dev/null +++ b/docs/src/crud-modules/models.md @@ -0,0 +1,87 @@ +--- +pageClass: twill-doc +--- + +# Models + +Set your fillables to prevent mass-assignement. This is very important, as we use `request()->all()` in the module controller. + +For fields that should default as null in the database when not sent by the form, use the `nullable` array. + +For fields that should default to false in the database when not sent by the form, use the `checkboxes` array. + +Depending upon the Twill features you need on your model, include the related traits and configure their respective options: + +#### HasPosition + +Implement the `A17\Twill\Models\Behaviors\Sortable` interface and add a position field to your fillables. + +#### HasTranslation + +Add translated fields in the `translatedAttributes` array. + +Twill's `HasTranslation` trait is a wrapper around the popular `astronomic/laravel-translatable` package. A default configuration will be automatically published to your `config` directory when you run the `twill:install` command. + +To setup your list of available languages for translated fields, modify the `locales` array in `config/translatable.php`, using ISO 639-1 two-letter languages codes as in the following example: + +```php + [ + 'en', + 'fr', + ], + ... +]; +``` + +#### HasSlug + +Specify the field(s) used to create the slug in the `slugAttributes` array. + +#### HasMedias + +Add the `mediasParams` configuration array: + +```php + [ // role name + 'default' => [ // crop name + [ + 'name' => 'default', // ratio name, same as crop name if single + 'ratio' => 16 / 9, // ratio as a fraction or number + ], + ], + 'mobile' => [ + [ + 'name' => 'landscape', // ratio name, multiple allowed + 'ratio' => 16 / 9, + ], + [ + 'name' => 'portrait', // ratio name, multiple allowed + 'ratio' => 3 / 4, + ], + ], + ], + '...' => [ // another role + ... // with crops + ] +]; +``` + +#### HasFiles: + +Add the `filesParams` configuration array: + +```php +ancestorsSlug; + +// for a specific locale: +$slug = $item->getAncestorsSlug($lang); + +// Get the combined slug for an item including all ancestors: +$slug = $item->nestedSlug; + +// for a specific locale: +$slug = $item->getNestedSlug($lang); +``` + +To include all ancestor slugs in the permalink of an item in the CMS, you can dynamically set the `$permalinkBase` property from the `form()` method of your module controller: + +```php +class PageController extends ModuleController +{ + //... + + protected function form($id, $item = null) + { + $item = $this->repository->getById($id, $this->formWith, $this->formWithCount); + + $this->permalinkBase = $item->ancestorsSlug; + + return parent::form($id, $item); + } +} +``` + +To implement routing for nested items, you can combine the `forNestedSlug()` method from `HandleNesting` with a wildcard route parameter: + +```php +// file: routes/web.php + +Route::get('{slug}', function ($slug) { + $page = app(PageRepository::class)->forNestedSlug($slug); + + abort_unless($page, 404); + + return view('site.page', ['page' => $page]); +})->where('slug', '.*'); +``` + +For more information on how to work with nested items in your application, you can refer to the +[laravel-nestedset package documentation](https://github.com/lazychaser/laravel-nestedset#retrieving-nodes). + +## Parent-child modules + +Parent-child modules are 2 distinct modules, where items of the child module are attached to items of the parent module (e.g. Issues can contain Articles): + +![parent-child modules](/docs/_media/nested-parent-index.png) + +Items of the child module can't be created independently. + +### Creating parent-child modules + +We'll use the `slug` and `position` features in this example but you can customize as needed: + +``` +php artisan twill:module issues -SP +php artisan twill:module issueArticles -SP +``` + +Add the `issue_id` foreign key to the child module's migration: + +```php +class CreateIssueArticlesTables extends Migration +{ + public function up() + { + Schema::create('issue_articles', function (Blueprint $table) { + // ... + $table->unsignedBigInteger('issue_id')->nullable(); + $table->foreign('issue_id')->references('id')->on('issues'); + }); + + // ... + } +} +``` + +Run the migrations: + +``` +php artisan migrate +``` + +Update the child model. Add the `issue_id` fillable and the relationship to the parent model: + +```php +class IssueArticle extends Model implements Sortable +{ + // ... + + protected $fillable = [ + // ... + 'issue_id', + ]; + + public function issue() + { + return $this->belongsTo(Issue::class); + } +} +``` + +Update the parent model. Add the relationship to the child model: + +```php +class Issue extends Model implements Sortable +{ + // ... + + public function articles() + { + return $this->hasMany(IssueArticle::class); + } +} +``` + +Update the child controller. Set the `$moduleName` and `$modelName` properties, then override the `getParentModuleForeignKey()` method: + +```php +class IssueArticleController extends BaseModuleController +{ + protected $moduleName = 'issues.articles'; + + protected $modelName = 'IssueArticle'; + + protected function getParentModuleForeignKey() + { + return 'issue_id'; + } +} +``` + +Update the parent controller. Set the `$indexColumns` property to include a new `Articles` column. This will be a link to the child module items, for each parent. + +```php +class IssueController extends BaseModuleController +{ + protected $moduleName = 'issues'; + + protected $indexColumns = [ + 'title' => [ + 'title' => 'Title', + 'field' => 'title', + ], + 'articles' => [ + 'title' => 'Articles', + 'nested' => 'articles', + ], + ]; +} +``` + +Add both modules to `routes/admin.php`: + +```php +Route::module('issues'); +Route::module('issues.articles'); +``` + +Add the parent module to `config/twill-navigation.php`: + +```php +return [ + 'issues' => [ + 'title' => 'Issues', + 'module' => true, + ], +]; +``` + +Then, rename and move the `articles/` views folder inside of the parent `issues/` folder: +``` +resources/views/admin/ +└── issues + ├── articles + │ └── form.blade.php + └── form.blade.php +... +``` + +### Using breadcrumbs for easier navigation + +In the child module controller, override the `indexData()` method to add the breadcrumbs to the index view: + +```php +class IssueArticleController extends BaseModuleController +{ + // ... + + protected function indexData($request) + { + $issue = app(IssueRepository::class)->getById(request('issue')); + + return [ + 'breadcrumb' => [ + [ + 'label' => 'Issues', + 'url' => moduleRoute('issues', '', 'index'), + ], + [ + 'label' => $issue->title, + 'url' => moduleRoute('issues', '', 'edit', $issue->id), + ], + [ + 'label' => 'Articles', + ], + ], + ]; + } +} +``` + +![child module index](/docs/_media/nested-child-index.png) + +
+ +Then, override the `formData()` method to do the same in the form view: + +```php + protected function formData($request) + { + $issue = app(IssueRepository::class)->getById(request('issue')); + + return [ + 'breadcrumb' => [ + [ + 'label' => 'Issues', + 'url' => moduleRoute('issues', '', 'index'), + ], + [ + 'label' => $issue->title, + 'url' => moduleRoute('issues', '', 'edit', $issue->id), + ], + [ + 'label' => 'Articles', + 'url' => moduleRoute('issues.articles', '', 'index'), + ], + [ + 'label' => 'Edit', + ], + ], + ]; + } +``` + +![nested child form](/docs/_media/nested-child-form.png) diff --git a/docs/src/crud-modules/repositories.md b/docs/src/crud-modules/repositories.md new file mode 100644 index 000000000..3aa89ac30 --- /dev/null +++ b/docs/src/crud-modules/repositories.md @@ -0,0 +1,149 @@ +--- +pageClass: twill-doc +--- + +# Repositories + +Depending on the model feature, include one or multiple of these traits: `HandleTranslations`, `HandleSlugs`, `HandleMedias`, `HandleFiles`, `HandleRevisions`, `HandleBlocks`, `HandleRepeaters`, `HandleTags`. + +Repositories allows you to modify the default behavior of your models by providing some entry points in the form of methods that you might implement: + +#### Filtering + +```php +addLikeFilterScope($query, $scopes, 'field_in_scope'); + + // add orWhereHas clauses + $this->searchIn($query, $scopes, 'field_in_scope', ['field1', 'field2', 'field3']); + + // add a whereHas clause + $this->addRelationFilterScope($query, $scopes, 'field_in_scope', 'relationName'); + + // or just go manually with the $query object + if (isset($scopes['field_in_scope'])) { + $query->orWhereHas('relationName', function ($query) use ($scopes) { + $query->where('field', 'like', '%' . $scopes['field_in_scope'] . '%'); + }); + } + + // don't forget to call the parent filter function + return parent::filter($query, $scopes); +} +``` + +#### Custom ordering + +```php +getFormFieldsForBrowser($object, 'relationName'); + + // get fields for a repeater + $fields = $this->getFormFieldsForRepeater($object, $fields, 'relationName', 'ModelName', 'repeaterItemName'); + + // return fields + return $fields +} +``` + +#### Custom field preparation before create action + + +```php +updateMultiSelect($object, $fields, 'relationName'); + + // which will simply run the following for you + $object->relationName()->sync($fields['relationName'] ?? []); + + // or, to save a oneToMany relationship + $this->updateOneToMany($object, $fields, 'relationName', 'formFieldName', 'relationAttribute') + + // or, to save a belongToMany relationship used with the browser field + $this->updateBrowser($object, $fields, 'relationName'); + + // or, to save a hasMany relationship used with the repeater field + $this->updateRepeater($object, $fields, 'relationName', 'ModelName', 'repeaterItemName'); + + // or, to save a belongToMany relationship used with the repeater field + $this->updateRepeaterMany($object, $fields, 'relationName', false); + + parent::afterSave($object, $fields); +} +``` + +#### Hydrating the model for preview of revisions + +```php +hydrateBrowser($object, $fields, 'relationName'); + + // or a multiselect + $this->hydrateMultiSelect($object, $fields, 'relationName'); + + // or a repeater + $this->hydrateRepeater($object, $fields, 'relationName'); + + return parent::hydrate($object, $fields); +} +``` diff --git a/docs/src/crud-modules/revisions-and-previewing.md b/docs/src/crud-modules/revisions-and-previewing.md new file mode 100644 index 000000000..f3d36bfab --- /dev/null +++ b/docs/src/crud-modules/revisions-and-previewing.md @@ -0,0 +1,49 @@ +--- +pageClass: twill-doc +--- + +# Revisions and Previewing + +When using the `HasRevisions` trait, Twill's UI gives publishers the ability to preview their changes without saving, as well as to preview and compare old revisions. + +If you are implementing your site using Laravel routing and Blade templating (ie. traditional server side rendering), you can follow Twill's convention of creating frontend views at `resources/views/site` and naming them according to their corresponding CRUD module name. When publishers try to preview their changes, Twill will render your frontend view within an iframe, passing the previewed record with it's unsaved changes to your view in the `$item` variable. + +If you want to provide Twill with a custom frontend views path, use the `frontend` configuration array of your `config/twill.php` file: + +```php +return [ + 'frontend' => [ + 'views_path' => 'site', + ], + ... +]; +``` + +If you named your frontend view differently than the name of its corresponding module, you can use the $previewView class property of your module's controller: + +```php + $item, + 'setting_name' => $settingRepository->byKey('setting_name') + ]; +} +``` diff --git a/docs/src/crud-modules/routes.md b/docs/src/crud-modules/routes.md new file mode 100644 index 000000000..91214c0f7 --- /dev/null +++ b/docs/src/crud-modules/routes.md @@ -0,0 +1,23 @@ +--- +pageClass: twill-doc +--- + +# Routes + +A router macro is available to create module routes quicker: +```php + ['reorder', 'feature', 'bucket', 'browser']]); + +// You can add an array of only/except action names for the resource controller as a third parameter +// By default, the following routes are created : 'index', 'store', 'show', 'edit', 'update', 'destroy' +Route::module('yourModulePluralName', [], ['only' => ['index', 'edit', 'store', 'destroy']]); + +// The last optional parameter disable the resource controller actions on the module +Route::module('yourPluralModuleName', [], [], false); +``` diff --git a/docs/src/custom-cms-pages/index.md b/docs/src/custom-cms-pages/index.md new file mode 100644 index 000000000..c0b5285d6 --- /dev/null +++ b/docs/src/custom-cms-pages/index.md @@ -0,0 +1,81 @@ +--- +pageClass: twill-doc +--- + +# Custom CMS Pages + +Twill includes the ability to create fully custom pages that includes your navigation, by extending the `twill::layouts.free` layout in a view located in your `resources/views/admin` folder. + +#### Example + +- Create a route in `routes/admin.php` + +```php + Route::name('customPage')->get('/customPage', 'MockController@show'); +``` + +- Add a link to your page in `config/twill-navigation.php` + +```php +return [ + ... + 'customPage' => [ + 'title' => 'Custom page', + 'route' => 'admin.customPage', + ], + ... +]; +``` + +- Add a controller to handle the request + +```php +namespace App\Http\Controllers\Admin; + +class MockController +{ + public function show() + { + return view('admin.customPage'); + } +} +``` + +- And create the view + +```php +@extends('twill::layouts.free') + +@section('customPageContent') + CUSTOM CONTENT GOES HERE +@stop +``` + +You can use Twill's Vue components if you need on those custom pages, for example: + +```php +@extends('twill::layouts.free') + +@section('customPageContent') + + + + +
+
+ +
+
+ +
+
+ + + + + + +
+ Button variant: validate +@stop +``` diff --git a/docs/src/dashboard/index.md b/docs/src/dashboard/index.md new file mode 100644 index 000000000..a736b934b --- /dev/null +++ b/docs/src/dashboard/index.md @@ -0,0 +1,51 @@ +--- +pageClass: twill-doc +--- + +# Dashboard + +Once you have created and configured multiple CRUD modules in your Twill's admin console, you can configure Twill's dashboard in `config/twill.php`. + +For each module that you want to enable in a part or all parts of the dashboad, add an entry to the `dashboard.modules` array, like in the following example: + +```php +return [ + 'dashboard' => [ + 'modules' => [ + 'projects' => [ // module name if you added a morph map entry for it, otherwise FQCN of the model (eg. App\Models\Project) + 'name' => 'projects', // module name + 'label' => 'projects', // optional, if the name of your module above does not work as a label + 'label_singular' => 'project', // optional, if the automated singular version of your name/label above does not work as a label + 'routePrefix' => 'work', // optional, if the module is living under a specific routes group + 'count' => true, // show total count with link to index of this module + 'create' => true, // show link in create new dropdown + 'activity' => true, // show activities on this module in actities list + 'draft' => true, // show drafts of this module for current user + 'search' => true, // show results for this module in global search + ], + ... + ], + ... + ], + ... +]; +``` + +You can also enable a Google Analytics module: + +```php +return [ + 'dashboard' => [ + ..., + 'analytics' => [ + 'enabled' => true, + 'service_account_credentials_json' => storage_path('app/analytics/service-account-credentials.json'), + ], + ], + ... +]; +``` + +It is using Spatie's [Laravel Analytics](https://github.com/spatie/laravel-analytics) package. + +Follow [Spatie's documentation](https://github.com/spatie/laravel-analytics#how-to-obtain-the-credentials-to-communicate-with-google-analytics) to setup a Google service account and download a json file containing your credentials, and provide your Analytics view ID using the `ANALYTICS_VIEW_ID` environment variable. diff --git a/docs/src/featuring-content/index.md b/docs/src/featuring-content/index.md new file mode 100644 index 000000000..f32b3bec7 --- /dev/null +++ b/docs/src/featuring-content/index.md @@ -0,0 +1,90 @@ +--- +pageClass: twill-doc +--- + +# Featuring Content + +Twill's buckets allow you to provide publishers with featured content management screens. You can add multiple pages of buckets anywhere you'd like in your CMS navigation and, in each page, multiple buckets with different rules and accepted modules. In the following example, we will assume that our application has a Guide model and that we want to feature guides on the homepage of our site. Our site's homepage has multiple zones for featured guides: a primary zone, that shows only one featured guide, and a secondary zone, that shows guides in a carousel of maximum 10 items. + +First, you will need to enable the buckets feature. In `config/twill.php`: +```php +'enabled' => [ + 'buckets' => true, +], +``` + +Then, define your buckets configuration: + +```php +'buckets' => [ + 'homepage' => [ + 'name' => 'Home', + 'buckets' => [ + 'home_primary_feature' => [ + 'name' => 'Home primary feature', + 'bucketables' => [ + [ + 'module' => 'guides', + 'name' => 'Guides', + 'scopes' => ['published' => true], + ], + ], + 'max_items' => 1, + ], + 'home_secondary_features' => [ + 'name' => 'Home secondary features', + 'bucketables' => [ + [ + 'module' => 'guides', + 'name' => 'Guides', + 'scopes' => ['published' => true], + ], + ], + 'max_items' => 10, + ], + ], + ], +], +``` + +You can allow mixing modules in a single bucket by adding more modules to the `bucketables` array. +Each `bucketable` should have its [model morph map](https://laravel.com/docs/5.5/eloquent-relationships#polymorphic-relations) defined because features are stored in a polymorphic table. +In your AppServiceProvider, you can do it like the following: + +```php +use Illuminate\Database\Eloquent\Relations\Relation; +... +public function boot() +{ + Relation::morphMap([ + 'guides' => 'App\Models\Guide', + ]); +} +``` + +Finally, add a link to your buckets page in your CMS navigation: + +```php +return [ + 'featured' => [ + 'title' => 'Features', + 'route' => 'admin.featured.homepage', + 'primary_navigation' => [ + 'homepage' => [ + 'title' => 'Homepage', + 'route' => 'admin.featured.homepage', + ], + ], + ], + ... +]; +``` + +By default, the buckets page (in our example, only homepage) will live under the /featured prefix. +But you might need to split your buckets page between sections of your CMS. For example if you want to have the homepage bucket page of our example under the /pages prefix in your navigation, you can use another configuration property: + +```php +'bucketsRoutes' => [ + 'homepage' => 'pages' +] +``` diff --git a/docs/src/form-fields/block-editor.md b/docs/src/form-fields/block-editor.md new file mode 100644 index 000000000..c0593c276 --- /dev/null +++ b/docs/src/form-fields/block-editor.md @@ -0,0 +1,21 @@ +--- +pageClass: twill-doc +--- + +# Block Editor + +![screenshot](/docs/_media/blockeditor.png) + +```php +@formField('block_editor', [ + 'blocks' => ['title', 'quote', 'text', 'image', 'grid', 'test', 'publications', 'news'] +]) +``` + +See [Block editor](/block-editor/) + +| Option | Description | Type/values | Default value | +| :--------------- | :----------------------------------------------------------- | :------------- | :------------ | +| blocks | Array of blocks | array | | +| label | Label used for the button | string | 'Add Content' | +| withoutSeparator | Defines if a separator before the block editor container should be rendered | true
false | false | diff --git a/docs/src/form-fields/browser.md b/docs/src/form-fields/browser.md new file mode 100644 index 000000000..af01c1f10 --- /dev/null +++ b/docs/src/form-fields/browser.md @@ -0,0 +1,158 @@ +--- +pageClass: twill-doc +--- + +# Browser + +![screenshot](/docs/_media/browser.png) + +```php +@formField('browser', [ + 'moduleName' => 'publications', + 'name' => 'publications', + 'label' => 'Publications', + 'max' => 4, +]) +``` + +| Option | Description | Type | Default value | +| :---------- | :------------------------------------------------------------------------------ | :-------| :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| moduleName | Name of the module (single related module) | string | | +| modules | Array of modules (multiple related modules), must include _name_ | array | | +| endpoints | Array of endpoints (multiple related modules), must include _value_ and _label_ | array | | +| max | Max number of attached items | integer | 1 | +| note | Hint message displayed in the field | string | | +| fieldNote | Hint message displayed above the field | string | | +| browserNote | Hint message displayed inside the browser modal | string | | +| itemLabel | Label used for the `Add` button | string | | +| buttonOnTop | Displays the `Add` button above the items | boolean | false | +| wide | Expands the browser modal to fill the viewport | boolean | false | +| sortable | Allows manually sorting the attached items | boolean | true | + +
+ +Browser fields can be used inside as well as outside the block editor. + +Inside the block editor, no migration is needed when using browsers. Refer to the section titled [Adding browser fields to a block](/block-editor/adding-browser-fields-to-a-block.html) for a detailed explanation. + +Outside the block editor, browser fields are used to save `belongsToMany` relationships. The relationships can be stored in Twill's own `related` table or in a custom pivot table. + +## Using browser fields as related items + +The following example demonstrates how to use a browser field to attach `Authors` to `Articles`. + +- Update the `Article` model to add the `HasRelated` trait: + +```php +use A17\Twill\Models\Behaviors\HasRelated; + +class Article extends Model +{ + use HasRelated; + + /* ... */ +} +``` + +- Update `ArticleRepository` to add the browser field to the `$relatedBrowsers` property: + +```php +class ArticleRepository extends ModuleRepository +{ + protected $relatedBrowsers = ['authors']; +} +``` + +- Add the browser field to `resources/views/admin/articles/form.blade.php`: + +```php +@extends('twill::layouts.form') + +@section('contentFields') + ... + + @formField('browser', [ + 'moduleName' => 'authors', + 'name' => 'authors', + 'label' => 'Authors', + 'max' => 4, + ]) +@stop +``` + +## Multiple modules as related items + +You can use the same approach to handle polymorphic relationships through Twill's `related` table. + +- Update `ArticleRepository`: + +```php +class ArticleRepository extends ModuleRepository +{ + protected $relatedBrowsers = ['collaborators']; +} +``` + +- Add the browser field to `resources/views/admin/articles/form.blade.php`: + +```php +@extends('twill::layouts.form') + +@section('contentFields') + ... + + @formField('browser', [ + 'modules' => [ + [ + 'label' => 'Authors', + 'name' => 'authors', + ], + [ + 'label' => 'Editors', + 'name' => 'editors', + ], + ], + 'name' => 'collaborators', + 'label' => 'Collaborators', + 'max' => 4, + ]) +@stop +``` + +- Alternatively, you can use manual endpoints instead of module names: + +```php + @formField('browser', [ + 'endpoints' => [ + [ + 'label' => 'Authors', + 'value' => '/authors/browser', + ], + [ + 'label' => 'Editors', + 'value' => '/editors/browser', + ], + ], + 'name' => 'collaborators', + 'label' => 'Collaborators', + 'max' => 4, + ]) +``` + +## Working with related items + +To retrieve the items in the frontend, you can use the `getRelated` method on models and blocks. It will return of collection of related models in the correct order: + +```php + $item->getRelated('collaborators'); + + // or, in a block: + + $block->getRelated('collaborators'); +``` + +## Using browser fields and custom pivot tables + +Checkout this [Spectrum tutorial](https://spectrum.chat/twill/tips-and-tricks/step-by-step-ii-creating-a-twill-app~37c36601-1198-4c53-857a-a2b47c6d11aa) that walks through the entire process of using browser fields with custom pivot tables. diff --git a/docs/src/form-fields/checkbox.md b/docs/src/form-fields/checkbox.md new file mode 100644 index 000000000..26a223494 --- /dev/null +++ b/docs/src/form-fields/checkbox.md @@ -0,0 +1,26 @@ +--- +pageClass: twill-doc +--- + +# Checkbox + +![screenshot](/docs/_media/checkbox.png) + +```php +@formField('checkbox', [ + 'name' => 'featured', + 'label' => 'Featured' +]) +``` + +| Option | Description | Type | Default value | +| :------------------ | :------------------------------------------------------ | :-------------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| note | Hint message displayed above the field | string | | +| default | Sets a default value | boolean | false | +| disabled | Disables the field | boolean | false | +| requireConfirmation | Displays a confirmation dialog when modifying the field | boolean | false | +| confirmTitleText | The title of the confirmation dialog | string | 'Confirm selection' | +| confirmMessageText | The text of the confirmation dialog | string | 'Are you sure you want to change this option ?' | +| border | Draws a border around the field | boolean | false | diff --git a/docs/src/form-fields/color.md b/docs/src/form-fields/color.md new file mode 100644 index 000000000..6a7e0e131 --- /dev/null +++ b/docs/src/form-fields/color.md @@ -0,0 +1,29 @@ +--- +pageClass: twill-doc +--- + +# Color + +![screenshot](/docs/_media/color.png) + +```php +@formField('color', [ + 'name' => 'main_color', + 'label' => 'Main color' +]) +``` + +| Option | Description | Type | Default value | +| :------ | :------------------ | :------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | + +A migration to save a `color` field would be: + +```php +Schema::table('posts', function (Blueprint $table) { + ... + $table->string('main_color', 10)->nullable(); + ... +}); +``` diff --git a/docs/src/form-fields/conditional-fields.md b/docs/src/form-fields/conditional-fields.md new file mode 100644 index 000000000..87a0fa741 --- /dev/null +++ b/docs/src/form-fields/conditional-fields.md @@ -0,0 +1,56 @@ +--- +pageClass: twill-doc +--- + +# Conditional Fields + +You can conditionally display fields based on the values of other fields in your form. For example, if you wanted to display a video embed text field only if the type of article, a radio field, is "video" you'd do something like the following: + +```php +@formField('radios', [ + 'name' => 'type', + 'label' => 'Article type', + 'default' => 'long_form', + 'inline' => true, + 'options' => [ + [ + 'value' => 'long_form', + 'label' => 'Long form article' + ], + [ + 'value' => 'video', + 'label' => 'Video article' + ] + ] +]) + +@formConnectedFields([ + 'fieldName' => 'type', + 'fieldValues' => 'video', + 'renderForBlocks' => true/false # (depending on regular form vs block form) +]) + @formField('input', [ + 'name' => 'video_embed', + 'label' => 'Video embed' + ]) +@endformConnectedFields +``` +Here's an example based on a checkbox field where the value is either true or false: + +```php +@formField('checkbox', [ + 'name' => 'vertical_article', + 'label' => 'Vertical Story' +]) + +@formConnectedFields([ + 'fieldName' => 'vertical_article', + 'fieldValues' => true, + 'renderForBlocks' => true/false # (depending on regular form vs block form) +]) + @formField('medias', [ + 'name' => 'vertical_image', + 'label' => 'Vertical Image', + ]) +@endformConnectedFields +``` diff --git a/docs/src/form-fields/datepicker.md b/docs/src/form-fields/datepicker.md new file mode 100644 index 000000000..1f6bd3aa4 --- /dev/null +++ b/docs/src/form-fields/datepicker.md @@ -0,0 +1,51 @@ +--- +pageClass: twill-doc +--- + +# Datepicker + +![screenshot](/docs/_media/datepicker.png) + +```php +@formField('date_picker', [ + 'name' => 'event_date', + 'label' => 'Event date', + 'minDate' => '2017-09-10 12:00', + 'maxDate' => '2017-12-10 12:00' +]) +``` + +| Option | Description | Type/values | Default value | +| :---------- | :----------------------------------------------------------- | :-------------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| minDate | Minimum selectable date | string | | +| maxDate | Maximum selectable date | string | | +| withTime | Define if the field will display the time selector | true
false | true | +| time24Hr | Pick time with a 24h picker instead of AM/PM | true
false | false | +| allowClear | Adds a button to clear the field | true
false | false | +| allowInput | Allow manually editing the selected date in the field | true
false | false | +| altFormat | Format used by [flatpickr](https://flatpickr.js.org/formatting/) | string | F j, Y | +| hourIncrement | Time picker hours increment | number | 1 | +| minuteIncrement | Time picker minutes increment | number | 30 | +| note | Hint message displayed above the field | string | | +| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | + + +A migration to save a `date_picker` field would be: + +```php +Schema::table('posts', function (Blueprint $table) { + ... + $table->date('event_date')->nullable(); + ... +}); +// OR +Schema::table('posts', function (Blueprint $table) { + ... + $table->dateTime('event_date')->nullable(); + ... +}); +``` + +When used in a [block](/block-editor/creating-a-block-editor.html), no migration is needed. diff --git a/docs/src/form-fields/files.md b/docs/src/form-fields/files.md new file mode 100644 index 000000000..416ba7e5b --- /dev/null +++ b/docs/src/form-fields/files.md @@ -0,0 +1,52 @@ +--- +pageClass: twill-doc +--- + +# Files + +![screenshot](/docs/_media/files.png) + +```php +@formField('files', [ + 'name' => 'single_file', + 'label' => 'Single file', + 'note' => 'Add one file (per language)' +]) + +@formField('files', [ + 'name' => 'files', + 'label' => 'Files', + 'max' => 4, +]) +``` + +| Option | Description | Type/values | Default value | +| :------------- | :---------------------------------------- | :------------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| itemLabel | Label used for the `Add` button | string | | +| max | Max number of attached items | integer | 1 | +| fieldNote | Hint message displayed above the field | string | | +| note | Hint message displayed in the field | string | | +| buttonOnTop | Displays the `Add` button above the files | true
false | false | + + +Similar to the media formField, to make the file field work, you have to include the `HasFiles` trait in your module's [Model](/crud-modules/models.html), and include `HandleFiles` trait in your module's [Repository](/crud-modules/repositories.html). At last, add the `filesParams` configuration array in your model. +```php +public $filesParams = ['file_role', ...]; // a list of file roles +``` + +Learn more at [Model](/crud-modules/models.html), [Repository](/crud-modules/repositories.html). + +If you are using the file formField in a block, you have to define the `files` key in `config/twill.php`. Add it under `block_editor` key and at the same level as `crops` key: +```php +return [ + 'block_editor' => [ + 'crops' => [ + ... + ], + 'files' => ['file_role1', 'file_role2', ...] + ] +``` + +No migration is needed to save `files` form fields. diff --git a/docs/src/form-fields/index.md b/docs/src/form-fields/index.md new file mode 100644 index 000000000..a0f387dd7 --- /dev/null +++ b/docs/src/form-fields/index.md @@ -0,0 +1,43 @@ +--- +pageClass: twill-doc +--- + +# Form Fields + +Your module `form` view should look something like this (`resources/views/admin/moduleName/form.blade.php`): + +```php +@extends('twill::layouts.form') +@section('contentFields') + @formField('...', [...]) + ... +@stop +``` + +The idea of the `contentFields` section is to contain your most important fields and, if applicable, the block editor as the last field. + +If you have other fields, like attributes, relationships, extra images, file attachments or repeaters, you'll want to add a `fieldsets` section after the `contentFields` section and use the `@formFieldset` directive to create new ones like in the following example: + +```php +@extends('twill::layouts.form', [ + 'additionalFieldsets' => [ + ['fieldset' => 'attributes', 'label' => 'Attributes'], + ] +]) + +@section('contentFields') + @formField('...', [...]) + ... +@stop + +@section('fieldsets') + @formFieldset(['id' => 'attributes', 'title' => 'Attributes']) + @formField('...', [...]) + ... + @endformFieldset +@stop +``` + +The additional fieldsets array passed to the form layout will display a sticky navigation of your fieldset on scroll. +You can also rename the content section by passing a `contentFieldsetLabel` property to the layout, or disable it entirely using +`'disableContentFieldset' => true`. diff --git a/docs/src/form-fields/input.md b/docs/src/form-fields/input.md new file mode 100644 index 000000000..622e7b2b4 --- /dev/null +++ b/docs/src/form-fields/input.md @@ -0,0 +1,76 @@ +--- +pageClass: twill-doc +--- + +# Input + +![screenshot](/docs/_media/input.png) + +```php +@formField('input', [ + 'name' => 'subtitle', + 'label' => 'Subtitle', + 'maxlength' => 100, + 'required' => true, + 'note' => 'Hint message goes here', + 'placeholder' => 'Placeholder goes here', +]) + +@formField('input', [ + 'translated' => true, + 'name' => 'subtitle_translated', + 'label' => 'Subtitle (translated)', + 'maxlength' => 250, + 'required' => true, + 'note' => 'Hint message goes here', + 'placeholder' => 'Placeholder goes here', + 'type' => 'textarea', + 'rows' => 3 +]) +``` + +| Option | Description | Type/values | Default value | +| :---------- | :------------------------------------------------------------------------------------------------------------------------| :------------------------------------------------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| type | Type of input field | text
texarea
email
number
password | text | +| translated | Defines if the field is translatable | true
false | false | +| maxlength | Max character count of the field | integer | | +| note | Hint message displayed above the field | string | | +| placeholder | Text displayed as a placeholder in the field | string | | +| prefix | Text displayed as a prefix in the field | string | | +| rows | Sets the number of rows in a textarea | integer | 5 | +| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | +| disabled | Disables the field | true
false | false | +| readonly | Sets the field as readonly | true
false | false | +| default | Sets a default value if empty | string | | + + +A migration to save an `input` field would be: + +```php +Schema::table('articles', function (Blueprint $table) { + ... + $table->string('subtitle', 100)->nullable(); + ... + +}); +// OR +Schema::table('article_translations', function (Blueprint $table) { + ... + $table->string('subtitle', 250)->nullable(); + ... +}); +``` + +If this `input` field is used for longer strings then the migration would be: + +```php +Schema::table('articles', function (Blueprint $table) { + ... + $table->text('subtitle')->nullable(); + ... +}); +``` + +When used in a [block](/block-editor/creating-a-block-editor.html), no migration is needed. diff --git a/docs/src/form-fields/map.md b/docs/src/form-fields/map.md new file mode 100644 index 000000000..3925b2dde --- /dev/null +++ b/docs/src/form-fields/map.md @@ -0,0 +1,72 @@ +--- +pageClass: twill-doc +--- + +# Map + +![screenshot](/docs/_media/map.png) + +```php +@formField('map', [ + 'name' => 'location', + 'label' => 'Location', + 'showMap' => true, +]) +``` + +| Option | Description | Type/values | Default value | +| :--------------- | :---------------------------------------------------------- | :-------------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| showMap | Adds a button to toggle the map visibility | true
false | true | +| openMap | Used with `showMap`, initialize the field with the map open | true
false | false | +| saveExtendedData | Enables saving Bounding Box Coordinates and Location types | true
false | false | + +This field requires that you provide a `GOOGLE_MAPS_API_KEY` variable in your .env file. + +A migration to save a `map` field would be: + +```php +Schema::table('posts', function (Blueprint $table) { + ... + $table->json('location')->nullable(); + ... +}); +``` + +The field used should also be casted as an array in your model: + +```php +public $casts = [ + 'location' => 'array', +]; +``` + +When used in a [block](/block-editor/creating-a-block-editor.html), no migration is needed. + +#### Example of data stored in the Database: + +Default data: + +```javascript +{ + "latlng": "48.85661400000001|2.3522219", + "address": "Paris, France" +} +``` + +Extended data: + +```javascript +{ + "latlng": "51.1808302|-2.256022799999999", + "address": "Warminster BA12 7LG, United Kingdom", + "types": ["point_of_interest", "establishment"], + "boundingBox": { + "east": -2.25289275, + "west": -2.257066149999999, + "north": 51.18158853029149, + "south": 51.17889056970849 + } +} +``` diff --git a/docs/src/form-fields/medias.md b/docs/src/form-fields/medias.md new file mode 100644 index 000000000..6472b90be --- /dev/null +++ b/docs/src/form-fields/medias.md @@ -0,0 +1,106 @@ +--- +pageClass: twill-doc +--- + +# Medias + +![screenshot](/docs/_media/medias.png) + +```php +@formField('medias', [ + 'name' => 'cover', + 'label' => 'Cover image', + 'note' => 'Also used in listings', + 'fieldNote' => 'Minimum image width: 1500px' +]) + +@formField('medias', [ + 'name' => 'slideshow', + 'label' => 'Slideshow', + 'max' => 5, + 'fieldNote' => 'Minimum image width: 1500px' +]) +``` + +| Option | Description | Type/values | Default value | +| :------------- | :--------------------------------------------------- | :------------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| translated | Defines if the field is translatable | true
false | false | +| max | Max number of attached items | integer | 1 | +| fieldNote | Hint message displayed above the field | string | | +| note | Hint message displayed in the field | string | | +| buttonOnTop | Displays the `Attach images` button above the images | true
false | false | + + +Right after declaring the `medias` formField in the blade template file, you still need to do a few things to make it work properly. + +If the formField is in a static content form, you have to include the `HasMedias` Trait in your module's [Model](/crud-modules/models.html) and inlcude `HandleMedias` in your module's [Repository](/crud-modules/repositories.html). In addition, you have to uncomment the `$mediasParams` section in your Model file to let the model know about fields you'd like to save from the form. + +Learn more about how Twill's media configurations work at [Model](/crud-modules/models.html), [Repository](/crud-modules/repositories.html), [Media Library Role & Crop Params](/media-library/image-rendering-service.html) + +If the formField is used inside a block, you need to define the `mediasParams` at `config/twill.php` under `crops` key, and you are good to go. You could checkout [Twill Default Configuration](/block-editor/default-configuration.html) and [Rendering Blocks](/block-editor/rendering-blocks.html) for references. + +If the formField is used inside a repeater, you need to define the `mediasParams` at `config/twill.php` under `block_editor.crops`. + +If you need medias fields to be translatable (ie. publishers can select different images for each locale), set the `twill.media_library.translated_form_fields` configuration value to `true`. + +##### Example + +To add a `medias` form field in a form, first add `$mediaParams` to the model. + +```php + [ + 'default' => [ + [ + 'name' => 'default', + 'ratio' => 16 / 9, + ], + ], + 'mobile' => [ + [ + 'name' => 'mobile', + 'ratio' => 1, + ], + ], + ], + ]; + + ... +} +``` + +Then, add the form field to the `form.blade.php` file. + +```php +@extends('twill::layouts.form') + +@section('contentFields') + + ... + + @formField('medias', [ + 'name' => 'cover', + 'label' => 'Cover image', + ]) + + ... +@stop +``` + +No migration is needed to save `medias` form fields. diff --git a/docs/src/form-fields/multi-select-inline.md b/docs/src/form-fields/multi-select-inline.md new file mode 100644 index 000000000..56267b92e --- /dev/null +++ b/docs/src/form-fields/multi-select-inline.md @@ -0,0 +1,39 @@ +--- +pageClass: twill-doc +--- + +# Multi Select Inline + +![screenshot](/docs/_media/multiselectinline.png) + +```php +@formField('multi_select', [ + 'name' => 'sectors', + 'label' => 'Sectors', + 'unpack' => false, + 'options' => [ + [ + 'value' => 'arts', + 'label' => 'Arts & Culture' + ], + [ + 'value' => 'finance', + 'label' => 'Banking & Finance' + ], + [ + 'value' => 'civic', + 'label' => 'Civic & Public' + ], + [ + 'value' => 'design', + 'label' => 'Design & Architecture' + ], + [ + 'value' => 'education', + 'label' => 'Education' + ] + ] +]) +``` + +See [Multi select](/form-fields/multi-select.html) for more information on how to implement the field with static and dynamic values. diff --git a/docs/src/form-fields/multi-select.md b/docs/src/form-fields/multi-select.md new file mode 100644 index 000000000..0442006ad --- /dev/null +++ b/docs/src/form-fields/multi-select.md @@ -0,0 +1,170 @@ +--- +pageClass: twill-doc +--- + +# Multi Select + +![screenshot](/docs/_media/multiselectunpacked.png) + +```php +@formField('multi_select', [ + 'name' => 'sectors', + 'label' => 'Sectors', + 'min' => 1, + 'max' => 2, + 'options' => [ + [ + 'value' => 'arts', + 'label' => 'Arts & Culture' + ], + [ + 'value' => 'finance', + 'label' => 'Banking & Finance' + ], + [ + 'value' => 'civic', + 'label' => 'Civic & Public' + ], + [ + 'value' => 'design', + 'label' => 'Design & Architecture' + ], + [ + 'value' => 'education', + 'label' => 'Education' + ] + ] +]) +``` + +| Option | Description | Type/values | Default value | +| :---------- | :----------------------------------------------------------- | :-------------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| min | Minimum number of selectable options | integer | | +| max | Maximum number of selectable options | integer | | +| options | Array of options for the dropdown, must include _value_ and _label_ | array | | +| unpack | Defines if the multi select will be displayed as an open list of options | true
false | true | +| columns | Aligns the options on a grid with a given number of columns | integer | 0 (off) | +| searchable | Filter the field values while typing | true
false | false | +| note | Hint message displayed above the field | string | | +| placeholder | Text displayed as a placeholder in the field | string | | +| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | +| disabled | Disables the field | true
false | false | + + +There are several ways to implement a `multi_select` form field. + +## Multi select with static values + +Sometimes you just have a set of values that are static. + +In this case that it can be implemented as follows: + +- Create the database migration to store a JSON or LONGTEXT: +```php +Schema::table('posts', function (Blueprint $table) { + ... + $table->json('sectors')->nullable(); + ... +}); + +// OR +Schema::table('posts', function (Blueprint $table) { + ... + $table->longtext('sectors')->nullable(); + ... +}); +``` + +- In your model add an accessor and a mutator: +```php +public function getSectorsAttribute($value) +{ + return collect(json_decode($value))->map(function($item) { + return ['id' => $item]; + })->all(); +} + +public function setSectorsAttribute($value) +{ + $this->attributes['sectors'] = collect($value)->filter()->values(); +} +``` + +- Cast the field to `array`: +```php +protected $casts = [ + 'sectors' => 'array' +] +``` + +## Multi select with dynamic values + +Sometimes the content for the `multi_select` is coming from another model. + +In this case that it can be implemented as follows: + +- Create a Sectors [module](/crud-modules/cli-generator.html) + +``` +php artisan twill:module sectors +``` + +- Create a migration for a pivot table. + +``` +php artisan make:migration create_post_sector_table +``` + +- Use Twill's `createDefaultRelationshipTableFields` to set it up: + +```php +public function up() +{ + Schema::create('post_sector', function (Blueprint $table) { + createDefaultRelationshipTableFields($table, 'sector', 'post'); + $table->integer('position')->unsigned()->index(); + }); +} +``` + +- In your model, add a `belongsToMany` relationship: + +```php +public function sectors() { + return $this->belongsToMany('App\Models\Sector'); +} +``` + +- In your repository, make sure to sync the association when saving: + +```php +public function afterSave($object, $fields) +{ + $object->sectors()->sync($fields['sectors'] ?? []); + + parent::afterSave($object, $fields); +} +``` + +- In your controller, add to the formData the collection of options: +```php +protected function formData($request) +{ + return [ + 'sectors' => app()->make(SectorRepository::class)->listAll() + ]; +} +``` + +- In the form, we can now add the field: +```php +@formField('multi_select', [ + 'name' => 'sectors', + 'label' => 'Sectors', + 'options' => $sectors +]) +``` + +When used in a [block](/block-editor/creating-a-block-editor.html), no migration is needed. diff --git a/docs/src/form-fields/multiple-checkboxes.md b/docs/src/form-fields/multiple-checkboxes.md new file mode 100644 index 000000000..7eb2c23f5 --- /dev/null +++ b/docs/src/form-fields/multiple-checkboxes.md @@ -0,0 +1,44 @@ +--- +pageClass: twill-doc +--- + +# Multiple Checkboxes + +![screenshot](/docs/_media/checkboxes.png) + +```php +@formField('checkboxes', [ + 'name' => 'sectors', + 'label' => 'Sectors', + 'note' => '3 sectors max & at least 1 sector', + 'min' => 1, + 'max' => 3, + 'inline' => true, + 'options' => [ + [ + 'value' => 'arts', + 'label' => 'Arts & Culture' + ], + [ + 'value' => 'finance', + 'label' => 'Banking & Finance' + ], + [ + 'value' => 'civic', + 'label' => 'Civic & Public' + ], + ] +]) +``` + +| Option | Description | Type | Default value | +| :------ | :------------------------------------------------------------------ | :-------| :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| min | Minimum number of selectable options | integer | | +| max | Maximum number of selectable options | integer | | +| options | Array of options for the dropdown, must include _value_ and _label_ | array | | +| inline | Defines if the options are displayed on one or multiple lines | boolean | false | +| note | Hint message displayed above the field | string | | +| border | Draws a border around the field | boolean | false | +| columns | Aligns the options on a grid with a given number of columns | integer | 0 (off) | diff --git a/docs/src/form-fields/radios.md b/docs/src/form-fields/radios.md new file mode 100644 index 000000000..5d639673e --- /dev/null +++ b/docs/src/form-fields/radios.md @@ -0,0 +1,45 @@ +--- +pageClass: twill-doc +--- + +# Radios + +![screenshot](/docs/_media/radios.png) + +```php +@formField('radios', [ + 'name' => 'discipline', + 'label' => 'Discipline', + 'default' => 'civic', + 'inline' => true, + 'options' => [ + [ + 'value' => 'arts', + 'label' => 'Arts & Culture' + ], + [ + 'value' => 'finance', + 'label' => 'Banking & Finance' + ], + [ + 'value' => 'civic', + 'label' => 'Civic & Public' + ], + ] +]) +``` + +| Option | Description | Type | Default value | +| :------------------ | :------------------------------------------------------------------ | :------ | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| note | Hint message displayed above the field | string | | +| options | Array of options for the dropdown, must include _value_ and _label_ | array | | +| inline | Defines if the options are displayed on one or multiple lines | boolean | false | +| default | Sets a default value | string | | +| requireConfirmation | Displays a confirmation dialog when modifying the field | boolean | false | +| confirmTitleText | The title of the confirmation dialog | string | 'Confirm selection' | +| confirmMessageText | The text of the confirmation dialog | string | 'Are you sure you want to change this option ?' | +| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | boolean | false | +| border | Draws a border around the field | boolean | false | +| columns | Aligns the options on a grid with a given number of columns | integer | 0 (off) | diff --git a/docs/src/form-fields/repeater.md b/docs/src/form-fields/repeater.md new file mode 100644 index 000000000..eea05a19a --- /dev/null +++ b/docs/src/form-fields/repeater.md @@ -0,0 +1,162 @@ +--- +pageClass: twill-doc +--- + +# Repeater + +![screenshot](/docs/_media/repeater.png) + +```php +@formField('repeater', ['type' => 'video']) +``` + +| Option | Description | Type | Default value | +| :----------- | :-------------------------------------------- | :-------| :------------- | +| type | Type of repeater items | string | | +| name | Name of the field | string | same as `type` | +| buttonAsLink | Displays the `Add` button as a centered link | boolean | false | + +
+ +Repeater fields can be used inside as well as outside the block editor. + +Inside the block editor, repeater blocks share the same model as regular blocks. By reading the section on the [block editor](/block-editor/) first, you will get a good overview of how to create and define repeater blocks for your project. No migration is needed when using repeater blocks. Refer to the section titled [Adding repeater fields to a block](/block-editor/adding-repeater-fields-to-a-block.html) for a detailed explanation. + +Outside the block editor, repeater fields are used to save `hasMany` or `morphMany` relationships. + +## Using repeater fields + +The following example demonstrates how to define a relationship between `Team` and `TeamMember` modules to implement a `team-member` repeater. + +- Create the modules. Make sure to enable the `position` feature on the `TeamMember` module: + +``` +php artisan twill:make:module Team +php artisan twill:make:module TeamMember -P +``` + +- Update the `create_team_members_tables` migration. Add the `team_id` foreign key used for the `TeamMember—Team` relationship: + +```php +class CreateTeamMembersTables extends Migration +{ + public function up() + { + Schema::create('team_members', function (Blueprint $table) { + /* ... */ + + $table->foreignId('team_id') + ->constrained() + ->onUpdate('cascade') + ->onDelete('cascade'); + }); + } +} +``` + +- Run the migrations: + +``` +php artisan migrate +``` + +- Update the `Team` model. Define the `members` relationship. The results should be ordered by position: + +```php +class Team extends Model +{ + /* ... */ + + public function members() + { + return $this->hasMany(TeamMember::class)->orderBy('position'); + } +} +``` + +- Update the `TeamMember` model. Add `team_id` to the `fillable` array: + +```php +class TeamMember extends Model +{ + protected $fillable = [ + /* ... */ + 'team_id', + ]; +} +``` + +- Update `TeamRepository`. Override the `afterSave` and `getFormFields` methods to process the repeater field: + +```php +class TeamRepository extends ModuleRepository +{ + /* ... */ + + public function afterSave($object, $fields) + { + $this->updateRepeater($object, $fields, 'members', 'TeamMember', 'team-member'); + parent::afterSave($object, $fields); + } + + public function getFormFields($object) + { + $fields = parent::getFormFields($object); + $fields = $this->getFormFieldsForRepeater($object, $fields, 'members', 'TeamMember', 'team-member'); + return $fields; + } +} +``` + +- Add the repeater Blade template: + +Create file `resources/views/admin/repeaters/team-member.blade.php`: + +```php +@twillRepeaterTitle('Team Member') +@twillRepeaterTrigger('Add member') +@twillRepeaterGroup('app') + +@formField('input', [ + 'name' => 'title', + 'label' => 'Title', + 'required' => true, +]) + +... +``` + +- Add the repeater field to the form: + +Update file `resources/views/admin/teams/form.blade.php`: + +```php +@extends('twill::layouts.form') + +@section('contentFields') + ... + + @formField('repeater', ['type' => 'team-member']) +@stop +``` + +- Finishing up: + +Add both modules to your `admin.php` routes. Add the `Team` module to your `twill-navigation.php` config and you are done! + +## Dynamic repeater titles + +In Twill >= 2.5, you can use the `@twillRepeaterTitleField` directive to include the value of a given field in the title of the repeater items. This directive also accepts a `hidePrefix` option to hide the generic repeater title: + +```php +@twillRepeaterTitle('Person') +@twillRepeaterTitleField('name', ['hidePrefix' => true]) +@twillRepeaterTrigger('Add person') +@twillRepeaterGroup('app') + +@formField('input', [ + 'name' => 'name', + 'label' => 'Name', + 'required' => true, +]) +``` diff --git a/docs/src/form-fields/select-unpacked.md b/docs/src/form-fields/select-unpacked.md new file mode 100644 index 000000000..cc5441305 --- /dev/null +++ b/docs/src/form-fields/select-unpacked.md @@ -0,0 +1,53 @@ +--- +pageClass: twill-doc +--- + +# Select Unpacked + +![screenshot](/docs/_media/selectunpacked.png) + +```php +@formField('select', [ + 'name' => 'discipline', + 'label' => 'Discipline', + 'unpack' => true, + 'options' => [ + [ + 'value' => 'arts', + 'label' => 'Arts & Culture' + ], + [ + 'value' => 'finance', + 'label' => 'Banking & Finance' + ], + [ + 'value' => 'civic', + 'label' => 'Civic & Public' + ], + [ + 'value' => 'design', + 'label' => 'Design & Architecture' + ], + [ + 'value' => 'education', + 'label' => 'Education' + ], + [ + 'value' => 'entertainment', + 'label' => 'Entertainment' + ], + ] +]) +``` + +A migration to save the above `select` field would be: + +```php +Schema::table('posts', function (Blueprint $table) { + ... + $table->string('discipline')->nullable(); + ... +}); +``` + +When used in a [block](/block-editor/creating-a-block-editor.html), no migration is needed. diff --git a/docs/src/form-fields/select.md b/docs/src/form-fields/select.md new file mode 100644 index 000000000..73ba861eb --- /dev/null +++ b/docs/src/form-fields/select.md @@ -0,0 +1,53 @@ +--- +pageClass: twill-doc +--- + +# Select + +![screenshot](/docs/_media/select.png) + +```php +@formField('select', [ + 'name' => 'office', + 'label' => 'Office', + 'placeholder' => 'Select an office', + 'options' => [ + [ + 'value' => 1, + 'label' => 'New York' + ], + [ + 'value' => 2, + 'label' => 'London' + ], + [ + 'value' => 3, + 'label' => 'Berlin' + ] + ] +]) +``` + +| Option | Description | Type/values | Default value | +| :---------- | :----------------------------------------------------------- | :-------------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| options | Array of options for the dropdown, must include _value_ and _label_ | array | | +| unpack | Defines if the select will be displayed as an open list of options | true
false | false | +| columns | Aligns the options on a grid with a given number of columns | integer | 0 (off) | +| searchable | Filter the field values while typing | true
false | false | +| note | Hint message displayed above the field | string | | +| placeholder | Text displayed as a placeholder in the field | string | | +| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | + +A migration to save a `select` field would be: + +```php +Schema::table('posts', function (Blueprint $table) { + ... + $table->integer('office')->nullable(); + ... +}); +``` + +When used in a [block](/block-editor/creating-a-block-editor.html), no migration is needed. diff --git a/docs/src/form-fields/timepicker.md b/docs/src/form-fields/timepicker.md new file mode 100644 index 000000000..1647a2453 --- /dev/null +++ b/docs/src/form-fields/timepicker.md @@ -0,0 +1,43 @@ +--- +pageClass: twill-doc +--- + +# Timepicker + +```php +@formField('time_picker', [ + 'name' => 'event_time', + 'label' => 'Event time', +]) +``` + +| Option | Description | Type/values | Default value | +| :---------- | :----------------------------------------------------------- | :-------------- | :------------ | +| name | Name of the field | string | | +| label | Label of the field | string | | +| time24Hr | Pick time with a 24h picker instead of AM/PM | true
false | false | +| allowClear | Adds a button to clear the field | true
false | false | +| allowInput | Allow manually editing the selected date in the field | true
false | false | +| hourIncrement | Time picker hours increment | number | 1 | +| minuteIncrement | Time picker minutes increment | number | 30 | +| altFormat | Format used by [flatpickr](https://flatpickr.js.org/formatting/) | string | h:i | +| note | Hint message displayed above the field | string | | +| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | + +A migration to save a `time_picker` field would be: + +```php +Schema::table('posts', function (Blueprint $table) { + ... + $table->time('event_time')->nullable(); + ... +}); +// OR, if you are merging with a date field +Schema::table('posts', function (Blueprint $table) { + ... + $table->dateTime('event_date')->nullable(); + ... +}); +``` + +When used in a [block](/block-editor/creating-a-block-editor.html), no migration is needed. diff --git a/docs/src/form-fields/wysiwyg.md b/docs/src/form-fields/wysiwyg.md new file mode 100644 index 000000000..af1228a32 --- /dev/null +++ b/docs/src/form-fields/wysiwyg.md @@ -0,0 +1,100 @@ +--- +pageClass: twill-doc +--- + +# WYSIWYG + +![screenshot](/docs/_media/wysiwyg.png) + +```php +@formField('wysiwyg', [ + 'name' => 'case_study', + 'label' => 'Case study text', + 'toolbarOptions' => ['list-ordered', 'list-unordered'], + 'placeholder' => 'Case study text', + 'maxlength' => 200, + 'note' => 'Hint message', +]) + +@formField('wysiwyg', [ + 'name' => 'case_study', + 'label' => 'Case study text', + 'toolbarOptions' => [ [ 'header' => [1, 2, false] ], 'list-ordered', 'list-unordered', [ 'indent' => '-1'], [ 'indent' => '+1' ] ], + 'placeholder' => 'Case study text', + 'maxlength' => 200, + 'editSource' => true, + 'note' => 'Hint message', +]) +``` + +By default, the WYSIWYG field is based on [Quill](https://quilljs.com/). + +You can add all [toolbar options](https://quilljs.com/docs/modules/toolbar/) from Quill with the `toolbarOptions` key. + +For example, this configuration will render a `wysiwyg` field with almost all features from Quill and Snow theme. + +```php + @formField('wysiwyg', [ + 'name' => 'case_study', + 'label' => 'Case study text', + 'toolbarOptions' => [ + ['header' => [2, 3, 4, 5, 6, false]], + 'bold', + 'italic', + 'underline', + 'strike', + ["script" => "super"], + ["script" => "sub"], + "blockquote", + "code-block", + ['list' => 'ordered'], + ['list' => 'bullet'], + ['indent' => '-1'], + ['indent' => '+1'], + ["align" => []], + ["direction" => "rtl"], + 'link', + "clean", + ], + 'placeholder' => 'Case study text', + 'maxlength' => 200, + 'editSource' => true, + 'note' => 'Hint message`', + ]) +``` + +Note that Quill outputs CSS classes in the HTML for certain toolbar modules (indent, font, align, etc.), and that the image module is not integrated with Twill's media library. It outputs the base64 representation of the uploaded image. It is not a recommended way of using and storing images, prefer using one or multiple `medias` form fields or blocks fields for flexible content. This will give you greater control over your frontend output. + +| Option | Description | Type/values | Default value | +| :------------- | :----------------------------------------------------------- | :--------------------------------------------------------- | :-------------------------------------- | +| name | Name of the field | string | | +| label | Label of the field | string | | +| type | Type of wysiwyg field | quill
tiptap | quill | +| toolbarOptions | Array of options/tools that will be displayed in the editor | [Quill options](https://quilljs.com/docs/modules/toolbar/) | bold
italic
underline
link | +| editSource | Displays a button to view source code | true
false | false | +| hideCounter | Hide the character counter displayed at the bottom | true
false | false | +| limitHeight | Limit the editor height from growing beyond the viewport | true
false | false | +| translated | Defines if the field is translatable | true
false | false | +| maxlength | Max character count of the field | integer | 255 | +| note | Hint message displayed above the field | string | | +| placeholder | Text displayed as a placeholder in the field | string | | +| required | Displays an indicator that this field is required
A backend validation rule is required to prevent users from saving | true
false | false | + + +A migration to save a `wysiwyg` field would be: + +```php +Schema::table('articles', function (Blueprint $table) { + ... + $table->text('case_study')->nullable(); + ... + +}); +// OR +Schema::table('article_translations', function (Blueprint $table) { + ... + $table->text('case_study')->nullable(); + ... +}); +``` +When used in a [block](/block-editor/creating-a-block-editor.html), no migration is needed. diff --git a/docs/.sections/getting-started/configuration.md b/docs/src/getting-started/configuration.md similarity index 96% rename from docs/.sections/getting-started/configuration.md rename to docs/src/getting-started/configuration.md index daabe76ab..66f5af747 100644 --- a/docs/.sections/getting-started/configuration.md +++ b/docs/src/getting-started/configuration.md @@ -1,10 +1,14 @@ -### Configuration +--- +pageClass: twill-doc +--- + +# Configuration As mentioned above, Twill's default configuration allows you to get up and running quickly by providing environment variables. Of course, you can override any of Twill's provided configurations values from the empty `config/twill.php` file that was published in your app when you ran the `twill:install` command. -#### Enabled features +## Enabled features You can opt-in or opt-out from certain Twill features using the `enabled` array in your `config/twill.php` file. Values presented in the following code snippet are Twill's defaults: @@ -45,7 +49,7 @@ return [ This is true to all following configuration arrays. -#### Global configuration +## Global configuration By default, Twill uses Laravel default application namespace `App`. You can provide your own using the `namespace` configuration in your `config/twill.php` file: @@ -124,7 +128,7 @@ return [ ]; ``` -**Migrations configuration** +#### Migrations configuration Since Laravel 5.8, migrations generated by Laravel use big integers on the `id` column. Twill migrations helpers can be configured to use regular integers for backwards compatibility. @@ -147,7 +151,7 @@ return [ ]; ``` -**Locale configuration** +#### Locale configuration Since Twill 2.0, Twill's own UI can be translated using lang files. Users can choose their own preferred languages in their own settings, but you might actually want to default to another language than English for all your users. @@ -160,7 +164,7 @@ return [ ]; ``` -**Publisher date and time format configuration** +#### Publisher date and time format configuration To change the format of the publication fields when using `publish_start_date` and `publish_end_date` on your model you can change these keys in `twill.php`. @@ -174,7 +178,7 @@ return [ ]; ``` -**Multiple subdomains CMS routing** +#### Multiple subdomains CMS routing ```php =5.7`) and PostgreSQL(`>=9.3`). + +## Summary + +| | Supported versions | Recommended version | +|:-----------|:------------------:|:-------------------:| +| PHP | >= 7.1 | 8.0 | +| Laravel | >= 5.8 | 8.x | +| npm | >= 5.7 | 6.13 | +| MySQL | >= 5.7 | 5.7 | +| PostgreSQL | >= 9.3 | 10 | + diff --git a/docs/.sections/getting-started/installation.md b/docs/src/getting-started/installation.md similarity index 92% rename from docs/.sections/getting-started/installation.md rename to docs/src/getting-started/installation.md index 1fc020dad..6228f4319 100644 --- a/docs/.sections/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -1,13 +1,18 @@ -### Installation +--- +pageClass: twill-doc +--- + +# Installation + +## Composer -#### Composer Twill is a package for Laravel applications, installable through Composer: ```bash composer require area17/twill:"^2.0" ``` -#### Artisan +## Artisan Run the `install` Artisan command: @@ -28,7 +33,7 @@ Twill's `install` command consists of: - publishing Twill's assets for the admin console UI. - prompting you to create a superadmin user. -#### .env +## .env By default, Twill's admin console is available at `admin.domain.test`. This is assuming that your .env `APP_URL` variable does not include a scheme (`http`/`https`): @@ -68,11 +73,11 @@ When running on 2 different subdomains (which is the default configuration as se SESSION_DOMAIN=.domain.test ``` -#### Accessing the admin console +## Accessing the admin console At this point, you should be able to login at `admin.domain.test`, `manage.domain.test` or `domain.test/admin` depending on your environment configuration. You should be presented with a dashboard with an empty activities list, a link to open Twill's media library and a dropdown to manage users, your own account and logout. -#### Setting up the media library +## Setting up the media library From there, you might want to configure Twill's media library's storage provider and its rendering service. By default, Twill is configured to store uploads on `AWS S3` and to render images via [Imgix](https://imgix.com). Provide the following .env variables to get up and running: @@ -91,10 +96,10 @@ MEDIA_LIBRARY_ENDPOINT_TYPE=local MEDIA_LIBRARY_IMAGE_SERVICE=A17\Twill\Services\MediaLibrary\Glide ``` -See the [media library's configuration documentation](#media-library-2) for more information. +See the [media library's configuration documentation](/media-library/) for more information. -#### A note about the frontend +## A note about the frontend On your frontend domain (`domain.test`), nothing changed, and that's ok! Twill does not make any assumptions regarding how you might want to build your own applications. It is up to you to setup Laravel routes that queries content created through Twill's admin console. You can decide to use server side rendering with Laravel's Blade templating and/or to define API endpoints to build your frontend application using any client side solution (eg. Vue, React, Angular, ...). -On a clean Laravel install, you should still see Laravel's welcome screen. If you installed Twill on an existing Laravel application, your setup should not be affected. Do not hesitate to reach out on [Github](https://github.com/area17/twill/issues) if you have a specific use case or any trouble using Twill with your existing application. +On a clean Laravel install, you should still see Laravel's welcome screen. If you installed Twill on an existing Laravel application, your setup should not be affected. Do not hesitate to reach out on [GitHub](https://github.com/area17/twill/issues) if you have a specific use case or any trouble using Twill with your existing application. diff --git a/docs/.sections/getting-started/navigation.md b/docs/src/getting-started/navigation.md similarity index 97% rename from docs/.sections/getting-started/navigation.md rename to docs/src/getting-started/navigation.md index 41817eaf6..bcd9cd63e 100644 --- a/docs/.sections/getting-started/navigation.md +++ b/docs/src/getting-started/navigation.md @@ -1,4 +1,8 @@ -### Navigation +--- +pageClass: twill-doc +--- + +# Navigation The `config/twill-navigation.php` file manages the navigation of your custom admin console. Using Twill's UI, the package provides 3 levels of navigation: global, primary and secondary. This file simply contains a nested array description of your navigation. @@ -7,7 +11,7 @@ The simplest entry has a `title` and a `route` option which is a Laravel route n Two other options are provided that are really useful in conjunction with the CRUD modules you'll create in your application: `module` and `can`. `module` is a boolean to indicate if the entry is routing to a module route. By default it will link to the index route of the module you used as your entry key. `can` allows you to display/hide navigation links depending on the current user and permission name you specify. -Example: +#### Example ```php [ + 'modules' => [ + 'projects' => [ + 'name' => 'projects', + 'routePrefix' => 'work', + 'count' => true, + 'create' => true, + 'activity' => true, + 'draft' => true, + 'search' => true, + 'search_fields' => ['name', 'description'] + ], + ... + ], + ... + ], + ... +]; +``` + +You can also customize the endpoint to handle search queries yourself: + +```php +return [ + 'dashboard' => [ + ..., + 'search_endpoint' => 'your.custom.search.endpoint.route.name', + ], + ... +]; +``` + +You will need to return a collection of values, like in the following example: + +```php +return $searchResults->map(function ($item) use ($module) { + try { + $author = $item->revisions()->latest()->first()->user->name ?? 'Admin'; + } catch (\Exception $e) { + $author = 'Admin'; + } + + return [ + 'id' => $item->id, + 'href' => moduleRoute($moduleName['name'], $moduleName['routePrefix'], 'edit', $item->id), + 'thumbnail' => $item->defaultCmsImage(['w' => 100, 'h' => 100]), + 'published' => $item->published, + 'activity' => 'Last edited', + 'date' => $item->updated_at->toIso8601String(), + 'title' => $item->title, + 'author' => $author, + 'type' => Str::singular($module['name']), + ]; +})->values(); +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 000000000..b19f51575 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,15 @@ +--- +pageClass: twill-doc +title: Documenation +next: "/preface/" +--- + +# Twill Documentation + +![Twill Dashboard](/docs/_media/twill-dashboard.jpg) + +#### About Twill + +Twill is an open source Laravel package that helps developers rapidly create a custom CMS that is beautiful, powerful, and flexible. By standardizing common functions without compromising developer control, Twill makes it easy to deliver a feature-rich admin console that focuses on modern publishing needs. + +Twill is an [AREA 17](https://area17.com) product. It was crafted with the belief that content management should be a creative, productive, and enjoyable experience for both publishers and developers. diff --git a/docs/src/media-library/file-library.md b/docs/src/media-library/file-library.md new file mode 100644 index 000000000..9acd20542 --- /dev/null +++ b/docs/src/media-library/file-library.md @@ -0,0 +1,15 @@ +--- +pageClass: twill-doc +--- + +# File Library + +The file library is much simpler but also works with S3 and local storage. To associate files to your model, use the `HasFiles` and `HandleFiles` traits, the `$filesParams` configuration and the `files` form field. + +When it comes to using those data model files in the frontend site, there are a few methods on the `HasFiles` trait that will help you to retrieve direct URLs. You can find the full +reference in the [HasFiles API documentation](https://twill.io/docs/api/2.x/A17/Twill/Models/Behaviors/HasFiles.html) + +::: tip INFO +The file library can be used to upload files of any type and to attach those files to records using the `file` form field. +For example, you could store video files and render them on your frontend, with a CDN on top of it. We recommend Youtube and Vimeo for regular video embeds, but for muted, decorative, autoplaying videos, .mp4 files in the file library can be a great solution. +::: diff --git a/docs/src/media-library/image-rendering-service.md b/docs/src/media-library/image-rendering-service.md new file mode 100644 index 000000000..028f1e1a0 --- /dev/null +++ b/docs/src/media-library/image-rendering-service.md @@ -0,0 +1,31 @@ +--- +pageClass: twill-doc +--- + +# Image Rendering Service + +This package currently ships with 3 rendering services, [Imgix](https://www.imgix.com/), [Glide](http://glide.thephpleague.com/) and a local minimalistic rendering service. It is very simple to implement another one like [Cloudinary](http://cloudinary.com/) or even another local service like or [Croppa](https://github.com/BKWLD/croppa). +Changing the image rendering service can be done by changing the `MEDIA_LIBRARY_IMAGE_SERVICE` environment variable to one of the following options: +- `A17\Twill\Services\MediaLibrary\Glide` +- `A17\Twill\Services\MediaLibrary\Imgix` +- `A17\Twill\Services\MediaLibrary\Local` + +For a custom image service you would have to implement the `ImageServiceInterface` and modify your `twill` configuration value `media_library.image_service` with your implementation class. +Here are the methods you would have to implement: + +```php + + + + https://YOUR_ADMIN_DOMAIN + http://YOUR_ADMIN_DOMAIN + POST + PUT + DELETE + 3000 + ETag + * + + +``` diff --git a/docs/src/media-library/index.md b/docs/src/media-library/index.md new file mode 100644 index 000000000..9c0aa09b1 --- /dev/null +++ b/docs/src/media-library/index.md @@ -0,0 +1,7 @@ +--- +pageClass: twill-doc +--- + +# Media Library + +![screenshot](/docs/_media/medialibrary.png) diff --git a/docs/src/media-library/role-crop-params.md b/docs/src/media-library/role-crop-params.md new file mode 100644 index 000000000..957803f29 --- /dev/null +++ b/docs/src/media-library/role-crop-params.md @@ -0,0 +1,18 @@ +--- +pageClass: twill-doc +--- + +# Role & Crop Params + +Each _Module_ in your application can have its own predefined image *crops* and *roles*. + +A _role_ is a way to define different contexts in which a image might be placed. For example, roles for a `People` model could be `profile` and `cover`. This would allow you to include your People model in list and show a cover image for each, or show an single person model with a profile image. You can associate any number of image roles with your Model. + +_Crops_ are more self-explanatory. Twill comes with some pre-defined crop settings to allow you to set different variants of a given image, so crops can be used in combination with _roles_ or they can be used on their own with a single role to define multiple cropping ratios on the same image. + +Using the Person example, your `cover` image could have a `square` crop for mobile screens, but could use a `16/9` crop on larger screens. Those values are editable at your convenience for each model, even if there are already some crops created in the CMS. + +The only thing you have to do to make it work is to compose your model and repository with the appropriate traits, respectively `HasMedias` and `HandleMedias`, setup your `$mediasParams` configuration and use the `medias` form partial in your form view (more info in the CRUD section). + +When it comes to using those data model images in the frontend site, there are a few methods on the `HasMedias` trait that will help you to retrieve them for each of your layouts. You can find the full +reference in the [HasMedias API documentation](https://twill.io/docs/api/2.x/A17/Twill/Models/Behaviors/HasMedias.html) diff --git a/docs/src/media-library/storage-provider.md b/docs/src/media-library/storage-provider.md new file mode 100644 index 000000000..47c9d8764 --- /dev/null +++ b/docs/src/media-library/storage-provider.md @@ -0,0 +1,7 @@ +--- +pageClass: twill-doc +--- + +# Storage Provider + +The media and files libraries currently support S3, Azure and local storage. Head over to the `twill` configuration file to setup your storage disk and configurations. Also check out the direct upload section of this documentation to setup your IAM users and bucket / container if you want to use S3 or Azure as a storage provider. diff --git a/docs/src/oauth-login/index.md b/docs/src/oauth-login/index.md new file mode 100644 index 000000000..fbe5b6004 --- /dev/null +++ b/docs/src/oauth-login/index.md @@ -0,0 +1,15 @@ +--- +pageClass: twill-doc +--- + +# Oauth Login + +You can enable the `twill.enabled.users-oauth` feature to let your users login to the CMS using a third party service supported by Laravel Socialite. +By default, `twill.oauth.providers` only has `google`, but you are free to change it or add more services to it. +In the case of using Google, you would of course need to provide the following environment variables: + +``` +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_CALLBACK_URL=https://admin.twill-based-cms.com/login/oauth/callback/google +``` diff --git a/docs/src/preface/1-x-documentation.md b/docs/src/preface/1-x-documentation.md new file mode 100644 index 000000000..40220a38d --- /dev/null +++ b/docs/src/preface/1-x-documentation.md @@ -0,0 +1,7 @@ +--- +pageClass: twill-doc +--- + +# 1.x Documentation + +Documentation for Twill versions below 2.0 is available for reference [here](/docs/1.x). diff --git a/docs/src/preface/architecture-concepts.md b/docs/src/preface/architecture-concepts.md new file mode 100644 index 000000000..9d07127f8 --- /dev/null +++ b/docs/src/preface/architecture-concepts.md @@ -0,0 +1,83 @@ +--- +pageClass: twill-doc +--- + +# Architecture Concepts + +## CRUD modules + +A Twill [CRUD module](/crud-modules/) is a set of classes and configurations in your Laravel application that enable your publishers to manage a certain type of content. The structure of a CRUD module is completely up to you. + +Another way to think of a CRUD module is as a feature rich Laravel resource. In other words (and for the non-Laravel developer), a CRUD module is basically a content type (or post type, as sometimes called by other CMS solutions) with CRUD operations (Create, Read, Update, Delete), as well as custom Twill-provided operations like: Publish, Feature, Tag, Preview, Restore, Restore revision, Reorder or Bulk edit. Using Twill's media library, images and files can be attached to modules records. Also, using Twill's block editor, a rich editing experience of a module's record can be offered to publishers. + +In Twill's UI, a CRUD module most often consists of a listing page and a form page or modal. Records created under a module can then be associated with other modules' records to create relationships between your content. Records of your Twill modules and any associations are stored in a traditional relational database schema, following Laravel's migrations and Eloquent model conventions. + +Twill's CRUD modules features are enabled using PHP traits you include in your Eloquent models and Twill repositories, as well as various configuration variables, and a bunch of conventions to follow. Further guidance is documented in the [CRUD modules](/crud-modules/) section. + +A Twill module can be modified however you like – you can include countless types of content fields, and change the organization and structure according to the needs of the module and your product. Setup is simple: you just need to compose a form using all of Twill's available form fields. + +## Recommended content types + +While possibilities for composition are endless, we’ve identified four standard content types: + +#### Entities + +Entities are your primary data models, usually represented on your frontend as listing and detail views. Generally speaking, entity listings are displayed programmatically (e.g., by date, price, etc.) but also can be manually ordered. For example, if you’re building an editorial site, your primary entity might be articles. If you’re building a site to showcase your company’s work, you might have entities for projects, case studies, people, etc. This is the default behavior of a Twill module. + +#### Attributes + +Attributes are secondary data models most often used to add structured details to an entity (for search, filtering, and/or display). Example attributes include: categories, types, sectors, industries, etc. In a Twill CMS, each attribute needs a listing screen and, within that screen, quick creation and editing ability. As attributes tend to be relatively simple (few content fields, etc), their form screen can often fit within a modal. This modal can be made available from other parts of the CMS rather than only from their own listing screen. In Twill, the `editInModal` index option of your module's controllers can be used to enable that behavior. + +#### Pages + +Pages are unstructured data models most often used for static content, such as an About page. Rather than being separated into listing and detail screens, pages are manually organized into parent/child relationships. Combined with the [kalnoy/nestedset](https://github.com/lazychaser/laravel-nestedset) package, a Twill module can be configured to show and make parent/child relationships manageable on a module's records. + +#### Elements + +Elements are modules or snippets of content that are added to an entity, page, or screen. Examples include the ability to manage footer text or create a global alert that can be turned on/off, etc. Twill offers developers the ability to quickly create [settings sections](/settings-sections/) to manage elements. A Twill module could also be configured to manage any sort of standalone element or content composition. There's nothing wrong with having a database table with a single record if that is what your product require, so you should feel free to create a Twill module to have a custom form for a single record. You can use a Laravel seeder or migration to initialize your admin console with those records. + +## CRUD listings + +One of the benefits of Twill is the ability to fully customize CRUD listing views. At minimum, you’ll want to include the key information for each data record so that publishers can have an at-a-glance view without having to click into a record. You can also set up a default view and give each publisher the ability to customize the columns and the number of records per pagination page. + +In certain cases, you may require nested CRUD modules. For example, if you are building a handbook website, the parent CRUD would be the handbooks and then within each handbook there are pages (child CRUD). In this case, the listing will be the parent CRUD and for each record, you’d include a column to access the child CRUDs for each. + +## CMS navigation + +One of the benefits of Twill is the ability to fully customize the navigation as needed to make it easy and intuitive for publishers to navigate through the CMS and perform their regular production duties. Twill has three levels of navigation: + +#### Main navigation + +We recommend that the main navigation reflects the frontend organization, in that way, it is intuitive for publishers. Additionally, the main navigation includes transversal items such as media library and global settings. + +#### Secondary navigation + +We recommend that you group all entities, attributes, pages, and possibly buckets (see below) under each main navigation item. For example, if you have a section called “Our work” then the secondary navigation will include: case studies (entity), sectors (attribute), how we work (page), featured (buckets), etc. + +#### Tertiary navigation + +In certain cases, you will need a third level of navigation, however we recommend that you only use it when absolutely necessary, otherwise content may be too buried. You also have the option to turn the tertiary navigation into a breadcrumb. + +## Block editor + +Central to the Twill experience is the block editor, giving publishers full control of how they construct the content of a record. A block is a composition of form fields made available to publshers in Twill's block editor form field. + +Generally speaking, with a standard CMS, all content is managed through fixed forms. While in a Twill CMS some of the content may be fixed (such as title, subtitle, intro, required content, etc.), when using the block editor, the content is constructed by adding and reordering blocks of content. This gives you maximum flexibility to build narrative experiences on the front end. + +For example, let’s say you’re building a blog. Your blog post form may require fixed content such as the title, short description, author, etc. But then you can use the block editor for the body of the post, allowing the publisher to add standardized blocks for text, images, quotes, slideshows, videos, related content, embeds, etc. and reorder them as needed. + +A block can include any combination of fields, including repeater fields and even data pulled from a third party service. Each block also can contain additional options so that a single block can be displayed according to different variations. This obviates the need to create a new block every time you need a different display of your content, and allows you to match the build of the page to the content, context or design required. For example, you can have a media block that may alternatively include a video or an image, be displayed at small, medium or large, or displayed inline with content or full screen. + +To keep page-building as simple as possible, we recommend that you keep blocks to a minimum – ideally no more than 8 blocks, if possible. When adding a new block, consider: is this a unique block or simply block options? Publishers will prefer switching an option using existing content rather than having to create another block and copy and paste. + +It is also important that you work with a designer early on to discuss the block strategy and make sure your content works well no matter how your publishers arrange it. Can all the blocks work in any combination or are there restrictions? If the latter, you can create form validations to block publishers from arranging blocks in certain contexts. + +## Buckets + +Buckets are used to feature content. While the name might be boring, your publishers will love them! + +The functionality is made up of two parts: an entity navigator and buckets. The entity navigator gives access to the entities, including search and filters. Buckets represent your feature areas. For example, let’s say you have a homepage with main features (such as a hero display pointing your users to 2-3 pages), secondary features (such as a grid of content), and tertiary features. You would create three buckets for each of these feature sections. Then, your publishers can simply drag the desired entity to the bucket they want it featured in. + +You can also associate rules for your buckets. For example, let’s say you only want three main features and five secondary features – but unlimited tertiary features. You can add those restrictions and when the publishers try to add more than the limit, they will be informed they need to remove an entity before they can add another. + +While buckets are primarily used for featuring, they can also be used for any purpose. For example, if you have a website that has different navigation for different market locations (e.g. USA, Europe, Asia), you can use buckets to manage this. diff --git a/docs/src/preface/benefits-overview.md b/docs/src/preface/benefits-overview.md new file mode 100644 index 000000000..62c8a1b8c --- /dev/null +++ b/docs/src/preface/benefits-overview.md @@ -0,0 +1,14 @@ +--- +pageClass: twill-doc +--- + +# Benefits Overview + +With Twill's vast number of pre-built features and associated library of Vue.js UI components, developers can focus their efforts on the unique aspects of their applications instead of rebuilding standard ones. + +Built to get out of your way, Twill offers: +- No lock-in, create your own data models or hook existing ones +- No front-end assumptions, use it within your Laravel app or as a headless CMS +- No bloat, turn off features you don’t need +- No need to write/adapt HTML for the admin UI +- No limits, extend as you see fit diff --git a/docs/src/preface/contribution-guide.md b/docs/src/preface/contribution-guide.md new file mode 100644 index 000000000..c353e1eeb --- /dev/null +++ b/docs/src/preface/contribution-guide.md @@ -0,0 +1,42 @@ +--- +pageClass: twill-doc +--- + +# Contribution Guide + +## Code of Conduct + +Twill is dedicated to building a welcoming, diverse, safe community. We expect everyone participating in the Twill community to abide by our [Code of Conduct](https://github.com/area17/twill/blob/main/CODE_OF_CONDUCT.md). Please read it. Please follow it. + +## Bug reports and features submission + +To submit an issue or request a feature, please do so on [GitHub](https://github.com/area17/twill/issues). + +If you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a bug report is to make it easy for yourself - and others - to replicate the bug and develop a fix. + +Remember, bug reports are created in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the bug report will automatically see any activity or that others will jump to fix it. Creating a bug report serves to help yourself and others start on the path of fixing the problem. + +## Security vulnerabilities + +If you discover a security vulnerability within Twill, please email us at [security@twill.io](mailto:security@twill.io). All security vulnerabilities will be promptly addressed. + +## Versioning scheme + +Twill follows [Semantic Versioning](https://semver.org/). Major releases are released only when breaking changes are necessary, while minor and patch releases may be released as often as every week. Minor and patch releases should never contain breaking changes. + +When referencing Twill from your application, you should always use a version constraint such as `^2.0`, since major releases of Twill do include breaking changes. + +## Which branch? + +All bug fixes should be sent to the latest stable branch (`2.x`). Bug fixes should never be sent to the `main` branch unless they fix features that exist only in the upcoming release. + +Minor features that are fully backwards compatible with the current Twill release may be sent to the latest stable branch (`2.x`). + +Major new features should always be sent to the `main` branch, which contains the upcoming Twill release. + +Please send coherent history — make sure each individual commit in your pull request is meaningful. If you had to make a lot of intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +## Coding style + +- PHP: [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). +- Javascript: [Standard](https://standardjs.com/), [Vue ESLint Essentials](https://github.com/vuejs/eslint-plugin-vue). diff --git a/docs/src/preface/credits.md b/docs/src/preface/credits.md new file mode 100644 index 000000000..861da4a53 --- /dev/null +++ b/docs/src/preface/credits.md @@ -0,0 +1,16 @@ +--- +pageClass: twill-doc +--- + +# Credits + +Over the last 15 years, nearly every engineer at AREA 17 has contributed to Twill in some capacity. The current iteration of Twill as an open source initiative was created by: + +- [Quentin Renard](https://area17.com/about/quentin-renard), lead application engineer +- [Antoine Doury](https://area17.com/about/antoine-doury), lead interface engineer +- [Antonin Caudron](https://area17.com/about/antonin-caudron), interface engineer +- [Martin Rettenbacher](https://area17.com/about/martin-rettenbacher), product designer +- [Jesse Golomb](https://area17.com/about/jesse-golomb), product owner +- [George Eid](https://area17.com/about/george-eid), product manager + +Additional contributors include [Laurens van Heems](https://area17.com/about/laurens-van-heems), [Fernando Petrelli](https://area17.com/about/fernando-petrelli), [Gilbert Moufflet](https://area17.com/about/gilbert-moufflet), [Mubashar Iqbal](https://area17.com/about/mubashar-iqbal), [Pablo Barrios](https://area17.com/about/pablo-barrios), [Luis Lavena](https://area17.com/about/luis-lavena), and [Mike Byrne](https://area17.com/about/mike-byrne). diff --git a/docs/src/preface/feature-list.md b/docs/src/preface/feature-list.md new file mode 100644 index 000000000..0447412ed --- /dev/null +++ b/docs/src/preface/feature-list.md @@ -0,0 +1,59 @@ +--- +pageClass: twill-doc +--- + +# Feature List + +## CRUD modules + +* Enhanced Laravel “resources” models +* Command line generator and conventions to speed up creating new ones +* Based on PHP traits and regular Laravel concepts (migrations, models, controllers, form requests, repositories, Blade views) +* Fully custom forms per content type +* Slug management, including the ability to automatically redirect old urls +* Configurable content listings with searching, filtering, sorting, publishing, featuring, reordering and more +* Support for all Eloquent ORM relationships (1-1, 1-n, n-n, polymorphic) +* Content versioning + +## UI Components + +* Large library of plugged-in Vue.js form components with tons of options for maximum flexibility and composition +* Completely abstracted HTML markup. You’ll never have to deal with Bootstrap HTML again, which means you won’t ever have to maintain frontend-related code for your CMS +* Input, text area, rich text area form fields with option to set SEO optimized limits +* Configurable WYSIWYG built with Quill.js +* Inline translated fields with independent publication status (no duplication) +* Select, multi-select, content type browsers for related content and tags +* Form repeaters +* Date and color pickers +* Flexible content block editor (dynamically composable from all form components) +* Custom content blocks per content type + +## Media library + +* Media/files library with S3 and imgix integration (3rd party services are swappable) +* Image selector with smart cropping +* Ability to set custom image requirements and cropping parameters per content type +* Multiple crops possible per image for art directed responsive +* Batch uploading and tagging +* Metadata editing (alternative text, caption) +* Multi fields search (filename, alternative text, tags, dimensions…) + +## Configuration based features + +* User authentication, authorization and management +* Fully configurable CMS navigation, with three levels of hierarchy and breadcrumbs for limitless content structure +* Configurable CMS dashboard with quick access links, activity log and Google Analytics integration +* Configurable CMS global search +* Intuitive content featuring, using a bucket UI. Put any of your content types in "buckets" to manage any layout of featured content or other concepts like localization + +## Developer experience + +* Maintain a Laravel application, not a Twill application +* Support for Laravel 5.6 and up – Twill will be updated to support all future versions +* Support for both MySQL and PostgreSQL databases +* No conflict with other Laravel packages – keep building with your tools of choice +* No specific server requirements, if you can deploy a Laravel application, you can deploy Twill +* Development and production ready toolset (debug bar, inspector, exceptions handler) +* No data lock in – all Twill content types are proper relational database tables, so it’s easy to move to Twill from other solutions and to expose content created with your Twill CMS to other applications +* Previewing and side by side comparison of fully rendered frontend site that you’ll get up and running very quickly no matter how you built your frontend (fully headed Laravel app, hybrid Laravel app with your own custom API endpoints or even full SPA with frameworks like React or Vue) +* Scales to very large amount of content without performance drawbacks, even on minimal resources servers (for what it’s worth, it’s running perfectly fine on a $5/month VPS, and you can cache frontend pages if you’d like through packages like laravel-response-cache or a CDN like Cloudfront) diff --git a/docs/src/preface/index.md b/docs/src/preface/index.md new file mode 100644 index 000000000..62c8a1b8c --- /dev/null +++ b/docs/src/preface/index.md @@ -0,0 +1,14 @@ +--- +pageClass: twill-doc +--- + +# Benefits Overview + +With Twill's vast number of pre-built features and associated library of Vue.js UI components, developers can focus their efforts on the unique aspects of their applications instead of rebuilding standard ones. + +Built to get out of your way, Twill offers: +- No lock-in, create your own data models or hook existing ones +- No front-end assumptions, use it within your Laravel app or as a headless CMS +- No bloat, turn off features you don’t need +- No need to write/adapt HTML for the admin UI +- No limits, extend as you see fit diff --git a/docs/src/preface/licensing.md b/docs/src/preface/licensing.md new file mode 100644 index 000000000..a31f10668 --- /dev/null +++ b/docs/src/preface/licensing.md @@ -0,0 +1,17 @@ +--- +pageClass: twill-doc +--- + +# Licensing + +## Software + +The Twill software is licensed under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). + +## User interface + +The Twill UI, including but not limited to images, icons, patterns, and derivatives thereof are licensed under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). + +## Attribution + +By using the Twill UI, you agree that any application which incorporates it shall prominently display the message “Made with Twill” in a legible manner in the footer of the admin console. This message must open a link to Twill.io when clicked or touched. For permission to remove the attribution, contact us at [hello@twill.io](mailto:hello@twill.io). diff --git a/docs/.sections/resources.md b/docs/src/resources/index.md similarity index 87% rename from docs/.sections/resources.md rename to docs/src/resources/index.md index 2d200e1ae..14a462b30 100644 --- a/docs/.sections/resources.md +++ b/docs/src/resources/index.md @@ -1,5 +1,11 @@ -## Resources -### Other useful packages +--- +pageClass: twill-doc +--- + +# Resources + +#### Other Useful Packages + - [laravel/scout](https://laravel.com/docs/5.3/scout) provide full text search on your Eloquent models. - [laravel/passport](https://laravel.com/docs/5.3/passport) makes API authentication a breeze. - [spatie/laravel-fractal](https://github.com/spatie/laravel-fractal) is a nice and easy integration with [Fractal](http://fractal.thephpleague.com) to create APIs. @@ -13,3 +19,7 @@ - [flynsarmy/csv-seeder](https://github.com/Flynsarmy/laravel-csv-seeder) allows CSV based database seeds. - [ufirst/lang-import-export](https://github.com/ufirstgroup/laravel-lang-import-export) provides artisan commands to import and export language files from and to CSV - [nikaia/translation-sheet](https://github.com/nikaia/translation-sheet) allows translating Laravel languages files using a Google Spreadsheet. + +#### Awesome Twill List + +For more resources from the Twill community, have a look at the [awesome-twill repository on GitHub](https://github.com/pboivin/awesome-twill). diff --git a/docs/src/settings-sections/index.md b/docs/src/settings-sections/index.md new file mode 100644 index 000000000..3b646faca --- /dev/null +++ b/docs/src/settings-sections/index.md @@ -0,0 +1,64 @@ +--- +pageClass: twill-doc +--- + +# Settings Sections + +Settings sections are standalone forms that you can add to your Twill's navigation to give publishers the ability to manage simple key/value records for you to then use anywhere in your application codebase. + +Start by enabling the `settings` feature in your `config/twill.php` configuration file `enabled` array. See [Twill's configuration documentation](/enabled-features/) for more information. + +If you did not enable this feature before running the `twill:install` command, you need to copy the migration in `vendor/area17/twill/migrations/create_settings_table.php` to your own `database/migrations` directory and migrate your database before continuing. + +To create a new settings section, add a blade file to your `resources/views/admin/settings` folder. The name of this file is the name of your new settings section. + +In this file, you can use `@formField('input')` Blade directives to add new settings. The name attribute of each form field is the name of a setting. Wrap them like in the following example: + +```php +@extends('twill::layouts.settings') + +@section('contentFields') + @formField('input', [ + 'label' => 'Site title', + 'name' => 'site_title', + 'textLimit' => '80' + ]) +@stop +``` + +If your `translatable.locales` configuration array contains multiple language codes, you can enable the `translated` option on your settings input form fields to make them translatable. + +At this point, you want to add an entry in your `config/twill-navigation.php` configuration file to show the settings section link: + +```php +return [ + ... + 'settings' => [ + 'title' => 'Settings', + 'route' => 'admin.settings', + 'params' => ['section' => 'section_name'], + 'primary_navigation' => [ + 'section_name' => [ + 'title' => 'Section name', + 'route' => 'admin.settings', + 'params' => ['section' => 'section_name'] + ], + ... + ] + ], +]; +``` + +Each Blade file you create in `resources/views/admin/settings` creates a new section available for you to add in the `primary_navigation` array of your `config/twill-navigation.php` file. + +You can then retrieve the value of a specific setting by its key, which is the name of the form field you defined in your settings form, either by directly using the `A17\Twill\Models\Setting` Eloquent model or by using the provided `byKey` helper in `A17\Twill\Repositories\SettingRepository`: + +```php +byKey('site_title'); +app(SettingRepository::class)->byKey('site_title', 'section_name'); +``` diff --git a/docs/src/user-management/index.md b/docs/src/user-management/index.md new file mode 100644 index 000000000..2e104593d --- /dev/null +++ b/docs/src/user-management/index.md @@ -0,0 +1,130 @@ +--- +pageClass: twill-doc +--- + +# User Management + +Authentication and authorization are provided by default in Laravel. This package simply leverages what Laravel provides and configures the views for you. By default, users can login at `/login` and can also reset their password through that same screen. New users have to reset their password before they can gain access to the admin application. By using the twill configuration file, you can change the default redirect path (`auth_login_redirect_path`) and send users to anywhere in your application following login. + +## Roles + +The package currently provides three different roles: +- view only +- publisher +- admin + +## Permissions + +Default permissions are as follows. To learn how permissions can be modified or extended, see the next section. + +View only users are able to: +- login +- view CRUD listings +- filter CRUD listings +- view media/file library +- download original files from the media/file library +- edit their own profile + +Publishers have the same permissions as view only users plus: +- full CRUD permissions +- publish +- sort +- feature +- upload new images/files to the media/file library + +Admin users have the same permissions as publisher users plus: +- full permissions on users + +There is also a super admin user that can impersonate other users at `/users/impersonate/{id}`. The super admin can be a useful tool for testing features with different user roles without having to logout/login manually, as well as for debugging issues reported by specific users. You can stop impersonating by going to `/users/impersonate/stop`. + +## Extending user roles and permissions + +You can create or modify new permissions for existing roles by using the Gate façade in your `AuthServiceProvider`. The `can` middleware, provided by default in Laravel, is very easy to use, either through route definition or controller constructor. + +To create new user roles, you could extend the default enum UserRole by overriding it using Composer autoloading. In `composer.json`: + +```json + "autoload": { + "classmap": [ + "database/seeds", + "database/factories" + ], + "psr-4": { + "App\\": "app/" + }, + "files": ["app/Models/Enums/UserRole.php"], + "exclude-from-classmap": ["vendor/area17/twill/src/Models/Enums/UserRole.php"] + } +``` + +In `app/Models/Enums/UserRole.php` (or anywhere else you'd like actually, only the namespace needs to be the same): + +```php + role_value, [ + UserRole::CUSTOM1, + UserRole::CUSTOM2, + UserRole::ADMIN, + ]); + }); + + Gate::define('edit', function ($user) { + return in_array($user->role_value, [ + UserRole::CUSTOM3, + UserRole::ADMIN, + ]); + }); + + Gate::define('custom-permission', function ($user) { + return in_array($user->role_value, [ + UserRole::CUSTOM2, + UserRole::ADMIN, + ]); + }); + } + } +``` + +You can use your new permission and existing ones in many places like the `twill-navigation` configuration using `can`: + +```php + 'projects' => [ + 'can' => 'custom-permission', + 'title' => 'Projects', + 'module' => true, + ], +``` + +Also in forms blade files using `@can`, as well as in middleware definitions in routes or controllers, see [Laravel's documentation](https://laravel.com/docs/5.7/authorization#via-middleware) for more info. + +You should follow the Laravel documentation regarding [authorization](https://laravel.com/docs/5.3/authorization). It's pretty good. Also if you would like to bring administration of roles and permissions to the admin application, [spatie/laravel-permission](https://github.com/spatie/laravel-permission) would probably be your best friend.