From b4397578494a7980c4004fdf1b34e60370cc50ee Mon Sep 17 00:00:00 2001 From: Arlen Johnson Date: Tue, 30 Aug 2022 11:54:21 -0400 Subject: [PATCH] Index view improvements (CFM-189) (#44) * Index view improvements (CFM-189): - Move index row actions into an "actions.inc" file; - Move actions into a dropdown menu to the left of the index rows; - Establish a convention for a row-link using the first link or relatedLink available in the indexColumns[]; - Add "bulk" action switch and checkboxes; - Remove Edit, View, and Delete from the default set of index actions: edit and view are available from the row-link, delete is available from entity edit page (or bulk actions). * Fix typo in comment (CFM-189) * Represent read-only items in index with lock icon (CFM-189) * Change read-only rows in index to use "edit_off" icon (CFM-189) --- app/templates/AdHocAttributes/columns.inc | 5 + app/templates/Addresses/columns.inc | 5 + app/templates/ApiUsers/columns.inc | 5 + app/templates/Cos/actions.inc | 39 + app/templates/Cos/columns.inc | 9 +- app/templates/Cous/columns.inc | 5 + app/templates/EmailAddresses/columns.inc | 5 + app/templates/ExternalIdentities/columns.inc | 5 + .../ExternalIdentityRoles/columns.inc | 5 + app/templates/GroupMembers/columns.inc | 5 + app/templates/GroupNestings/columns.inc | 5 + app/templates/GroupOwners/columns.inc | 5 + app/templates/Groups/actions.inc | 73 ++ app/templates/Groups/columns.inc | 37 +- app/templates/HistoryRecords/columns.inc | 2 +- app/templates/Identifiers/columns.inc | 17 +- app/templates/Names/actions.inc | 39 + app/templates/Names/columns.inc | 9 +- app/templates/People/columns.inc | 5 + app/templates/PersonRoles/columns.inc | 13 +- app/templates/Standard/index.php | 803 ++++++++++-------- app/templates/TelephoneNumbers/columns.inc | 5 + app/templates/Types/actions.inc | 43 + app/templates/Types/columns.inc | 13 +- app/templates/Urls/columns.inc | 5 + app/templates/element/javascript.php | 59 +- app/templates/element/menuAction.php | 11 +- app/webroot/css/co-base.css | 149 ++-- app/webroot/css/co-responsive.css | 49 +- 29 files changed, 893 insertions(+), 537 deletions(-) create mode 100644 app/templates/Cos/actions.inc create mode 100644 app/templates/Groups/actions.inc create mode 100644 app/templates/Names/actions.inc create mode 100644 app/templates/Types/actions.inc diff --git a/app/templates/AdHocAttributes/columns.inc b/app/templates/AdHocAttributes/columns.inc index 4fee411ec..335fb303c 100644 --- a/app/templates/AdHocAttributes/columns.inc +++ b/app/templates/AdHocAttributes/columns.inc @@ -33,3 +33,8 @@ $indexColumns = [ 'type' => 'echo' ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; \ No newline at end of file diff --git a/app/templates/Addresses/columns.inc b/app/templates/Addresses/columns.inc index 21eee18bf..50ea05056 100644 --- a/app/templates/Addresses/columns.inc +++ b/app/templates/Addresses/columns.inc @@ -37,3 +37,8 @@ $indexColumns = [ 'class' => 'LanguageEnum' ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; diff --git a/app/templates/ApiUsers/columns.inc b/app/templates/ApiUsers/columns.inc index 29d9aef96..a88ee2263 100644 --- a/app/templates/ApiUsers/columns.inc +++ b/app/templates/ApiUsers/columns.inc @@ -59,3 +59,8 @@ if($vv_cur_co->id > 1) { $indexColumns['valid_through'] = [ 'type' => 'datetime' ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; \ No newline at end of file diff --git a/app/templates/Cos/actions.inc b/app/templates/Cos/actions.inc new file mode 100644 index 000000000..364970055 --- /dev/null +++ b/app/templates/Cos/actions.inc @@ -0,0 +1,39 @@ + 'duplicate', + 'class' => '', + 'icon' => 'content_copy' + ] +]; \ No newline at end of file diff --git a/app/templates/Cos/columns.inc b/app/templates/Cos/columns.inc index a7ff2a79e..d51b1a503 100644 --- a/app/templates/Cos/columns.inc +++ b/app/templates/Cos/columns.inc @@ -38,10 +38,7 @@ $indexColumns = [ ] ]; -$indexActions = [ - [ - 'action' => 'duplicate', - 'class' => '', - 'icon' => 'content_copy' - ] +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true ]; diff --git a/app/templates/Cous/columns.inc b/app/templates/Cous/columns.inc index 72f9f5fd3..600ce6deb 100644 --- a/app/templates/Cous/columns.inc +++ b/app/templates/Cous/columns.inc @@ -37,3 +37,8 @@ $indexColumns = [ 'type' => 'echo' ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; \ No newline at end of file diff --git a/app/templates/EmailAddresses/columns.inc b/app/templates/EmailAddresses/columns.inc index 93ca3b009..0af877b80 100644 --- a/app/templates/EmailAddresses/columns.inc +++ b/app/templates/EmailAddresses/columns.inc @@ -33,3 +33,8 @@ $indexColumns = [ 'type' => 'fk' ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; \ No newline at end of file diff --git a/app/templates/ExternalIdentities/columns.inc b/app/templates/ExternalIdentities/columns.inc index 2ad6a7341..d7482c7ee 100644 --- a/app/templates/ExternalIdentities/columns.inc +++ b/app/templates/ExternalIdentities/columns.inc @@ -36,3 +36,8 @@ $indexColumns = [ 'sortable' => 'PrimaryName.family' ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; \ No newline at end of file diff --git a/app/templates/ExternalIdentityRoles/columns.inc b/app/templates/ExternalIdentityRoles/columns.inc index 79ebb1dba..72d853f16 100644 --- a/app/templates/ExternalIdentityRoles/columns.inc +++ b/app/templates/ExternalIdentityRoles/columns.inc @@ -43,3 +43,8 @@ $indexColumns = [ 'sortable' => true ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; \ No newline at end of file diff --git a/app/templates/GroupMembers/columns.inc b/app/templates/GroupMembers/columns.inc index e213ddf9c..b1d33928b 100644 --- a/app/templates/GroupMembers/columns.inc +++ b/app/templates/GroupMembers/columns.inc @@ -55,4 +55,9 @@ $indexColumns = [ 'valid_through' => [ 'type' => 'datetime' ] +]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true ]; \ No newline at end of file diff --git a/app/templates/GroupNestings/columns.inc b/app/templates/GroupNestings/columns.inc index e1d6e3698..2fa1abbfb 100644 --- a/app/templates/GroupNestings/columns.inc +++ b/app/templates/GroupNestings/columns.inc @@ -32,4 +32,9 @@ $indexColumns = [ 'model' => 'target_group', 'field' => 'name' ] +]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true ]; \ No newline at end of file diff --git a/app/templates/GroupOwners/columns.inc b/app/templates/GroupOwners/columns.inc index fcda854dd..e65333f78 100644 --- a/app/templates/GroupOwners/columns.inc +++ b/app/templates/GroupOwners/columns.inc @@ -34,4 +34,9 @@ $indexColumns = [ // XXX not clear how to sort on submodel, but maybe just leave it for UX revisions? // 'sortable' => 'PrimaryName.family' ] +]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true ]; \ No newline at end of file diff --git a/app/templates/Groups/actions.inc b/app/templates/Groups/actions.inc new file mode 100644 index 000000000..9f200ddbc --- /dev/null +++ b/app/templates/Groups/actions.inc @@ -0,0 +1,73 @@ + 'group_members', + 'action' => 'index', + 'icon' => 'people', + 'type' => 'tab' + ], + [ + 'controller' => 'group_owners', + 'action' => 'index', + 'icon' => 'settings', + 'type' => 'tab' + ], + [ + 'controller' => 'group_nestings', + 'action' => 'index', + 'icon' => 'group_add', + 'type' => 'tab' + ], + [ + 'controller' => 'history_records', + 'action' => 'index', + 'icon' => 'history', + 'type' => 'tab' + ], + [ + 'controller' => 'identifiers', + 'action' => 'index', + 'icon' => 'label', + 'if' => 'notAutomatic', + 'type' => 'tab' + ], + [ + 'action' => 'reconcile', + 'icon' => 'sync', + 'type' => 'top-link' + ] +]; \ No newline at end of file diff --git a/app/templates/Groups/columns.inc b/app/templates/Groups/columns.inc index 087c4106d..cb489e127 100644 --- a/app/templates/Groups/columns.inc +++ b/app/templates/Groups/columns.inc @@ -39,38 +39,7 @@ $indexColumns = [ ] ]; -// XXX This should show on fields.inc and more generally perhaps we should merge -// this with the set of buttons available on the add-edit-view page (maybe just -// have that page use $indexActions?) (CO-647) -$indexActions = [ - [ - 'controller' => 'group_members', - 'action' => 'index', - 'icon' => 'people', - ], - [ - 'controller' => 'group_owners', - 'action' => 'index', - 'icon' => 'settings' - ], - [ - 'controller' => 'group_nestings', - 'action' => 'index', - 'icon' => 'group_add' - ], - [ - 'controller' => 'history_records', - 'action' => 'index', - 'icon' => 'history' - ], - [ - 'controller' => 'identifiers', - 'action' => 'index', - 'icon' => 'label', - 'if' => 'notAutomatic' - ], - [ - 'action' => 'reconcile', - 'icon' => 'sync' - ] +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true ]; \ No newline at end of file diff --git a/app/templates/HistoryRecords/columns.inc b/app/templates/HistoryRecords/columns.inc index b25e57c4d..b2ec8a4de 100644 --- a/app/templates/HistoryRecords/columns.inc +++ b/app/templates/HistoryRecords/columns.inc @@ -27,7 +27,7 @@ $indexColumns = [ 'id' => [ - 'type' => 'echo' + 'type' => 'link' ], 'created' => [ 'type' => 'datetime' diff --git a/app/templates/Identifiers/columns.inc b/app/templates/Identifiers/columns.inc index 7f6b88ecc..1eb1d552e 100644 --- a/app/templates/Identifiers/columns.inc +++ b/app/templates/Identifiers/columns.inc @@ -34,17 +34,22 @@ $indexColumns = [ ] ]; +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; + $indexActions = [ [ 'controller' => 'authentication_events', 'action' => 'index', 'icon' => 'person', 'if' => 'isLogin', - 'query' => function ($e) { - return [ - 'authenticated_identifier' => - \App\Lib\Util\StringUtilities::urlbase64encode($e->identifier) - ]; - } + 'query' => function ($e) { + return [ + 'authenticated_identifier' => + \App\Lib\Util\StringUtilities::urlbase64encode($e->identifier) + ]; + } ] ]; \ No newline at end of file diff --git a/app/templates/Names/actions.inc b/app/templates/Names/actions.inc new file mode 100644 index 000000000..5966a2c86 --- /dev/null +++ b/app/templates/Names/actions.inc @@ -0,0 +1,39 @@ + 'primary', + 'icon' => 'publish', + 'if' => 'notPrimary' + ] +]; \ No newline at end of file diff --git a/app/templates/Names/columns.inc b/app/templates/Names/columns.inc index 32d756092..58ae42d4d 100644 --- a/app/templates/Names/columns.inc +++ b/app/templates/Names/columns.inc @@ -39,10 +39,7 @@ $indexColumns = [ ] ]; -$indexActions = [ - [ - 'action' => 'primary', - 'icon' => 'publish', - 'if' => 'notPrimary' - ] +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true ]; \ No newline at end of file diff --git a/app/templates/People/columns.inc b/app/templates/People/columns.inc index 4b6c5bbaf..641aaa171 100644 --- a/app/templates/People/columns.inc +++ b/app/templates/People/columns.inc @@ -39,3 +39,8 @@ $indexColumns = [ 'sortable' => true ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'expunge' => true +]; \ No newline at end of file diff --git a/app/templates/PersonRoles/columns.inc b/app/templates/PersonRoles/columns.inc index bccd329ad..1637e09e7 100644 --- a/app/templates/PersonRoles/columns.inc +++ b/app/templates/PersonRoles/columns.inc @@ -28,14 +28,14 @@ // Which action to render as the main link for the row $indexColumns = [ - 'cou_id' => [ - 'type' => 'fk', - 'sortable' => true - ], 'title' => [ 'type' => 'link', 'sortable' => true ], + 'cou_id' => [ + 'type' => 'fk', + 'sortable' => true + ], 'affiliation_type_id' => [ 'type' => 'fk', 'label' => __d('field', 'affiliation'), @@ -47,3 +47,8 @@ $indexColumns = [ 'sortable' => true ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; \ No newline at end of file diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index 3f9f3084a..3f8ce602a 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -42,15 +42,18 @@ $tableName = Inflector::tableize(Inflector::singularize($this->name)); $tableFK = Inflector::singularize($tableName) . "_id"; -// Do we have records for this index? This will be set to true during render if we do. -// Otherwise, we'll print out a "no records" message. +// Do we have records for this index? This will be set to true during render of the +// first table data row if we do. Otherwise, we'll print out a "no records" message. $recordsExist = false; // Our default link actions, in order of preference, unless the column config overrides it $linkActions = ['edit', 'view']; -// Read the index configuration ($indexColumns) for this model +// Read the index configuration ($indexColumns) and the associated actions for this model include(ROOT . DS . "templates" . DS . $modelsName . DS . "columns.inc"); +if(file_exists(ROOT . DS . "templates" . DS . $modelsName . DS . "actions.inc")) { + include(ROOT . DS . "templates" . DS . $modelsName . DS . "actions.inc"); +} // $linkFilter is used for models that belong to a specific parent model (eg: co_id) $linkFilter = []; @@ -113,7 +116,7 @@ function _column_key($modelsName, $c, $tz=null) { 'label' => __d('operation', 'add.a', __d('controller', $modelsName, [1])), ]; - foreach(($topLinks ?? []) as $t) { + foreach(($indexTopLinks ?? []) as $t) { if($vv_permissions[ $t['link']['action'] ]) { // We need to inject $linkFilter, but not overwrite any existing query params if(!empty($t['link']['?'])) { @@ -130,14 +133,21 @@ function _column_key($modelsName, $c, $tz=null) { ]; } } - } - - if(!empty($action_args['vv_actions'])) { - print ''; + // Declare the type of actions being sent so we can produce the bulk actions switch only for top-links. + // XXX Bulk actions are currently being provided if a user has "add" permissions. Review this. + $action_args['vv_actions_type'] = 'top-links'; + + if(!empty($bulkActions)) { + $action_args['vv_bulk_actions'] = $bulkActions; + } } ?> + + + + @@ -164,381 +174,476 @@ function _column_key($modelsName, $c, $tz=null) {
- - - $cfg): ?> - > + +
+ + + + + + $cfg): ?> + > + '; + } + + $label = !empty($cfg['label']) ? $cfg['label'] : _column_key($modelsName, $col, $vv_tz); + + if(isset($cfg['sortable']) && $cfg['sortable']) { + if(is_string($cfg['sortable'])) { + print $this->Paginator->sort($cfg['sortable'], $label); + } else { + print $this->Paginator->sort($col, $label); + } + } else { + print $label; + } - if(isset($cfg['sortable']) && $cfg['sortable']) { - if(is_string($cfg['sortable'])) { - print $this->Paginator->sort($cfg['sortable'], $label); + if($firstHeading) { + print ''; + } + ?> + +
+ + +
+ + + + +
+ + + + + + id; + + // Edit / View + /* TODO: Keep as reference for now; ultimately remove. + * if($vv_permission_set[$entity->id]['edit']) { + $action_args['vv_actions'][] = array( + 'order' => $this->Menu->getMenuOrder('Edit'), + 'icon' => $this->Menu->getMenuIcon('Edit'), + 'url' => $this->Url->build(['action' => 'edit', $entity->id]), + 'label' => __d('operation', 'edit') + ); + } elseif($vv_permission_set[$entity->id]['view']) { + $action_args['vv_actions'][] = array( + 'order' => $this->Menu->getMenuOrder('View'), + 'icon' => $this->Menu->getMenuIcon('View'), + 'url' => $this->Url->build(['action' => 'view', $entity->id]), + 'label' => __d('operation', 'view') + ); + }*/ + + // Insert actions as per the .inc file + + // TODO: create an element or move this to MenuHelper so it can be used by topLinks as well as actions + // XXX this isn't quite the right test + // if(isset($entity->status) && $entity->status == StatusEnum::Active) { + $actionOrderDefault = $this->Menu->getMenuOrder('Default'); + foreach($actions as $a) { + $ok = false; + if(!empty($a['controller'])) { + $tableName = Inflector::camelize($a['controller']); + + if(isset($vv_permission_set[$entity->id][$tableName][ $a['action'] ])) { + $ok = $vv_permission_set[$entity->id][$tableName][ $a['action'] ]; + } } else { - print $this->Paginator->sort($col, $label); + $ok = $vv_permission_set[$entity->id][ $a['action'] ]; + } + + if($ok && !empty($a['if'])) { + // If there's a conditional on the field, test the entity + $f = $a['if']; + + $ok = $entity->$f(); + } + + if($ok) { + $actionOrder = !empty($a['order']) ? $a['order'] : $actionOrderDefault++; + $actionIcon = !empty($a['icon']) ? $a['icon'] : $this->Menu->getMenuIcon('Default'); + $actionClass = !empty($a['class']) ? $a['class'] : ''; + $actionUrl = ''; + $actionLabel = ''; + $actionOnClick = []; // used for confirmation dialog + + // Generate the link text and urls: + + // If we have a .confirm text, we need to generate a confirm dialog box + $confirmKey = $a['action'].'.confirm'; + $confirmTxt = __d('operation', $confirmKey); + + if($confirmTxt != $confirmKey) { + // We found the localized string + $actionPostBtnArray = ['action' => $a['action'], $entity->id]; + $actionUrl = $this->Url->build(['action' => $a['action'], $entity->id]); + // XXX should be configurable which field we put in, maybe displayField? + $action_args['vv_actions'][] = array( + 'order' => $actionOrder, + 'icon' => $actionIcon, + 'url' => 'javascript:void(0);', + 'label' => __d('operation', $a['action']), + 'class' => !empty($actionClass) ? $actionClass . ' nospin' : 'nospin', + 'onclick' => array( + 'dg_bd_txt' => __d('operation', $confirmKey, [$entity->id]), // dialog body text + 'dg_post_btn_array' => $actionPostBtnArray, // postButton array for building the postButton + 'dg_url' => $actionUrl, // action url for building a unique ID + 'dg_conf_btn' => __d('operation', 'confirm'), // dialog confirm button text + 'dg_cancel_btn' => __d('operation', 'cancel'), // dialog cancel button text + 'dg_title' => __d('operation', 'confirm'), // dialog box title + 'dg_bd_txt_repl_str' => '' // dialog body text replacement strings + ), + ); + } elseif(!empty($a['controller'])) { + // We're linking into a related controller + $actionLabel = __d('controller', Inflector::camelize(Inflector::pluralize($a['controller'])), [99]); + $actionUrl = $this->Url->build( + ['controller' => $a['controller'], + 'action' => $a['action'], + '?' => [ $tableFK => $entity->id] ] + ); + } else { + $actionLabel = __d('operation', $a['action']); + $actionUrl = $this->Url->build(['action' => $a['action'], $entity->id]); + } + + // If a specific label is sent in the config, use it instead + if(!empty($a['label'])) { + $actionLabel = $a['label']; + } + + // Set the action link configuration + $action_args['vv_actions'][] = array( + 'order' => $actionOrder, + 'icon' => $actionIcon, + 'url' => $actionUrl, + 'label' => $actionLabel, + 'class' => $actionClass, + 'onclick' => $actionOnClick + ); } - } else { - print $label; } - ?> - - - - - - + +// } + + // Delete + /* TODO: Keep as reference for now - ultimately remove. + * if($vv_permission_set[$entity->id]['delete']) { + $actionPostBtnArray = ['action' => 'delete', $entity->id]; + $actionUrl = $this->Url->build(['action' => 'delete', $entity->id]); + $action_args['vv_actions'][] = array( + 'order' => $this->Menu->getMenuOrder('Delete'), + 'icon' => $this->Menu->getMenuIcon('Delete'), + 'url' => 'javascript:void(0);', + 'label' => __d('operation', 'delete'), + 'class' => 'deletebutton nospin', + 'onclick' => array( + 'dg_bd_txt' => __d('operation', 'delete.confirm', [$entity->id]), + 'dg_post_btn_array' => $actionPostBtnArray, + 'dg_url' => $actionUrl, + 'dg_conf_btn' => __d('operation', 'remove'), + 'dg_cancel_btn' => __d('operation', 'cancel'), + 'dg_title' => __d('operation', 'remove'), + 'dg_bd_txt_repl_str' => '' + ), + ); + }*/ + + ?> + + + + + $cfg): ?> - > - > + $f(); - - if(!empty($str)) { - // For our first pass, we insert a comma, but this might not generalize - $suffix = ", " . $str; - } - } - - switch($cfg['type']) { - case 'boolean': - if(!empty($entity->$col) && $entity->$col) { - print __d('enumeration', $cfg['class'].'.1') . $suffix; - } else { - print __d('enumeration', $cfg['class'].'.0') . $suffix; - } - break; - case 'datetime': - // XXX dates can be rendered as eg $entity->created->format(DATE_RFC850); - print !empty($entity->$col) ? $this->Time->nice($entity->$col, $vv_tz) . $suffix : ""; - break; - case 'enum': - if($entity->$col) { - // XXX Need to add badging - see index.php in Match - print __d('enumeration', $cfg['class'].'.'.$entity->$col) . $suffix; + if(!empty($cfg['append'])) { + // The value is a method on the entity that returns a string to + // append to the label + $f = $cfg['append']; + + $str = $entity->$f(); + + if(!empty($str)) { + // For our first pass, we insert a comma, but this might not generalize + $suffix = ", " . $str; } - break; - case 'fk': - // Assuming $col is of the form foo_id, look to see if the corresponding - // AutoViewVar $foos is set, and if so render the lookup value instead - $f = null; - if(preg_match('/^(.*?)_id$/', $col, $f)) { - $avv = Inflector::variable(Inflector::pluralize($f[1])); - - if(!empty(${$avv}[$entity->$col])) { - // We found the viewvar (eg: $foos), and it has a corresponding value - // (eg: $foos[3]), so render it - print ${$avv}[$entity->$col]. $suffix; // XXX filter_var? + } + + switch($cfg['type']) { + case 'boolean': + if(!empty($entity->$col) && $entity->$col) { + print __d('enumeration', $cfg['class'].'.1') . $suffix; + } else { + print __d('enumeration', $cfg['class'].'.0') . $suffix; + } + break; + case 'datetime': + // XXX dates can be rendered as eg $entity->created->format(DATE_RFC850); + print !empty($entity->$col) ? $this->Time->nice($entity->$col, $vv_tz) . $suffix : ""; + break; + case 'enum': + if($entity->$col) { + // XXX Need to add badging - see index.php in Match + print __d('enumeration', $cfg['class'].'.'.$entity->$col) . $suffix; + } + break; + case 'fk': + // Assuming $col is of the form foo_id, look to see if the corresponding + // AutoViewVar $foos is set, and if so render the lookup value instead + $f = null; + if(preg_match('/^(.*?)_id$/', $col, $f)) { + $avv = Inflector::variable(Inflector::pluralize($f[1])); + + if(!empty(${$avv}[$entity->$col])) { + // We found the viewvar (eg: $foos), and it has a corresponding value + // (eg: $foos[3]), so render it + print ${$avv}[$entity->$col]. $suffix; // XXX filter_var? + } else { + // No match, just render the value + print $entity->$col. $suffix; + } } else { - // No match, just render the value + // Just print the value print $entity->$col. $suffix; } - } else { - // Just print the value - print $entity->$col. $suffix; - } - break; - case 'button': - if(!empty($entity->$col)) { - $buttonAttrs = []; - $buttonText = $entity->$col; - if(!empty($cfg['button']['attrs'])) { - $buttonAttrs = $cfg['button']['attrs']; + break; + case 'button': + if(!empty($entity->$col)) { + $buttonAttrs = []; + $buttonText = $entity->$col; + if(!empty($cfg['button']['attrs'])) { + $buttonAttrs = $cfg['button']['attrs']; + } + $buttonAttrs['type'] = 'button'; + if(!empty($cfg['button']['text']) && $cfg['button']['text'] != 'fieldVal') { + $buttonText = $cfg['button']['text']; + } + if(!empty($cfg['truncate']) && is_int($cfg['truncate'])) { + // We check for $truncate + 1 because there's no point trimming + // the last character if we're just going to replace it with ... + $buttonText = (strlen($buttonText) > $cfg['truncate'] + 1) ? substr($buttonText,0,$cfg['truncate']).'...' : $buttonText; + } + if(!empty($cfg['button']['popover'])) { + if($cfg['button']['popover'] == 'fieldVal') { + $buttonAttrs['data-bs-content'] = $entity->$col; + } else { + $buttonAttrs['data-bs-content'] = $cfg['button']['popover']; + } + $label = !empty($cfg['label']) ? $cfg['label'] : _column_key($modelsName, $col, $vv_tz); + $buttonAttrs['title'] = $label; + $buttonAttrs['data-bs-toggle'] = 'popover'; + $buttonAttrs['data-bs-container'] = 'body'; + $buttonAttrs['data-bs-placement'] = 'top'; + $buttonAttrs['data-bs-animation'] = 'false'; + } + print $this->Form->button($buttonText, $buttonAttrs); } - $buttonAttrs['type'] = 'button'; - if(!empty($cfg['button']['text']) && $cfg['button']['text'] != 'fieldVal') { - $buttonText = $cfg['button']['text']; + break; + case 'closure': + $fn = $cfg['function']; + print $fn($entity); + break; + case 'datetime': + // XXX dates can be rendered as eg $entity->created->format(DATE_RFC850); + if(!empty($entity->$col)) { + print $this->Time->nice($entity->$col, $vv_tz) . $suffix; } - if(!empty($cfg['truncate']) && is_int($cfg['truncate'])) { - // We check for $truncate + 1 because there's no point trimming - // the last character if we're just going to replace it with ... - $buttonText = (strlen($buttonText) > $cfg['truncate'] + 1) ? substr($buttonText,0,$cfg['truncate']).'...' : $buttonText; + break; + case 'enum': + if($entity->$col) { + // XXX Need to add badging - see index.php in Match + print __d('enumeration', $cfg['class'].'.'.$entity->$col) . $suffix; } - if(!empty($cfg['button']['popover'])) { - if($cfg['button']['popover'] == 'fieldVal') { - $buttonAttrs['data-bs-content'] = $entity->$col; + break; + case 'fk': + // Assuming $col is of the form foo_id, look to see if the corresponding + // AutoViewVar $foos is set, and if so render the lookup value instead + $f = null; + if(preg_match('/^(.*?)_id$/', $col, $f)) { + $avv = Inflector::variable(Inflector::pluralize($f[1])); + + if(!empty(${$avv}[$entity->$col])) { + // We found the viewvar (eg: $foos), and it has a corresponding value + // (eg: $foos[3]), so render it + print ${$avv}[$entity->$col]. $suffix; // XXX filter_var? } else { - $buttonAttrs['data-bs-content'] = $cfg['button']['popover']; + // No match, just render the value + print $entity->$col. $suffix; } - $label = !empty($cfg['label']) ? $cfg['label'] : _column_key($modelsName, $col, $vv_tz); - $buttonAttrs['title'] = $label; - $buttonAttrs['data-bs-toggle'] = 'popover'; - $buttonAttrs['data-bs-container'] = 'body'; - $buttonAttrs['data-bs-placement'] = 'top'; - $buttonAttrs['data-bs-animation'] = 'false'; - } - print $this->Form->button($buttonText, $buttonAttrs); - } - break; - case 'closure': - $fn = $cfg['function']; - print $fn($entity); - break; - case 'datetime': - // XXX dates can be rendered as eg $entity->created->format(DATE_RFC850); - if(!empty($entity->$col)) { - print $this->Time->nice($entity->$col, $vv_tz) . $suffix; - } - break; - case 'enum': - if($entity->$col) { - // XXX Need to add badging - see index.php in Match - print __d('enumeration', $cfg['class'].'.'.$entity->$col) . $suffix; - } - break; - case 'fk': - // Assuming $col is of the form foo_id, look to see if the corresponding - // AutoViewVar $foos is set, and if so render the lookup value instead - $f = null; - if(preg_match('/^(.*?)_id$/', $col, $f)) { - $avv = Inflector::variable(Inflector::pluralize($f[1])); - - if(!empty(${$avv}[$entity->$col])) { - // We found the viewvar (eg: $foos), and it has a corresponding value - // (eg: $foos[3]), so render it - print ${$avv}[$entity->$col]. $suffix; // XXX filter_var? } else { - // No match, just render the value + // Just print the value print $entity->$col. $suffix; } - } else { - // Just print the value - print $entity->$col. $suffix; - } - break; - case 'link': - case 'relatedLink': - case 'echo': - default: - // By default our label is the column value, but it might be overridden - $label = $entity->$col . $suffix; - - if(!empty($cfg['model']) && !empty($cfg['field'])) { - $m = $cfg['model']; - $f = $cfg['field']; + break; + case 'link': + case 'relatedLink': + case 'echo': + default: + // By default our label is the column value, but it might be overridden + $label = $entity->$col . $suffix; - if(!empty($cfg['submodel'])) { - // We have a related model, eg actor_person.primary_name - $sm = $cfg['submodel']; + if(!empty($cfg['model']) && !empty($cfg['field'])) { + $m = $cfg['model']; + $f = $cfg['field']; - if(!empty($entity->$m->$sm->$f)) { - $label = $entity->$m->$sm->$f . $suffix; - } - } else { - if(!empty($entity->$m->$f)) { - $label = $entity->$m->$f . $suffix; + if(!empty($cfg['submodel'])) { + // We have a related model, eg actor_person.primary_name + $sm = $cfg['submodel']; + + if(!empty($entity->$m->$sm->$f)) { + $label = $entity->$m->$sm->$f . $suffix; + } + } else { + if(!empty($entity->$m->$f)) { + $label = $entity->$m->$f . $suffix; + } } } - } - - $linked = false; - - // $linkActions can be overridden in columns.inc to apply to all - // generated links, or $cfg['action'] can be set to apply only to - // a specific field (column). - $tryActions = (!empty($cfg['action']) ? [ $cfg['action'] ] : $linkActions); - - if($cfg['type'] == 'link') { - foreach($tryActions as $a) { - // Does this user have permission for this action? - if($vv_permission_set[$entity->id][$a]) { - print $this->Html->link($label, ['action' => $a, $entity->id]); - $linked = true; - break 2; - } + + $linked = false; + + // Output the bulk-action checkbox and label + if($isFirstLink) { + print '
'; + print ''; + print ''; + print '
'; } - } elseif($cfg['type'] == 'relatedLink') { - $m = $cfg['model']; - if(!empty($entity->$m->id)) { - // We need the controller for the related entity, however $m - // might be an alias and $entity->getSource() returns the - // aliased class name. So we use PHP's get_class instead. - $c = Inflector::tableize(substr(get_class($entity->$m), strrpos(get_class($entity->$m), '\\')+1)); - + // $linkActions can be overridden in columns.inc to apply to all + // generated links, or $cfg['action'] can be set to apply only to + // a specific field (column). + $tryActions = (!empty($cfg['action']) ? [ $cfg['action'] ] : $linkActions); + + if($cfg['type'] == 'link') { foreach($tryActions as $a) { // Does this user have permission for this action? -// XXX we actually need to know the permissions on the target (ie: actor person) - if(true || - $vv_permission_set[$entity->id][$a]) { - print $this->Html->link($label, ['controller' => $c, 'action' => $a, $entity->$m->id]); + if($vv_permission_set[$entity->id][$a]) { + $args = []; + $readOnlyIcon = ''; + if($isFirstLink) { + $linkClass = 'row-link'; + if($a == 'edit') { + $linkClass .= ' row-link-edit'; + } elseif ($a == 'view') { + $linkClass .= ' row-link-view'; + $readOnlyIcon = ' edit_off'; + } + $args = ['class' => $linkClass]; + $isFirstLink = false; + } + // Output the link + if(!empty($readOnlyIcon)) { + print ''; + } $linked = true; break 2; } } - } - } - - if(!$linked) { - // Just echo the value - print $label; - } - break; - } - ?> - - -
- - - - - - + break; + } + ?> + + + + + + + + +
+
+ + element('menuAction', $action_args); ?> + +
+
- id; - - // Edit - if($vv_permission_set[$entity->id]['edit']) { - $action_args['vv_actions'][] = array( - 'order' => $this->Menu->getMenuOrder('Edit'), - 'icon' => $this->Menu->getMenuIcon('Edit'), - 'url' => $this->Url->build(['action' => 'edit', $entity->id]), - 'label' => __d('operation', 'edit') - ); - } elseif($vv_permission_set[$entity->id]['view']) { - $action_args['vv_actions'][] = array( - 'order' => $this->Menu->getMenuOrder('View'), - 'icon' => $this->Menu->getMenuIcon('View'), - 'url' => $this->Url->build(['action' => 'view', $entity->id]), - 'label' => __d('operation', 'view') - ); - } - - // Insert additional actions as per the .inc file - if(!empty($indexActions)) { -// TODO: create an element or move this to MenuHelper so it can be used by topLinks as well as indexActions -// XXX this isn't quite the right test -// if(isset($entity->status) && $entity->status == StatusEnum::Active) { - $actionOrderDefault = $this->Menu->getMenuOrder('Default'); - foreach($indexActions as $a) { - $ok = false; - if(!empty($a['controller'])) { - $tableName = Inflector::camelize($a['controller']); + } elseif($cfg['type'] == 'relatedLink') { + $m = $cfg['model']; - if(isset($vv_permission_set[$entity->id][$tableName][ $a['action'] ])) { - $ok = $vv_permission_set[$entity->id][$tableName][ $a['action'] ]; + if(!empty($entity->$m->id)) { + // We need the controller for the related entity, however $m + // might be an alias and $entity->getSource() returns the + // aliased class name. So we use PHP's get_class instead. + $c = Inflector::tableize(substr(get_class($entity->$m), strrpos(get_class($entity->$m), '\\')+1)); + + foreach($tryActions as $a) { + // Does this user have permission for this action? + // XXX we actually need to know the permissions on the target (ie: actor person) + if(true || + $vv_permission_set[$entity->id][$a]) { + $args = []; + $readOnlyIcon = ''; + if($isFirstLink) { + $linkClass = 'row-link'; + if($a == 'edit') { + $linkClass .= ' row-link-edit'; + } elseif ($a == 'view') { + $linkClass .= ' row-link-view'; + $readOnlyIcon = ' edit_off'; + } + $args = ['class' => $linkClass]; + $isFirstLink = false; + } + // Output the link + if(!empty($readOnlyIcon)) { + print ''; + } + $linked = true; + break 2; + } + } } - } else { - $ok = $vv_permission_set[$entity->id][ $a['action'] ]; } - if($ok && !empty($a['if'])) { - // If there's a conditional on the field, test the entity - $f = $a['if']; - - $ok = $entity->$f(); - } - - if($ok) { - $actionOrder = !empty($a['order']) ? $a['order'] : $actionOrderDefault++; - $actionIcon = !empty($a['icon']) ? $a['icon'] : $this->Menu->getMenuIcon('Default'); - $actionClass = !empty($a['class']) ? $a['class'] : ''; - $actionUrl = ''; - $actionLabel = ''; - $actionOnClick = []; // used for confirmation dialog - - // Generate the link text and urls: - - // If we have a .confirm text, we need to generate a confirm dialog box - $confirmKey = $a['action'].'.confirm'; - $confirmTxt = __d('operation', $confirmKey); - - if($confirmTxt != $confirmKey) { - // We found the localized string - $actionPostBtnArray = ['action' => $a['action'], $entity->id]; - $actionUrl = $this->Url->build(['action' => $a['action'], $entity->id]); - // XXX should be configurable which field we put in, maybe displayField? - $action_args['vv_actions'][] = array( - 'order' => $actionOrder, - 'icon' => $actionIcon, - 'url' => 'javascript:void(0);', - 'label' => __d('operation', $a['action']), - 'class' => !empty($actionClass) ? $actionClass . ' nospin' : 'nospin', - 'onclick' => array( - 'dg_bd_txt' => __d('operation', $confirmKey, [$entity->id]), // dialog body text - 'dg_post_btn_array' => $actionPostBtnArray, // postButton array for building the postButton - 'dg_url' => $actionUrl, // action url for building a unique ID - 'dg_conf_btn' => __d('operation', 'confirm'), // dialog confirm button text - 'dg_cancel_btn' => __d('operation', 'cancel'), // dialog cancel button text - 'dg_title' => __d('operation', 'confirm'), // dialog box title - 'dg_bd_txt_repl_str' => '' // dialog body text replacement strings - ), - ); - } elseif(!empty($a['controller'])) { - // We're linking into a related controller - $actionLabel = __d('controller', Inflector::camelize(Inflector::pluralize($a['controller'])), [99]); - $actionUrl = $this->Url->build( - ['controller' => $a['controller'], - 'action' => $a['action'], - // We support the use of closures for custom query strings - '?' => (!empty($a['query']) ? $a['query']($entity) : [ $tableFK => $entity->id ])] - ); - } else { - $actionLabel = __d('operation', $a['action']); - $actionUrl = $this->Url->build(['action' => $a['action'], $entity->id]); - } - - // If a specific label is sent in the config, use it instead - if(!empty($a['label'])) { - $actionLabel = $a['label']; - } - - // Set the action link configuration - $action_args['vv_actions'][] = array( - 'order' => $actionOrder, - 'icon' => $actionIcon, - 'url' => $actionUrl, - 'label' => $actionLabel, - 'class' => $actionClass, - 'onclick' => $actionOnClick - ); + if(!$linked) { + // Just echo the value + print $label; } - } - -// } - } - - // Delete - if($vv_permission_set[$entity->id]['delete']) { - $actionPostBtnArray = ['action' => 'delete', $entity->id]; - $actionUrl = $this->Url->build(['action' => 'delete', $entity->id]); - $action_args['vv_actions'][] = array( - 'order' => $this->Menu->getMenuOrder('Delete'), - 'icon' => $this->Menu->getMenuIcon('Delete'), - 'url' => 'javascript:void(0);', - 'label' => __d('operation', 'delete'), - 'class' => 'deletebutton nospin', - 'onclick' => array( - 'dg_bd_txt' => __d('operation', 'delete.confirm', [$entity->id]), - 'dg_post_btn_array' => $actionPostBtnArray, - 'dg_url' => $actionUrl, - 'dg_conf_btn' => __d('operation', 'remove'), - 'dg_cancel_btn' => __d('operation', 'cancel'), - 'dg_title' => __d('operation', 'remove'), - 'dg_bd_txt_repl_str' => '' - ), - ); - } - - if(!empty($action_args['vv_actions'])) { - print '
'; - print $this->element('menuAction', $action_args); - print '
'; - } - - ?> -
-element("pagination"); \ No newline at end of file +element("pagination"); \ No newline at end of file diff --git a/app/templates/TelephoneNumbers/columns.inc b/app/templates/TelephoneNumbers/columns.inc index 6b2eff199..a8916b276 100644 --- a/app/templates/TelephoneNumbers/columns.inc +++ b/app/templates/TelephoneNumbers/columns.inc @@ -33,3 +33,8 @@ $indexColumns = [ 'type' => 'fk' ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; \ No newline at end of file diff --git a/app/templates/Types/actions.inc b/app/templates/Types/actions.inc new file mode 100644 index 000000000..cc9ceba6e --- /dev/null +++ b/app/templates/Types/actions.inc @@ -0,0 +1,43 @@ + 'settings_backup_restore', + 'order' => 'Default', + 'label' => __d('operation', 'Types.restore'), + 'link' => [ + 'action' => 'restore' + ], + 'class' => '' + ] +]; + +// Actions appear as row-level menu items in the index view gear icon and as a tab menu or top-link menu on +// edit and view pages based on user permissions. +$actions = []; \ No newline at end of file diff --git a/app/templates/Types/columns.inc b/app/templates/Types/columns.inc index 8eeeff44a..54b8b77ac 100644 --- a/app/templates/Types/columns.inc +++ b/app/templates/Types/columns.inc @@ -41,14 +41,7 @@ $indexColumns = [ ] ]; -$topLinks = [ - [ - 'icon' => 'settings_backup_restore', - 'order' => 'Default', - 'label' => __d('operation', 'Types.restore'), - 'link' => [ - 'action' => 'restore' - ], - 'class' => '' - ] +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true ]; \ No newline at end of file diff --git a/app/templates/Urls/columns.inc b/app/templates/Urls/columns.inc index dfcedffe4..f7a32778e 100644 --- a/app/templates/Urls/columns.inc +++ b/app/templates/Urls/columns.inc @@ -33,3 +33,8 @@ $indexColumns = [ 'type' => 'fk' ] ]; + +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; \ No newline at end of file diff --git a/app/templates/element/javascript.php b/app/templates/element/javascript.php index 97f28c837..3de10d6bd 100644 --- a/app/templates/element/javascript.php +++ b/app/templates/element/javascript.php @@ -128,28 +128,63 @@ placeholder: "-- Select --" }); - // Enable Bootstrap Popovers. Unless needed elsewhere, constrain this to #content - // XXX Enable when/if needed - // $('#content [data-bs-toggle="popover"]').popover(); - // Generic row click handling for div-based rows - $('div.linked-row').click(function (e) { + $('div.linked-row').click(function(e) { location.href = $(this).find('a.row-link').attr('href'); }); - // Generic row click handling for table rows - $('td.row-link').each(function (e) { - url = $(this).find('a').attr('href'); + // Generic row click handling for index-table rows + // First capture mouse location to test if we're clicking or drag-selecting (for copy) + var mouseDownEvent = null; + $('table.index-table tr').mousedown(function(e) { + mouseDownEvent = e; + }); + + $('table.index-table tr').each(function(e) { + url = $(this).find('a.row-link').attr('href'); if(url != undefined && url != '') { - $(this).closest('tr').addClass('linked-row').attr('data-cm-target',url).click(function (e) { - location.href = $(this).attr('data-cm-target'); - }).find('a').on('click',function(e){ - // don't propagate on other links to avoid redirecting dialog boxes + $(this).addClass('linked-row').attr('data-cm-target',url).mouseup(function(e) { + if(Math.abs(e.clientX-mouseDownEvent.clientX) < 5 && + Math.abs(e.clientY-mouseDownEvent.clientY < 5)) { + // We have a click event on the row. Now determine what mode we're in and act accordingly. + if($(this).closest('table.index-table').hasClass('bulk-edit-mode')) { + // We're in bulk edit mode, so click the associated checkbox unless we mouseup on a label or checkbox input. + if(e.target.nodeName != 'LABEL' && e.target.nodeName != 'INPUT') { + $(this).find('.form-check-input').click(); + } + } else { + // We're in list mode, so follow the row-link target. + location.href = $(this).attr('data-cm-target'); + } + } + mouseDownEvent = null; + }); + // don't propagate on other links to avoid redirecting dialog boxes and action menus + $(this).find('a').on('click, mouseup',function(e){ e.stopPropagation(); }); } }); + // Bulk edit switch + $('#bulk-edit-switch').click(function() { + if($("#bulk-edit-switch").is(':checked')) { + $("table.index-table").removeClass('list-mode').addClass('bulk-edit-mode'); + } else { + $("table.index-table").removeClass('bulk-edit-mode').addClass('list-mode'); + } + }); + + // Bulk edit select all checkbox + $('#bulk-action-select-all').click(function() { + if($(this).is(":checked")) { + $('table.index-table.bulk-edit-mode .form-check-input').prop('checked', true); + } else { + $('table.index-table.bulk-edit-mode .form-check-input').prop('checked', false); + } + }); + + // Add loading animation when a form is submitted, when any item with a "spin" class is clicked, // or on any anchor tag lacking the .nospin class. We do not automatically add this to buttons // because they are often on-page controls. Add a "spin" class to buttons that need it. diff --git a/app/templates/element/menuAction.php b/app/templates/element/menuAction.php index e7f91e775..80f732367 100644 --- a/app/templates/element/menuAction.php +++ b/app/templates/element/menuAction.php @@ -29,6 +29,7 @@ $actionsCountClass = $actionsCount > 0 ? ' actions-count-' . $actionsCount : ''; $actionsMenuClass = 'field-actions-menu dropdown dropleft' . $actionsCountClass; $actionsMenuUid = md5($vv_attr_id); +$actionsType = isset($vv_actions_type) ? $vv_actions_type : 'row-actions'; ?>
+ +
  • +
    + + +
    +
  • + -
    + \ No newline at end of file diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css index 855c3b0fd..69300673c 100644 --- a/app/webroot/css/co-base.css +++ b/app/webroot/css/co-base.css @@ -760,74 +760,6 @@ ul.form-list li.alert-banner .co-alert { white-space: nowrap; margin-right: 0.5em; } -/* RECONCILE TABLE */ -#reconcile-table { - width: auto; - border: none; - font-size: 0.8em; -} -#reconcile-table td, -#reconcile-table th { - border: 1px solid var(--cmg-color-lightgray-006); -} -#reconcile-table th { - font-weight: normal; -} -#reconcile-table th.col-names { - text-align: center; - background-color: var(--cmg-color-white); - border: none; - font-weight: bold; -} -#reconcile-table td.empty { - background-color: var(--cmg-color-white) !important; - border: none; - font-size: 0.5em; -} -#reconcile-table tbody td.reference-ids { - text-align: center; - word-break: break-all; - background-color: var(--cmg-color-lightgray-003) !important; -} -#reconcile-table td.reconcile-actions { - text-align: center; -} -#reconcile-table .btn { - font-size: 1em; -} -#reconcile-table th.attr-title { - border: none; - background-color: var(--cmg-color-white); - text-align: right; -} -/* set the borders on the "new" reference id */ -#reconcile-table tbody td:nth-child(2) { - border-left: 2px solid var(--cmg-color-lightgray-007); - border-right: 2px solid var(--cmg-color-lightgray-007); -} -#reconcile-table tbody tr:first-child td:nth-child(2) { - border-top: 2px solid var(--cmg-color-lightgray-007); -} -#reconcile-table tbody tr:last-child td:nth-child(2) { - border-bottom: 2px solid var(--cmg-color-lightgray-007); -} -/* no zebra for this table */ -#reconcile-table tr:nth-child(2n+1) td { - background-color: var(--cmg-color-white); -} -#reconcile-table tr.match td { - background-color: var(--cmg-color-green-004); -} -/* MATCHGRID MANAGEMENT */ -#matchgrid-management { - padding: 0; - margin: 0 0 3em; -} -.matchgrid-description { - margin-top: -1em; - margin-bottom: 2em; - font-style: italic; -} /* CO CONFIGURATION DASHBOARD */ #configuration-menu { list-style: none; @@ -843,17 +775,9 @@ ul.form-list li.alert-banner .co-alert { color: var(--cmg-color-gray-001); margin-right: 0.5em; } -/* MATCHGRID RECORDS INDEX */ -body.matchgridrecords .popover { - max-width: unset; -} -body.matchgridrecords .popover-header { - font-size: 1em; -} /* INDEX ACTION COMMAND MENUS */ -td.actions, -th.actions { - text-align: center; +a.action-menu-toggle { + padding: 0.5rem; } .field-actions .dropdown-menu { padding: 0; @@ -862,12 +786,40 @@ th.actions { } .field-actions a.dropdown-item { padding: 0.5em 1em; - color: var(--cmg-color-blue-001); + color: var(--cmg-color-gray-001); } #main .field-actions a.dropdown-item:hover { color: var(--cmg-color-black); text-decoration: none; } +.dropdown-item.active, +.dropdown-item:active { + background-color: var(--cmg-color-lightgray-004); +} +a.dropdown-item.deletebutton { + color: var(--cmg-color-red-002); +} +/* INDEX ACTION BULK EDIT */ +.field-actions.top-links #bulk-edit-switch-container { + padding: 0.5em 1em; +} +table.bulk-edit-mode a.row-link, +table.bulk-edit-mode .read-only-link-container, +table.bulk-edit-mode .row-link-heading, +table.bulk-edit-mode td.actions .field-actions { + display: none; +} +table.list-mode a.row-link, +table.list-mode td.actions .field-actions { + display: block; +} +table.list-mode .bulk-action-checkbox-container { + display: none; +} +table.index-table .form-check { + margin-bottom: 0; + min-height: unset; +} /* PAGINATION */ #pagination { margin: 0; @@ -1128,7 +1080,8 @@ ul.form-list li.field-stack textarea { display: inline; margin: 0; } -#content .material-icons { +#content .material-icons, +#content .material-icons-outlined { font-size: 17px; margin-top: 1px; vertical-align: top; @@ -1351,7 +1304,7 @@ code, .fixed-width * { font-family: "Courier New","Courier",monospace !important; } -/* Tables */ +/* INDEX VIEWS and TABLES */ table { width: 100%; border-collapse: collapse; @@ -1373,12 +1326,21 @@ th { td { border-bottom: 1px solid var(--cmg-color-lightgray-005); } -tr th:first-child, -tr td:first-child { +th:first-child, +td:first-child { padding-left: 1.25em; } -tr th:last-child, -tr td:last-child { +table.list-mode th.actions:first-child, +table.list-mode td.actions:first-child { + padding-left: 0; + text-align: center; +} +table.bulk-edit-mode th.actions, +table.bulk-edit-mode td.actions { + width: 1.25em; +} +th:last-child, +td:last-child { border-right: none; } tr.noborder td { @@ -1387,6 +1349,14 @@ tr.noborder td { td.indented { border-left: 3em solid var(--cmg-color-white); } +table.index-table tr:hover td { + background-color: var(--cmg-color-lightgray-001); + cursor: pointer; +} +table.index-table.with-actions th:nth-child(2), +table.index-table.with-actions td:nth-child(2) { + padding-left: 0; +} .linked-row { cursor: pointer !important; } @@ -1394,6 +1364,15 @@ td.indented { .linked-row:hover td { background-color: var(--cmg-color-lightgray-001) !important; } +table.list-mode .read-only-link-container { + display: flex; + justify-content: space-between; + align-items: center; +} +.linked-row .read-only-icon { + color: var(--cmg-color-gray-003); + padding: 0 0.5em; +} .menuitembutton { width: 250px; } diff --git a/app/webroot/css/co-responsive.css b/app/webroot/css/co-responsive.css index 70e85faba..a5bb77c8a 100644 --- a/app/webroot/css/co-responsive.css +++ b/app/webroot/css/co-responsive.css @@ -235,6 +235,9 @@ display: flex; } /* GENERAL */ + .table-container { + overflow: unset; + } .two-col { -webkit-column-count: 2; /* Chrome, Safari, Opera */ -moz-column-count: 2; /* Firefox */ @@ -287,47 +290,47 @@ #reconcile-table { font-size: 1em; } - /* INDEX ACTION COMMAND MENUS */ - /* convert the dropdown menus to be visible on wider screens */ - .field-actions .action-menu-toggle { + /* INDEX ACTION ROW MENUS */ + .field-actions .dropdown-menu { + font-size: 0.9em; + white-space: nowrap; + } + th.actions, + td.actions { + width: 40px; /* this is pushed wider by the nowrap on .field-actions .dropdown-menu */ + padding: 0; + } + /* EDIT / VIEW / INDEX ACTION TOP-LINKS MENU */ + /* Convert the dropdown menus to be visible on wider screens. + XXX We will likely collapse the toplinks when there are many, but for now, expose them. */ + .field-actions.top-links { + align-self: end; + } + .field-actions.top-links .action-menu-toggle { display: none; } - .field-actions .dropdown-menu { + .field-actions.top-links .dropdown-menu { position: static; display: inline-block; + min-width: 0; font-size: 0.9em; border: none; background-color: transparent; white-space: nowrap; } - .field-actions .action-list-item { + .field-actions.top-links .action-list-item { display: inline-block; } - .field-actions a.dropdown-item { + .field-actions.top-links a.dropdown-item { padding: 0 0.5em; } - #main .field-actions a.dropdown-item:hover { + #main .field-actions.top-links a.dropdown-item:hover { color: var(--cmg-color-blue-001); text-decoration: underline; } - .field-actions a.dropdown-item:hover { + .field-actions.top-links a.dropdown-item:hover { background-color: transparent; } - th.actions { - width: 100px; /* this is pushed wider by the nowrap on .field-actions .dropdown-menu */ - padding-left: 1.5em; - text-align: left; - } - td.actions { - text-align: left; - } - /* EDIT / VIEW ACTION TOP MENU */ - .field-actions.top-links { - align-self: end; - } - .field-actions.top-links .dropdown-menu { - min-width: 0; - } /* FOOTER */ #co-footer { position: static;