diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4dff10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +phpunit.phar +/vendor +composer.phar +composer.lock +*.project +.idea/ +.php_cs.cache +.vscode/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..51f14aa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: php + +php: + - 7 + +matrix: + fast_finish: true + allow_failures: + - php: hhvm + +sudo: false + +services: + - mysql + +before_script: + - mysql -e 'create database if not exists laravel_admin;' + - travis_retry composer self-update + - travis_retry composer install --no-interaction + +script: + - composer test \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cc17429 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contribute + +## Introduction + +First, thank you for considering contributing to laravel-admin! It's people like you that make the open source community such a great community! 😊 + +We welcome any type of contribution, not only code. You can help with +- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) +- **Marketing**: writing blog posts, howto's, printing stickers, ... +- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... +- **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. +- **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/laravel-admin). + +## Your First Contribution + +Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). + +## Submitting code + +Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. + +## Code review process + +The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. +It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? + +## Financial contributions + +We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/laravel-admin). +Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. + +## Questions + +If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!). +You can also reach us at hello@laravel-admin.opencollective.com. + +## Credits + +### Contributors + +Thank you to all the people who have already contributed to laravel-admin! + + + +### Backers + +Thank you to all our backers! [[Become a backer](https://opencollective.com/laravel-admin#backer)] + + + + +### Sponsors + +Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/laravel-admin#sponsor)) + + + + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..229071a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jens Segers + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f5c0cc --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +

+

Laravel-yiadmin

+ +

laravel-yiadmin which based on laravel-admin 1.6.9 is administrative interface builder for laravel which can help you build CRUD backends just with few lines of code.

+ + +Screenshots +------------ + +![laravel-yiadmin](https://cloud.githubusercontent.com/assets/1479100/19625297/3b3deb64-9947-11e6-807c-cffa999004be.jpg) + +Requirements +------------ + - PHP >= 7.0.0 + - Laravel >= 5.5.0 + - Fileinfo PHP Extension + +Installation +------------ + +> This package requires PHP 7+ and Laravel 5.5. + +First, install laravel 5.5, and make sure that the database connection settings are correct. + +``` +composer require encore/laravel-admin +``` + +Then run these commands to publish assets and config: + +``` +php artisan vendor:publish --provider="Encore\Admin\AdminServiceProvider" +``` +After run command you can find config file in `config/admin.php`, in this file you can change the install directory,db connection or table names. + +At last run following command to finish install. +``` +php artisan admin:install +``` + +Open `http://localhost/admin/` in browser,use username `admin` and password `admin` to login. + +Configurations +------------ +The file `config/admin.php` contains an array of configurations, you can find the default configurations in there. + +## Extensions + +| Extension | Description | laravel-admin | +| ------------------------------------------------ | ---------------------------------------- |---------------------------------------- | +| [helpers](https://github.com/laravel-admin-extensions/helpers) | Several tools to help you in development | ~1.5 | +| [media-manager](https://github.com/laravel-admin-extensions/media-manager) | Provides a web interface to manage local files | ~1.5 | +| [api-tester](https://github.com/laravel-admin-extensions/api-tester) | Help you to test the local laravel APIs |~1.5 | +| [scheduling](https://github.com/laravel-admin-extensions/scheduling) | Scheduling task manager for laravel-admin |~1.5 | +| [redis-manager](https://github.com/laravel-admin-extensions/redis-manager) | Redis manager for laravel-admin |~1.5 | +| [backup](https://github.com/laravel-admin-extensions/backup) | An admin interface for managing backups |~1.5 | +| [log-viewer](https://github.com/laravel-admin-extensions/log-viewer) | Log viewer for laravel |~1.5 | +| [config](https://github.com/laravel-admin-extensions/config) | Config manager for laravel-admin |~1.5 | +| [reporter](https://github.com/laravel-admin-extensions/reporter) | Provides a developer-friendly web interface to view the exception |~1.5 | +| [wangEditor](https://github.com/laravel-admin-extensions/wangEditor) | A rich text editor based on [wangeditor](http://www.wangeditor.com/) |~1.6 | +| [summernote](https://github.com/laravel-admin-extensions/summernote) | A rich text editor based on [summernote](https://summernote.org/) |~1.6 | +| [china-distpicker](https://github.com/laravel-admin-extensions/china-distpicker) | 一个基于[distpicker](https://github.com/fengyuanchen/distpicker)的中国省市区选择器 |~1.6 | +| [simplemde](https://github.com/laravel-admin-extensions/simplemde) | A markdow editor based on [simplemde](https://github.com/sparksuite/simplemde-markdown-editor) |~1.6 | +| [phpinfo](https://github.com/laravel-admin-extensions/phpinfo) | Integrate the `phpinfo` page into laravel-admin |~1.6 | +| [php-editor](https://github.com/laravel-admin-extensions/php-editor)
[python-editor](https://github.com/laravel-admin-extensions/python-editor)
[js-editor](https://github.com/laravel-admin-extensions/js-editor)
[css-editor](https://github.com/laravel-admin-extensions/css-editor)
[clike-editor](https://github.com/laravel-admin-extensions/clike-editor)| Several programing language editor extensions based on code-mirror |~1.6 | +| [star-rating](https://github.com/laravel-admin-extensions/star-rating) | Star Rating extension for laravel-admin |~1.6 | +| [json-editor](https://github.com/laravel-admin-extensions/json-editor) | JSON Editor for Laravel-admin |~1.6 | +| [grid-lightbox](https://github.com/laravel-admin-extensions/grid-lightbox) | Turn your grid into a lightbox & gallery |~1.6 | +| [daterangepicker](https://github.com/laravel-admin-extensions/daterangepicker) | Integrates daterangepicker into laravel-admin |~1.6 | +| [material-ui](https://github.com/laravel-admin-extensions/material-ui) | Material-UI extension for laravel-admin |~1.6 | +| [sparkline](https://github.com/laravel-admin-extensions/sparkline) | Integrates jQuery sparkline into laravel-admin |~1.6 | +| [chartjs](https://github.com/laravel-admin-extensions/chartjs) | Use Chartjs in laravel-admin |~1.6 | +| [simditor](https://github.com/laravel-admin-extensions/simditor) | Integrates simditor full-rich editor into laravel-admin |~1.6 | +| [cropper](https://github.com/laravel-admin-extensions/cropper) | A simple jQuery image cropping plugin. |~1.6 | +| [composer-viewer](https://github.com/laravel-admin-extensions/composer-viewer) | A web interface of composer packages in laravel. |~1.6 | + + + + +Other +------------ +`laravel-yiadmin` based on following plugins or services: + ++ [Laravel](https://laravel.com/) ++ [AdminLTE](https://almsaeedstudio.com/) ++ [Datetimepicker](http://eonasdan.github.io/bootstrap-datetimepicker/) ++ [font-awesome](http://fontawesome.io) ++ [moment](http://momentjs.com/) ++ [Google map](https://www.google.com/maps) ++ [Tencent map](http://lbs.qq.com/) ++ [bootstrap-fileinput](https://github.com/kartik-v/bootstrap-fileinput) ++ [jquery-pjax](https://github.com/defunkt/jquery-pjax) ++ [Nestable](http://dbushell.github.io/Nestable/) ++ [toastr](http://codeseven.github.io/toastr/) ++ [X-editable](http://github.com/vitalets/x-editable) ++ [bootstrap-number-input](https://github.com/wpic/bootstrap-number-input) ++ [fontawesome-iconpicker](https://github.com/itsjavi/fontawesome-iconpicker) ++ [sweetalert2](https://github.com/sweetalert2/sweetalert2) + +License +------------ +`laravel-yiadmin` is licensed under [The MIT License (MIT)](LICENSE). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ac515b9 --- /dev/null +++ b/composer.json @@ -0,0 +1,63 @@ +{ + "name": "flymorn/laravel-yiadmin", + "description": "laravel yiadmin", + "type": "library", + "keywords": ["laravel", "admin", "grid", "form"], + "homepage": "https://github.com/flymorn/laravel-yiadmin", + "license": "MIT", + "authors": [ + { + "name": "flymorn", + "email": "flymorn@gmail.com" + } + ], + "require": { + "php": ">=7.0.0", + "symfony/dom-crawler": "~3.1|~4.0", + "laravel/framework": "~5.5", + "doctrine/dbal": "2.*" + }, + "require-dev": { + "phpunit/phpunit": "~6.0", + "laravel/laravel": "~5.5", + "symfony/css-selector": "~3.1", + "fzaninotto/faker": "~1.4", + "intervention/image": "~2.3", + "laravel/browser-kit-testing": "^2.0" + }, + "autoload": { + "psr-4": { + "Encore\\Admin\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Tests\\Models\\": "tests/models", + "Tests\\Controllers\\": "tests/controllers" + }, + "classmap": [ + "tests/TestCase.php" + ] + }, + "scripts": { + "test": "./vendor/bin/phpunit" + }, + "suggest": { + "intervention/image": "Required to handling and manipulation upload images (~2.3).", + "spatie/eloquent-sortable": "Required to built orderable gird." + }, + "extra": { + "laravel": { + "providers": [ + "Encore\\Admin\\AdminServiceProvider" + ], + "aliases": { + "Admin": "Encore\\Admin\\Facades\\Admin" + } + + } + } +} \ No newline at end of file diff --git a/config/admin.php b/config/admin.php new file mode 100644 index 0000000..24fea14 --- /dev/null +++ b/config/admin.php @@ -0,0 +1,320 @@ + 'Laravel-admin', + + /* + |-------------------------------------------------------------------------- + | Laravel-admin logo + |-------------------------------------------------------------------------- + | + | The logo of all admin pages. You can also set it as an image by using a + | `img` tag, eg 'Admin logo'. + | + */ + 'logo' => 'Laravel admin', + + /* + |-------------------------------------------------------------------------- + | Laravel-admin mini logo + |-------------------------------------------------------------------------- + | + | The logo of all admin pages when the sidebar menu is collapsed. You can + | also set it as an image by using a `img` tag, eg + | 'Admin logo'. + | + */ + 'logo-mini' => 'La', + + /* + |-------------------------------------------------------------------------- + | Laravel-admin route settings + |-------------------------------------------------------------------------- + | + | The routing configuration of the admin page, including the path prefix, + | the controller namespace, and the default middleware. If you want to + | access through the root path, just set the prefix to empty string. + | + */ + 'route' => [ + + 'prefix' => env('ADMIN_ROUTE_PREFIX', 'admin'), + + 'namespace' => 'App\\Admin\\Controllers', + + 'middleware' => ['web', 'admin'], + ], + + /* + |-------------------------------------------------------------------------- + | Laravel-admin install directory + |-------------------------------------------------------------------------- + | + | The installation directory of the controller and routing configuration + | files of the administration page. The default is `app/Admin`, which must + | be set before running `artisan admin::install` to take effect. + | + */ + 'directory' => app_path('Admin'), + + /* + |-------------------------------------------------------------------------- + | Laravel-admin html title + |-------------------------------------------------------------------------- + | + | Html title for all pages. + | + */ + 'title' => 'Admin', + + /* + |-------------------------------------------------------------------------- + | Access via `https` + |-------------------------------------------------------------------------- + | + | If your page is going to be accessed via https, set it to `true`. + | + */ + 'https' => env('ADMIN_HTTPS', false), + + /* + |-------------------------------------------------------------------------- + | Laravel-admin auth setting + |-------------------------------------------------------------------------- + | + | Authentication settings for all admin pages. Include an authentication + | guard and a user provider setting of authentication driver. + | + | You can specify a controller for `login` `logout` and other auth routes. + | + */ + 'auth' => [ + + 'controller' => App\Admin\Controllers\AuthController::class, + + 'guards' => [ + 'admin' => [ + 'driver' => 'session', + 'provider' => 'admin', + ], + ], + + 'providers' => [ + 'admin' => [ + 'driver' => 'eloquent', + 'model' => Encore\Admin\Auth\Database\Administrator::class, + ], + ], + + // Add "remember me" to login form + 'remember' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Laravel-admin upload setting + |-------------------------------------------------------------------------- + | + | File system configuration for form upload files and images, including + | disk and upload path. + | + */ + 'upload' => [ + + // Disk in `config/filesystem.php`. + 'disk' => 'admin', + + // Image and file upload path under the disk above. + 'directory' => [ + 'image' => 'images', + 'file' => 'files', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Laravel-admin database settings + |-------------------------------------------------------------------------- + | + | Here are database settings for laravel-admin builtin model & tables. + | + */ + 'database' => [ + + // Database connection for following tables. + 'connection' => '', + + // User tables and model. + 'users_table' => 'admin_users', + 'users_model' => Encore\Admin\Auth\Database\Administrator::class, + + // Role table and model. + 'roles_table' => 'admin_roles', + 'roles_model' => Encore\Admin\Auth\Database\Role::class, + + // Permission table and model. + 'permissions_table' => 'admin_permissions', + 'permissions_model' => Encore\Admin\Auth\Database\Permission::class, + + // Menu table and model. + 'menu_table' => 'admin_menu', + 'menu_model' => Encore\Admin\Auth\Database\Menu::class, + + // Pivot table for table above. + 'operation_log_table' => 'admin_operation_log', + 'user_permissions_table' => 'admin_user_permissions', + 'role_users_table' => 'admin_role_users', + 'role_permissions_table' => 'admin_role_permissions', + 'role_menu_table' => 'admin_role_menu', + ], + + /* + |-------------------------------------------------------------------------- + | User operation log setting + |-------------------------------------------------------------------------- + | + | By setting this option to open or close operation log in laravel-admin. + | + */ + 'operation_log' => [ + + 'enable' => true, + + /* + * Only logging allowed methods in the list + */ + 'allowed_methods' => ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'], + + /* + * Routes that will not log to database. + * + * All method to path like: admin/auth/logs + * or specific method to path like: get:admin/auth/logs. + */ + 'except' => [ + 'admin/auth/logs*', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Admin map field provider + |-------------------------------------------------------------------------- + | + | Supported: "tencent", "google", "yandex". + | + */ + 'map_provider' => 'google', + + /* + |-------------------------------------------------------------------------- + | Application Skin + |-------------------------------------------------------------------------- + | + | This value is the skin of admin pages. + | @see https://adminlte.io/docs/2.4/layout + | + | Supported: + | "skin-blue", "skin-blue-light", "skin-yellow", "skin-yellow-light", + | "skin-green", "skin-green-light", "skin-purple", "skin-purple-light", + | "skin-red", "skin-red-light", "skin-black", "skin-black-light". + | + */ + 'skin' => 'skin-blue-light', + + /* + |-------------------------------------------------------------------------- + | Application layout + |-------------------------------------------------------------------------- + | + | This value is the layout of admin pages. + | @see https://adminlte.io/docs/2.4/layout + | + | Supported: "fixed", "layout-boxed", "layout-top-nav", "sidebar-collapse", + | "sidebar-mini". + | + */ + 'layout' => ['sidebar-mini', 'sidebar-collapse'], + + /* + |-------------------------------------------------------------------------- + | Login page background image + |-------------------------------------------------------------------------- + | + | This value is used to set the background image of login page. + | + */ + 'login_background_image' => '', + + /* + |-------------------------------------------------------------------------- + | Show version at footer + |-------------------------------------------------------------------------- + | + | Whether to display the version number of laravel-admin at the footer of + | each page + | + */ + 'show_version' => true, + + /* + |-------------------------------------------------------------------------- + | Show environment at footer + |-------------------------------------------------------------------------- + | + | Whether to display the environment at the footer of each page + | + */ + 'show_environment' => true, + + /* + |-------------------------------------------------------------------------- + | Menu bind to permission + |-------------------------------------------------------------------------- + | + | whether enable menu bind to a permission + */ + 'menu_bind_permission' => true, + + /* + |-------------------------------------------------------------------------- + | Enable default breadcrumb + |-------------------------------------------------------------------------- + | + | Whether enable default breadcrumb for every page content. + */ + 'enable_default_breadcrumb' => true, + + /* + |-------------------------------------------------------------------------- + | Extension Directory + |-------------------------------------------------------------------------- + | + | When you use command `php artisan admin:extend` to generate extensions, + | the extension files will be generated in this directory. + */ + 'extension_dir' => app_path('Admin/Extensions'), + + /* + |-------------------------------------------------------------------------- + | Settings for extensions. + |-------------------------------------------------------------------------- + | + | You can find all available extensions here + | https://github.com/laravel-admin-extensions. + | + */ + 'extensions' => [ + + ], +]; diff --git a/database/migrations/2016_01_04_173148_create_admin_tables.php b/database/migrations/2016_01_04_173148_create_admin_tables.php new file mode 100644 index 0000000..8c21313 --- /dev/null +++ b/database/migrations/2016_01_04_173148_create_admin_tables.php @@ -0,0 +1,114 @@ +create(config('admin.database.users_table'), function (Blueprint $table) { + $table->increments('id'); + $table->string('username', 190)->unique(); + $table->string('password', 60); + $table->string('name'); + $table->string('avatar')->nullable(); + $table->string('remember_token', 100)->nullable(); + $table->timestamps(); + }); + + Schema::connection($connection)->create(config('admin.database.roles_table'), function (Blueprint $table) { + $table->increments('id'); + $table->string('name', 50)->unique(); + $table->string('slug', 50); + $table->timestamps(); + }); + + Schema::connection($connection)->create(config('admin.database.permissions_table'), function (Blueprint $table) { + $table->increments('id'); + $table->string('name', 50)->unique(); + $table->string('slug', 50); + $table->string('http_method')->nullable(); + $table->text('http_path')->nullable(); + $table->timestamps(); + }); + + Schema::connection($connection)->create(config('admin.database.menu_table'), function (Blueprint $table) { + $table->increments('id'); + $table->integer('parent_id')->default(0); + $table->integer('order')->default(0); + $table->string('title', 50); + $table->string('icon', 50); + $table->string('uri', 50)->nullable(); + $table->string('permission')->nullable(); + + $table->timestamps(); + }); + + Schema::connection($connection)->create(config('admin.database.role_users_table'), function (Blueprint $table) { + $table->integer('role_id'); + $table->integer('user_id'); + $table->index(['role_id', 'user_id']); + $table->timestamps(); + }); + + Schema::connection($connection)->create(config('admin.database.role_permissions_table'), function (Blueprint $table) { + $table->integer('role_id'); + $table->integer('permission_id'); + $table->index(['role_id', 'permission_id']); + $table->timestamps(); + }); + + Schema::connection($connection)->create(config('admin.database.user_permissions_table'), function (Blueprint $table) { + $table->integer('user_id'); + $table->integer('permission_id'); + $table->index(['user_id', 'permission_id']); + $table->timestamps(); + }); + + Schema::connection($connection)->create(config('admin.database.role_menu_table'), function (Blueprint $table) { + $table->integer('role_id'); + $table->integer('menu_id'); + $table->index(['role_id', 'menu_id']); + $table->timestamps(); + }); + + Schema::connection($connection)->create(config('admin.database.operation_log_table'), function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id'); + $table->string('path'); + $table->string('method', 10); + $table->string('ip'); + $table->text('input'); + $table->index('user_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $connection = config('admin.database.connection') ?: config('database.default'); + + Schema::connection($connection)->dropIfExists(config('admin.database.users_table')); + Schema::connection($connection)->dropIfExists(config('admin.database.roles_table')); + Schema::connection($connection)->dropIfExists(config('admin.database.permissions_table')); + Schema::connection($connection)->dropIfExists(config('admin.database.menu_table')); + Schema::connection($connection)->dropIfExists(config('admin.database.user_permissions_table')); + Schema::connection($connection)->dropIfExists(config('admin.database.role_users_table')); + Schema::connection($connection)->dropIfExists(config('admin.database.role_permissions_table')); + Schema::connection($connection)->dropIfExists(config('admin.database.role_menu_table')); + Schema::connection($connection)->dropIfExists(config('admin.database.operation_log_table')); + } +} diff --git a/docs/en/LICENSE.md b/docs/en/LICENSE.md new file mode 100644 index 0000000..229071a --- /dev/null +++ b/docs/en/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jens Segers + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/en/README.md b/docs/en/README.md new file mode 100644 index 0000000..699c7c5 --- /dev/null +++ b/docs/en/README.md @@ -0,0 +1,75 @@ +laravel-admin +===== + +[![Build Status](https://travis-ci.org/z-song/laravel-admin.svg?branch=master)](https://travis-ci.org/z-song/laravel-admin) +[![StyleCI](https://styleci.io/repos/48796179/shield)](https://styleci.io/repos/48796179) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/z-song/laravel-admin/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/z-song/laravel-admin/?branch=master) +[![Packagist](https://img.shields.io/packagist/l/encore/laravel-admin.svg?maxAge=2592000)](https://packagist.org/packages/encore/laravel-admin) +[![Total Downloads](https://img.shields.io/packagist/dt/encore/laravel-admin.svg?style=flat-square)](https://packagist.org/packages/encore/laravel-admin) +[![Awesome Laravel](https://img.shields.io/badge/Awesome-Laravel-brightgreen.svg)](https://github.com/z-song/laravel-admin) + +`laravel-admin` is administrative interface builder for laravel which can help you build CRUD backends just with few lines of code. + +[Demo](http://laravel-admin.org/demo) use `username/password:admin/admin` + +Inspired by [SleepingOwlAdmin](https://github.com/sleeping-owl/admin) and [rapyd-laravel](https://github.com/zofe/rapyd-laravel). + +[Documentation](http://laravel-admin.org/docs) | [中文文档](http://laravel-admin.org/docs/#/zh/) + +Screenshots +------------ + +![laravel-admin](https://cloud.githubusercontent.com/assets/1479100/19625297/3b3deb64-9947-11e6-807c-cffa999004be.jpg) + +Installation +------------ + +> This package requires PHP 7+ and Laravel 5.5, for old versions please refer to [1.4](http://laravel-admin.org/docs/v1.4/#/) + +First, install laravel 5.5, and make sure that the database connection settings are correct. + +``` +composer require encore/laravel-admin 1.5.* +``` + +Then run these commands to publish assets and config: + +``` +php artisan vendor:publish --provider="Encore\Admin\AdminServiceProvider" +``` +After run command you can find config file in `config/admin.php`, in this file you can change the install directory,db connection or table names. + +At last run following command to finish install. +``` +php artisan admin:install +``` + +Open `http://localhost/admin/` in browser,use username `admin` and password `admin` to login. + +Default Settings +------------ +The file in `config/admin.php` contains an array of settings, you can find the default settings in there. + + +Other +------------ +`laravel-admin` based on following plugins or services: + ++ [Laravel](https://laravel.com/) ++ [AdminLTE](https://almsaeedstudio.com/) ++ [Datetimepicker](http://eonasdan.github.io/bootstrap-datetimepicker/) ++ [font-awesome](http://fontawesome.io) ++ [moment](http://momentjs.com/) ++ [Google map](https://www.google.com/maps) ++ [Tencent map](http://lbs.qq.com/) ++ [bootstrap-fileinput](https://github.com/kartik-v/bootstrap-fileinput) ++ [jquery-pjax](https://github.com/defunkt/jquery-pjax) ++ [Nestable](http://dbushell.github.io/Nestable/) ++ [toastr](http://codeseven.github.io/toastr/) ++ [X-editable](http://github.com/vitalets/x-editable) ++ [bootstrap-number-input](https://github.com/wpic/bootstrap-number-input) ++ [fontawesome-iconpicker](https://github.com/itsjavi/fontawesome-iconpicker) + +License +------------ +`laravel-admin` is licensed under [The MIT License (MIT)](LICENSE). diff --git a/docs/en/_sidebar.md b/docs/en/_sidebar.md new file mode 100644 index 0000000..16911a7 --- /dev/null +++ b/docs/en/_sidebar.md @@ -0,0 +1,34 @@ + +- Getting started + - [Installation](/en/installation.md) + - [Quick start](/en/quick-start.md) + - [Page content & Layout](/en/content-layout.md) +- Model grid + - [Basic usage](/en/model-grid.md) + - [Row actions](/en/model-grid-actions.md) + - [Column actions](/en/model-grid-column.md) + - [Custom tools](/en/model-grid-custom-tools.md) + - [Filters](/en/model-grid-filters.md) + - [Data export](/en/model-grid-export.md) +- Model form + - [Basic usage](/en/model-form.md) + - [Image/File upload](/en/model-form-upload.md) + - [Form fields](/en/model-form-fields.md) + - [Form field management](/en/model-form-field-management.md) + - [Form validation](/en/model-form-validation.md) + - [Save callback](/en/model-form-callback.md) +- [Model-tree](/en/model-tree.md) +- Admin extensions + - [Helpers](/en/extension-helpers.md) + - [Media manager](/en/extension-media-manager.md) + - [API tester](/en/extension-api-tester.md) + - [Config manager](/en/extension-config.md) + - [Task scheduling](/en/extension-scheduling.md) +- [Widgets](/en/widgets.md) +- [Permissions](/en/permission.md) +- [Custom authentication](/en/custom-authentication.md) +- [Custom Navbar](/en/custom-navbar.md) +- [Custom chart](/en/custom-chart.md) +- [Helpers](/en/helpers.md) +- [Upgrade precautions](/en/upgrade.md) +- [Change log](/en/change-log.md) \ No newline at end of file diff --git a/docs/en/change-log.md b/docs/en/change-log.md new file mode 100644 index 0000000..4dc24c2 --- /dev/null +++ b/docs/en/change-log.md @@ -0,0 +1,12 @@ +# Change log + +## v1.2.9、v1.3.3、v1.4.1 + +- Add user settings and modify avatar function +- Embedded form support +- Support for customize navigation bar (upper right corner) +- Add scaffolding, database command line tool, web artisan help tool +- Support for customize login page and login logic +- The form supports setting the width and setting the action +- Optimize table filters +- Fix bugs, optimize code and logic \ No newline at end of file diff --git a/docs/en/content-layout.md b/docs/en/content-layout.md new file mode 100644 index 0000000..7508767 --- /dev/null +++ b/docs/en/content-layout.md @@ -0,0 +1,139 @@ +# Page content + +The layout usage of `laravel-admin` can be found in the `index()` method of the home page's layout file [HomeController.php](https://github.com/z-song/laravel-admin/blob/master/src/Console/stubs/HomeController.stub). + +The `Encore\Admin\Layout\Content` class is used to implement the layout of the content area. The `Content::body ($element)` method is used to add page content: + +The page code for an unfilled content is as follows: + +```php +public function index() +{ + return Admin::content(function (Content $content) { + + // optional + $content->header('page header'); + + // optional + $content->description('page description'); + + // add breadcrumb since v1.5.7 + $content->breadcrumb( + ['text' => 'Dashboard', 'url' => '/admin'], + ['text' => 'User management', 'url' => '/admin/users'], + ['text' => 'Edit user'] + ); + + // Fill the page body part, you can put any renderable objects here + $content->body('hello world'); + }); +} +``` + +Method `$content->body();` can accepts any renderable objects, like string, number, class that has method `__toString`, or implements `Renderable`、`Htmlable` interface , include Laravel View objects. + +## Layout + +`laravel-admin` use grid system of bootstrap,The length of each line is 12, the following is a few simple examples: + +Add a line of content: + +```php +$content->row('hello') + +--------------------------------- +|hello | +| | +| | +| | +| | +| | +--------------------------------- + +``` + +Add multiple columns within the line: + +```php +$content->row(function(Row $row) { + $row->column(4, 'foo'); + $row->column(4, 'bar'); + $row->column(4, 'baz'); +}); +---------------------------------- +|foo |bar |baz | +| | | | +| | | | +| | | | +| | | | +| | | | +---------------------------------- + + +$content->row(function(Row $row) { + $row->column(4, 'foo'); + $row->column(8, 'bar'); +}); +---------------------------------- +|foo |bar | +| | | +| | | +| | | +| | | +| | | +---------------------------------- + +``` + +Column in the column: + +```php +$content->row(function (Row $row) { + + $row->column(4, 'xxx'); + + $row->column(8, function (Column $column) { + $column->row('111'); + $column->row('222'); + $column->row('333'); + }); +}); +---------------------------------- +|xxx |111 | +| |---------------------| +| |222 | +| |---------------------| +| |333 | +| | | +---------------------------------- + + +``` + + +Add rows in rows and add columns: + +```php +$content->row(function (Row $row) { + + $row->column(4, 'xxx'); + + $row->column(8, function (Column $column) { + $column->row('111'); + $column->row('222'); + $column->row(function(Row $row) { + $row->column(6, '444'); + $row->column(6, '555'); + }); + }); +}); +---------------------------------- +|xxx |111 | +| |---------------------| +| |222 | +| |---------------------| +| |444 |555 | +| | | | +---------------------------------- +``` + diff --git a/docs/en/custom-authentication.md b/docs/en/custom-authentication.md new file mode 100644 index 0000000..5238fb7 --- /dev/null +++ b/docs/en/custom-authentication.md @@ -0,0 +1,111 @@ +# Custom authentication + +If you do not use the `laravel-admin` built-in authentication login logic, you can refer to the following way to customize the login authentication logic. + +First of all, you need define a `User provider`, used to obtain the user identity, such as `app/Providers/CustomUserProvider.php`: + +```php +registerPolicies(); + + Auth::provider('custom', function ($app, array $config) { + + // Return an instance of Illuminate\Contracts\Auth\UserProvider... + return new CustomUserProvider(); + }); + } +} +``` + +Finally modify the configuration, open `config/admin.php`, find the `auth` part: + +```php + 'auth' => [ + 'guards' => [ + 'admin' => [ + 'driver' => 'session', + 'provider' => 'admin', + ] + ], + + // Modify the following + 'providers' => [ + 'admin' => [ + 'driver' => 'custom', + ] + ], + ], +``` +This completes the logic of custom authentication. diff --git a/docs/en/custom-chart.md b/docs/en/custom-chart.md new file mode 100644 index 0000000..e23c79f --- /dev/null +++ b/docs/en/custom-chart.md @@ -0,0 +1,78 @@ +# Custom chart + +`laravel-admin 1.5` has removed all the chart components. If you want to add chart components to the page, you can refer to the following process + +Use `chartjs` for example, first download [chartjs](http://chartjs.org/), put it under the public directory, such as in the `public/vendor/chartjs` directory + +Then import the component in `app/Admin/bootstrap.php`: +```php +use Encore\Admin\Facades\Admin; + +Admin::js('/vendor/chartjs/dist/Chart.min.js'); + +``` + +Create a new view file `resources/views/admin/charts/bar.blade.php` + +```php + + +``` + +And then you can introduce this chart view anywhere on the page: + +```php +public function index() +{ + return Admin::content(function (Content $content) { + + $content->header('chart'); + $content->description('.....'); + + $content->body(view('admin.charts.bar')); + }); +} + +``` + +In the above way you can introduce any chart library. multi-chart page layout, refer to [view layout] (/en/layout.md) \ No newline at end of file diff --git a/docs/en/custom-navbar.md b/docs/en/custom-navbar.md new file mode 100644 index 0000000..a7a17d0 --- /dev/null +++ b/docs/en/custom-navbar.md @@ -0,0 +1,148 @@ +# Customize the head navigation bar + +Since version `1.5.6`, you can add the html element to the top navigation bar, open `app/Admin/bootstrap.php`: +```php +use Encore\Admin\Facades\Admin; + +Admin::navbar(function (\Encore\Admin\Widgets\Navbar $navbar) { + + $navbar->left('html...'); + + $navbar->right('html...'); + +}); +``` + +Methods `left` and `right` are used to add content to the left and right sides of the head, the method parameters can be any object that can be rendered (objects which impletements `Htmlable`, `Renderable`, or has method `__toString()`) or strings. + +## Add elements to the left + +For example, add a search bar on the left, first create a view `resources/views/search-bar.blade.php`: +```php + + +
+
+ + + + +
+
+``` +Then add it to the head navigation bar: +```php +$navbar->left(view('search-bar')); +``` + +## Add elements to the right + +You can only add the `
  • ` tag on the right side of the navigation, such as adding some prompt icons, creating a new rendering class `app/Admin/Extensions/Nav/Links.php` +```php + + + + 4 + +
  • + +
  • + + + 7 + +
  • + +
  • + + + 9 + +
  • + +HTML; + } +} +``` + +Then add it to the head navigation bar: +```php +$navbar->right(new \App\Admin\Extensions\Nav\Links()); +``` + +Or use the following html to add a drop-down menu: +```html + +``` + +More components can be found here [Bootstrap](https://getbootstrap.com/) diff --git a/docs/en/extension-api-tester.md b/docs/en/extension-api-tester.md new file mode 100644 index 0000000..5b4b671 --- /dev/null +++ b/docs/en/extension-api-tester.md @@ -0,0 +1,60 @@ +# Laravel API tester + +`api-tester` is an API testing tool developed for `laravel` that helps you test your laravel API like `postman`. + +![wx20170809-164424](https://user-images.githubusercontent.com/1479100/29112946-1e32971c-7d22-11e7-8cc0-5b7ad25d084e.png) + +## Installation + +```shell +$ composer require laravel-admin-ext/api-tester -vvv + +$ php artisan vendor:publish --tag=api-tester + +``` +And then run the following command to import menus and permissions (which can also be added manually) + +```shell +$ php artisan admin:import api-tester +``` + +Then you can find the entry link in the admin menu, `http://localhost/admin/api-tester`. + +## Usage + +Open `routes/api.php` try to add an api: + +```php +Route::get('test', function () { + return 'hello world'; +}); +``` + +Open the `api-tester` page, you can see `api/test` on the left, select it and click the `Send` button to send request to the api + +### Login as + +`Login as` Fill in the user id you want to log in, you can log in as the user to request the API, add the following API: + +```php +use Illuminate\Http\Request; + +Route::middleware('auth:api')->get('user', function (Request $request) { + return $request->user(); +}); +``` +Fill in the user ID in `Login as` input , then request the api and will respond with the user's model + +### Parameters + +Used to set the request parameters for api , the type can be a string or file, add the following API: + +```php +use Illuminate\Http\Request; + +Route::get('parameters', function (Request $request) { + return $request->all(); +}); +``` + +Fill in the parameters send request and you can see the results \ No newline at end of file diff --git a/docs/en/extension-config.md b/docs/en/extension-config.md new file mode 100644 index 0000000..9d18b31 --- /dev/null +++ b/docs/en/extension-config.md @@ -0,0 +1,44 @@ +# Configuration management + +This tool will store the configuration data in the database + +![wx20170810-100226](https://user-images.githubusercontent.com/1479100/29151322-0879681a-7db3-11e7-8005-03310686c884.png) + +## Installation + +``` +$ composer require laravel-admin-ext/config + +$ php artisan migrate +``` + +Open `app/Providers/AppServiceProvider.php`, and call the `Config::load()` method within the `boot` method: + +```php + Part of the function of the tool will create or delete files in the project, there may be some file or directory permissions errors, the problem needs to be resolved. +> Another part of the database and artisan command can not be used in the web environment. + +## Scaffold + +This Tool can help you build controller, model, migrate files, and run migration files. +access by visit `http://localhost/admin/helpers/scaffold`. + +Which set the migration table structure, the primary key field is automatically generated do not need to fill out. + +![qq20170220-2](https://cloud.githubusercontent.com/assets/1479100/23147949/cbf03e84-f81d-11e6-82b7-d7929c3033a0.png) + +## Database command line + +Database command line tool for web integration,Currently supports `mysql`,` mongodb` and `redis`,access by visit `http://localhost/admin/helpers/terminal/database`. + +Change the database connection in the upper right corner, and then in the bottom of the input box to enter the corresponding database query and then enter, you can get the query results: + +![qq20170220-3](https://cloud.githubusercontent.com/assets/1479100/23147951/ce08e5d6-f81d-11e6-8b20-605e8cd06167.png) + +The use of the database and the operation of the database is consistent, you can run the selected database support query. + +## Artisan command line + +Web version of `Laravel`'s `artisan` command line,you can run artisan commands in it,access it by visit `http://localhost/admin/helpers/terminal/artisan`. + +![qq20170220-1](https://cloud.githubusercontent.com/assets/1479100/23147963/da8a5d30-f81d-11e6-97b9-239eea900ad3.png) + + +## Route list + +This tool can use more intuitive to show all the routes, including uri, http methods and middleware, and also you can query routes. access it by visit`http://localhost/admin/helpers/routes`. + +![helpers_routes](https://user-images.githubusercontent.com/1479100/30899066-e8bdd5ca-a390-11e7-809d-4ceccd0da27f.png) \ No newline at end of file diff --git a/docs/en/extension-media-manager.md b/docs/en/extension-media-manager.md new file mode 100644 index 0000000..61d7ae8 --- /dev/null +++ b/docs/en/extension-media-manager.md @@ -0,0 +1,50 @@ +# Media manager + +This tool for manage local files + +![wx20170809-170104](https://user-images.githubusercontent.com/1479100/29113762-99886c32-7d24-11e7-922d-5981a5849c7a.png) + +## Installation + +``` +$ composer require laravel-admin-ext/media-manager -vvv + +$ php artisan admin:import media-manager +``` + +## Configuration + +Open `config/admin.php` specify the disk you want to manage + +```php + + 'extensions' => [ + + 'media-manager' => [ + 'disk' => 'public' // Points to the disk set in config/filesystem.php + ], + ], + +``` + +`disk` is the local disk you configured in `config/filesystem.php`, visit by access `http://localhost/admin/media`. + +Note If you want to preview the picture in the disk, you must set the access url in the disk configuration: + + +`config/filesystem.php`: +```php + + 'disks' => [ + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', // set url + 'visibility' => 'public', + ], + + ... + ] +``` + diff --git a/docs/en/extension-scheduling.md b/docs/en/extension-scheduling.md new file mode 100644 index 0000000..677d057 --- /dev/null +++ b/docs/en/extension-scheduling.md @@ -0,0 +1,34 @@ +# Task scheduling + +This tool is a web interface for manage Laravel's scheduled tasks + +![wx20170810-101048](https://user-images.githubusercontent.com/1479100/29151552-8affc0b2-7db4-11e7-932a-a10d8a42ec50.png) + +## Installation + +``` +$ composer require laravel-admin-ext/scheduling -vvv + +$ php artisan admin:import scheduling +``` + +Then open `http://localhost/admin/scheduling` + +## Add tasks + +Open `app/Console/Kernel.php`, try adding two scheduled tasks: + +```php +class Kernel extends ConsoleKernel +{ + protected function schedule(Schedule $schedule) + { + $schedule->command('inspire')->everyTenMinutes(); + + $schedule->command('route:list')->dailyAt('02:00'); + } +} + +``` + +And then you can see the tasks with details in the page, and you can also directly run these two tasks in the page. diff --git a/docs/en/installation.md b/docs/en/installation.md new file mode 100644 index 0000000..3df0c7d --- /dev/null +++ b/docs/en/installation.md @@ -0,0 +1,57 @@ +# Installation + +> This package requires PHP 7+ and Laravel 5.5, for old versions please refer to [1.4](http://laravel-admin.org/docs/v1.4/#/) + +First, install laravel, and make sure that the database connection settings are correct. + +Then install require this package with command: +``` +composer require encore/laravel-admin "1.5.*" +``` + +Publish assets and config with command: +``` +php artisan vendor:publish --provider="Encore\Admin\AdminServiceProvider" +``` + +After runnung previous command you can find config file in `config/admin.php`, in this file you can change default install directory (```/app/Admin```), db connection or table names. + +At last run following command to finish install: +``` +php artisan admin:install +``` + +To check that all is working, run `php artisan serve` and open `http://localhost/admin/` in browser, use username `admin` and password `admin` to login. + +## Generated files + +After the installation is complete, the following files are generated in the project directory: + +### Configuration file + +After the installation is complete, all configurations are in the `config/admin.php` file. + +### Admin files + +After install,you can find directory`app/Admin`,and then most of our develop work is under this directory. + +``` +app/Admin +├── Controllers +│   ├── ExampleController.php +│   └── HomeController.php +├── bootstrap.php +└── routes.php +``` + +`app/Admin/routes.php` is used to define routes. + +`app/Admin/bootstrap.php` is bootstrapper for laravel-admin, for usage examples see comments inside it. + +The `app/Admin/Controllers` directory is used to store all the controllers. +The `HomeController.php` file under this directory is used to handle home request of admin. +The `ExampleController.php` file is a controller example. + +### Static assets + +The front-end static files are in the `/public/packages/admin` directory. diff --git a/docs/en/model-form-callback.md b/docs/en/model-form-callback.md new file mode 100644 index 0000000..5219ac2 --- /dev/null +++ b/docs/en/model-form-callback.md @@ -0,0 +1,103 @@ +# Model form callback + +`model-form` currently has three methods for receiving callback functions: + +```php +// callback after form submission +$form->submitted(function (Form $form) { + //... +}); + +// callback before save +$form->saving(function (Form $form) { + //... +}); + +// callback after save +$form->saved(function (Form $form) { + //... +}); + +``` +If required, you can add additional fields to ignore using the submitted function e.g. +```php +$form->submitted(function (Form $form) { + $form->ignore('username'); + +}); + +``` +The form data that is currently submitted can be retrieved from the callback parameter `$form`: + +```php +$form->saving(function (Form $form) { + + dump($form->username); + +}); + +``` + +Get data in model +```php +$form->saved(function (Form $form) { + + $form->model()->id; + +}); +``` + +Can redirect other urls by returning an instance of `Symfony\Component\HttpFoundation\Response` directly in the callback: + +```php +$form->saving(function (Form $form) { + + // returns a simple response + return response('xxxx'); + +}); + +$form->saving(function (Form $form) { + + // redirect url + return redirect('/admin/users'); + +}); + +$form->saving(function (Form $form) { + + // throws an exception + throw new \Exception('Error friends. . .'); + +}); + +``` + +Return error or success information on the page: + +```php +use Illuminate\Support\MessageBag; + +// redirect back with an error message +$form->saving(function ($form) { + + $error = new MessageBag([ + 'title' => 'title...', + 'message' => 'message....', + ]); + + return back()->with(compact('error')); +}); + +// redirect back with a successful message +$form->saving(function ($form) { + + $success = new MessageBag([ + 'title' => 'title...', + 'message' => 'message....', + ]); + + return back()->with(compact('success')); +}); + +``` diff --git a/docs/en/model-form-field-management.md b/docs/en/model-form-field-management.md new file mode 100644 index 0000000..54b2d2d --- /dev/null +++ b/docs/en/model-form-field-management.md @@ -0,0 +1,188 @@ +# Fields management + + +## Remove field + +The built-in `map` and `editor` fields requires the front-end files via cdn, and if there are problems with the network, they can be removed in the following ways + +Locate the file `app/Admin/bootstrap.php`. If the file does not exist, update `laravel-admin` and create this file. + +```php + +script = <<id}"), { + lineNumbers: true, + mode: "text/x-php", + extraKeys: { + "Tab": function(cm){ + cm.replaceSelection(" " , "end"); + } + } +}); + +EOT; + return parent::render(); + + } +} + +``` + +>Static resources in the class can also be imported from outside, see [Editor.php](https://github.com/z-song/laravel-admin/blob/1.3/src/Form/Field/Editor.php) + +Create a view file `resources/views/admin/php-editor.blade.php`: + +```php + +
    + + + +
    + + @include('admin::form.error') + + +
    +
    + +``` + +Finally, find the file `app/Admin/bootstrap.php`, if the file does not exist, update `laravel-admin`, and then create this file, add the following code: + +``` +php('code'); + +``` + +In this way, you can add any form fields you want to add. + +## Integrate CKEditor + +Here is another example to show you how to integrate ckeditor. + +At first download [CKEditor](http://ckeditor.com/download), unzip to public directory, for example `public/packages/ckeditor/`. + +Then Write Extension class `app/Admin/Extensions/Form/CKEditor.php`: +```php +script = "$('textarea.{$this->getElementClass()}').ckeditor();"; + + return parent::render(); + } +} +``` +Add blade file `resources/views/admin/ckeditor.blade.php` for view `admin.ckeditor` : +```php +
    + + + +
    + + @include('admin::form.error') + + + + @include('admin::form.help-block') + +
    +
    + +``` +Register this extension in `app/Admin/bootstrap.php`: + +```php +use Encore\Admin\Form; +use App\Admin\Extensions\Form\CKEditor; + +Form::extend('ckeditor', CKEditor::class); +``` +After this you can use ckeditor in your form: + +```php +$form->ckeditor('content'); +``` diff --git a/docs/en/model-form-fields.md b/docs/en/model-form-fields.md new file mode 100644 index 0000000..d8d5209 --- /dev/null +++ b/docs/en/model-form-fields.md @@ -0,0 +1,650 @@ +# Builtin form fields + +There are a lots of form components built into the `model-form` to help you quickly build forms. + +## Public methods + +### Set the value to save +```php +$form->text('title')->value('text...'); +``` + +### Set default value +```php +$form->text('title')->default('text...'); +``` + +### Set help message +```php +$form->text('title')->help('help...'); +``` + +### Set attributes of field element +```php +$form->text('title')->attribute(['data-title' => 'title...']); + +$form->text('title')->attribute('data-title', 'title...'); +``` + +### Set placeholder +```php +$form->text('title')->placeholder('Please input...'); +``` + +### Model-form-tab + +If the form contains too many fields, will lead to form page is too long, in which case you can use the tab to separate the form: + +```php + +$form->tab('Basic info', function ($form) { + + $form->text('username'); + $form->email('email'); + +})->tab('Profile', function ($form) { + + $form->image('avatar'); + $form->text('address'); + $form->mobile('phone'); + +})->tab('Jobs', function ($form) { + + $form->hasMany('jobs', function () { + $form->text('company'); + $form->date('start_date'); + $form->date('end_date'); + }); + + }) + +``` + +## Text input + +```php +$form->text($column, [$label]); + +// Add a submission validation rule +$form->text($column, [$label])->rules('required|min:10'); +``` + +## Select +```php +$form->select($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name']); +``` + +If have too many options, you can load option by ajax: + +```php +$form->select('user_id')->options(function ($id) { + $user = User::find($id); + + if ($user) { + return [$user->id => $user->name]; + } +})->ajax('/admin/api/users'); + +// using ajax and show selected item: + +$form->select('user_id')->options(User::class)->ajax('/admin/api/users'); + +// or specifying the name and id + +$form->select('user_id')->options(User::class, 'name', 'id')->ajax('/admin/api/users'); +``` + +Notice:if you have modified the value of the `route.prefix` in the `config/admin.php` file, this api route should be modified to `config('admin.route.prefix').'/api/users'`. + +The controller method for api `/admin/api/users` is: + +```php +public function users(Request $request) +{ + $q = $request->get('q'); + + return User::where('name', 'like', "%$q%")->paginate(null, ['id', 'name as text']); +} + +``` + +The json returned from api `/admin/demo/options`: +``` +{ + "total": 4, + "per_page": 15, + "current_page": 1, + "last_page": 1, + "next_page_url": null, + "prev_page_url": null, + "from": 1, + "to": 3, + "data": [ + { + "id": 9, + "text": "xxx" + }, + { + "id": 21, + "text": "xxx" + }, + { + "id": 42, + "text": "xxx" + }, + { + "id": 48, + "text": "xxx" + } + ] +} +``` + +### Select linkage + +`select` component supports one-way linkage of parent-child relationship: +```php +$form->select('province')->options(...)->load('city', '/api/city'); + +$form->select('city'); + +``` + +Where `load('city', '/api/city');` means that, after the current select option is changed, the current option will call the api `/api/city` via the argument` q` api returns the data to fill the options for the city selection box, where api `/api/city` returns the data format that must match: + +```php +[ + { + "id": 1, + "text": "foo" + }, + { + "id": 2, + "text": "bar" + }, + ... +] +``` +The code for the controller action is as follows: + +```php +public function city(Request $request) +{ + $provinceId = $request->get('q'); + + return ChinaArea::city()->where('parent_id', $provinceId)->get(['id', DB::raw('name as text')]); +} +``` + +## Multiple select +```php +$form->multipleSelect($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name']); + +// using ajax and show selected items: + +$form->multipleSelect($column[, $label])->options(Model::class)->ajax('ajax_url'); + +// or specifying the name and id + +$form->multipleSelect($column[, $label])->options(Model::class, 'name', 'id')->ajax('ajax_url'); +``` + +You can store value of multiple select in two ways, one is `many-to-many` relation. + +``` + +class Post extends Models +{ + public function tags() + { + return $this->belongsToMany(Tag::class); + } +} + +$form->multipleSelect('tags')->options(Tag::all()->pluck('name', 'id')); + +``` + +The second is to store the option array into a single field. If the field is a string type, it is necessary to define [accessor and Mutator](https://laravel.com/docs/5.5/eloquent-mutators) for the field. + +If have too many options, you can load option by ajax + +```php +$form->select('user_id')->options(function ($id) { + $user = User::find($id); + + if ($user) { + return [$user->id => $user->name]; + } +})->ajax('/admin/api/users'); +``` + +Notice:If you have modified the value of the `route.prefix` in the `config/admin.php` file, this api route should be modified to `config('admin.route.prefix').'/api/users'`. + +The controller method for api `/admin/api/users` is: + +```php +public function users(Request $request) +{ + $q = $request->get('q'); + + return User::where('name', 'like', "%$q%")->paginate(null, ['id', 'name as text']); +} + +``` + +The json returned from api `/admin/demo/options`: +``` +{ + "total": 4, + "per_page": 15, + "current_page": 1, + "last_page": 1, + "next_page_url": null, + "prev_page_url": null, + "from": 1, + "to": 3, + "data": [ + { + "id": 9, + "text": "xxx" + }, + { + "id": 21, + "text": "xxx" + }, + { + "id": 42, + "text": "xxx" + }, + { + "id": 48, + "text": "xxx" + } + ] +} +``` + +## Listbox + +The usage is as same as mutipleSelect. + +```php +$form->listbox($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name']); +``` + +## Textarea +```php +$form->textarea($column[, $label])->rows(10); +``` + +## Radio +```php +$form->radio($column[, $label])->options(['m' => 'Female', 'f'=> 'Male'])->default('m'); + +$form->radio($column[, $label])->options(['m' => 'Female', 'f'=> 'Male'])->default('m')->stacked(); +``` + +## Checkbox + +`checkbox` can store values in two ways, see[multiple select](#Multiple select) + +The `options()` method is used to set options: +```php +$form->checkbox($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name']); + +$form->checkbox($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name'])->stacked(); +``` + +## Email input +```php +$form->email($column[, $label]); +``` + +## Password input +```php +$form->password($column[, $label]); +``` + +## URL input +```php +$form->url($column[, $label]); +``` + +## Ip input +```php +$form->ip($column[, $label]); +``` + +## Phone number input +```php +$form->mobile($column[, $label])->options(['mask' => '999 9999 9999']); +``` + +## Color select +```php +$form->color($column[, $label])->default('#ccc'); +``` + +## Time input +```php +$form->time($column[, $label]); + +// Set the time format, more formats reference http://momentjs.com/docs/#/displaying/format/ +$form->time($column[, $label])->format('HH:mm:ss'); +``` + +## Date input +```php +$form->date($column[, $label]); + +// Date format setting,more format please see http://momentjs.com/docs/#/displaying/format/ +$form->date($column[, $label])->format('YYYY-MM-DD'); +``` + +## Datetime input +```php +$form->datetime($column[, $label]); + +// Set the date format, more format reference http://momentjs.com/docs/#/displaying/format/ +$form->datetime($column[, $label])->format('YYYY-MM-DD HH:mm:ss'); +``` + +## Time range select +`$startTime`、`$endTime`is the start and end time fields: +```php +$form->timeRange($startTime, $endTime, 'Time Range'); +``` + +## Date range select +`$startDate`、`$endDate`is the start and end date fields: +```php +$form->dateRange($startDate, $endDate, 'Date Range'); +``` + +## Datetime range select +`$startDateTime`、`$endDateTime` is the start and end datetime fields: +```php +$form->datetimeRange($startDateTime, $endDateTime, 'DateTime Range'); +``` + +## Currency input +```php +$form->currency($column[, $label]); + +// set the unit symbol +$form->currency($column[, $label])->symbol('¥'); + +``` + +## Number input +```php +$form->number($column[, $label]); +``` + +## Rate input +```php +$form->rate($column[, $label]); +``` + +## Image upload + +Before use upload field, you must complete upload configuration, see [image/file upload](/en/model-form-upload.md). + +You can use compression, crop, add watermarks and other methods, please refer to [[Intervention] (http://image.intervention.io/getting_started/introduction)], picture upload directory in the file `config / admin.php` `Upload.image` configuration, if the directory does not exist, you need to create the directory and open write permissions: +```php +$form->image($column[, $label]); + +// Modify the image upload path and file name +$form->image($column[, $label])->move($dir, $name); + +// Crop picture +$form->image($column[, $label])->crop(int $width, int $height, [int $x, int $y]); + +// Add a watermark +$form->image($column[, $label])->insert($watermark, 'center'); + +// add delete button +$form->image($column[, $label])->removable(); + +``` + +## File upload + +Before use upload field, you must complete upload configuration, see [image/file upload](/en/model-form-upload.md). + +The file upload directory is configured in `upload.file` in the file `config/admin.php`. If the directory does not exist, it needs to be created and write-enabled. +```php +$form->file($column[, $label]); + +// Modify the file upload path and file name +$form->file($column[, $label])->move($dir, $name); + +// And set the upload file type +$form->file($column[, $label])->rules('mimes:doc,docx,xlsx'); + +// add delete button +$form->file($column[, $label])->removable(); + +``` + +## Multiple image/file upload + +```php +// multiple image +$form->multipleImage($column[, $label]); + +// multiple file +$form->multipleFile($column[, $label]); + +// add delete button +$form->multipleFile($column[, $label])->removable(); +``` + +The type of data submitted from multiple image/file field is array, if you the type of column in mysql table is array, or use mongodb, then you can save the array directly, +but if you use string type to store the array data ,you need to specify a string format, For example, if you want to use json string to store the array data, you need to define + a mutator for the column in model mutator, such as the field named `pictures`, define mutator: + +```php +public function setPicturesAttribute($pictures) +{ + if (is_array($pictures)) { + $this->attributes['pictures'] = json_encode($pictures); + } +} + +public function getPicturesAttribute($pictures) +{ + return json_decode($pictures, true); +} +``` +Of course, you can also specify any other format. + +## Map + +The map field refers to the network resource, and if there is a problem with the network refer to [form Component Management](/en/model-form-field-management.md) to remove the component. + +Used to select the latitude and longitude, `$ latitude`,` $ longitude` for the latitude and longitude field, using Tencent map when `locale` set of laravel is` zh_CN`, otherwise use Google Maps: +```php +$form->map($latitude, $longitude, $label); + +// Use Tencent map +$form->map($latitude, $longitude, $label)->useTencentMap(); + +// Use google map +$form->map($latitude, $longitude, $label)->useGoogleMap(); +``` + +## Slider +Can be used to select the type of digital fields, such as age: +```php +$form->slider($column[, $label])->options(['max' => 100, 'min' => 1, 'step' => 1, 'postfix' => 'years old']); +``` +More options please ref to https://github.com/IonDen/ion.rangeSlider#settings + +## Rich text editor + +The editor field refers to the network resource, and if there is a problem with the network refer to [form Component Management](/en/model-form-field-management.md) to remove the component. + +```php +$form->editor($column[, $label]); +``` + +## Hidden field +```php +$form->hidden($column); +``` + +## Switch +`On` and` off` pairs of switches with the values `1` and` 0`: +```php +$states = [ + 'on' => ['value' => 1, 'text' => 'enable', 'color' => 'success'], + 'off' => ['value' => 0, 'text' => 'disable', 'color' => 'danger'], +]; + +$form->switch($column[, $label])->states($states); +``` + +## Display field +Only display the fields and without any action: +```php +$form->display($column[, $label]); +``` + +## Divide +```php +$form->divide(); +``` + +## Html +insert html,the argument passed in could be objects which impletements `Htmlable`、`Renderable`, or has method `__toString()` +```php +$form->html('html contents'); +``` + +## Tags +Insert the comma (,) separated string `tags` +```php +$form->tags('keywords'); +``` + +## Icon +Select the `font-awesome` icon. +```php +$form->icon('icon'); +``` + +## HasMany + +One-to-many built-in tables for dealing with one-to-many relationships. Here is a simple example: + +There are two tables are one-to-many relationship: + +```sql +CREATE TABLE `demo_painters` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `bio` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +CREATE TABLE `demo_paintings` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `painter_id` int(10) unsigned NOT NULL, + `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `body` text COLLATE utf8_unicode_ci NOT NULL, + `completed_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`id`), + KEY painter_id (`painter_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +``` + +The model of tables are: +```php +hasMany(Painting::class, 'painter_id'); + } +} + +belongsTo(Painter::class, 'painter_id'); + } +} +``` + +Build the form code as follows: +```php +$form->display('id', 'ID'); + +$form->text('username')->rules('required'); +$form->textarea('bio')->rules('required'); + +$form->hasMany('paintings', function (Form\NestedForm $form) { + $form->text('title'); + $form->image('body'); + $form->datetime('completed_at'); +}); + +$form->display('created_at', 'Created At'); +$form->display('updated_at', 'Updated At'); +``` + +## Embeds + +Used to handle the `JSON` type field data of `mysql` or `object` type data of `mongodb`, or the data values of multiple fields can be stored in the form of the` JSON` string in the character type of mysql + +Such as the `extra` column of the `JSON` or string type in the orders table, used to store data for multiple fields: + +```php +class Order extends Model +{ + protected $casts = [ + 'extra' => 'json', + ]; +} +``` +And then use in the form: +```php +$form->embeds('extra', function ($form) { + + $form->text('extra1')->rules('required'); + $form->email('extra2')->rules('required'); + $form->mobile('extra3'); + $form->datetime('extra4'); + + $form->dateRange('extra5', 'extra6', 'Date range')->rules('required'); + +}); + +// Customize the title +$form->embeds('extra', 'Extra', function ($form) { + ... +}); +``` + +Callback function inside the form element to create the method call and the outside is the same. diff --git a/docs/en/model-form-upload.md b/docs/en/model-form-upload.md new file mode 100644 index 0000000..b1e5154 --- /dev/null +++ b/docs/en/model-form-upload.md @@ -0,0 +1,115 @@ +# File/Image upload + +[model-form](/en/model-form.md) can build file and image upload field with following codes + +```php +$form->file('file_column'); +$form->image('image_column'); +``` + +### Change store path and name + +```php + +// change upload path +$form->image('picture')->move('public/upload/image1/'); + +// use a unique name (md5(uniqid()).extension) +$form->image('picture')->uniqueName(); + +// specify filename +$form->image('picture')->name(function ($file) { + return 'test.'.$file->guessExtension(); +}); + +``` + +[model-form](/en/model-form.md) both support for local and cloud storage upload + +### Upload to local + +first add storage configuration, add a disk in `config/filesystems.php`: + +```php + +'disks' => [ + ... , + + 'admin' => [ + 'driver' => 'local', + 'root' => public_path('uploads'), + 'visibility' => 'public', + 'url' => env('APP_URL').'/uploads', + ], +], + +``` + +set upload path to `public/upload`(public_path('upload')). + +And then in `config/admin.php` select the `disk` set up above: + +```php + +'upload' => [ + + 'disk' => 'admin', + + 'directory' => [ + 'image' => 'image', + 'file' => 'file', + ], +], + +``` + +Set `disk` to the` admin` that you added above,`directory.image` and `directory.file` is the upload path for `$form->image($column)` and `$form->file($column)`. + +`host` is url prefix for your uploaded files. + + +### Upload to cloud + +If you need to upload to the cloud storage, need to install a driver which supports `flysystem` adapter, take `qiniu` cloud storage as example. + +first install [zgldh/qiniu-laravel-storage](https://github.com/zgldh/qiniu-laravel-storage). + +Also configure the disk, in the `config/filesystems.php` add an item: + +```php +'disks' => [ + ... , + 'qiniu' => [ + 'driver' => 'qiniu', + 'domains' => [ + 'default' => 'xxxxx.com1.z0.glb.clouddn.com', + 'https' => 'dn-yourdomain.qbox.me', + 'custom' => 'static.abc.com', + ], + 'access_key'=> '', //AccessKey + 'secret_key'=> '', //SecretKey + 'bucket' => '', //Bucket + 'notify_url'=> '', // + 'url' => 'http://of8kfibjo.bkt.clouddn.com/', + ], +], + +``` + +Then modify the upload configuration of `laravel-admin` and open `config/admin.php` to find: + +```php + +'upload' => [ + + 'disk' => 'qiniu', + + 'directory' => [ + 'image' => 'image', + 'file' => 'file', + ], +], + +``` + +Select the above configuration` qiniu` for `disk` \ No newline at end of file diff --git a/docs/en/model-form-validation.md b/docs/en/model-form-validation.md new file mode 100644 index 0000000..955a0a2 --- /dev/null +++ b/docs/en/model-form-validation.md @@ -0,0 +1,36 @@ +Form validation +======== + +`model-form` uses laravel's validation rules to verify the data submitted by the form: + +```php +$form->text('title')->rules('required|min:3'); + +// Complex validation rules can be implemented in the callback +$form->text('title')->rules(function ($form) { + + // If it is not an edit state, add field unique verification + if (!$id = $form->model()->id) { + return 'unique:users,email_address'; + } + +}); + +``` + +You can also customize the error message for the validation rule: + +```php +$form->text('code')->rules('required|regex:/^\d+$/|min:10', [ + 'regex' => 'code must be numbers', + 'min' => 'code can not be less than 10 characters', +]); +``` + +If you want to allow the field to be empty, first in the database table to face the field set to `NULL`, and then + +```php +$form->text('title')->rules('nullable'); +``` + +Please refer to the more rules [Validation](https://laravel.com/docs/5.5/validation). \ No newline at end of file diff --git a/docs/en/model-form.md b/docs/en/model-form.md new file mode 100644 index 0000000..8456e0c --- /dev/null +++ b/docs/en/model-form.md @@ -0,0 +1,179 @@ +# Model-Form + +The `Encore\Admin\Form` class is used to generate a data model-based form. For example, there is a` movies` table in the database + +```sql +CREATE TABLE `movies` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `director` int(10) unsigned NOT NULL, + `describe` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `rate` tinyint unsigned NOT NULL, + `released` enum(0, 1), + `release_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +``` + +The corresponding data model is `App\Models\Movie`, and the following code can generate the` movies` data form: + +```php + +use App\Models\Movie; +use Encore\Admin\Form; +use Encore\Admin\Facades\Admin; + +$grid = Admin::form(Movie::class, function(Form $grid){ + + // Displays the record id + $form->display('id', 'ID'); + + // Add an input box of type text + $form->text('title', 'Movie title'); + + $directors = [ + 1 => 'John', + 2 => 'Smith', + 3 => 'Kate', + ]; + + $form->select('director', 'Director')->options($directors); + + // Add textarea for the describe field + $form->textarea('describe', 'Describe'); + + // Number input + $form->number('rate', 'Rate'); + + // Add a switch field + $form->switch('released', 'Released?'); + + // Add a date and time selection box + $form->dateTime('release_at', 'release time'); + + // Display two time column + $form->display('created_at', 'Created time'); + $form->display('updated_at', 'Updated time'); +}); + +``` + +## Custom tools + +The top right corner of the form has two button tools by default. You can modify it in the following way: + +```php +$form->tools(function (Form\Tools $tools) { + + // Disable back btn. + $tools->disableBackButton(); + + // Disable list btn + $tools->disableListButton(); + + // Add a button, the argument can be a string, or an instance of the object that implements the Renderable or Htmlable interface + $tools->add('  delete'); +}); +``` + +## Other methods + +Disable submit btn: + +```php +$form->disableSubmit(); +``` + +Disable reset btn: +```php +$form->disableReset(); +``` + +Ignore fields to store +```php +$form->ignore('column1', 'column2', 'column3'); +``` + +Set width for label and field + +```php +$form->setWidth(10, 2); +``` + +Set form action + +```php +$form->setAction('admin/users'); +``` + +## Model relationship + + +### One to One +The `users` table and the `profiles` table are generated one-to-one relation through the `profiles.user_id` field. + +```sql + +CREATE TABLE `users` ( +`id` int(10) unsigned NOT NULL AUTO_INCREMENT, +`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`email` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', +`updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', +PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +CREATE TABLE `profiles` ( +`id` int(10) unsigned NOT NULL AUTO_INCREMENT, +`user_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`age` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`gender` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', +`updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', +PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +``` + +The corresponding data model are: + +```php + +class User extends Model +{ + public function profile() + { + return $this->hasOne(Profile::class); + } +} + +class Profile extends Model +{ + public function user() + { + return $this->belongsTo(User::class); + } +} + +``` + +You can associate them in a form with the following code: + +```php +Admin::form(User::class, function (Form $form) { + + $form->display('id'); + + $form->text('name'); + $form->text('email'); + + $form->text('profile.age'); + $form->text('profile.gender'); + + $form->datetime('created_at'); + $form->datetime('updated_at'); +}); + +``` diff --git a/docs/en/model-grid-actions.md b/docs/en/model-grid-actions.md new file mode 100644 index 0000000..9f182f5 --- /dev/null +++ b/docs/en/model-grid-actions.md @@ -0,0 +1,105 @@ +# Model grid row actions + +`model-grid` By default, there are two actions `edit` and `delete`, which can be turned off in the following way: + +```php + $grid->actions(function ($actions) { + $actions->disableDelete(); + $actions->disableEdit(); +}); +``` +You can get the data for the current row by `$actions` parameter passed in: +```php + $grid->actions(function ($actions) { + + // the array of data for the current row + $actions->row; + + // gets the current row primary key value + $actions->getKey(); +}); +``` + +If you have a custom action button, you can add the following: + +```php +$grid->actions(function ($actions) { + + // append an action. + $actions->append(''); + + // prepend an action. + $actions->prepend(''); +} +``` + +If you have more complex actions, you can refer to the following ways: + +First define the action class: +```php +id = $id; + } + + protected function script() + { + return << +``` + +然后就可以在页面的任何地方引入这个图表视图了: + +```php +public function index() +{ + return Admin::content(function (Content $content) { + + $content->header('chart'); + $content->description('.....'); + + $content->body(view('admin.charts.bar')); + }); +} + +``` + +按照上面的方式可以引入任意图表库,多图表页面的布局,参考[视图布局](/zh/layout.md) \ No newline at end of file diff --git a/docs/zh/custom-navbar.md b/docs/zh/custom-navbar.md new file mode 100644 index 0000000..8f5fdf6 --- /dev/null +++ b/docs/zh/custom-navbar.md @@ -0,0 +1,148 @@ +# 自定义头部导航条 + +从版本`1.5.6`开始,可以在顶部导航条上添加html元素了, 打开`app/Admin/bootstrap.php`: +```php +use Encore\Admin\Facades\Admin; + +Admin::navbar(function (\Encore\Admin\Widgets\Navbar $navbar) { + + $navbar->left('html...'); + + $navbar->right('html...'); + +}); +``` + +`left`和`right`方法分别用来在头部的左右两边添加内容,方法参数可以是任何可以渲染的对象(实现了`Htmlable`、`Renderable`接口或者包含`__toString()`方法的对象)或字符串 + +## 左侧添加示例 + +举个例子,比如在左边添加一个搜索条,先创建一个blade视图`resources/views/search-bar.blade.php`: +```php + + +
    +
    + + + + +
    +
    +``` +然后加入头部导航条: +```php +$navbar->left(view('search-bar')); +``` + +## 右侧添加示例 + +导航右侧只能添加`
  • `标签, 比如要添加一些提示图标,新建渲染对象`app/Admin/Extensions/Nav/Links.php` +```php + + + + 4 + +
  • + +
  • + + + 7 + +
  • + +
  • + + + 9 + +
  • + +HTML; + } +} +``` + +然后加入头部导航条: +```php +$navbar->right(new \App\Admin\Extensions\Nav\Links()); +``` + +或者用下面的html加入下拉菜单: +```html + +``` + +更多的组件可以参考[Bootstrap](https://getbootstrap.com/) \ No newline at end of file diff --git a/docs/zh/extension-api-tester.md b/docs/zh/extension-api-tester.md new file mode 100644 index 0000000..2e3cfae --- /dev/null +++ b/docs/zh/extension-api-tester.md @@ -0,0 +1,60 @@ +# Laravel API测试 + +`api-tester`是专门针对`laravel`开发的API测试工具,能够帮助你像`postman`一样测试你的laravel API。 + +![wx20170809-164424](https://user-images.githubusercontent.com/1479100/29112946-1e32971c-7d22-11e7-8cc0-5b7ad25d084e.png) + +## 安装 + +```shell +$ composer require laravel-admin-ext/api-tester -vvv + +$ php artisan vendor:publish --tag=api-tester + +``` +然后运行下面的命令导入菜单和权限(也可以手动添加) + +```shell +$ php artisan admin:import api-tester +``` + +然后就能在后台的左侧菜单找到入口链接,`http://localhost/admin/api-tester`。 + +## 使用 + +打开`routes/api.php`试着添加一个api: + +```php +Route::get('test', function () { + return 'hello world'; +}); +``` + +打开`api-tester`页面,就能在左侧看到`api/test`, 选择它然后点击右侧的`Send`,就能请求这个API,下面会输出请求结果, + +### Login as + +`Login as`填写你要登陆的用户的id, 就可以以这个用户的身份登陆来请求API,加入下面的API: + +```php +use Illuminate\Http\Request; + +Route::middleware('auth:api')->get('user', function (Request $request) { + return $request->user(); +}); +``` +`Login as`填写用户ID,请求接口后就能返回这个用户的模型 + +### Parameters + +用来填写接口的请求参数,类型可以是字符串或者文件, 添加下面的API: + +```php +use Illuminate\Http\Request; + +Route::get('parameters', function (Request $request) { + return $request->all(); +}); +``` + +然后填写参数可以看到效果 \ No newline at end of file diff --git a/docs/zh/extension-config.md b/docs/zh/extension-config.md new file mode 100644 index 0000000..d1d725e --- /dev/null +++ b/docs/zh/extension-config.md @@ -0,0 +1,47 @@ +# 配置管理 + +这个工具将配置数据存在数据库中,然后在能在Laravel中能像普通配置一样使用 + +![wx20170810-100226](https://user-images.githubusercontent.com/1479100/29151322-0879681a-7db3-11e7-8005-03310686c884.png) + +## 安装 + +``` +$ composer require laravel-admin-ext/config + +$ php artisan migrate +``` + +打开`app/Providers/AppServiceProvider.php`, 在`boot`方法中添加`Config::load();`: + +```php + 工具的部分功能会在项目中创建或删除文件,可能会出现文件或目录权限的问题,这个问题需要自行解决。 +> 另外部分数据库和artisan命令无法在web环境下使用。 + +## 脚手架工具 + +脚手架工具能帮你一键生成控制器、模型、迁移文件,并运行迁移文件,访问`http://localhost/admin/helpers/scaffold`打开。 + +其中设置迁移表结构的时候,主键字段是自动生成的不需要填写。 + +![qq20170220-2](https://cloud.githubusercontent.com/assets/1479100/23147949/cbf03e84-f81d-11e6-82b7-d7929c3033a0.png) + +## 数据库命令行 + +数据库命令行工具的web集成,目前支持`mysql`、`mongodb` 和 `redis`,访问`http://localhost/admin/helpers/terminal/database`打开。 + +在右上角的`select`选择框切换数据库连接,然后在底部的输入框输入对应数据库的查询语句然后回车,就能得到查询结果: + +![qq20170220-3](https://cloud.githubusercontent.com/assets/1479100/23147951/ce08e5d6-f81d-11e6-8b20-605e8cd06167.png) + +实用方式和终端上操作数据库是一致的,可以运行所选择数据库的所支持的查询语句。 + +## artisan命令行工具 + +`Laravel`的`artisan`命令的web实现,可以在上面运行artisan命令,访问`http://localhost/admin/helpers/terminal/artisan`打开。 + +![qq20170220-1](https://cloud.githubusercontent.com/assets/1479100/23147963/da8a5d30-f81d-11e6-97b9-239eea900ad3.png) + +## 路由列表 + +这个工具能用用比较直观的展现出系统的所有路由,包括路由的uri、方法和中间件等,还能查询路由。访问`http://localhost/admin/helpers/routes`打开。 + +![helpers_routes](https://user-images.githubusercontent.com/1479100/30899066-e8bdd5ca-a390-11e7-809d-4ceccd0da27f.png) diff --git a/docs/zh/extension-media-manager.md b/docs/zh/extension-media-manager.md new file mode 100644 index 0000000..4a313f3 --- /dev/null +++ b/docs/zh/extension-media-manager.md @@ -0,0 +1,50 @@ +# 文件管理 + +文件管理是一个对本地文件的可视化管理的工具 + +![wx20170809-170104](https://user-images.githubusercontent.com/1479100/29113762-99886c32-7d24-11e7-922d-5981a5849c7a.png) + +## 安装 + +``` +$ composer require laravel-admin-ext/media-manager -vvv + +$ php artisan admin:import media-manager +``` + +## 配置 + +打开`config/admin.php`指定你要管理的disk + +```php + + 'extensions' => [ + + 'media-manager' => [ + 'disk' => 'public' // 指向config/filesystem.php中设置的disk + ], + ], + +``` + +`disk`为`config/filesystem.php`中设置的本地disk,然后打开`http://localhost/admin/media`访问. + +注意如果要预览disk中的图片,必须在disk中设置访问url前缀: + + +`config/filesystem.php`: +```php + + 'disks' => [ + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', // 设置文件访问url + 'visibility' => 'public', + ], + + ... + ] +``` + diff --git a/docs/zh/extension-scheduling.md b/docs/zh/extension-scheduling.md new file mode 100644 index 0000000..717e428 --- /dev/null +++ b/docs/zh/extension-scheduling.md @@ -0,0 +1,34 @@ +# 定时任务 + +这个工具是管理Laravel计划任务的web管理页面 + +![wx20170810-101048](https://user-images.githubusercontent.com/1479100/29151552-8affc0b2-7db4-11e7-932a-a10d8a42ec50.png) + +## 安装 + +``` +$ composer require laravel-admin-ext/scheduling -vvv + +$ php artisan admin:import scheduling +``` + +打开`http://localhost/admin/scheduling`访问。 + +## 添加任务 + +打开`app/Console/Kernel.php`, 试着添加两项计划任务: + +```php +class Kernel extends ConsoleKernel +{ + protected function schedule(Schedule $schedule) + { + $schedule->command('inspire')->everyTenMinutes(); + + $schedule->command('route:list')->dailyAt('02:00'); + } +} + +``` + +然后就能在后台看到这两项计划任务的详细情况,也能直接运行这两个计划任务。 diff --git a/docs/zh/installation.md b/docs/zh/installation.md new file mode 100644 index 0000000..73d0f65 --- /dev/null +++ b/docs/zh/installation.md @@ -0,0 +1,54 @@ +# 安装 + +> 当前版本(1.5)需要安装`PHP 7+`和`Laravel 5.5`, 如果你使用更早的版本,请参考文档: [1.4](http://laravel-admin.org/docs/v1.4/#/zh/) + +首先确保安装好了`laravel`,并且数据库连接设置正确。 + +``` +composer require encore/laravel-admin "1.5.*" +``` + +然后运行下面的命令来发布资源: + +``` +php artisan vendor:publish --provider="Encore\Admin\AdminServiceProvider" +``` + +在该命令会生成配置文件`config/admin.php`,可以在里面修改安装的地址、数据库连接、以及表名,建议都是用默认配置不修改。 + +然后运行下面的命令完成安装: +``` +php artisan admin:install +``` + +启动服务后,在浏览器打开 `http://localhost/admin/` ,使用用户名 `admin` 和密码 `admin`登陆. + +## 生成的文件 + +安装完成之后,会在项目目录中生成以下的文件: + +### 配置文件 + +安装完成之后,`laravel-admin`所有的配置都在`config/admin.php`文件中。 + +### 后台项目文件 +安装完成之后,后台的安装目录为`app/Admin`,之后大部分的后台开发编码工作都是在这个目录下进行。 + +``` +app/Admin +├── Controllers +│   ├── ExampleController.php +│   └── HomeController.php +├── bootstrap.php +└── routes.php +``` + +`app/Admin/routes.php`文件用来配置后台路由。 + +`app/Admin/bootstrap.php` 是`laravel-admin`的启动文件, 使用方法请参考文件里面的注释. + +`app/Admin/Controllers`目录用来存放后台控制器文件,该目录下的`HomeController.php`文件是后台首页的显示控制器,`ExampleController.php`为实例文件。 + +### 静态文件 + +后台所需的前端静态文件在`/public/vendor/laravel-admin`目录下. \ No newline at end of file diff --git a/docs/zh/model-form-callback.md b/docs/zh/model-form-callback.md new file mode 100644 index 0000000..ffafb3e --- /dev/null +++ b/docs/zh/model-form-callback.md @@ -0,0 +1,89 @@ +# 模型表单回调 + +`model-form`目前提供了两个方法来接收回调函数: + +```php +//保存前回调 +$form->saving(function (Form $form) { + //... +}); + +//保存后回调 +$form->saved(function (Form $form) { + //... +}); + +``` +可以从回调参数`$form`中获取当前提交的表单数据: + +```php +$form->saving(function (Form $form) { + + dump($form->username); + +}); + +``` + +获取获取模型中的数据 +```php +$form->saved(function (Form $form) { + + $form->model()->id; + +}); +``` + +可以直接在回调中返回`Symfony\Component\HttpFoundation\Response`的实例,来跳转或进入页面: +```php +$form->saving(function (Form $form) { + + // 返回一个简单response + return response('xxxx'); + +}); + +$form->saving(function (Form $form) { + + // 跳转页面 + return redirect('/admin/users'); + +}); + +$form->saving(function (Form $form) { + + // 抛出异常 + throw new \Exception('出错啦。。。'); + +}); + +``` + +返回错误或者成功信息在页面上: + +```php +use Illuminate\Support\MessageBag; + +// 抛出错误信息 +$form->saving(function ($form) { + + $error = new MessageBag([ + 'title' => 'title...', + 'message' => 'message....', + ]); + + return back()->with(compact('error')); +}); + +// 抛出成功信息 +$form->saving(function ($form) { + + $success = new MessageBag([ + 'title' => 'title...', + 'message' => 'message....', + ]); + + return back()->with(compact('success')); +}); + +``` \ No newline at end of file diff --git a/docs/zh/model-form-field-management.md b/docs/zh/model-form-field-management.md new file mode 100644 index 0000000..a78ac68 --- /dev/null +++ b/docs/zh/model-form-field-management.md @@ -0,0 +1,280 @@ +# 组件管理 + +## 移除已有组件 + +form表单内置的`map`和`editor`组件通过cdn的方式引用了前端文件,如果网络方面有问题,可以通过下面的方式将它们移除 + +找到文件`app/Admin/bootstrap.php`,如果文件不存在,请更新`laravel-admin`,然后新建该文件 + +```php + +formatName($this->column); + + $this->script = <<id}'); +editor.customConfig.zIndex = 0 +editor.customConfig.uploadImgShowBase64 = true +editor.customConfig.onchange = function (html) { + $('input[name=$name]').val(html); +} +editor.create() + +EOT; + return parent::render(); + } +} + +``` + +新建视图文件`resources/views/admin/wang-editor.blade.php`: +```php +
    + + + +
    + + @include('admin::form.error') + +
    +

    {!! old($column, $value) !!}

    +
    + + + +
    +
    +``` + +然后注册进`laravel-admin`,在`app/Admin/bootstrap.php`中添加以下代码: + +```php + +editor('body'); + +``` + +### 集成富文本编辑器ckeditor + +先下载[ckeditor](http://ckeditor.com/download) 并解压到/public目录,比如放在`/public/packages/`目录下。 + +然后新建扩展文件`app/Admin/Extensions/Form/CKEditor.php`: +```php +script = "$('textarea.{$this->getElementClass()}').ckeditor();"; + + return parent::render(); + } +} +``` + +新建view `resources/views/admin/ckeditor.blade.php`: +```php +
    + + + +
    + + @include('admin::form.error') + + + + @include('admin::form.help-block') + +
    +
    + +``` + +然后在`app/Admin/bootstrap.php`中引入扩展: +```php +use App\Admin\Extensions\Form\CKEditor; +use Encore\Admin\Form; + +Form::extend('ckeditor', CKEditor::class); +``` + +然后就能在form中使用了: +```php +$form->ckeditor('content'); +``` + +### 集成PHP editor + + +通过下面的步骤来扩展一个基于[codemirror](http://codemirror.net/index.html)的PHP代码编辑器,效果参考[PHP mode](http://codemirror.net/mode/php/)。 + +先将[codemirror](http://codemirror.net/codemirror.zip)库下载并解压到前端资源目录下,比如放在`public/packages/codemirror-5.20.2`目录下。 + +新建组件类`app/Admin/Extensions/PHPEditor.php`: + +```php +script = <<id}"), { + lineNumbers: true, + mode: "text/x-php", + extraKeys: { + "Tab": function(cm){ + cm.replaceSelection(" " , "end"); + } + } +}); + +EOT; + return parent::render(); + + } +} + +``` + +>类中的静态资源也同样可以从外部引入,参考[Editor.php](https://github.com/z-song/laravel-admin/blob/1.3/src/Form/Field/Editor.php) + +创建视图`resources/views/admin/php-editor.blade.php`: + +```php + +
    + + + +
    + + @include('admin::form.error') + + +
    +
    + +``` + +最后找到文件`app/Admin/bootstrap.php`,如果文件不存在,请更新`laravel-admin`,然后新建该文件,添加下面代码: + +``` +php('code'); + +``` + +通过这种方式,可以添加任意你想要添加的form组件。 \ No newline at end of file diff --git a/docs/zh/model-form-fields.md b/docs/zh/model-form-fields.md new file mode 100644 index 0000000..b61bbb6 --- /dev/null +++ b/docs/zh/model-form-fields.md @@ -0,0 +1,702 @@ +# 表单组件 + +在`model-form`中内置了大量的form组件来帮助你快速的构建form表单 + +## 公共方法 + +### 设置保存值 +```php +$form->text('title')->value('text...'); +``` + +### 设置默认值 +```php +$form->text('title')->default('text...'); +``` + +### 设置help信息 +```php +$form->text('title')->help('help...'); +``` + +### 设置属性 +```php +$form->text('title')->attribute(['data-title' => 'title...']); + +$form->text('title')->attribute('data-title', 'title...'); +``` + +### 设置placeholder +```php +$form->text('title')->placeholder('请输入。。。'); +``` + +### model-form-tab + +如果表单元素太多,会导致form页面太长, 这种情况下可以使用tab来分隔form: + +```php + +$form->tab('Basic info', function ($form) { + + $form->text('username'); + $form->email('email'); + +})->tab('Profile', function ($form) { + + $form->image('avatar'); + $form->text('address'); + $form->mobile('phone'); + +})->tab('Jobs', function ($form) { + + $form->hasMany('jobs', function () { + $form->text('company'); + $form->date('start_date'); + $form->date('end_date'); + }); + + }) + +``` + +## 文本输入框 + +```php +$form->text($column, [$label]); + +// 添加提交验证规则 +$form->text($column, [$label])->rules('required|min:10'); +``` + +## select选择框 +```php +$form->select($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name']); +``` + +或者从api中获取选项列表: +```php +$form->select($column[, $label])->options('/api/users'); + +// 使用ajax并显示所选项目 + +$form->select($column[, $label])->options(Model::class)->ajax('/api/users'); + +// 或指定名称和ID + +$form->select($column[, $label])->options(Model::class, 'name', 'id')->ajax('/api/users'); +``` +其中api接口的格式必须为下面格式: +```php +[ + { + "id": 9, + "text": "xxx" + }, + { + "id": 21, + "text": "xxx" + }, + ... +] +``` + +如果选项过多,可通过ajax方式动态分页载入选项: + +```php +$form->select('user_id')->options(function ($id) { + $user = User::find($id); + + if ($user) { + return [$user->id => $user->name]; + } +})->ajax('/admin/api/users'); +``` + +注:如果你修改了`config/admin.php`配置文件中`route.prefix`的值,此处的接口路由应该修改为`config('admin.route.prefix').'/api/users'`。 + +API `/admin/api/users`接口的代码: + +```php +public function users(Request $request) +{ + $q = $request->get('q'); + + return User::where('name', 'like', "%$q%")->paginate(null, ['id', 'name as text']); +} + +``` +接口返回的数据结构为 +``` +{ + "total": 4, + "per_page": 15, + "current_page": 1, + "last_page": 1, + "next_page_url": null, + "prev_page_url": null, + "from": 1, + "to": 3, + "data": [ + { + "id": 9, + "text": "xxx" + }, + { + "id": 21, + "text": "xxx" + }, + { + "id": 42, + "text": "xxx" + }, + { + "id": 48, + "text": "xxx" + } + ] +} +``` + +### select 联动 + +`select`组件支持父子关系的单向联动: +```php +$form->select('province')->options(...)->load('city', '/api/city'); + +$form->select('city'); + +``` + +其中`load('city', '/api/city');`的意思是,在当前select的选项切换之后,会把当前选项的值通过参数`q`, 调用接口`/api/city`,并把api返回的数据填充为city选择框的选项,其中api`/api/city`返回的数据格式必须符合: + +```php +[ + { + "id": 9, + "text": "xxx" + }, + { + "id": 21, + "text": "xxx" + }, + ... +] +``` +控制器action的代码示例如下: + +```php +public function city(Request $request) +{ + $provinceId = $request->get('q'); + + return ChinaArea::city()->where('parent_id', $provinceId)->get(['id', DB::raw('name as text')]); +} +``` + +## 多选框 + +```php +$form->multipleSelect($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name']); + +// 使用ajax并显示所选项目: + +$form->multipleSelect($column[, $label])->options(Model::class)->ajax('ajax_url'); + +// 或指定名称和ID + +$form->multipleSelect($column[, $label])->options(Model::class, 'name', 'id')->ajax('ajax_url'); +``` + +多选框可以处理两种情况,第一种是`ManyToMany`的关系。 + +``` + +class Post extends Models +{ + public function tags() + { + return $this->belongsToMany(Tag::class); + } +} + +$form->multipleSelect('tags')->options(Tag::all()->pluck('name', 'id')); + +``` + +第二种是将选项数组存储到单字段中,如果字段是字符串类型,那就需要在模型里面为该字段定义[访问器和修改器](https://laravel.com/docs/5.5/eloquent-mutators)来存储和读取了。 + +如果选项过多,可通过ajax方式动态分页载入选项: + +```php +$form->select('friends')->options(function ($ids) { + + return User::find($ids)->pluck('name', 'id'); + +})->ajax('/admin/api/users'); +``` + +注:如果你修改了`config/admin.php`配置文件中`route.prefix`的值,此处的接口路由应该修改为`config('admin.route.prefix').'/api/users'`。 + +API `/admin/api/users`接口的代码: + +```php +public function users(Request $request) +{ + $q = $request->get('q'); + + return User::where('name', 'like', "%$q%")->paginate(null, ['id', 'name as text']); +} + +``` +接口返回的数据结构为 +``` +{ + "total": 4, + "per_page": 15, + "current_page": 1, + "last_page": 1, + "next_page_url": null, + "prev_page_url": null, + "from": 1, + "to": 3, + "data": [ + { + "id": 9, + "text": "xxx" + }, + { + "id": 21, + "text": "xxx" + }, + { + "id": 42, + "text": "xxx" + }, + { + "id": 48, + "text": "xxx" + } + ] +} +``` + +## listbox + +使用方法和`multipleSelect`类似 + +```php +$form->listbox($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name']); +``` + +## textarea输入框 +```php +$form->textarea($column[, $label])->rows(10); +``` + +## radio选择 +```php +$form->radio($column[, $label])->options(['m' => 'Female', 'f'=> 'Male'])->default('m'); + +// 竖排 +$form->radio($column[, $label])->options(['m' => 'Female', 'f'=> 'Male'])->stacked(); +``` + +## checkbox选择 + +`checkbox`能处理两种数据存储情况,参考[多选框](#多选框) + +`options()`方法用来设置选择项: +```php +$form->checkbox($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name']); + +// 竖排 +$form->checkbox($column[, $label])->options([1 => 'foo', 2 => 'bar', 'val' => 'Option name'])->stacked(); +``` + +## email个数输入框 +```php +$form->email($column[, $label]); +``` + +## 密码输入框 +```php +$form->password($column[, $label]); +``` + +## url输入框 +```php +$form->url($column[, $label]); +``` + +## ip输入框 +```php +$form->ip($column[, $label]); +``` + +## 电话号码输入框 +```php +$form->mobile($column[, $label])->options(['mask' => '999 9999 9999']); +``` + +## 颜色选择框 +```php +$form->color($column[, $label])->default('#ccc'); +``` + +## 时间输入框 +```php +$form->time($column[, $label]); + +// 设置时间格式,更多格式参考http://momentjs.com/docs/#/displaying/format/ +$form->time($column[, $label])->format('HH:mm:ss'); +``` + +## 日期输入框 +```php +$form->date($column[, $label]); + +// 设置日期格式,更多格式参考http://momentjs.com/docs/#/displaying/format/ +$form->date($column[, $label])->format('YYYY-MM-DD'); +``` + +## 日期时间输入框 +```php +$form->datetime($column[, $label]); + +// 设置日期格式,更多格式参考http://momentjs.com/docs/#/displaying/format/ +$form->datetime($column[, $label])->format('YYYY-MM-DD HH:mm:ss'); +``` + +## 时间范围选择框 +`$startTime`、`$endTime`为开始和结束时间字段: +```php +$form->timeRange($startTime, $endTime, 'Time Range'); +``` + +## 日期范围选框 +`$startDate`、`$endDate`为开始和结束日期字段: +```php +$form->dateRange($startDate, $endDate, 'Date Range'); +``` + +## 时间日期范围选择框 +`$startDateTime`、`$endDateTime`为开始和结束时间日期: +```php +$form->datetimeRange($startDateTime, $endDateTime, 'DateTime Range'); +``` + +## 货币输入框 +```php +$form->currency($column[, $label]); + +// 设置单位符号 +$form->currency($column[, $label])->symbol('¥'); + +``` + +## 数字输入框 +```php +$form->number($column[, $label]); +``` + +## 比例输入框 +```php +$form->rate($column[, $label]); +``` + +## 图片上传 + +使用图片上传功能之前需要先完成上传配置,请参考:[图片/文件上传](/zh/model-form-upload.md). + +图片上传目录在文件`config/admin.php`中的`upload.image`中配置,如果目录不存在,需要创建该目录并开放写权限。 + +可以使用压缩、裁切、添加水印等各种方法,需要先安装[intervention/image](http://image.intervention.io/getting_started/installation). + +更多使用方法请参考[[Intervention](http://image.intervention.io/getting_started/introduction)]: +```php +$form->image($column[, $label]); + +// 修改图片上传路径和文件名 +$form->image($column[, $label])->move($dir, $name); + +// 剪裁图片 +$form->image($column[, $label])->crop(int $width, int $height, [int $x, int $y]); + +// 加水印 +$form->image($column[, $label])->insert($watermark, 'center'); + +// 添加图片删除按钮 +$form->image($column[, $label])->removable(); + +``` + +## 文件上传 + +使用图片上传功能之前需要先完成上传配置,请参考:[图片/文件上传](/zh/model-form-upload.md). + +文件上传目录在文件`config/admin.php`中的`upload.file`中配置,如果目录不存在,需要创建该目录并开放写权限。 +```php +$form->file($column[, $label]); + +// 修改文件上传路径和文件名 +$form->file($column[, $label])->move($dir, $name); + +// 并设置上传文件类型 +$form->file($column[, $label])->rules('mimes:doc,docx,xlsx'); + +// 添加文件删除按钮 +$form->file($column[, $label])->removable(); + +``` + +## 多图/文件上传 + +```php +// 多图 +$form->multipleImage($column[, $label]); + +// 添加删除按钮 +$form->multipleImage($column[, $label])->removable(); + +// 多文件 +$form->multipleFile($column[, $label]); + +// 添加删除按钮 +$form->multipleFile($column[, $label])->removable(); +``` + +多图/文件上传的时候提交的数据为文件路径数组,可以直接用mysql的`JSON`类型字段存储,如果用mongodb的话也能直接存储,但是如果用字符串类型来存储的话,就需要指定数据的存储格式了, +比如,如果要用json字符串来存储文件数据,就需要在模型中定义字段的mutator,比如字段名为`pictures`,定义mutator: +```php +public function setPicturesAttribute($pictures) +{ + if (is_array($pictures)) { + $this->attributes['pictures'] = json_encode($pictures); + } +} + +public function getPicturesAttribute($pictures) +{ + return json_decode($pictures, true); +} +``` +当然你也可以指定其它任何格式. + +## 地图控件 + +地图组件引用了网络资源,默认关闭,如果要开启这个组件参考[form组件管理](/zh/model-form-field-management.md) + +地图控件,用来选择经纬度,`$latitude`, `$longitude`为经纬度字段,`Laravel`的`locale`设置为`zh_CN`的时候使用腾讯地图,否则使用Google地图: +```php +$form->map($latitude, $longitude, $label); +``` + +## 滑动选择控件 +可以用来数字类型字段的选择,比如年龄: +```php +$form->slider($column[, $label])->options(['max' => 100, 'min' => 1, 'step' => 1, 'postfix' => 'years old']); +``` +更多options请参考:https://github.com/IonDen/ion.rangeSlider#settings + +## 富文本编辑框 + +编辑器组件引用了网络资源,默认关闭,如果要开启这个组件参考[form组件管理](/zh/model-form-field-management.md). + +```php +$form->editor($column[, $label]); +``` + +## 隐藏域 +```php +$form->hidden($column); +``` + +## 开关选择 +`on`和`off`对用开关的两个值`1`和`0`: +```php +$states = [ + 'on' => ['value' => 1, 'text' => '打开', 'color' => 'success'], + 'off' => ['value' => 0, 'text' => '关闭', 'color' => 'danger'], +]; + +$form->switch($column[, $label])->states($states); +``` + +## 显示字段 +只显示字段,不做任何操作: +```php +$form->display($column[, $label]); + + +//更复杂的显示 +$form->display($column[, $label])->with(function ($value) { + return ""; +}); +``` + +## 分割线 +```php +$form->divide(); +``` + +## Html +插入html内容,参数可以是实现了`Htmlable`、`Renderable`或者实现了`__toString()`方法的类 +```php +$form->html('你的html内容', $label = ''); +``` + +## 标签 +插入逗号(,)隔开的字符串`tags` +```php +$form->tags('keywords'); +``` + +`tags`同样支持`ManyToMany`的关系,示例如下: + +```php +$form->tags('tags', '文章标签') + ->pluck('name', 'id') // name 为需要显示的 Tag 模型的字段,id 为主键 + ->options(Tag::all());// 下拉框选项 +``` + +注意:处理`ManyToMany`关系时必须调用`pluck`方法,指定显示的字段名和主键。 +此外 `options` 方法传入一个`Collection`对象时,`options`会自动调用该对象的`pluck`方法转为`['主键名' => '显示字段名']` 数组,作为下拉框选项。或者可以直接使用`['主键名' => '显示字段名']`这样的数组作为参数。 + +`tags`还支持`saving`方法用于处理提交的数据,示例如下: + +```php +$form->tags('tags', '文章标签') + ->pluck('name', 'id') + ->options(Tag::all()) + ->saving(function ($value) { + return $value; + }); +``` + +`saving` 方法接收一个「参数为 tags 的提交值,返回值为修改后的 tags 提交值」的闭包,可以用于实现自动创建新 tag 或其它功能。 + +## 图标 +选择`font-awesome`图标 +```php +$form->icon('icon'); +``` + +## 一对多 + +一对多内嵌表格,用于处理一对多的关系,下面是个简单的例子: + +有两张表是一对多关系: + +```sql +CREATE TABLE `demo_painters` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `bio` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +CREATE TABLE `demo_paintings` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `painter_id` int(10) unsigned NOT NULL, + `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `body` text COLLATE utf8_unicode_ci NOT NULL, + `completed_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`id`), + KEY painter_id (`painter_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +``` + +表的模型为: +```php +hasMany(Painting::class, 'painter_id'); + } +} + +belongsTo(Painter::class, 'painter_id'); + } +} +``` + +构建表单代码如下: +```php +$form->display('id', 'ID'); + +$form->text('username')->rules('required'); +$form->textarea('bio')->rules('required'); + +$form->hasMany('paintings', function (Form\NestedForm $form) { + $form->text('title'); + $form->image('body'); + $form->datetime('completed_at'); +}); + +$form->display('created_at', 'Created At'); +$form->display('updated_at', 'Updated At'); + +// 也可以设置label + +$form->hasMany('paintings', '画作', function (Form\NestedForm $form) { + +}); +``` + +## 内嵌 + +用于处理`mysql`的`JSON`类型字段数据或者`mongodb`的`object`类型数据,也可以将多个field的数据值以`JSON`字符串的形式存储在`mysql`的字符串类型字段中 + +比如`orders`表中的`JSON`或字符串类型的`extra`字段,用来存储多个field的数据,先定义model: +```php +class Order extends Model +{ + protected $casts = [ + 'extra' => 'json', + ]; +} +``` +然后在form中使用: +```php +$form->embeds('extra', function ($form) { + + $form->text('extra1')->rules('required'); + $form->email('extra2')->rules('required'); + $form->mobile('extra3'); + $form->datetime('extra4'); + + $form->dateRange('extra5', 'extra6', '范围')->rules('required'); + +}); + +// 自定义标题 +$form->embeds('extra', '附加信息', function ($form) { + ... +}); +``` + +回调函数里面构建表单元素的方法调用和外面是一样的。 diff --git a/docs/zh/model-form-upload.md b/docs/zh/model-form-upload.md new file mode 100644 index 0000000..57d6ebc --- /dev/null +++ b/docs/zh/model-form-upload.md @@ -0,0 +1,113 @@ +# 文件/图片上传 + +[model-form](/zh/model-form.md)通过以下的调用来生成form元素。 + +```php +$form->file('file_column'); +$form->image('image_column'); +``` + +## 修改存储路径或文件名 + +```php + +// 修改上传目录 +$form->image('picture')->move('public/upload/image1/'); + +// 使用随机生成文件名 (md5(uniqid()).extension) +$form->image('picture')->uniqueName(); + +// 自定义文件名 +$form->image('picture')->name(function ($file) { + return 'test.'.$file->guessExtension(); +}); + +``` + +[model-form](/zh/model-form.md)支持本地和云存储的文件上传 + +## 本地上传 + +先添加存储配置,`config/filesystems.php` 添加一项`disk`: + +```php + +'disks' => [ + ... , + + 'admin' => [ + 'driver' => 'local', + 'root' => public_path('uploads'), + 'visibility' => 'public', + 'url' => env('APP_URL').'/uploads', + ], +], + +``` + +设置上传的路径为`public/uploads`(public_path('uploads'))。 + +然后选择上传的`disk`,打开`config/admin.php`找到: + +```php + +'upload' => [ + + 'disk' => 'admin', + + 'directory' => [ + 'image' => 'images', + 'file' => 'files', + ] +], + +``` + +将`disk`设置为上面添加的`admin`,`directory.image`和`directory.file`分别为用`$form->image($column)`和`$form->file($column)`上传的图片和文件的上传目录。 + + +## 云盘上传 + +如果需要上传到云存储,需要安装对应`laravel storage`的适配器,拿七牛云存储举例 + +首先安装 [zgldh/qiniu-laravel-storage](https://github.com/zgldh/qiniu-laravel-storage) + +同样配置好disk,在`config/filesystems.php` 添加一项: + +```php +'disks' => [ + ... , + 'qiniu' => [ + 'driver' => 'qiniu', + 'domains' => [ + 'default' => 'xxxxx.com1.z0.glb.clouddn.com', //你的七牛域名 + 'https' => 'dn-yourdomain.qbox.me', //你的HTTPS域名 + 'custom' => 'static.abc.com', //你的自定义域名 + ], + 'access_key'=> '', //AccessKey + 'secret_key'=> '', //SecretKey + 'bucket' => '', //Bucket名字 + 'notify_url'=> '', //持久化处理回调地址 + 'url' => 'http://of8kfibjo.bkt.clouddn.com/', // 填写文件访问根url + ], +], + +``` + +然后修改`laravel-admin`的上传配置,打开`config/admin.php`找到: + +```php + +'upload' => [ + + 'disk' => 'qiniu', + + 'directory' => [ + 'image' => 'image', + 'file' => 'file', + ], +], + +``` + +`disk`选择上面配置的`qiniu`。 diff --git a/docs/zh/model-form-validation.md b/docs/zh/model-form-validation.md new file mode 100644 index 0000000..4bae24f --- /dev/null +++ b/docs/zh/model-form-validation.md @@ -0,0 +1,36 @@ +表单验证 +======== + +`model-form`使用laravel的验证规则来验证表单提交的数据: + +```php +$form->text('title')->rules('required|min:3'); + +// 复杂的验证规则可以在回调里面实现 +$form->text('title')->rules(function ($form) { + + // 如果不是编辑状态,则添加字段唯一验证 + if (!$id = $form->model()->id) { + return 'unique:users,email_address'; + } + +}); + +``` + +也可以给验证规则自定义错误提示消息: + +```php +$form->text('code')->rules('required|regex:/^\d+$/|min:10', [ + 'regex' => 'code必须全部为数字', + 'min' => 'code不能少于10个字符', +]); +``` + +如果要允许字段为空,首先要在数据库的表里面对该字段设置为`NULL`,然后 + +```php +$form->text('title')->rules('nullable'); +``` + +更多规则请参考[Validation](https://laravel.com/docs/5.5/validation). \ No newline at end of file diff --git a/docs/zh/model-form.md b/docs/zh/model-form.md new file mode 100644 index 0000000..7d65655 --- /dev/null +++ b/docs/zh/model-form.md @@ -0,0 +1,180 @@ +# 基于数据模型的表单 + +`Encore\Admin\Form`类用于生成基于数据模型的表单,先来个例子,数据库中有`movies`表 + +```sql +CREATE TABLE `movies` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `director` int(10) unsigned NOT NULL, + `describe` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `rate` tinyint unsigned NOT NULL, + `released` enum(0, 1), + `release_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +``` + +对应的数据模型为`App\Models\Movie`,下面的代码可以生成`movies`的数据表单: + +```php + +use App\Models\Movie; +use Encore\Admin\Form; +use Encore\Admin\Facades\Admin; + +$grid = Admin::form(Movie::class, function(Form $form){ + + // 显示记录id + $form->display('id', 'ID'); + + // 添加text类型的input框 + $form->text('title', '电影标题'); + + $directors = [ + 1 => 'John', + 2 => 'Smith', + 3 => 'Kate', + ]; + + $form->select('director', '导演')->options($directors); + + // 添加describe的textarea输入框 + $form->textarea('describe', '简介'); + + // 数字输入框 + $form->number('rate', '打分'); + + // 添加开关操作 + $form->switch('released', '发布?'); + + // 添加日期时间选择框 + $form->dateTime('release_at', '发布时间'); + + // 两个时间显示 + $form->display('created_at', '创建时间'); + $form->display('updated_at', '修改时间'); +}); + +``` + +## 自定义工具 + +表单右上角默认有返回和跳转列表两个按钮工具, 可以使用下面的方式修改它: + +```php +$form->tools(function (Form\Tools $tools) { + + // 去掉返回按钮 + $tools->disableBackButton(); + + // 去掉跳转列表按钮 + $tools->disableListButton(); + + // 添加一个按钮, 参数可以是字符串, 或者实现了Renderable或Htmlable接口的对象实例 + $tools->add('  delete'); +}); +``` + +## 其它方法 + +去掉提交按钮: + +```php +$form->disableSubmit(); +``` + +去掉重置按钮: +```php +$form->disableReset(); +``` + +忽略掉不需要保存的字段 + +```php +$form->ignore(['column1', 'column2', 'column3']); +``` + +设置宽度 + +```php +$form->setWidth(10, 2); +``` + +设置表单提交的action + +```php +$form->setAction('admin/users'); +``` + +## 关联模型 + + +### 一对一 +`users`表和`profiles`表通过`profiles.user_id`字段生成一对一关联 + +```sql + +CREATE TABLE `users` ( +`id` int(10) unsigned NOT NULL AUTO_INCREMENT, +`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`email` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', +`updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', +PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +CREATE TABLE `profiles` ( +`id` int(10) unsigned NOT NULL AUTO_INCREMENT, +`user_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`age` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`gender` varchar(255) COLLATE utf8_unicode_ci NOT NULL, +`created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', +`updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', +PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +``` + +对应的数据模分别为: + +```php + +class User extends Model +{ + public function profile() + { + return $this->hasOne(Profile::class); + } +} + +class Profile extends Model +{ + public function user() + { + return $this->belongsTo(User::class); + } +} + +``` + +通过下面的代码可以关联在一个form里面: + +```php +Admin::form(User::class, function (Form $form) { + + $form->display('id'); + + $form->text('name'); + $form->text('email'); + + $form->text('profile.age'); + $form->text('profile.gender'); + + $form->datetime('created_at'); + $form->datetime('updated_at'); +}); + +``` diff --git a/docs/zh/model-grid-actions.md b/docs/zh/model-grid-actions.md new file mode 100644 index 0000000..5f1b041 --- /dev/null +++ b/docs/zh/model-grid-actions.md @@ -0,0 +1,91 @@ +# 模型表格行操作 + +`model-grid`默认有三个行操作`编辑`、`删除`和`详情`,可以通过下面的方式关闭它们: + +```php + $grid->actions(function ($actions) { + $actions->disableDelete(); + $actions->disableEdit(); + $actions->disableView(); +}); +``` +可以通过传入的`$actions`参数来获取当前行的数据: +```php + $grid->actions(function ($actions) { + + // 当前行的数据数组 + $actions->row; + + // 获取当前行主键值 + $actions->getKey(); +}); +``` + +如果有自定义的操作按钮,可以通过下面的方式添加: + +```php +$grid->actions(function ($actions) { + + // append一个操作 + $actions->append(''); + + // prepend一个操作 + $actions->prepend(''); +} +``` + +如果有比较复杂的操作,可以参考下面的方式: + + +先定义操作类 +```php +id = $id; + } + + protected function script() + { + return << + {!! Admin::headerJs() !!} + + + + + +
    + + @include('admin::partials.header') + + @include('admin::partials.sidebar') + +
    +
    + @yield('content') +
    + {!! Admin::script() !!} +
    + + @include('admin::partials.footer') + +
    + + + + +{!! Admin::js() !!} + + + diff --git a/resources/views/login.blade.php b/resources/views/login.blade.php new file mode 100644 index 0000000..25cbe52 --- /dev/null +++ b/resources/views/login.blade.php @@ -0,0 +1,98 @@ + + + + + + {{config('admin.title')}} | {{ trans('admin.login') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/pagination.blade.php b/resources/views/pagination.blade.php new file mode 100644 index 0000000..7ee6c26 --- /dev/null +++ b/resources/views/pagination.blade.php @@ -0,0 +1,34 @@ +
      + + @if ($paginator->onFirstPage()) +
    • «
    • + @else +
    • + @endif + + + @foreach ($elements as $element) + + @if (is_string($element)) +
    • {{ $element }}
    • + @endif + + + @if (is_array($element)) + @foreach ($element as $page => $url) + @if ($page == $paginator->currentPage()) +
    • {{ $page }}
    • + @else +
    • {{ $page }}
    • + @endif + @endforeach + @endif + @endforeach + + + @if ($paginator->hasMorePages()) +
    • + @else +
    • »
    • + @endif +
    diff --git a/resources/views/partials/alerts.blade.php b/resources/views/partials/alerts.blade.php new file mode 100644 index 0000000..7124d5e --- /dev/null +++ b/resources/views/partials/alerts.blade.php @@ -0,0 +1,41 @@ +@if($error = session()->get('error')) +
    + +

    {{ array_get($error->get('title'), 0) }}

    +

    {!! array_get($error->get('message'), 0) !!}

    +
    +@elseif ($errors = session()->get('errors')) + @if ($errors->hasBag('error')) +
    + + + @foreach($errors->getBag("error")->toArray() as $message) +

    {!! array_get($message, 0) !!}

    + @endforeach +
    + @endif +@endif + +@if($success = session()->get('success')) +
    + +

    {{ array_get($success->get('title'), 0) }}

    +

    {!! array_get($success->get('message'), 0) !!}

    +
    +@endif + +@if($info = session()->get('info')) +
    + +

    {{ array_get($info->get('title'), 0) }}

    +

    {!! array_get($info->get('message'), 0) !!}

    +
    +@endif + +@if($warning = session()->get('warning')) +
    + +

    {{ array_get($warning->get('title'), 0) }}

    +

    {!! array_get($warning->get('message'), 0) !!}

    +
    +@endif \ No newline at end of file diff --git a/resources/views/partials/css.blade.php b/resources/views/partials/css.blade.php new file mode 100644 index 0000000..6df8e7f --- /dev/null +++ b/resources/views/partials/css.blade.php @@ -0,0 +1,3 @@ +@foreach($css as $c) + +@endforeach \ No newline at end of file diff --git a/resources/views/partials/exception.blade.php b/resources/views/partials/exception.blade.php new file mode 100644 index 0000000..bb293fd --- /dev/null +++ b/resources/views/partials/exception.blade.php @@ -0,0 +1,12 @@ +@if($errors->hasBag('exception') && env('APP_DEBUG') == true) + getBag('exception');?> +
    + +

    + + {{ class_basename($error->get('type')[0]) }} + In {{ basename($error->get('file')[0]) }} line {{ $error->get('line')[0] }} : +

    +

    {!! $error->get('message')[0] !!}

    +
    +@endif diff --git a/resources/views/partials/footer.blade.php b/resources/views/partials/footer.blade.php new file mode 100644 index 0000000..ce6fbb8 --- /dev/null +++ b/resources/views/partials/footer.blade.php @@ -0,0 +1,18 @@ + +
    + + + + Powered by laravel-admin +
    \ No newline at end of file diff --git a/resources/views/partials/header.blade.php b/resources/views/partials/header.blade.php new file mode 100644 index 0000000..aece4f5 --- /dev/null +++ b/resources/views/partials/header.blade.php @@ -0,0 +1,63 @@ + +
    + + + + + + +
    \ No newline at end of file diff --git a/resources/views/partials/js.blade.php b/resources/views/partials/js.blade.php new file mode 100644 index 0000000..0c104e9 --- /dev/null +++ b/resources/views/partials/js.blade.php @@ -0,0 +1,3 @@ +@foreach($js as $j) + +@endforeach \ No newline at end of file diff --git a/resources/views/partials/menu.blade.php b/resources/views/partials/menu.blade.php new file mode 100644 index 0000000..ceb944b --- /dev/null +++ b/resources/views/partials/menu.blade.php @@ -0,0 +1,35 @@ +@if(Admin::user()->visible($item['roles']) && (empty($item['permission']) ?: Admin::user()->can($item['permission']))) + @if(!isset($item['children'])) +
  • + @if(url()->isValidUrl($item['uri'])) + + @else + + @endif + + @if (Lang::has($titleTranslation = 'admin.menu_titles.' . trim(str_replace(' ', '_', strtolower($item['title']))))) + {{ __($titleTranslation) }} + @else + {{ $item['title'] }} + @endif + +
  • + @else +
  • + + + @if (Lang::has($titleTranslation = 'admin.menu_titles.' . trim(str_replace(' ', '_', strtolower($item['title']))))) + {{ __($titleTranslation) }} + @else + {{ $item['title'] }} + @endif + + +
      + @foreach($item['children'] as $item) + @include('admin::partials.menu', $item) + @endforeach +
    +
  • + @endif +@endif \ No newline at end of file diff --git a/resources/views/partials/script.blade.php b/resources/views/partials/script.blade.php new file mode 100644 index 0000000..08f6ec6 --- /dev/null +++ b/resources/views/partials/script.blade.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php new file mode 100644 index 0000000..3ce3971 --- /dev/null +++ b/resources/views/partials/sidebar.blade.php @@ -0,0 +1,40 @@ + \ No newline at end of file diff --git a/resources/views/partials/toastr.blade.php b/resources/views/partials/toastr.blade.php new file mode 100644 index 0000000..ca028e2 --- /dev/null +++ b/resources/views/partials/toastr.blade.php @@ -0,0 +1,13 @@ +@if(Session::has('toastr')) + @php + $toastr = Session::get('toastr'); + $type = array_get($toastr->get('type'), 0, 'success'); + $message = array_get($toastr->get('message'), 0, ''); + $options = json_encode($toastr->get('options', [])); + @endphp + +@endif \ No newline at end of file diff --git a/resources/views/show.blade.php b/resources/views/show.blade.php new file mode 100644 index 0000000..0856a68 --- /dev/null +++ b/resources/views/show.blade.php @@ -0,0 +1,11 @@ +
    +
    + {!! $panel !!} +
    + +
    + @foreach($relations as $relation) + {!! $relation->render() !!} + @endforeach +
    +
    \ No newline at end of file diff --git a/resources/views/show/field.blade.php b/resources/views/show/field.blade.php new file mode 100644 index 0000000..43dc4fc --- /dev/null +++ b/resources/views/show/field.blade.php @@ -0,0 +1,23 @@ +
    + +
    + @if($wrapped) +
    + +
    + @if($escape) + {{ $content }}  + @else + {!! $content !!}  + @endif +
    +
    + @else + @if($escape) + {{ $content }} + @else + {!! $content !!} + @endif + @endif +
    +
    \ No newline at end of file diff --git a/resources/views/show/panel.blade.php b/resources/views/show/panel.blade.php new file mode 100644 index 0000000..f6ed686 --- /dev/null +++ b/resources/views/show/panel.blade.php @@ -0,0 +1,25 @@ +
    +
    +

    {{ $title }}

    + +
    + {!! $tools !!} +
    +
    + + +
    + +
    + +
    + + @foreach($fields as $field) + {!! $field->render() !!} + @endforeach +
    + +
    + +
    +
    \ No newline at end of file diff --git a/resources/views/tree.blade.php b/resources/views/tree.blade.php new file mode 100644 index 0000000..99d444e --- /dev/null +++ b/resources/views/tree.blade.php @@ -0,0 +1,46 @@ +
    + +
    + + + + @if($useSave) + + @endif + + @if($useRefresh) + + @endif + +
    + {!! $tools !!} +
    + + @if($useCreate) + + @endif + +
    + +
    +
    +
      + @each($branchView, $items, 'branch') +
    +
    +
    + +
    diff --git a/resources/views/tree/branch.blade.php b/resources/views/tree/branch.blade.php new file mode 100644 index 0000000..c6e12a4 --- /dev/null +++ b/resources/views/tree/branch.blade.php @@ -0,0 +1,16 @@ +
  • +
    + {!! $branchCallback($branch) !!} + + + + +
    + @if(isset($branch['children'])) +
      + @foreach($branch['children'] as $branch) + @include($branchView, $branch) + @endforeach +
    + @endif +
  • \ No newline at end of file diff --git a/resources/views/widgets/alert.blade.php b/resources/views/widgets/alert.blade.php new file mode 100644 index 0000000..cabd331 --- /dev/null +++ b/resources/views/widgets/alert.blade.php @@ -0,0 +1,5 @@ +
    + +

    {{ $title }}

    + {!! $content !!} +
    \ No newline at end of file diff --git a/resources/views/widgets/box.blade.php b/resources/views/widgets/box.blade.php new file mode 100644 index 0000000..81890bb --- /dev/null +++ b/resources/views/widgets/box.blade.php @@ -0,0 +1,13 @@ +
    +
    +

    {{ $title }}

    +
    + @foreach($tools as $tool) + {!! $tool !!} + @endforeach +
    +
    +
    + {!! $content !!} +
    +
    \ No newline at end of file diff --git a/resources/views/widgets/callout.blade.php b/resources/views/widgets/callout.blade.php new file mode 100644 index 0000000..1d83bbe --- /dev/null +++ b/resources/views/widgets/callout.blade.php @@ -0,0 +1,6 @@ +
    + @if(isset($title)) +

    {{ $title }}

    + @endif + {!! $content !!} +
    \ No newline at end of file diff --git a/resources/views/widgets/carousel.blade.php b/resources/views/widgets/carousel.blade.php new file mode 100644 index 0000000..d134464 --- /dev/null +++ b/resources/views/widgets/carousel.blade.php @@ -0,0 +1,27 @@ +
    + + + + + + + + +
    diff --git a/resources/views/widgets/collapse.blade.php b/resources/views/widgets/collapse.blade.php new file mode 100644 index 0000000..baad5d5 --- /dev/null +++ b/resources/views/widgets/collapse.blade.php @@ -0,0 +1,19 @@ +
    + @foreach($items as $key => $item) +
    + +
    +
    + {!! $item['content'] !!} +
    +
    +
    + @endforeach + +
    diff --git a/resources/views/widgets/form.blade.php b/resources/views/widgets/form.blade.php new file mode 100644 index 0000000..5a34d22 --- /dev/null +++ b/resources/views/widgets/form.blade.php @@ -0,0 +1,34 @@ +
    +
    + + @foreach($fields as $field) + {!! $field->render() !!} + @endforeach + +
    + + @if ($method != 'GET') + + @endif + + + @if(count($buttons) > 0) + + @endif +
    diff --git a/resources/views/widgets/info-box.blade.php b/resources/views/widgets/info-box.blade.php new file mode 100644 index 0000000..850bfc0 --- /dev/null +++ b/resources/views/widgets/info-box.blade.php @@ -0,0 +1,14 @@ +
    +
    +

    {{ $info }}

    + +

    {{ $name }}

    +
    +
    + +
    + + {{ trans('admin.more') }}  + + +
    \ No newline at end of file diff --git a/resources/views/widgets/tab.blade.php b/resources/views/widgets/tab.blade.php new file mode 100644 index 0000000..baa7f08 --- /dev/null +++ b/resources/views/widgets/tab.blade.php @@ -0,0 +1,34 @@ +
    + +
    + @foreach($tabs as $id => $tab) +
    + {!! array_get($tab, 'content') !!} +
    + @endforeach + +
    +
    \ No newline at end of file diff --git a/resources/views/widgets/table.blade.php b/resources/views/widgets/table.blade.php new file mode 100644 index 0000000..459bf2c --- /dev/null +++ b/resources/views/widgets/table.blade.php @@ -0,0 +1,18 @@ + + + + @foreach($headers as $header) + + @endforeach + + + + @foreach($rows as $row) + + @foreach($row as $item) + + @endforeach + + @endforeach + +
    {{ $header }}
    {!! $item !!}
    \ No newline at end of file diff --git a/src/Admin.php b/src/Admin.php new file mode 100644 index 0000000..0a88625 --- /dev/null +++ b/src/Admin.php @@ -0,0 +1,295 @@ +version %s', self::VERSION); + } + + /** + * @param $model + * @param Closure $callable + * + * @return \Encore\Admin\Grid + * + * @deprecated since v1.6.1 + */ + public function grid($model, Closure $callable) + { + return new Grid($this->getModel($model), $callable); + } + + /** + * @param $model + * @param Closure $callable + * + * @return \Encore\Admin\Form + * + * @deprecated since v1.6.1 + */ + public function form($model, Closure $callable) + { + return new Form($this->getModel($model), $callable); + } + + /** + * Build a tree. + * + * @param $model + * + * @return \Encore\Admin\Tree + */ + public function tree($model, Closure $callable = null) + { + return new Tree($this->getModel($model), $callable); + } + + /** + * Build show page. + * + * @param $model + * @param mixed $callable + * + * @return Show + * + * @deprecated since v1.6.1 + */ + public function show($model, $callable = null) + { + return new Show($this->getModel($model), $callable); + } + + /** + * @param Closure $callable + * + * @return \Encore\Admin\Layout\Content + * + * @deprecated since v1.6.1 + */ + public function content(Closure $callable = null) + { + return new Content($callable); + } + + /** + * @param $model + * + * @return mixed + */ + public function getModel($model) + { + if ($model instanceof Model) { + return $model; + } + + if (is_string($model) && class_exists($model)) { + return $this->getModel(new $model()); + } + + throw new InvalidArgumentException("$model is not a valid model"); + } + + /** + * Left sider-bar menu. + * + * @return array + */ + public function menu() + { + $menuModel = config('admin.database.menu_model'); + + return (new $menuModel())->toTree(); + } + + /** + * Set admin title. + * + * @return void + */ + public static function setTitle($title) + { + self::$metaTitle = $title; + } + + /** + * Get admin title. + * + * @return Config + */ + public function title() + { + return self::$metaTitle ? self::$metaTitle : config('admin.title'); + } + + /** + * Get current login user. + * + * @return mixed + */ + public function user() + { + return Auth::guard('admin')->user(); + } + + /** + * Set navbar. + * + * @param Closure|null $builder + * + * @return Navbar + */ + public function navbar(Closure $builder = null) + { + if (is_null($builder)) { + return $this->getNavbar(); + } + + call_user_func($builder, $this->getNavbar()); + } + + /** + * Get navbar object. + * + * @return \Encore\Admin\Widgets\Navbar + */ + public function getNavbar() + { + if (is_null($this->navbar)) { + $this->navbar = new Navbar(); + } + + return $this->navbar; + } + + /** + * Register the auth routes. + * + * @return void + */ + public function registerAuthRoutes() + { + $attributes = [ + 'prefix' => config('admin.route.prefix'), + 'middleware' => config('admin.route.middleware'), + ]; + + app('router')->group($attributes, function ($router) { + + /* @var \Illuminate\Routing\Router $router */ + $router->namespace('Encore\Admin\Controllers')->group(function ($router) { + + /* @var \Illuminate\Routing\Router $router */ + $router->resource('auth/users', 'UserController'); + $router->resource('auth/roles', 'RoleController'); + $router->resource('auth/permissions', 'PermissionController'); + $router->resource('auth/menu', 'MenuController', ['except' => ['create']]); + $router->resource('auth/logs', 'LogController', ['only' => ['index', 'destroy']]); + }); + + $authController = config('admin.auth.controller', AuthController::class); + + /* @var \Illuminate\Routing\Router $router */ + $router->get('auth/login', $authController.'@getLogin'); + $router->post('auth/login', $authController.'@postLogin'); + $router->get('auth/logout', $authController.'@getLogout'); + $router->get('auth/setting', $authController.'@getSetting'); + $router->put('auth/setting', $authController.'@putSetting'); + }); + } + + /** + * Extend a extension. + * + * @param string $name + * @param string $class + * + * @return void + */ + public static function extend($name, $class) + { + static::$extensions[$name] = $class; + } + + /** + * @param callable $callback + */ + public static function booting(callable $callback) + { + static::$booting[] = $callback; + } + + /** + * @param callable $callback + */ + public static function booted(callable $callback) + { + static::$booted[] = $callback; + } + + /* + * Disable Pjax for current Request + * + * @return void + */ + public function disablePjax() + { + if (request()->pjax()) { + request()->headers->set('X-PJAX', false); + } + } +} diff --git a/src/AdminServiceProvider.php b/src/AdminServiceProvider.php new file mode 100644 index 0000000..aadcec5 --- /dev/null +++ b/src/AdminServiceProvider.php @@ -0,0 +1,129 @@ + Middleware\Authenticate::class, + 'admin.pjax' => Middleware\Pjax::class, + 'admin.log' => Middleware\LogOperation::class, + 'admin.permission' => Middleware\Permission::class, + 'admin.bootstrap' => Middleware\Bootstrap::class, + ]; + + /** + * The application's route middleware groups. + * + * @var array + */ + protected $middlewareGroups = [ + 'admin' => [ + 'admin.auth', + 'admin.pjax', + 'admin.log', + 'admin.bootstrap', + 'admin.permission', + ], + ]; + + /** + * Boot the service provider. + * + * @return void + */ + public function boot() + { + $this->loadViewsFrom(__DIR__.'/../resources/views', 'admin'); + + if (config('admin.https') || config('admin.secure')) { + \URL::forceScheme('https'); + $this->app['request']->server->set('HTTPS', true); + } + + if (file_exists($routes = admin_path('routes.php'))) { + $this->loadRoutesFrom($routes); + } + + if ($this->app->runningInConsole()) { + $this->publishes([__DIR__.'/../config' => config_path()], 'laravel-admin-config'); + $this->publishes([__DIR__.'/../resources/lang' => resource_path('lang')], 'laravel-admin-lang'); +// $this->publishes([__DIR__.'/../resources/views' => resource_path('views/vendor/admin')], 'laravel-admin-views'); + $this->publishes([__DIR__.'/../database/migrations' => database_path('migrations')], 'laravel-admin-migrations'); + $this->publishes([__DIR__.'/../resources/assets' => public_path('vendor/laravel-admin')], 'laravel-admin-assets'); + } + + //remove default feature of double encoding enable in laravel 5.6 or later. + $bladeReflectionClass = new \ReflectionClass('\Illuminate\View\Compilers\BladeCompiler'); + if ($bladeReflectionClass->hasMethod('withoutDoubleEncoding')) { + Blade::withoutDoubleEncoding(); + } + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $this->loadAdminAuthConfig(); + + $this->registerRouteMiddleware(); + + $this->commands($this->commands); + } + + /** + * Setup auth configuration. + * + * @return void + */ + protected function loadAdminAuthConfig() + { + config(array_dot(config('admin.auth', []), 'auth.')); + } + + /** + * Register the route middleware. + * + * @return void + */ + protected function registerRouteMiddleware() + { + // register route middleware. + foreach ($this->routeMiddleware as $key => $middleware) { + app('router')->aliasMiddleware($key, $middleware); + } + + // register middleware group. + foreach ($this->middlewareGroups as $key => $middleware) { + app('router')->middlewareGroup($key, $middleware); + } + } +} diff --git a/src/Auth/Database/AdminTablesSeeder.php b/src/Auth/Database/AdminTablesSeeder.php new file mode 100644 index 0000000..f18823a --- /dev/null +++ b/src/Auth/Database/AdminTablesSeeder.php @@ -0,0 +1,128 @@ + 'admin', + 'password' => bcrypt('admin'), + 'name' => 'Administrator', + ]); + + // create a role. + Role::truncate(); + Role::create([ + 'name' => 'Administrator', + 'slug' => 'administrator', + ]); + + // add role to user. + Administrator::first()->roles()->save(Role::first()); + + //create a permission + Permission::truncate(); + Permission::insert([ + [ + 'name' => 'All permission', + 'slug' => '*', + 'http_method' => '', + 'http_path' => '*', + ], + [ + 'name' => 'Dashboard', + 'slug' => 'dashboard', + 'http_method' => 'GET', + 'http_path' => '/', + ], + [ + 'name' => 'Login', + 'slug' => 'auth.login', + 'http_method' => '', + 'http_path' => "/auth/login\r\n/auth/logout", + ], + [ + 'name' => 'User setting', + 'slug' => 'auth.setting', + 'http_method' => 'GET,PUT', + 'http_path' => '/auth/setting', + ], + [ + 'name' => 'Auth management', + 'slug' => 'auth.management', + 'http_method' => '', + 'http_path' => "/auth/roles\r\n/auth/permissions\r\n/auth/menu\r\n/auth/logs", + ], + ]); + + Role::first()->permissions()->save(Permission::first()); + + // add default menus. + Menu::truncate(); + Menu::insert([ + [ + 'parent_id' => 0, + 'order' => 1, + 'title' => 'Index', + 'icon' => 'fa-bar-chart', + 'uri' => '/', + ], + [ + 'parent_id' => 0, + 'order' => 2, + 'title' => 'Admin', + 'icon' => 'fa-tasks', + 'uri' => '', + ], + [ + 'parent_id' => 2, + 'order' => 3, + 'title' => 'Users', + 'icon' => 'fa-users', + 'uri' => 'auth/users', + ], + [ + 'parent_id' => 2, + 'order' => 4, + 'title' => 'Roles', + 'icon' => 'fa-user', + 'uri' => 'auth/roles', + ], + [ + 'parent_id' => 2, + 'order' => 5, + 'title' => 'Permission', + 'icon' => 'fa-ban', + 'uri' => 'auth/permissions', + ], + [ + 'parent_id' => 2, + 'order' => 6, + 'title' => 'Menu', + 'icon' => 'fa-bars', + 'uri' => 'auth/menu', + ], + [ + 'parent_id' => 2, + 'order' => 7, + 'title' => 'Operation log', + 'icon' => 'fa-history', + 'uri' => 'auth/logs', + ], + ]); + + // add role to menu. + Menu::find(2)->roles()->save(Role::first()); + } +} diff --git a/src/Auth/Database/Administrator.php b/src/Auth/Database/Administrator.php new file mode 100644 index 0000000..75eea9b --- /dev/null +++ b/src/Auth/Database/Administrator.php @@ -0,0 +1,84 @@ +setConnection($connection); + + $this->setTable(config('admin.database.users_table')); + + parent::__construct($attributes); + } + + /** + * Get avatar attribute. + * + * @param string $avatar + * + * @return string + */ + public function getAvatarAttribute($avatar) + { + $disk = config('admin.upload.disk'); + + if ($avatar && array_key_exists($disk, config('filesystems.disks'))) { + return Storage::disk(config('admin.upload.disk'))->url($avatar); + } + + return admin_asset('/vendor/laravel-admin/AdminLTE/dist/img/user2-160x160.jpg'); + } + + /** + * A user has and belongs to many roles. + * + * @return BelongsToMany + */ + public function roles() : BelongsToMany + { + $pivotTable = config('admin.database.role_users_table'); + + $relatedModel = config('admin.database.roles_model'); + + return $this->belongsToMany($relatedModel, $pivotTable, 'user_id', 'role_id'); + } + + /** + * A User has and belongs to many permissions. + * + * @return BelongsToMany + */ + public function permissions() : BelongsToMany + { + $pivotTable = config('admin.database.user_permissions_table'); + + $relatedModel = config('admin.database.permissions_model'); + + return $this->belongsToMany($relatedModel, $pivotTable, 'user_id', 'permission_id'); + } +} diff --git a/src/Auth/Database/HasPermissions.php b/src/Auth/Database/HasPermissions.php new file mode 100644 index 0000000..a76a059 --- /dev/null +++ b/src/Auth/Database/HasPermissions.php @@ -0,0 +1,119 @@ +roles()->with('permissions')->get()->pluck('permissions')->flatten()->merge($this->permissions); + } + + /** + * Check if user has permission. + * + * @param $ability + * @param array $arguments + * + * @return bool + */ + public function can($ability, $arguments = []) : bool + { + if ($this->isAdministrator()) { + return true; + } + + if ($this->permissions->pluck('slug')->contains($ability)) { + return true; + } + + return $this->roles->pluck('permissions')->flatten()->pluck('slug')->contains($ability); + } + + /** + * Check if user has no permission. + * + * @param $permission + * + * @return bool + */ + public function cannot(string $permission) : bool + { + return !$this->can($permission); + } + + /** + * Check if user is administrator. + * + * @return mixed + */ + public function isAdministrator() : bool + { + return $this->isRole('administrator'); + } + + /** + * Check if user is $role. + * + * @param string $role + * + * @return mixed + */ + public function isRole(string $role) : bool + { + return $this->roles->pluck('slug')->contains($role); + } + + /** + * Check if user in $roles. + * + * @param array $roles + * + * @return mixed + */ + public function inRoles(array $roles = []) : bool + { + return $this->roles->pluck('slug')->intersect($roles)->isNotEmpty(); + } + + /** + * If visible for roles. + * + * @param $roles + * + * @return bool + */ + public function visible(array $roles = []) : bool + { + if (empty($roles)) { + return true; + } + + $roles = array_column($roles, 'slug'); + + return $this->inRoles($roles) || $this->isAdministrator(); + } + + /** + * Detach models from the relationship. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + + static::deleting(function ($model) { + $model->roles()->detach(); + + $model->permissions()->detach(); + }); + } +} diff --git a/src/Auth/Database/Menu.php b/src/Auth/Database/Menu.php new file mode 100644 index 0000000..83efd60 --- /dev/null +++ b/src/Auth/Database/Menu.php @@ -0,0 +1,97 @@ +setConnection($connection); + + $this->setTable(config('admin.database.menu_table')); + + parent::__construct($attributes); + } + + /** + * A Menu belongs to many roles. + * + * @return BelongsToMany + */ + public function roles() : BelongsToMany + { + $pivotTable = config('admin.database.role_menu_table'); + + $relatedModel = config('admin.database.roles_model'); + + return $this->belongsToMany($relatedModel, $pivotTable, 'menu_id', 'role_id'); + } + + /** + * @return array + */ + public function allNodes() : array + { + $connection = config('admin.database.connection') ?: config('database.default'); + $orderColumn = DB::connection($connection)->getQueryGrammar()->wrap($this->orderColumn); + + $byOrder = $orderColumn.' = 0,'.$orderColumn; + + return static::with('roles')->orderByRaw($byOrder)->get()->toArray(); + } + + /** + * determine if enable menu bind permission. + * + * @return bool + */ + public function withPermission() + { + return (bool) config('admin.menu_bind_permission'); + } + + /** + * Detach models from the relationship. + * + * @return void + */ + protected static function boot() + { + static::treeBoot(); + + static::deleting(function ($model) { + $model->roles()->detach(); + }); + } +} diff --git a/src/Auth/Database/OperationLog.php b/src/Auth/Database/OperationLog.php new file mode 100644 index 0000000..a1437c4 --- /dev/null +++ b/src/Auth/Database/OperationLog.php @@ -0,0 +1,49 @@ + 'green', + 'POST' => 'yellow', + 'PUT' => 'blue', + 'DELETE' => 'red', + ]; + + public static $methods = [ + 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH', + 'LINK', 'UNLINK', 'COPY', 'HEAD', 'PURGE', + ]; + + /** + * Create a new Eloquent model instance. + * + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + $connection = config('admin.database.connection') ?: config('database.default'); + + $this->setConnection($connection); + + $this->setTable(config('admin.database.operation_log_table')); + + parent::__construct($attributes); + } + + /** + * Log belongs to users. + * + * @return BelongsTo + */ + public function user() : BelongsTo + { + return $this->belongsTo(Administrator::class); + } +} diff --git a/src/Auth/Database/Permission.php b/src/Auth/Database/Permission.php new file mode 100644 index 0000000..0b4429e --- /dev/null +++ b/src/Auth/Database/Permission.php @@ -0,0 +1,147 @@ +setConnection($connection); + + $this->setTable(config('admin.database.permissions_table')); + + parent::__construct($attributes); + } + + /** + * Permission belongs to many roles. + * + * @return BelongsToMany + */ + public function roles() : BelongsToMany + { + $pivotTable = config('admin.database.role_permissions_table'); + + $relatedModel = config('admin.database.roles_model'); + + return $this->belongsToMany($relatedModel, $pivotTable, 'permission_id', 'role_id'); + } + + /** + * If request should pass through the current permission. + * + * @param Request $request + * + * @return bool + */ + public function shouldPassThrough(Request $request) : bool + { + if (empty($this->http_method) && empty($this->http_path)) { + return true; + } + + $method = $this->http_method; + + $matches = array_map(function ($path) use ($method) { + $path = trim(config('admin.route.prefix'), '/').$path; + + if (Str::contains($path, ':')) { + list($method, $path) = explode(':', $path); + $method = explode(',', $method); + } + + return compact('method', 'path'); + }, explode("\r\n", $this->http_path)); + + foreach ($matches as $match) { + if ($this->matchRequest($match, $request)) { + return true; + } + } + + return false; + } + + /** + * If a request match the specific HTTP method and path. + * + * @param array $match + * @param Request $request + * + * @return bool + */ + protected function matchRequest(array $match, Request $request) : bool + { + if (!$request->is(trim($match['path'], '/'))) { + return false; + } + + $method = collect($match['method'])->filter()->map(function ($method) { + return strtoupper($method); + }); + + return $method->isEmpty() || $method->contains($request->method()); + } + + /** + * @param $method + */ + public function setHttpMethodAttribute($method) + { + if (is_array($method)) { + $this->attributes['http_method'] = implode(',', $method); + } + } + + /** + * @param $method + * + * @return array + */ + public function getHttpMethodAttribute($method) + { + if (is_string($method)) { + return array_filter(explode(',', $method)); + } + + return $method; + } + + /** + * Detach models from the relationship. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + + static::deleting(function ($model) { + $model->roles()->detach(); + }); + } +} diff --git a/src/Auth/Database/Role.php b/src/Auth/Database/Role.php new file mode 100644 index 0000000..0753c04 --- /dev/null +++ b/src/Auth/Database/Role.php @@ -0,0 +1,95 @@ +setConnection($connection); + + $this->setTable(config('admin.database.roles_table')); + + parent::__construct($attributes); + } + + /** + * A role belongs to many users. + * + * @return BelongsToMany + */ + public function administrators() : BelongsToMany + { + $pivotTable = config('admin.database.role_users_table'); + + $relatedModel = config('admin.database.users_model'); + + return $this->belongsToMany($relatedModel, $pivotTable, 'role_id', 'user_id'); + } + + /** + * A role belongs to many permissions. + * + * @return BelongsToMany + */ + public function permissions() : BelongsToMany + { + $pivotTable = config('admin.database.role_permissions_table'); + + $relatedModel = config('admin.database.permissions_model'); + + return $this->belongsToMany($relatedModel, $pivotTable, 'role_id', 'permission_id'); + } + + /** + * Check user has permission. + * + * @param $permission + * + * @return bool + */ + public function can(string $permission) : bool + { + return $this->permissions()->where('slug', $permission)->exists(); + } + + /** + * Check user has no permission. + * + * @param $permission + * + * @return bool + */ + public function cannot(string $permission) : bool + { + return !$this->can($permission); + } + + /** + * Detach models from the relationship. + * + * @return void + */ + protected static function boot() + { + parent::boot(); + + static::deleting(function ($model) { + $model->administrators()->detach(); + + $model->permissions()->detach(); + }); + } +} diff --git a/src/Auth/Permission.php b/src/Auth/Permission.php new file mode 100644 index 0000000..e88e0b5 --- /dev/null +++ b/src/Auth/Permission.php @@ -0,0 +1,106 @@ +each(function ($permission) { + call_user_func([Permission::class, 'check'], $permission); + }); + + return; + } + + if (Auth::guard('admin')->user()->cannot($permission)) { + static::error(); + } + } + + /** + * Roles allowed to access. + * + * @param $roles + * + * @return true + */ + public static function allow($roles) + { + if (static::isAdministrator()) { + return true; + } + + if (!Auth::guard('admin')->user()->inRoles($roles)) { + static::error(); + } + } + + /** + * Don't check permission. + * + * @return bool + */ + public static function free() + { + return true; + } + + /** + * Roles denied to access. + * + * @param $roles + * + * @return true + */ + public static function deny($roles) + { + if (static::isAdministrator()) { + return true; + } + + if (Auth::guard('admin')->user()->inRoles($roles)) { + static::error(); + } + } + + /** + * Send error response page. + */ + public static function error() + { + $response = response(Admin::content()->withError(trans('admin.deny'))); + + if (!request()->pjax() && request()->ajax()) { + abort(403, trans('admin.deny')); + } + + Pjax::respond($response); + } + + /** + * If current user is administrator. + * + * @return mixed + */ + public static function isAdministrator() + { + return Auth::guard('admin')->user()->isRole('administrator'); + } +} diff --git a/src/Console/AdminCommand.php b/src/Console/AdminCommand.php new file mode 100644 index 0000000..51a9459 --- /dev/null +++ b/src/Console/AdminCommand.php @@ -0,0 +1,109 @@ +line(static::$logo); + $this->line(Admin::getLongVersion()); + + $this->comment(''); + $this->comment('Available commands:'); + + $this->listAdminCommands(); + } + + /** + * List all admin commands. + * + * @return void + */ + protected function listAdminCommands() + { + $commands = collect(Artisan::all())->mapWithKeys(function ($command, $key) { + if (Str::startsWith($key, 'admin:')) { + return [$key => $command]; + } + + return []; + })->toArray(); + + $width = $this->getColumnWidth($commands); + + /** @var Command $command */ + foreach ($commands as $command) { + $this->line(sprintf(" %-{$width}s %s", $command->getName(), $command->getDescription())); + } + } + + /** + * @param (Command|string)[] $commands + * + * @return int + */ + private function getColumnWidth(array $commands) + { + $widths = []; + + foreach ($commands as $command) { + $widths[] = static::strlen($command->getName()); + foreach ($command->getAliases() as $alias) { + $widths[] = static::strlen($alias); + } + } + + return $widths ? max($widths) + 2 : 0; + } + + /** + * Returns the length of a string, using mb_strwidth if it is available. + * + * @param string $string The string to check its length + * + * @return int The length of the string + */ + public static function strlen($string) + { + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return strlen($string); + } + + return mb_strwidth($string, $encoding); + } +} diff --git a/src/Console/CreateUserCommand.php b/src/Console/CreateUserCommand.php new file mode 100644 index 0000000..e36405b --- /dev/null +++ b/src/Console/CreateUserCommand.php @@ -0,0 +1,54 @@ +ask('Please enter a username to login'); + + $password = bcrypt($this->secret('Please enter a password to login')); + + $name = $this->ask('Please enter a name to display'); + + $roles = $roleModel::all(); + + /** @var array $selected */ + $selected = $this->choice('Please choose a role for the user', $roles->pluck('name')->toArray(), null, null, true); + + $roles = $roles->filter(function ($role) use ($selected) { + return in_array($role->name, $selected); + }); + + $user = new $userModel(compact('username', 'password', 'name')); + + $user->save(); + + $user->roles()->attach($roles); + + $this->info("User [$name] created successfully."); + } +} diff --git a/src/Console/ExportSeedCommand.php b/src/Console/ExportSeedCommand.php new file mode 100644 index 0000000..4ff4d78 --- /dev/null +++ b/src/Console/ExportSeedCommand.php @@ -0,0 +1,150 @@ +argument('classname'); + $exceptFields = []; + $exportUsers = $this->option('users'); + + $seedFile = $this->laravel->databasePath().'/seeds/'.$name.'.php'; + $contents = $this->getStub('AdminTablesSeeder'); + + $replaces = [ + 'DummyClass' => $name, + + 'ClassMenu' => config('admin.database.menu_model'), + 'ClassPermission' => config('admin.database.permissions_model'), + 'ClassRole' => config('admin.database.roles_model'), + + 'TableRoleMenu' => config('admin.database.role_menu_table'), + 'TableRolePermissions' => config('admin.database.role_permissions_table'), + + 'ArrayMenu' => $this->getTableDataArrayAsString(config('admin.database.menu_table'), $exceptFields), + 'ArrayPermission' => $this->getTableDataArrayAsString(config('admin.database.permissions_table'), $exceptFields), + 'ArrayRole' => $this->getTableDataArrayAsString(config('admin.database.roles_table'), $exceptFields), + + 'ArrayPivotRoleMenu' => $this->getTableDataArrayAsString(config('admin.database.role_menu_table'), $exceptFields), + 'ArrayPivotRolePermissions' => $this->getTableDataArrayAsString(config('admin.database.role_permissions_table'), $exceptFields), + ]; + + if ($exportUsers) { + $replaces = array_merge($replaces, [ + 'ClassUsers' => config('admin.database.users_model'), + 'TableRoleUsers' => config('admin.database.role_users_table'), + 'TablePermissionsUsers' => config('admin.database.user_permissions_table'), + + 'ArrayUsers' => $this->getTableDataArrayAsString(config('admin.database.users_table'), $exceptFields), + 'ArrayPivotRoleUsers' => $this->getTableDataArrayAsString(config('admin.database.role_users_table'), $exceptFields), + 'ArrayPivotPermissionsUsers' => $this->getTableDataArrayAsString(config('admin.database.user_permissions_table'), $exceptFields), + ]); + } else { + $contents = preg_replace('/\/\/ users tables[\s\S]*?(?=\/\/ finish)/mu', '', $contents); + } + + $contents = str_replace(array_keys($replaces), array_values($replaces), $contents); + + $this->laravel['files']->put($seedFile, $contents); + + $this->line('Admin tables seed file was created: '.str_replace(base_path(), '', $seedFile)); + $this->line("Use: php artisan db:seed --class={$name}"); + } + + /** + * Get data array from table as string result var_export. + * + * @param string $table + * @param array $exceptFields + * + * @return string + */ + protected function getTableDataArrayAsString($table, $exceptFields = []) + { + $fields = \DB::getSchemaBuilder()->getColumnListing($table); + $fields = array_diff($fields, $exceptFields); + + $array = \DB::table($table)->get($fields)->map(function ($item) { + return (array) $item; + })->all(); + + return $this->varExport($array, str_repeat(' ', 12)); + } + + /** + * Get stub contents. + * + * @param $name + * + * @return string + */ + protected function getStub($name) + { + return $this->laravel['files']->get(__DIR__."/stubs/$name.stub"); + } + + /** + * Custom var_export for correct work with \r\n. + * + * @param $var + * @param string $indent + * + * @return string + */ + protected function varExport($var, $indent = '') + { + switch (gettype($var)) { + + case 'string': + return '"'.addcslashes($var, "\\\$\"\r\n\t\v\f").'"'; + + case 'array': + $indexed = array_keys($var) === range(0, count($var) - 1); + + $r = []; + + foreach ($var as $key => $value) { + $r[] = "$indent " + .($indexed ? '' : $this->varExport($key).' => ') + .$this->varExport($value, "{$indent} "); + } + + return "[\n".implode(",\n", $r)."\n".$indent.']'; + + case 'boolean': + return $var ? 'true' : 'false'; + + case 'integer': + case 'double': + return $var; + + default: + return var_export($var, true); + } + } +} diff --git a/src/Console/ExtendCommand.php b/src/Console/ExtendCommand.php new file mode 100644 index 0000000..040c826 --- /dev/null +++ b/src/Console/ExtendCommand.php @@ -0,0 +1,308 @@ +filesystem = $filesystem; + + $this->extensionDir = config('admin.extension_dir'); + + InputExtensionDir: + if (empty($this->extensionDir)) { + $this->extensionDir = $this->ask('Please input a directory to store your extension:'); + } + + if (!file_exists($this->extensionDir)) { + $this->makeDir(); + } + + $this->package = $this->argument('extension'); + + InputExtensionName: + if (!$this->validateExtensionName($this->package)) { + $this->package = $this->ask("[$this->package] is not a valid package name, please input a name like (/)"); + goto InputExtensionName; + } + + $this->makeDirs(); + $this->makeFiles(); + + $this->info("The extension scaffolding generated successfully. \r\n"); + $this->showTree(); + } + + /** + * Show extension scaffolding with tree structure. + */ + protected function showTree() + { + $tree = <<extensionPath()} + ├── LICENSE + ├── README.md + ├── composer.json + ├── database + │   ├── migrations + │   └── seeds + ├── resources + │   ├── assets + │   └── views + │   └── index.blade.php + ├── routes + │   └── web.php + └── src + ├── {$this->className}.php + ├── {$this->className}ServiceProvider.php + └── Http + └── Controllers + └── {$this->className}Controller.php +TREE; + + $this->info($tree); + } + + /** + * Make extension files. + */ + protected function makeFiles() + { + $this->namespace = $this->getRootNameSpace(); + + $this->className = $this->getClassName(); + + // copy files + $this->copy([ + __DIR__.'/stubs/extension/view.stub' => 'resources/views/index.blade.php', + __DIR__.'/stubs/extension/.gitignore.stub' => '.gitignore', + __DIR__.'/stubs/extension/README.md.stub' => 'README.md', + __DIR__.'/stubs/extension/LICENSE.stub' => 'LICENSE', + ]); + + // make composer.json + $composerContents = str_replace( + [':package', ':namespace', ':class_name'], + [$this->package, str_replace('\\', '\\\\', $this->namespace).'\\\\', $this->className], + file_get_contents(__DIR__.'/stubs/extension/composer.json.stub') + ); + $this->putFile('composer.json', $composerContents); + + // make class + $classContents = str_replace( + [':namespace', ':class_name', ':title', ':path', ':base_package'], + [$this->namespace, $this->className, title_case($this->className), basename($this->package), basename($this->package)], + file_get_contents(__DIR__.'/stubs/extension/extension.stub') + ); + $this->putFile("src/{$this->className}.php", $classContents); + + // make service provider + $providerContents = str_replace( + [':namespace', ':class_name', ':base_package', ':package'], + [$this->namespace, $this->className, basename($this->package), $this->package], + file_get_contents(__DIR__.'/stubs/extension/service-provider.stub') + ); + $this->putFile("src/{$this->className}ServiceProvider.php", $providerContents); + + // make controller + $controllerContent = str_replace( + [':namespace', ':class_name', ':base_package'], + [$this->namespace, $this->className, basename($this->package)], + file_get_contents(__DIR__.'/stubs/extension/controller.stub') + ); + $this->putFile("src/Http/Controllers/{$this->className}Controller.php", $controllerContent); + + // make routes + $routesContent = str_replace( + [':namespace', ':class_name', ':path'], + [$this->namespace, $this->className, basename($this->package)], + file_get_contents(__DIR__.'/stubs/extension/routes.stub') + ); + $this->putFile('routes/web.php', $routesContent); + } + + /** + * Get root namespace for this package. + * + * @return array|null|string + */ + protected function getRootNameSpace() + { + if (!$namespace = $this->option('namespace')) { + list($vendor, $name) = explode('/', $this->package); + + $default = str_replace(['-', '-'], '', title_case($vendor).'\\'.title_case($name)); + + $namespace = $this->ask('Root namespace', $default); + } + + return $namespace; + } + + /** + * Get extension class name. + * + * @return string + */ + protected function getClassName() + { + return class_basename($this->namespace); + } + + /** + * Create package dirs. + */ + protected function makeDirs() + { + $this->basePath = rtrim($this->extensionDir, '/').'/'.ltrim($this->package, '/'); + + $this->makeDir($this->dirs); + } + + /** + * Validate extension name. + * + * @param string $name + * + * @return int + */ + protected function validateExtensionName($name) + { + return preg_match('/^[\w\-_]+\/[\w\-_]+$/', $name); + } + + /** + * Extension path. + * + * @param string $path + * + * @return string + */ + protected function extensionPath($path = '') + { + $path = rtrim($path, '/'); + + if (empty($path)) { + return rtrim($this->basePath, '/'); + } + + return rtrim($this->basePath, '/').'/'.ltrim($path, '/'); + } + + /** + * Put contents to file. + * + * @param string $to + * @param string $content + */ + protected function putFile($to, $content) + { + $to = $this->extensionPath($to); + + $this->filesystem->put($to, $content); + } + + /** + * Copy files to extension path. + * + * @param string|array $from + * @param string|null $to + */ + protected function copy($from, $to = null) + { + if (is_array($from) && is_null($to)) { + foreach ($from as $key => $value) { + $this->copy($key, $value); + } + + return; + } + + if (!file_exists($from)) { + return; + } + + $to = $this->extensionPath($to); + + $this->filesystem->copy($from, $to); + } + + /** + * Make new directory. + * + * @param array|string $paths + */ + protected function makeDir($paths = '') + { + foreach ((array) $paths as $path) { + $path = $this->extensionPath($path); + + $this->filesystem->makeDirectory($path, 0755, true, true); + } + } +} diff --git a/src/Console/ImportCommand.php b/src/Console/ImportCommand.php new file mode 100644 index 0000000..0002160 --- /dev/null +++ b/src/Console/ImportCommand.php @@ -0,0 +1,49 @@ +argument('extension'); + + if (empty($extension) || !array_has(Admin::$extensions, $extension)) { + $extension = $this->choice('Please choose a extension to import', array_keys(Admin::$extensions)); + } + + $className = array_get(Admin::$extensions, $extension); + + if (!class_exists($className) || !method_exists($className, 'import')) { + $this->error("Invalid Extension [$className]"); + + return; + } + + call_user_func([$className, 'import'], $this); + + $this->info("Extension [$className] imported"); + } +} diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php new file mode 100644 index 0000000..dc72982 --- /dev/null +++ b/src/Console/InstallCommand.php @@ -0,0 +1,186 @@ +initDatabase(); + + $this->initAdminDirectory(); + } + + /** + * Create tables and seed it. + * + * @return void + */ + public function initDatabase() + { + $this->call('migrate'); + + $userModel = config('admin.database.users_model'); + + if ($userModel::count() == 0) { + $this->call('db:seed', ['--class' => \Encore\Admin\Auth\Database\AdminTablesSeeder::class]); + } + } + + /** + * Initialize the admAin directory. + * + * @return void + */ + protected function initAdminDirectory() + { + $this->directory = config('admin.directory'); + + if (is_dir($this->directory)) { + $this->line("{$this->directory} directory already exists ! "); + + return; + } + + $this->makeDir('/'); + $this->line('Admin directory was created: '.str_replace(base_path(), '', $this->directory)); + + $this->makeDir('Controllers'); + + $this->createHomeController(); + $this->createAuthController(); + $this->createExampleController(); + + $this->createBootstrapFile(); + $this->createRoutesFile(); + } + + /** + * Create HomeController. + * + * @return void + */ + public function createHomeController() + { + $homeController = $this->directory.'/Controllers/HomeController.php'; + $contents = $this->getStub('HomeController'); + + $this->laravel['files']->put( + $homeController, + str_replace('DummyNamespace', config('admin.route.namespace'), $contents) + ); + $this->line('HomeController file was created: '.str_replace(base_path(), '', $homeController)); + } + + /** + * Create AuthController. + * + * @return void + */ + public function createAuthController() + { + $authController = $this->directory.'/Controllers/AuthController.php'; + $contents = $this->getStub('AuthController'); + + $this->laravel['files']->put( + $authController, + str_replace('DummyNamespace', config('admin.route.namespace'), $contents) + ); + $this->line('AuthController file was created: '.str_replace(base_path(), '', $authController)); + } + + /** + * Create HomeController. + * + * @return void + */ + public function createExampleController() + { + $exampleController = $this->directory.'/Controllers/ExampleController.php'; + $contents = $this->getStub('ExampleController'); + + $this->laravel['files']->put( + $exampleController, + str_replace('DummyNamespace', config('admin.route.namespace'), $contents) + ); + $this->line('ExampleController file was created: '.str_replace(base_path(), '', $exampleController)); + } + + /** + * Create routes file. + * + * @return void + */ + protected function createBootstrapFile() + { + $file = $this->directory.'/bootstrap.php'; + + $contents = $this->getStub('bootstrap'); + $this->laravel['files']->put($file, $contents); + $this->line('Bootstrap file was created: '.str_replace(base_path(), '', $file)); + } + + /** + * Create routes file. + * + * @return void + */ + protected function createRoutesFile() + { + $file = $this->directory.'/routes.php'; + + $contents = $this->getStub('routes'); + $this->laravel['files']->put($file, str_replace('DummyNamespace', config('admin.route.namespace'), $contents)); + $this->line('Routes file was created: '.str_replace(base_path(), '', $file)); + } + + /** + * Get stub contents. + * + * @param $name + * + * @return string + */ + protected function getStub($name) + { + return $this->laravel['files']->get(__DIR__."/stubs/$name.stub"); + } + + /** + * Make new directory. + * + * @param string $path + */ + protected function makeDir($path = '') + { + $this->laravel['files']->makeDirectory("{$this->directory}/$path", 0755, true, true); + } +} diff --git a/src/Console/MakeCommand.php b/src/Console/MakeCommand.php new file mode 100644 index 0000000..4bd92d5 --- /dev/null +++ b/src/Console/MakeCommand.php @@ -0,0 +1,182 @@ +modelExists()) { + $this->error('Model does not exists !'); + + return false; + } + + $stub = $this->option('stub'); + + if ($stub and !is_file($stub)) { + $this->error('The stub file dose not exist.'); + + return false; + } + + $modelName = $this->option('model'); + + $this->generator = new ResourceGenerator($modelName); + + if ($this->option('output')) { + return $this->output($modelName); + } + + parent::handle(); + } + + /** + * @param string $modelName + */ + protected function output($modelName) + { + $this->alert("laravel-admin controller code for model [{$modelName}]"); + + $this->info($this->generator->generateGrid()); + $this->info($this->generator->generateShow()); + $this->info($this->generator->generateForm()); + } + + /** + * Determine if the model is exists. + * + * @return bool + */ + protected function modelExists() + { + $model = $this->option('model'); + + if (empty($model)) { + return true; + } + + return class_exists($model) && is_subclass_of($model, Model::class); + } + + /** + * Replace the class name for the given stub. + * + * @param string $stub + * @param string $name + * + * @return string + */ + protected function replaceClass($stub, $name) + { + $stub = parent::replaceClass($stub, $name); + + return str_replace( + [ + 'DummyModelNamespace', + 'DummyModel', + 'DummyGrid', + 'DummyShow', + 'DummyForm', + ], + [ + $this->option('model'), + class_basename($this->option('model')), + $this->indentCodes($this->generator->generateGrid()), + $this->indentCodes($this->generator->generateShow()), + $this->indentCodes($this->generator->generateForm()), + ], + $stub + ); + } + + /** + * @param string $code + * + * @return string + */ + protected function indentCodes($code) + { + $indent = str_repeat(' ', 8); + + return rtrim($indent.preg_replace("/\r\n/", "\r\n{$indent}", $code)); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + if ($stub = $this->option('stub')) { + return $stub; + } + + if ($this->option('model')) { + return __DIR__.'/stubs/controller.stub'; + } + + return __DIR__.'/stubs/blank.stub'; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + $directory = config('admin.directory'); + + $namespace = ucfirst(basename($directory)); + + return $rootNamespace."\\$namespace\Controllers"; + } + + /** + * Get the desired class name from the input. + * + * @return string + */ + protected function getNameInput() + { + $name = trim($this->argument('name')); + + $this->type = $this->qualifyClass($name); + + return $name; + } +} diff --git a/src/Console/MenuCommand.php b/src/Console/MenuCommand.php new file mode 100644 index 0000000..726dbd5 --- /dev/null +++ b/src/Console/MenuCommand.php @@ -0,0 +1,35 @@ +option('force'); + $options = ['--provider' => 'Encore\Admin\AdminServiceProvider']; + if ($force == true) { + $options['--force'] = true; + } + $this->call('vendor:publish', $options); + $this->call('view:clear'); + } +} diff --git a/src/Console/ResetPasswordCommand.php b/src/Console/ResetPasswordCommand.php new file mode 100644 index 0000000..7249394 --- /dev/null +++ b/src/Console/ResetPasswordCommand.php @@ -0,0 +1,58 @@ +askWithCompletion('Please enter a username who needs to reset his password', $users->pluck('username')->toArray()); + + $user = $users->first(function ($user) use ($username) { + return $user->username == $username; + }); + + if (is_null($user)) { + $this->error('The user you entered is not exists'); + goto askForUserName; + } + + enterPassword: + $password = $this->secret('Please enter a password'); + + if ($password !== $this->secret('Please confirm the password')) { + $this->error('The passwords entered twice do not match, please re-enter'); + goto enterPassword; + } + + $user->password = bcrypt($password); + + $user->save(); + + $this->info('User password reset successfully.'); + } +} diff --git a/src/Console/ResourceGenerator.php b/src/Console/ResourceGenerator.php new file mode 100644 index 0000000..91c4d13 --- /dev/null +++ b/src/Console/ResourceGenerator.php @@ -0,0 +1,254 @@ + "\$form->%s('%s', '%s')", + 'show_field' => "\$show->%s('%s')", + 'grid_column' => "\$grid->%s('%s')", + ]; + + /** + * @var array + */ + private $doctrineTypeMapping = [ + 'string' => [ + 'enum', 'geometry', 'geometrycollection', 'linestring', + 'polygon', 'multilinestring', 'multipoint', 'multipolygon', + 'point', + ], + ]; + + /** + * @var array + */ + protected $fieldTypeMapping = [ + 'ip' => 'ip', + 'email' => 'email|mail', + 'password' => 'password|pwd', + 'url' => 'url|link|src|href', + 'mobile' => 'mobile|phone', + 'color' => 'color|rgb', + 'image' => 'image|img|avatar|pic|picture|cover', + 'file' => 'file|attachment', + ]; + + /** + * ResourceGenerator constructor. + * + * @param mixed $model + */ + public function __construct($model) + { + $this->model = $this->getModel($model); + } + + /** + * @param mixed $model + * + * @return mixed + */ + protected function getModel($model) + { + if ($model instanceof Model) { + return $model; + } + + if (!class_exists($model) || !is_string($model) || !is_subclass_of($model, Model::class)) { + throw new \InvalidArgumentException("Invalid model [$model] !"); + } + + return new $model(); + } + + /** + * @return string + */ + public function generateForm() + { + $reservedColumns = $this->getReservedColumns(); + + $output = ''; + + foreach ($this->getTableColumns() as $column) { + $name = $column->getName(); + if (in_array($name, $reservedColumns)) { + continue; + } + $type = $column->getType()->getName(); + $default = $column->getDefault(); + + $defaultValue = ''; + + // set column fieldType and defaultValue + switch ($type) { + case 'boolean': + case 'bool': + $fieldType = 'switch'; + break; + case 'json': + case 'array': + case 'object': + $fieldType = 'text'; + break; + case 'string': + $fieldType = 'text'; + foreach ($this->fieldTypeMapping as $type => $regex) { + if (preg_match("/^($regex)$/i", $name) !== 0) { + $fieldType = $type; + break; + } + } + $defaultValue = "'{$default}'"; + break; + case 'integer': + case 'bigint': + case 'smallint': + case 'timestamp': + $fieldType = 'number'; + break; + case 'decimal': + case 'float': + case 'real': + $fieldType = 'decimal'; + break; + case 'datetime': + $fieldType = 'datetime'; + $defaultValue = "date('Y-m-d H:i:s')"; + break; + case 'date': + $fieldType = 'date'; + $defaultValue = "date('Y-m-d')"; + break; + case 'time': + $fieldType = 'time'; + $defaultValue = "date('H:i:s')"; + break; + case 'text': + case 'blob': + $fieldType = 'textarea'; + break; + default: + $fieldType = 'text'; + $defaultValue = "'{$default}'"; + } + + $defaultValue = $defaultValue ?: $default; + + $label = $this->formatLabel($name); + + $output .= sprintf($this->formats['form_field'], $fieldType, $name, $label); + + if (trim($defaultValue, "'\"")) { + $output .= "->default({$defaultValue})"; + } + + $output .= ";\r\n"; + } + + return $output; + } + + public function generateShow() + { + $output = ''; + + foreach ($this->getTableColumns() as $column) { + $name = $column->getName(); + + // set column label + $label = $this->formatLabel($name); + + $output .= sprintf($this->formats['show_field'], $name, $label); + + $output .= ";\r\n"; + } + + return $output; + } + + public function generateGrid() + { + $output = ''; + + foreach ($this->getTableColumns() as $column) { + $name = $column->getName(); + $label = $this->formatLabel($name); + + $output .= sprintf($this->formats['grid_column'], $name, $label); + $output .= ";\r\n"; + } + + return $output; + } + + protected function getReservedColumns() + { + return [ + $this->model->getKeyName(), + $this->model->getCreatedAtColumn(), + $this->model->getUpdatedAtColumn(), + 'deleted_at', + ]; + } + + /** + * Get columns of a giving model. + * + * @throws \Exception + * + * @return \Doctrine\DBAL\Schema\Column[] + */ + protected function getTableColumns() + { + if (!$this->model->getConnection()->isDoctrineAvailable()) { + throw new \Exception( + 'You need to require doctrine/dbal: ~2.3 in your own composer.json to get database columns. ' + ); + } + + $table = $this->model->getConnection()->getTablePrefix().$this->model->getTable(); + /** @var \Doctrine\DBAL\Schema\MySqlSchemaManager $schema */ + $schema = $this->model->getConnection()->getDoctrineSchemaManager($table); + + // custom mapping the types that doctrine/dbal does not support + $databasePlatform = $schema->getDatabasePlatform(); + + foreach ($this->doctrineTypeMapping as $doctrineType => $dbTypes) { + foreach ($dbTypes as $dbType) { + $databasePlatform->registerDoctrineTypeMapping($dbType, $doctrineType); + } + } + + $database = null; + if (strpos($table, '.')) { + list($database, $table) = explode('.', $table); + } + + return $schema->listTableColumns($table, $database); + } + + /** + * Format label. + * + * @param string $value + * + * @return string + */ + protected function formatLabel($value) + { + return ucfirst(str_replace(['-', '_'], ' ', $value)); + } +} diff --git a/src/Console/UninstallCommand.php b/src/Console/UninstallCommand.php new file mode 100644 index 0000000..25d51c6 --- /dev/null +++ b/src/Console/UninstallCommand.php @@ -0,0 +1,50 @@ +confirm('Are you sure to uninstall laravel-admin?')) { + return; + } + + $this->removeFilesAndDirectories(); + + $this->line('Uninstalling laravel-admin!'); + } + + /** + * Remove files and directories. + * + * @return void + */ + protected function removeFilesAndDirectories() + { + $this->laravel['files']->deleteDirectory(config('admin.directory')); + $this->laravel['files']->deleteDirectory(public_path('vendor/laravel-admin/')); + $this->laravel['files']->delete(config_path('admin.php')); + } +} diff --git a/src/Console/stubs/AdminTablesSeeder.stub b/src/Console/stubs/AdminTablesSeeder.stub new file mode 100644 index 0000000..84a3d04 --- /dev/null +++ b/src/Console/stubs/AdminTablesSeeder.stub @@ -0,0 +1,59 @@ +truncate(); + DB::table('TableRoleMenu')->insert( + ArrayPivotRoleMenu + ); + + DB::table('TableRolePermissions')->truncate(); + DB::table('TableRolePermissions')->insert( + ArrayPivotRolePermissions + ); + + // users tables + ClassUsers::truncate(); + ClassUsers::insert( + ArrayUsers + ); + + DB::table('TableRoleUsers')->truncate(); + DB::table('TableRoleUsers')->insert( + ArrayPivotRoleUsers + ); + + DB::table('TablePermissionsUsers')->truncate(); + DB::table('TablePermissionsUsers')->insert( + ArrayPivotPermissionsUsers + ); + + // finish + } +} diff --git a/src/Console/stubs/AuthController.stub b/src/Console/stubs/AuthController.stub new file mode 100644 index 0000000..55ac734 --- /dev/null +++ b/src/Console/stubs/AuthController.stub @@ -0,0 +1,10 @@ +header('Index') + ->description('description') + ->body($this->grid()); + } + + /** + * Show interface. + * + * @param mixed $id + * @param Content $content + * @return Content + */ + public function show($id, Content $content) + { + return $content + ->header('Detail') + ->description('description') + ->body($this->detail($id)); + } + + /** + * Edit interface. + * + * @param mixed $id + * @param Content $content + * @return Content + */ + public function edit($id, Content $content) + { + return $content + ->header('Edit') + ->description('description') + ->body($this->form()->edit($id)); + } + + /** + * Create interface. + * + * @param Content $content + * @return Content + */ + public function create(Content $content) + { + return $content + ->header('Create') + ->description('description') + ->body($this->form()); + } + + /** + * Make a grid builder. + * + * @return Grid + */ + protected function grid() + { + $grid = new Grid(new YourModel); + + $grid->id('ID')->sortable(); + $grid->created_at('Created at'); + $grid->updated_at('Updated at'); + + return $grid; + } + + /** + * Make a show builder. + * + * @param mixed $id + * @return Show + */ + protected function detail($id) + { + $show = new Show(YourModel::findOrFail($id)); + + $show->id('ID'); + $show->created_at('Created at'); + $show->updated_at('Updated at'); + + return $show; + } + + /** + * Make a form builder. + * + * @return Form + */ + protected function form() + { + $form = new Form(new YourModel); + + $form->display('id', 'ID'); + $form->display('created_at', 'Created At'); + $form->display('updated_at', 'Updated At'); + + return $form; + } +} diff --git a/src/Console/stubs/HomeController.stub b/src/Console/stubs/HomeController.stub new file mode 100644 index 0000000..d7fedf4 --- /dev/null +++ b/src/Console/stubs/HomeController.stub @@ -0,0 +1,34 @@ +header('Dashboard') + ->description('Description...') + ->row(Dashboard::title()) + ->row(function (Row $row) { + + $row->column(4, function (Column $column) { + $column->append(Dashboard::environment()); + }); + + $row->column(4, function (Column $column) { + $column->append(Dashboard::extensions()); + }); + + $row->column(4, function (Column $column) { + $column->append(Dashboard::dependencies()); + }); + }); + } +} diff --git a/src/Console/stubs/blank.stub b/src/Console/stubs/blank.stub new file mode 100644 index 0000000..89344cf --- /dev/null +++ b/src/Console/stubs/blank.stub @@ -0,0 +1,11 @@ + + * + * Bootstraper for Admin. + * + * Here you can remove builtin form field: + * Encore\Admin\Form::forget(['map', 'editor']); + * + * Or extend custom form field: + * Encore\Admin\Form::extend('php', PHPEditor::class); + * + * Or require js and css assets: + * Admin::css('/packages/prettydocs/css/styles.css'); + * Admin::js('/packages/prettydocs/js/main.js'); + * + */ + +Encore\Admin\Form::forget(['map', 'editor']); diff --git a/src/Console/stubs/controller.stub b/src/Console/stubs/controller.stub new file mode 100644 index 0000000..4720745 --- /dev/null +++ b/src/Console/stubs/controller.stub @@ -0,0 +1,117 @@ +header('Index') + ->description('description') + ->body($this->grid()); + } + + /** + * Show interface. + * + * @param mixed $id + * @param Content $content + * @return Content + */ + public function show($id, Content $content) + { + return $content + ->header('Detail') + ->description('description') + ->body($this->detail($id)); + } + + /** + * Edit interface. + * + * @param mixed $id + * @param Content $content + * @return Content + */ + public function edit($id, Content $content) + { + return $content + ->header('Edit') + ->description('description') + ->body($this->form()->edit($id)); + } + + /** + * Create interface. + * + * @param Content $content + * @return Content + */ + public function create(Content $content) + { + return $content + ->header('Create') + ->description('description') + ->body($this->form()); + } + + /** + * Make a grid builder. + * + * @return Grid + */ + protected function grid() + { + $grid = new Grid(new DummyModel); + +DummyGrid + + return $grid; + } + + /** + * Make a show builder. + * + * @param mixed $id + * @return Show + */ + protected function detail($id) + { + $show = new Show(DummyModel::findOrFail($id)); + +DummyShow + + return $show; + } + + /** + * Make a form builder. + * + * @return Form + */ + protected function form() + { + $form = new Form(new DummyModel); + +DummyForm + + return $form; + } +} diff --git a/src/Console/stubs/extension/.gitignore.stub b/src/Console/stubs/extension/.gitignore.stub new file mode 100644 index 0000000..9d4b362 --- /dev/null +++ b/src/Console/stubs/extension/.gitignore.stub @@ -0,0 +1,7 @@ +.DS_Store +phpunit.phar +/vendor +composer.phar +composer.lock +*.project +.idea/ \ No newline at end of file diff --git a/src/Console/stubs/extension/LICENSE.stub b/src/Console/stubs/extension/LICENSE.stub new file mode 100644 index 0000000..229071a --- /dev/null +++ b/src/Console/stubs/extension/LICENSE.stub @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jens Segers + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Console/stubs/extension/README.md.stub b/src/Console/stubs/extension/README.md.stub new file mode 100644 index 0000000..cc7db8c --- /dev/null +++ b/src/Console/stubs/extension/README.md.stub @@ -0,0 +1,4 @@ +laravel-admin extension +====== + + diff --git a/src/Console/stubs/extension/composer.json.stub b/src/Console/stubs/extension/composer.json.stub new file mode 100644 index 0000000..d1cf431 --- /dev/null +++ b/src/Console/stubs/extension/composer.json.stub @@ -0,0 +1,34 @@ +{ + "name": ":package", + "description": "description...", + "type": "library", + "keywords": ["laravel-admin", "extension"], + "homepage": "https://github.com/:package", + "license": "MIT", + "authors": [ + { + "name": "your name", + "email": "your email" + } + ], + "require": { + "php": ">=7.0.0", + "encore/laravel-admin": "~1.6" + }, + "require-dev": { + "phpunit/phpunit": "~6.0" + }, + "autoload": { + "psr-4": { + ":namespace": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + ":namespace:class_nameServiceProvider" + ] + + } + } +} diff --git a/src/Console/stubs/extension/controller.stub b/src/Console/stubs/extension/controller.stub new file mode 100644 index 0000000..465cede --- /dev/null +++ b/src/Console/stubs/extension/controller.stub @@ -0,0 +1,17 @@ +header('Title') + ->description('Description') + ->body(view(':base_package::index')); + } +} \ No newline at end of file diff --git a/src/Console/stubs/extension/extension.stub b/src/Console/stubs/extension/extension.stub new file mode 100644 index 0000000..bb66f4d --- /dev/null +++ b/src/Console/stubs/extension/extension.stub @@ -0,0 +1,20 @@ + ':title', + 'path' => ':path', + 'icon' => 'fa-gears', + ]; +} \ No newline at end of file diff --git a/src/Console/stubs/extension/routes.stub b/src/Console/stubs/extension/routes.stub new file mode 100644 index 0000000..6786984 --- /dev/null +++ b/src/Console/stubs/extension/routes.stub @@ -0,0 +1,5 @@ +views()) { + $this->loadViewsFrom($views, ':base_package'); + } + + if ($this->app->runningInConsole() && $assets = $extension->assets()) { + $this->publishes( + [$assets => public_path('vendor/:package')], + ':base_package' + ); + } + + $this->app->booted(function () { + :class_name::routes(__DIR__.'/../routes/web.php'); + }); + } +} \ No newline at end of file diff --git a/src/Console/stubs/extension/view.stub b/src/Console/stubs/extension/view.stub new file mode 100644 index 0000000..d08d550 --- /dev/null +++ b/src/Console/stubs/extension/view.stub @@ -0,0 +1 @@ +Welcome to laravel-admin \ No newline at end of file diff --git a/src/Console/stubs/routes.stub b/src/Console/stubs/routes.stub new file mode 100644 index 0000000..95aa7f7 --- /dev/null +++ b/src/Console/stubs/routes.stub @@ -0,0 +1,15 @@ + config('admin.route.prefix'), + 'namespace' => config('admin.route.namespace'), + 'middleware' => config('admin.route.middleware'), +], function (Router $router) { + + $router->get('/', 'HomeController@index'); + +}); diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php new file mode 100644 index 0000000..6ace9d8 --- /dev/null +++ b/src/Controllers/AuthController.php @@ -0,0 +1,205 @@ +guard()->check()) { + return redirect($this->redirectPath()); + } + + return view('admin::login'); + } + + /** + * Handle a login request. + * + * @param Request $request + * + * @return mixed + */ + public function postLogin(Request $request) + { + $credentials = $request->only([$this->username(), 'password']); + $remember = $request->get('remember', false); + + /** @var \Illuminate\Validation\Validator $validator */ + $validator = Validator::make($credentials, [ + $this->username() => 'required', + 'password' => 'required', + ]); + + if ($validator->fails()) { + return back()->withInput()->withErrors($validator); + } + + if ($this->guard()->attempt($credentials, $remember)) { + return $this->sendLoginResponse($request); + } + + return back()->withInput()->withErrors([ + $this->username() => $this->getFailedLoginMessage(), + ]); + } + + /** + * User logout. + * + * @return Redirect + */ + public function getLogout(Request $request) + { + $this->guard()->logout(); + + $request->session()->invalidate(); + + return redirect(config('admin.route.prefix')); + } + + /** + * User setting page. + * + * @param Content $content + * + * @return Content + */ + public function getSetting(Content $content) + { + $form = $this->settingForm(); + $form->tools( + function (Form\Tools $tools) { + $tools->disableList(); + } + ); + + return $content + ->header(trans('admin.user_setting')) + ->body($form->edit(Admin::user()->id)); + } + + /** + * Update user setting. + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function putSetting() + { + return $this->settingForm()->update(Admin::user()->id); + } + + /** + * Model-form for user setting. + * + * @return Form + */ + protected function settingForm() + { + $class = config('admin.database.users_model'); + + $form = new Form(new $class()); + + $form->display('username', trans('admin.username')); + $form->text('name', trans('admin.name'))->rules('required'); + $form->image('avatar', trans('admin.avatar')); + $form->password('password', trans('admin.password'))->rules('confirmed|required'); + $form->password('password_confirmation', trans('admin.password_confirmation'))->rules('required') + ->default(function ($form) { + return $form->model()->password; + }); + + $form->setAction(admin_base_path('auth/setting')); + + $form->ignore(['password_confirmation']); + + $form->saving(function (Form $form) { + if ($form->password && $form->model()->password != $form->password) { + $form->password = bcrypt($form->password); + } + }); + + $form->saved(function () { + admin_toastr(trans('admin.update_succeeded')); + + return redirect(admin_base_path('auth/setting')); + }); + + return $form; + } + + /** + * @return string|\Symfony\Component\Translation\TranslatorInterface + */ + protected function getFailedLoginMessage() + { + return Lang::has('auth.failed') + ? trans('auth.failed') + : 'These credentials do not match our records.'; + } + + /** + * Get the post login redirect path. + * + * @return string + */ + protected function redirectPath() + { + if (method_exists($this, 'redirectTo')) { + return $this->redirectTo(); + } + + return property_exists($this, 'redirectTo') ? $this->redirectTo : config('admin.route.prefix'); + } + + /** + * Send the response after the user was authenticated. + * + * @param \Illuminate\Http\Request $request + * + * @return \Illuminate\Http\Response + */ + protected function sendLoginResponse(Request $request) + { + admin_toastr(trans('admin.login_successful')); + + $request->session()->regenerate(); + + return redirect()->intended($this->redirectPath()); + } + + /** + * Get the login username to be used by the controller. + * + * @return string + */ + protected function username() + { + return 'username'; + } + + /** + * Get the guard to be used during authentication. + * + * @return \Illuminate\Contracts\Auth\StatefulGuard + */ + protected function guard() + { + return Auth::guard('admin'); + } +} diff --git a/src/Controllers/Dashboard.php b/src/Controllers/Dashboard.php new file mode 100644 index 0000000..272a65a --- /dev/null +++ b/src/Controllers/Dashboard.php @@ -0,0 +1,114 @@ + 'PHP version', 'value' => 'PHP/'.PHP_VERSION], + ['name' => 'Laravel version', 'value' => app()->version()], + ['name' => 'CGI', 'value' => php_sapi_name()], + ['name' => 'Uname', 'value' => php_uname()], + ['name' => 'Server', 'value' => array_get($_SERVER, 'SERVER_SOFTWARE')], + + ['name' => 'Cache driver', 'value' => config('cache.default')], + ['name' => 'Session driver', 'value' => config('session.driver')], + ['name' => 'Queue driver', 'value' => config('queue.default')], + + ['name' => 'Timezone', 'value' => config('app.timezone')], + ['name' => 'Locale', 'value' => config('app.locale')], + ['name' => 'Env', 'value' => config('app.env')], + ['name' => 'URL', 'value' => config('app.url')], + ]; + + return view('admin::dashboard.environment', compact('envs')); + } + + /** + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public static function extensions() + { + $extensions = [ + 'helpers' => [ + 'name' => 'laravel-admin-ext/helpers', + 'link' => 'https://github.com/laravel-admin-extensions/helpers', + 'icon' => 'gears', + ], + 'log-viewer' => [ + 'name' => 'laravel-admin-ext/log-viewer', + 'link' => 'https://github.com/laravel-admin-extensions/log-viewer', + 'icon' => 'database', + ], + 'backup' => [ + 'name' => 'laravel-admin-ext/backup', + 'link' => 'https://github.com/laravel-admin-extensions/backup', + 'icon' => 'copy', + ], + 'config' => [ + 'name' => 'laravel-admin-ext/config', + 'link' => 'https://github.com/laravel-admin-extensions/config', + 'icon' => 'toggle-on', + ], + 'api-tester' => [ + 'name' => 'laravel-admin-ext/api-tester', + 'link' => 'https://github.com/laravel-admin-extensions/api-tester', + 'icon' => 'sliders', + ], + 'media-manager' => [ + 'name' => 'laravel-admin-ext/media-manager', + 'link' => 'https://github.com/laravel-admin-extensions/media-manager', + 'icon' => 'file', + ], + 'scheduling' => [ + 'name' => 'laravel-admin-ext/scheduling', + 'link' => 'https://github.com/laravel-admin-extensions/scheduling', + 'icon' => 'clock-o', + ], + 'reporter' => [ + 'name' => 'laravel-admin-ext/reporter', + 'link' => 'https://github.com/laravel-admin-extensions/reporter', + 'icon' => 'bug', + ], + 'redis-manager' => [ + 'name' => 'laravel-admin-ext/redis-manager', + 'link' => 'https://github.com/laravel-admin-extensions/redis-manager', + 'icon' => 'flask', + ], + ]; + + foreach ($extensions as &$extension) { + $name = explode('/', $extension['name']); + $extension['installed'] = array_key_exists(end($name), Admin::$extensions); + } + + return view('admin::dashboard.extensions', compact('extensions')); + } + + /** + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public static function dependencies() + { + $json = file_get_contents(base_path('composer.json')); + + $dependencies = json_decode($json, true)['require']; + + return view('admin::dashboard.dependencies', compact('dependencies')); + } +} diff --git a/src/Controllers/HasResourceActions.php b/src/Controllers/HasResourceActions.php new file mode 100644 index 0000000..c802aa6 --- /dev/null +++ b/src/Controllers/HasResourceActions.php @@ -0,0 +1,52 @@ +form()->update($id); + } + + /** + * Store a newly created resource in storage. + * + * @return mixed + */ + public function store() + { + return $this->form()->store(); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * + * @return \Illuminate\Http\Response + */ + public function destroy($id) + { + if ($this->form()->destroy($id)) { + $data = [ + 'status' => true, + 'message' => trans('admin.delete_succeeded'), + ]; + } else { + $data = [ + 'status' => false, + 'message' => trans('admin.delete_failed'), + ]; + } + + return response()->json($data); + } +} diff --git a/src/Controllers/LogController.php b/src/Controllers/LogController.php new file mode 100644 index 0000000..d19903e --- /dev/null +++ b/src/Controllers/LogController.php @@ -0,0 +1,99 @@ +header(trans('admin.operation_log')) + ->description(trans('admin.list')) + ->body($this->grid()); + } + + /** + * @return Grid + */ + protected function grid() + { + $grid = new Grid(new OperationLog()); + + $grid->model()->orderBy('id', 'DESC'); + + $grid->id('ID')->sortable(); + $grid->user()->name('User'); + $grid->method()->display(function ($method) { + $color = array_get(OperationLog::$methodColors, $method, 'grey'); + + return "$method"; + }); + $grid->path()->label('info'); + $grid->ip()->label('primary'); + $grid->input()->display(function ($input) { + $input = json_decode($input, true); + $input = array_except($input, ['_pjax', '_token', '_method', '_previous_']); + if (empty($input)) { + return '{}'; + } + + return '
    '.json_encode($input, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE).'
    '; + }); + + $grid->created_at(trans('admin.created_at')); + + $grid->actions(function (Grid\Displayers\Actions $actions) { + $actions->disableEdit(); + $actions->disableView(); + }); + + $grid->disableCreation(); + + $grid->filter(function ($filter) { + $userModel = config('admin.database.users_model'); + + $filter->equal('user_id', 'User')->select($userModel::all()->pluck('name', 'id')); + $filter->equal('method')->select(array_combine(OperationLog::$methods, OperationLog::$methods)); + $filter->like('path'); + $filter->equal('ip'); + }); + + return $grid; + } + + /** + * @param mixed $id + * + * @return \Illuminate\Http\JsonResponse + */ + public function destroy($id) + { + $ids = explode(',', $id); + + if (OperationLog::destroy(array_filter($ids))) { + $data = [ + 'status' => true, + 'message' => trans('admin.delete_succeeded'), + ]; + } else { + $data = [ + 'status' => false, + 'message' => trans('admin.delete_failed'), + ]; + } + + return response()->json($data); + } +} diff --git a/src/Controllers/MenuController.php b/src/Controllers/MenuController.php new file mode 100644 index 0000000..0431db7 --- /dev/null +++ b/src/Controllers/MenuController.php @@ -0,0 +1,150 @@ +header(trans('admin.menu')) + ->description(trans('admin.list')) + ->row(function (Row $row) { + $row->column(6, $this->treeView()->render()); + + $row->column(6, function (Column $column) { + $form = new \Encore\Admin\Widgets\Form(); + $form->action(admin_base_path('auth/menu')); + + $menuModel = config('admin.database.menu_model'); + $permissionModel = config('admin.database.permissions_model'); + $roleModel = config('admin.database.roles_model'); + + $form->select('parent_id', trans('admin.parent_id'))->options($menuModel::selectOptions()); + $form->text('title', trans('admin.title'))->rules('required'); + $form->icon('icon', trans('admin.icon'))->default('fa-bars')->rules('required')->help($this->iconHelp()); + $form->text('uri', trans('admin.uri')); + $form->multipleSelect('roles', trans('admin.roles'))->options($roleModel::all()->pluck('name', 'id')); + if ((new $menuModel())->withPermission()) { + $form->select('permission', trans('admin.permission'))->options($permissionModel::pluck('name', 'slug')); + } + $form->hidden('_token')->default(csrf_token()); + + $column->append((new Box(trans('admin.new'), $form))->style('success')); + }); + }); + } + + /** + * Redirect to edit page. + * + * @param int $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function show($id) + { + return redirect()->route('menu.edit', ['id' => $id]); + } + + /** + * @return \Encore\Admin\Tree + */ + protected function treeView() + { + $menuModel = config('admin.database.menu_model'); + + return $menuModel::tree(function (Tree $tree) { + $tree->disableCreate(); + + $tree->branch(function ($branch) { + $payload = " {$branch['title']}"; + + if (!isset($branch['children'])) { + if (url()->isValidUrl($branch['uri'])) { + $uri = $branch['uri']; + } else { + $uri = admin_base_path($branch['uri']); + } + + $payload .= "   $uri"; + } + + return $payload; + }); + }); + } + + /** + * Edit interface. + * + * @param string $id + * @param Content $content + * + * @return Content + */ + public function edit($id, Content $content) + { + return $content + ->header(trans('admin.menu')) + ->description(trans('admin.edit')) + ->row($this->form()->edit($id)); + } + + /** + * Make a form builder. + * + * @return Form + */ + public function form() + { + $menuModel = config('admin.database.menu_model'); + $permissionModel = config('admin.database.permissions_model'); + $roleModel = config('admin.database.roles_model'); + + $form = new Form(new $menuModel()); + + $form->display('id', 'ID'); + + $form->select('parent_id', trans('admin.parent_id'))->options($menuModel::selectOptions()); + $form->text('title', trans('admin.title'))->rules('required'); + $form->icon('icon', trans('admin.icon'))->default('fa-bars')->rules('required')->help($this->iconHelp()); + $form->text('uri', trans('admin.uri')); + $form->multipleSelect('roles', trans('admin.roles'))->options($roleModel::all()->pluck('name', 'id')); + if ($form->model()->withPermission()) { + $form->select('permission', trans('admin.permission'))->options($permissionModel::pluck('name', 'slug')); + } + + $form->display('created_at', trans('admin.created_at')); + $form->display('updated_at', trans('admin.updated_at')); + + return $form; + } + + /** + * Help message for icon field. + * + * @return string + */ + protected function iconHelp() + { + return 'For more icons please see http://fontawesome.io/icons/'; + } +} diff --git a/src/Controllers/ModelForm.php b/src/Controllers/ModelForm.php new file mode 100644 index 0000000..d88ffe8 --- /dev/null +++ b/src/Controllers/ModelForm.php @@ -0,0 +1,13 @@ +header(trans('admin.permissions')) + ->description(trans('admin.list')) + ->body($this->grid()->render()); + } + + /** + * Show interface. + * + * @param mixed $id + * @param Content $content + * + * @return Content + */ + public function show($id, Content $content) + { + return $content + ->header(trans('admin.permissions')) + ->description(trans('admin.detail')) + ->body($this->detail($id)); + } + + /** + * Edit interface. + * + * @param $id + * @param Content $content + * + * @return Content + */ + public function edit($id, Content $content) + { + return $content + ->header(trans('admin.permissions')) + ->description(trans('admin.edit')) + ->body($this->form()->edit($id)); + } + + /** + * Create interface. + * + * @param Content $content + * + * @return Content + */ + public function create(Content $content) + { + return $content + ->header(trans('admin.permissions')) + ->description(trans('admin.create')) + ->body($this->form()); + } + + /** + * Make a grid builder. + * + * @return Grid + */ + protected function grid() + { + $permissionModel = config('admin.database.permissions_model'); + + $grid = new Grid(new $permissionModel()); + + $grid->id('ID')->sortable(); + $grid->slug(trans('admin.slug')); + $grid->name(trans('admin.name')); + + $grid->http_path(trans('admin.route'))->display(function ($path) { + return collect(explode("\r\n", $path))->map(function ($path) { + $method = $this->http_method ?: ['ANY']; + + if (Str::contains($path, ':')) { + list($method, $path) = explode(':', $path); + $method = explode(',', $method); + } + + $method = collect($method)->map(function ($name) { + return strtoupper($name); + })->map(function ($name) { + return "{$name}"; + })->implode(' '); + + if (!empty(config('admin.route.prefix'))) { + $path = '/'.trim(config('admin.route.prefix'), '/').$path; + } + + return "
    $method$path
    "; + })->implode(''); + }); + + $grid->created_at(trans('admin.created_at')); + $grid->updated_at(trans('admin.updated_at')); + + $grid->tools(function (Grid\Tools $tools) { + $tools->batch(function (Grid\Tools\BatchActions $actions) { + $actions->disableDelete(); + }); + }); + + return $grid; + } + + /** + * Make a show builder. + * + * @param mixed $id + * + * @return Show + */ + protected function detail($id) + { + $permissionModel = config('admin.database.permissions_model'); + + $show = new Show($permissionModel::findOrFail($id)); + + $show->id('ID'); + $show->slug(trans('admin.slug')); + $show->name(trans('admin.name')); + + $show->http_path(trans('admin.route'))->as(function ($path) { + return collect(explode("\r\n", $path))->map(function ($path) { + $method = $this->http_method ?: ['ANY']; + + if (Str::contains($path, ':')) { + list($method, $path) = explode(':', $path); + $method = explode(',', $method); + } + + $method = collect($method)->map(function ($name) { + return strtoupper($name); + })->map(function ($name) { + return "{$name}"; + })->implode(' '); + + if (!empty(config('admin.route.prefix'))) { + $path = '/'.trim(config('admin.route.prefix'), '/').$path; + } + + return "
    $method$path
    "; + })->implode(''); + }); + + $show->created_at(trans('admin.created_at')); + $show->updated_at(trans('admin.updated_at')); + + return $show; + } + + /** + * Make a form builder. + * + * @return Form + */ + public function form() + { + $permissionModel = config('admin.database.permissions_model'); + + $form = new Form(new $permissionModel()); + + $form->display('id', 'ID'); + + $form->text('slug', trans('admin.slug'))->rules('required'); + $form->text('name', trans('admin.name'))->rules('required'); + + $form->multipleSelect('http_method', trans('admin.http.method')) + ->options($this->getHttpMethodsOptions()) + ->help(trans('admin.all_methods_if_empty')); + $form->textarea('http_path', trans('admin.http.path')); + + $form->display('created_at', trans('admin.created_at')); + $form->display('updated_at', trans('admin.updated_at')); + + return $form; + } + + /** + * Get options of HTTP methods select field. + * + * @return array + */ + protected function getHttpMethodsOptions() + { + $permissionModel = config('admin.database.permissions_model'); + + return array_combine($permissionModel::$httpMethods, $permissionModel::$httpMethods); + } +} diff --git a/src/Controllers/RoleController.php b/src/Controllers/RoleController.php new file mode 100644 index 0000000..1eb44ec --- /dev/null +++ b/src/Controllers/RoleController.php @@ -0,0 +1,160 @@ +header(trans('admin.roles')) + ->description(trans('admin.list')) + ->body($this->grid()->render()); + } + + /** + * Show interface. + * + * @param mixed $id + * @param Content $content + * + * @return Content + */ + public function show($id, Content $content) + { + return $content + ->header(trans('admin.roles')) + ->description(trans('admin.detail')) + ->body($this->detail($id)); + } + + /** + * Edit interface. + * + * @param mixed $id + * @param Content $content + * + * @return Content + */ + public function edit($id, Content $content) + { + return $content + ->header(trans('admin.roles')) + ->description(trans('admin.edit')) + ->body($this->form()->edit($id)); + } + + /** + * Create interface. + * + * @param Content $content + * + * @return Content + */ + public function create(Content $content) + { + return $content + ->header(trans('admin.roles')) + ->description(trans('admin.create')) + ->body($this->form()); + } + + /** + * Make a grid builder. + * + * @return Grid + */ + protected function grid() + { + $roleModel = config('admin.database.roles_model'); + + $grid = new Grid(new $roleModel()); + + $grid->id('ID')->sortable(); + $grid->slug(trans('admin.slug')); + $grid->name(trans('admin.name')); + + $grid->permissions(trans('admin.permission'))->pluck('name')->label(); + + $grid->created_at(trans('admin.created_at')); + $grid->updated_at(trans('admin.updated_at')); + + $grid->actions(function (Grid\Displayers\Actions $actions) { + if ($actions->row->slug == 'administrator') { + $actions->disableDelete(); + } + }); + + $grid->tools(function (Grid\Tools $tools) { + $tools->batch(function (Grid\Tools\BatchActions $actions) { + $actions->disableDelete(); + }); + }); + + return $grid; + } + + /** + * Make a show builder. + * + * @param mixed $id + * + * @return Show + */ + protected function detail($id) + { + $roleModel = config('admin.database.roles_model'); + + $show = new Show($roleModel::findOrFail($id)); + + $show->id('ID'); + $show->slug(trans('admin.slug')); + $show->name(trans('admin.name')); + $show->permissions(trans('admin.permissions'))->as(function ($permission) { + return $permission->pluck('name'); + })->label(); + $show->created_at(trans('admin.created_at')); + $show->updated_at(trans('admin.updated_at')); + + return $show; + } + + /** + * Make a form builder. + * + * @return Form + */ + public function form() + { + $permissionModel = config('admin.database.permissions_model'); + $roleModel = config('admin.database.roles_model'); + + $form = new Form(new $roleModel()); + + $form->display('id', 'ID'); + + $form->text('slug', trans('admin.slug'))->rules('required'); + $form->text('name', trans('admin.name'))->rules('required'); + $form->listbox('permissions', trans('admin.permissions'))->options($permissionModel::all()->pluck('name', 'id')); + + $form->display('created_at', trans('admin.created_at')); + $form->display('updated_at', trans('admin.updated_at')); + + return $form; + } +} diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php new file mode 100644 index 0000000..e369a7e --- /dev/null +++ b/src/Controllers/UserController.php @@ -0,0 +1,173 @@ +header(trans('admin.administrator')) + ->description(trans('admin.list')) + ->body($this->grid()->render()); + } + + /** + * Show interface. + * + * @param mixed $id + * @param Content $content + * + * @return Content + */ + public function show($id, Content $content) + { + return $content + ->header(trans('admin.administrator')) + ->description(trans('admin.detail')) + ->body($this->detail($id)); + } + + /** + * Edit interface. + * + * @param $id + * + * @return Content + */ + public function edit($id, Content $content) + { + return $content + ->header(trans('admin.administrator')) + ->description(trans('admin.edit')) + ->body($this->form()->edit($id)); + } + + /** + * Create interface. + * + * @return Content + */ + public function create(Content $content) + { + return $content + ->header(trans('admin.administrator')) + ->description(trans('admin.create')) + ->body($this->form()); + } + + /** + * Make a grid builder. + * + * @return Grid + */ + protected function grid() + { + $userModel = config('admin.database.users_model'); + + $grid = new Grid(new $userModel()); + + $grid->id('ID')->sortable(); + $grid->username(trans('admin.username')); + $grid->name(trans('admin.name')); + $grid->roles(trans('admin.roles'))->pluck('name')->label(); + $grid->created_at(trans('admin.created_at')); + $grid->updated_at(trans('admin.updated_at')); + + $grid->actions(function (Grid\Displayers\Actions $actions) { + if ($actions->getKey() == 1) { + $actions->disableDelete(); + } + }); + + $grid->tools(function (Grid\Tools $tools) { + $tools->batch(function (Grid\Tools\BatchActions $actions) { + $actions->disableDelete(); + }); + }); + + return $grid; + } + + /** + * Make a show builder. + * + * @param mixed $id + * + * @return Show + */ + protected function detail($id) + { + $userModel = config('admin.database.users_model'); + + $show = new Show($userModel::findOrFail($id)); + + $show->id('ID'); + $show->username(trans('admin.username')); + $show->name(trans('admin.name')); + $show->roles(trans('admin.roles'))->as(function ($roles) { + return $roles->pluck('name'); + })->label(); + $show->permissions(trans('admin.permissions'))->as(function ($permission) { + return $permission->pluck('name'); + })->label(); + $show->created_at(trans('admin.created_at')); + $show->updated_at(trans('admin.updated_at')); + + return $show; + } + + /** + * Make a form builder. + * + * @return Form + */ + public function form() + { + $userModel = config('admin.database.users_model'); + $permissionModel = config('admin.database.permissions_model'); + $roleModel = config('admin.database.roles_model'); + + $form = new Form(new $userModel()); + + $form->display('id', 'ID'); + + $form->text('username', trans('admin.username'))->rules('required'); + $form->text('name', trans('admin.name'))->rules('required'); + $form->image('avatar', trans('admin.avatar')); + $form->password('password', trans('admin.password'))->rules('required|confirmed'); + $form->password('password_confirmation', trans('admin.password_confirmation'))->rules('required') + ->default(function ($form) { + return $form->model()->password; + }); + + $form->ignore(['password_confirmation']); + + $form->multipleSelect('roles', trans('admin.roles'))->options($roleModel::all()->pluck('name', 'id')); + $form->multipleSelect('permissions', trans('admin.permissions'))->options($permissionModel::all()->pluck('name', 'id')); + + $form->display('created_at', trans('admin.created_at')); + $form->display('updated_at', trans('admin.updated_at')); + + $form->saving(function (Form $form) { + if ($form->password && $form->model()->password != $form->password) { + $form->password = bcrypt($form->password); + } + }); + + return $form; + } +} diff --git a/src/Exception/Handler.php b/src/Exception/Handler.php new file mode 100644 index 0000000..361f0b9 --- /dev/null +++ b/src/Exception/Handler.php @@ -0,0 +1,46 @@ + get_class($exception), + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]); + + $errors = new ViewErrorBag(); + $errors->put('exception', $error); + + return view('admin::partials.exception', compact('errors'))->render(); + } + + /** + * Flash a error message to content. + * + * @param string $title + * @param string $message + * + * @return mixed + */ + public static function error($title = '', $message = '') + { + $error = new MessageBag(compact('title', 'message')); + + return session()->flash('error', $error); + } +} diff --git a/src/Extension.php b/src/Extension.php new file mode 100644 index 0000000..cce33ff --- /dev/null +++ b/src/Extension.php @@ -0,0 +1,352 @@ + 'required', + 'path' => 'required', + 'icon' => 'required', + ]; + + /** + * The permission validation rules. + * + * @var array + */ + protected $permissionValidationRules = [ + 'name' => 'required', + 'slug' => 'required', + 'path' => 'required', + ]; + + /** + * Returns the singleton instance. + * + * @return self + */ + protected static function getInstance() + { + $class = get_called_class(); + + if (!isset(self::$instance[$class]) || !self::$instance[$class] instanceof $class) { + self::$instance[$class] = new static(); + } + + return static::$instance[$class]; + } + + /** + * Bootstrap this extension. + */ + public static function boot() + { + $extension = static::getInstance(); + + Admin::extend($extension->name, get_called_class()); + + if ($extension->disabled()) { + return false; + } + + if (!empty($css = $extension->css())) { + Admin::css($css); + } + + if (!empty($js = $extension->js())) { + Admin::js($js); + } + + return true; + } + + /** + * Get the path of assets files. + * + * @return string + */ + public function assets() + { + return $this->assets; + } + + /** + * Get the paths of css files. + * + * @return array + */ + public function css() + { + return $this->css; + } + + /** + * Get the paths of js files. + * + * @return array + */ + public function js() + { + return $this->js; + } + + /** + * Get the path of view files. + * + * @return string + */ + public function views() + { + return $this->views; + } + + /** + * Get the path of migration files. + * + * @return string + */ + public function migrations() + { + return $this->migrations; + } + + /** + * @return array + */ + public function menu() + { + return $this->menu; + } + + /** + * @return array + */ + public function permission() + { + return $this->permission; + } + + /** + * Whether the extension is enabled. + * + * @return bool + */ + public function enabled() + { + return static::config('enable') !== false; + } + + /** + * Whether the extension is disabled. + * + * @return bool + */ + public function disabled() + { + return !$this->enabled(); + } + + /** + * Get config set in config/admin.php. + * + * @param string $key + * @param null $default + * + * @return \Illuminate\Config\Repository|mixed + */ + public static function config($key = null, $default = null) + { + $name = array_search(get_called_class(), Admin::$extensions); + + if (is_null($key)) { + $key = sprintf('admin.extensions.%s', strtolower($name)); + } else { + $key = sprintf('admin.extensions.%s.%s', strtolower($name), $key); + } + + return config($key, $default); + } + + /** + * Import menu item and permission to laravel-admin. + */ + public static function import() + { + $extension = static::getInstance(); + + if ($menu = $extension->menu()) { + if ($extension->validateMenu($menu)) { + extract($menu); + static::createMenu($title, $path, $icon); + } + } + + if ($permission = $extension->permission()) { + if ($extension->validatePermission($permission)) { + extract($permission); + static::createPermission($name, $slug, $path); + } + } + } + + /** + * Validate menu fields. + * + * @param array $menu + * + * @throws \Exception + * + * @return bool + */ + public function validateMenu(array $menu) + { + /** @var \Illuminate\Validation\Validator $validator */ + $validator = Validator::make($menu, $this->menuValidationRules); + + if ($validator->passes()) { + return true; + } + + $message = "Invalid menu:\r\n".implode("\r\n", array_flatten($validator->errors()->messages())); + + throw new \Exception($message); + } + + /** + * Validate permission fields. + * + * @param array $permission + * + * @throws \Exception + * + * @return bool + */ + public function validatePermission(array $permission) + { + /** @var \Illuminate\Validation\Validator $validator */ + $validator = Validator::make($permission, $this->permissionValidationRules); + + if ($validator->passes()) { + return true; + } + + $message = "Invalid permission:\r\n".implode("\r\n", array_flatten($validator->errors()->messages())); + + throw new \Exception($message); + } + + /** + * Create a item in laravel-admin left side menu. + * + * @param string $title + * @param string $uri + * @param string $icon + * @param int $parentId + */ + protected static function createMenu($title, $uri, $icon = 'fa-bars', $parentId = 0) + { + $menuModel = config('admin.database.menu_model'); + + $lastOrder = $menuModel::max('order'); + + $menuModel::create([ + 'parent_id' => $parentId, + 'order' => $lastOrder + 1, + 'title' => $title, + 'icon' => $icon, + 'uri' => $uri, + ]); + } + + /** + * Create a permission for this extension. + * + * @param $name + * @param $slug + * @param $path + */ + protected static function createPermission($name, $slug, $path) + { + $permissionModel = config('admin.database.permissions_model'); + + $permissionModel::create([ + 'name' => $name, + 'slug' => $slug, + 'http_path' => '/'.trim($path, '/'), + ]); + } + + /** + * Set routes for this extension. + * + * @param $callback + */ + public static function routes($callback) + { + $attributes = array_merge( + [ + 'prefix' => config('admin.route.prefix'), + 'middleware' => config('admin.route.middleware'), + ], + static::config('route', []) + ); + + Route::group($attributes, $callback); + } +} diff --git a/src/Facades/Admin.php b/src/Facades/Admin.php new file mode 100644 index 0000000..8420f08 --- /dev/null +++ b/src/Facades/Admin.php @@ -0,0 +1,31 @@ +model = $model; + + $this->builder = new Builder($this); + + if ($callback instanceof Closure) { + $callback($this); + } + + $this->isSoftDeletes = in_array(SoftDeletes::class, class_uses($this->model)); + } + + /** + * @param Field $field + * + * @return $this + */ + public function pushField(Field $field) + { + $field->setForm($this); + + $this->builder->fields()->push($field); + + return $this; + } + + /** + * @return Model + */ + public function model() + { + return $this->model; + } + + /** + * @return Builder + */ + public function builder() + { + return $this->builder; + } + + /** + * Generate a edit form. + * + * @param $id + * + * @return $this + */ + public function edit($id) + { + $this->builder->setMode(Builder::MODE_EDIT); + $this->builder->setResourceId($id); + + $this->setFieldValue($id); + + return $this; + } + + /** + * Use tab to split form. + * + * @param string $title + * @param Closure $content + * + * @return $this + */ + public function tab($title, Closure $content, $active = false) + { + $this->getTab()->append($title, $content, $active); + + return $this; + } + + /** + * Get Tab instance. + * + * @return Tab + */ + public function getTab() + { + if (is_null($this->tab)) { + $this->tab = new Tab($this); + } + + return $this->tab; + } + + /** + * Destroy data entity and remove files. + * + * @param $id + * + * @return mixed + */ + public function destroy($id) + { + collect(explode(',', $id))->filter()->each(function ($id) { + $builder = $this->model()->newQuery(); + + if ($this->isSoftDeletes) { + $builder = $builder->withTrashed(); + } + + $model = $builder->with($this->getRelations())->findOrFail($id); + + if ($this->isSoftDeletes && $model->trashed()) { + $this->deleteFiles($model, true); + $model->forceDelete(); + + return; + } + + $this->deleteFiles($model); + $model->delete(); + }); + + return true; + } + + /** + * Remove files in record. + * + * @param Model $model + * @param bool $forceDelete + */ + protected function deleteFiles(Model $model, $forceDelete = false) + { + // If it's a soft delete, the files in the data will not be deleted. + if (!$forceDelete && $this->isSoftDeletes) { + return; + } + + $data = $model->toArray(); + + $this->builder->fields()->filter(function ($field) { + return $field instanceof Field\File; + })->each(function (Field\File $file) use ($data) { + $file->setOriginal($data); + + $file->destroy(); + }); + } + + /** + * Store a new record. + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse + */ + public function store() + { + $data = Input::all(); + + // Handle validation errors. + if ($validationMessages = $this->validationMessages($data)) { + return back()->withInput()->withErrors($validationMessages); + } + + if (($response = $this->prepare($data)) instanceof Response) { + return $response; + } + + DB::transaction(function () { + $inserts = $this->prepareInsert($this->updates); + + foreach ($inserts as $column => $value) { + $this->model->setAttribute($column, $value); + } + + $this->model->save(); + + $this->updateRelation($this->relations); + }); + + if (($response = $this->callSaved()) instanceof Response) { + return $response; + } + + if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) { + return $response; + } + + return $this->redirectAfterStore(); + } + + /** + * Get ajax response. + * + * @param string $message + * + * @return bool|\Illuminate\Http\JsonResponse + */ + protected function ajaxResponse($message) + { + $request = Request::capture(); + + // ajax but not pjax + if ($request->ajax() && !$request->pjax()) { + return response()->json([ + 'status' => true, + 'message' => $message, + ]); + } + + return false; + } + + /** + * Prepare input data for insert or update. + * + * @param array $data + * + * @return mixed + */ + protected function prepare($data = []) + { + if (($response = $this->callSubmitted()) instanceof Response) { + return $response; + } + + $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs); + + if (($response = $this->callSaving()) instanceof Response) { + return $response; + } + + $this->relations = $this->getRelationInputs($this->inputs); + + $this->updates = array_except($this->inputs, array_keys($this->relations)); + } + + /** + * Remove ignored fields from input. + * + * @param array $input + * + * @return array + */ + protected function removeIgnoredFields($input) + { + array_forget($input, $this->ignored); + + return $input; + } + + /** + * Get inputs for relations. + * + * @param array $inputs + * + * @return array + */ + protected function getRelationInputs($inputs = []) + { + $relations = []; + + foreach ($inputs as $column => $value) { + if (method_exists($this->model, $column)) { + $relation = call_user_func([$this->model, $column]); + + if ($relation instanceof Relations\Relation) { + $relations[$column] = $value; + } + } + } + + return $relations; + } + + /** + * Call editing callbacks. + * + * @return void + */ + protected function callEditing() + { + foreach ($this->editing as $func) { + call_user_func($func, $this); + } + } + + /** + * Call submitted callback. + * + * @return mixed + */ + protected function callSubmitted() + { + foreach ($this->submitted as $func) { + if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) { + return $ret; + } + } + } + + /** + * Call saving callback. + * + * @return mixed + */ + protected function callSaving() + { + foreach ($this->saving as $func) { + if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) { + return $ret; + } + } + } + + /** + * Callback after saving a Model. + * + * @return mixed|null + */ + protected function callSaved() + { + foreach ($this->saved as $func) { + if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) { + return $ret; + } + } + } + + /** + * Handle update. + * + * @param int $id + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function update($id, $data = null) + { + $data = ($data) ?: Input::all(); + + $isEditable = $this->isEditable($data); + + $data = $this->handleEditable($data); + + $data = $this->handleFileDelete($data); + + if ($this->handleOrderable($id, $data)) { + return response([ + 'status' => true, + 'message' => trans('admin.update_succeeded'), + ]); + } + + /* @var Model $this->model */ + $this->model = $this->model->with($this->getRelations())->findOrFail($id); + + $this->setFieldOriginalValue(); + + // Handle validation errors. + if ($validationMessages = $this->validationMessages($data)) { + if (!$isEditable) { + return back()->withInput()->withErrors($validationMessages); + } else { + return response()->json(['errors' => array_dot($validationMessages->getMessages())], 422); + } + } + + if (($response = $this->prepare($data)) instanceof Response) { + return $response; + } + + DB::transaction(function () { + $updates = $this->prepareUpdate($this->updates); + + foreach ($updates as $column => $value) { + /* @var Model $this->model */ + $this->model->setAttribute($column, $value); + } + + $this->model->save(); + + $this->updateRelation($this->relations); + }); + + if (($result = $this->callSaved()) instanceof Response) { + return $result; + } + + if ($response = $this->ajaxResponse(trans('admin.update_succeeded'))) { + return $response; + } + + return $this->redirectAfterUpdate($id); + } + + /** + * Get RedirectResponse after store. + * + * @return \Illuminate\Http\RedirectResponse + */ + protected function redirectAfterStore() + { + $resourcesPath = $this->resource(0); + + $key = $this->model->getKey(); + + return $this->redirectAfterSaving($resourcesPath, $key); + } + + /** + * Get RedirectResponse after update. + * + * @param mixed $key + * + * @return \Illuminate\Http\RedirectResponse + */ + protected function redirectAfterUpdate($key) + { + $resourcesPath = $this->resource(-1); + + return $this->redirectAfterSaving($resourcesPath, $key); + } + + /** + * Get RedirectResponse after data saving. + * + * @param string $resourcesPath + * @param string $key + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + protected function redirectAfterSaving($resourcesPath, $key) + { + if (request('after-save') == 1) { + // continue editing + $url = rtrim($resourcesPath, '/')."/{$key}/edit"; + } elseif (request('after-save') == 2) { + // continue creating + $url = rtrim($resourcesPath, '/').'/create'; + } elseif (request('after-save') == 3) { + // view resource + $url = rtrim($resourcesPath, '/')."/{$key}"; + } else { + $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath; + } + + admin_toastr(trans('admin.save_succeeded')); + + return redirect($url); + } + + /** + * Check if request is from editable. + * + * @param array $input + * + * @return bool + */ + protected function isEditable(array $input = []) + { + return array_key_exists('_editable', $input); + } + + /** + * Handle editable update. + * + * @param array $input + * + * @return array + */ + protected function handleEditable(array $input = []) + { + if (array_key_exists('_editable', $input)) { + $name = $input['name']; + $value = $input['value']; + + array_forget($input, ['pk', 'value', 'name']); + array_set($input, $name, $value); + } + + return $input; + } + + /** + * @param array $input + * + * @return array + */ + protected function handleFileDelete(array $input = []) + { + if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) { + $input[Field::FILE_DELETE_FLAG] = $input['key']; + unset($input['key']); + } + + Input::replace($input); + + return $input; + } + + /** + * Handle orderable update. + * + * @param int $id + * @param array $input + * + * @return bool + */ + protected function handleOrderable($id, array $input = []) + { + if (array_key_exists('_orderable', $input)) { + $model = $this->model->find($id); + + if ($model instanceof Sortable) { + $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown(); + + return true; + } + } + + return false; + } + + /** + * Update relation data. + * + * @param array $relationsData + * + * @return void + */ + protected function updateRelation($relationsData) + { + foreach ($relationsData as $name => $values) { + if (!method_exists($this->model, $name)) { + continue; + } + + $relation = $this->model->$name(); + + $oneToOneRelation = $relation instanceof Relations\HasOne + || $relation instanceof Relations\MorphOne + || $relation instanceof Relations\BelongsTo; + + $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation); + + if (empty($prepared)) { + continue; + } + + switch (true) { + case $relation instanceof Relations\BelongsToMany: + case $relation instanceof Relations\MorphToMany: + if (isset($prepared[$name])) { + $relation->sync($prepared[$name]); + } + break; + case $relation instanceof Relations\HasOne: + + $related = $this->model->$name; + + // if related is empty + if (is_null($related)) { + $related = $relation->getRelated(); + $qualifiedParentKeyName = $relation->getQualifiedParentKeyName(); + $localKey = array_last(explode('.', $qualifiedParentKeyName)); + $related->{$relation->getForeignKeyName()} = $this->model->{$localKey}; + } + + foreach ($prepared[$name] as $column => $value) { + $related->setAttribute($column, $value); + } + + $related->save(); + break; + case $relation instanceof Relations\BelongsTo: + + $parent = $this->model->$name; + + // if related is empty + if (is_null($parent)) { + $parent = $relation->getRelated(); + } + + foreach ($prepared[$name] as $column => $value) { + $parent->setAttribute($column, $value); + } + + $parent->save(); + + // When in creating, associate two models + if (!$this->model->{$relation->getForeignKey()}) { + $this->model->{$relation->getForeignKey()} = $parent->getKey(); + + $this->model->save(); + } + + break; + case $relation instanceof Relations\MorphOne: + $related = $this->model->$name; + if (is_null($related)) { + $related = $relation->make(); + } + foreach ($prepared[$name] as $column => $value) { + $related->setAttribute($column, $value); + } + $related->save(); + break; + case $relation instanceof Relations\HasMany: + case $relation instanceof Relations\MorphMany: + + foreach ($prepared[$name] as $related) { + /** @var Relations\Relation $relation */ + $relation = $this->model()->$name(); + + $keyName = $relation->getRelated()->getKeyName(); + + $instance = $relation->findOrNew(array_get($related, $keyName)); + + if ($related[static::REMOVE_FLAG_NAME] == 1) { + $instance->delete(); + + continue; + } + + array_forget($related, static::REMOVE_FLAG_NAME); + + $instance->fill($related); + + $instance->save(); + } + + break; + } + } + } + + /** + * Prepare input data for update. + * + * @param array $updates + * @param bool $oneToOneRelation If column is one-to-one relation. + * + * @return array + */ + protected function prepareUpdate(array $updates, $oneToOneRelation = false) + { + $prepared = []; + + /** @var Field $field */ + foreach ($this->builder->fields() as $field) { + $columns = $field->column(); + + // If column not in input array data, then continue. + if (!array_has($updates, $columns)) { + continue; + } + + if ($this->invalidColumn($columns, $oneToOneRelation)) { + continue; + } + + $value = $this->getDataByColumn($updates, $columns); + + $value = $field->prepare($value); + + if (is_array($columns)) { + foreach ($columns as $name => $column) { + array_set($prepared, $column, $value[$name]); + } + } elseif (is_string($columns)) { + array_set($prepared, $columns, $value); + } + } + + return $prepared; + } + + /** + * @param string|array $columns + * @param bool $oneToOneRelation + * + * @return bool + */ + protected function invalidColumn($columns, $oneToOneRelation = false) + { + foreach ((array) $columns as $column) { + if ((!$oneToOneRelation && Str::contains($column, '.')) || + ($oneToOneRelation && !Str::contains($column, '.'))) { + return true; + } + } + + return false; + } + + /** + * Prepare input data for insert. + * + * @param $inserts + * + * @return array + */ + protected function prepareInsert($inserts) + { + if ($this->isHasOneRelation($inserts)) { + $inserts = array_dot($inserts); + } + + foreach ($inserts as $column => $value) { + if (is_null($field = $this->getFieldByColumn($column))) { + unset($inserts[$column]); + continue; + } + + $inserts[$column] = $field->prepare($value); + } + + $prepared = []; + + foreach ($inserts as $key => $value) { + array_set($prepared, $key, $value); + } + + return $prepared; + } + + /** + * Is input data is has-one relation. + * + * @param array $inserts + * + * @return bool + */ + protected function isHasOneRelation($inserts) + { + $first = current($inserts); + + if (!is_array($first)) { + return false; + } + + if (is_array(current($first))) { + return false; + } + + return Arr::isAssoc($first); + } + + /** + * Set after getting editing model callback. + * + * @param Closure $callback + * + * @return void + */ + public function editing(Closure $callback) + { + $this->editing[] = $callback; + } + + /** + * Set submitted callback. + * + * @param Closure $callback + * + * @return void + */ + public function submitted(Closure $callback) + { + $this->submitted[] = $callback; + } + + /** + * Set saving callback. + * + * @param Closure $callback + * + * @return void + */ + public function saving(Closure $callback) + { + $this->saving[] = $callback; + } + + /** + * Set saved callback. + * + * @param Closure $callback + * + * @return void + */ + public function saved(Closure $callback) + { + $this->saved[] = $callback; + } + + /** + * Ignore fields to save. + * + * @param string|array $fields + * + * @return $this + */ + public function ignore($fields) + { + $this->ignored = array_merge($this->ignored, (array) $fields); + + return $this; + } + + /** + * @param array $data + * @param string|array $columns + * + * @return array|mixed + */ + protected function getDataByColumn($data, $columns) + { + if (is_string($columns)) { + return array_get($data, $columns); + } + + if (is_array($columns)) { + $value = []; + foreach ($columns as $name => $column) { + if (!array_has($data, $column)) { + continue; + } + $value[$name] = array_get($data, $column); + } + + return $value; + } + } + + /** + * Find field object by column. + * + * @param $column + * + * @return mixed + */ + protected function getFieldByColumn($column) + { + return $this->builder->fields()->first( + function (Field $field) use ($column) { + if (is_array($field->column())) { + return in_array($column, $field->column()); + } + + return $field->column() == $column; + } + ); + } + + /** + * Set original data for each field. + * + * @return void + */ + protected function setFieldOriginalValue() + { +// static::doNotSnakeAttributes($this->model); + + $values = $this->model->toArray(); + + $this->builder->fields()->each(function (Field $field) use ($values) { + $field->setOriginal($values); + }); + } + + /** + * Set all fields value in form. + * + * @param $id + * + * @return void + */ + protected function setFieldValue($id) + { + $relations = $this->getRelations(); + + $builder = $this->model(); + + if ($this->isSoftDeletes) { + $builder->withTrashed(); + } + + $this->model = $builder->with($relations)->findOrFail($id); + + $this->callEditing(); + +// static::doNotSnakeAttributes($this->model); + + $data = $this->model->toArray(); + + $this->builder->fields()->each(function (Field $field) use ($data) { + if (!in_array($field->column(), $this->ignored)) { + $field->fill($data); + } + }); + } + + /** + * Don't snake case attributes. + * + * @param Model $model + * + * @return void + */ + protected static function doNotSnakeAttributes(Model $model) + { + $class = get_class($model); + + $class::$snakeAttributes = false; + } + + /** + * Get validation messages. + * + * @param array $input + * + * @return MessageBag|bool + */ + public function validationMessages($input) + { + $failedValidators = []; + + /** @var Field $field */ + foreach ($this->builder->fields() as $field) { + if (!$validator = $field->getValidator($input)) { + continue; + } + + if (($validator instanceof Validator) && !$validator->passes()) { + $failedValidators[] = $validator; + } + } + + $message = $this->mergeValidationMessages($failedValidators); + + return $message->any() ? $message : false; + } + + /** + * Merge validation messages from input validators. + * + * @param \Illuminate\Validation\Validator[] $validators + * + * @return MessageBag + */ + protected function mergeValidationMessages($validators) + { + $messageBag = new MessageBag(); + + foreach ($validators as $validator) { + $messageBag = $messageBag->merge($validator->messages()); + } + + return $messageBag; + } + + /** + * Get all relations of model from callable. + * + * @return array + */ + public function getRelations() + { + $relations = $columns = []; + + /** @var Field $field */ + foreach ($this->builder->fields() as $field) { + $columns[] = $field->column(); + } + + foreach (array_flatten($columns) as $column) { + if (str_contains($column, '.')) { + list($relation) = explode('.', $column); + + if (method_exists($this->model, $relation) && + $this->model->$relation() instanceof Relations\Relation + ) { + $relations[] = $relation; + } + } elseif (method_exists($this->model, $column) && + !method_exists(Model::class, $column) + ) { + $relations[] = $column; + } + } + + return array_unique($relations); + } + + /** + * Set action for form. + * + * @param string $action + * + * @return $this + */ + public function setAction($action) + { + $this->builder()->setAction($action); + + return $this; + } + + /** + * Set field and label width in current form. + * + * @param int $fieldWidth + * @param int $labelWidth + * + * @return $this + */ + public function setWidth($fieldWidth = 8, $labelWidth = 2) + { + $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) { + /* @var Field $field */ + $field->setWidth($fieldWidth, $labelWidth); + }); + + $this->builder()->setWidth($fieldWidth, $labelWidth); + + return $this; + } + + /** + * Set view for form. + * + * @param string $view + * + * @return $this + */ + public function setView($view) + { + $this->builder()->setView($view); + + return $this; + } + + /** + * Set title for form. + * + * @param string $title + * + * @return $this + */ + public function setTitle($title = '') + { + $this->builder()->setTitle($title); + + return $this; + } + + /** + * Add a row in form. + * + * @param Closure $callback + * + * @return $this + */ + public function row(Closure $callback) + { + $this->rows[] = new Row($callback, $this); + + return $this; + } + + /** + * Tools setting for form. + * + * @param Closure $callback + */ + public function tools(Closure $callback) + { + $callback->call($this, $this->builder->getTools()); + } + + /** + * Disable form submit. + * + * @return $this + * + * @deprecated + */ + public function disableSubmit() + { + $this->builder()->getFooter()->disableSubmit(); + + return $this; + } + + /** + * Disable form reset. + * + * @return $this + * + * @deprecated + */ + public function disableReset() + { + $this->builder()->getFooter()->disableReset(); + + return $this; + } + + /** + * Disable View Checkbox on footer. + * + * @return $this + */ + public function disableViewCheck() + { + $this->builder()->getFooter()->disableViewCheck(); + + return $this; + } + + /** + * Disable Editing Checkbox on footer. + * + * @return $this + */ + public function disableEditingCheck() + { + $this->builder()->getFooter()->disableEditingCheck(); + + return $this; + } + + /** + * Disable Creating Checkbox on footer. + * + * @return $this + */ + public function disableCreatingCheck() + { + $this->builder()->getFooter()->disableCreatingCheck(); + + return $this; + } + + /** + * Footer setting for form. + * + * @param Closure $callback + */ + public function footer(Closure $callback) + { + call_user_func($callback, $this->builder()->getFooter()); + } + + /** + * Get current resource route url. + * + * @param int $slice + * + * @return string + */ + public function resource($slice = -2) + { + $segments = explode('/', trim(app('request')->getUri(), '/')); + + if ($slice != 0) { + $segments = array_slice($segments, 0, $slice); + } + + return implode('/', $segments); + } + + /** + * Render the form contents. + * + * @return string + */ + public function render() + { + try { + return $this->builder->render(); + } catch (\Exception $e) { + return Handler::renderException($e); + } + } + + /** + * Get or set input data. + * + * @param string $key + * @param null $value + * + * @return array|mixed + */ + public function input($key, $value = null) + { + if (is_null($value)) { + return array_get($this->inputs, $key); + } + + return array_set($this->inputs, $key, $value); + } + + /** + * Register builtin fields. + * + * @return void + */ + public static function registerBuiltinFields() + { + $map = [ + 'button' => Field\Button::class, + 'checkbox' => Field\Checkbox::class, + 'color' => Field\Color::class, + 'currency' => Field\Currency::class, + 'date' => Field\Date::class, + 'dateRange' => Field\DateRange::class, + 'datetime' => Field\Datetime::class, + 'dateTimeRange' => Field\DatetimeRange::class, + 'datetimeRange' => Field\DatetimeRange::class, + 'decimal' => Field\Decimal::class, + 'display' => Field\Display::class, + 'divider' => Field\Divide::class, + 'divide' => Field\Divide::class, + 'embeds' => Field\Embeds::class, + 'editor' => Field\Editor::class, + 'email' => Field\Email::class, + 'file' => Field\File::class, + 'hasMany' => Field\HasMany::class, + 'hidden' => Field\Hidden::class, + 'id' => Field\Id::class, + 'image' => Field\Image::class, + 'ip' => Field\Ip::class, + 'map' => Field\Map::class, + 'mobile' => Field\Mobile::class, + 'month' => Field\Month::class, + 'multipleSelect' => Field\MultipleSelect::class, + 'number' => Field\Number::class, + 'password' => Field\Password::class, + 'radio' => Field\Radio::class, + 'rate' => Field\Rate::class, + 'select' => Field\Select::class, + 'slider' => Field\Slider::class, + 'switch' => Field\SwitchField::class, + 'text' => Field\Text::class, + 'textarea' => Field\Textarea::class, + 'time' => Field\Time::class, + 'timeRange' => Field\TimeRange::class, + 'url' => Field\Url::class, + 'year' => Field\Year::class, + 'html' => Field\Html::class, + 'tags' => Field\Tags::class, + 'icon' => Field\Icon::class, + 'multipleFile' => Field\MultipleFile::class, + 'multipleImage' => Field\MultipleImage::class, + 'captcha' => Field\Captcha::class, + 'listbox' => Field\Listbox::class, + ]; + + foreach ($map as $abstract => $class) { + static::extend($abstract, $class); + } + } + + /** + * Register custom field. + * + * @param string $abstract + * @param string $class + * + * @return void + */ + public static function extend($abstract, $class) + { + static::$availableFields[$abstract] = $class; + } + + /** + * Set form field alias. + * + * @param string $field + * @param string $alias + * + * @return void + */ + public static function alias($field, $alias) + { + static::$fieldAlias[$alias] = $field; + } + + /** + * Remove registered field. + * + * @param array|string $abstract + */ + public static function forget($abstract) + { + array_forget(static::$availableFields, $abstract); + } + + /** + * Find field class. + * + * @param string $method + * + * @return bool|mixed + */ + public static function findFieldClass($method) + { + // If alias exists. + if (isset(static::$fieldAlias[$method])) { + $method = static::$fieldAlias[$method]; + } + + $class = array_get(static::$availableFields, $method); + + if (class_exists($class)) { + return $class; + } + + return false; + } + + /** + * Collect assets required by registered field. + * + * @return array + */ + public static function collectFieldAssets() + { + if (!empty(static::$collectedAssets)) { + return static::$collectedAssets; + } + + $css = collect(); + $js = collect(); + + foreach (static::$availableFields as $field) { + if (!method_exists($field, 'getAssets')) { + continue; + } + + $assets = call_user_func([$field, 'getAssets']); + + $css->push(array_get($assets, 'css')); + $js->push(array_get($assets, 'js')); + } + + return static::$collectedAssets = [ + 'css' => $css->flatten()->unique()->filter()->toArray(), + 'js' => $js->flatten()->unique()->filter()->toArray(), + ]; + } + + /** + * Getter. + * + * @param string $name + * + * @return array|mixed + */ + public function __get($name) + { + return $this->input($name); + } + + /** + * Setter. + * + * @param string $name + * @param $value + */ + public function __set($name, $value) + { + return array_set($this->inputs, $name, $value); + } + + /** + * Generate a Field object and add to form builder if Field exists. + * + * @param string $method + * @param array $arguments + * + * @return Field + */ + public function __call($method, $arguments) + { + if ($className = static::findFieldClass($method)) { + $column = array_get($arguments, 0, ''); //[0]; + + $element = new $className($column, array_slice($arguments, 1)); + + $this->pushField($element); + + return $element; + } + + admin_error('Error', "Field type [$method] does not exist."); + + return new Field\Nullable(); + } +} diff --git a/src/Form/Builder.php b/src/Form/Builder.php new file mode 100644 index 0000000..7d38dd2 --- /dev/null +++ b/src/Form/Builder.php @@ -0,0 +1,604 @@ + 2, + 'field' => 8, + ]; + + /** + * View for this form. + * + * @var string + */ + protected $view = 'admin::form'; + + /** + * Form title. + * + * @var string + */ + protected $title; + + /** + * Builder constructor. + * + * @param Form $form + */ + public function __construct(Form $form) + { + $this->form = $form; + + $this->fields = new Collection(); + + $this->init(); + } + + /** + * Do initialize. + */ + public function init() + { + $this->tools = new Tools($this); + $this->footer = new Footer($this); + } + + /** + * Get form tools instance. + * + * @return Tools + */ + public function getTools() + { + return $this->tools; + } + + /** + * Get form footer instance. + * + * @return Footer + */ + public function getFooter() + { + return $this->footer; + } + + /** + * Set the builder mode. + * + * @param string $mode + * + * @return void + */ + public function setMode($mode = 'create') + { + $this->mode = $mode; + } + + /** + * @return string + */ + public function getMode() + { + return $this->mode; + } + + /** + * Returns builder is $mode. + * + * @param $mode + * + * @return bool + */ + public function isMode($mode) + { + return $this->mode == $mode; + } + + /** + * Check if is creating resource. + * + * @return bool + */ + public function isCreating() + { + return $this->isMode(static::MODE_CREATE); + } + + /** + * Check if is editing resource. + * + * @return bool + */ + public function isEditing() + { + return $this->isMode(static::MODE_EDIT); + } + + /** + * Set resource Id. + * + * @param $id + * + * @return void + */ + public function setResourceId($id) + { + $this->id = $id; + } + + /** + * Get Resource id. + * + * @return mixed + */ + public function getResourceId() + { + return $this->id; + } + + /** + * @return string + */ + public function getResource($slice = null) + { + if ($this->mode == self::MODE_CREATE) { + return $this->form->resource(-1); + } + if ($slice !== null) { + return $this->form->resource($slice); + } + + return $this->form->resource(); + } + + /** + * @param int $field + * @param int $label + * + * @return $this + */ + public function setWidth($field = 8, $label = 2) + { + $this->width = [ + 'label' => $label, + 'field' => $field, + ]; + + return $this; + } + + /** + * Get label and field width. + * + * @return array + */ + public function getWidth() + { + return $this->width; + } + + /** + * Set form action. + * + * @param string $action + */ + public function setAction($action) + { + $this->action = $action; + } + + /** + * Get Form action. + * + * @return string + */ + public function getAction() + { + if ($this->action) { + return $this->action; + } + + if ($this->isMode(static::MODE_EDIT)) { + return $this->form->resource().'/'.$this->id; + } + + if ($this->isMode(static::MODE_CREATE)) { + return $this->form->resource(-1); + } + + return ''; + } + + /** + * Set view for this form. + * + * @param string $view + * + * @return $this + */ + public function setView($view) + { + $this->view = $view; + + return $this; + } + + /** + * Set title for form. + * + * @param string $title + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * Get fields of this builder. + * + * @return Collection + */ + public function fields() + { + return $this->fields; + } + + /** + * Get specify field. + * + * @param string $name + * + * @return mixed + */ + public function field($name) + { + return $this->fields()->first(function (Field $field) use ($name) { + return $field->column() == $name; + }); + } + + /** + * If the parant form has rows. + * + * @return bool + */ + public function hasRows() + { + return !empty($this->form->rows); + } + + /** + * Get field rows of form. + * + * @return array + */ + public function getRows() + { + return $this->form->rows; + } + + /** + * @return array + */ + public function getHiddenFields() + { + return $this->hiddenFields; + } + + /** + * @param Field $field + * + * @return void + */ + public function addHiddenField(Field $field) + { + $this->hiddenFields[] = $field; + } + + /** + * Add or get options. + * + * @param array $options + * + * @return array|null + */ + public function options($options = []) + { + if (empty($options)) { + return $this->options; + } + + $this->options = array_merge($this->options, $options); + } + + /** + * Get or set option. + * + * @param string $option + * @param mixed $value + * + * @return $this + */ + public function option($option, $value = null) + { + if (func_num_args() == 1) { + return array_get($this->options, $option); + } + + $this->options[$option] = $value; + + return $this; + } + + /** + * @return string + */ + public function title() + { + if ($this->title) { + return $this->title; + } + + if ($this->mode == static::MODE_CREATE) { + return trans('admin.create'); + } + + if ($this->mode == static::MODE_EDIT) { + return trans('admin.edit'); + } + + return ''; + } + + /** + * Determine if form fields has files. + * + * @return bool + */ + public function hasFile() + { + foreach ($this->fields() as $field) { + if ($field instanceof Field\File) { + return true; + } + } + + return false; + } + + /** + * Add field for store redirect url after update or store. + * + * @return void + */ + protected function addRedirectUrlField() + { + $previous = URL::previous(); + + if (!$previous || $previous == URL::current()) { + return; + } + + if (Str::contains($previous, url($this->getResource()))) { + $this->addHiddenField((new Hidden(static::PREVIOUS_URL_KEY))->value($previous)); + } + } + + /** + * Open up a new HTML form. + * + * @param array $options + * + * @return string + */ + public function open($options = []) + { + $attributes = []; + + if ($this->isMode(self::MODE_EDIT)) { + $this->addHiddenField((new Hidden('_method'))->value('PUT')); + } + + $this->addRedirectUrlField(); + + $attributes['action'] = $this->getAction(); + $attributes['method'] = array_get($options, 'method', 'post'); + $attributes['accept-charset'] = 'UTF-8'; + + $attributes['class'] = array_get($options, 'class'); + + if ($this->hasFile()) { + $attributes['enctype'] = 'multipart/form-data'; + } + + $html = []; + foreach ($attributes as $name => $value) { + $html[] = "$name=\"$value\""; + } + + return '
    '; + } + + /** + * Close the current form. + * + * @return string + */ + public function close() + { + $this->form = null; + $this->fields = null; + + return '
    '; + } + + /** + * Remove reserved fields like `id` `created_at` `updated_at` in form fields. + * + * @return void + */ + protected function removeReservedFields() + { + if (!$this->isMode(static::MODE_CREATE)) { + return; + } + + $reservedColumns = [ + $this->form->model()->getKeyName(), + $this->form->model()->getCreatedAtColumn(), + $this->form->model()->getUpdatedAtColumn(), + ]; + + $this->fields = $this->fields()->reject(function (Field $field) use ($reservedColumns) { + return in_array($field->column(), $reservedColumns); + }); + } + + /** + * Render form header tools. + * + * @return string + */ + public function renderTools() + { + return $this->tools->render(); + } + + /** + * Render form footer. + * + * @return string + */ + public function renderFooter() + { + return $this->footer->render(); + } + + /** + * Render form. + * + * @return string + */ + public function render() + { + $this->removeReservedFields(); + + $tabObj = $this->form->getTab(); + + if (!$tabObj->isEmpty()) { + $script = <<<'SCRIPT' + +var hash = document.location.hash; +if (hash) { + $('.nav-tabs a[href="' + hash + '"]').tab('show'); +} + +// Change hash for page-reload +$('.nav-tabs a').on('shown.bs.tab', function (e) { + history.pushState(null,null, e.target.hash); +}); + +if ($('.has-error').length) { + $('.has-error').each(function () { + var tabId = '#'+$(this).closest('.tab-pane').attr('id'); + $('li a[href="'+tabId+'"] i').removeClass('hide'); + }); + + var first = $('.has-error:first').closest('.tab-pane').attr('id'); + $('li a[href="#'+first+'"]').tab('show'); +} + +SCRIPT; + Admin::script($script); + } + + $data = [ + 'form' => $this, + 'tabObj' => $tabObj, + 'width' => $this->width, + ]; + + return view($this->view, $data)->render(); + } +} diff --git a/src/Form/EmbeddedForm.php b/src/Form/EmbeddedForm.php new file mode 100644 index 0000000..a12429e --- /dev/null +++ b/src/Form/EmbeddedForm.php @@ -0,0 +1,282 @@ +column = $column; + + $this->fields = new Collection(); + } + + /** + * Get all fields in current form. + * + * @return Collection + */ + public function fields() + { + return $this->fields; + } + + /** + * Set parent form for this form. + * + * @param Form $parent + * + * @return $this + */ + public function setParent(Form $parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * Set original values for fields. + * + * @param array $data + * + * @return $this + */ + public function setOriginal($data) + { + if (empty($data)) { + return $this; + } + + if (is_string($data)) { + $data = json_decode($data, true); + } + + $this->original = $data; + + return $this; + } + + /** + * Prepare for insert or update. + * + * @param array $input + * + * @return mixed + */ + public function prepare($input) + { + foreach ($input as $key => $record) { + $this->setFieldOriginalValue($key); + $input[$key] = $this->prepareValue($key, $record); + } + + return $input; + } + + /** + * Do prepare work for each field. + * + * @param string $key + * @param string $record + * + * @return mixed + */ + protected function prepareValue($key, $record) + { + $field = $this->fields->first(function (Field $field) use ($key) { + return in_array($key, (array) $field->column()); + }); + + if (method_exists($field, 'prepare')) { + return $field->prepare($record); + } + + return $record; + } + + /** + * Set original data for each field. + * + * @param string $key + * + * @return void + */ + protected function setFieldOriginalValue($key) + { + if (array_key_exists($key, $this->original)) { + $values = $this->original[$key]; + + $this->fields->each(function (Field $field) use ($values) { + $field->setOriginal($values); + }); + } + } + + /** + * Fill data to all fields in form. + * + * @param array $data + * + * @return $this + */ + public function fill(array $data) + { + $this->fields->each(function (Field $field) use ($data) { + $field->fill($data); + }); + + return $this; + } + + /** + * Format form, set `element name` `error key` and `element class`. + * + * @param Field $field + * + * @return Field + */ + protected function formatField(Field $field) + { + $jsonKey = $field->column(); + + $elementName = $elementClass = $errorKey = []; + + if (is_array($jsonKey)) { + foreach ($jsonKey as $index => $name) { + $elementName[$index] = "{$this->column}[$name]"; + $errorKey[$index] = "{$this->column}.$name"; + $elementClass[$index] = "{$this->column}_$name"; + } + } else { + $elementName = "{$this->column}[$jsonKey]"; + $errorKey = "{$this->column}.$jsonKey"; + $elementClass = "{$this->column}_$jsonKey"; + } + + $field->setElementName($elementName) + ->setErrorKey($errorKey) + ->setElementClass($elementClass); + + return $field; + } + + /** + * Add a field to form. + * + * @param Field $field + * + * @return $this + */ + public function pushField(Field $field) + { + $field = $this->formatField($field); + + $this->fields->push($field); + + return $this; + } + + /** + * Add nested-form fields dynamically. + * + * @param string $method + * @param array $arguments + * + * @return Field|$this + */ + public function __call($method, $arguments) + { + if ($className = Form::findFieldClass($method)) { + $column = array_get($arguments, 0, ''); + + /** @var Field $field */ + $field = new $className($column, array_slice($arguments, 1)); + + $field->setForm($this->parent); + + $this->pushField($field); + + return $field; + } + + return $this; + } +} diff --git a/src/Form/Field.php b/src/Form/Field.php new file mode 100644 index 0000000..97e03d9 --- /dev/null +++ b/src/Form/Field.php @@ -0,0 +1,1143 @@ + 2, + 'field' => 8, + ]; + + /** + * If the form horizontal layout. + * + * @var bool + */ + protected $horizontal = true; + + /** + * column data format. + * + * @var \Closure + */ + protected $customFormat = null; + + /** + * @var bool + */ + protected $display = true; + + /** + * @var array + */ + protected $labelClass = []; + + /** + * Field constructor. + * + * @param $column + * @param array $arguments + */ + public function __construct($column, $arguments = []) + { + $this->column = $column; + $this->label = $this->formatLabel($arguments); + $this->id = $this->formatId($column); + } + + /** + * Get assets required by this field. + * + * @return array + */ + public static function getAssets() + { + return [ + 'css' => static::$css, + 'js' => static::$js, + ]; + } + + /** + * Format the field element id. + * + * @param string|array $column + * + * @return string|array + */ + protected function formatId($column) + { + return str_replace('.', '_', $column); + } + + /** + * Format the label value. + * + * @param array $arguments + * + * @return string + */ + protected function formatLabel($arguments = []) + { + $column = is_array($this->column) ? current($this->column) : $this->column; + + $label = isset($arguments[0]) ? $arguments[0] : ucfirst($column); + + return str_replace(['.', '_'], ' ', $label); + } + + /** + * Format the name of the field. + * + * @param string $column + * + * @return array|mixed|string + */ + protected function formatName($column) + { + if (is_string($column)) { + $name = explode('.', $column); + + if (count($name) == 1) { + return $name[0]; + } + + $html = array_shift($name); + foreach ($name as $piece) { + $html .= "[$piece]"; + } + + return $html; + } + + if (is_array($this->column)) { + $names = []; + foreach ($this->column as $key => $name) { + $names[$key] = $this->formatName($name); + } + + return $names; + } + + return ''; + } + + /** + * Set form element name. + * + * @param string $name + * + * @return $this + * + * @author Edwin Hui + */ + public function setElementName($name) + { + $this->elementName = $name; + + return $this; + } + + /** + * Fill data to the field. + * + * @param array $data + * + * @return void + */ + public function fill($data) + { + // Field value is already setted. +// if (!is_null($this->value)) { +// return; +// } + + $this->data = $data; + + if (is_array($this->column)) { + foreach ($this->column as $key => $column) { + $this->value[$key] = array_get($data, $column); + } + + return; + } + + $this->value = array_get($data, $this->column); + if (isset($this->customFormat) && $this->customFormat instanceof \Closure) { + $this->value = call_user_func($this->customFormat, $this->value); + } + } + + /** + * custom format form column data when edit. + * + * @param \Closure $call + * + * @return $this + */ + public function customFormat(\Closure $call) + { + $this->customFormat = $call; + + return $this; + } + + /** + * Set original value to the field. + * + * @param array $data + * + * @return void + */ + public function setOriginal($data) + { + if (is_array($this->column)) { + foreach ($this->column as $key => $column) { + $this->original[$key] = array_get($data, $column); + } + + return; + } + + $this->original = array_get($data, $this->column); + } + + /** + * @param Form $form + * + * @return $this + */ + public function setForm(Form $form = null) + { + $this->form = $form; + + return $this; + } + + /** + * Set width for field and label. + * + * @param int $field + * @param int $label + * + * @return $this + */ + public function setWidth($field = 8, $label = 2) + { + $this->width = [ + 'label' => $label, + 'field' => $field, + ]; + + return $this; + } + + /** + * Set the field options. + * + * @param array $options + * + * @return $this + */ + public function options($options = []) + { + if ($options instanceof Arrayable) { + $options = $options->toArray(); + } + + $this->options = array_merge($this->options, $options); + + return $this; + } + + /** + * Set the field option checked. + * + * @param array $checked + * + * @return $this + */ + public function checked($checked = []) + { + if ($checked instanceof Arrayable) { + $checked = $checked->toArray(); + } + + $this->checked = array_merge($this->checked, $checked); + + return $this; + } + + /** + * Get or set rules. + * + * @param null $rules + * @param array $messages + * + * @return $this + */ + public function rules($rules = null, $messages = []) + { + if ($rules instanceof \Closure) { + $this->rules = $rules; + } + + if (is_array($rules)) { + $thisRuleArr = array_filter(explode('|', $this->rules)); + + $this->rules = array_merge($thisRuleArr, $rules); + } elseif (is_string($rules)) { + $rules = array_filter(explode('|', "{$this->rules}|$rules")); + + $this->rules = implode('|', $rules); + } + + $this->validationMessages = $messages; + + return $this; + } + + /** + * Get field validation rules. + * + * @return string + */ + protected function getRules() + { + if ($this->rules instanceof \Closure) { + return $this->rules->call($this, $this->form); + } + + return $this->rules; + } + + /** + * Remove a specific rule by keyword. + * + * @param string $rule + * + * @return void + */ + protected function removeRule($rule) + { + if (!is_string($this->rules)) { + return; + } + + $pattern = "/{$rule}[^\|]?(\||$)/"; + $this->rules = preg_replace($pattern, '', $this->rules, -1); + } + + /** + * Set field validator. + * + * @param callable $validator + * + * @return $this + */ + public function validator(callable $validator) + { + $this->validator = $validator; + + return $this; + } + + /** + * Get key for error message. + * + * @return string + */ + public function getErrorKey() + { + return $this->errorKey ?: $this->column; + } + + /** + * Set key for error message. + * + * @param string $key + * + * @return $this + */ + public function setErrorKey($key) + { + $this->errorKey = $key; + + return $this; + } + + /** + * Set or get value of the field. + * + * @param null $value + * + * @return mixed + */ + public function value($value = null) + { + if (is_null($value)) { + return is_null($this->value) ? $this->getDefault() : $this->value; + } + + $this->value = $value; + + return $this; + } + + /** + * Set or get data. + * + * @param array $data + * + * @return $this + */ + public function data(array $data = null) + { + if (is_null($data)) { + return $this->data; + } + + $this->data = $data; + + return $this; + } + + /** + * Set default value for field. + * + * @param $default + * + * @return $this + */ + public function default($default) + { + $this->default = $default; + + return $this; + } + + /** + * Get default value. + * + * @return mixed + */ + public function getDefault() + { + if ($this->default instanceof \Closure) { + return call_user_func($this->default, $this->form); + } + + return $this->default; + } + + /** + * Set help block for current field. + * + * @param string $text + * @param string $icon + * + * @return $this + */ + public function help($text = '', $icon = 'fa-info-circle') + { + $this->help = compact('text', 'icon'); + + return $this; + } + + /** + * Get column of the field. + * + * @return string|array + */ + public function column() + { + return $this->column; + } + + /** + * Get label of the field. + * + * @return string + */ + public function label() + { + return $this->label; + } + + /** + * Get original value of the field. + * + * @return mixed + */ + public function original() + { + return $this->original; + } + + /** + * Get validator for this field. + * + * @param array $input + * + * @return bool|Validator + */ + public function getValidator(array $input) + { + if ($this->validator) { + return $this->validator->call($this, $input); + } + + $rules = $attributes = []; + + if (!$fieldRules = $this->getRules()) { + return false; + } + + if (is_string($this->column)) { + if (!array_has($input, $this->column)) { + return false; + } + + $input = $this->sanitizeInput($input, $this->column); + + $rules[$this->column] = $fieldRules; + $attributes[$this->column] = $this->label; + } + + if (is_array($this->column)) { + foreach ($this->column as $key => $column) { + if (!array_key_exists($column, $input)) { + continue; + } + $input[$column.$key] = array_get($input, $column); + $rules[$column.$key] = $fieldRules; + $attributes[$column.$key] = $this->label."[$column]"; + } + } + + return Validator::make($input, $rules, $this->validationMessages, $attributes); + } + + /** + * Sanitize input data. + * + * @param array $input + * @param string $column + * + * @return array + */ + protected function sanitizeInput($input, $column) + { + if ($this instanceof Field\MultipleSelect) { + $value = array_get($input, $column); + array_set($input, $column, array_filter($value)); + } + + return $input; + } + + /** + * Add html attributes to elements. + * + * @param array|string $attribute + * @param mixed $value + * + * @return $this + */ + public function attribute($attribute, $value = null) + { + if (is_array($attribute)) { + $this->attributes = array_merge($this->attributes, $attribute); + } else { + $this->attributes[$attribute] = (string) $value; + } + + return $this; + } + + /** + * Specifies a regular expression against which to validate the value of the input. + * + * @param string $regexp + * + * @return Field + */ + public function pattern($regexp) + { + return $this->attribute('pattern', $regexp); + } + + /** + * set the input filed required. + * + * @param bool $isLabelAsterisked + * + * @return Field + */ + public function required($isLabelAsterisked = true) + { + if ($isLabelAsterisked) { + $this->setLabelClass(['asterisk']); + } + + return $this->attribute('required', true); + } + + /** + * Set the field automatically get focus. + * + * @return Field + */ + public function autofocus() + { + return $this->attribute('autofocus', true); + } + + /** + * Set the field as readonly mode. + * + * @return Field + */ + public function readOnly() + { + return $this->attribute('readonly', true); + } + + /** + * Set field as disabled. + * + * @return Field + */ + public function disable() + { + return $this->attribute('disabled', true); + } + + /** + * Set field placeholder. + * + * @param string $placeholder + * + * @return Field + */ + public function placeholder($placeholder = '') + { + $this->placeholder = $placeholder; + + return $this; + } + + /** + * Get placeholder. + * + * @return string + */ + public function getPlaceholder() + { + return $this->placeholder ?: trans('admin.input').' '.$this->label; + } + + /** + * Prepare for a field value before update or insert. + * + * @param $value + * + * @return mixed + */ + public function prepare($value) + { + return $value; + } + + /** + * Format the field attributes. + * + * @return string + */ + protected function formatAttributes() + { + $html = []; + + foreach ($this->attributes as $name => $value) { + $html[] = $name.'="'.e($value).'"'; + } + + return implode(' ', $html); + } + + /** + * @return $this + */ + public function disableHorizontal() + { + $this->horizontal = false; + + return $this; + } + + /** + * @return array + */ + public function getViewElementClasses() + { + if ($this->horizontal) { + return [ + 'label' => "col-sm-{$this->width['label']} {$this->getLabelClass()}", + 'field' => "col-sm-{$this->width['field']}", + 'form-group' => 'form-group ', + ]; + } + + return ['label' => "{$this->getLabelClass()}", 'field' => '', 'form-group' => '']; + } + + /** + * Set form element class. + * + * @param string|array $class + * + * @return $this + */ + public function setElementClass($class) + { + $this->elementClass = array_merge($this->elementClass, (array) $class); + + return $this; + } + + /** + * Get element class. + * + * @return array + */ + protected function getElementClass() + { + if (!$this->elementClass) { + $name = $this->elementName ?: $this->formatName($this->column); + + $this->elementClass = (array) str_replace(['[', ']'], '_', $name); + } + + return $this->elementClass; + } + + /** + * Get element class string. + * + * @return mixed + */ + protected function getElementClassString() + { + $elementClass = $this->getElementClass(); + + if (Arr::isAssoc($elementClass)) { + $classes = []; + + foreach ($elementClass as $index => $class) { + $classes[$index] = is_array($class) ? implode(' ', $class) : $class; + } + + return $classes; + } + + return implode(' ', $elementClass); + } + + /** + * Get element class selector. + * + * @return string|array + */ + protected function getElementClassSelector() + { + $elementClass = $this->getElementClass(); + + if (Arr::isAssoc($elementClass)) { + $classes = []; + + foreach ($elementClass as $index => $class) { + $classes[$index] = '.'.(is_array($class) ? implode('.', $class) : $class); + } + + return $classes; + } + + return '.'.implode('.', $elementClass); + } + + /** + * Add the element class. + * + * @param $class + * + * @return $this + */ + public function addElementClass($class) + { + if (is_array($class) || is_string($class)) { + $this->elementClass = array_merge($this->elementClass, (array) $class); + + $this->elementClass = array_unique($this->elementClass); + } + + return $this; + } + + /** + * Remove element class. + * + * @param $class + * + * @return $this + */ + public function removeElementClass($class) + { + $delClass = []; + + if (is_string($class) || is_array($class)) { + $delClass = (array) $class; + } + + foreach ($delClass as $del) { + if (($key = array_search($del, $this->elementClass))) { + unset($this->elementClass[$key]); + } + } + + return $this; + } + + /** + * Add variables to field view. + * + * @param array $variables + * + * @return $this + */ + protected function addVariables(array $variables = []) + { + $this->variables = array_merge($this->variables, $variables); + + return $this; + } + + /** + * @return string + */ + public function getLabelClass() + : string + { + return implode(' ', $this->labelClass); + } + + /** + * @param array $labelClass + * + * @return self + */ + public function setLabelClass(array $labelClass) + : self + { + $this->labelClass = $labelClass; + + return $this; + } + + /** + * Get the view variables of this field. + * + * @return array + */ + public function variables() + { + return array_merge($this->variables, [ + 'id' => $this->id, + 'name' => $this->elementName ?: $this->formatName($this->column), + 'help' => $this->help, + 'class' => $this->getElementClassString(), + 'value' => $this->value(), + 'label' => $this->label, + 'viewClass' => $this->getViewElementClasses(), + 'column' => $this->column, + 'errorKey' => $this->getErrorKey(), + 'attributes' => $this->formatAttributes(), + 'placeholder' => $this->getPlaceholder(), + ]); + } + + /** + * Get view of this field. + * + * @return string + */ + public function getView() + { + if (!empty($this->view)) { + return $this->view; + } + + $class = explode('\\', get_called_class()); + + return 'admin::form.'.strtolower(end($class)); + } + + /** + * Get script of current field. + * + * @return string + */ + public function getScript() + { + return $this->script; + } + + /** + * Set script of current field. + * + * @return self + */ + public function setScript($script) + { + $this->script = $script; + + return $this; + } + + /** + * To set this field should render or not. + * + * @return self + */ + public function setDisplay(bool $display) + { + $this->display = $display; + + return $this; + } + + /** + * If this field should render. + * + * @return bool + */ + protected function shouldRender() + { + if (!$this->display) { + return false; + } + + return true; + } + + /** + * Render this filed. + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|string + */ + public function render() + { + if (!$this->shouldRender()) { + return ''; + } + + Admin::script($this->script); + + return view($this->getView(), $this->variables()); + } + + /** + * @return string + */ + public function __toString() + { + return $this->render()->render(); + } +} diff --git a/src/Form/Field/Button.php b/src/Form/Field/Button.php new file mode 100644 index 0000000..e55c11c --- /dev/null +++ b/src/Form/Field/Button.php @@ -0,0 +1,28 @@ +class = 'btn-info'; + + return $this; + } + + public function on($event, $callback) + { + $this->script = <<getElementClassSelector()}').on('$event', function() { + $callback + }); + +EOT; + } +} diff --git a/src/Form/Field/Captcha.php b/src/Form/Field/Captcha.php new file mode 100644 index 0000000..e032cd5 --- /dev/null +++ b/src/Form/Field/Captcha.php @@ -0,0 +1,44 @@ +column = '__captcha__'; + $this->label = trans('admin.captcha'); + } + + public function setForm(Form $form = null) + { + $this->form = $form; + + $this->form->ignore($this->column); + + return $this; + } + + public function render() + { + $this->script = <<column}-captcha').click(function () { + $(this).attr('src', $(this).attr('src')+'?'+Math.random()); +}); + +EOT; + + return parent::render(); + } +} diff --git a/src/Form/Field/Checkbox.php b/src/Form/Field/Checkbox.php new file mode 100644 index 0000000..7caea51 --- /dev/null +++ b/src/Form/Field/Checkbox.php @@ -0,0 +1,90 @@ +toArray(); + } + + $this->options = (array) $options; + + return $this; + } + + /** + * Set checked. + * + * @param array|callable|string $checked + * + * @return $this + */ + public function checked($checked = []) + { + if ($checked instanceof Arrayable) { + $checked = $checked->toArray(); + } + + $this->checked = (array) $checked; + + return $this; + } + + /** + * Draw inline checkboxes. + * + * @return $this + */ + public function inline() + { + $this->inline = true; + + return $this; + } + + /** + * Draw stacked checkboxes. + * + * @return $this + */ + public function stacked() + { + $this->inline = false; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function render() + { + $this->script = "$('{$this->getElementClassSelector()}').iCheck({checkboxClass:'icheckbox_minimal-blue'});"; + + $this->addVariables(['checked' => $this->checked, 'inline' => $this->inline]); + + return parent::render(); + } +} diff --git a/src/Form/Field/Color.php b/src/Form/Field/Color.php new file mode 100644 index 0000000..2e54944 --- /dev/null +++ b/src/Form/Field/Color.php @@ -0,0 +1,61 @@ +options(['format' => 'hex']); + } + + /** + * Use `rgb` format. + * + * @return $this + */ + public function rgb() + { + return $this->options(['format' => 'rgb']); + } + + /** + * Use `rgba` format. + * + * @return $this + */ + public function rgba() + { + return $this->options(['format' => 'rgba']); + } + + /** + * Render this filed. + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function render() + { + $options = json_encode($this->options); + + $this->script = "$('{$this->getElementClassSelector()}').parent().colorpicker($options);"; + + $this->prepend('') + ->defaultAttribute('style', 'width: 140px'); + + return parent::render(); + } +} diff --git a/src/Form/Field/Currency.php b/src/Form/Field/Currency.php new file mode 100644 index 0000000..d34e29a --- /dev/null +++ b/src/Form/Field/Currency.php @@ -0,0 +1,77 @@ + 'currency', + 'radixPoint' => '.', + 'prefix' => '', + 'removeMaskOnSubmit' => true, + ]; + + /** + * Set symbol for currency field. + * + * @param string $symbol + * + * @return $this + */ + public function symbol($symbol) + { + $this->symbol = $symbol; + + return $this; + } + + /** + * Set digits for input number. + * + * @param int $digits + * + * @return $this + */ + public function digits($digits) + { + return $this->options(compact('digits')); + } + + /** + * {@inheritdoc} + */ + public function prepare($value) + { + return (float) $value; + } + + /** + * {@inheritdoc} + */ + public function render() + { + $this->inputmask($this->options); + + $this->prepend($this->symbol) + ->defaultAttribute('style', 'width: 120px'); + + return parent::render(); + } +} diff --git a/src/Form/Field/Date.php b/src/Form/Field/Date.php new file mode 100644 index 0000000..c362840 --- /dev/null +++ b/src/Form/Field/Date.php @@ -0,0 +1,47 @@ +format = $format; + + return $this; + } + + public function prepare($value) + { + if ($value === '') { + $value = null; + } + + return $value; + } + + public function render() + { + $this->options['format'] = $this->format; + $this->options['locale'] = config('app.locale'); + $this->options['allowInputToggle'] = true; + + $this->script = "$('{$this->getElementClassSelector()}').parent().datetimepicker(".json_encode($this->options).');'; + + $this->prepend('') + ->defaultAttribute('style', 'width: 110px'); + + return parent::render(); + } +} diff --git a/src/Form/Field/DateRange.php b/src/Form/Field/DateRange.php new file mode 100644 index 0000000..e451c96 --- /dev/null +++ b/src/Form/Field/DateRange.php @@ -0,0 +1,70 @@ +column['start'] = $column; + $this->column['end'] = $arguments[0]; + + array_shift($arguments); + $this->label = $this->formatLabel($arguments); + $this->id = $this->formatId($this->column); + + $this->options(['format' => $this->format]); + } + + public function prepare($value) + { + if ($value === '') { + $value = null; + } + + return $value; + } + + public function render() + { + $this->options['locale'] = config('app.locale'); + + $startOptions = json_encode($this->options); + $endOptions = json_encode($this->options + ['useCurrent' => false]); + + $class = $this->getElementClassSelector(); + + $this->script = <<defaultAttribute('style', 'width: 160px'); + + return parent::render(); + } +} diff --git a/src/Form/Field/DatetimeRange.php b/src/Form/Field/DatetimeRange.php new file mode 100644 index 0000000..c2ee931 --- /dev/null +++ b/src/Form/Field/DatetimeRange.php @@ -0,0 +1,8 @@ + 'decimal', + 'rightAlign' => true, + ]; + + public function render() + { + $this->inputmask($this->options); + + $this->prepend('') + ->defaultAttribute('style', 'width: 130px'); + + return parent::render(); + } +} diff --git a/src/Form/Field/Display.php b/src/Form/Field/Display.php new file mode 100644 index 0000000..711e9e3 --- /dev/null +++ b/src/Form/Field/Display.php @@ -0,0 +1,25 @@ +callback = $callback; + } + + public function render() + { + if ($this->callback instanceof Closure) { + $this->value = $this->callback->call($this->form->model(), $this->value); + } + + return parent::render(); + } +} diff --git a/src/Form/Field/Divide.php b/src/Form/Field/Divide.php new file mode 100644 index 0000000..8061088 --- /dev/null +++ b/src/Form/Field/Divide.php @@ -0,0 +1,13 @@ +'; + } +} diff --git a/src/Form/Field/Editor.php b/src/Form/Field/Editor.php new file mode 100644 index 0000000..b743a0a --- /dev/null +++ b/src/Form/Field/Editor.php @@ -0,0 +1,19 @@ +script = "CKEDITOR.replace('{$this->id}');"; + + return parent::render(); + } +} diff --git a/src/Form/Field/Email.php b/src/Form/Field/Email.php new file mode 100644 index 0000000..aecf077 --- /dev/null +++ b/src/Form/Field/Email.php @@ -0,0 +1,16 @@ +prepend('') + ->defaultAttribute('type', 'email'); + + return parent::render(); + } +} diff --git a/src/Form/Field/Embeds.php b/src/Form/Field/Embeds.php new file mode 100644 index 0000000..44fe60c --- /dev/null +++ b/src/Form/Field/Embeds.php @@ -0,0 +1,253 @@ +column = $column; + + if (count($arguments) == 1) { + $this->label = $this->formatLabel(); + $this->builder = $arguments[0]; + } + + if (count($arguments) == 2) { + list($this->label, $this->builder) = $arguments; + } + } + + /** + * Prepare input data for insert or update. + * + * @param array $input + * + * @return array + */ + public function prepare($input) + { + $form = $this->buildEmbeddedForm(); + + return $form->setOriginal($this->original)->prepare($input); + } + + /** + * {@inheritdoc} + */ + public function getValidator(array $input) + { + if (!array_key_exists($this->column, $input)) { + return false; + } + + $input = array_only($input, $this->column); + + $rules = $attributes = []; + + /** @var Field $field */ + foreach ($this->buildEmbeddedForm()->fields() as $field) { + if (!$fieldRules = $field->getRules()) { + continue; + } + + $column = $field->column(); + + /* + * + * For single column field format rules to: + * [ + * 'extra.name' => 'required' + * 'extra.email' => 'required' + * ] + * + * For multiple column field with rules like 'required': + * 'extra' => [ + * 'start' => 'start_at' + * 'end' => 'end_at', + * ] + * + * format rules to: + * [ + * 'extra.start_atstart' => 'required' + * 'extra.end_atend' => 'required' + * ] + */ + if (is_array($column)) { + foreach ($column as $key => $name) { + $rules["{$this->column}.$name$key"] = $fieldRules; + } + + $this->resetInputKey($input, $column); + } else { + $rules["{$this->column}.$column"] = $fieldRules; + } + + /** + * For single column field format attributes to: + * [ + * 'extra.name' => $label + * 'extra.email' => $label + * ]. + * + * For multiple column field with rules like 'required': + * 'extra' => [ + * 'start' => 'start_at' + * 'end' => 'end_at', + * ] + * + * format rules to: + * [ + * 'extra.start_atstart' => "$label[start_at]" + * 'extra.end_atend' => "$label[end_at]" + * ] + */ + $attributes = array_merge( + $attributes, + $this->formatValidationAttribute($input, $field->label(), $column) + ); + } + + if (empty($rules)) { + return false; + } + + return Validator::make($input, $rules, $this->validationMessages, $attributes); + } + + /** + * Format validation attributes. + * + * @param array $input + * @param string $label + * @param string $column + * + * @return array + */ + protected function formatValidationAttribute($input, $label, $column) + { + $new = $attributes = []; + + if (is_array($column)) { + foreach ($column as $index => $col) { + $new[$col.$index] = $col; + } + } + + foreach (array_keys(array_dot($input)) as $key) { + if (is_string($column)) { + if (Str::endsWith($key, ".$column")) { + $attributes[$key] = $label; + } + } else { + foreach ($new as $k => $val) { + if (Str::endsWith($key, ".$k")) { + $attributes[$key] = $label."[$val]"; + } + } + } + } + + return $attributes; + } + + /** + * Reset input key for validation. + * + * @param array $input + * @param array $column $column is the column name array set + * + * @return void. + */ + public function resetInputKey(array &$input, array $column) + { + $column = array_flip($column); + + foreach ($input[$this->column] as $key => $value) { + if (!array_key_exists($key, $column)) { + continue; + } + + $newKey = $key.$column[$key]; + + /* + * set new key + */ + array_set($input, "{$this->column}.$newKey", $value); + /* + * forget the old key and value + */ + array_forget($input, "{$this->column}.$key"); + } + } + + /** + * Get data for Embedded form. + * + * Normally, data is obtained from the database. + * + * When the data validation errors, data is obtained from session flash. + * + * @return array + */ + protected function getEmbeddedData() + { + if ($old = old($this->column)) { + return $old; + } + + if (empty($this->value)) { + return []; + } + + if (is_string($this->value)) { + return json_decode($this->value, true); + } + + return (array) $this->value; + } + + /** + * Build a Embedded Form and fill data. + * + * @return EmbeddedForm + */ + protected function buildEmbeddedForm() + { + $form = new EmbeddedForm($this->column); + + $form->setParent($this->form); + + call_user_func($this->builder, $form); + + $form->fill($this->getEmbeddedData()); + + return $form; + } + + /** + * Render the form. + * + * @return \Illuminate\View\View + */ + public function render() + { + return parent::render()->with(['form' => $this->buildEmbeddedForm()]); + } +} diff --git a/src/Form/Field/File.php b/src/Form/Field/File.php new file mode 100644 index 0000000..202097a --- /dev/null +++ b/src/Form/Field/File.php @@ -0,0 +1,196 @@ +initStorage(); + + parent::__construct($column, $arguments); + } + + /** + * Default directory for file to upload. + * + * @return mixed + */ + public function defaultDirectory() + { + return config('admin.upload.directory.file'); + } + + /** + * {@inheritdoc} + */ + public function getValidator(array $input) + { + if (request()->has(static::FILE_DELETE_FLAG)) { + return false; + } + + if ($this->validator) { + return $this->validator->call($this, $input); + } + + /* + * If has original value, means the form is in edit mode, + * then remove required rule from rules. + */ + if ($this->original()) { + $this->removeRule('required'); + } + + /* + * Make input data validatable if the column data is `null`. + */ + if (array_has($input, $this->column) && is_null(array_get($input, $this->column))) { + $input[$this->column] = ''; + } + + $rules = $attributes = []; + + if (!$fieldRules = $this->getRules()) { + return false; + } + + $rules[$this->column] = $fieldRules; + $attributes[$this->column] = $this->label; + + return Validator::make($input, $rules, $this->validationMessages, $attributes); + } + + /** + * Prepare for saving. + * + * @param UploadedFile|array $file + * + * @return mixed|string + */ + public function prepare($file) + { + if (request()->has(static::FILE_DELETE_FLAG)) { + return $this->destroy(); + } + + $this->name = $this->getStoreName($file); + + return $this->uploadAndDeleteOriginal($file); + } + + /** + * Upload file and delete original file. + * + * @param UploadedFile $file + * + * @return mixed + */ + protected function uploadAndDeleteOriginal(UploadedFile $file) + { + $this->renameIfExists($file); + + $path = null; + + if (!is_null($this->storage_permission)) { + $path = $this->storage->putFileAs($this->getDirectory(), $file, $this->name, $this->storage_permission); + } else { + $path = $this->storage->putFileAs($this->getDirectory(), $file, $this->name); + } + + $this->destroy(); + + return $path; + } + + /** + * Preview html for file-upload plugin. + * + * @return string + */ + protected function preview() + { + return $this->objectUrl($this->value); + } + + /** + * Initialize the caption. + * + * @param string $caption + * + * @return string + */ + protected function initialCaption($caption) + { + return basename($caption); + } + + /** + * @return array + */ + protected function initialPreviewConfig() + { + return [ + ['caption' => basename($this->value), 'key' => 0], + ]; + } + + /** + * Render file upload field. + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function render() + { + $this->options(['overwriteInitial' => true]); + $this->setupDefaultOptions(); + + if (!empty($this->value)) { + $this->attribute('data-initial-preview', filter_var($this->preview(), FILTER_VALIDATE_URL)); + $this->attribute('data-initial-caption', $this->initialCaption($this->value)); + + $this->setupPreviewOptions(); + } + + $options = json_encode($this->options); + + $this->script = <<getElementClassSelector()}").fileinput({$options}); + +EOT; + + return parent::render(); + } +} diff --git a/src/Form/Field/HasMany.php b/src/Form/Field/HasMany.php new file mode 100644 index 0000000..9299cf5 --- /dev/null +++ b/src/Form/Field/HasMany.php @@ -0,0 +1,665 @@ + 'admin::form.hasmany', + 'tab' => 'admin::form.hasmanytab', + 'table' => 'admin::form.hasmanytable', + ]; + + /** + * Options for template. + * + * @var array + */ + protected $options = [ + 'allowCreate' => true, + 'allowDelete' => true, + ]; + + /** + * Create a new HasMany field instance. + * + * @param $relationName + * @param array $arguments + */ + public function __construct($relationName, $arguments = []) + { + $this->relationName = $relationName; + + $this->column = $relationName; + + if (count($arguments) == 1) { + $this->label = $this->formatLabel(); + $this->builder = $arguments[0]; + } + + if (count($arguments) == 2) { + list($this->label, $this->builder) = $arguments; + } + } + + /** + * Get validator for this field. + * + * @param array $input + * + * @return bool|Validator + */ + public function getValidator(array $input) + { + if (!array_key_exists($this->column, $input)) { + return false; + } + + $input = array_only($input, $this->column); + + $form = $this->buildNestedForm($this->column, $this->builder); + + $rules = $attributes = []; + + /* @var Field $field */ + foreach ($form->fields() as $field) { + if (!$fieldRules = $field->getRules()) { + continue; + } + + $column = $field->column(); + + if (is_array($column)) { + foreach ($column as $key => $name) { + $rules[$name.$key] = $fieldRules; + } + + $this->resetInputKey($input, $column); + } else { + $rules[$column] = $fieldRules; + } + + $attributes = array_merge( + $attributes, + $this->formatValidationAttribute($input, $field->label(), $column) + ); + } + + array_forget($rules, NestedForm::REMOVE_FLAG_NAME); + + if (empty($rules)) { + return false; + } + + $newRules = []; + $newInput = []; + + foreach ($rules as $column => $rule) { + foreach (array_keys($input[$this->column]) as $key) { + $newRules["{$this->column}.$key.$column"] = $rule; + if (isset($input[$this->column][$key][$column]) && + is_array($input[$this->column][$key][$column])) { + foreach ($input[$this->column][$key][$column] as $vkey => $value) { + $newInput["{$this->column}.$key.{$column}$vkey"] = $value; + } + } + } + } + + if (empty($newInput)) { + $newInput = $input; + } + + return Validator::make($newInput, $newRules, $this->validationMessages, $attributes); + } + + /** + * Format validation attributes. + * + * @param array $input + * @param string $label + * @param string $column + * + * @return array + */ + protected function formatValidationAttribute($input, $label, $column) + { + $new = $attributes = []; + + if (is_array($column)) { + foreach ($column as $index => $col) { + $new[$col.$index] = $col; + } + } + + foreach (array_keys(array_dot($input)) as $key) { + if (is_string($column)) { + if (Str::endsWith($key, ".$column")) { + $attributes[$key] = $label; + } + } else { + foreach ($new as $k => $val) { + if (Str::endsWith($key, ".$k")) { + $attributes[$key] = $label."[$val]"; + } + } + } + } + + return $attributes; + } + + /** + * Reset input key for validation. + * + * @param array $input + * @param array $column $column is the column name array set + * + * @return void. + */ + protected function resetInputKey(array &$input, array $column) + { + /** + * flip the column name array set. + * + * for example, for the DateRange, the column like as below + * + * ["start" => "created_at", "end" => "updated_at"] + * + * to: + * + * [ "created_at" => "start", "updated_at" => "end" ] + */ + $column = array_flip($column); + + /** + * $this->column is the inputs array's node name, default is the relation name. + * + * So... $input[$this->column] is the data of this column's inputs data + * + * in the HasMany relation, has many data/field set, $set is field set in the below + */ + foreach ($input[$this->column] as $index => $set) { + + /* + * foreach the field set to find the corresponding $column + */ + foreach ($set as $name => $value) { + /* + * if doesn't have column name, continue to the next loop + */ + if (!array_key_exists($name, $column)) { + continue; + } + + /** + * example: $newKey = created_atstart. + * + * Σ( ° △ °|||)︴ + * + * I don't know why a form need range input? Only can imagine is for range search.... + */ + $newKey = $name.$column[$name]; + + /* + * set new key + */ + array_set($input, "{$this->column}.$index.$newKey", $value); + /* + * forget the old key and value + */ + array_forget($input, "{$this->column}.$index.$name"); + } + } + } + + /** + * Prepare input data for insert or update. + * + * @param array $input + * + * @return array + */ + public function prepare($input) + { + $form = $this->buildNestedForm($this->column, $this->builder); + + return $form->setOriginal($this->original, $this->getKeyName())->prepare($input); + } + + /** + * Build a Nested form. + * + * @param string $column + * @param \Closure $builder + * @param null $key + * + * @return NestedForm + */ + protected function buildNestedForm($column, \Closure $builder, $key = null) + { + $form = new Form\NestedForm($column, $key); + + $form->setForm($this->form); + + call_user_func($builder, $form); + + $form->hidden($this->getKeyName()); + + $form->hidden(NestedForm::REMOVE_FLAG_NAME)->default(0)->addElementClass(NestedForm::REMOVE_FLAG_CLASS); + + return $form; + } + + /** + * Get the HasMany relation key name. + * + * @return string + */ + protected function getKeyName() + { + if (is_null($this->form)) { + return; + } + + return $this->form->model()->{$this->relationName}()->getRelated()->getKeyName(); + } + + /** + * Set view mode. + * + * @param string $mode currently support `tab` mode. + * + * @return $this + * + * @author Edwin Hui + */ + public function mode($mode) + { + $this->viewMode = $mode; + + return $this; + } + + /** + * Use tab mode to showing hasmany field. + * + * @return HasMany + */ + public function useTab() + { + return $this->mode('tab'); + } + + /** + * Use table mode to showing hasmany field. + * + * @return HasMany + */ + public function useTable() + { + return $this->mode('table'); + } + + /** + * Build Nested form for related data. + * + * @throws \Exception + * + * @return array + */ + protected function buildRelatedForms() + { + if (is_null($this->form)) { + return []; + } + + $model = $this->form->model(); + + $relation = call_user_func([$model, $this->relationName]); + + if (!$relation instanceof Relation && !$relation instanceof MorphMany) { + throw new \Exception('hasMany field must be a HasMany or MorphMany relation.'); + } + + $forms = []; + + /* + * If redirect from `exception` or `validation error` page. + * + * Then get form data from session flash. + * + * Else get data from database. + */ + if ($values = old($this->column)) { + foreach ($values as $key => $data) { + if ($data[NestedForm::REMOVE_FLAG_NAME] == 1) { + continue; + } + + $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $key) + ->fill($data); + } + } else { + foreach ($this->value as $data) { + $key = array_get($data, $relation->getRelated()->getKeyName()); + + $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $key) + ->fill($data); + } + } + + return $forms; + } + + /** + * Setup script for this field in different view mode. + * + * @param string $script + * + * @return void + */ + protected function setupScript($script) + { + $method = 'setupScriptFor'.ucfirst($this->viewMode).'View'; + + call_user_func([$this, $method], $script); + } + + /** + * Setup default template script. + * + * @param string $templateScript + * + * @return void + */ + protected function setupScriptForDefaultView($templateScript) + { + $removeClass = NestedForm::REMOVE_FLAG_CLASS; + $defaultKey = NestedForm::DEFAULT_KEY_NAME; + + /** + * When add a new sub form, replace all element key in new sub form. + * + * @example comments[new___key__][title] => comments[new_{index}][title] + * + * {count} is increment number of current sub form count. + */ + $script = <<column}').on('click', '.add', function () { + + var tpl = $('template.{$this->column}-tpl'); + + index++; + + var template = tpl.html().replace(/{$defaultKey}/g, index); + $('.has-many-{$this->column}-forms').append(template); + {$templateScript} +}); + +$('#has-many-{$this->column}').on('click', '.remove', function () { + $(this).closest('.has-many-{$this->column}-form').hide(); + $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1); +}); + +EOT; + + Admin::script($script); + } + + /** + * Setup tab template script. + * + * @param string $templateScript + * + * @return void + */ + protected function setupScriptForTabView($templateScript) + { + $removeClass = NestedForm::REMOVE_FLAG_CLASS; + $defaultKey = NestedForm::DEFAULT_KEY_NAME; + + $script = <<column} > .nav').off('click', 'i.close-tab').on('click', 'i.close-tab', function(){ + var \$navTab = $(this).siblings('a'); + var \$pane = $(\$navTab.attr('href')); + if( \$pane.hasClass('new') ){ + \$pane.remove(); + }else{ + \$pane.removeClass('active').find('.$removeClass').val(1); + } + if(\$navTab.closest('li').hasClass('active')){ + \$navTab.closest('li').remove(); + $('#has-many-{$this->column} > .nav > li:nth-child(1) > a').tab('show'); + }else{ + \$navTab.closest('li').remove(); + } +}); + +var index = 0; +$('#has-many-{$this->column} > .header').off('click', '.add').on('click', '.add', function(){ + index++; + var navTabHtml = $('#has-many-{$this->column} > template.nav-tab-tpl').html().replace(/{$defaultKey}/g, index); + var paneHtml = $('#has-many-{$this->column} > template.pane-tpl').html().replace(/{$defaultKey}/g, index); + $('#has-many-{$this->column} > .nav').append(navTabHtml); + $('#has-many-{$this->column} > .tab-content').append(paneHtml); + $('#has-many-{$this->column} > .nav > li:last-child a').tab('show'); + {$templateScript} +}); + +if ($('.has-error').length) { + $('.has-error').parent('.tab-pane').each(function () { + var tabId = '#'+$(this).attr('id'); + $('li a[href="'+tabId+'"] i').removeClass('hide'); + }); + + var first = $('.has-error:first').parent().attr('id'); + $('li a[href="#'+first+'"]').tab('show'); +} +EOT; + + Admin::script($script); + } + + /** + * Setup default template script. + * + * @param string $templateScript + * + * @return void + */ + protected function setupScriptForTableView($templateScript) + { + $removeClass = NestedForm::REMOVE_FLAG_CLASS; + $defaultKey = NestedForm::DEFAULT_KEY_NAME; + + /** + * When add a new sub form, replace all element key in new sub form. + * + * @example comments[new___key__][title] => comments[new_{index}][title] + * + * {count} is increment number of current sub form count. + */ + $script = <<column}').on('click', '.add', function () { + + var tpl = $('template.{$this->column}-tpl'); + + index++; + + var template = tpl.html().replace(/{$defaultKey}/g, index); + $('.has-many-{$this->column}-forms').append(template); + {$templateScript} +}); + +$('#has-many-{$this->column}').on('click', '.remove', function () { + $(this).closest('.has-many-{$this->column}-form').hide(); + $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1); +}); + +EOT; + + Admin::script($script); + } + + /** + * Disable create button. + * + * @return $this + */ + public function disableCreate() + { + $this->options['allowCreate'] = false; + + return $this; + } + + /** + * Disable delete button. + * + * @return $this + */ + public function disableDelete() + { + $this->options['allowDelete'] = false; + + return $this; + } + + /** + * Render the `HasMany` field. + * + * @throws \Exception + * + * @return \Illuminate\View\View + */ + public function render() + { + if ($this->viewMode == 'table') { + return $this->renderTable(); + } + + // specify a view to render. + $this->view = $this->views[$this->viewMode]; + + list($template, $script) = $this->buildNestedForm($this->column, $this->builder) + ->getTemplateHtmlAndScript(); + + $this->setupScript($script); + + return parent::render()->with([ + 'forms' => $this->buildRelatedForms(), + 'template' => $template, + 'relationName' => $this->relationName, + 'options' => $this->options, + ]); + } + + /** + * Render the `HasMany` field for table style. + * + * @throws \Exception + * + * @return mixed + */ + protected function renderTable() + { + $headers = []; + $fields = []; + $hidden = []; + $scripts = []; + + /* @var Field $field */ + foreach ($this->buildNestedForm($this->column, $this->builder)->fields() as $field) { + if (is_a($field, Hidden::class)) { + $hidden[] = $field->render(); + } else { + /* Hide label and set field width 100% */ + $field->setLabelClass(['hidden']); + $field->setWidth(12, 0); + $fields[] = $field->render(); + $headers[] = $field->label(); + } + + /* + * Get and remove the last script of Admin::$script stack. + */ + if ($field->getScript()) { + $scripts[] = array_pop(Admin::$script); + } + } + + /* Build row elements */ + $template = array_reduce($fields, function ($all, $field) { + $all .= "{$field}"; + + return $all; + }, ''); + + /* Build cell with hidden elements */ + $template .= ''.implode('', $hidden).''; + + $this->setupScript(implode("\r\n", $scripts)); + + // specify a view to render. + $this->view = $this->views[$this->viewMode]; + + return parent::render()->with([ + 'headers' => $headers, + 'forms' => $this->buildRelatedForms(), + 'template' => $template, + 'relationName' => $this->relationName, + 'options' => $this->options, + ]); + } +} diff --git a/src/Form/Field/Hidden.php b/src/Form/Field/Hidden.php new file mode 100644 index 0000000..cd18cf7 --- /dev/null +++ b/src/Form/Field/Hidden.php @@ -0,0 +1,9 @@ +html = $html; + + $this->label = array_get($arguments, 0); + } + + /** + * @return $this + */ + public function plain() + { + $this->plain = true; + + return $this; + } + + /** + * Render html field. + * + * @return string + */ + public function render() + { + if ($this->html instanceof \Closure) { + $this->html = $this->html->call($this->form->model(), $this->form); + } + + if ($this->plain) { + return $this->html; + } + + $viewClass = $this->getViewElementClasses(); + + return << + +
    + {$this->html} +
    + +EOT; + } +} diff --git a/src/Form/Field/Icon.php b/src/Form/Field/Icon.php new file mode 100644 index 0000000..d81870f --- /dev/null +++ b/src/Form/Field/Icon.php @@ -0,0 +1,30 @@ +script = <<getElementClassSelector()}').iconpicker({placement:'bottomLeft'}); + +EOT; + + $this->prepend('') + ->defaultAttribute('style', 'width: 140px'); + + return parent::render(); + } +} diff --git a/src/Form/Field/Id.php b/src/Form/Field/Id.php new file mode 100644 index 0000000..1f08fc6 --- /dev/null +++ b/src/Form/Field/Id.php @@ -0,0 +1,9 @@ +has(static::FILE_DELETE_FLAG)) { + return $this->destroy(); + } + + $this->name = $this->getStoreName($image); + + $this->callInterventionMethods($image->getRealPath()); + + return $this->uploadAndDeleteOriginal($image); + } +} diff --git a/src/Form/Field/ImageField.php b/src/Form/Field/ImageField.php new file mode 100644 index 0000000..f41e8a1 --- /dev/null +++ b/src/Form/Field/ImageField.php @@ -0,0 +1,88 @@ +interventionCalls)) { + $image = ImageManagerStatic::make($target); + + foreach ($this->interventionCalls as $call) { + call_user_func_array( + [$image, $call['method']], + $call['arguments'] + )->save($target); + } + } + + return $target; + } + + /** + * Call intervention methods. + * + * @param string $method + * @param array $arguments + * + * @throws \Exception + * + * @return $this + */ + public function __call($method, $arguments) + { + if (static::hasMacro($method)) { + return $this; + } + + if (!class_exists(ImageManagerStatic::class)) { + throw new \Exception('To use image handling and manipulation, please install [intervention/image] first.'); + } + + $this->interventionCalls[] = [ + 'method' => $method, + 'arguments' => $arguments, + ]; + + return $this; + } + + /** + * Render a image form field. + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function render() + { + $this->options(['allowedFileTypes' => ['image']]); + + return parent::render(); + } +} diff --git a/src/Form/Field/Ip.php b/src/Form/Field/Ip.php new file mode 100644 index 0000000..ab651b1 --- /dev/null +++ b/src/Form/Field/Ip.php @@ -0,0 +1,31 @@ + 'ip', + ]; + + public function render() + { + $this->inputmask($this->options); + + $this->prepend('') + ->defaultAttribute('style', 'width: 130px'); + + return parent::render(); + } +} diff --git a/src/Form/Field/Listbox.php b/src/Form/Field/Listbox.php new file mode 100644 index 0000000..529993f --- /dev/null +++ b/src/Form/Field/Listbox.php @@ -0,0 +1,49 @@ +settings = $settings; + + return $this; + } + + public function render() + { + $settings = array_merge($this->settings, [ + 'infoText' => trans('admin.listbox.text_total'), + 'infoTextEmpty' => trans('admin.listbox.text_empty'), + 'infoTextFiltered' => trans('admin.listbox.filtered'), + 'filterTextClear' => trans('admin.listbox.filter_clear'), + 'filterPlaceHolder' => trans('admin.listbox.filter_placeholder'), + ]); + + $settings = json_encode($settings); + + $this->script = <<