Skip to content

Commit

Permalink
Add filter selection UI to the top-filters on an index view (CFM-296) (
Browse files Browse the repository at this point in the history
…#131)

* Add filter selection UI to the top-filters on an index view (CFM-296) (#130)

* Join Model associations when using filtering (#132)

* Return distinct records

* add field ordering. Fix labeling lexicals.

* Small improvements to the filter rebalance styling when boolean checkboxes are available (CFM-296)

* Further correction to merge conflict on PeopleTable (CFM-296)
  • Loading branch information
Ioannis authored Oct 11, 2023
1 parent 23f9182 commit 67f9fed
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 56 deletions.
6 changes: 6 additions & 0 deletions app/resources/locales/en_US/menu.po
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
msgid "artifacts"
msgstr "Available {0} Artifacts"

msgid "available.filters"
msgstr "Available Filters"

msgid "co.artifacts"
msgstr "Artifacts"

Expand Down Expand Up @@ -201,6 +204,9 @@ msgstr "User Menu"
msgid "menu.toggle"
msgstr "Toggle menu collapse button"

msgid "options"
msgstr "Options"

msgid "registries"
msgstr "Available {0} Registries"

Expand Down
4 changes: 3 additions & 1 deletion app/src/Controller/PeopleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
// XXX not doing anything with Log yet
use Cake\Log\Log;
use Cake\ORM\TableRegistry;
use http\QueryString;

class PeopleController extends StandardController {
public $paginate = [
Expand All @@ -49,7 +50,8 @@ class PeopleController extends StandardController {
'sortableFields' => [
'PrimaryName.given',
'PrimaryName.family'
]
],
'finder' => 'indexed'
];

/**
Expand Down
10 changes: 7 additions & 3 deletions app/src/Controller/StandardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -546,9 +546,13 @@ public function index() {
if(!empty($link->attr)) {
// If a link attribute is defined but no value is provided, then query
// where the link attribute is NULL
$query = $table->find()->where([$table->getAlias().'.'.$link->attr => $link->value]);
// "all" is the default finder. But since we are utilizing the paginator here, we will check the configuration
// for any custom finder.
$query = $table->find(
$this->paginate['finder'] ?? "all"
)->where([$table->getAlias().'.'.$link->attr => $link->value]);
} else {
$query = $table->find();
$query = $table->find($this->paginate['finder'] ?? "all");
}

// QueryModificationTrait
Expand All @@ -563,7 +567,7 @@ public function index() {

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) {
foreach($searchableAttributes as $attribute => $options) {
if(!empty($this->request->getQuery($attribute))) {
$query = $table->whereFilter($query, $attribute, $this->request->getQuery($attribute));
} elseif (!empty($this->request->getQuery($attribute . "_starts_at"))
Expand Down
100 changes: 89 additions & 11 deletions app/src/Lib/Traits/SearchFilterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@
trait SearchFilterTrait {
// Array (and configuration) of permitted search filters
private $searchFilters = array();
// Optional filter configuration that dictates display state and allows for related models
private $filterConfig = array();

/**
* Get explicilty defined filter configuration defined in the table class.
*
* @since COmanage Registry v5.0.0
*/

public function getFilterConfig(): array {
return $this->filterConfig;
}

/**
* Obtain the set of permitted search attributes.
*
Expand All @@ -44,14 +57,45 @@ trait SearchFilterTrait {

public function getSearchableAttributes(string $controller, string $vv_tz=null): array {
$modelname = Inflector::classify(Inflector::underscore($controller));
$filterConfig = $this->getFilterConfig();

// Gather up related models defined in the $filterConfig
// XXX For now, we'll list these first - but we should probably provide a better way to order these.
foreach ($filterConfig as $field => $f) {
if($f['type'] == 'relatedModel') {
$fieldName = Inflector::classify(Inflector::underscore($field));
$this->searchFilters[$field] = [
'type' => 'string', // XXX for now - this needs to be looked up.
'label' => \App\Lib\Util\StringUtilities::columnKey($fieldName, $field, $vv_tz, true),
'active' => isset($f['active']) ? $f['active'] : true,
'model' => $f['model'],
'order' => $f['order']
];
}
}

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;
}

// Set defaults
$fieldIsActive = true;

// Gather filter configurations, if any, for local table fields.
// An active field is visible in the filter form. An inactive field is not but can be enabled.
if(!empty($filterConfig[$column])) {
if(isset($filterConfig[$column]['active'])) {
$fieldIsActive = $filterConfig[$column]['active'];
}
}

$this->searchFilters[$column] = [
'type' => $type,
'label' => \App\Lib\Util\StringUtilities::columnKey($modelname, $column, $vv_tz, true)
'label' => \App\Lib\Util\StringUtilities::columnKey($modelname, $column, $vv_tz, true),
'active' => $fieldIsActive,
'order' => 99 // this is the default
];

// For the date fields we search ranges
Expand All @@ -63,6 +107,16 @@ public function getSearchableAttributes(string $controller, string $vv_tz=null):

return $this->searchFilters ?? [];
}

/**
* Set explicilty defined filter configuration defined in the table class.
*
* @since COmanage Registry v5.0.0
*/

public function setFilterConfig(array $filterConfig): void {
$this->filterConfig = $filterConfig;
}

/**
* Build a query where() clause for the configured attribute.
Expand All @@ -81,36 +135,60 @@ public function whereFilter(\Cake\ORM\Query $query, string $attribute, string|ar
return $query;
}

if(isset($this->searchFilters[$attribute]['model'])) {
$changelog_fk = strtolower(Inflector::underscore($this->searchFilters[$attribute]['model'])) . '_id';
$fk = strtolower(Inflector::underscore(Inflector::singularize($this->_alias))) . '_id';
$mtable_name = Inflector::tableize(Inflector::pluralize($this->searchFilters[$attribute]['model']));
$mtable_alias = Inflector::pluralize($this->searchFilters[$attribute]['model']);
$query->join([$mtable_alias => [
'table' => $mtable_name,
'conditions' => [
$mtable_alias . '.' . $fk . '=' . $this->_alias . '.id',
$mtable_alias . '.' . 'deleted IS NOT TRUE',
$mtable_alias . '.' . $changelog_fk . ' IS NULL'
],
'type' => 'INNER'
]]);
}

// Prepend the Model name to the attribute
$attributeWithModelPrefix = isset($this->searchFilters[$attribute]['model']) ?
Inflector::pluralize($this->searchFilters[$attribute]['model']) . '.' . $attribute :
$this->_alias . '.' . $attribute;

$search = $q;
$sub = false;
// Primitive types
$search_types = ['integer', 'boolean'];
if( $this->searchFilters[$attribute]['type'] == "string") {
$search = "%" . $search . "%";
$sub = true;
// Search type
} elseif(in_array($this->searchFilters[$attribute]['type'], $search_types, true)) {
return $query->where([$attribute => $search]);
return $query->where([$attributeWithModelPrefix => $search]);
// Date
} elseif($this->searchFilters[$attribute]['type'] == "date") {
// Parse the date string with FrozenTime to improve error handling
return $query->where([$attribute => FrozenTime::parseDate($search, 'y-M-d')]);
return $query->where([$attributeWithModelPrefix => FrozenTime::parseDate($search, 'y-M-d')]);
// Timestamp
} 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] . "'");
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attributeWithModelPrefix, $search) {
return $exp->between($attributeWithModelPrefix, "'" . $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);
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attributeWithModelPrefix, $search) {
return $exp->gte("'" . FrozenTime::parse($search[0]) . "'", $attributeWithModelPrefix);
});
// 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);
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attributeWithModelPrefix, $search) {
return $exp->lte("'" . FrozenTime::parse($search[1]) . "'", $attributeWithModelPrefix);
});
} else {
// We return everything
Expand All @@ -120,8 +198,8 @@ public function whereFilter(\Cake\ORM\Query $query, string $attribute, string|ar
}

// 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 $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attributeWithModelPrefix, $search, $sub) {
$lower = $query->func()->lower([$attributeWithModelPrefix => 'identifier']);
return ($sub) ? $exp->like($lower, strtolower($search))
: $exp->eq($lower, strtolower($search));
});
Expand Down
8 changes: 7 additions & 1 deletion app/src/Lib/Util/StringUtilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,16 @@ public static function classNameToForeignKey(string $className): string {

public static function columnKey($modelsName, $c, $tz=null, $useCustomClMdlLabel=false): string {
if(strpos($c, "_id", strlen($c)-3)) {
$postfix = "";
if($c == "parent_id") {
// This means we are working with a model that implements a Tree behavior
$postfix = " ({$modelsName})";
}

// Key is of the form field_id, use .ct label instead
$k = self::foreignKeyToClassName($c);

return __d('controller', $k, [1]);
return __d('controller', $k, [1]) . $postfix;
}

// Look for a model specific key first
Expand Down
68 changes: 62 additions & 6 deletions app/src/Model/Table/PeopleTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,39 @@ public function initialize(array $config): void {
]
]);

// XXX expand/revise this as needed to work best with looking up the related models
$this->setFilterConfig([
'family' => [
'type' => 'relatedModel',
'model' => 'Name',
'active' => true,
'order' => 2
],
'given' => [
'type' => 'relatedModel',
'model' => 'Name',
'active' => true,
'order' => 1
],
'mail' => [
'type' => 'relatedModel',
'model' => 'EmailAddress',
'active' => true,
'order' => 3
],
'identifier' => [
'type' => 'relatedModel',
'model' => 'Identifier',
'active' => true,
'order' => 4
],
'timezone' => [
'type' => 'field',
'active' => false,
'order' => 99
]
]);

$this->setPermissions([
// Actions that operate over an entity (ie: require an $id)
// See also CFM-126
Expand Down Expand Up @@ -245,6 +278,29 @@ public function beforeDelete(\Cake\Event\Event $event, $entity, \ArrayObject $op
return true;
}

/**
* Customized finder for the Index Population View
*
* @param Query $query Cake ORM Query
* @param array $options Cake ORM Query options
*
* @return CakeORMQuery Cake ORM Query
* @since COmanage Registry v5.0.0
*/
public function findIndexed(Query $query, array $options): Query {
return $query->select([
'People.id',
'PrimaryName.given',
'PrimaryName.family',
'People.status',
'People.created',
'People.modified',
'People.timezone',
'People.date_of_birth'
])
->distinct();
}

/**
* Table specific logic to generate a display field.
*
Expand Down Expand Up @@ -429,9 +485,9 @@ public function marshalProvisioningData(int $id): array {

$identifiers = [];

foreach($ret['data']->identifiers as $id) {
if($id->status == SuspendableStatusEnum::Active) {
$identifiers[] = $id;
foreach($ret['data']->identifiers as $ident) {
if($ident->status == SuspendableStatusEnum::Active) {
$identifiers[] = $ident;
}
}

Expand All @@ -454,9 +510,9 @@ public function marshalProvisioningData(int $id): array {

$identifiers = [];

foreach($ret['data']->identifiers as $id) {
if($id->status == SuspendableStatusEnum::Active) {
$identifiers[] = $id;
foreach($ret['data']->identifiers as $ident) {
if($ident->status == SuspendableStatusEnum::Active) {
$identifiers[] = $ident;
}
}

Expand Down
Loading

0 comments on commit 67f9fed

Please sign in to comment.