diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26fc7122..44c76d7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,70 +2,14 @@ name: CI on: [push, pull_request] +permissions: + contents: read + jobs: testsuite: - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - php-version: ['7.4', '8.0', '8.1', '8.2'] - dependencies: ['highest'] - include: - - php-version: '7.2' - dependencies: 'lowest' - - steps: - - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: mbstring, intl, pdo_${{ matrix.db-type }} - coverage: pcov - - - name: Composer install - uses: ramsey/composer-install@v2 - with: - dependency-versions: ${{ matrix.dependencies }} - - - name: Run PHPUnit - run: | - if [[ ${{ matrix.php-version }} == '7.4' ]]; then - vendor/bin/phpunit --coverage-clover=coverage.xml - else - vendor/bin/phpunit - fi - - - name: Code Coverage Report - if: matrix.php-version == '7.4' - uses: codecov/codecov-action@v3 + uses: cakephp/.github/.github/workflows/testsuite-without-db.yml@5.x + secrets: inherit cs-stan: - name: Coding Standard & Static Analysis - runs-on: ubuntu-22.04 - - steps: - - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - extensions: mbstring, intl - coverage: none - tools: vimeo/psalm:5.9, phpstan:1.10 - - - name: Composer Install - run: composer require --dev cakephp/cakephp-codesniffer:^4.1 - - - name: Run phpcs - run: vendor/bin/phpcs --report=checkstyle --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/ - - - name: Run psalm - if: success() || failure() - run: psalm --output-format=github - - - name: Run phpstan - if: success() || failure() - run: phpstan analyse --error-format=github + uses: ADmad/.github/.github/workflows/cs-stan.yml@master + secrets: inherit diff --git a/.gitignore b/.gitignore index 5209c916..ad935574 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ vendor/ composer.lock tmp .phpunit.result.cache +.phpunit.cache diff --git a/README.md b/README.md index 580444f0..2f44c481 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,3 @@ If you happen to stumble upon a bug, please feel free to create a pull request w (optionally with a test), and a description of the bug and how it was resolved. You can also create an issue with a description to raise awareness of the bug. - -# Features - -If you have a good idea for a Crud View feature, please join us on IRC and let's discuss it. Pull -requests are always more than welcome. - -# Support / Questions - -You can join us on IRC in the #FriendsOfCake freenode channel for any support or questions. diff --git a/composer.json b/composer.json index 36542f62..17234207 100644 --- a/composer.json +++ b/composer.json @@ -29,14 +29,14 @@ } ], "require": { - "cakephp/cakephp": "^4.0", - "friendsofcake/bootstrap-ui": "^3.0", - "friendsofcake/crud": "^6.0" + "cakephp/cakephp": "^5.0", + "friendsofcake/crud": "^7.0", + "friendsofcake/bootstrap-ui": "^5.0" }, "require-dev": { - "friendsofcake/cakephp-test-utilities": "^2.0", - "markstory/asset_compress": "^4.0", - "phpunit/phpunit": "~8.5.0 || ^9.3" + "friendsofcake/cakephp-test-utilities": "^3.0", + "markstory/asset_compress": "^5.0", + "phpunit/phpunit": "^10.1 || ^11.0" }, "autoload": { "psr-4": { @@ -54,7 +54,6 @@ "wiki": "http://cakephp.nu/cakephp-crud/", "irc": "irc://irc.freenode.org/friendsofcake" }, - "prefer-stable": true, "config": { "sort-packages": true, "allow-plugins": { diff --git a/config/asset_compress.ini b/config/asset_compress.ini index 1fb9575c..3646c735 100644 --- a/config/asset_compress.ini +++ b/config/asset_compress.ini @@ -1,13 +1,13 @@ [crudview.css] -files[]=https://cdn.jsdelivr.net/npm/bootstrap@4.5/dist/css/bootstrap.min.css +files[]=https://cdn.jsdelivr.net/npm/bootstrap@5.3/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[]=https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3/dist/select2-bootstrap-5-theme.min.css files[]=plugin:CrudView:css/local.css [crudview_head.js] -files[]=https://cdn.jsdelivr.net/npm/jquery@3.5/dist/jquery.min.js -files[]=https://cdn.jsdelivr.net/npm/bootstrap@4.5/dist/js/bootstrap.min.js +files[]=https://cdn.jsdelivr.net/npm/jquery@3.7/dist/jquery.min.js +files[]=https://cdn.jsdelivr.net/npm/bootstrap@5.3/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.0/jquery.dirtyforms.min.js diff --git a/config/defaults.php b/config/defaults.php index d012b56d..ba532599 100644 --- a/config/defaults.php +++ b/config/defaults.php @@ -39,5 +39,6 @@ 'tablesBlacklist' => [ 'phinxlog', ], + 'helperConfig' => [], ], ]; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a039fbbe..e45215cb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -11,7 +11,7 @@ parameters: path: src/Listener/ViewListener.php - - message: "#^Parameter \\#1 \\$field of method CrudView\\\\Listener\\\\ViewListener\\:\\:_deriveFieldFromContext\\(\\) expects string, array\\\\|string\\|null given\\.$#" + message: "#^Parameter \\#1 \\$field of method CrudView\\\\Listener\\\\ViewListener\\:\\:_deriveFieldFromContext\\(\\) expects string, array\\\\|string\\|null given\\.$#" count: 1 path: src/Listener/ViewListener.php @@ -24,24 +24,3 @@ parameters: message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:searchManager\\(\\)\\.$#" count: 1 path: src/Listener/ViewSearchListener.php - - - - message: "#^Binary operation \"\\+\" between string and non\\-empty\\-array 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 3b09bdc5..20cb9a85 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,12 +2,11 @@ includes: - phpstan-baseline.neon parameters: - level: 7 - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false + level: 8 paths: - src universalObjectCratesClasses: - Crud\Event\Subject - bootstrapFiles: - - vendor/cakephp/cakephp/src/Database/Exception/DatabaseException.php + ignoreErrors: + - identifier: missingType.iterableValue + - identifier: missingType.generics diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 279d1795..af94e927 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,36 +1,19 @@ - - - - - - + ./tests/TestCase - - - - - - - - - - - - - - ./src/ - - + + + + + + + + + src/ + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 00000000..8bdfa5f2 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,3 @@ + + + diff --git a/psalm.xml b/psalm.xml index d38a0aea..e3b7c384 100644 --- a/psalm.xml +++ b/psalm.xml @@ -5,9 +5,10 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" + errorBaseline="psalm-baseline.xml" + usePhpDocMethodsWithoutMagicCall="true" findUnusedBaselineEntry="true" findUnusedCode="false" - > diff --git a/src/Action/DashboardAction.php b/src/Action/DashboardAction.php index cc9bfde4..9001fe8f 100644 --- a/src/Action/DashboardAction.php +++ b/src/Action/DashboardAction.php @@ -6,12 +6,13 @@ use Crud\Action\BaseAction; use Crud\Traits\ViewTrait; use CrudView\Dashboard\Dashboard; +use function Cake\I18n\__d; class DashboardAction extends BaseAction { use ViewTrait; - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'enabled' => true, 'view' => null, ]; @@ -21,7 +22,7 @@ class DashboardAction extends BaseAction * * @return void */ - protected function _get() + protected function _get(): void { $pageTitle = $this->getConfig('scaffold.page_title', __d('CrudView', 'Dashboard')); $this->setConfig('scaffold.page_title', $pageTitle); diff --git a/src/Breadcrumb/ActiveBreadcrumb.php b/src/Breadcrumb/ActiveBreadcrumb.php index fc15cd47..069c925f 100644 --- a/src/Breadcrumb/ActiveBreadcrumb.php +++ b/src/Breadcrumb/ActiveBreadcrumb.php @@ -11,7 +11,7 @@ class ActiveBreadcrumb extends Breadcrumb * @inheritDoc * @psalm-suppress MissingParamType */ - public function __construct($title, $url = null, array $options = []) + public function __construct(string|array $title, string|array|null $url = null, array $options = []) { if (!isset($options['class'])) { $options['class'] = ''; diff --git a/src/Breadcrumb/Breadcrumb.php b/src/Breadcrumb/Breadcrumb.php index 200bc21a..23269192 100644 --- a/src/Breadcrumb/Breadcrumb.php +++ b/src/Breadcrumb/Breadcrumb.php @@ -10,35 +10,35 @@ class Breadcrumb * If the specified $url is a link, then this will * also be wrapped by `` tags. * - * @var string|array + * @var array|string **/ - protected $title; + protected string|array $title; /** * Cake-relative URL or array of URL parameters, or * external URL (starts with http://) * - * @var string|array|null + * @var array|string|null */ - protected $url = null; + protected string|array|null $url = null; /** * Array of options and HTML attributes. * * @var array **/ - protected $options = []; + protected array $options = []; /** * Contains a breadcrumb entry * - * @param string|array $title If provided as a string, it represents the title of the crumb. + * @param array|string $title If provided as a string, it represents the title of the crumb. * Alternatively, if you want to add multiple crumbs at once, you can provide an array, with each values being a * single crumb. Arrays are expected to be of this form: * - *title* The title of the crumb * - *link* The link of the crumb. If not provided, no link will be made * - *options* Options of the crumb. See description of params option of this method. - * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to + * @param array|string|null $url URL of the crumb. Either a string, an array of route params to pass to * Url::build() or null / empty if the crumb does not have a link. * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will * be rendered in (a
  • tag by default). It accepts two special keys: @@ -46,7 +46,7 @@ class Breadcrumb * the link) * - *templateVars*: Specific template vars in case you override the templates provided. */ - public function __construct($title, $url = null, array $options = []) + public function __construct(string|array $title, string|array|null $url = null, array $options = []) { $this->title = $title; $this->url = $url; @@ -56,9 +56,9 @@ public function __construct($title, $url = null, array $options = []) /** * Returns the menu item title * - * @return string|array + * @return array|string */ - public function getTitle() + public function getTitle(): string|array { return $this->title; } @@ -66,9 +66,9 @@ public function getTitle() /** * Returns the menu item ur * - * @return string|array|null + * @return array|string|null */ - public function getUrl() + public function getUrl(): string|array|null { return $this->url; } diff --git a/src/CrudViewPlugin.php b/src/CrudViewPlugin.php new file mode 100644 index 00000000..efd93639 --- /dev/null +++ b/src/CrudViewPlugin.php @@ -0,0 +1,61 @@ + 12, diff --git a/src/Dashboard/Module/ActionLinkItem.php b/src/Dashboard/Module/ActionLinkItem.php index 2948f585..8bb8b512 100644 --- a/src/Dashboard/Module/ActionLinkItem.php +++ b/src/Dashboard/Module/ActionLinkItem.php @@ -12,19 +12,19 @@ class ActionLinkItem extends LinkItem * * @var array **/ - protected $actions = []; + protected array $actions = []; /** * Constructor * - * @param string|array $title The content to be wrapped by `` tags. + * @param array|string $title The content to be wrapped by `` tags. * Can be an array if $url is null. If $url is null, $title will be used as both the URL and title. - * @param string|array|null $url Cake-relative URL or array of URL parameters, or + * @param array|string|null $url Cake-relative URL or array of URL parameters, or * external URL (starts with http://) * @param array $options Array of options and HTML attributes. * @param array $actions Array of ActionItems */ - public function __construct($title, $url, $options = [], array $actions = []) + public function __construct(string|array $title, string|array|null $url, array $options = [], array $actions = []) { parent::__construct($title, $url, $options); $this->set('actions', $actions); @@ -36,7 +36,7 @@ public function __construct($title, $url, $options = [], array $actions = []) * @param array $actions Array of options and HTML attributes. * @return array */ - protected function _setActions($actions) + protected function _setActions(array $actions): array { return (new Collection($actions))->map(function ($value) { $options = (array)$value->get('options') + ['class' => ['btn btn-secondary']]; diff --git a/src/Dashboard/Module/LinkItem.php b/src/Dashboard/Module/LinkItem.php index eec60d11..2ad4b969 100644 --- a/src/Dashboard/Module/LinkItem.php +++ b/src/Dashboard/Module/LinkItem.php @@ -15,33 +15,33 @@ class LinkItem * * @var string **/ - protected $title; + protected string $title; /** * Cake-relative URL or array of URL parameters, or * external URL (starts with http://) * - * @var string|array|null + * @var array|string|null */ - protected $url = null; + protected string|array|null $url = null; /** * Array of options and HTML attributes. * * @var array **/ - protected $options = []; + protected array $options = []; /** * Constructor * - * @param string|array $title The content to be wrapped by `` tags. + * @param array|string $title The content to be wrapped by `` tags. * Can be an array if $url is null. If $url is null, $title will be used as both the URL and title. - * @param string|array|null $url Cake-relative URL or array of URL parameters, or + * @param array|string|null $url Cake-relative URL or array of URL parameters, or * external URL (starts with http://) * @param array $options Array of options and HTML attributes. */ - public function __construct($title, $url, array $options = []) + public function __construct(string|array $title, string|array|null $url, array $options = []) { $this->set('title', $title); $this->set('url', $url); @@ -51,12 +51,12 @@ public function __construct($title, $url, array $options = []) /** * title property setter * - * @param string|array|null $title A title for the link - * @return string|array + * @param array|string|null $title A title for the link + * @return array|string */ - protected function _setTitle($title) + protected function _setTitle(string|array|null $title): string|array { - if (empty($title)) { + if ($title === null || $title === '') { throw new InvalidArgumentException('Missing title for LinkItem action'); } @@ -66,13 +66,13 @@ protected function _setTitle($title) /** * url property setter * - * @param string|array|null $url Cake-relative URL or array of URL parameters, or + * @param array|string|null $url Cake-relative URL or array of URL parameters, or * external URL (starts with http://) - * @return string|array + * @return array|string */ - protected function _setUrl($url) + protected function _setUrl(string|array|null $url): string|array { - if ($url === null || empty($url)) { + if ($url === null || $url === '') { throw new InvalidArgumentException('Invalid url specified for LinkItem'); } @@ -83,14 +83,10 @@ protected function _setUrl($url) * options property setter * * @param array $options Array of options and HTML attributes. - * @return string|array + * @return array|string */ - protected function _setOptions($options) + protected function _setOptions(array $options): string|array { - if (empty($options)) { - $options = []; - } - $url = $this->get('url'); if (!is_array($url)) { $isHttp = substr($url, 0, 7) === 'http://'; diff --git a/src/Listener/Traits/FormTypeTrait.php b/src/Listener/Traits/FormTypeTrait.php index 11e70f72..e00d40a0 100644 --- a/src/Listener/Traits/FormTypeTrait.php +++ b/src/Listener/Traits/FormTypeTrait.php @@ -6,6 +6,7 @@ use Cake\Controller\Controller; use Crud\Action\BaseAction; use Crud\Action\EditAction; +use function Cake\I18n\__d; trait FormTypeTrait { @@ -160,9 +161,9 @@ protected function _getDefaultExtraLeftButtons(): array /** * Get form url. * - * @return mixed + * @return array|string|null */ - protected function _getFormUrl() + protected function _getFormUrl(): array|string|null { $action = $this->_action(); diff --git a/src/Listener/Traits/IndexTypeTrait.php b/src/Listener/Traits/IndexTypeTrait.php index d19dee76..5ff4f06b 100644 --- a/src/Listener/Traits/IndexTypeTrait.php +++ b/src/Listener/Traits/IndexTypeTrait.php @@ -4,6 +4,7 @@ namespace CrudView\Listener\Traits; use Cake\Controller\Controller; +use Cake\Datasource\RepositoryInterface; use Crud\Action\BaseAction; use Crud\Action\IndexAction; @@ -84,7 +85,7 @@ protected function _getIndexTitleField(): string $field = $action->getConfig('scaffold.index_title_field'); if ($field === null) { - $field = $this->_table()->getDisplayField(); + $field = $this->_model()->getDisplayField(); } return $field; @@ -154,5 +155,5 @@ abstract protected function _action(?string $name = null): BaseAction; /** * @inheritDoc */ - abstract protected function _table(); + abstract protected function _model(): RepositoryInterface; } diff --git a/src/Listener/Traits/SidebarNavigationTrait.php b/src/Listener/Traits/SidebarNavigationTrait.php index 37340418..5eb32cec 100644 --- a/src/Listener/Traits/SidebarNavigationTrait.php +++ b/src/Listener/Traits/SidebarNavigationTrait.php @@ -24,9 +24,9 @@ public function beforeRenderSidebarNavigation(): void /** * Returns the sidebar navigation to show on scaffolded view * - * @return string|null|false + * @return string|false|null */ - protected function _getSidebarNavigation() + protected function _getSidebarNavigation(): string|false|null { $action = $this->_action(); diff --git a/src/Listener/ViewListener.php b/src/Listener/ViewListener.php index 98f63012..33aa22bf 100644 --- a/src/Listener/ViewListener.php +++ b/src/Listener/ViewListener.php @@ -4,7 +4,7 @@ namespace CrudView\Listener; use Cake\Collection\Collection; -use Cake\Database\Exception; +use Cake\Database\Exception\DatabaseException; use Cake\Event\EventInterface; use Cake\Utility\Hash; use Cake\Utility\Inflector; @@ -15,7 +15,12 @@ use CrudView\Listener\Traits\SiteTitleTrait; use CrudView\Listener\Traits\UtilityNavigationTrait; use CrudView\Traits\CrudViewConfigTrait; +use function Cake\Core\pluginSplit; +use function Cake\I18n\__d; +/** + * @method \Cake\ORM\Table _model() + */ class ViewListener extends BaseListener { use CrudViewConfigTrait; @@ -28,15 +33,16 @@ class ViewListener extends BaseListener /** * Default associations config * - * @var array|null + * @var array */ - protected $associations = null; + protected array $associations; /** * [beforeFind description] * * @param \Cake\Event\EventInterface $event Event. * @return void + * @psalm-param \Cake\Event\EventInterface<\Crud\Event\Subject> $event */ public function beforeFind(EventInterface $event): void { @@ -57,6 +63,7 @@ public function beforeFind(EventInterface $event): void * * @param \Cake\Event\EventInterface $event Event. * @return void + * @psalm-param \Cake\Event\EventInterface<\Crud\Event\Subject> $event */ public function beforePaginate(EventInterface $event): void { @@ -77,6 +84,7 @@ public function beforePaginate(EventInterface $event): void * * @param \Cake\Event\EventInterface $event Event. * @return void + * @psalm-param \Cake\Event\EventInterface<\Crud\Event\Subject> $event */ public function beforeRender(EventInterface $event): void { @@ -88,7 +96,8 @@ public function beforeRender(EventInterface $event): void $this->_entity = $event->getSubject()->entity; } - if ($this->associations === null) { + /** @psalm-suppress RedundantPropertyInitializationCheck */ + if (!isset($this->associations)) { $this->associations = $this->_associations(array_keys($this->_getRelatedModels())); } @@ -125,10 +134,12 @@ public function beforeRender(EventInterface $event): void * * @param \Cake\Event\EventInterface $event Event. * @return void + * @psalm-param \Cake\Event\EventInterface<\Crud\Event\Subject> $event */ public function setFlash(EventInterface $event): void { unset($event->getSubject()->params['class']); + /** @psalm-suppress UndefinedPropertyAssignment */ $event->getSubject()->element = ltrim($event->getSubject()->type); } @@ -161,14 +172,14 @@ protected function _getPageTitle(): string } $primaryKeyValue = $this->_primaryKeyValue(); - if (empty($primaryKeyValue)) { + if ($primaryKeyValue === null) { return sprintf('%s %s', $actionName, $controllerName); } $displayFieldValue = $this->_displayFieldValue(); if ( $displayFieldValue === null - || $this->_table()->getDisplayField() === $this->_table()->getPrimaryKey() + || $this->_model()->getDisplayField() === $this->_model()->getPrimaryKey() ) { /** @psalm-var string $primaryKeyValue */ return sprintf('%s %s #%s', $actionName, $controllerName, $primaryKeyValue); @@ -199,7 +210,7 @@ protected function _getBreadcrumbs(): array * The user can choose to suppress specific relations using the blacklist * functionality. * - * @param string[] $associationTypes List of association types. + * @param array $associationTypes List of association types. * @return array */ protected function _getRelatedModels(array $associationTypes = []): array @@ -213,12 +224,12 @@ protected function _getRelatedModels(array $associationTypes = []): array if (empty($models)) { $associations = []; if (empty($associationTypes)) { - $associations = $this->_table()->associations(); + $associations = $this->_model()->associations(); } else { foreach ($associationTypes as $assocType) { $associations = array_merge( $associations, - $this->_table()->associations()->getByType($assocType) + $this->_model()->associations()->getByType($assocType) ); } } @@ -229,18 +240,9 @@ protected function _getRelatedModels(array $associationTypes = []): array } } - $models = Hash::normalize($models); - $blacklist = $this->_action()->getConfig('scaffold.relations_blacklist'); if (!empty($blacklist)) { - $blacklist = Hash::normalize($blacklist); - $models = array_diff_key($models, $blacklist); - } - - foreach ($models as $key => $value) { - if (!is_array($value)) { - $models[$key] = []; - } + $models = array_diff($models, $blacklist); } return $models; @@ -263,7 +265,7 @@ protected function _blacklist(): array */ protected function _getPageVariables(): array { - $table = $this->_table(); + $table = $this->_model(); $modelClass = $table->getAlias(); $controller = $this->_controller(); $scope = $this->_action()->getConfig('scope'); @@ -284,7 +286,7 @@ protected function _getPageVariables(): array 'displayField' => $table->getDisplayField(), 'primaryKey' => $table->getPrimaryKey(), ]; - } catch (Exception $e) { + } catch (DatabaseException) { // May be empty if there is no table object for the action } @@ -306,18 +308,18 @@ protected function _getPageVariables(): array protected function _scaffoldFields(array $associations = []): array { $action = $this->_action(); - $scaffoldFields = (array)$action->getConfig('scaffold.fields'); - if (!empty($scaffoldFields)) { - $scaffoldFields = Hash::normalize($scaffoldFields); - } + $scaffoldFields = Hash::normalize( + (array)$action->getConfig('scaffold.fields'), + default: [] + ); if (empty($scaffoldFields) || $action->getConfig('scaffold.autoFields')) { - $cols = $this->_table()->getSchema()->columns(); - $cols = Hash::normalize($cols); + $cols = $this->_model()->getSchema()->columns(); + $cols = Hash::normalize($cols, default: []); $scope = $action->getConfig('scope'); if ($scope === 'entity' && !empty($associations['manyToMany'])) { - foreach ($associations['manyToMany'] as $alias => $options) { + foreach ($associations['manyToMany'] as $options) { $cols[sprintf('%s._ids', $options['entities'])] = [ 'multiple' => true, ]; @@ -328,24 +330,20 @@ protected function _scaffoldFields(array $associations = []): array } // Check for blacklisted fields - $blacklist = $action->getConfig('scaffold.fields_blacklist'); - if (!empty($blacklist)) { + $blacklist = $this->_blacklist(); + if ($blacklist) { $scaffoldFields = array_diff_key($scaffoldFields, array_combine($blacklist, $blacklist)); } // Make sure all array values are an array foreach ($scaffoldFields as $field => $options) { - if (!is_array($options)) { - $scaffoldFields[$field] = (array)$options; - } - $scaffoldFields[$field] += ['formatter' => null]; } - $fieldSettings = $action->getConfig('scaffold.field_settings'); - if (empty($fieldSettings)) { - $fieldSettings = []; - } + $fieldSettings = Hash::normalize( + (array)$action->getConfig('scaffold.field_settings'), + default: [] + ); $fieldSettings = array_intersect_key($fieldSettings, $scaffoldFields); $scaffoldFields = Hash::merge($scaffoldFields, $fieldSettings); @@ -395,14 +393,11 @@ protected function _getControllerActions(): array $actionBlacklist = []; $groups = $this->_action()->getConfig('scaffold.action_groups') ?: []; foreach ($groups as $group) { - $group = Hash::normalize($group); + $group = Hash::normalize($group, default: []); foreach ($group as $actionName => $config) { if (isset($table[$actionName]) || isset($entity[$actionName])) { continue; } - if ($config === null) { - $config = []; - } [$scope, $actionConfig] = $this->_getControllerActionConfiguration($actionName, $config); $realAction = Hash::get($actionConfig, 'url.action', $actionName); if (!isset(${$scope}[$realAction])) { @@ -497,8 +492,8 @@ protected function _getAllowedActions(): array $extraActions = $this->_action()->getConfig('scaffold.extra_actions') ?: []; $allActions = array_merge( - $this->_normalizeActions($actions), - $this->_normalizeActions($extraActions) + Hash::normalize($actions, default: []), + Hash::normalize($extraActions, default: []) ); $blacklist = (array)$this->_action()->getConfig('scaffold.actions_blacklist'); @@ -515,32 +510,6 @@ protected function _getAllowedActions(): array return array_diff_key($allActions, $blacklist); } - /** - * Convert mixed action configs to unified structure - * - * [ - * 'ACTION_1' => [..config...], - * 'ACTION_2' => [..config...], - * 'ACTION_N' => [..config...] - * ] - * - * @param array $actions Actions - * @return array - */ - protected function _normalizeActions(array $actions): array - { - $normalized = []; - foreach ($actions as $key => $config) { - if (is_array($config)) { - $normalized[$key] = $config; - } else { - $normalized[$config] = []; - } - } - - return $normalized; - } - /** * Returns associations for controllers models. * @@ -549,7 +518,7 @@ protected function _normalizeActions(array $actions): array */ protected function _associations(array $whitelist = []): array { - $table = $this->_table(); + $table = $this->_model(); $associationConfiguration = []; @@ -596,11 +565,11 @@ protected function _associations(array $whitelist = []): array * * If no value can be found, NULL is returned * - * @return array|string + * @return array|string|int|null */ - protected function _primaryKeyValue() + protected function _primaryKeyValue(): array|string|int|null { - $fields = (array)$this->_table()->getPrimaryKey(); + $fields = (array)$this->_model()->getPrimaryKey(); $values = []; foreach ($fields as $field) { $values[] = $this->_deriveFieldFromContext($field); @@ -620,10 +589,10 @@ protected function _primaryKeyValue() * * @return string|int|null */ - protected function _displayFieldValue() + protected function _displayFieldValue(): string|int|null { /** @psalm-suppress PossiblyInvalidArgument */ - return $this->_deriveFieldFromContext($this->_table()->getDisplayField()); + return $this->_deriveFieldFromContext($this->_model()->getDisplayField()); } /** @@ -633,15 +602,15 @@ protected function _displayFieldValue() * @param string $field Name of field. * @return mixed */ - protected function _deriveFieldFromContext(string $field) + protected function _deriveFieldFromContext(string $field): mixed { $controller = $this->_controller(); - $modelClass = $this->_table()->getAlias(); + $modelClass = $this->_model()->getAlias(); $entity = $this->_entity(); $request = $this->_request(); $value = $entity->get($field); - if ($value) { + if ($value !== null) { return $value; } diff --git a/src/Listener/ViewSearchListener.php b/src/Listener/ViewSearchListener.php index 1575e65c..7376dd7e 100644 --- a/src/Listener/ViewSearchListener.php +++ b/src/Listener/ViewSearchListener.php @@ -6,8 +6,13 @@ use Cake\Event\EventInterface; use Cake\Routing\Router; use Cake\Utility\Hash; +use Cake\Utility\Inflector; use Crud\Listener\BaseListener; +use function Cake\I18n\__d; +/** + * @method \Cake\ORM\Table _model() + */ class ViewSearchListener extends BaseListener { /** @@ -25,7 +30,7 @@ class ViewSearchListener extends BaseListener * * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'enabled' => null, 'autocomplete' => true, 'select2' => true, @@ -56,7 +61,7 @@ public function implementedEvents(): array */ public function afterPaginate(EventInterface $event): void { - if (!$this->_table()->behaviors()->has('Search')) { + if (!$this->_model()->behaviors()->has('Search')) { return; } @@ -79,16 +84,17 @@ public function afterPaginate(EventInterface $event): void */ public function fields(): array { - $fields = $this->getConfig('fields') ?: []; + /** @var array $fields */ + $fields = $this->getConfig('fields', []); $config = $this->getConfig(); - $schema = $this->_table()->getSchema(); + $schema = $this->_model()->getSchema(); $request = $this->_request(); if ($fields) { - $fields = Hash::normalize($fields); + $fields = Hash::normalize($fields, default: []); } else { - $filters = $this->_table()->searchManager()->getFilters($config['collection']); + $filters = $this->_model()->searchManager()->getFilters($config['collection']); foreach ($filters as $filter) { $opts = $filter->getConfig('form'); @@ -106,26 +112,22 @@ public function fields(): array 'type' => 'text', ]; - if (substr($field, -3) === '_id' && $field !== '_id') { + if (str_ends_with($field, '_id') && $field !== '_id') { $input['type'] = 'select'; } - $input = (array)$opts + $input; + $input = $opts + $input; $input['value'] = $request->getQuery($field); - if (empty($input['options']) && $schema->getColumnType($field) === 'boolean') { - $input['options'] = [__d('crud', 'No'), __d('crud', 'Yes')]; + if (!isset($input['options']) && $schema->getColumnType($field) === 'boolean') { + $input['options'] = [1 => __d('crud', 'Yes'), 0 => __d('crud', 'No')]; $input['type'] = 'select'; } - /** @psalm-suppress PossiblyUndefinedArrayOffset */ - if ($input['type'] === 'select') { - $input += ['empty' => true]; - } + if (isset($input['options'])) { + $input['empty'] ??= $this->getPlaceholder($field); - if (!empty($input['options'])) { - $input['empty'] = true; if (empty($input['class']) && !$config['select2']) { $input['class'] = 'no-select2'; } @@ -135,7 +137,7 @@ public function fields(): array continue; } - if (empty($input['class']) && $config['autocomplete']) { + if ($input['type'] === 'select' && empty($input['class']) && $config['autocomplete']) { $input['class'] = 'autocomplete'; } @@ -151,6 +153,7 @@ public function fields(): array $input['options'][$input['value']] = $input['value']; } + /** @psalm-suppress PossiblyInvalidOperand */ $input += [ 'data-input-type' => 'text', 'data-tags' => 'true', @@ -159,7 +162,18 @@ public function fields(): array ]; } - if (!isset($input['data-url'])) { + if ($input['type'] === 'text') { + $input['placeholder'] ??= $this->getPlaceholder($field); + } + if ($input['type'] === 'select') { + $input['empty'] ??= $this->getPlaceholder($field); + } + + if ( + !empty($input['class']) + && strpos($input['class'], 'autocomplete') !== false + && !isset($input['data-url']) + ) { $urlArgs = []; $fieldKeys = $input['fields'] ?? ['id' => $field, 'value' => $field]; @@ -179,4 +193,23 @@ public function fields(): array return $fields; } + + /** + * Get placeholder text for a field. + * + * @param string $field Field name. + * @return string + */ + protected function getPlaceholder(string $field): string + { + if (str_contains($field, '.')) { + [, $field] = explode('.', $field); + } + + if (str_ends_with($field, '_id') && $field !== '_id') { + $field = substr($field, 0, -3); + } + + return Inflector::humanize($field); + } } diff --git a/src/Menu/MenuDropdown.php b/src/Menu/MenuDropdown.php index 00d16895..4a190794 100644 --- a/src/Menu/MenuDropdown.php +++ b/src/Menu/MenuDropdown.php @@ -10,14 +10,14 @@ class MenuDropdown * * @var string **/ - protected $title; + protected string $title; /** * Array of MenuDivider|MenuItem entries * * @var array **/ - protected $entries = []; + protected array $entries = []; /** * Contains an HTML link. diff --git a/src/Menu/MenuItem.php b/src/Menu/MenuItem.php index 0746a141..f640a506 100644 --- a/src/Menu/MenuItem.php +++ b/src/Menu/MenuItem.php @@ -10,32 +10,32 @@ class MenuItem * * @var string **/ - protected $title; + protected string $title; /** * Cake-relative URL or array of URL parameters, or * external URL (starts with http://) * - * @var string|array|null + * @var array|string|null */ - protected $url = null; + protected string|array|null $url = null; /** * Array of options and HTML attributes. * * @var array **/ - protected $options = []; + protected array $options = []; /** * Contains an HTML link. * * @param string $title The content to be wrapped by `` tags. - * @param string|array|null $url Cake-relative URL or array of URL parameters, or + * @param array|string|null $url Cake-relative URL or array of URL parameters, or * external URL (starts with http://) * @param array $options Array of options and HTML attributes. */ - public function __construct(string $title, $url = null, array $options = []) + public function __construct(string $title, string|array|null $url = null, array $options = []) { $this->title = $title; $this->url = $url; @@ -55,9 +55,9 @@ public function getTitle(): string /** * Returns the menu item ur * - * @return string|array|null + * @return array|string|null */ - public function getUrl() + public function getUrl(): string|array|null { return $this->url; } diff --git a/src/Plugin.php b/src/Plugin.php deleted file mode 100644 index 679fa93c..00000000 --- a/src/Plugin.php +++ /dev/null @@ -1,19 +0,0 @@ -getSchemaCollection(); $tables = $schema->listTables(); - ksort($tables); + sort($tables); + /** @psalm-suppress RiskyTruthyFalsyComparison */ if (!empty($blacklist)) { $tables = array_diff($tables, $blacklist); } @@ -48,6 +50,6 @@ public function display(?array $tables = null, ?array $blacklist = null) $normal[$table] = $config; } - return $this->set('tables', $normal); + $this->set('tables', $normal); } } diff --git a/src/View/CrudView.php b/src/View/CrudView.php index f6198dbc..1705060a 100644 --- a/src/View/CrudView.php +++ b/src/View/CrudView.php @@ -24,7 +24,7 @@ class CrudView extends View implements EventListenerInterface * * @var string */ - protected $layout = 'CrudView.default'; + protected string $layout = 'CrudView.default'; /** * Initialization hook method. @@ -63,7 +63,7 @@ public function implementedEvents(): array * * @return void */ - public function beforeLayout() + public function beforeLayout(): void { $this->_loadAssets(); } diff --git a/src/View/Helper/CrudViewHelper.php b/src/View/Helper/CrudViewHelper.php index 3dafdc52..6a9bd3bd 100644 --- a/src/View/Helper/CrudViewHelper.php +++ b/src/View/Helper/CrudViewHelper.php @@ -3,13 +3,23 @@ namespace CrudView\View\Helper; +use BackedEnum; +use Cake\Chronos\ChronosDate; +use Cake\Chronos\ChronosTime; +use Cake\Core\Configure; +use Cake\Core\Exception\CakeException; +use Cake\Database\Type\EnumLabelInterface; use Cake\Datasource\EntityInterface; use Cake\Datasource\SchemaInterface; +use Cake\I18n\Date; +use Cake\I18n\Time; use Cake\Utility\Inflector; use Cake\Utility\Text; +use Cake\View\Form\EntityContext; use Cake\View\Helper; -use Cake\View\Helper\FormHelper; -use DateTimeInterface; +use UnitEnum; +use function Cake\Core\h; +use function Cake\I18n\__d; /** * @property \BootstrapUI\View\Helper\FormHelper $Form @@ -23,24 +33,35 @@ class CrudViewHelper extends Helper * * @var array */ - protected $helpers = ['Form', 'Html', 'Time']; + protected array $helpers = ['Form', 'Html', 'Time']; /** - * Context + * Entity context * - * @var \Cake\Datasource\EntityInterface + * @var \Cake\View\Form\EntityContext */ - protected $_context; + protected EntityContext $_context; /** * Default config. * * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'fieldFormatters' => null, + 'dateTimeFormat' => null, + 'dateFormat' => null, + 'timeFormat' => null, ]; + /** + * @inheritDoc + */ + public function initialize(array $config): void + { + $this->setConfig(Configure::read('CrudView.helperConfig', [])); + } + /** * Set context * @@ -49,15 +70,15 @@ class CrudViewHelper extends Helper */ public function setContext(EntityInterface $record): void { - $this->_context = $record; + $this->_context = new EntityContext(['entity' => $record]); } /** * Get context * - * @return \Cake\Datasource\EntityInterface + * @return \Cake\View\Form\EntityContext */ - public function getContext(): EntityInterface + public function getContext(): EntityContext { return $this->_context; } @@ -68,17 +89,17 @@ public function getContext(): EntityInterface * @param string $field The field to process. * @param \Cake\Datasource\EntityInterface $data The entity data. * @param array $options Processing options. - * @return string|null|array|bool|int + * @return array|string|int|bool|null */ - public function process(string $field, EntityInterface $data, array $options = []) + public function process(string $field, EntityInterface $data, array $options = []): string|array|bool|int|null { $this->setContext($data); - $value = $this->fieldValue($data, $field); + $value = $this->getContext()->val($field, ['schemaDefault' => false]); $options += ['formatter' => null]; if ($options['formatter'] === 'element') { - $context = $this->getContext(); + $context = $this->getContext()->entity(); return $this->_View->element($options['element'], compact('context', 'field', 'value', 'options')); } @@ -91,7 +112,7 @@ public function process(string $field, EntityInterface $data, array $options = [ } if (is_callable($options['formatter'])) { - return $options['formatter']($field, $value, $this->getContext(), $options, $this->getView()); + return $options['formatter']($field, $value, $this->getContext()->entity(), $options, $this->getView()); } $value = $this->introspect($field, $value, $options); @@ -102,14 +123,14 @@ public function process(string $field, EntityInterface $data, array $options = [ /** * Get the current field value * - * @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. + * @param \Cake\Datasource\EntityInterface|null $data The entity data. * @return mixed */ - public function fieldValue(?EntityInterface $data, string $field) + public function fieldValue(string $field, ?EntityInterface $data = null): mixed { - if (empty($data)) { - $data = $this->getContext(); + if ($data === null) { + return $this->getContext()->val($field, ['schemaDefault' => false]); } return $data->get($field); @@ -121,9 +142,9 @@ public function fieldValue(?EntityInterface $data, string $field) * @param string $field Name of field. * @param mixed $value The value that the field should have within related data. * @param array $options Options array. - * @return array|bool|null|int|string + * @return array|string|int|bool|null */ - public function introspect(string $field, $value, array $options = []) + public function introspect(string $field, mixed $value, array $options = []): array|bool|int|string|null { $output = $this->relation($field); if ($output) { @@ -136,7 +157,13 @@ public function introspect(string $field, $value, array $options = []) if (isset($fieldFormatters[$type])) { /** @psalm-suppress PossiblyNullArrayOffset */ if (is_callable($fieldFormatters[$type])) { - return $fieldFormatters[$type]($field, $value, $this->getContext(), $options, $this->getView()); + return $fieldFormatters[$type]( + $field, + $value, + $this->getContext()->entity(), + $options, + $this->getView() + ); } /** @psalm-suppress PossiblyNullArrayOffset */ @@ -147,12 +174,12 @@ public function introspect(string $field, $value, array $options = []) return $this->formatBoolean($field, $value, $options); } - if (in_array($type, ['datetime', 'date', 'timestamp'])) { - return $this->formatDate($field, $value, $options); + if (in_array($type, ['datetime', 'date', 'time', 'timestamp'], true)) { + return $this->formatDateTime($field, $value, $options); } - if ($type === 'time') { - return $this->formatTime($field, $value, $options); + if ($type !== null && str_starts_with($type, 'enum-')) { + return $this->formatEnum($field, $value, $options); } $value = $this->formatString($field, $value); @@ -172,9 +199,7 @@ public function introspect(string $field, $value, array $options = []) */ public function columnType(string $field): ?string { - $schema = $this->schema(); - - return $schema->getColumnType($field); + return $this->getContext()->type($field); } /** @@ -185,7 +210,7 @@ public function columnType(string $field): ?string * @param array $options Options array * @return string */ - public function formatBoolean(string $field, $value, array $options): string + public function formatBoolean(string $field, mixed $value, array $options): string { return (bool)$value ? $this->Html->badge(__d('crud', 'Yes'), ['class' => empty($options['inverted']) ? 'success' : 'danger']) : @@ -200,41 +225,47 @@ public function formatBoolean(string $field, $value, array $options): string * @param array $options Options array. * @return string */ - public function formatDate(string $field, $value, array $options): string + public function formatDateTime(string $field, mixed $value, array $options): string { if ($value === null) { - return $this->Html->badge(__d('crud', 'N/A'), ['class' => 'info']); + return $this->nullValueDisplay(); } - if ( - is_int($value) - || is_string($value) - || $value instanceof DateTimeInterface - ) { - return $this->Time->timeAgoInWords($value, $options); + if ($value instanceof Date) { + return (string)$value->i18nFormat($options['format'] ?? $this->getConfig('dateFormat')); } - return $this->Html->badge(__d('crud', 'N/A'), ['class' => 'info']); + if ($value instanceof Time) { + return (string)$value->i18nFormat($options['format'] ?? $this->getConfig('timeFormat')); + } + + if ($value instanceof ChronosDate || $value instanceof ChronosTime) { + return (string)$value; + } + + return (string)$this->Time->i18nFormat($value, $options['format'] ?? $this->getConfig('dateTimeFormat'), '') + ?: $this->nullValueDisplay(); } /** - * Format a time for display + * Format an enum for display * * @param string $field Name of field. - * @param mixed $value Value of field. - * @param array $options Options array. + * @param \UnitEnum|\BackedEnum|string|int|null $value Value of field. * @return string */ - public function formatTime(string $field, $value, array $options): string + public function formatEnum(string $field, UnitEnum|BackedEnum|string|int|null $value, array $options): string { - $format = $options['format'] ?? 'KK:mm:ss a'; - /** @var string $value */ - $value = $this->Time->format($value, $format, ''); - if ($value === '') { - return $this->Html->badge(__d('crud', 'N/A'), ['class' => 'info']); + if ($value === null) { + return $this->nullValueDisplay(); } - return $value; + if (is_scalar($value)) { + return (string)$value; + } + + return $value instanceof EnumLabelInterface ? + $value->label() : Inflector::humanize(Inflector::underscore($value->name)); } /** @@ -244,7 +275,7 @@ public function formatTime(string $field, $value, array $options): string * @param mixed $value Value of field. * @return string */ - public function formatString(string $field, $value): string + public function formatString(string $field, mixed $value): string { return h(Text::truncate((string)$value, 200)); } @@ -256,25 +287,35 @@ public function formatString(string $field, $value): string * @param array $options Options array. * @return string */ - public function formatDisplayField($value, array $options): string + public function formatDisplayField(string $value, array $options): string { return $this->createViewLink($value, ['escape' => false]); } + /** + * Display for `null` values + * + * @return string + */ + protected function nullValueDisplay(): string + { + return $this->Html->badge(__d('crud', 'N/A'), ['class' => 'info']); + } + /** * Returns a formatted relation output for a given field * * @param string $field Name of field. * @return mixed Array of data to output, false if no match found */ - public function relation(string $field) + public function relation(string $field): mixed { $associations = $this->associations(); if (empty($associations['manyToOne'])) { return false; } - $data = $this->getContext(); + $data = $this->getContext()->entity(); foreach ($associations['manyToOne'] as $alias => $details) { if ($field !== $details['foreignKey']) { @@ -290,7 +331,7 @@ public function relation(string $field) return [ 'alias' => $alias, - 'output' => $this->Html->link($entity->{$details['displayField']}, [ + 'output' => $this->Html->link((string)$entity->{$details['displayField']}, [ 'plugin' => $details['plugin'], 'controller' => $details['controller'], 'action' => 'view', @@ -326,11 +367,16 @@ public function redirectUrl(): ?string return null; } + try { + $this->Form->unlockField('_redirect_url'); + } catch (CakeException) { + // If FormProtectorComponent is not loaded, FormHelper::unlockField() throws an exception + } + return $this->Form->hidden('_redirect_url', [ 'name' => '_redirect_url', 'value' => $redirectUrl, 'id' => null, - 'secure' => FormHelper::SECURE_SKIP, ]); } @@ -368,9 +414,12 @@ public function createRelationLink(string $alias, array $relation, array $option */ public function createViewLink(string $title, array $options = []): string { + $entity = $this->getContext()->entity(); + assert($entity instanceof EntityInterface); + return $this->Html->link( $title, - ['action' => 'view', $this->getContext()->get($this->getViewVar('primaryKey'))], + ['action' => 'view', $entity->get($this->getViewVar('primaryKey'))], $options ); } @@ -421,7 +470,7 @@ public function associations(): array * @param string $key View variable to get. * @return mixed */ - public function getViewVar(string $key) + public function getViewVar(string $key): mixed { return $this->_View->get($key); } diff --git a/src/View/Widget/DateTimeWidget.php b/src/View/Widget/DateTimeWidget.php index 60465071..4846061f 100644 --- a/src/View/Widget/DateTimeWidget.php +++ b/src/View/Widget/DateTimeWidget.php @@ -3,10 +3,11 @@ namespace CrudView\View\Widget; +use BootstrapUI\View\Widget\DateTimeWidget as BUIDateTimeWidget; use Cake\Core\Configure; use Cake\View\Form\ContextInterface; -class DateTimeWidget extends \BootstrapUI\View\Widget\DateTimeWidget +class DateTimeWidget extends BUIDateTimeWidget { // phpcs:disable /** @@ -122,6 +123,7 @@ public function render(array $data, ContextInterface $context): string $this->_templates->add(['datetimePicker' => $this->defaultTemplate]); } + /** @var array $data */ $data = $this->_templates->addClass($data, 'form-control'); $wrap = $datetimePicker['data-wrap'] === 'true'; if ($wrap) { @@ -135,13 +137,10 @@ public function render(array $data, ContextInterface $context): string } } else { $data += $datetimePicker; + /** @var array $data */ $data = $this->_templates->addClass($data, 'flatpickr'); } - /** - * @psalm-suppress PossiblyInvalidArrayOffset - * @psalm-suppress PossiblyInvalidArgument - */ $input = $this->_templates->format('input', [ 'name' => $data['name'], 'type' => 'text', diff --git a/templates/element/action-groups.php b/templates/element/action-groups.php index 6ac81752..443208a8 100644 --- a/templates/element/action-groups.php +++ b/templates/element/action-groups.php @@ -16,7 +16,7 @@ $group) : ?>
    -
  • > - +
  • diff --git a/templates/element/index/bulk_actions/form_end.php b/templates/element/index/bulk_actions/form_end.php index ec8ac0cf..ba849f39 100644 --- a/templates/element/index/bulk_actions/form_end.php +++ b/templates/element/index/bulk_actions/form_end.php @@ -20,6 +20,6 @@ 'select' => '
    ' . $submitButton . '
    ', ], 'type' => 'select', - 'class' => 'no-selectize', + 'class' => 'no-select2', ]); echo $this->Form->end(); diff --git a/templates/element/index/gallery.php b/templates/element/index/gallery.php index e86bf683..5577fc31 100644 --- a/templates/element/index/gallery.php +++ b/templates/element/index/gallery.php @@ -18,7 +18,7 @@
    CrudView->fieldValue($singularVar, $indexTitleField); + $imageAltContent = $this->CrudView->fieldValue($indexTitleField, $singularVar); $titleContent = $this->CrudView->process($indexTitleField, $singularVar, $titleOptions); $bodyContent = $this->CrudView->process($indexBodyField, $singularVar, $bodyOptions); $imageContent = $this->CrudView->process($indexImageField, $singularVar, $imageOptions); diff --git a/templates/element/index/table.php b/templates/element/index/table.php index 2e6cd25c..58876b16 100644 --- a/templates/element/index/table.php +++ b/templates/element/index/table.php @@ -1,3 +1,6 @@ +
    @@ -10,7 +13,7 @@
    Paginator->sort($field, $options['title'] ?? null, $options); } diff --git a/templates/element/menu/dropdown.php b/templates/element/menu/dropdown.php index b406a1d8..1458ab01 100644 --- a/templates/element/menu/dropdown.php +++ b/templates/element/menu/dropdown.php @@ -1,5 +1,5 @@