diff --git a/.editorconfig b/.editorconfig index 3a050c04..8c863121 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,12 +12,10 @@ insert_final_newline = true trim_trailing_whitespace = true [*.yml] -indent_style = space indent_size = 2 -[*.js] -indent_style = space -indent_size = 4 - [Makefile] indent_style = tab + +[*.neon] +indent_style = tab diff --git a/.gitattributes b/.gitattributes index 76a35dd6..6d2ed281 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,6 @@ docs export-ignore .github export-ignore .coveralls.yml export-ignore Makefile export-ignore +phpstan.neon export-ignore +phpstan-baseline.neon export-ignore +psalm.xml export-ignore diff --git a/.gitignore b/.gitignore index 2e8ba9c3..5209c916 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ phpunit.xml vendor/ composer.lock tmp +.phpunit.result.cache diff --git a/.stickler.yml b/.stickler.yml index d78d6bd9..b8f57db8 100644 --- a/.stickler.yml +++ b/.stickler.yml @@ -1,8 +1,8 @@ --- linters: phpcs: - standard: CakePHP - extensions: 'php,ctp' + standard: CakePHP4 + extensions: 'php' fixer: true fixers: diff --git a/.travis.yml b/.travis.yml index a5a51405..fcfdfade 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: php php: - - 5.6 - - 7.0 - - 7.1 - 7.2 + - 7.3 + - 7.4 env: global: @@ -15,33 +14,33 @@ matrix: fast_finish: true include: - - php: 7.0 + - php: 7.2 env: PHPCS=1 DEFAULT=0 - - php: 7.0 - env: PHPSTAN=1 DEFAULT=0 - + - php: 7.2 + env: STATIC_ANALYSIS=1 DEFAULT=0 + - php: 7.2 env: DEFAULT=1 LOWEST=1 before_script: - - if [[ $TRAVIS_PHP_VERSION != 7.0 ]]; then phpenv config-rm xdebug.ini; fi + - if [[ $TRAVIS_PHP_VERSION != 7.3 ]]; then phpenv config-rm xdebug.ini; fi - if [[ $LOWEST = 0 ]]; then composer install --prefer-dist --no-interaction; fi - if [[ $LOWEST = 1 ]]; then composer update --prefer-lowest --prefer-dist --no-interaction; fi - - if [[ $PHPCS = 1 ]]; then composer require cakephp/cakephp-codesniffer:dev-master; fi - - if [[ $PHPSTAN = 1 ]]; then composer require phpstan/phpstan:^0.9; fi + - if [[ $PHPCS = 1 ]]; then composer require cakephp/cakephp-codesniffer:^4.2; fi + - if [[ $STATIC_ANALYSIS = 1 ]]; then composer require phpstan/phpstan:^0.12 psalm/phar:^3.7; fi script: - - if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION != 7.0 ]]; then vendor/bin/phpunit; fi - - if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.0 ]]; then vendor/bin/phpunit --coverage-clover=clover.xml; fi + - if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION != 7.3 ]]; then vendor/bin/phpunit; fi + - if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.3 ]]; then vendor/bin/phpunit --coverage-clover=clover.xml; fi - if [[ $PHPCS = 1 ]]; then vendor/bin/phpcs -p --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ config/; fi - - if [[ $PHPSTAN = 1 ]]; then vendor/bin/phpstan analyse -c phpstan.neon -l 5 src; fi + - if [[ $STATIC_ANALYSIS = 1 ]]; then vendor/bin/phpstan.phar analyse src && vendor/bin/psalm.phar; fi after_success: - - if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.0 ]]; then bash <(curl -s https://codecov.io/bash); fi + - if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.3 ]]; then bash <(curl -s https://codecov.io/bash); fi notifications: email: false diff --git a/README.md b/README.md index f7c53299..9929ffe3 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ Automated admin backend based on your [Crud](https://github.com/friendsofcake/cr # Installation -For CakePHP 3.x: - ```shell composer require friendsofcake/crud-view ``` diff --git a/composer.json b/composer.json index df3096e4..8ac8a0d4 100644 --- a/composer.json +++ b/composer.json @@ -29,14 +29,14 @@ } ], "require": { - "cakephp/cakephp": "^3.7", - "friendsofcake/crud": "^5.4", - "friendsofcake/bootstrap-ui": "^1.4" + "cakephp/cakephp": "^4.0", + "friendsofcake/crud": "^6.0", + "friendsofcake/bootstrap-ui": "^3.0" }, "require-dev": { - "friendsofcake/cakephp-test-utilities": "^1.0", - "markstory/asset_compress": "^3.2", - "phpunit/phpunit": "^5.7.14|^6.0" + "friendsofcake/cakephp-test-utilities": "^2.0", + "markstory/asset_compress": "^4.0", + "phpunit/phpunit": "~8.5.0" }, "autoload": { "psr-4": { @@ -53,5 +53,6 @@ "issues": "https://github.com/FriendsOfCake/crud-view/issues", "wiki": "http://cakephp.nu/cakephp-crud/", "irc": "irc://irc.freenode.org/friendsofcake" - } + }, + "prefer-stable": true } diff --git a/config/asset_compress.ini b/config/asset_compress.ini index 9e2a1de9..a54b68e2 100644 --- a/config/asset_compress.ini +++ b/config/asset_compress.ini @@ -1,16 +1,16 @@ [crudview.css] -files[]=https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.css -files[]=https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker.min.css -files[]=https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/css/selectize.bootstrap3.min.css +files[]=https://cdn.jsdelivr.net/npm/bootstrap@^4.5/dist/css/bootstrap.min.css +files[]=https://cdn.jsdelivr.net/npm/flatpickr@4.6/dist/flatpickr.min.css +files[]=https://cdn.jsdelivr.net/npm/select2@4.0/dist/css/select2.min.css +files[]=https://cdn.jsdelivr.net/npm/@ttskch/select2-bootstrap4-theme@1.2/dist/select2-bootstrap4.css files[]=plugin:CrudView:css/local.css [crudview_head.js] -files[]=https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js -files[]=https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js -files[]=https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.min.js -files[]=https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js -files[]=https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/js/standalone/selectize.js -files[]=https://cdn.jsdelivr.net/jquery.dirtyforms/1.2.3/jquery.dirtyforms.min.js +files[]=https://cdn.jsdelivr.net/npm/jquery@^3.4/dist/jquery.min.js +files[]=https://cdn.jsdelivr.net/npm/bootstrap@^4.5/dist/js/bootstrap.min.js +files[]=https://cdn.jsdelivr.net/npm/flatpickr@4.6 +files[]=https://cdn.jsdelivr.net/npm/select2@4.0 +files[]=https://cdn.jsdelivr.net/npm/jquery.dirtyforms@2/jquery.dirtyforms.min.js [crudview.js] files[]=plugin:CrudView:js/local.js diff --git a/config/defaults.php b/config/defaults.php index d4241418..c7003bc0 100644 --- a/config/defaults.php +++ b/config/defaults.php @@ -1,4 +1,7 @@ $assets['crudview_head.js']['files'], 'script' => $assets['crudview.js']['files'], ], - 'timezoneAwareDateTimeWidget' => false, + 'datetimePicker' => false, 'useAssetCompress' => Plugin::isLoaded('AssetCompress'), 'tablesBlacklist' => [ 'phinxlog', ], - ] + ], ]; diff --git a/config/forms.php b/config/forms.php index 39d3eed4..7c31f88b 100644 --- a/config/forms.php +++ b/config/forms.php @@ -1,5 +1,5 @@ '{{content}}', 'formGroup' => '{{label}}
{{input}}{{error}}
', diff --git a/config/paginator.php b/config/paginator.php index 82893124..b1743001 100644 --- a/config/paginator.php +++ b/config/paginator.php @@ -1,5 +1,5 @@ '', - 'prevActive' => '' + 'prevActive' => '', ]; diff --git a/docs/_partials/fields/field-blacklist.rst b/docs/_partials/fields/field-blacklist.rst index 33fa05aa..ca3a2e12 100644 --- a/docs/_partials/fields/field-blacklist.rst +++ b/docs/_partials/fields/field-blacklist.rst @@ -8,4 +8,4 @@ functionality: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields_blacklist', ['created', 'modified']); + $action->setConfig('scaffold.fields_blacklist', ['created', 'modified']); diff --git a/docs/_partials/fields/field-settings.rst b/docs/_partials/fields/field-settings.rst index d60bd357..c2093c10 100644 --- a/docs/_partials/fields/field-settings.rst +++ b/docs/_partials/fields/field-settings.rst @@ -8,7 +8,7 @@ to specify settings for a few of the fields, you can use the .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.field_settings', [ + $action->setConfig('scaffold.field_settings', [ 'title' => [ // options here ] diff --git a/docs/_partials/fields/formatter-callable.rst b/docs/_partials/fields/formatter-callable.rst index 84da1b68..31ecdb75 100644 --- a/docs/_partials/fields/formatter-callable.rst +++ b/docs/_partials/fields/formatter-callable.rst @@ -21,7 +21,7 @@ wanted to also display who approved the article: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', [ + $action->setConfig('scaffold.fields', [ 'title', 'published_time' => [ 'formatter' => function ($name, $value, $entity) { @@ -37,7 +37,7 @@ only configure the settings for one or two. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.field_settings', [ + $action->setConfig('scaffold.field_settings', [ 'published_time' => [ 'formatter' => function ($name, Time $value, Entity $entity) { return $value->nice() . sprintf(' (Approved by %s)', $entity->approver->name); @@ -50,7 +50,7 @@ In some cases, it may be useful to access a helper within the callable. For inst .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', [ + $action->setConfig('scaffold.fields', [ 'title', 'external_id' => [ 'formatter' => function ($name, $value, $entity, $options, $View) { diff --git a/docs/_partials/fields/formatter-element.rst b/docs/_partials/fields/formatter-element.rst index 70817440..9dc16c29 100644 --- a/docs/_partials/fields/formatter-element.rst +++ b/docs/_partials/fields/formatter-element.rst @@ -16,7 +16,7 @@ to the same index action by passing some search arguments: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', [ + $action->setConfig('scaffold.fields', [ 'published_time' => [ 'formatter' => 'element', 'element' => 'search/published_time', @@ -29,7 +29,7 @@ it is just a matter of creating the element file with the right content: .. code-block:: php - // src/Template/Element/search/published_time.ctp + // templates/element/search/published_time.ctp echo $this->Html->link($value->timeAgoInWords(), [ 'action' => $options['action'], diff --git a/docs/_partials/fields/index.rst b/docs/_partials/fields/index.rst index 4189e4e3..f3519b5a 100644 --- a/docs/_partials/fields/index.rst +++ b/docs/_partials/fields/index.rst @@ -8,7 +8,7 @@ in scope. To limit the fields used, simply specify an array of fields. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', ['title', 'description']); + $action->setConfig('scaffold.fields', ['title', 'description']); You may also specify an options array. For forms, *CrudView* makes use of the ``FormHelper::inputs()`` method and will pass your array values as options when @@ -17,7 +17,7 @@ generating the output. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', [ + $action->setConfig('scaffold.fields', [ 'title', 'thread_id' => [ 'type' => 'text' diff --git a/docs/_partials/fields/sorting.rst b/docs/_partials/fields/sorting.rst index 52d366b2..f3216b12 100644 --- a/docs/_partials/fields/sorting.rst +++ b/docs/_partials/fields/sorting.rst @@ -8,7 +8,7 @@ the ``disableSort`` option: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', [ + $action->setConfig('scaffold.fields', [ 'title' => [ 'disableSort' => true, ] diff --git a/docs/_partials/fields/tab-groups.rst b/docs/_partials/fields/tab-groups.rst index 4fc9842e..f23e99a3 100644 --- a/docs/_partials/fields/tab-groups.rst +++ b/docs/_partials/fields/tab-groups.rst @@ -7,7 +7,7 @@ configuration key: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_tab_groups', [ + $action->setConfig('scaffold.form_tab_groups', [ 'First Tab Header' => ['field_1', 'field_2'], 'Second Tab Header' => ['field_3', 'field_4'], ]); @@ -19,4 +19,4 @@ the primary group's name using `scaffold.form_primary_tab` config. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_primary_tab', 'Key Info'); + $action->setConfig('scaffold.form_primary_tab', 'Key Info'); diff --git a/docs/_partials/pages/dashboard/elements.rst b/docs/_partials/pages/dashboard/elements.rst index 81eff7ec..e0c394e3 100644 --- a/docs/_partials/pages/dashboard/elements.rst +++ b/docs/_partials/pages/dashboard/elements.rst @@ -2,15 +2,15 @@ Available Elements ------------------ All the *CrudView* templates are built from several elements that can be -overridden by creating them in your own ``src/Template/Element`` folder. The +overridden by creating them in your own ``templates/element`` folder. The following sections will list all the elements that can be overridden for each type of action. In general, if you want to override a template, it is a good idea to copy the original implementation from -``vendor/friendsofcake/crud-view/src/Template/Element`` +``vendor/friendsofcake/crud-view/templates/element`` action-header - Create ``src/Template/Element/action-header.ctp`` to have full control over + Create ``templates/element/action-header.ctp`` to have full control over what is displayed at the top of the page. This is shared across all page types. diff --git a/docs/_partials/pages/form/elements.rst b/docs/_partials/pages/form/elements.rst index 27d73cd3..fc8bb2dd 100644 --- a/docs/_partials/pages/form/elements.rst +++ b/docs/_partials/pages/form/elements.rst @@ -2,20 +2,20 @@ Available Elements ------------------ All the *CrudView* templates are built from several elements that can be -overridden by creating them in your own ``src/Template/Element`` folder. The +overridden by creating them in your own ``templates/element`` folder. The following sections will list all the elements that can be overridden for each type of action. In general, if you want to override a template, it is a good idea to copy the original implementation from -``vendor/friendsofcake/crud-view/src/Template/Element`` +``vendor/friendsofcake/crud-view/templates/element`` action-header - Create ``src/Template/Element/action-header.ctp`` to have full control over + Create ``templates/element/action-header.ctp`` to have full control over what is displayed at the top of the page. This is shared across all page types. form/buttons - Create ``src/Template/Element/form/buttons.ctp`` to change what is displayed + Create ``templates/element/form/buttons.ctp`` to change what is displayed for form submission. You can expect the ``$formSubmitButtonText`` and ``$formSubmitExtraButtons`` variables to be available diff --git a/docs/_partials/pages/form/submission.rst b/docs/_partials/pages/form/submission.rst index c7e502d9..b6d3781b 100644 --- a/docs/_partials/pages/form/submission.rst +++ b/docs/_partials/pages/form/submission.rst @@ -22,4 +22,4 @@ However, you may force a user prompt by enabling dirty form checks using the .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_enable_dirty_check', true); + $action->setConfig('scaffold.form_enable_dirty_check', true); diff --git a/docs/_partials/pages/form/submit-buttons.rst b/docs/_partials/pages/form/submit-buttons.rst index b161026f..4f60e1c6 100644 --- a/docs/_partials/pages/form/submit-buttons.rst +++ b/docs/_partials/pages/form/submit-buttons.rst @@ -10,7 +10,7 @@ You can change the submit button text from it's default using the .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_submit_button_text', _('Submit')); + $action->setConfig('scaffold.form_submit_button_text', _('Submit')); Modifying the Default Extra Buttons ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -27,7 +27,7 @@ to true: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_submit_extra_buttons', true); + $action->setConfig('scaffold.form_submit_extra_buttons', true); You can also customize this by using the ``scaffold.form_submit_extra_buttons`` configuration key as follows: @@ -35,7 +35,7 @@ configuration key as follows: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_submit_extra_buttons', [ + $action->setConfig('scaffold.form_submit_extra_buttons', [ [ 'title' => __d('crud', 'Save & continue editing'), 'options' => ['class' => 'btn btn-success btn-save-continue', 'name' => '_edit', 'value' => true], @@ -73,7 +73,7 @@ to true: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_submit_extra_left_buttons', true); + $action->setConfig('scaffold.form_submit_extra_left_buttons', true); You can also customize this by using the ``scaffold.form_submit_extra_left_buttons`` configuration key as follows: @@ -81,7 +81,7 @@ configuration key as follows: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_submit_extra_left_buttons', [ + $action->setConfig('scaffold.form_submit_extra_left_buttons', [ [ 'title' => __d('crud', 'Save & continue editing'), 'options' => ['class' => 'btn btn-success btn-save-continue', 'name' => '_edit', 'value' => true], @@ -98,7 +98,7 @@ completely: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_submit_extra_buttons', false); + $action->setConfig('scaffold.form_submit_extra_buttons', false); Disabling the Default Extra Left Buttons ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -108,4 +108,4 @@ Disabling the default left extra buttons can also be done in a similar fashion: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.form_submit_extra_left_buttons', false); + $action->setConfig('scaffold.form_submit_extra_left_buttons', false); diff --git a/docs/_partials/pages/index/bulk-actions.rst b/docs/_partials/pages/index/bulk-actions.rst index f4d39dae..e50d5867 100644 --- a/docs/_partials/pages/index/bulk-actions.rst +++ b/docs/_partials/pages/index/bulk-actions.rst @@ -22,6 +22,6 @@ array of key/value pairs, where the key is the url and the value is the title. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.bulk_actions', [ + $action->setConfig('scaffold.bulk_actions', [ Router::url(['action' => 'deleteAll']) => __('Delete records'), ]); diff --git a/docs/_partials/pages/index/buttons.rst b/docs/_partials/pages/index/buttons.rst index 9c8d8a54..2b053488 100644 --- a/docs/_partials/pages/index/buttons.rst +++ b/docs/_partials/pages/index/buttons.rst @@ -10,12 +10,12 @@ key: $action = $this->Crud->action(); // restrict to just the add button, which will show up globally - $action->config('scaffold.actions', [ + $action->setConfig('scaffold.actions', [ 'add' ]); // restrict to just the delete/edit/view actions, which are scoped to entities - $action->config('scaffold.actions', [ + $action->setConfig('scaffold.actions', [ 'delete', 'edit', 'view', @@ -27,7 +27,7 @@ generating action buttons. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.actions', [ + $action->setConfig('scaffold.actions', [ 'duplicate' => [ // An alternative title for the action 'link_title' => 'Duplicate this record', @@ -60,7 +60,7 @@ the link by default: // For the PostsController, will generate // /posts/translate/english/1 - $action->config('scaffold.actions', [ + $action->setConfig('scaffold.actions', [ 'translate' => [ 'url' => ['action' => 'translate', 'english'] ] @@ -76,7 +76,7 @@ specified. // For the PostsController, will generate // /posts/translate/1/english - $action->config('scaffold.actions', [ + $action->setConfig('scaffold.actions', [ 'translate' => [ 'url' => ['action' => 'translate', ':primaryKey:', 'english'] ] @@ -92,7 +92,7 @@ Crud action classes are mapped but should not all be shown on the main UI. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.actions_blacklist', ['add', 'delete']); + $action->setConfig('scaffold.actions_blacklist', ['add', 'delete']); By default, we blacklist the action which is mapped to ``Crud.LookupAction``. As this action is meant to be used solely for autocompletion, it *cannot* be removed @@ -108,8 +108,8 @@ configuration key. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.actions', ['view', 'edit', 'delete']); - $action->config('scaffold.action_groups', [ + $action->setConfig('scaffold.actions', ['view', 'edit', 'delete']); + $action->setConfig('scaffold.action_groups', [ 'Actions' => [ 'view', 'edit', @@ -125,8 +125,8 @@ You can specify multiple action groups: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.actions', ['view', 'edit', 'delete', 'disable', 'delete']); - $action->config('scaffold.action_groups', [ + $action->setConfig('scaffold.actions', ['view', 'edit', 'delete', 'disable', 'delete']); + $action->setConfig('scaffold.action_groups', [ 'Actions' => [ 'view', 'edit', @@ -143,8 +143,8 @@ Finally, you can also set configuration for each entry in an action group: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.actions', ['view', 'edit', 'delete', 'english', 'spanish']); - $action->config('scaffold.action_groups', [ + $action->setConfig('scaffold.actions', ['view', 'edit', 'delete', 'english', 'spanish']); + $action->setConfig('scaffold.action_groups', [ 'Actions' => [ 'view', 'edit', diff --git a/docs/_partials/pages/index/download-format-links.rst b/docs/_partials/pages/index/download-format-links.rst index eba4d217..0c6f0f53 100644 --- a/docs/_partials/pages/index/download-format-links.rst +++ b/docs/_partials/pages/index/download-format-links.rst @@ -17,7 +17,7 @@ Each sub-array should contain ``title`` and ``url`` parameters. // include the querystring argument as specified or you will lose any // currently applied filters $action = $this->Crud->action(); - $action->config('scaffold.index_formats', [ + $action->setConfig('scaffold.index_formats', [ [ 'title' => 'JSON', 'url' => ['_ext' => 'json', '?' => $this->request->getQueryParams()] @@ -83,7 +83,7 @@ can be modified to add a CSV Download Link. $this->set('_extract', ['id', 'active', 'title', 'created']); } - $this->Crud->action()->config('scaffold.index_formats', [ + $this->Crud->action()->setConfig('scaffold.index_formats', [ [ 'title' => 'CSV', 'url' => ['_ext' => 'csv', '?' => $this->request->getQueryParams()] diff --git a/docs/_partials/pages/index/filters.rst b/docs/_partials/pages/index/filters.rst index 574af032..9ef14269 100644 --- a/docs/_partials/pages/index/filters.rst +++ b/docs/_partials/pages/index/filters.rst @@ -8,12 +8,14 @@ to be installed and filters configured for your model using the search manager. .. code-block:: php addBehavior('Search.Search'); $this->searchManager() ->useCollection('default') diff --git a/docs/_partials/pages/index/finder-scopes.rst b/docs/_partials/pages/index/finder-scopes.rst index 05c9f87b..fafbfa71 100644 --- a/docs/_partials/pages/index/finder-scopes.rst +++ b/docs/_partials/pages/index/finder-scopes.rst @@ -12,7 +12,7 @@ Each sub-array should contain ``title`` and ``finder`` parameters. .. code-block:: php - $this->Crud->action()->config('scaffold.index_finder_scopes', [ + $this->Crud->action()->setConfig('scaffold.index_finder_scopes', [ [ 'title' => __('All'), 'finder' => 'all', @@ -54,7 +54,7 @@ result-set. This can be done in the mapped action as follows: public function index() { - $this->Crud->action()->config('scaffold.index_finder_scopes', [ + $this->Crud->action()->setConfig('scaffold.index_finder_scopes', [ [ 'title' => __('All'), 'finder' => 'all', diff --git a/docs/basic-usage.rst b/docs/basic-usage.rst index d860cc87..1abaf988 100644 --- a/docs/basic-usage.rst +++ b/docs/basic-usage.rst @@ -14,7 +14,7 @@ action to the ``Crud`` component and the ``CrudView.View`` as a listener. .. code-block:: php - public function initialize() + public function initialize(): void { $this->loadComponent('Crud.Crud', [ 'actions' => [ @@ -35,6 +35,8 @@ page. .. code-block:: php Crud->action(); // Gets the IndexAction object - debug($action->config()); // Show all configuration related to this action + debug($action->getConfig()); // Show all configuration related to this action return $this->Crud->execute(); } @@ -77,7 +79,7 @@ Fore example you may want to not fetch the ``Authors`` association of the public function index() { $action = $this->Crud->action(); - $action->config('scaffold.relations_blacklist', ['Authors', ...]); + $action->setConfig('scaffold.relations_blacklist', ['Authors', ...]); return $this->Crud->execute(); } @@ -89,7 +91,7 @@ the ``scaffold.relations`` configuration key: public function index() { $action = $this->Crud->action(); - $action->config('scaffold.relations', ['Categories', 'Tags']); + $action->setConfig('scaffold.relations', ['Categories', 'Tags']); return $this->Crud->execute(); } @@ -125,7 +127,7 @@ displayed in the index table: public function index() { $action = $this->Crud->action(); - $action->config('scaffold.fields_blacklist', ['created', 'modified']); + $action->setConfig('scaffold.fields_blacklist', ['created', 'modified']); return $this->Crud->execute(); } @@ -137,7 +139,7 @@ appear in the index table: public function index() { $action = $this->Crud->action(); - $action->config('scaffold.fields', ['title', 'body', 'category', 'published_time']); + $action->setConfig('scaffold.fields', ['title', 'body', 'category', 'published_time']); return $this->Crud->execute(); } @@ -157,7 +159,7 @@ table: public function index() { $action = $this->Crud->action(); - $action->config('scaffold.actions_blacklist', ['delete']); + $action->setConfig('scaffold.actions_blacklist', ['delete']); return $this->Crud->execute(); } @@ -169,7 +171,7 @@ specifically displayed in the index view: public function index() { $action = $this->Crud->action(); - $action->config('scaffold.actions', ['view', 'add', 'edit']); + $action->setConfig('scaffold.actions', ['view', 'add', 'edit']); return $this->Crud->execute(); } @@ -215,7 +217,7 @@ the ``Crud`` component. This is the recommended configuration: .. code-block:: php - public function initialize() + public function initialize(): void { $this->loadComponent('Crud.Crud', [ 'actions' => [ @@ -251,7 +253,7 @@ fields in the form: public function add() { $action = $this->Crud->action(); - $action->config('scaffold.fields_blacklist', ['created', 'modified']); + $action->setConfig('scaffold.fields_blacklist', ['created', 'modified']); return $this->Crud->execute(); } @@ -263,7 +265,7 @@ form by using the ``scaffold.fields`` configuration key: public function edit() { $action = $this->Crud->action(); - $action->config('scaffold.fields', ['title', 'body', 'category_id']); + $action->setConfig('scaffold.fields', ['title', 'body', 'category_id']); return $this->Crud->execute(); } @@ -276,7 +278,7 @@ add the ``placeholder`` property to the ``title`` input: public function add() { $action = $this->Crud->action(); - $action->config('scaffold.fields', [ + $action->setConfig('scaffold.fields', [ 'title' => ['placeholder' => 'Insert a title here'], 'body', 'category_id' @@ -335,7 +337,7 @@ to be saved. For this case, you may use the ``scaffold.relations`` and { $action $this->Crud->action(); // Only fetch association info for Categories and Tags - $action->config('scaffold.relations', ['Categories', 'Tags']); + $action->setConfig('scaffold.relations', ['Categories', 'Tags']); return $this->Crud->execute(); } @@ -348,7 +350,7 @@ specify those association that should not be added to ``contain()``: { $action $this->Crud->action(); // Only fetch association info for Categories and Tags - $action->config('scaffold.relations_blacklist', ['Authors']); + $action->setConfig('scaffold.relations_blacklist', ['Authors']); return $this->Crud->execute(); } @@ -364,7 +366,7 @@ buttons. If you wish to only keep the "Save" button, you set the public function add() { $action = $this->Crud->action(); - $action->config('scaffold.form_submit_extra_buttons', false); + $action->setConfig('scaffold.form_submit_extra_buttons', false); return $this->Crud->execute(); } @@ -410,7 +412,7 @@ displayed in the view table: public function view() { $action = $this->Crud->action(); - $action->config('scaffold.fields_blacklist', ['created', 'modified']); + $action->setConfig('scaffold.fields_blacklist', ['created', 'modified']); return $this->Crud->execute(); } @@ -422,7 +424,7 @@ appear in the index table: public function view() { $action = $this->Crud->action(); - $action->config('scaffold.fields', ['title', 'body', 'category', 'published_time']); + $action->setConfig('scaffold.fields', ['title', 'body', 'category', 'published_time']); return $this->Crud->execute(); } @@ -441,7 +443,7 @@ Fore example you may want to not fetch the ``Authors`` association of the public function view() { $action = $this->Crud->action(); - $action->config('scaffold.relations_blacklist', ['Authors', ...]); + $action->setConfig('scaffold.relations_blacklist', ['Authors', ...]); return $this->Crud->execute(); } @@ -453,7 +455,7 @@ the ``scaffold.relations`` configuration key: public function view() { $action = $this->Crud->action(); - $action->config('scaffold.relations', ['Categories', 'Tags']); + $action->setConfig('scaffold.relations', ['Categories', 'Tags']); return $this->Crud->execute(); } diff --git a/docs/customizing-templates.rst b/docs/customizing-templates.rst index 35475596..0d56f049 100644 --- a/docs/customizing-templates.rst +++ b/docs/customizing-templates.rst @@ -28,7 +28,7 @@ title for any of the fields by using the ``scaffold.fields`` configuration .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', [ + $action->setConfig('scaffold.fields', [ 'author_id' => ['label' => 'Author Name'], // The rest of the fields to display here ]); @@ -66,10 +66,10 @@ Overriding Template Elements ---------------------------- All the *CrudView* templates are built from several elements that can be -overridden by creating them in your own ``src/Template/Element`` folder. The +overridden by creating them in your own ``templates/element`` folder. The following sections will list all the elements that can be overridden for each type of action. In general, if you want to override a template, it is a good idea to copy the original implementation from -``vendor/friendsofcake/crud-view/src/Template/Element`` +``vendor/friendsofcake/crud-view/templates/element`` diff --git a/docs/dashboard-pages/customizing-the-dashboard-page.rst b/docs/dashboard-pages/customizing-the-dashboard-page.rst index d7454e5a..5dc516d2 100644 --- a/docs/dashboard-pages/customizing-the-dashboard-page.rst +++ b/docs/dashboard-pages/customizing-the-dashboard-page.rst @@ -9,7 +9,7 @@ To use the "Dashboard", the custom ``DashboardAction`` needs to be mapped: .. code-block:: php - public function initialize() + public function initialize(): void { parent::initialize(); @@ -91,7 +91,7 @@ show a single subheading for your Dashboard. In addition to showing a title, it is also possible to show a list of links. This can be done by adding a ``links`` key with an array of ``LinkItem`` objects as the value. -Links containing urls for external websites will open in a new window by default. +Links containing urls for external websites will open in a new window by default. .. code-block:: php diff --git a/docs/general-configuration/breadcrumbs.rst b/docs/general-configuration/breadcrumbs.rst index 287d1baf..25653b54 100644 --- a/docs/general-configuration/breadcrumbs.rst +++ b/docs/general-configuration/breadcrumbs.rst @@ -24,7 +24,7 @@ The ``scaffold.breadcrumbs`` configuration key takes an array of use CrudView\Breadcrumb\Breadcrumb; - $this->Crud->action()->config('scaffold.breadcrumbs', [ + $this->Crud->action()->setConfig('scaffold.breadcrumbs', [ new BreadCrumb('Home'), ]); @@ -35,7 +35,7 @@ breadcrumb entries. However, they also take ``url`` and ``options`` arrays: use CrudView\Breadcrumb\Breadcrumb; - $this->Crud->action()->config('scaffold.breadcrumbs', [ + $this->Crud->action()->setConfig('scaffold.breadcrumbs', [ new BreadCrumb('Home', ['controller' => 'Posts', 'action' => 'index'], ['class' => 'derp']), ]); @@ -51,14 +51,14 @@ You may also set any given breadcrumb to "active" by either setting the // Using the "class" option method: use CrudView\Breadcrumb\Breadcrumb; - $this->Crud->action()->config('scaffold.breadcrumbs', [ + $this->Crud->action()->setConfig('scaffold.breadcrumbs', [ new BreadCrumb('Home', '#', ['class' => 'active']), ]); // Using the ActiveBreadCrumb method: use CrudView\Breadcrumb\ActiveBreadCrumb; - $this->Crud->action()->config('scaffold.breadcrumbs', [ + $this->Crud->action()->setConfig('scaffold.breadcrumbs', [ new ActiveBreadCrumb('Home', '#'), ]); diff --git a/docs/general-configuration/sidebar-navigation.rst b/docs/general-configuration/sidebar-navigation.rst index 301ed7f4..a15406e8 100644 --- a/docs/general-configuration/sidebar-navigation.rst +++ b/docs/general-configuration/sidebar-navigation.rst @@ -14,7 +14,7 @@ You can specify the exact tables to show in the sidebar via the .. code-block:: php // only show the posts table - $this->Crud->action()->config('scaffold.tables', ['posts']); + $this->Crud->action()->setConfig('scaffold.tables', ['posts']); Blacklisting tables @@ -27,7 +27,7 @@ the ``scaffold.tables_blacklist`` configuration key to specify tables to .. code-block:: php // do not show the ``phinxlog`` and ``users`` tables - $this->Crud->action()->config('scaffold.tables_blacklist', [ + $this->Crud->action()->setConfig('scaffold.tables_blacklist', [ 'phinxlog', 'users', ]); @@ -47,7 +47,7 @@ The sidebar navigation can also be completely disabled by setting the value to ` .. code-block:: php - $this->Crud->action()->config('scaffold.sidebar_navigation', false); + $this->Crud->action()->setConfig('scaffold.sidebar_navigation', false); Custom Menus ------------ @@ -60,7 +60,7 @@ provide your own menu to be rendered in its place: use CrudView\Menu\MenuDivider; use CrudView\Menu\MenuItem; - $this->Crud->action()->config('scaffold.sidebar_navigation', [ + $this->Crud->action()->setConfig('scaffold.sidebar_navigation', [ new MenuItem('Section Header'), new MenuItem( 'CrudView Docs', diff --git a/docs/general-configuration/site-title-options.rst b/docs/general-configuration/site-title-options.rst index 56ad5dcc..2e9e61e2 100644 --- a/docs/general-configuration/site-title-options.rst +++ b/docs/general-configuration/site-title-options.rst @@ -15,7 +15,7 @@ not set, it will fallback to the following alternative: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.site_title', 'My Admin Site'); + $action->setConfig('scaffold.site_title', 'My Admin Site'); Site Title Link --------------- @@ -27,7 +27,7 @@ route arrays are supported. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.site_title_link', '/'); + $action->setConfig('scaffold.site_title_link', '/'); Site Title Image ---------------- @@ -38,7 +38,7 @@ title link. If set, it replaces ``scaffold.site_title``. $action = $this->Crud->action(); // Use an image included in your codebase - $action->config('scaffold.site_title_image', 'site_image.png'); + $action->setConfig('scaffold.site_title_image', 'site_image.png'); // Use an exact url - $action->config('scaffold.site_title_image', 'http://www.google.com/images/logos/google_logo_41.png'); + $action->setConfig('scaffold.site_title_image', 'http://www.google.com/images/logos/google_logo_41.png'); diff --git a/docs/general-configuration/utility-navigation.rst b/docs/general-configuration/utility-navigation.rst index 50ed5279..bc7eb605 100644 --- a/docs/general-configuration/utility-navigation.rst +++ b/docs/general-configuration/utility-navigation.rst @@ -12,7 +12,7 @@ The utility navigation can also be completely disabled by setting the value to ` .. code-block:: php - $this->Crud->action()->config('scaffold.utility_navigation', false); + $this->Crud->action()->setConfig('scaffold.utility_navigation', false); Custom Menus ------------ @@ -27,7 +27,7 @@ provide your own menu to be rendered in its place: use CrudView\Menu\MenuItem; $action = $this->Crud->action(); - $action->config('scaffold.utility_navigation', [ + $action->setConfig('scaffold.utility_navigation', [ new MenuItem( 'CrudView Docs', 'https://crud-view.readthedocs.io/en/latest/contents.html', diff --git a/docs/index-pages/components/blog.rst b/docs/index-pages/components/blog.rst index 24613219..e6a8f392 100644 --- a/docs/index-pages/components/blog.rst +++ b/docs/index-pages/components/blog.rst @@ -19,8 +19,8 @@ The blog index type has two main options: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.index_title_field', 'name'); - $action->config('scaffold.index_body_field', 'content'); + $action->setConfig('scaffold.index_title_field', 'name'); + $action->setConfig('scaffold.index_body_field', 'content'); Available Variables ------------------- diff --git a/docs/index-pages/components/custom.rst b/docs/index-pages/components/custom.rst index 03531bf3..e76f7e87 100644 --- a/docs/index-pages/components/custom.rst +++ b/docs/index-pages/components/custom.rst @@ -6,7 +6,7 @@ To use a custom index element, you can set the ``scaffold.index_type`` config op .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.index_type', 'an_element'); + $action->setConfig('scaffold.index_type', 'an_element'); Available Variables ------------------- diff --git a/docs/index-pages/components/gallery.rst b/docs/index-pages/components/gallery.rst index 3f051a10..8cf1d516 100644 --- a/docs/index-pages/components/gallery.rst +++ b/docs/index-pages/components/gallery.rst @@ -6,7 +6,7 @@ Render your index page as a gallery. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.index_type', 'gallery'); + $action->setConfig('scaffold.index_type', 'gallery'); Customizing the Gallery fields ------------------------------ @@ -26,10 +26,10 @@ The gallery index type has several options: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.index_title_field', 'name'); - $action->config('scaffold.index_image_field', 'image'); - $action->config('scaffold.index_body_field', 'content'); - $action->config('scaffold.index_gallery_css_classes', 'col-sm-4 col-md-2'); + $action->setConfig('scaffold.index_title_field', 'name'); + $action->setConfig('scaffold.index_image_field', 'image'); + $action->setConfig('scaffold.index_body_field', 'content'); + $action->setConfig('scaffold.index_gallery_css_classes', 'col-sm-4 col-md-2'); Customizing Gallery Field Output -------------------------------- @@ -40,7 +40,7 @@ configuration key for formatting each field: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', [ + $action->setConfig('scaffold.fields', [ 'image' => [ 'width' => '240', 'height' => '240' diff --git a/docs/index-pages/components/table.rst b/docs/index-pages/components/table.rst index 43f9db3a..5124c794 100644 --- a/docs/index-pages/components/table.rst +++ b/docs/index-pages/components/table.rst @@ -15,7 +15,7 @@ config option. .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', ['id', 'title']); + $action->setConfig('scaffold.fields', ['id', 'title']); To specify the title used in the pagination header, you need to set ``scaffold.fields`` to an associative array and use the ``title`` parameter: @@ -23,7 +23,7 @@ To specify the title used in the pagination header, you need to set .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', [ + $action->setConfig('scaffold.fields', [ 'author_id' => ['title' => 'Author Name'], ]); @@ -31,43 +31,43 @@ Index Action Elements --------------------- All the *CrudView* templates are built from several elements that can be -overridden by creating them in your own ``src/Template/Element`` folder. The +overridden by creating them in your own ``templates/element`` folder. The following sections will list all the elements that can be overridden for each type of action. In general, if you want to override a template, it is a good idea to copy the original implementation from -``vendor/friendsofcake/crud-view/src/Template/Element`` +``vendor/friendsofcake/crud-view/templates/element`` search - Create ``src/Template/Element/search.ctp`` for having full control over how + Create ``templates/element/search.ctp`` for having full control over how the search filters are displayed in your pagination table. You can expect the ``$searchInputs`` and ``$searchOptions`` variables to be available index/table - Create ``src/Template/Element/index/table.ctp`` To implement your own + Create ``templates/element/index/table.ctp`` To implement your own table. index/pagination - Create ``src/Template/Element/index/pagination.ctp`` To implement your own + Create ``templates/element/index/pagination.ctp`` To implement your own pagination links and counter. index/bulk_actions/table - Create ``src/Template/Element/index/bulk_actions/table.ctp`` for changing how + Create ``templates/element/index/bulk_actions/table.ctp`` for changing how the bulk action inputs for the whole table. You can expect the ``$bulkActions``, ``$primaryKey`` and ``$singularVar`` variables to be available. index/bulk_actions/record - Create ``src/Template/Element/index/bulk_actions/record.ctp`` for changing how + Create ``templates/element/index/bulk_actions/record.ctp`` for changing how the bulk action inputs for each row are displayed. You can expect the ``$bulkActions``, ``$primaryKey`` and ``$singularVar`` variables to be available. index/bulk_actions/form_start - Create ``src/Template/Element/index/bulk_actions/form_start.ctp`` To customize + Create ``templates/element/index/bulk_actions/form_start.ctp`` To customize the Form create call for bulk actions index/bulk_actions/form_end - Create ``src/Template/Element/index/bulk_actions/form_end.ctp`` To customize + Create ``templates/element/index/bulk_actions/form_end.ctp`` To customize the Form end call for bulk actions diff --git a/docs/index-pages/customizing-the-index-page.rst b/docs/index-pages/customizing-the-index-page.rst index 047d8c7f..ac0b44a5 100644 --- a/docs/index-pages/customizing-the-index-page.rst +++ b/docs/index-pages/customizing-the-index-page.rst @@ -11,7 +11,7 @@ in scope. To limit the fields used, simply specify an array of fields: .. code-block:: php $action = $this->Crud->action(); - $action->config('scaffold.fields', ['title', 'description']); + $action->setConfig('scaffold.fields', ['title', 'description']); .. include:: /_partials/fields/field-settings.rst .. include:: /_partials/fields/field-blacklist.rst diff --git a/docs/installation.rst b/docs/installation.rst index 9135c205..29d79fc6 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -6,7 +6,7 @@ Installing CRUD view requires only a few steps Requirements ------------ -* CakePHP 3.x +* CakePHP 4.x Getting the Code ---------------- diff --git a/docs/quick-start.rst b/docs/quick-start.rst index e96a14c7..7f5c6762 100644 --- a/docs/quick-start.rst +++ b/docs/quick-start.rst @@ -19,13 +19,15 @@ If you haven't configured the CRUD plugin already, add the following lines to yo .. code-block:: php viewBuilder()->getClassName() === null) { $this->viewBuilder()->setClassName('CrudView\View\CrudView'); - } - - // For CakePHP 3.1+ - if ($this->viewBuilder()->className() === null) { - $this->viewBuilder()->className('CrudView\View\CrudView'); - } - - // For CakePHP 3.0 - /* - if ($this->viewClass === null) { - $this->viewClass = 'CrudView\View\CrudView'; } - */ } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..4886d491 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,87 @@ +parameters: + ignoreErrors: + - + message: "#^Access to an undefined property object\\:\\:\\$query\\.$#" + count: 4 + path: src/Listener/ViewListener.php + + - + message: "#^Access to an undefined property object\\:\\:\\$params\\.$#" + count: 1 + path: src/Listener/ViewListener.php + + - + message: "#^Access to an undefined property object\\:\\:\\$element\\.$#" + count: 1 + path: src/Listener/ViewListener.php + + - + message: "#^Access to an undefined property object\\:\\:\\$type\\.$#" + count: 1 + path: src/Listener/ViewListener.php + + - + message: "#^Parameter \\#2 \\$arr2 of function array_diff_key expects array, array\\|false given\\.$#" + count: 1 + path: src/Listener/ViewListener.php + + - + message: "#^Result of \\|\\| is always false\\.$#" + count: 1 + path: src/Listener/ViewListener.php + + - + message: "#^Cannot unset offset mixed on array\\(\\)\\.$#" + count: 2 + path: src/Listener/ViewListener.php + + - + message: "#^Parameter \\#1 \\$field of method CrudView\\\\Listener\\\\ViewListener\\:\\:_deriveFieldFromContext\\(\\) expects string, array\\\\|string\\|null given\\.$#" + count: 1 + path: src/Listener/ViewListener.php + + - + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:searchManager\\(\\)\\.$#" + count: 1 + path: src/Listener/ViewSearchListener.php + + - + message: "#^Offset 'data\\-wrap' does not exist on array\\(\\?'data\\-enable\\-seconds' \\=\\> 'true', 'data\\-date\\-format' \\=\\> string, \\?'data\\-enable\\-time' \\=\\> 'true', \\?'data\\-alt\\-format' \\=\\> mixed, \\?'data\\-alt\\-input' \\=\\> 'true', \\?'data\\-no\\-calendar' \\=\\> 'true'\\)\\.$#" + count: 1 + path: src/View/Widget/DateTimeWidget.php + + - + message: "#^Cannot assign offset 'class' to array\\\\|string\\.$#" + count: 1 + path: src/View/Widget/DateTimeWidget.php + + - + message: "#^Cannot assign offset 'datetimePicker' to array\\\\|string\\.$#" + count: 1 + path: src/View/Widget/DateTimeWidget.php + + - + message: "#^Binary operation \"\\+\" between string and array\\(\\?'data\\-enable\\-seconds' \\=\\> 'true', 'data\\-date\\-format' \\=\\> string, \\?'data\\-enable\\-time' \\=\\> 'true', \\?'data\\-alt\\-format' \\=\\> mixed, \\?'data\\-alt\\-input' \\=\\> 'true', \\?'data\\-no\\-calendar' \\=\\> 'true', \\?'data\\-alt\\-input\\-class' \\=\\> string, 'class' \\=\\> array\\('input\\-group', 'flatpickr'\\)\\) results in an error\\.$#" + count: 1 + path: src/View/Widget/DateTimeWidget.php + + - + message: "#^Binary operation \"\\+\\=\" between array\\\\|string and array\\(\\?'data\\-enable\\-seconds' \\=\\> 'true', 'data\\-date\\-format' \\=\\> string, \\?'data\\-enable\\-time' \\=\\> 'true', \\?'data\\-alt\\-format' \\=\\> mixed, \\?'data\\-alt\\-input' \\=\\> 'true', \\?'data\\-no\\-calendar' \\=\\> 'true'\\) results in an error\\.$#" + count: 1 + path: src/View/Widget/DateTimeWidget.php + + - + message: "#^Offset 'name' does not exist on array\\\\|string\\.$#" + count: 1 + path: src/View/Widget/DateTimeWidget.php + + - + message: "#^Offset 'templateVars' does not exist on array\\\\|string\\.$#" + count: 2 + path: src/View/Widget/DateTimeWidget.php + + - + message: "#^Parameter \\#1 \\$options of method Cake\\\\View\\\\StringTemplate\\:\\:formatAttributes\\(\\) expects array\\|null, array\\\\|string given\\.$#" + count: 1 + path: src/View/Widget/DateTimeWidget.php + diff --git a/phpstan.neon b/phpstan.neon index f8f597a8..8473436d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,10 @@ +includes: + - phpstan-baseline.neon + parameters: - universalObjectCratesClasses: - - Crud\Event\Subject - ignoreErrors: - - '#Access to an undefined property object::\$element#' - - '#Access to an undefined property object::\$params#' - - '#Access to an undefined property object::\$query#' - - '#Access to an undefined property object::\$type#' - - '#Parameter \#1 $dateTime of method Cake\\View\\Helper\\TimeHelper::timeAgoInWords\(\) expects Cake\\Chronos\\ChronosInterface|DateTime|int|string, DateTime|DateTimeImmutable|int|string given#' - - '#Parameter \#1 $dateString of method Cake\\View\\Helper\\TimeHelper::nice() expects DateTime|int|string|null, DateTime|DateTimeImmutable|int|string given#' + level: 7 + checkMissingIterableValueType: false + paths: + - src + universalObjectCratesClasses: + - Crud\Event\Subject diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 445791ae..279d1795 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,7 +3,6 @@ colors="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" bootstrap="./tests/bootstrap.php" > diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..048f79f3 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Action/DashboardAction.php b/src/Action/DashboardAction.php index e9f6e534..fd8b12b1 100644 --- a/src/Action/DashboardAction.php +++ b/src/Action/DashboardAction.php @@ -1,9 +1,11 @@ options; } diff --git a/src/Dashboard/Dashboard.php b/src/Dashboard/Dashboard.php index cb4599e2..c76cac32 100644 --- a/src/Dashboard/Dashboard.php +++ b/src/Dashboard/Dashboard.php @@ -1,4 +1,6 @@ get('children'); if (isset($children[$column])) { @@ -45,11 +47,11 @@ public function getColumnChildren($column) /** * Adds a Cell to a given column * - * @param Cell $module instance of Cell + * @param \Cake\View\Cell $module instance of Cell * @param int $column a column number * @return $this */ - public function addToColumn(Cell $module, $column = 1) + public function addToColumn(Cell $module, int $column = 1) { $children = $this->get('children'); $children[$column][] = $module; @@ -65,7 +67,7 @@ public function addToColumn(Cell $module, $column = 1) * @return int * @throws \InvalidArgumentException the column count is invalid */ - protected function _setColumns($value) + protected function _setColumns(int $value) { $columnMap = [ 1 => 12, diff --git a/src/Dashboard/Module/ActionLinkItem.php b/src/Dashboard/Module/ActionLinkItem.php index 7bfc08f8..2948f585 100644 --- a/src/Dashboard/Module/ActionLinkItem.php +++ b/src/Dashboard/Module/ActionLinkItem.php @@ -1,9 +1,9 @@ map(function ($value) { - $options = (array)$value->get('options') + ['class' => ['btn btn-default']]; + $options = (array)$value->get('options') + ['class' => ['btn btn-secondary']]; $value->set('options', $options); return $value; diff --git a/src/Dashboard/Module/LinkItem.php b/src/Dashboard/Module/LinkItem.php index 8b75b887..eec60d11 100644 --- a/src/Dashboard/Module/LinkItem.php +++ b/src/Dashboard/Module/LinkItem.php @@ -1,8 +1,9 @@ set('title', $title); $this->set('url', $url); @@ -51,7 +52,7 @@ public function __construct($title, $url, $options = []) * title property setter * * @param string|array|null $title A title for the link - * @return string + * @return string|array */ protected function _setTitle($title) { diff --git a/src/Listener/Traits/DeprecatedConfigurationKeyTrait.php b/src/Listener/Traits/DeprecatedConfigurationKeyTrait.php index 52719341..81e78aac 100644 --- a/src/Listener/Traits/DeprecatedConfigurationKeyTrait.php +++ b/src/Listener/Traits/DeprecatedConfigurationKeyTrait.php @@ -1,9 +1,10 @@ _controller(); @@ -26,7 +30,7 @@ protected function beforeRenderFormType() * * @return bool */ - protected function _getFormEnableDirtyCheck() + protected function _getFormEnableDirtyCheck(): bool { $action = $this->_action(); @@ -36,9 +40,9 @@ protected function _getFormEnableDirtyCheck() /** * Get form submit button text. * - * @return bool + * @return string */ - protected function _getFormSubmitButtonText() + protected function _getFormSubmitButtonText(): string { $action = $this->_action(); @@ -48,9 +52,9 @@ protected function _getFormSubmitButtonText() /** * Get extra form submit buttons. * - * @return bool + * @return array */ - protected function _getFormSubmitExtraButtons() + protected function _getFormSubmitExtraButtons(): array { $action = $this->_action(); $buttons = $action->getConfig('scaffold.form_submit_extra_buttons'); @@ -69,9 +73,9 @@ protected function _getFormSubmitExtraButtons() /** * Get extra form submit left buttons. * - * @return bool + * @return array */ - protected function _getFormSubmitExtraLeftButtons() + protected function _getFormSubmitExtraLeftButtons(): array { $action = $this->_action(); $buttons = $action->getConfig('scaffold.form_submit_extra_left_buttons'); @@ -92,7 +96,7 @@ protected function _getFormSubmitExtraLeftButtons() * * @return array */ - protected function _getDefaultExtraButtons() + protected function _getDefaultExtraButtons(): array { return [ 'save_and_continue' => [ @@ -110,7 +114,7 @@ protected function _getDefaultExtraButtons() 'back' => [ 'title' => __d('crud', 'Back'), 'url' => ['action' => 'index'], - 'options' => ['class' => 'btn btn-default', 'role' => 'button'], + 'options' => ['class' => 'btn btn-secondary', 'role' => 'button'], 'type' => 'link', '_label' => 'back', ], @@ -122,25 +126,28 @@ protected function _getDefaultExtraButtons() * * @return array */ - protected function _getDefaultExtraLeftButtons() + protected function _getDefaultExtraLeftButtons(): array { $buttons = []; $action = $this->_action(); if ($action instanceof EditAction) { - $buttons[] = [ - 'title' => __d('crud', 'Delete'), - 'url' => ['action' => 'delete'], - 'options' => [ - 'block' => 'form.after_end', - 'class' => 'btn btn-danger btn-delete', - 'confirm' => __d('crud', 'Are you sure you want to delete this record?'), - 'name' => '_delete', - 'style' => 'margin-left: 0', - ], - 'type' => 'postLink', - '_label' => 'delete', - ]; + $blacklist = $action->getConfig('scaffold.actions_blacklist', []); + if (!in_array('delete', $blacklist, true)) { + $buttons[] = [ + 'title' => __d('crud', 'Delete'), + 'url' => ['action' => 'delete'], + 'options' => [ + 'block' => 'form.after_end', + 'class' => 'btn btn-danger btn-delete', + 'confirm' => __d('crud', 'Are you sure you want to delete this record?'), + 'name' => '_delete', + 'style' => 'margin-left: 0', + ], + 'type' => 'postLink', + '_label' => 'delete', + ]; + } } return $buttons; @@ -159,12 +166,12 @@ protected function _getFormUrl() } /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _controller(); + abstract protected function _controller(): Controller; /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _action($name = null); + abstract protected function _action(?string $name = null): BaseAction; } diff --git a/src/Listener/Traits/IndexTypeTrait.php b/src/Listener/Traits/IndexTypeTrait.php index 599ef77d..d19dee76 100644 --- a/src/Listener/Traits/IndexTypeTrait.php +++ b/src/Listener/Traits/IndexTypeTrait.php @@ -1,6 +1,10 @@ _action() instanceof IndexAction)) { return; @@ -30,9 +34,9 @@ public function beforeRenderIndexType() * Returns a list of finder scopes, where the key is the title * and the value is the finder to link to * - * @return string + * @return array */ - protected function _getIndexFinderScopes() + protected function _getIndexFinderScopes(): array { $action = $this->_action(); @@ -43,9 +47,9 @@ protected function _getIndexFinderScopes() * Returns a list of index formats, where the key is the link title * and the value is the url to route to * - * @return string + * @return array */ - protected function _getIndexFormats() + protected function _getIndexFormats(): array { $action = $this->_action(); @@ -57,7 +61,7 @@ protected function _getIndexFormats() * * @return string */ - protected function _getIndexType() + protected function _getIndexType(): string { $action = $this->_action(); @@ -74,7 +78,7 @@ protected function _getIndexType() * * @return string */ - protected function _getIndexTitleField() + protected function _getIndexTitleField(): string { $action = $this->_action(); @@ -91,7 +95,7 @@ protected function _getIndexTitleField() * * @return string */ - protected function _getIndexBodyField() + protected function _getIndexBodyField(): string { $action = $this->_action(); @@ -108,7 +112,7 @@ protected function _getIndexBodyField() * * @return string */ - protected function _getIndexImageField() + protected function _getIndexImageField(): string { $action = $this->_action(); @@ -125,7 +129,7 @@ protected function _getIndexImageField() * * @return string */ - protected function _getIndexGalleryCssClasses() + protected function _getIndexGalleryCssClasses(): string { $action = $this->_action(); @@ -138,17 +142,17 @@ protected function _getIndexGalleryCssClasses() } /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _controller(); + abstract protected function _controller(): Controller; /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _action($name = null); + abstract protected function _action(?string $name = null): BaseAction; /** - * {@inheritDoc} + * @inheritDoc */ abstract protected function _table(); } diff --git a/src/Listener/Traits/SidebarNavigationTrait.php b/src/Listener/Traits/SidebarNavigationTrait.php index d4fea72c..37340418 100644 --- a/src/Listener/Traits/SidebarNavigationTrait.php +++ b/src/Listener/Traits/SidebarNavigationTrait.php @@ -1,6 +1,11 @@ _controller(); $sidebarNavigation = $this->_getSidebarNavigation(); - $controller->set('disableSidebar', ($sidebarNavigation === false) ? true : false); + $controller->set('disableSidebar', $sidebarNavigation === false ? true : false); $controller->set('sidebarNavigation', $sidebarNavigation); } @@ -29,12 +34,12 @@ protected function _getSidebarNavigation() } /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _controller(); + abstract protected function _controller(): Controller; /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _action($name = null); + abstract protected function _action(?string $name = null): BaseAction; } diff --git a/src/Listener/Traits/SiteTitleTrait.php b/src/Listener/Traits/SiteTitleTrait.php index b1c71187..38a55a59 100644 --- a/src/Listener/Traits/SiteTitleTrait.php +++ b/src/Listener/Traits/SiteTitleTrait.php @@ -1,7 +1,11 @@ _controller(); @@ -24,7 +28,7 @@ public function beforeRenderSiteTitle() * * @return string */ - protected function _getSiteTitle() + protected function _getSiteTitle(): string { $action = $this->_action(); @@ -41,7 +45,7 @@ protected function _getSiteTitle() * * @return string */ - protected function _getSiteTitleLink() + protected function _getSiteTitleLink(): string { $action = $this->_action(); @@ -58,7 +62,7 @@ protected function _getSiteTitleLink() * * @return string */ - protected function _getSiteTitleImage() + protected function _getSiteTitleImage(): string { $action = $this->_action(); @@ -71,12 +75,12 @@ protected function _getSiteTitleImage() } /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _controller(); + abstract protected function _controller(): Controller; /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _action($name = null); + abstract protected function _action(?string $name = null): BaseAction; } diff --git a/src/Listener/Traits/UtilityNavigationTrait.php b/src/Listener/Traits/UtilityNavigationTrait.php index 147003c0..998a8f7d 100644 --- a/src/Listener/Traits/UtilityNavigationTrait.php +++ b/src/Listener/Traits/UtilityNavigationTrait.php @@ -1,6 +1,10 @@ _controller(); $controller->set('utilityNavigation', $this->_getUtilityNavigation()); @@ -21,7 +25,7 @@ public function beforeRenderUtilityNavigation() * * @return array */ - protected function _getUtilityNavigation() + protected function _getUtilityNavigation(): array { $action = $this->_action(); @@ -37,12 +41,12 @@ protected function _getUtilityNavigation() } /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _controller(); + abstract protected function _controller(): Controller; /** - * {@inheritDoc} + * @inheritDoc */ - abstract protected function _action($name = null); + abstract protected function _action(?string $name = null): BaseAction; } diff --git a/src/Listener/ViewListener.php b/src/Listener/ViewListener.php index 7d609c4a..7f3c0397 100644 --- a/src/Listener/ViewListener.php +++ b/src/Listener/ViewListener.php @@ -1,19 +1,20 @@ _getRelatedModels(); if ($related === []) { @@ -54,10 +55,10 @@ public function beforeFind(Event $event) /** * [beforePaginate description] * - * @param \Cake\Event\Event $event Event. + * @param \Cake\Event\EventInterface $event Event. * @return void */ - public function beforePaginate(Event $event) + public function beforePaginate(EventInterface $event): void { $related = $this->_getRelatedModels(); if ($related === []) { @@ -74,10 +75,10 @@ public function beforePaginate(Event $event) /** * beforeRender event * - * @param \Cake\Event\Event $event Event. + * @param \Cake\Event\EventInterface $event Event. * @return void */ - public function beforeRender(Event $event) + public function beforeRender(EventInterface $event): void { if ($this->_controller()->getName() === 'Error') { return; @@ -122,10 +123,10 @@ public function beforeRender(Event $event) /** * Make sure flash messages are properly handled by BootstrapUI.FlashHelper * - * @param \Cake\Event\Event $event Event. + * @param \Cake\Event\EventInterface $event Event. * @return void */ - public function setFlash(Event $event) + public function setFlash(EventInterface $event): void { unset($event->getSubject()->params['class']); $event->getSubject()->element = ltrim($event->getSubject()->type); @@ -136,7 +137,7 @@ public function setFlash(Event $event) * * @return string */ - protected function _getPageTitle() + protected function _getPageTitle(): string { $action = $this->_action(); @@ -160,15 +161,20 @@ protected function _getPageTitle() } $primaryKeyValue = $this->_primaryKeyValue(); - if ($primaryKeyValue === null) { + if (empty($primaryKeyValue)) { return sprintf('%s %s', $actionName, $controllerName); } $displayFieldValue = $this->_displayFieldValue(); - if ($displayFieldValue === null || $this->_table()->getDisplayField() == $this->_table()->getPrimaryKey()) { + if ( + $displayFieldValue === null + || $this->_table()->getDisplayField() === $this->_table()->getPrimaryKey() + ) { + /** @psalm-var string $primaryKeyValue */ return sprintf('%s %s #%s', $actionName, $controllerName, $primaryKeyValue); } + /** @psalm-var string $primaryKeyValue */ return sprintf('%s %s #%s: %s', $actionName, $controllerName, $primaryKeyValue, $displayFieldValue); } @@ -177,7 +183,7 @@ protected function _getPageTitle() * * @return array */ - protected function _getBreadcrumbs() + protected function _getBreadcrumbs(): array { $action = $this->_action(); @@ -196,7 +202,7 @@ protected function _getBreadcrumbs() * @param string[] $associationTypes List of association types. * @return array */ - protected function _getRelatedModels($associationTypes = []) + protected function _getRelatedModels(array $associationTypes = []): array { $models = $this->_action()->getConfig('scaffold.relations'); @@ -245,7 +251,7 @@ protected function _getRelatedModels($associationTypes = []) * * @return array */ - protected function _blacklist() + protected function _blacklist(): array { return (array)$this->_action()->getConfig('scaffold.fields_blacklist'); } @@ -255,15 +261,18 @@ protected function _blacklist() * * @return array */ - protected function _getPageVariables() + protected function _getPageVariables(): array { $table = $this->_table(); + $modelClass = $table->getAlias(); $controller = $this->_controller(); $scope = $this->_action()->getConfig('scope'); $data = [ - 'modelClass' => $controller->modelClass, - 'singularHumanName' => Inflector::humanize(Inflector::underscore(Inflector::singularize($controller->modelClass))), + 'modelClass' => $modelClass, + 'singularHumanName' => Inflector::humanize( + Inflector::underscore(Inflector::singularize($modelClass)) + ), 'pluralHumanName' => Inflector::humanize(Inflector::underscore($controller->getName())), 'singularVar' => Inflector::singularize($controller->getName()), 'pluralVar' => Inflector::variable($controller->getName()), @@ -281,7 +290,7 @@ protected function _getPageVariables() if ($scope === 'entity') { $data += [ - 'primaryKeyValue' => $this->_primaryKeyValue() + 'primaryKeyValue' => $this->_primaryKeyValue(), ]; } @@ -294,7 +303,7 @@ protected function _getPageVariables() * @param array $associations Associations list. * @return array */ - protected function _scaffoldFields(array $associations = []) + protected function _scaffoldFields(array $associations = []): array { $action = $this->_action(); $scaffoldFields = (array)$action->getConfig('scaffold.fields'); @@ -310,7 +319,7 @@ protected function _scaffoldFields(array $associations = []) if ($scope === 'entity' && !empty($associations['manyToMany'])) { foreach ($associations['manyToMany'] as $alias => $options) { $cols[sprintf('%s._ids', $options['entities'])] = [ - 'multiple' => true + 'multiple' => true, ]; } } @@ -348,7 +357,7 @@ protected function _scaffoldFields(array $associations = []) * * @return string */ - protected function _controllerName() + protected function _controllerName(): string { $inflections = [ 'underscore', @@ -373,13 +382,13 @@ protected function _controllerName() * * @return array */ - protected function _getControllerActions() + protected function _getControllerActions(): array { $table = $entity = []; $actions = $this->_getAllowedActions(); foreach ($actions as $actionName => $config) { - list($scope, $actionConfig) = $this->_getControllerActionConfiguration($actionName, $config); + [$scope, $actionConfig] = $this->_getControllerActionConfiguration($actionName, $config); ${$scope}[$actionName] = $actionConfig; } @@ -394,7 +403,7 @@ protected function _getControllerActions() if ($config === null) { $config = []; } - list($scope, $actionConfig) = $this->_getControllerActionConfiguration($actionName, $config); + [$scope, $actionConfig] = $this->_getControllerActionConfiguration($actionName, $config); $realAction = Hash::get($actionConfig, 'url.action', $actionName); if (!isset(${$scope}[$realAction])) { continue; @@ -406,7 +415,9 @@ protected function _getControllerActions() } foreach ($actionBlacklist as $actionName) { + /** @psalm-suppress EmptyArrayAccess */ unset($table[$actionName]); + /** @psalm-suppress EmptyArrayAccess */ unset($entity[$actionName]); } @@ -422,16 +433,24 @@ protected function _getControllerActions() * @param array $config Config array. * @return array */ - protected function _getControllerActionConfiguration($actionName, $config) + protected function _getControllerActionConfiguration(string $actionName, array $config): array { $realAction = Hash::get($config, 'url.action', $actionName); + $url = ['action' => $realAction]; + + if (isset($config['url'])) { + $url = $config['url'] + $url; + } + if ($this->_crud()->isActionMapped($realAction)) { $action = $this->_action($realAction); $class = get_class($action); + /** @psalm-suppress PossiblyFalseOperand */ $class = substr($class, strrpos($class, '\\') + 1); if ($class === 'DeleteAction') { $config += ['method' => 'DELETE']; + $url['?']['_redirect_url'] = $this->_request()->getRequestTarget(); } if (!isset($config['scope'])) { @@ -440,14 +459,12 @@ protected function _getControllerActionConfiguration($actionName, $config) } // apply defaults if necessary - $scope = isset($config['scope']) ? $config['scope'] : 'entity'; - $method = isset($config['method']) ? $config['method'] : 'GET'; + $scope = $config['scope'] ?? 'entity'; + $method = $config['method'] ?? 'GET'; - $title = !empty($config['link_title']) ? $config['link_title'] : Inflector::humanize(Inflector::underscore($actionName)); - $url = ['action' => $realAction]; - if (isset($config['url'])) { - $url = $config['url'] + $url; - } + $title = !empty($config['link_title']) + ? $config['link_title'] + : Inflector::humanize(Inflector::underscore($actionName)); $actionConfig = [ 'title' => $title, @@ -456,7 +473,7 @@ protected function _getControllerActionConfiguration($actionName, $config) 'options' => array_diff_key( $config, array_flip(['method', 'scope', 'link_title', 'url', 'scaffold', 'callback']) - ) + ), ]; if (!empty($config['callback'])) { $actionConfig['callback'] = $config['callback']; @@ -470,7 +487,7 @@ protected function _getControllerActionConfiguration($actionName, $config) * * @return array */ - protected function _getAllowedActions() + protected function _getAllowedActions(): array { $actions = $this->_action()->getConfig('scaffold.actions'); if ($actions === null) { @@ -507,7 +524,7 @@ protected function _getAllowedActions() * @param array $actions Actions * @return array */ - protected function _normalizeActions($actions) + protected function _normalizeActions(array $actions): array { $normalized = []; foreach ($actions as $key => $config) { @@ -527,7 +544,7 @@ protected function _normalizeActions($actions) * @param array $whitelist Whitelist of associations to return. * @return array Associations for model */ - protected function _associations(array $whitelist = []) + protected function _associations(array $whitelist = []): array { $table = $this->_table(); @@ -537,9 +554,10 @@ protected function _associations(array $whitelist = []) $keys = $associations->keys(); if (!empty($whitelist)) { - $keys = array_intersect($keys, array_map('strtolower', $whitelist)); + $keys = array_intersect($keys, $whitelist); } foreach ($keys as $associationName) { + /** @var \Cake\ORM\Association $association */ $association = $associations->get($associationName); $type = $association->type(); @@ -556,10 +574,15 @@ protected function _associations(array $whitelist = []) $associationConfiguration[$type][$assocKey]['propertyName'] = $association->getProperty(); $associationConfiguration[$type][$assocKey]['plugin'] = pluginSplit($association->getClassName())[0]; $associationConfiguration[$type][$assocKey]['controller'] = $assocKey; - $associationConfiguration[$type][$assocKey]['entity'] = Inflector::singularize(Inflector::underscore($assocKey)); + $associationConfiguration[$type][$assocKey]['entity'] = Inflector::singularize( + Inflector::underscore($assocKey) + ); $associationConfiguration[$type][$assocKey]['entities'] = Inflector::underscore($assocKey); - $associationConfiguration[$type][$assocKey] = array_merge($associationConfiguration[$type][$assocKey], $this->_action()->getConfig('association.' . $assocKey) ?: []); + $associationConfiguration[$type][$assocKey] = array_merge( + $associationConfiguration[$type][$assocKey], + $this->_action()->getConfig('association.' . $assocKey) ?: [] + ); } return $associationConfiguration; @@ -592,10 +615,11 @@ protected function _primaryKeyValue() * * If no value can be found, NULL is returned * - * @return string|null + * @return string|int|null */ protected function _displayFieldValue() { + /** @psalm-suppress PossiblyInvalidArgument */ return $this->_deriveFieldFromContext($this->_table()->getDisplayField()); } @@ -606,9 +630,10 @@ protected function _displayFieldValue() * @param string $field Name of field. * @return mixed */ - protected function _deriveFieldFromContext($field) + protected function _deriveFieldFromContext(string $field) { $controller = $this->_controller(); + $modelClass = $this->_table()->getAlias(); $entity = $this->_entity(); $request = $this->_request(); $value = $entity->get($field); @@ -617,13 +642,13 @@ protected function _deriveFieldFromContext($field) return $value; } - $path = "{$controller->modelClass}.{$field}"; + $path = "{$modelClass}.{$field}"; if (!empty($request->getData())) { $value = Hash::get((array)$request->getData(), $path); } - $singularVar = Inflector::variable($controller->modelClass); - if (!empty($controller->viewVars[$singularVar])) { + $singularVar = Inflector::variable($modelClass); + if ($controller->viewBuilder()->getVar($singularVar)) { $value = $entity->get($field); } @@ -635,7 +660,7 @@ protected function _deriveFieldFromContext($field) * * @return array */ - protected function _getViewBlocks() + protected function _getViewBlocks(): array { $action = $this->_action(); @@ -647,7 +672,7 @@ protected function _getViewBlocks() * * @return array */ - protected function _getBulkActions() + protected function _getBulkActions(): array { $action = $this->_action(); @@ -659,7 +684,7 @@ protected function _getBulkActions() * * @return array */ - protected function _getActionGroups() + protected function _getActionGroups(): array { $action = $this->_action(); $groups = $action->getConfig('scaffold.action_groups') ?: []; @@ -682,9 +707,10 @@ protected function _getActionGroups() * @param array $fields Form fields. * @return array */ - protected function _getFormTabGroups(array $fields = []) + protected function _getFormTabGroups(array $fields = []): array { $action = $this->_action(); + /** @var array $groups */ $groups = $action->getConfig('scaffold.form_tab_groups'); if (empty($groups)) { diff --git a/src/Listener/ViewSearchListener.php b/src/Listener/ViewSearchListener.php index 0b01ffd4..eac470c8 100644 --- a/src/Listener/ViewSearchListener.php +++ b/src/Listener/ViewSearchListener.php @@ -1,14 +1,15 @@ null, 'autocomplete' => true, - 'selectize' => true, + 'select2' => true, 'collection' => 'default', - 'fields' => null + 'fields' => null, ]; /** @@ -37,10 +38,10 @@ class ViewSearchListener extends BaseListener * * @return array */ - public function implementedEvents() + public function implementedEvents(): array { return [ - 'Crud.afterPaginate' => ['callable' => 'afterPaginate'] + 'Crud.afterPaginate' => ['callable' => 'afterPaginate'], ]; } @@ -50,12 +51,11 @@ public function implementedEvents() * Only after a crud paginate call does this listener do anything. So listen * for that * - * @param \Cake\Event\Event $event Event. + * @param \Cake\Event\EventInterface $event Event. * @return void */ - public function afterPaginate(Event $event) + public function afterPaginate(EventInterface $event): void { - $event; if (!$this->_table()->behaviors()->has('Search')) { return; } @@ -66,58 +66,51 @@ public function afterPaginate(Event $event) } $fields = $this->fields(); - $this->_controller()->set('searchInputs', $fields); - } - /** - * Get field options for search filter inputs. - * - * @return array - */ - public function fields() - { - return $this->getConfig('fields') ?: $this->_deriveFields(); + $this->_controller()->viewBuilder() + ->setVar('searchInputs', $fields) + ->setHelpers(['Search.Search']); } /** - * Derive field options for search filter inputs based on filter collection. + * Get field options for search filter inputs. * * @return array */ - protected function _deriveFields() + public function fields(): array { + $fields = $this->getConfig('fields') ?: []; $config = $this->getConfig(); - $table = $this->_table(); - if (method_exists($table, 'searchConfiguration')) { - $searchManager = $table->searchConfiguration(); + $schema = $this->_table()->getSchema(); + $request = $this->_request(); + + if ($fields) { + $fields = Hash::normalize($fields); } else { - $searchManager = $table->searchManager(); - } + $filters = $this->_table()->searchManager()->getFilters($config['collection']); - $fields = []; - $schema = $table->getSchema(); - $request = $this->_request(); + foreach ($filters as $filter) { + $opts = $filter->getConfig('form'); + if ($opts === false) { + continue; + } - foreach ($searchManager->getFilters($config['collection']) as $filter) { - if ($filter->getConfig('form') === false) { - continue; + $fields[$filter->name()] = $opts ?: []; } + } - $field = $filter->name(); + foreach ($fields as $field => $opts) { $input = [ 'required' => false, - 'type' => 'text' + 'type' => 'text', ]; if (substr($field, -3) === '_id' && $field !== '_id') { $input['type'] = 'select'; } - $filterFormConfig = $filter->getConfig('form'); - if (!empty($filterFormConfig)) { - $input = $filterFormConfig + $input; - } + $input = (array)$opts + $input; $input['value'] = $request->getQuery($field); @@ -128,8 +121,8 @@ protected function _deriveFields() if (!empty($input['options'])) { $input['empty'] = true; - if (empty($input['class']) && !$config['selectize']) { - $input['class'] = 'no-selectize'; + if (empty($input['class']) && !$config['select2']) { + $input['class'] = 'no-select2'; } $fields[$field] = $input; @@ -141,9 +134,28 @@ protected function _deriveFields() $input['class'] = 'autocomplete'; } + if ( + !empty($input['class']) + && strpos($input['class'], 'autocomplete') !== false + && $input['type'] !== 'select' + ) { + $input['type'] = 'select'; + + if (!empty($input['value'])) { + $input['options'][$input['value']] = $input['value']; + } + + $input += [ + 'data-input-type' => 'text', + 'data-tags' => 'true', + 'data-allow-clear' => 'true', + 'data-placeholder' => '', + ]; + } + $urlArgs = []; - $fieldKeys = isset($input['fields']) ? $input['fields'] : ['id' => $field, 'value' => $field]; + $fieldKeys = $input['fields'] ?? ['id' => $field, 'value' => $field]; if (is_array($fieldKeys)) { foreach ($fieldKeys as $key => $val) { $urlArgs[$key] = $val; @@ -151,7 +163,7 @@ protected function _deriveFields() } unset($input['fields']); - $url = array_merge(['action' => 'lookup', '_ext' => 'json'], $urlArgs); + $url = array_merge(['action' => 'lookup', '_ext' => 'json'], ['?' => $urlArgs]); $input['data-url'] = Router::url($url); $fields[$field] = $input; diff --git a/src/Menu/MenuDivider.php b/src/Menu/MenuDivider.php index d776cc6b..9903aa8f 100644 --- a/src/Menu/MenuDivider.php +++ b/src/Menu/MenuDivider.php @@ -1,4 +1,6 @@ title = $title; $this->entries = $entries; @@ -34,7 +36,7 @@ public function __construct($title, array $entries = []) * * @return string */ - public function getTitle() + public function getTitle(): string { return $this->title; } @@ -44,7 +46,7 @@ public function getTitle() * * @return array */ - public function getEntries() + public function getEntries(): array { return $this->entries; } diff --git a/src/Menu/MenuItem.php b/src/Menu/MenuItem.php index d6b9dfbc..0746a141 100644 --- a/src/Menu/MenuItem.php +++ b/src/Menu/MenuItem.php @@ -1,4 +1,6 @@ title = $title; $this->url = $url; @@ -45,7 +47,7 @@ public function __construct($title, $url = null, array $options = []) * * @return string */ - public function getTitle() + public function getTitle(): string { return $this->title; } @@ -65,7 +67,7 @@ public function getUrl() * * @return array */ - public function getOptions() + public function getOptions(): array { return $this->options; } diff --git a/src/Plugin.php b/src/Plugin.php index b2f6fb71..679fa93c 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -1,4 +1,6 @@ $group) { - $exists = false; - foreach ($group as $action => $config) { - $subaction = is_array($config) ? $action : $config; - if (array_key_exists($subaction, $links)) { - $exists = true; - } - } - if (!$exists) { - unset($groups[$key]); - } -} -?> - - $group) : ?> -
- Html->link( - sprintf("%s %s", $key, $this->Html->tag('span', '', ['class' => 'caret'])), - '#', - ['class' => 'btn btn-default dropdown-toggle', 'escape' => false, 'data-toggle' => 'dropdown', 'aria-haspopup' => true, 'aria-expanded' => false] - ) ?> - -
- -
- Form->button($button['title'], $button['options']); - } elseif ($button['type'] === 'link') { - echo $this->Html->link($button['title'], $button['url'], $button['options']); - } elseif ($button['type'] === 'postLink') { - if (is_array($button['url'])) { - $button['url'][] = $$viewVar->get($primaryKey); - } - echo $this->Form->postLink($button['title'], $button['url'], $button['options']); - } - } - } - ?> -
- - -
- Form->button( - $formSubmitButtonText, - ['class' => 'btn btn-primary', 'name' => '_save', 'value' => '1'] - ) ?> - Form->button($button['title'], $button['options']); - } elseif ($button['type'] === 'link') { - echo $this->Html->link($button['title'], $button['url'], $button['options']); - } - } - } - ?> -
diff --git a/src/Template/Scaffold/index.ctp b/src/Template/Scaffold/index.ctp deleted file mode 100644 index 48a6e961..00000000 --- a/src/Template/Scaffold/index.ctp +++ /dev/null @@ -1,52 +0,0 @@ -fetch('before_index'); ?> - -
- - exists('search')) { - $this->start('search'); - echo $this->element('search'); - $this->end(); - } - ?> - element('action-header') ?> - - fetch('search'); ?> - -
- - element('index/bulk_actions/form_start', compact('bulkActions')); ?> - - element('index/finder_scopes', compact('indexFinderScopes')); ?> - - $fields, - 'actions' => $actions, - 'bulkActions' => $bulkActions, - 'primaryKey' => $primaryKey, - 'singularVar' => $singularVar, - 'viewVar' => $viewVar, - $viewVar => ${$viewVar}, - ]; - switch ($indexType) { - case 'table': - echo $this->element('index/table', $_data); - break; - case 'gallery': - echo $this->element('index/gallery', $_data); - break; - case 'blog': - echo $this->element('index/blog', $_data); - break; - default: - echo $this->element($indexType, $_data); - break; - } - ?> - - element('index/bulk_actions/form_end', compact('bulkActions')); ?> - element('index/pagination'); ?> -
- -fetch('after_index'); ?> diff --git a/src/Traits/CrudViewConfigTrait.php b/src/Traits/CrudViewConfigTrait.php index ecacf401..40203595 100644 --- a/src/Traits/CrudViewConfigTrait.php +++ b/src/Traits/CrudViewConfigTrait.php @@ -1,4 +1,6 @@ 'index', 'title' => Inflector::humanize($table), - 'controller' => Inflector::camelize($table) + 'controller' => Inflector::camelize($table), ]; $normal[$table] = $config; diff --git a/src/View/CrudView.php b/src/View/CrudView.php index 7225817d..a4b827a6 100644 --- a/src/View/CrudView.php +++ b/src/View/CrudView.php @@ -1,9 +1,10 @@ 'beforeLayout', @@ -74,7 +75,7 @@ public function beforeLayout() * * @return void */ - protected function _loadAssets() + protected function _loadAssets(): void { if (Configure::read('CrudView.useAssetCompress')) { $this->AssetCompress->css('CrudView.crudview', ['block' => true]); @@ -105,25 +106,22 @@ protected function _loadAssets() * * @return void */ - protected function _setupHelpers() + protected function _setupHelpers(): void { $helpers = [ 'Html' => ['className' => 'BootstrapUI.Html'], 'Form' => [ 'className' => 'BootstrapUI.Form', 'widgets' => [ - 'datetime' => ['CrudView\View\Widget\DateTimeWidget', 'select'] + 'datetime' => ['CrudView\View\Widget\DateTimeWidget', 'select'], ], ], 'Flash' => ['className' => 'BootstrapUI.Flash'], 'Paginator' => ['className' => 'BootstrapUI.Paginator'], + 'Breadcrumbs' => ['className' => 'BootstrapUI.Breadcrumbs'], 'CrudView' => ['className' => 'CrudView.CrudView'], ]; - if (class_exists('\Cake\View\Helper\BreadcrumbsHelper')) { - $helpers['Breadcrumbs'] = ['className' => 'BootstrapUI.Breadcrumbs']; - } - if (Configure::read('CrudView.useAssetCompress')) { $helpers['AssetCompress'] = ['className' => 'AssetCompress.AssetCompress']; } @@ -136,7 +134,7 @@ protected function _setupHelpers() * * @return void */ - protected function _setupPaths() + protected function _setupPaths(): void { $paths = Configure::read('App.paths.templates'); @@ -144,7 +142,7 @@ protected function _setupPaths() if (!empty($extraPaths)) { $paths = array_merge($paths, (array)$extraPaths); } - $paths[] = Plugin::classPath('CrudView') . 'Template' . DS; + $paths[] = Plugin::templatePath('CrudView'); Configure::write('App.paths.templates', $paths); } @@ -158,7 +156,7 @@ protected function _setupPaths() * @return string default The block content or $default if the block does not exist. * @see ViewBlock::get() */ - public function fetch($name, $default = '') + public function fetch(string $name, string $default = ''): string { $viewblock = ''; $viewblocks = $this->get('viewblocks', []); @@ -175,10 +173,9 @@ public function fetch($name, $default = '') * Check if a block exists * * @param string $name Name of the block - * * @return bool */ - public function exists($name) + public function exists(string $name): bool { $viewblocks = $this->get('viewblocks', []); @@ -189,19 +186,18 @@ public function exists($name) * Constructs a ViewBlock from an array of configured data * * @param array $data ViewBlock data - * * @return string */ - protected function _createViewblock($data) + protected function _createViewblock(array $data): string { $output = ''; foreach ($data as $key => $type) { if ($type === 'element') { $output = $this->element($key); } elseif ($type === 'Html::css') { - $output .= $this->Html->css($key); + $output .= (string)$this->Html->css($key); } elseif ($type === 'Html::script') { - $output .= $this->Html->script($key); + $output .= (string)$this->Html->script($key); } else { $output .= $key; } @@ -211,27 +207,27 @@ protected function _createViewblock($data) } /** - * Returns filename of given action's template file (.ctp) as a string. + * Returns filename of given action's template file as a string. * * @param string|null $name Controller action to find template filename for. * @return string Template filename * @throws \Cake\View\Exception\MissingTemplateException When a view file could not be found. */ - protected function _getViewFileName($name = null) + protected function _getTemplateFileName(?string $name = null): string { if ($this->templatePath === 'Error') { - return parent::_getViewFileName($name); + return parent::_getTemplateFileName($name); } try { - return parent::_getViewFileName($name); + return parent::_getTemplateFileName($name); } catch (MissingTemplateException $exception) { - $this->subDir = null; - $this->templatePath = 'Scaffold'; + $this->subDir = ''; + $this->templatePath = 'scaffold'; } try { - return parent::_getViewFileName($this->template); + return parent::_getTemplateFileName($this->template); } catch (MissingTemplateException $exception) { - return parent::_getViewFileName($name); + return parent::_getTemplateFileName($name); } } } diff --git a/src/View/Helper/CrudViewHelper.php b/src/View/Helper/CrudViewHelper.php index 4014b8a4..efd04489 100644 --- a/src/View/Helper/CrudViewHelper.php +++ b/src/View/Helper/CrudViewHelper.php @@ -1,14 +1,15 @@ null + 'fieldFormatters' => null, ]; /** @@ -47,7 +47,7 @@ class CrudViewHelper extends Helper * @param \Cake\Datasource\EntityInterface $record Entity. * @return void */ - public function setContext(EntityInterface $record) + public function setContext(EntityInterface $record): void { $this->_context = $record; } @@ -57,7 +57,7 @@ public function setContext(EntityInterface $record) * * @return \Cake\Datasource\EntityInterface */ - public function getContext() + public function getContext(): EntityInterface { return $this->_context; } @@ -70,7 +70,7 @@ public function getContext() * @param array $options Processing options. * @return string|null|array|bool|int */ - public function process($field, EntityInterface $data, array $options = []) + public function process(string $field, EntityInterface $data, array $options = []) { $this->setContext($data); @@ -102,11 +102,11 @@ public function process($field, EntityInterface $data, array $options = []) /** * Get the current field value * - * @param \Cake\Datasource\EntityInterface $data The entity data. + * @param \Cake\Datasource\EntityInterface|null $data The entity data. * @param string $field The field to extract, if null, the field from the entity context is used. * @return mixed */ - public function fieldValue(EntityInterface $data, $field) + public function fieldValue(?EntityInterface $data, string $field) { if (empty($data)) { $data = $this->getContext(); @@ -123,7 +123,7 @@ public function fieldValue(EntityInterface $data, $field) * @param array $options Options array. * @return array|bool|null|int|string */ - public function introspect($field, $value, array $options = []) + public function introspect(string $field, $value, array $options = []) { $output = $this->relation($field); if ($output) { @@ -134,10 +134,12 @@ public function introspect($field, $value, array $options = []) $fieldFormatters = $this->getConfig('fieldFormatters'); if (isset($fieldFormatters[$type])) { + /** @psalm-suppress PossiblyNullArrayOffset */ if (is_callable($fieldFormatters[$type])) { return $fieldFormatters[$type]($field, $value, $this->getContext(), $options, $this->getView()); } + /** @psalm-suppress PossiblyNullArrayOffset */ return $this->{$fieldFormatters[$type]}($field, $value, $options); } @@ -166,9 +168,9 @@ public function introspect($field, $value, array $options = []) * Get column type from schema. * * @param string $field Field to get column type for - * @return string + * @return string|null */ - public function columnType($field) + public function columnType(string $field): ?string { $schema = $this->schema(); @@ -183,11 +185,11 @@ public function columnType($field) * @param array $options Options array * @return string */ - public function formatBoolean($field, $value, $options) + public function formatBoolean(string $field, $value, array $options): string { return (bool)$value ? - $this->Html->label(__d('crud', 'Yes'), ['type' => empty($options['inverted']) ? 'success' : 'danger']) : - $this->Html->label(__d('crud', 'No'), ['type' => empty($options['inverted']) ? 'danger' : 'success']); + $this->Html->badge(__d('crud', 'Yes'), ['class' => empty($options['inverted']) ? 'success' : 'danger']) : + $this->Html->badge(__d('crud', 'No'), ['class' => empty($options['inverted']) ? 'danger' : 'success']); } /** @@ -198,17 +200,22 @@ public function formatBoolean($field, $value, $options) * @param array $options Options array. * @return string */ - public function formatDate($field, $value, array $options) + public function formatDate(string $field, $value, array $options): string { if ($value === null) { - return $this->Html->label(__d('crud', 'N/A'), ['type' => 'info']); + return $this->Html->badge(__d('crud', 'N/A'), ['class' => 'info']); } - if (is_int($value) || is_string($value) || $value instanceof DateTime || $value instanceof DateTimeImmutable) { + /** @psalm-suppress ArgumentTypeCoercion */ + if ( + is_int($value) + || is_string($value) + || $value instanceof DateTimeInterface + ) { return $this->Time->timeAgoInWords($value, $options); } - return $this->Html->label(__d('crud', 'N/A'), ['type' => 'info']); + return $this->Html->badge(__d('crud', 'N/A'), ['class' => 'info']); } /** @@ -219,18 +226,23 @@ public function formatDate($field, $value, array $options) * @param array $options Options array. * @return string */ - public function formatTime($field, $value, array $options) + public function formatTime(string $field, $value, array $options): string { if ($value === null) { - return $this->Html->label(__d('crud', 'N/A'), ['type' => 'info']); + return $this->Html->badge(__d('crud', 'N/A'), ['class' => 'info']); } - $format = isset($options['format']) ? $options['format'] : null; - - if (is_int($value) || is_string($value) || $value instanceof DateTime || $value instanceof DateTimeImmutable) { + $format = $options['format'] ?? null; + + /** @psalm-suppress ArgumentTypeCoercion */ + if ( + is_int($value) + || is_string($value) + || $value instanceof DateTimeInterface + ) { return $this->Time->nice($value, $format); } - return $this->Html->label(__d('crud', 'N/A'), ['type' => 'info']); + return $this->Html->badge(__d('crud', 'N/A'), ['class' => 'info']); } /** @@ -240,7 +252,7 @@ public function formatTime($field, $value, array $options) * @param mixed $value Value of field. * @return string */ - public function formatString($field, $value) + public function formatString(string $field, $value): string { return h(Text::truncate((string)$value, 200)); } @@ -252,7 +264,7 @@ public function formatString($field, $value) * @param array $options Options array. * @return string */ - public function formatDisplayField($value, array $options) + public function formatDisplayField($value, array $options): string { return $this->createViewLink($value, ['escape' => false]); } @@ -263,7 +275,7 @@ public function formatDisplayField($value, array $options) * @param string $field Name of field. * @return mixed Array of data to output, false if no match found */ - public function relation($field) + public function relation(string $field) { $associations = $this->associations(); if (empty($associations['manyToOne'])) { @@ -271,9 +283,6 @@ public function relation($field) } $data = $this->getContext(); - if (empty($data)) { - return false; - } foreach ($associations['manyToOne'] as $alias => $details) { if ($field !== $details['foreignKey']) { @@ -293,8 +302,8 @@ public function relation($field) 'plugin' => $details['plugin'], 'controller' => $details['controller'], 'action' => 'view', - $entity->{$details['primaryKey']} - ]) + $entity->{$details['primaryKey']}, + ]), ]; } @@ -307,7 +316,7 @@ public function relation($field) * * @return string|null */ - public function redirectUrl() + public function redirectUrl(): ?string { $redirectUrl = $this->getView()->getRequest()->getQuery('_redirect_url'); $redirectUrlViewVar = $this->getViewVar('_redirect_url'); @@ -329,7 +338,7 @@ public function redirectUrl() 'name' => '_redirect_url', 'value' => $redirectUrl, 'id' => null, - 'secure' => FormHelper::SECURE_SKIP + 'secure' => FormHelper::SECURE_SKIP, ]); } @@ -341,7 +350,7 @@ public function redirectUrl() * @param array $options Options array to be passed to the link function * @return string */ - public function createRelationLink($alias, $relation, $options = []) + public function createRelationLink(string $alias, array $relation, array $options = []): string { return $this->Html->link( __d('crud', 'Add {0}', [Inflector::singularize(Inflector::humanize(Inflector::underscore($alias)))]), @@ -351,8 +360,8 @@ public function createRelationLink($alias, $relation, $options = []) 'action' => 'add', '?' => [ $relation['foreignKey'] => $this->getViewVar('primaryKeyValue'), - '_redirect_url' => $this->getView()->getRequest()->getUri()->getPath() - ] + '_redirect_url' => $this->getView()->getRequest()->getUri()->getPath(), + ], ], $options ); @@ -365,7 +374,7 @@ public function createRelationLink($alias, $relation, $options = []) * @param array $options Options array to be passed to the link function * @return string */ - public function createViewLink($title, $options = []) + public function createViewLink(string $title, array $options = []): string { return $this->Html->link( $title, @@ -379,7 +388,7 @@ public function createViewLink($title, $options = []) * * @return string */ - public function currentModel() + public function currentModel(): string { return $this->getViewVar('modelClass'); } @@ -389,7 +398,7 @@ public function currentModel() * * @return \Cake\Database\Schema\TableSchemaInterface */ - public function schema() + public function schema(): TableSchemaInterface { return $this->getViewVar('modelSchema'); } @@ -399,7 +408,7 @@ public function schema() * * @return string */ - public function viewVar() + public function viewVar(): string { return $this->getViewVar('viewVar'); } @@ -409,9 +418,9 @@ public function viewVar() * * @return array List of associations. */ - public function associations() + public function associations(): array { - return $this->getViewVar('associations'); + return $this->getViewVar('associations') ?? []; } /** @@ -420,9 +429,9 @@ public function associations() * @param string $key View variable to get. * @return mixed */ - public function getViewVar($key = null) + public function getViewVar(string $key) { - return Hash::get($this->_View->viewVars, $key); + return $this->_View->get($key); } /** @@ -430,7 +439,7 @@ public function getViewVar($key = null) * * @return string */ - public function getCssClasses() + public function getCssClasses(): string { $action = (string)$this->getView()->getRequest()->getParam('action'); $pluralVar = $this->getViewVar('pluralVar'); @@ -438,6 +447,7 @@ public function getCssClasses() $args = func_get_args(); return implode( + ' ', array_unique(array_merge( [ 'scaffold-action', @@ -447,8 +457,7 @@ public function getCssClasses() ], $args, $viewClasses - )), - ' ' + )) ); } } diff --git a/src/View/Widget/DateTimeWidget.php b/src/View/Widget/DateTimeWidget.php index 7602e038..37ffd18c 100644 --- a/src/View/Widget/DateTimeWidget.php +++ b/src/View/Widget/DateTimeWidget.php @@ -1,172 +1,188 @@ ' + . '{{input}}' + . '
' + . '' + . '' + . '
' + . ''; + + /** + * @var string + */ + protected $calendarIcon = '' + . '' + . ''; /** - * Renders a date time widget. + * @var string + */ + protected $clockIcon = '' + . '' + . '' + . ''; + + /** + * @var string + */ + protected $clearIcon = '' + . '' + . '' + . ''; + // phpcs:enable + + /** + * Render flatpickr * - * @param array $data Data to render with. - * @param \Cake\View\Form\ContextInterface $context The current form context. - * @return string A generated select box. - * @throws \RuntimeException When option data is invalid. + * @param array $data Data + * @param \Cake\View\Form\ContextInterface $context Context. + * @return string */ - public function render(array $data, ContextInterface $context) + public function render(array $data, ContextInterface $context): string { - $id = $data['id']; - $name = $data['name']; - $val = $data['val']; - $type = $data['type']; - $required = $data['required'] ? 'required' : ''; - $disabled = isset($data['disabled']) && $data['disabled'] ? 'disabled' : ''; - $role = isset($data['role']) ? $data['role'] : 'datetime-picker'; - $format = null; - $locale = isset($data['locale']) ? $data['locale'] : I18n::getLocale(); + $datetimePicker = Configure::read('CrudView.datetimePicker', false); + if (isset($data['datetimePicker'])) { + $defaults = $datetimePicker; - $timezoneAware = Configure::read('CrudView.timezoneAwareDateTimeWidget'); + $datetimePicker = $data['datetimePicker']; + unset($data['datetimePicker']); - $timestamp = null; - $timezoneOffset = null; + if ($datetimePicker === false) { + return $this->_withInputGroup($data, $context); + } - if (isset($data['data-format'])) { - $format = $this->_convertPHPToMomentFormat($data['data-format']); + if (is_array($defaults)) { + $datetimePicker += $defaults; + } } - if (!($val instanceof DateTimeInterface) && !empty($val)) { - switch ($type) { - case 'date': - case 'time': - $val = Type::build($type)->marshal($val); - break; + if ($datetimePicker === false) { + return $this->_withInputGroup($data, $context); + } - default: - $val = Type::build('datetime')->marshal($val); - } + $datetimePicker += [ + 'data-alt-input-class' => '', + 'data-wrap' => 'true', + ]; + + $data = $this->mergeDefaults($data, $context) + ['data-input' => '']; + + $data['value'] = $this->formatDateTime($data['val'], $data); + unset($data['val'], $data['timezone']); + + // This is the format for value POSTed to server + $datetimePicker['data-date-format'] = $this->convertPHPToDatePickerFormat($this->formatMap[$data['type']]); + + // This just to allow upgrading easier + if (isset($data['data-format'])) { + $datetimePicker['data-alt-format'] = $data['data-format']; + unset($data['data-format']); + } + // This is the format that will be display to user + if (isset($datetimePicker['data-alt-format'])) { + $datetimePicker['data-alt-format'] = $this->convertPHPToDatePickerFormat( + $datetimePicker['data-alt-format'] + ); + $datetimePicker['data-alt-input'] = 'true'; } - if ($val && !is_string($val)) { - if ($timezoneAware) { - $timestamp = $val->format('U'); - $dateTimeZone = new DateTimeZone(date_default_timezone_get()); - $timezoneOffset = ($dateTimeZone->getOffset($val) / 60); + if ($data['type'] === 'time' || $data['type'] === 'datetime-local') { + if ($data['type'] === 'time') { + $datetimePicker['data-no-calendar'] = 'true'; } - $val = $val->format($type === 'date' ? 'Y-m-d' : 'Y-m-d H:i:s'); + + $datetimePicker['data-enable-time'] = 'true'; + $datetimePicker += ['data-enable-seconds' => 'true']; + } + + $clearIcon = $this->clearIcon; + $toggleIcon = $this->calendarIcon; + if (isset($datetimePicker['iconClass'])) { + $toggleIcon = $datetimePicker['iconClass']; + unset($datetimePicker['iconClass']); + } elseif ($data['type'] === 'time') { + $toggleIcon = $this->clockIcon; + } + + if ($this->_templates->get('datetimePicker') === null) { + $this->_templates->add(['datetimePicker' => $this->defaultTemplate]); } - if ($format === null) { - if ($type === 'date') { - $format = 'L'; - } elseif ($type === 'time') { - $format = 'LT'; - } else { - $format = 'L LT'; + $data = $this->_templates->addClass($data, 'form-control'); + $wrap = $datetimePicker['data-wrap'] === 'true'; + if ($wrap) { + if (isset($data['class'])) { + $datetimePicker['data-alt-input-class'] = $data['class']; } + $datetimePicker['class'] = ['input-group', 'flatpickr']; + if (isset($data['datetimePicker'])) { + $datetimePicker = $data['datetimePicker'] + $datetimePicker; + unset($data['datetimePicker']); + } + } else { + $data += $datetimePicker; + $data = $this->_templates->addClass($data, 'flatpickr'); } - $icon = $type === 'time' - ? 'time' - : 'calendar'; - - $widget = << - _templates->format('input', [ + 'name' => $data['name'], + 'type' => 'text', + 'templateVars' => $data['templateVars'], + 'attrs' => $this->_templates->formatAttributes( + $data, + ['name', 'type'] + ), + ]); + + if (!$wrap) { + return $input; } - $widget .= << - - -html; - - return $widget; + + /** @psalm-suppress PossiblyInvalidArrayOffset */ + return $this->_templates->format('datetimePicker', [ + 'input' => $input, + 'toggleIcon' => $toggleIcon, + 'clearIcon' => $clearIcon, + 'templateVars' => $data['templateVars'], + 'attrs' => $this->_templates->formatAttributes( + $datetimePicker, + ['toggleIcon', 'clearIcon'] + ), + ]); } /** - * Converts PHP date format to one supported by MomentJS. + * Converts PHP date format to one supported by flatpickr. * * @param string $format PHP date format. - * @return string MomentJS date format. - * @see http://stackoverflow.com/a/30192680 + * @return string flatpickr date format. + * @see https://flatpickr.js.org/formatting/ */ - protected function _convertPHPToMomentFormat($format) + protected function convertPHPToDatePickerFormat(string $format): string { $replacements = [ - 'd' => 'DD', - 'D' => 'ddd', - 'j' => 'D', - 'l' => 'dddd', - 'N' => 'E', - 'S' => 'o', - 'w' => 'e', - 'z' => 'DDD', - 'W' => 'W', - 'F' => 'MMMM', - 'm' => 'MM', - 'M' => 'MMM', - 'n' => 'M', - 't' => '', // no equivalent - 'L' => '', // no equivalent - 'o' => 'YYYY', - 'Y' => 'YYYY', - 'y' => 'YY', - 'a' => 'a', - 'A' => 'A', - 'B' => '', // no equivalent - 'g' => 'h', - 'G' => 'H', - 'h' => 'hh', - 'H' => 'HH', - 'i' => 'mm', - 's' => 'ss', - 'u' => 'SSS', - 'I' => '', // no equivalent - 'O' => '', // no equivalent - 'P' => '', // no equivalent - 'T' => '', // no equivalent - 'Z' => '', // no equivalent - 'c' => '', // no equivalent - 'r' => '', // no equivalent - 'U' => 'X', + 's' => 'S', + 'A' => 'K', ]; - $momentFormat = strtr($format, $replacements); - - return $momentFormat; - } - - /** - * {@inheritDoc} - */ - public function secureFields(array $data) - { - if (!isset($data['name']) || $data['name'] === '') { - return []; - } + $datePickerFormat = strtr($format, $replacements); - return [$data['name']]; + return $datePickerFormat; } } diff --git a/src/Template/Cell/DashboardTable/display.ctp b/templates/cell/DashboardTable/display.php similarity index 93% rename from src/Template/Cell/DashboardTable/display.ctp rename to templates/cell/DashboardTable/display.php index 8494fd27..29371c18 100644 --- a/src/Template/Cell/DashboardTable/display.ctp +++ b/templates/cell/DashboardTable/display.php @@ -5,7 +5,7 @@ - + get('actions')) : ?> diff --git a/src/Template/Cell/TablesList/display.ctp b/templates/cell/TablesList/display.php similarity index 100% rename from src/Template/Cell/TablesList/display.ctp rename to templates/cell/TablesList/display.php diff --git a/src/Template/Element/action-button.ctp b/templates/element/action-button.php similarity index 99% rename from src/Template/Element/action-button.ctp rename to templates/element/action-button.php index 93d75df8..2a47c23f 100644 --- a/src/Template/Element/action-button.ctp +++ b/templates/element/action-button.php @@ -1,11 +1,13 @@ Form->postLink($config['title'], $config['url'], $config['options']); + return; } diff --git a/templates/element/action-groups.php b/templates/element/action-groups.php new file mode 100644 index 00000000..6ac81752 --- /dev/null +++ b/templates/element/action-groups.php @@ -0,0 +1,40 @@ + $group) { + $exists = false; + foreach ($group as $action => $config) { + $subaction = is_array($config) ? $action : $config; + if (array_key_exists($subaction, $links)) { + $exists = true; + } + } + if (!$exists) { + unset($groups[$key]); + } +} +?> + + $group) : ?> +
+ + +
+ diff --git a/src/Template/Element/action-header.ctp b/templates/element/action-header.php similarity index 94% rename from src/Template/Element/action-header.ctp rename to templates/element/action-header.php index cd178022..6c63b216 100644 --- a/src/Template/Element/action-header.ctp +++ b/templates/element/action-header.php @@ -5,6 +5,7 @@ 'actions' => $actions['table'], 'singularVar' => false, ]); + // to make sure ${$viewVar} is a single entity, not a collection if (${$viewVar} instanceof \Cake\Datasource\EntityInterface && !${$viewVar}->isNew()) { echo $this->element('actions', [ @@ -16,6 +17,6 @@ } ?>

get('title'); ?>

-
+
fetch('actions'); ?>
diff --git a/src/Template/Element/actions.ctp b/templates/element/actions.php similarity index 73% rename from src/Template/Element/actions.ctp rename to templates/element/actions.php index 92f6d951..5751101f 100644 --- a/src/Template/Element/actions.ctp +++ b/templates/element/actions.php @@ -3,7 +3,8 @@ foreach ($actions as $name => $config) { $config += ['method' => 'GET']; - if ((empty($config['url']['controller']) || $this->request->getParam('controller') === $config['url']['controller']) && + if ( + (empty($config['url']['controller']) || $this->request->getParam('controller') === $config['url']['controller']) && (!empty($config['url']['action']) && $this->request->getParam('action') === $config['url']['action']) ) { continue; @@ -17,13 +18,14 @@ if ($config['method'] === 'DELETE') { $linkOptions += [ 'block' => 'action_link_forms', - 'confirm' => __d('crud', 'Are you sure you want to delete record #{0}?', [$singularVar->{$primaryKey}]) + 'confirm' => __d('crud', 'Are you sure you want to delete record #{0}?', [$singularVar->{$primaryKey}]), ]; } if ($config['method'] !== 'GET') { $linkOptions += [ - 'method' => $config['method'] + 'method' => $config['method'], + 'block' => 'action_link_forms', ]; } @@ -32,6 +34,11 @@ unset($config['callback']); $config['options'] = $linkOptions; $links[$name] = $callback($config, !empty($singularVar) ? $singularVar : null, $this); + + if ($links[$name]['method'] !== 'GET' && !isset($links[$name]['options']['block'])) { + $links[$name]['options']['block'] = 'action_link_forms'; + } + continue; } @@ -61,12 +68,13 @@ 'title' => $config['title'], 'url' => $url, 'options' => $linkOptions, - 'method' => $config['method'] + 'method' => $config['method'], ]; } ?> element('action-button', ['config' => $config]); + $btns[] = $this->element('action-button', ['config' => $config]); } unset($actionGroups['primary']); // render grouped actions -echo $this->element('action-groups', ['groups' => $actionGroups, 'links' => $links]); +$groupedBtns = trim($this->element('action-groups', ['groups' => $actionGroups, 'links' => $links])); +if ($groupedBtns) { + $btns[] = $groupedBtns; +} + +echo implode(' ', $btns); diff --git a/src/Template/Element/breadcrumbs.ctp b/templates/element/breadcrumbs.php similarity index 81% rename from src/Template/Element/breadcrumbs.ctp rename to templates/element/breadcrumbs.php index 0c73dbe5..5dbb4acd 100644 --- a/src/Template/Element/breadcrumbs.ctp +++ b/templates/element/breadcrumbs.php @@ -3,7 +3,7 @@ return; } ?> - + Breadcrumbs->add($breadcrumb->getTitle(), $breadcrumb->getUrl(), $breadcrumb->getOptions()); ?> Breadcrumbs->render(); ?> diff --git a/src/Template/Element/form.ctp b/templates/element/form.php similarity index 100% rename from src/Template/Element/form.ctp rename to templates/element/form.php diff --git a/templates/element/form/buttons.php b/templates/element/form/buttons.php new file mode 100644 index 00000000..42e25919 --- /dev/null +++ b/templates/element/form/buttons.php @@ -0,0 +1,38 @@ + +
+ Form->button($button['title'], $button['options']); + } elseif ($button['type'] === 'link') { + $btns[] = $this->Html->link($button['title'], $button['url'], $button['options']); + } elseif ($button['type'] === 'postLink') { + if (is_array($button['url'])) { + $button['url'][] = $$viewVar->get($primaryKey); + } + $btns[] = $this->Form->postLink($button['title'], $button['url'], $button['options']); + } + } + echo implode(' ', $btns); + ?> +
+ + +
+ Form->button( + $formSubmitButtonText, + ['class' => 'btn btn-primary', 'name' => '_save', 'value' => '1'] + ) ?> + Form->button($button['title'], $button['options']); + } elseif ($button['type'] === 'link') { + echo ' ' . $this->Html->link($button['title'], $button['url'], $button['options']); + } + } + } + ?> +
diff --git a/src/Template/Element/form/tabs.ctp b/templates/element/form/tabs.php similarity index 100% rename from src/Template/Element/form/tabs.ctp rename to templates/element/form/tabs.php diff --git a/src/Template/Element/index/blog.ctp b/templates/element/index/blog.php similarity index 100% rename from src/Template/Element/index/blog.ctp rename to templates/element/index/blog.php diff --git a/src/Template/Element/index/bulk_actions/form_end.ctp b/templates/element/index/bulk_actions/form_end.php similarity index 95% rename from src/Template/Element/index/bulk_actions/form_end.ctp rename to templates/element/index/bulk_actions/form_end.php index 2c47546c..ec8ac0cf 100644 --- a/src/Template/Element/index/bulk_actions/form_end.ctp +++ b/templates/element/index/bulk_actions/form_end.php @@ -20,6 +20,6 @@ 'select' => '
' . $submitButton . '
', ], 'type' => 'select', - 'class' => 'no-selectize' + 'class' => 'no-selectize', ]); echo $this->Form->end(); diff --git a/src/Template/Element/index/bulk_actions/form_start.ctp b/templates/element/index/bulk_actions/form_start.php similarity index 64% rename from src/Template/Element/index/bulk_actions/form_start.ctp rename to templates/element/index/bulk_actions/form_start.php index edc7ca15..4e758de8 100644 --- a/src/Template/Element/index/bulk_actions/form_start.ctp +++ b/templates/element/index/bulk_actions/form_start.php @@ -4,5 +4,5 @@ } echo $this->Form->create(null, [ - 'class' => 'bulk-actions form-horizontal' + 'class' => 'bulk-actions form-horizontal', ]); diff --git a/src/Template/Element/index/bulk_actions/record.ctp b/templates/element/index/bulk_actions/record.php similarity index 100% rename from src/Template/Element/index/bulk_actions/record.ctp rename to templates/element/index/bulk_actions/record.php diff --git a/src/Template/Element/index/bulk_actions/table.ctp b/templates/element/index/bulk_actions/table.php similarity index 100% rename from src/Template/Element/index/bulk_actions/table.ctp rename to templates/element/index/bulk_actions/table.php diff --git a/src/Template/Element/index/download_formats.ctp b/templates/element/index/download_formats.php similarity index 90% rename from src/Template/Element/index/download_formats.ctp rename to templates/element/index/download_formats.php index 4be547f2..6670556f 100644 --- a/src/Template/Element/index/download_formats.ctp +++ b/templates/element/index/download_formats.php @@ -7,7 +7,7 @@ : Html->link($indexFormat['title'], $indexFormat['url'], [ - 'target' => '_blank' + 'target' => '_blank', ]); ?>
diff --git a/src/Template/Element/index/finder_scopes.ctp b/templates/element/index/finder_scopes.php similarity index 87% rename from src/Template/Element/index/finder_scopes.ctp rename to templates/element/index/finder_scopes.php index 61c80f4e..12cd2ce3 100644 --- a/src/Template/Element/index/finder_scopes.ctp +++ b/templates/element/index/finder_scopes.php @@ -5,7 +5,7 @@ $finder = $this->request->query('finder'); foreach ($indexFinderScopes as $indexFinderScope) { - $scopeOptions = ['class' => 'btn btn-default btn-sm', 'role' => 'button']; + $scopeOptions = ['class' => 'btn btn-secondary btn-sm', 'role' => 'button']; $scopeFinder = $indexFinderScope['finder']; if (empty($finder) && $scopeFinder === 'all') { diff --git a/src/Template/Element/index/gallery.ctp b/templates/element/index/gallery.php similarity index 80% rename from src/Template/Element/index/gallery.ctp rename to templates/element/index/gallery.php index 84647e3f..e86bf683 100644 --- a/src/Template/Element/index/gallery.ctp +++ b/templates/element/index/gallery.php @@ -25,10 +25,10 @@ ?>
Html->image($imageContent, $imageOptions + ['alt' => $imageAltContent]); + if (empty($imageContent)) { + $imageContent = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; + } + echo $this->Html->image($imageContent, $imageOptions + ['alt' => $imageAltContent]); ?> diff --git a/src/Template/Element/index/pagination.ctp b/templates/element/index/pagination.php similarity index 82% rename from src/Template/Element/index/pagination.ctp rename to templates/element/index/pagination.php index 501d77d3..54ce66bb 100644 --- a/src/Template/Element/index/pagination.ctp +++ b/templates/element/index/pagination.php @@ -5,7 +5,9 @@
Paginator->hasPage(2)) { - echo $this->Paginator->numbers([ + echo $this->Paginator->links([ + 'first' => true, + 'last' => true, 'prev' => true, 'next' => true, ]); diff --git a/src/Template/Element/index/table.ctp b/templates/element/index/table.php similarity index 79% rename from src/Template/Element/index/table.ctp rename to templates/element/index/table.php index 4044bee0..2184d7e8 100644 --- a/src/Template/Element/index/table.ctp +++ b/templates/element/index/table.php @@ -10,16 +10,16 @@
- + @@ -33,10 +33,10 @@ element('index/bulk_actions/record', compact('bulkActions', 'primaryKey', 'singularVar')); ?> element('index/table_columns', compact('singularVar')); ?> - + diff --git a/src/Template/Element/index/table_columns.ctp b/templates/element/index/table_columns.php similarity index 74% rename from src/Template/Element/index/table_columns.ctp rename to templates/element/index/table_columns.php index 8028a4d6..77505cb5 100644 --- a/src/Template/Element/index/table_columns.ctp +++ b/templates/element/index/table_columns.php @@ -1,6 +1,6 @@ $options) { - $tdOptions = isset($options['td']) ? $options['td'] : []; + $tdOptions = $options['td'] ?? []; unset($options['td']); echo $this->Html->tag('td', $this->CrudView->process($field, $singularVar, $options), $tdOptions); diff --git a/src/Template/Element/menu/divider.ctp b/templates/element/menu/divider.php similarity index 100% rename from src/Template/Element/menu/divider.ctp rename to templates/element/menu/divider.php diff --git a/src/Template/Element/menu/dropdown.ctp b/templates/element/menu/dropdown.php similarity index 100% rename from src/Template/Element/menu/dropdown.ctp rename to templates/element/menu/dropdown.php diff --git a/src/Template/Element/menu/item.ctp b/templates/element/menu/item.php similarity index 100% rename from src/Template/Element/menu/item.ctp rename to templates/element/menu/item.php diff --git a/src/Template/Element/search.ctp b/templates/element/search.php similarity index 52% rename from src/Template/Element/search.ctp rename to templates/element/search.php index 42fa2f2c..990e513d 100644 --- a/src/Template/Element/search.ctp +++ b/templates/element/search.php @@ -4,19 +4,22 @@ } ?> -
+
'form-inline', 'id' => 'searchFilter']; echo $this->Form->create(null, $searchOptions); echo $this->Form->hidden('_search'); ?> -
- Form->controls($searchInputs, ['fieldset' => false]); ?> + Form->controls($searchInputs, ['fieldset' => false]); ?> +
Form->button(__d('crud', 'Filter results'), ['type' => 'submit', 'class' => 'btn btn-primary']); ?> -
+ Search->isSearch()) : ?> + Search->resetLink(__d('crud', 'Reset'), ['class' => 'btn btn-primary']) ?> + +
Form->end(); ?>
diff --git a/src/Template/Element/sidebar.ctp b/templates/element/sidebar.php similarity index 65% rename from src/Template/Element/sidebar.ctp rename to templates/element/sidebar.php index b8063350..19da75e6 100644 --- a/src/Template/Element/sidebar.ctp +++ b/templates/element/sidebar.php @@ -1,8 +1,8 @@ array_merge( (array)Hash::get($actionConfig, 'scaffold.tables_blacklist'), (array)Configure::read('CrudView.tablesBlacklist') - ) + ), ]) ?> element('menu/item', ['item' => $entry]); - } elseif ($entry instanceof MenuDivider) { - echo '
'; - } else { - throw new \Exception('Invalid Menu Item class'); - } + foreach ($sidebarNavigation as $entry) { + if ($entry instanceof MenuItem) { + echo $this->element('menu/item', ['item' => $entry]); + } elseif ($entry instanceof MenuDivider) { + echo '
'; + } else { + throw new \Exception('Invalid Menu Item class'); } + } ?> diff --git a/src/Template/Element/topbar.ctp b/templates/element/topbar.php similarity index 100% rename from src/Template/Element/topbar.ctp rename to templates/element/topbar.php diff --git a/src/Template/Element/view/related.ctp b/templates/element/view/related.php similarity index 100% rename from src/Template/Element/view/related.ctp rename to templates/element/view/related.php diff --git a/src/Template/Element/view/related/has_many.ctp b/templates/element/view/related/has_many.php similarity index 73% rename from src/Template/Element/view/related/has_many.ctp rename to templates/element/view/related/has_many.php index 134d34d7..211c57c2 100644 --- a/src/Template/Element/view/related/has_many.ctp +++ b/templates/element/view/related/has_many.php @@ -11,16 +11,16 @@ $relations = array_merge($associations['oneToMany'], $associations['manyToMany']); $i = 0; -foreach ($relations as $alias => $details): +foreach ($relations as $alias => $details) : $otherSingularVar = $details['propertyName']; ?>
Html->link($link->get('title'), $link->get('url'), $link->get('options')) ?> Paginator->sort($field, isset($options['title']) ? $options['title'] : null, $options); + echo $this->Paginator->sort($field, $options['title'] ?? null, $options); } ?>
element('actions', [ 'singularVar' => $singularVar, - 'actions' => $actions['entity'] + 'actions' => $actions['entity'], ]); ?>
@@ -33,7 +33,7 @@ } foreach ($otherFields as $field) { - echo ""; + echo ''; } ?> @@ -53,9 +53,9 @@ } ?> - diff --git a/src/Template/Element/view/related/has_one.ctp b/templates/element/view/related/has_one.php similarity index 74% rename from src/Template/Element/view/related/has_one.ctp rename to templates/element/view/related/has_one.php index d2c96427..d5954a7e 100644 --- a/src/Template/Element/view/related/has_one.ctp +++ b/templates/element/view/related/has_one.php @@ -5,7 +5,7 @@ return; } -foreach ($associations['oneToOne'] as $alias => $details): +foreach ($associations['oneToOne'] as $alias => $details) : $alias = $details['propertyName']; ?> - diff --git a/src/Template/Layout/default.ctp b/templates/layout/default.php similarity index 80% rename from src/Template/Layout/default.ctp rename to templates/layout/default.php index bdccb581..bcef44b4 100644 --- a/src/Template/Layout/default.ctp +++ b/templates/layout/default.php @@ -22,14 +22,14 @@ Html->image($siteTitleImage); - } - if (empty($siteTitleLink)) { - echo $this->Html->tag('span', $siteTitleContent, ['class' => 'navbar-brand', 'escape' => false]); - } else { - echo $this->Html->link($siteTitleContent, $siteTitleLink, ['class' => 'navbar-brand', 'escape' => false]); - } + if (!empty($siteTitleImage)) { + $siteTitleContent = $this->Html->image($siteTitleImage); + } + if (empty($siteTitleLink)) { + echo $this->Html->tag('span', $siteTitleContent, ['class' => 'navbar-brand', 'escape' => false]); + } else { + echo $this->Html->link($siteTitleContent, $siteTitleLink, ['class' => 'navbar-brand', 'escape' => false]); + } ?> diff --git a/src/Template/Scaffold/add.ctp b/templates/scaffold/add.php similarity index 100% rename from src/Template/Scaffold/add.ctp rename to templates/scaffold/add.php diff --git a/src/Template/Scaffold/dashboard.ctp b/templates/scaffold/dashboard.php similarity index 96% rename from src/Template/Scaffold/dashboard.ctp rename to templates/scaffold/dashboard.php index 6f08d99b..1b60ab65 100644 --- a/src/Template/Scaffold/dashboard.ctp +++ b/templates/scaffold/dashboard.php @@ -3,7 +3,7 @@
element('action-header') ?>
- get('columns')) as $columnNumber): ?> + get('columns')) as $columnNumber) : ?>
getColumnChildren($columnNumber) as $module) : ?> diff --git a/src/Template/Scaffold/edit.ctp b/templates/scaffold/edit.php similarity index 100% rename from src/Template/Scaffold/edit.ctp rename to templates/scaffold/edit.php diff --git a/templates/scaffold/index.php b/templates/scaffold/index.php new file mode 100644 index 00000000..9cee9b1c --- /dev/null +++ b/templates/scaffold/index.php @@ -0,0 +1,52 @@ +fetch('before_index'); ?> + +
+ + exists('search')) { + $this->start('search'); + echo $this->element('search'); + $this->end(); + } + ?> + element('action-header') ?> + + fetch('search'); ?> + +
+ + element('index/bulk_actions/form_start', compact('bulkActions')); ?> + + element('index/finder_scopes', compact('indexFinderScopes')); ?> + + $fields, + 'actions' => $actions, + 'bulkActions' => $bulkActions, + 'primaryKey' => $primaryKey, + 'singularVar' => $singularVar, + 'viewVar' => $viewVar, + $viewVar => ${$viewVar}, + ]; + switch ($indexType) { + case 'table': + echo $this->element('index/table', $_data); + break; + case 'gallery': + echo $this->element('index/gallery', $_data); + break; + case 'blog': + echo $this->element('index/blog', $_data); + break; + default: + echo $this->element($indexType, $_data); + break; + } + ?> + + element('index/bulk_actions/form_end', compact('bulkActions')); ?> + element('index/pagination'); ?> +
+ +fetch('after_index'); ?> diff --git a/src/Template/Scaffold/view.ctp b/templates/scaffold/view.php similarity index 67% rename from src/Template/Scaffold/view.ctp rename to templates/scaffold/view.php index ec140cee..aed328a5 100644 --- a/src/Template/Scaffold/view.ctp +++ b/templates/scaffold/view.php @@ -1,5 +1,5 @@ extract('foreignKey')->toArray()) : @@ -12,16 +12,16 @@ CrudView->setContext(${$viewVar}); foreach ($fields as $field => $options) { - if (in_array($field, array($primaryKey))) { + if (in_array($field, [$primaryKey], true)) { continue; } echo '
'; - printf("", array_key_exists($field, $assocMap) ? + printf('', array_key_exists($field, $assocMap) ? Inflector::singularize(Inflector::humanize(Inflector::underscore($assocMap[$field]))) : - (isset($options['title']) ? $options['title'] : Inflector::humanize($field))); - printf("", $this->CrudView->process($field, ${$viewVar}, $options) ?: " "); + ($options['title'] ?? Inflector::humanize($field))); + printf('', $this->CrudView->process($field, ${$viewVar}, $options) ?: ' '); echo ''; } diff --git a/tests/TestCase/Dashboard/DashboardTest.php b/tests/TestCase/Dashboard/DashboardTest.php index 3e53eba0..576e600d 100644 --- a/tests/TestCase/Dashboard/DashboardTest.php +++ b/tests/TestCase/Dashboard/DashboardTest.php @@ -1,6 +1,10 @@ assertEquals($expected, $dashboard->get('title')); @@ -27,22 +31,21 @@ public function testConstruct() $this->assertEquals($expected, $dashboard->get('title')); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Valid columns value must be one of [1, 2, 3, 4, 6, 12] - */ public function testInvalidConstruct() { - $dashboard = new Dashboard(null, 0); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Valid columns value must be one of [1, 2, 3, 4, 6, 12]'); + + new Dashboard(null, 0); } public function testColumnChildren() { - $dashboard = new Dashboard; + $dashboard = new Dashboard(); $expected = []; $this->assertEquals($expected, $dashboard->getColumnChildren(1)); - $cell = new DashboardTableCell; + $cell = new DashboardTableCell(new ServerRequest(), new Response()); $return = $dashboard->addToColumn($cell); $this->assertEquals($dashboard, $return); $this->assertEquals([$cell], $dashboard->getColumnChildren(1)); diff --git a/tests/TestCase/Dashboard/Module/ActionLinkItemTest.php b/tests/TestCase/Dashboard/Module/ActionLinkItemTest.php index 95140b6f..de33ff56 100644 --- a/tests/TestCase/Dashboard/Module/ActionLinkItemTest.php +++ b/tests/TestCase/Dashboard/Module/ActionLinkItemTest.php @@ -1,4 +1,6 @@ assertEquals($expected, $item->get('actions')); $actions = $item->get('actions'); - $expected = ['class' => ['btn btn-default']]; + $expected = ['class' => ['btn btn-secondary']]; $this->assertEquals($expected, $actions[0]->get('options')); } } diff --git a/tests/TestCase/Dashboard/Module/LinkItemTest.php b/tests/TestCase/Dashboard/Module/LinkItemTest.php index cbcd82b0..2cdeaebd 100644 --- a/tests/TestCase/Dashboard/Module/LinkItemTest.php +++ b/tests/TestCase/Dashboard/Module/LinkItemTest.php @@ -1,4 +1,6 @@ assertEquals($expected, $item->get('options')); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Missing title for LinkItem action - */ public function testInvalidTitle() { - $item = new LinkItem('', null); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Missing title for LinkItem action'); + + new LinkItem('', null); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Invalid url specified for LinkItem - */ public function testInvalidNullUrl() { - $item = new LinkItem('Title', null); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid url specified for LinkItem'); + + new LinkItem('Title', null); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Invalid url specified for LinkItem - */ public function testInvalidEmptyUrl() { - $item = new LinkItem('Title', ''); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid url specified for LinkItem'); + + new LinkItem('Title', ''); } public function testOptions() diff --git a/tests/TestCase/View/CrudViewTest.php b/tests/TestCase/View/CrudViewTest.php index d059d288..cee98ee4 100644 --- a/tests/TestCase/View/CrudViewTest.php +++ b/tests/TestCase/View/CrudViewTest.php @@ -1,13 +1,15 @@ [ 'CrudView' => [ 'className' => 'CrudView.CrudView', - 'fieldFormatters' => ['datetime' => 'formatTime'] - ] - ] + 'fieldFormatters' => ['datetime' => 'formatTime'], + ], + ], ] ); diff --git a/tests/TestCase/View/Helper/CrudViewHelperTest.php b/tests/TestCase/View/Helper/CrudViewHelperTest.php index e22a9b93..70998e9a 100644 --- a/tests/TestCase/View/Helper/CrudViewHelperTest.php +++ b/tests/TestCase/View/Helper/CrudViewHelperTest.php @@ -1,7 +1,10 @@ CrudView = $this->getMockBuilder(CrudViewHelper::class) ->setConstructorArgs([$this->View]) - ->setMethods(['columnType']) + ->setMethods(['columnType', 'getContext']) ->getMock(); + $this->CrudView + ->expects($this->any()) + ->method('getContext') + ->will($this->returnValue(new Entity())); + $this->CrudView ->expects($this->any()) ->method('columnType') diff --git a/tests/TestCase/View/Widget/DateTimeWidgetTest.php b/tests/TestCase/View/Widget/DateTimeWidgetTest.php index 2f73da4d..c5b6e203 100644 --- a/tests/TestCase/View/Widget/DateTimeWidgetTest.php +++ b/tests/TestCase/View/Widget/DateTimeWidgetTest.php @@ -1,7 +1,10 @@ initComparePath(); + Configure::write('CrudView.datetimePicker', []); } /** @@ -32,7 +37,9 @@ public function setUp() public function testRenderSimple($compareFileName, $data) { $context = $this->getMockBuilder(ContextInterface::class)->getMock(); - $templates = new StringTemplate(); + $templates = new StringTemplate([ + 'input' => '', + ]); $selectBox = new SelectBoxWidget($templates); $instance = new DateTimeWidget($templates, $selectBox); @@ -52,16 +59,56 @@ public function renderProvider() return [ [ 'simple.html', - ['id' => 'the-id', 'name' => 'the-name', 'val' => '', 'type' => 'x', 'required' => false] + [ + 'id' => 'the-id', + 'name' => 'the-name', + 'val' => '', + 'type' => 'date', + 'required' => false, + ], ], [ 'with-string-value.html', - ['id' => 'the-id', 'name' => 'the-name', 'val' => '2000-01-01', 'type' => 'x', 'required' => false] + [ + 'id' => 'the-id2', + 'name' => 'the-name2', + 'val' => '2000-01-01', + 'type' => 'date', + 'required' => false, + ], ], [ 'with-date-value.html', - ['id' => 'the-id', 'name' => 'the-name', 'val' => (new FrozenTime(strtotime('2000-01-01'))), 'type' => 'x', 'required' => false] - ] + [ + 'id' => 'the-id3', + 'name' => 'the-name3', + 'val' => new FrozenDate('2000-01-01'), + 'type' => 'date', + 'required' => false, + ], + ], + [ + 'no-datetime-picker.html', + [ + 'id' => 'the-id4', + 'name' => 'the-name4', + 'val' => '', + 'type' => 'date', + 'required' => false, + 'datetimePicker' => false, + ], + ], + [ + 'no-wrap.html', + [ + 'id' => 'the-id5', + 'name' => 'the-name5', + 'val' => '', + 'type' => 'date', + 'required' => false, + 'datetimePicker' => ['data-wrap' => 'false'], + ], + ], ]; } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 4f4d373d..edfce42b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,6 @@ dirname(dirname(__FILE__)) . DS]); +Plugin::getCollection()->add(new \CrudView\Plugin()); diff --git a/tests/comparisons/View/Widget/DateTimeWidget/no-datetime-picker.html b/tests/comparisons/View/Widget/DateTimeWidget/no-datetime-picker.html new file mode 100644 index 00000000..8a6205a9 --- /dev/null +++ b/tests/comparisons/View/Widget/DateTimeWidget/no-datetime-picker.html @@ -0,0 +1,6 @@ + diff --git a/tests/comparisons/View/Widget/DateTimeWidget/no-wrap.html b/tests/comparisons/View/Widget/DateTimeWidget/no-wrap.html new file mode 100644 index 00000000..b9d556ba --- /dev/null +++ b/tests/comparisons/View/Widget/DateTimeWidget/no-wrap.html @@ -0,0 +1,10 @@ + diff --git a/tests/comparisons/View/Widget/DateTimeWidget/simple.html b/tests/comparisons/View/Widget/DateTimeWidget/simple.html index b68d94cf..b1f84ce4 100644 --- a/tests/comparisons/View/Widget/DateTimeWidget/simple.html +++ b/tests/comparisons/View/Widget/DateTimeWidget/simple.html @@ -1,19 +1,51 @@
+ data-alt-input-class="form-control" + data-wrap="true" + data-date-format="Y-m-d" + class="input-group flatpickr"> - + data-input="" + value="" + class="form-control"/> +
+ + +
diff --git a/tests/comparisons/View/Widget/DateTimeWidget/with-date-value.html b/tests/comparisons/View/Widget/DateTimeWidget/with-date-value.html index f4723f39..1ae26bdb 100644 --- a/tests/comparisons/View/Widget/DateTimeWidget/with-date-value.html +++ b/tests/comparisons/View/Widget/DateTimeWidget/with-date-value.html @@ -1,19 +1,51 @@
+ data-alt-input-class="form-control" + data-wrap="true" + data-date-format="Y-m-d" + class="input-group flatpickr"> - + name="the-name3" + id="the-id3" + data-input="" + value="2000-01-01" + class="form-control"/> +
+ + +
diff --git a/tests/comparisons/View/Widget/DateTimeWidget/with-string-value.html b/tests/comparisons/View/Widget/DateTimeWidget/with-string-value.html index 6776043d..f5056d18 100644 --- a/tests/comparisons/View/Widget/DateTimeWidget/with-string-value.html +++ b/tests/comparisons/View/Widget/DateTimeWidget/with-string-value.html @@ -1,19 +1,51 @@
+ data-alt-input-class="form-control" + data-wrap="true" + data-date-format="Y-m-d" + class="input-group flatpickr"> - + class="form-control"/> +
+ + +
diff --git a/webroot/css/local.css b/webroot/css/local.css index 367a419c..94b1ad82 100644 --- a/webroot/css/local.css +++ b/webroot/css/local.css @@ -1,37 +1,28 @@ -/****************************************************************************** - * Use cloudflare's glyphicons - *****************************************************************************/ -@font-face { - font-family: 'Glyphicons Halflings'; - - src: url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/fonts/glyphicons-halflings-regular.eot'); - src: url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), - url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/fonts/glyphicons-halflings-regular.woff2') format('woff2'), - url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/fonts/glyphicons-halflings-regular.woff') format('woff'), - url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/fonts/glyphicons-halflings-regular.ttf') format('truetype'), - url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); -} - - /****************************************************************************** * Generic form hacks *****************************************************************************/ + .bulk-action { width: 32px; } + .bulk-action .checkbox { margin-bottom: 0; } + .bulk-action-submit { width: 50%; } + .bulk-action-submit label { width: auto; } + .bulk-action-submit select { display: inline-block; width: 50%; } + .bulk-action-submit input { width: auto; } @@ -48,68 +39,68 @@ div.input { margin-bottom: 10px } -.btn { - margin-left: 5px -} h2 .actions { margin-left: 15px; } -.search-filters .form-inline .form-group { - padding-left:10px; -} - -.search-filters .form-inline .form-group label { - padding-right:10px; +.search-filters .form-inline .form-group label, +.search-filters .form-inline .btn { + margin-right: .25rem !important; } -.search-filters .form-inline .form-control { - min-width:90px !important; +.search-filters .form-inline .form-group { + margin-right: .5rem !important; } -.search-filters .form-inline .form-control input, .search-filters .form-inline .form-control select { - min-width:90px !important; +.search-filters .form-inline .form-group { + margin-bottom: 1rem !important; } -.selectize-input.items.full { - padding-right:33px; +.search-filters .form-inline .form-control input { + min-width: 90px !important; } -div.actions-wrapper { - margin: 10px 0; +.search-filters .form-inline select.autocomplete { + min-width: 150px; } .text-truncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } + .gallery-content { text-align: center; } + .pagination-wrapper { clear: both; margin: 20px 0; text-align: center; } + .download-links { float: left; } + .pagination-container { float: right } + .pagination-container .pagination { margin: 0; } + table tr td.actions { white-space: nowrap; width: 1px; } .nav > li > .nav-header { - border-radius: 4px; - display: block; - font-weight: bold; - padding: 10px 15px; - position: relative; + border-radius: 4px; + display: block; + font-weight: bold; + padding: 10px 15px; + position: relative; } diff --git a/webroot/js/local.js b/webroot/js/local.js index 3de938e0..ebe5b895 100644 --- a/webroot/js/local.js +++ b/webroot/js/local.js @@ -13,71 +13,65 @@ var CrudView = { } }, - datePicker: function (selector) { - $(selector).each(function() { - var picker = $(this); - var date = null; - - if (picker.data('timestamp') && picker.data('timezone-offset')) { - var timezoneOffset = picker.data('timezone-offset'); - date = new Date(picker.data('timestamp') * 1000); - - picker.parents('form').on('submit', function () { - var timezoneDiff = timezoneOffset + date.getTimezoneOffset(); - var currentDate = picker.data('DateTimePicker').date(); - var convertedDate = currentDate.add(timezoneDiff, 'minutes'); - picker.data('DateTimePicker').date(convertedDate); - }); + flatpickr: function (selector) { + $(selector).flatpickr(); + }, + + select2: function (selector) { + $(selector).each(function () { + var $this = $(this), + config = {theme: 'bootstrap4'}; + + if (!$this.prop('multiple') && $this.find('option:first').val() === '') { + config.allowClear = true; + config.placeholder = ''; } - picker.datetimepicker({ - locale: $(this).data('locale'), - format: $(this).data('format'), - date: date ? date : picker.val() - }); + $(this).select2(config); }); }, - selectize: function (selector) { - $(selector).selectize({plugins: ['remove_button']}); - }, - autocomplete: function (selector) { - $(selector).each(function (i, e) { - e = $(e); - e.selectize({ - maxItems: e.data('max-items') || 1, - maxOptions: e.data('max-options') || 10, - hideSelected: e.data('hide-selected'), - closeAfterSelect: e.data('close-after-select'), - create: !e.data('exact-match'), - persist: false, - render: { - 'option_create': function(data, escape) { - return '
🔍 ' + escape(data.input) + '
'; - } - }, - load: function (query, callback) { - var data = {}; + $(selector).each(function (i, ele) { + var $ele = $(ele); - data[e.data('filter-field') || e.attr('name')] = query; + $ele.select2({ + theme: 'bootstrap4', + minimumInputLength: 1, + ajax: { + delay: 250, + url: $ele.data('url'), + dataType: 'json', + data: function (params) { + var query = {}; + query[$ele.data('filter-field') || $ele.attr('name')] = params.term; - if (e.data('dependent-on') && $('#' + e.data('dependent-on')).val()) { - data[e.data('dependent-on-field')] = $('#' + e.data('dependent-on')).val(); - } - $.ajax({ - url: e.data('url'), - dataType: 'json', - data: data, - error: function() { - callback(); - }, - success: function(res) { - callback($.map(res.data, function (name, id) { - return {value: id, text: name}; - })); + if ($ele.data('dependent-on') && $('#' + $ele.data('dependent-on')).val()) { + data[$ele.data('dependent-on-field')] = $('#' + $ele.data('dependent-on')).val(); } - }); + + return query; + }, + processResults: function (data, params) { + var results = [], + inputType = $ele.data('inputType'), + term = params.term.toLowerCase(); + + if (data.data) { + $.each(data.data, function(id, text) { + if (text.toLowerCase().indexOf(term) > -1) { + results.push({ + id: inputType === 'text' ? text : id, + text: text + }); + } + }); + } + + return { + results: results + }; + } } }); }); @@ -100,16 +94,21 @@ var CrudView = { }) }, - initialize: function() { + tooltip: function () { + $('[data-toggle="tooltip"]').tooltip(); + }, + + initialize: function () { this.bulkActionForm('.bulk-actions'); - this.datePicker('[role=datetime-picker]'); - this.selectize('select:not(.autocomplete, .no-selectize)'); + this.flatpickr('.flatpickr'); + this.select2('select[multiple]:not(.no-select2), select.select2'); this.autocomplete('input.autocomplete, select.autocomplete'); this.dirtyForms(); this.dropdown(); + this.tooltip(); } }; -$(function() { +$(function () { CrudView.initialize(); });
" . Inflector::humanize($field) . "' . Inflector::humanize($field) . 'Actions - Html->link(__d('crud', 'View'), array('plugin' => $details['plugin'], 'controller' => $details['controller'], 'action' => 'view', ${$otherSingularVar}[$details['primaryKey']]), array('class' => 'btn btn-default')); ?> - Html->link(__d('crud', 'Edit'), array('plugin' => $details['plugin'], 'controller' => $details['controller'], 'action' => 'edit', ${$otherSingularVar}[$details['primaryKey']]), array('class' => 'btn btn-default')); ?> - Html->link(__d('crud', 'Delete'), array('plugin' => $details['plugin'], 'controller' => $details['controller'], 'action' => 'delete', ${$otherSingularVar}[$details['primaryKey']]), array('class' => 'btn btn-default')); ?> + Html->link(__d('crud', 'View'), ['plugin' => $details['plugin'], 'controller' => $details['controller'], 'action' => 'view', ${$otherSingularVar}[$details['primaryKey']]], ['class' => 'btn btn-secondary']); ?> + Html->link(__d('crud', 'Edit'), ['plugin' => $details['plugin'], 'controller' => $details['controller'], 'action' => 'edit', ${$otherSingularVar}[$details['primaryKey']]], ['class' => 'btn btn-secondary']); ?> + Html->link(__d('crud', 'Delete'), ['plugin' => $details['plugin'], 'controller' => $details['controller'], 'action' => 'delete', ${$otherSingularVar}[$details['primaryKey']]], ['class' => 'btn btn-secondary']); ?>
%s%s%s%s