diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 8ddb7eeff..180629d86 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -440,9 +440,17 @@ public function index() { $searchableAttributes = $table->getSearchableAttributes(); if(!empty($searchableAttributes)) { + // Here we iterate over the attributes and we add a new where clause for each one foreach(array_keys($searchableAttributes) as $attribute) { if(!empty($this->request->getQuery($attribute))) { $query = $table->whereFilter($query, $attribute, $this->request->getQuery($attribute)); + } elseif (!empty($this->request->getQuery($attribute . "_starts_at")) + || !empty($this->request->getQuery($attribute . "_ends_at"))) { + $search_date = []; + // We allow empty for dates since we might refer to infinity (from whenever or to always) + $search_date[] = $this->request->getQuery($attribute . "_starts_at") ?? ""; + $search_date[] = $this->request->getQuery($attribute . "_ends_at") ?? ""; + $query = $table->whereFilter($query, $attribute, $search_date); } } @@ -572,6 +580,14 @@ protected function populateAutoViewVars(object $obj=null) { $this->set($vvar, $query->toArray()); break; + case 'parent': + $modelsName = $this->name; + // $table = the actual table object + $table = $this->$modelsName; + // XXX We assume that all models that load the Tree behavior will + // implement a potentialParents method + $this->set($vvar, $table->potentialParents($this->getCOID())); + break; default: // XXX I18n? and in match? throw new \LogicException('Unknonwn Auto View Var Type {0}', [$avv['type']]); diff --git a/app/src/Lib/Traits/SearchFilterTrait.php b/app/src/Lib/Traits/SearchFilterTrait.php index dd05c5fe3..77b1452c5 100644 --- a/app/src/Lib/Traits/SearchFilterTrait.php +++ b/app/src/Lib/Traits/SearchFilterTrait.php @@ -30,37 +30,11 @@ namespace App\Lib\Traits; use Cake\Utility\Inflector; +use Cake\I18n\FrozenTime; trait SearchFilterTrait { // Array (and configuration) of permitted search filters private $searchFilters = array(); - - /** - * Determine the UI label for the specified attribute. - * - * @since COmanage Registry v5.0.0 - * @param string $attribute Attribute - * @return string Label - * @todo Merge this with _column_key from index.ctp - */ - - public function getLabel(string $attribute): string { - if(isset($this->searchFilters[$attribute]['label']) - && $this->searchFilters[$attribute]['label'] !== null) { - return $this->searchFilters[$attribute]['label']; - } - - // Try to construct a label from the language key. - $l = __d('field', $attribute); - - if($l != $attribute) { - return $l; - } - - // If we make it here, just return a pretty version of the $attribute name - return Inflector::humanize($attribute); - } - /** * Obtain the set of permitted search attributes. * @@ -69,83 +43,83 @@ public function getLabel(string $attribute): string { */ public function getSearchableAttributes(): array { - // Not every configuration element is necessary for the search form, and - // some need to be calculated, so we do that work here. - - $ret = []; - - foreach(array_keys($this->searchFilters) as $attr) { - $ret[ $attr ] = [ - 'label' => $this->getLabel($attr) + foreach ($this->filterMetadataFields() as $column => $type) { + // If the column is an array then we are accessing the Metadata fields. Skip + if(is_array($type)) { + continue; + } + $this->searchFilters[$column] = [ + 'type' => $type, + 'label' => (__d('field', $column) ?? Inflector::humanize($column)) ]; + + // For the date fields we search ranges + if($type === 'timestamp') { + $this->searchFilters[$column]['alias'][] = $column . '_starts_at'; + $this->searchFilters[$column]['alias'][] = $column . '_ends_at'; + } } - - return $ret; - } - - /** - * Add a permitted search filters. - * - * @since COmanage Registry v5.0.0 - * @param string $attribute Attribute that filtering is permitted on (database name) - * @param bool $caseSensitive Whether this attribute is case sensitive - * @param string $label Label for this search field, or null to autocalculate - * @param bool $substring Whether substring searching is permitted for this attribute - */ - - public function setSearchFilter(string $attribute, - bool $caseSensitive=false, - string $label=null, - bool $substring=true): void { - $this->searchFilters[$attribute] = compact('caseSensitive', 'label', 'substring'); + + return $this->searchFilters ?? []; } - + /** * Build a query where() clause for the configured attribute. * - * @since COmanage Registry v5.0.0 - * @param \Cake\ORM\Query $query Cake ORM Query object - * @param string $attribute Attribute to filter on (database name) - * @param string $q Value to filter on + * @param \Cake\ORM\Query $query Cake ORM Query object + * @param string $attribute Attribute to filter on (database name) + * @param string|array $q Value to filter on + * * @return \Cake\ORM\Query Cake ORM Query object + * @since COmanage Registry v5.0.0 */ - public function whereFilter(\Cake\ORM\Query $query, string $attribute, string $q): object { - if(!empty($this->searchFilters[$attribute])) { - $cs = (isset($this->searchFilters[$attribute]['caseSensitive']) - && $this->searchFilters[$attribute]['caseSensitive']); - - $sub = (isset($this->searchFilters[$attribute]['substring']) - && $this->searchFilters[$attribute]['substring']); - - $search = $q; - - if($sub) { - // Substring - // note, for now at least, a user may infix their own % - $search .= "%"; - } - - if($cs) { - // Case sensitive - $query->where([$attribute => $search]); - } else { - // Case insensitive - $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search, $sub) { - $lower = $query->func()->lower([ - // https://book.cakephp.org/3/en/orm/query-builder.html#function-arguments - $attribute => 'identifier' - ]); - if($sub) { - return $exp->like($lower, strtolower($search)); - } else { - return $exp->eq($lower, strtolower($search)); - } + public function whereFilter(\Cake\ORM\Query $query, string $attribute, string|array $q): object { + // not a permitted attribute + if(empty($this->searchFilters[$attribute])) { + return $query; + } + + $search = $q; + $sub = false; + // Primitive types + $search_types = ['integer', 'boolean']; + if( $this->searchFilters[$attribute]['type'] == "string") { + $search = "%" . $search . "%"; + $sub = true; + } elseif(in_array($this->searchFilters[$attribute]['type'], $search_types, true)) { + return $query->where([$attribute => $search]); + } elseif( $this->searchFilters[$attribute]['type'] == "timestamp") { + // Date between dates + if(!empty($search[0]) + && !empty($search[1])) { + return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) { + return $exp->between($attribute, "'" . $search[0] . "'", "'" . $search[1] . "'"); + }); + // The starts at is non empty. So the data should be greater than the starts_at date + } elseif(!empty($search[0]) + && empty($search[1])) { + return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) { + return $exp->gte("'" . FrozenTime::parse($search[0]) . "'", $attribute); }); + // The ends at is non-empty. So the data should be less than the ends at date + } elseif(!empty($search[1]) + && empty($search[0])) { + return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) { + return $exp->lte("'" . FrozenTime::parse($search[1]) . "'", $attribute); + }); + } else { + // We return everything + return $query; } + } - // else not a permitted attribute - - return $query; + + // String values + return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search, $sub) { + $lower = $query->func()->lower([$attribute => 'identifier']); + return ($sub) ? $exp->like($lower, strtolower($search)) + : $exp->eq($lower, strtolower($search)); + }); } } diff --git a/app/src/Lib/Traits/TableMetaTrait.php b/app/src/Lib/Traits/TableMetaTrait.php index f43e3cfce..41e27b584 100644 --- a/app/src/Lib/Traits/TableMetaTrait.php +++ b/app/src/Lib/Traits/TableMetaTrait.php @@ -29,6 +29,8 @@ namespace App\Lib\Traits; +use Cake\Utility\Inflector; + trait TableMetaTrait { // Does this Table represent Registry objects or configuration? private $confTable = false; @@ -54,4 +56,67 @@ public function getIsConfigurationTable() { public function setIsConfigurationTable(bool $confTable) { $this->confTable = $confTable; } + + + /** + * Filter metadata fields. + * + * @since COmanage Registry v5.0.0 + * @return array An array of columns distinguished in metadata and non-metadata + */ + + protected function filterMetadataFields() { + // Get the list of columns + $coltype = $this->getSchema()->typeMap(); + $entity = $this->getEntityClass(); + $entity_namespace = explode('\\', $entity); + $modelName = end($entity_namespace); + + // Get the list of belongs_to associations and construct an exclude array + $assc_keys = []; + foreach ($this->associations() as $assc) { + if($assc->type() === "manyToOne") { + $assc_keys[] = Inflector::underscore(Inflector::classify($assc->getClassName())) . "_id"; + } + } + // Map the model (eg: Person) to the changelog key (person_id) + $mfk = Inflector::underscore($modelName) . "_id"; + + + $meta_fields = [ + ...$assc_keys, + $mfk, + 'actor_identifier', + // 'provisioning_target_id', + 'created', // todo: I might need to revisit this. We might want to filter according to date in some occassions. Like petitions + 'deleted', + 'id', + 'modified', + 'revision', + 'lft', // XXX For now i skip lft.rght column for tree structures + 'rght', + // 'parent_id', // todo: We need to filter using the parent_id. This should be an enumerator and should apply for all the models that use TreeBehavior + 'api_key' + // 'source_ad_hoc_attribute_id', + // 'source_address_id', + // 'source_email_address_id', + // 'source_identifier_id', + // 'source_name_id', + // 'source_external_identity_id', + // 'source_telephone_number_id', + ]; + + $newa = array(); + foreach($coltype as $clmn => $type) { + if(in_array($clmn, $meta_fields,true)) { + // Move the value to metadata + $newa['meta'][$clmn] = $type; + } else { + // Just copy the value + $newa[$clmn] = $type; + } + } + + return $newa ?? []; + } } diff --git a/app/src/Model/Table/AdHocAttributesTable.php b/app/src/Model/Table/AdHocAttributesTable.php index 0a3ef2dc6..1b23b8666 100644 --- a/app/src/Model/Table/AdHocAttributesTable.php +++ b/app/src/Model/Table/AdHocAttributesTable.php @@ -39,6 +39,7 @@ class AdHocAttributesTable extends Table { use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; /** * Perform Cake Model initialization. diff --git a/app/src/Model/Table/AddressesTable.php b/app/src/Model/Table/AddressesTable.php index d39bff85b..935d57221 100644 --- a/app/src/Model/Table/AddressesTable.php +++ b/app/src/Model/Table/AddressesTable.php @@ -43,6 +43,7 @@ class AddressesTable extends Table { use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\TypeTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; // Default "out of the box" types for this model. Entries here should be // given a default localization in app/resources/locales/*/defaultType.po diff --git a/app/src/Model/Table/ApiUsersTable.php b/app/src/Model/Table/ApiUsersTable.php index 2b2eca51f..c235f472c 100644 --- a/app/src/Model/Table/ApiUsersTable.php +++ b/app/src/Model/Table/ApiUsersTable.php @@ -47,7 +47,8 @@ class ApiUsersTable extends Table { use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + use \App\Lib\Traits\SearchFilterTrait; + /** * Perform Cake Model initialization. * diff --git a/app/src/Model/Table/CousTable.php b/app/src/Model/Table/CousTable.php index 082ad40d6..7a641f17d 100644 --- a/app/src/Model/Table/CousTable.php +++ b/app/src/Model/Table/CousTable.php @@ -72,11 +72,6 @@ public function initialize(array $config): void { $this->setPrimaryLink('co_id'); $this->setRequiresCO(true); - - // Set up the fields that may be filtered in the index view - $this->setSearchFilter('name', false, null, true); - $this->setSearchFilter('parent_id', true, null, false); - $this->setSearchFilter('description', false, null, true); $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) @@ -91,6 +86,12 @@ public function initialize(array $config): void { 'index' => ['platformAdmin', 'coAdmin'] ] ]); + + $this->setAutoViewVars([ + 'parent_ids' => [ + 'type' => 'parent' + ] + ]); } /** @@ -122,6 +123,7 @@ public function buildTableRules(RulesChecker $rules): RulesChecker { * @param int $id COU ID to determine potential parents of, or null for any (or a new) COU * @param bool $hierarchy Render the hierarchy in the name * @return Array Array of COU IDs and COU Names + * @todo Make a TreeTrait and move the function there */ public function potentialParents(int $coId, int $id=null, bool $hierarchy=false) { diff --git a/app/src/Model/Table/EmailAddressesTable.php b/app/src/Model/Table/EmailAddressesTable.php index f1cccae3a..7cdeda814 100644 --- a/app/src/Model/Table/EmailAddressesTable.php +++ b/app/src/Model/Table/EmailAddressesTable.php @@ -41,6 +41,7 @@ class EmailAddressesTable extends Table { use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\TypeTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; // Default "out of the box" types for this model. Entries here should be // given a default localization in app/resources/locales/*/defaultType.po diff --git a/app/src/Model/Table/ExternalIdentitiesTable.php b/app/src/Model/Table/ExternalIdentitiesTable.php index 4111abe6b..604e4888b 100644 --- a/app/src/Model/Table/ExternalIdentitiesTable.php +++ b/app/src/Model/Table/ExternalIdentitiesTable.php @@ -44,6 +44,7 @@ class ExternalIdentitiesTable extends Table { use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; /** * Perform Cake Model initialization. diff --git a/app/src/Model/Table/ExternalIdentityRolesTable.php b/app/src/Model/Table/ExternalIdentityRolesTable.php index 16d200bcf..987648b41 100644 --- a/app/src/Model/Table/ExternalIdentityRolesTable.php +++ b/app/src/Model/Table/ExternalIdentityRolesTable.php @@ -44,6 +44,7 @@ class ExternalIdentityRolesTable extends Table { use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; /** * Perform Cake Model initialization. diff --git a/app/src/Model/Table/HistoryRecordsTable.php b/app/src/Model/Table/HistoryRecordsTable.php index fb08bef6c..573c56843 100644 --- a/app/src/Model/Table/HistoryRecordsTable.php +++ b/app/src/Model/Table/HistoryRecordsTable.php @@ -39,6 +39,7 @@ class HistoryRecordsTable extends Table { use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; /** * Perform Cake Model initialization. diff --git a/app/src/Model/Table/IdentifiersTable.php b/app/src/Model/Table/IdentifiersTable.php index 867202eb7..da5c4b62c 100644 --- a/app/src/Model/Table/IdentifiersTable.php +++ b/app/src/Model/Table/IdentifiersTable.php @@ -42,6 +42,7 @@ class IdentifiersTable extends Table { use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\TypeTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; // Default "out of the box" types for this model. Entries here should be // given a default localization in app/resources/locales/*/defaultType.po diff --git a/app/src/Model/Table/NamesTable.php b/app/src/Model/Table/NamesTable.php index 2f36199fb..26a058a79 100644 --- a/app/src/Model/Table/NamesTable.php +++ b/app/src/Model/Table/NamesTable.php @@ -46,7 +46,8 @@ class NamesTable extends Table { use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\TypeTrait; use \App\Lib\Traits\ValidationTrait; - + use \App\Lib\Traits\SearchFilterTrait; + // Default "out of the box" types for this model. Entries here should be // given a default localization in app/resources/locales/*/defaultType.po protected $defaultTypes = [ diff --git a/app/src/Model/Table/PeopleTable.php b/app/src/Model/Table/PeopleTable.php index 572384117..5a4aee63b 100644 --- a/app/src/Model/Table/PeopleTable.php +++ b/app/src/Model/Table/PeopleTable.php @@ -44,7 +44,8 @@ class PeopleTable extends Table { use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + use \App\Lib\Traits\SearchFilterTrait; + /** * Perform Cake Model initialization. * diff --git a/app/src/Model/Table/PersonRolesTable.php b/app/src/Model/Table/PersonRolesTable.php index 7519ec709..c22e76ca6 100644 --- a/app/src/Model/Table/PersonRolesTable.php +++ b/app/src/Model/Table/PersonRolesTable.php @@ -44,6 +44,7 @@ class PersonRolesTable extends Table { use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; /** * Perform Cake Model initialization. diff --git a/app/src/Model/Table/TelephoneNumbersTable.php b/app/src/Model/Table/TelephoneNumbersTable.php index 400e46e14..c61680ebe 100644 --- a/app/src/Model/Table/TelephoneNumbersTable.php +++ b/app/src/Model/Table/TelephoneNumbersTable.php @@ -42,6 +42,7 @@ class TelephoneNumbersTable extends Table { use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\TypeTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; // Default "out of the box" types for this model. Entries here should be // given a default localization in app/resources/locales/*/defaultType.po diff --git a/app/src/Model/Table/TypesTable.php b/app/src/Model/Table/TypesTable.php index 856662495..a43558dd5 100644 --- a/app/src/Model/Table/TypesTable.php +++ b/app/src/Model/Table/TypesTable.php @@ -113,11 +113,6 @@ public function initialize(array $config): void { 'class' => 'SuspendableStatusEnum' ] ]); - - // Set up the fields that may be filtered in the index view - $this->setSearchFilter('display_name', false, null, true); - $this->setSearchFilter('attribute', true, null, false); - $this->setSearchFilter('statuses', false, null, true); $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) diff --git a/app/src/Model/Table/UrlsTable.php b/app/src/Model/Table/UrlsTable.php index 94100fd5d..d6f1e3a21 100644 --- a/app/src/Model/Table/UrlsTable.php +++ b/app/src/Model/Table/UrlsTable.php @@ -41,6 +41,7 @@ class UrlsTable extends Table { use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\TypeTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\SearchFilterTrait; // Default "out of the box" types for this model. Entries here should be // given a default localization in app/resources/locales/*/defaultType.po diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 0da6e5aa5..8ed937619 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -47,7 +47,7 @@ class FieldHelper extends Helper { // The current entity, if edit or view protected $entity = null; - + /** * Emit an informational banner. * @@ -100,85 +100,30 @@ public function control(string $fieldName, if($fieldName == 'valid_from' || $fieldName == 'valid_through') { // Append the timezone to the label $label = __d('field', $fieldName.".tz", [$this->_View->get('vv_tz')]); - + // A datetime field will be rendered as plain text input with adjacent date and time pickers // that will interact with the field value. Allowing direct access to the input field is for // accessibility purposes. $coptions['class'] = 'form-control datepicker'; $coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS'; // TODO: test for date-only inputs and send only the date - $coptions['id'] = $fieldName; + $coptions['id'] = str_replace("_", "-", $fieldName); - $entity = $this->_View->get('vv_obj'); + $entity = $this->getView()->get('vv_obj'); $pickerDate = ''; if(!empty($entity->$fieldName)) { // Adjust the time back to the user's timezone - $coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->_View->get('vv_tz')); - $pickerDate = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->_View->get('vv_tz')); + $coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->getView()->get('vv_tz')); + $pickerDate = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz')); } + $date_args = [ + 'fieldName' => $fieldName, + 'pickerDate' => $pickerDate + ]; // Create a text field to hold our value. - $controlCode = $this->Form->text($fieldName, $coptions); - - // Create a date/time picker. The yyyy-MM-dd format is set above in $pickerDate. - $pickerId = 'datepicker-' . $fieldName; - $pickerTarget = $fieldName; - $pickerTimed = true; // TODO: set false if date-only - $pickerAmPm = false; // TODO: allow change between AM/PM and 24-hour mode - - $controlCode .= ' - -
- - -
'; + $controlCode = $this->Form->text($fieldName, $coptions) + . $this->getView()->element('datePicker', $date_args); $liClass = "fields-datepicker"; } else { diff --git a/app/templates/ApiUsers/columns.inc b/app/templates/ApiUsers/columns.inc index 29d9aef96..87c426e6b 100644 --- a/app/templates/ApiUsers/columns.inc +++ b/app/templates/ApiUsers/columns.inc @@ -32,6 +32,9 @@ if($vv_cur_co->id == 1) { ]; } +// Turn off the search/filter box for this index view +//$disableFiltering = true; + $indexColumns = [ 'username' => [ 'type' => 'link', diff --git a/app/templates/Cous/columns.inc b/app/templates/Cous/columns.inc index d2b50c09b..d50fd5ed2 100644 --- a/app/templates/Cous/columns.inc +++ b/app/templates/Cous/columns.inc @@ -25,8 +25,8 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -// Turn on the search/filter box for this index view -$enableFiltering = true; +// Turn off the search/filter box for this index view +//$disableFiltering = true; $indexColumns = [ 'name' => [ diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index d602c32e1..5ed37a01d 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -45,6 +45,9 @@ // Otherwise, we'll print out a "no records" message. $recordsExist = false; +// By default Index filtering is on and we need to explicitly disable it +$disableFiltering = false; + // Our default link actions, in order of preference, unless the column config overrides it $linkActions = ['edit', 'view']; @@ -157,9 +160,9 @@ function _column_key($modelsName, $c, $tz=null) { - + element('filter'); ?> - +
@@ -214,7 +217,7 @@ function _column_key($modelsName, $c, $tz=null) { break; case 'datetime': // XXX dates can be rendered as eg $entity->created->format(DATE_RFC850); - print $this->Time->nice($entity->$col, $vv_tz) . $suffix; + print !empty($entity->$col) ? $this->Time->nice($entity->$col, $vv_tz) . $suffix : ""; break; case 'enum': if($entity->$col) { diff --git a/app/templates/Types/columns.inc b/app/templates/Types/columns.inc index 34a9deb23..24b9ee2f1 100644 --- a/app/templates/Types/columns.inc +++ b/app/templates/Types/columns.inc @@ -25,8 +25,8 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -// Turn on the search/filter box for this index view -$enableFiltering = true; +// Turn off the search/filter box for this index view +//$disableFiltering = true; $indexColumns = [ 'display_name' => [ @@ -47,6 +47,7 @@ $indexColumns = [ $topLinks = [ [ 'icon' => 'settings_backup_restore', + 'order' => 'Default', 'label' => __d('operation', 'Types.restore'), 'link' => [ 'action' => 'restore' diff --git a/app/templates/element/datePicker.php b/app/templates/element/datePicker.php new file mode 100644 index 000000000..077242672 --- /dev/null +++ b/app/templates/element/datePicker.php @@ -0,0 +1,70 @@ + + + +
+ + +
diff --git a/app/templates/element/filter.php b/app/templates/element/filter.php index 33c25a691..ff1805d40 100644 --- a/app/templates/element/filter.php +++ b/app/templates/element/filter.php @@ -24,16 +24,48 @@ * @since COmanage Registry v5.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ +use Cake\Collection\Collection; +use Cake\Utility\Inflector; + // $this->name = Models $modelsName = $this->name; // $modelName = Model -$modelName = \Cake\Utility\Inflector::singularize($modelsName); +$modelName = Inflector::singularize($modelsName); // Get the query string and separate the search params from the non-search params $query = $this->request->getQueryParams(); -$non_search_params = array_diff_key($query, $vv_searchable_attributes); -$search_params = array_intersect_key($query, $vv_searchable_attributes); +// Search attributes collection +$search_attributes_collection = new Collection($vv_searchable_attributes); +$alias_params = $search_attributes_collection->filter(fn ($val, $attr) => (is_array($val) && array_key_exists('alias', $val)) ) + ->extract('alias') + ->unfold() + ->toArray(); +// For the non search params we need to search the alias params as well +$searchable_parameters = [ + ...array_keys($vv_searchable_attributes), + ...$alias_params + ]; +$non_search_params = (new Collection($query))->filter( fn($value, $key) => !in_array($key, $searchable_parameters) ) + ->toArray(); + +// Filter the search params and take params with aliases into consideration +$search_params = []; +foreach ($vv_searchable_attributes as $attr => $value) { + if(isset($query[$attr])) { + $search_params[$attr] = $query[$attr]; + continue; + } + + if(isset($value['alias']) + && is_array($value['alias'])) { + foreach ($value['alias'] as $alias_key) { + if(isset($query[$alias_key])) { + $search_params[$attr][$alias_key] = $query[$alias_key]; + } + } + } +} // Begin the form print $this->Form->create(null, [ @@ -60,7 +92,6 @@ search - @@ -68,17 +99,28 @@ - @@ -92,11 +134,19 @@
-
+
$options) { + if($options['type'] == 'boolean') { + $field_booleans_columns[$key] = $options; + continue; + } elseif ($options['type'] == 'timestamp') { + $field_datetime_columns[$key] = $options; + continue; + } $formParams = [ 'label' => $options['label'], // The default type is text, but we might convert to select below @@ -105,11 +155,14 @@ 'required' => false, ]; - if(isset($$key)) { + // The populated variables are in plural while the column names are singular + // Convention: It is a prerequisite that the vvar should be the plural of the column name + $populated_vvar = Inflector::pluralize($key); + if(isset($$populated_vvar)) { // If we have an AutoViewVar matching the name of this key, // convert to a select $formParams['type'] = 'select'; - $formParams['options'] = $$key; + $formParams['options'] = $$populated_vvar; // Allow empty so a filter doesn't require (eg) SOR $formParams['empty'] = true; } @@ -118,6 +171,105 @@ } ?>
+ +
+
On-Off
+
+ $options): ?> +
+ Form->label($key); + print $this->Form->checkbox($key, [ + 'id' => str_replace("_", "-", $key), + 'class' => 'form-check-input', + 'checked' => $query[$key] ?? 0, + 'hiddenField' => false, + 'required' => false + ]); + ?> +
+ +
+
+ + + $options): ?> +
+
+
+ +
+
+ i18nFormat("yyyy-MM-dd HH:mm:ss", $this->get('vv_tz')); + $pickerDate = $starts_date->i18nFormat("yyyy-MM-dd", $this->get('vv_tz')); + } + + $date_args = [ + 'fieldName' => $starts_field, + 'pickerDate' => $pickerDate + ]; + // Create a text field to hold our value. + print $this->Form->label($starts_field, 'Starts at:', ['class' => 'filter-datepicker-lbl']); + print $this->Form->text($starts_field, $coptions) . $this->element('datePicker', $date_args); + ?> +
+
+ +
+
+ i18nFormat("yyyy-MM-dd HH:mm:ss", $this->get('vv_tz')); + $pickerDate = $ends_date->i18nFormat("yyyy-MM-dd", $this->get('vv_tz')); + } + + $date_args = [ + 'fieldName' => $ends_field, + 'pickerDate' => $pickerDate + ]; + // Create a text field to hold our value. + print $this->Form->label($ends_field, 'Ends at:', ['class' => 'filter-datepicker-lbl']); + print $this->Form->text($ends_field, $coptions) . $this->element('datePicker', $date_args); + ?> +
+
+
+
+ + +
> { + // CAKEPHP transforms snake case variables to kebab when the name has the format of a foreign key. + // As a result searching for the initial key will fail. This is used for use cases + // like the models that use the Tree behavior and have the column parent_id + let ident_to_snake = ident.replace(/_/g, "-"); + let filterId = '#' + ident_to_snake; + $(filterId).val(""); + }); + + // Remove the Vue date fields if exist + let dateWidgetInputs = document.querySelectorAll('duet-date-picker input'); + // Remove all the Vue related fields + Array.prototype.slice.call(dateWidgetInputs).forEach( (el) => { + el.parentNode.removeChild(el); + }); + $(this).closest('form').submit(); }); @@ -147,6 +161,14 @@ // Make all select form controls Bootstrappy $("select").addClass("form-select"); + // Use select2 library everywhere except + // - duet-date + $("select").not(".duet-date__select--month").not(".duet-date__select--year").select2({ + width: '100%', + tags: true, + placeholder: "-- Select --" + }); + // Enable Bootstrap Popovers. Unless needed elsewhere, constrain this to #content // XXX Enable when/if needed // $('#content [data-bs-toggle="popover"]').popover(); diff --git a/app/templates/layout/default.php b/app/templates/layout/default.php index 3552c8c0b..c33c83c12 100644 --- a/app/templates/layout/default.php +++ b/app/templates/layout/default.php @@ -40,7 +40,7 @@ - Html->meta('viewport', 'width=device-width, initial-scale=1.0') . "\n"; ?> + Html->meta('viewport', 'width=device-width, initial-scale=1.0') . PHP_EOL ?> Html->charset(); ?> <?= (!empty($vv_title) ? $vv_title : __('registry.meta.registry')); ?> @@ -52,22 +52,24 @@ ?> --> - Html->meta('favicon.ico', '/favicon.ico', array('type' => 'icon')) . "\n"; ?> + Html->meta('favicon.ico', '/favicon.ico', array('type' => 'icon')) . PHP_EOL ?> Html->css([ 'fonts/Font-Awesome-4.6.3/css/font-awesome.min', 'bootstrap/bootstrap.min', + 'select2/select2.min', 'co-color', 'co-base', 'co-responsive' - ]) . "\n"; ?> + ]) . PHP_EOL ?> Html->script([ 'bootstrap/bootstrap.bundle.min.js', - 'jquery/jquery.min.js' - ]) . "\n"; ?> + 'jquery/jquery.min.js', + 'select2/select2.min.js' + ]) . PHP_EOL ?> fetch('meta') ?> @@ -102,7 +104,7 @@
menu
@@ -120,7 +122,7 @@ ['escape' => false]); ?> - Html->link(__('registry.meta.registry'), '/'); ?> + Html->link(__('registry.meta.registry'), '/') ?>
@@ -144,7 +146,7 @@ @@ -156,7 +158,7 @@ @@ -164,14 +166,14 @@ - fetch('content'); ?> + fetch('content') ?>
@@ -188,10 +190,10 @@ class="toast-container" - element('dialog'); ?> + element('dialog') ?> - Html->script('jstimezonedetect/jstz.min.js'); ?> + Html->script('jstimezonedetect/jstz.min.js') ?>