diff --git a/app/availableplugins/FileConnector/src/Model/Table/FileSourcesTable.php b/app/availableplugins/FileConnector/src/Model/Table/FileSourcesTable.php index c51a21307..0e26f851a 100644 --- a/app/availableplugins/FileConnector/src/Model/Table/FileSourcesTable.php +++ b/app/availableplugins/FileConnector/src/Model/Table/FileSourcesTable.php @@ -44,7 +44,8 @@ class FileSourcesTable extends Table { use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + use \App\Lib\Traits\TabTrait; + // Cache of the field configuration protected $fieldCfg = null; @@ -78,6 +79,23 @@ public function initialize(array $config): void { ] ]); + // All the tabs share the same configuration in the ModelTable file + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['ExternalIdentitySources', 'FileConnector.FileSources', 'ExternalIdentitySources@action.search'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'ExternalIdentitySources' => ['edit', 'view', 'search'], + 'FileConnector.FileSources' => ['edit'], + 'ExternalIdentitySources@action.search' => [], + ], + ] + ); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) 'entity' => [ diff --git a/app/availableplugins/FileConnector/templates/FileSources/fields-nav.inc b/app/availableplugins/FileConnector/templates/FileSources/fields-nav.inc deleted file mode 100644 index 4228ce579..000000000 --- a/app/availableplugins/FileConnector/templates/FileSources/fields-nav.inc +++ /dev/null @@ -1,31 +0,0 @@ - 'plugin', - 'active' => 'plugin' - ]; \ No newline at end of file diff --git a/app/plugins/CoreEnroller/src/Model/Table/AttributeCollectorsTable.php b/app/plugins/CoreEnroller/src/Model/Table/AttributeCollectorsTable.php index 902419507..2c1699ddf 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/AttributeCollectorsTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/AttributeCollectorsTable.php @@ -42,6 +42,7 @@ class AttributeCollectorsTable extends Table { use \App\Lib\Traits\CoLinkTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; @@ -74,6 +75,40 @@ public function initialize(array $config): void { $this->setRequiresCO(true); $this->setAllowLookupPrimaryLink(['dispatch', 'display']); + // All the tabs share the same configuration in the ModelTable file + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['EnrollmentFlows', 'EnrollmentFlowSteps', 'Petitions'], + // What actions will inlcude the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'EnrollmentFlows' => ['edit', 'view'], + 'EnrollmentFlowSteps' => ['index'], + 'Petitions' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['EnrollmentFlowSteps', 'Petitions'], + 'nested' => [ + // Ordered list of Tabs + 'tabs' => ['EnrollmentFlowSteps', 'CoreEnroller.AttributeCollectors', 'CoreEnroller.EnrollmentAttributes'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'EnrollmentFlowSteps' => ['edit', 'view'], + 'CoreEnroller.AttributeCollectors' => ['edit'], + 'CoreEnroller.EnrollmentAttributes' => ['index'] + ], + // What model will have a counter-badge after the tab title + 'counter' => ['CoreEnroller.EnrollmentAttributes'], + ] + ] + ); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) 'entity' => [ diff --git a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php index fb0bbad24..d37681bc0 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php @@ -46,6 +46,7 @@ class EnrollmentAttributesTable extends Table { use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\TabTrait; /** * Perform Cake Model initialization. @@ -157,6 +158,40 @@ public function initialize(array $config): void { ] ]); + // All the tabs share the same configuration in the ModelTable file + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['EnrollmentFlows', 'EnrollmentFlowSteps', 'Petitions'], + // What actions will inlcude the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'EnrollmentFlows' => ['edit', 'view'], + 'EnrollmentFlowSteps' => ['index'], + 'Petitions' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['EnrollmentFlowSteps', 'Petitions'], + 'nested' => [ + // Ordered list of Tabs + 'tabs' => ['EnrollmentFlowSteps', 'CoreEnroller.AttributeCollectors', 'CoreEnroller.EnrollmentAttributes'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'EnrollmentFlowSteps' => ['edit', 'view'], + 'CoreEnroller.AttributeCollectors' => ['edit'], + 'CoreEnroller.EnrollmentAttributes' => ['index'] + ], + // What model will have a counter-badge after the tab title + 'counter' => ['CoreEnroller.EnrollmentAttributes'], + ] + ] + ); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) 'entity' => [ diff --git a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc index d0a6475ec..86257c536 100644 --- a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc +++ b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc @@ -38,13 +38,14 @@ if($vv_action == 'dispatch') { } } - print $this->Field->control( - // We prefix the attribute ID with a string because Cake seems to sometimes have - // problems with field names that are purely integers (even if cast to strings) - fieldName: "field-".$attr->id, - labelText: $attr->label, - controlType: "string", - options: $options - ); + print $this->element('form/listItem', [ + 'arguments' => [ + // We prefix the attribute ID with a string because Cake seems to sometimes have + // problems with field names that are purely integers (even if cast to strings) + 'fieldName' => 'field-' . $attr->id, + 'fieldOptions' => $options, + 'fieldLabel' => $attr->label + ] + ]); } } \ No newline at end of file diff --git a/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc b/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc index 1948022cf..8791062f7 100644 --- a/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc +++ b/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc @@ -533,19 +533,6 @@ if($vv_action == 'add' || $vv_action == 'edit') { ] ]]); -// print $this->Field->control( -// fieldName: 'default_value_validity_type', -// controlType: 'select', -// options: [ -// 'options' => [ -// 'on' => __d('core_enroller', 'enumeration.DefaultValueValidityType.on'), -// 'after' => __d('core_enroller', 'enumeration.DefaultValueValidityType.after') -// ], -//// 'empty' => false, -// 'onChange' => 'updateValidityGadgets()' -// ] -// ); - // The default value is always stored in default_value, however for select based fields // (such as cou_id or affiliation) the value is copied into default_value and the // field specific attribute is used for display purposes only diff --git a/app/src/Controller/Component/BreadcrumbComponent.php b/app/src/Controller/Component/BreadcrumbComponent.php index 071733ee8..f6e54df4f 100644 --- a/app/src/Controller/Component/BreadcrumbComponent.php +++ b/app/src/Controller/Component/BreadcrumbComponent.php @@ -191,7 +191,7 @@ public function injectPrimaryLink(object $link, bool $index=true, string $linkLa // In the case we are dealing with non-standard actions we need to fallback to a standard one // in order to get access to the contain array. We will use the permissions to decide which // action to fall back to - if(!in_array($requestAction, [ + if(!\in_array($requestAction, [ 'index', 'view', 'delete', 'add', 'edit' ])) { $permissionsArray = $this->getController()->RegistryAuth->calculatePermissionsForView($requestAction); @@ -276,7 +276,7 @@ public function injectTitleLink( $entity, string $action='edit', ?string $label=null - ) { + ): void { $displayField = $table->getDisplayField(); $this->injectTitleLinks[] = [ @@ -298,7 +298,8 @@ public function injectTitleLink( * @param array $skipPaths Array of regular expressions describing paths to be skipped */ - public function skipAll(array $skipPaths) { + public function skipAll(array $skipPaths): void + { $this->skipAllPaths = $skipPaths; } @@ -311,7 +312,8 @@ public function skipAll(array $skipPaths) { * @param array $skipPaths Array of regular expressions describing paths */ - public function skipConfig(array $skipPaths) { + public function skipConfig(array $skipPaths): void + { $this->skipConfigPaths = $skipPaths; } @@ -323,7 +325,8 @@ public function skipConfig(array $skipPaths) { * @param array $skipPaths Array of regular expressions describing paths */ - public function skipParents(array $skipPaths) { + public function skipParents(array $skipPaths): void + { $this->skipParentPaths = $skipPaths; } } \ No newline at end of file diff --git a/app/templates/element/tabs/tabTitleWithCount.php b/app/src/Lib/Traits/TabTrait.php similarity index 69% rename from app/templates/element/tabs/tabTitleWithCount.php rename to app/src/Lib/Traits/TabTrait.php index 07845fa6c..0144c11e3 100644 --- a/app/templates/element/tabs/tabTitleWithCount.php +++ b/app/src/Lib/Traits/TabTrait.php @@ -1,6 +1,6 @@ Common->getModelTotalCount($model, $where); -} +declare(strict_types = 1); + +namespace App\Lib\Traits; + +trait TabTrait +{ + /** + * Tabs configuration + * + * @var array + */ + private array $tabsConfig = []; + + /** + * @return array + */ + public function getTabsConfig(): array + { + return $this->tabsConfig; + } -?> + /** + * @param array $tabsConfig + * + * @return void + */ + public function setTabsConfig(array $tabsConfig): void + { + $this->tabsConfig = $tabsConfig; + } - - = $num ?> - -= $title ?> \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php index 97437c183..cad2457de 100644 --- a/app/src/Lib/Util/StringUtilities.php +++ b/app/src/Lib/Util/StringUtilities.php @@ -220,12 +220,13 @@ public static function foreignKeyToClassName(string $s): string { /** * Localize a controller name, accounting for plugins. - * - * @since COmanage Registry v5.0.0 - * @param string $controllerName Name of controller to localize - * @param string $pluginName Plugin name, if appropriate - * @param bool $plural Whether to use plural localization + * + * @param string $controllerName Name of controller to localize + * @param string|null $pluginName Plugin name, if appropriate + * @param bool $plural Whether to use plural localization + * * @return string Localized text string + * @since COmanage Registry v5.0.0 */ public static function localizeController(string $controllerName, ?string $pluginName, bool $plural=false): string { diff --git a/app/src/Model/Table/EnrollmentFlowStepsTable.php b/app/src/Model/Table/EnrollmentFlowStepsTable.php index 8071e772b..d550ddc97 100644 --- a/app/src/Model/Table/EnrollmentFlowStepsTable.php +++ b/app/src/Model/Table/EnrollmentFlowStepsTable.php @@ -44,9 +44,10 @@ class EnrollmentFlowStepsTable extends Table { use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PluggableModelTrait; use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + /** * Perform Cake Model initialization. * @@ -110,6 +111,41 @@ public function initialize(array $config): void { ] ] ]); + + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['EnrollmentFlows', 'EnrollmentFlowSteps', 'Petitions'], + // What actions will inlcude the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'EnrollmentFlows' => ['edit', 'view'], + 'EnrollmentFlowSteps' => ['index'], + 'Petitions' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['EnrollmentFlowSteps', 'Petitions'], + 'nested' => [ + // Ordered list of Tabs + 'tabs' => ['EnrollmentFlowSteps', 'EnrollmentFlowSteps.Plugin', 'EnrollmentFlowSteps.Hierarchy'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'EnrollmentFlowSteps' => ['edit', 'view'], + 'EnrollmentFlowSteps.Plugin' => ['edit'], + // This means that we are looking at the plugins associated model + // EnrollmentFlowSteps -> plugin -> @plugin + 'EnrollmentFlowSteps.Hierarchy' => ['index'] + ], + // What model will have a counter-badge after the tab title + 'counter' => ['EnrollmentFlowSteps.Hierarchy'], + ] + ] + ); } /** diff --git a/app/src/Model/Table/EnrollmentFlowsTable.php b/app/src/Model/Table/EnrollmentFlowsTable.php index 1ef0e2a64..58cc0c743 100644 --- a/app/src/Model/Table/EnrollmentFlowsTable.php +++ b/app/src/Model/Table/EnrollmentFlowsTable.php @@ -47,9 +47,10 @@ class EnrollmentFlowsTable extends Table { use \App\Lib\Traits\CoLinkTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + /** * Perform Cake Model initialization. * @@ -121,6 +122,24 @@ public function initialize(array $config): void { ] ] ]); + + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['EnrollmentFlows', 'EnrollmentFlowSteps', 'Petitions'], + // What actions will inlcude the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'EnrollmentFlows' => ['edit', 'view'], + 'EnrollmentFlowSteps' => ['index'], + 'Petitions' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['EnrollmentFlowSteps', 'Petitions'] + ] + ); } /** diff --git a/app/src/Model/Table/ExtIdentitySourceRecordsTable.php b/app/src/Model/Table/ExtIdentitySourceRecordsTable.php index e4db956b9..e16c54f74 100644 --- a/app/src/Model/Table/ExtIdentitySourceRecordsTable.php +++ b/app/src/Model/Table/ExtIdentitySourceRecordsTable.php @@ -50,6 +50,7 @@ class ExtIdentitySourceRecordsTable extends Table { use \App\Lib\Traits\SearchFilterTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\TabTrait; /** * Perform Cake Model initialization. @@ -102,6 +103,38 @@ public function initialize(array $config): void { ] ]);*/ + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['People', 'PersonRoles', 'ExternalIdentities'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'People' => ['edit', 'view'], + 'PersonRoles' => ['index'], + 'ExternalIdentities' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['PersonRoles', 'ExternalIdentities'], + 'nested' => [ + // Ordered list of Tabs + 'tabs' => ['ExternalIdentities', 'ExternalIdentityRoles'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'ExternalIdentities' => ['edit', 'view'], + 'ExternalIdentityRoles' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['ExternalIdentityRoles'], + ] + ] + ); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) 'entity' => [ diff --git a/app/src/Model/Table/ExternalIdentitiesTable.php b/app/src/Model/Table/ExternalIdentitiesTable.php index 9f945a039..dca4900e6 100644 --- a/app/src/Model/Table/ExternalIdentitiesTable.php +++ b/app/src/Model/Table/ExternalIdentitiesTable.php @@ -47,7 +47,8 @@ class ExternalIdentitiesTable extends Table { use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; use \App\Lib\Traits\SearchFilterTrait; - + use \App\Lib\Traits\TabTrait; + /** * Perform Cake Model initialization. * @@ -147,7 +148,40 @@ public function initialize(array $config): void { 'class' => 'StatusEnum' ] ]); - + + // All the tabs share the same configuration in the ModelTable file + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['People', 'PersonRoles', 'ExternalIdentities'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'People' => ['edit', 'view'], + 'PersonRoles' => ['index'], + 'ExternalIdentities' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['PersonRoles', 'ExternalIdentities'], + 'nested' => [ + // Ordered list of Tabs + 'tabs' => ['ExternalIdentities', 'ExternalIdentityRoles'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'ExternalIdentities' => ['edit', 'view'], + 'ExternalIdentityRoles' => ['index'] + ], + // What model will have a counter-badge after the tab title + 'counter' => ['ExternalIdentityRoles'], + ] + ] + ); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) // See also CFM-126 diff --git a/app/src/Model/Table/ExternalIdentityRolesTable.php b/app/src/Model/Table/ExternalIdentityRolesTable.php index 35a267ba1..d67b2f81d 100644 --- a/app/src/Model/Table/ExternalIdentityRolesTable.php +++ b/app/src/Model/Table/ExternalIdentityRolesTable.php @@ -49,7 +49,8 @@ class ExternalIdentityRolesTable extends Table { use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; use \App\Lib\Traits\SearchFilterTrait; - + use \App\Lib\Traits\TabTrait; + /** * Perform Cake Model initialization. * @@ -118,7 +119,39 @@ public function initialize(array $config): void { 'attribute' => 'PersonRoles.affiliation_type' ] ]); - + + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['People', 'PersonRoles', 'ExternalIdentities'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'People' => ['edit', 'view'], + 'PersonRoles' => ['index'], + 'ExternalIdentities' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['PersonRoles', 'ExternalIdentities'], + 'nested' => [ + // Ordered list of Tabs + 'tabs' => ['ExternalIdentities', 'ExternalIdentityRoles'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'ExternalIdentities' => ['edit', 'view'], + 'ExternalIdentityRoles' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['ExternalIdentityRoles'], + ] + ] + ); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) // See also CFM-126 diff --git a/app/src/Model/Table/ExternalIdentitySourcesTable.php b/app/src/Model/Table/ExternalIdentitySourcesTable.php index f50542d44..45312a0c4 100644 --- a/app/src/Model/Table/ExternalIdentitySourcesTable.php +++ b/app/src/Model/Table/ExternalIdentitySourcesTable.php @@ -48,6 +48,7 @@ class ExternalIdentitySourcesTable extends Table { use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; + use \App\Lib\Traits\TabTrait; // Cache of the EIS configuration, keyed on id protected $eisCache = null; @@ -99,7 +100,24 @@ public function initialize(array $config): void { 'class' => 'SyncModeEnum' ] ]); - + + // All the tabs share the same configuration in the ModelTable file + $this->setTabsConfig( + [ + // Ordered-list of Tabs + 'tabs' => ['ExternalIdentitySources', 'ExternalIdentitySources.Plugin', 'ExternalIdentitySources@action.search'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'ExternalIdentitySources' => ['edit', 'view', 'search'], + 'ExternalIdentitySources.Plugin' => ['edit'], + 'ExternalIdentitySources@action.search' => [], + ], + ] + ); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) 'entity' => [ diff --git a/app/src/Model/Table/GroupMembersTable.php b/app/src/Model/Table/GroupMembersTable.php index b645aaf2a..5b8b61865 100644 --- a/app/src/Model/Table/GroupMembersTable.php +++ b/app/src/Model/Table/GroupMembersTable.php @@ -52,9 +52,10 @@ class GroupMembersTable extends Table { use \App\Lib\Traits\ProvisionableTrait; use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\SearchFilterTrait; + use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + /** * Perform Cake Model initialization. * @@ -131,6 +132,18 @@ public function initialize(array $config): void { 'order' => 1 ], ]); + + $this->setTabsConfig( + [ + 'tabs' => ['Groups', 'GroupMembers', 'GroupNestings'], + 'action' => [ + 'Groups' => ['edit', 'view'], + 'GroupMembers' => ['index'], + 'GroupNestings' => ['index'], + ], + 'counter' => ['GroupMembers'] + ] + ); } /** diff --git a/app/src/Model/Table/GroupNestingsTable.php b/app/src/Model/Table/GroupNestingsTable.php index 0fdda7cb2..6eb28c8c5 100644 --- a/app/src/Model/Table/GroupNestingsTable.php +++ b/app/src/Model/Table/GroupNestingsTable.php @@ -43,9 +43,10 @@ class GroupNestingsTable extends Table { use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\QueryModificationTrait; + use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + /** * Perform Cake Model initialization. * @@ -96,6 +97,18 @@ public function initialize(array $config): void { ] ]); + $this->setTabsConfig( + [ + 'tabs' => ['Groups', 'GroupMembers', 'GroupNestings'], + 'action' => [ + 'Groups' => ['edit', 'view'], + 'GroupMembers' => ['index'], + 'GroupNestings' => ['index'], + ], + 'counter' => ['GroupMembers'] + ] + ); + // XXX Keeping for functionality reference // $this->setAutoViewVars([ // 'groupMembers' => [ diff --git a/app/src/Model/Table/GroupsTable.php b/app/src/Model/Table/GroupsTable.php index f42470ee1..4df6db0cd 100644 --- a/app/src/Model/Table/GroupsTable.php +++ b/app/src/Model/Table/GroupsTable.php @@ -52,10 +52,11 @@ class GroupsTable extends Table { use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\ProvisionableTrait; use \App\Lib\Traits\QueryModificationTrait; + use \App\Lib\Traits\SearchFilterTrait; + use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - use \App\Lib\Traits\SearchFilterTrait; - + /** * Perform Cake Model initialization. * @@ -182,7 +183,25 @@ public function initialize(array $config): void { ] ] ]); - + + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['Groups', 'GroupMembers', 'GroupNestings'], + // What actions will inlcude the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'Groups' => ['edit', 'view'], + 'GroupMembers' => ['index'], + 'GroupNestings' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['GroupMembers'] + ] + ); + $this->setPermissions([ // XXX update for couAdmins, etc // Actions that operate over an entity (ie: require an $id) diff --git a/app/src/Model/Table/PeopleTable.php b/app/src/Model/Table/PeopleTable.php index 9759511c4..e829ecd0b 100644 --- a/app/src/Model/Table/PeopleTable.php +++ b/app/src/Model/Table/PeopleTable.php @@ -50,9 +50,10 @@ class PeopleTable extends Table { use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\ProvisionableTrait; use \App\Lib\Traits\QueryModificationTrait; + use \App\Lib\Traits\SearchFilterTrait; + use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - use \App\Lib\Traits\SearchFilterTrait; /** * Perform Cake Model initialization. @@ -225,7 +226,25 @@ public function initialize(array $config): void { 'order' => 99 ] ]); - + + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['People', 'PersonRoles', 'ExternalIdentities'], + // What actions will inlcude the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'People' => ['edit', 'view'], + 'PersonRoles' => ['index'], + 'ExternalIdentities' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['PersonRoles', 'ExternalIdentities'] + ] + ); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) // See also CFM-126 diff --git a/app/src/Model/Table/PersonRolesTable.php b/app/src/Model/Table/PersonRolesTable.php index ac797fee5..69e481101 100644 --- a/app/src/Model/Table/PersonRolesTable.php +++ b/app/src/Model/Table/PersonRolesTable.php @@ -53,7 +53,8 @@ class PersonRolesTable extends Table { use \App\Lib\Traits\TypeTrait; use \App\Lib\Traits\ValidationTrait; use \App\Lib\Traits\SearchFilterTrait; - + use \App\Lib\Traits\TabTrait; + // 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 = [ @@ -163,7 +164,25 @@ public function initialize(array $config): void { 'model' => 'Cous' ] ]); - + + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['People', 'PersonRoles', 'ExternalIdentities'], + // What actions will inlcude the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'People' => ['edit', 'view'], + 'PersonRoles' => ['index'], + 'ExternalIdentities' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['PersonRoles', 'ExternalIdentities'] + ] + ); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) // See also CFM-126 diff --git a/app/src/Model/Table/PetitionsTable.php b/app/src/Model/Table/PetitionsTable.php index becb95aea..e8dff786a 100644 --- a/app/src/Model/Table/PetitionsTable.php +++ b/app/src/Model/Table/PetitionsTable.php @@ -49,9 +49,10 @@ class PetitionsTable extends Table { use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\QueryModificationTrait; + use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + /** * Perform Cake Model initialization. * @@ -138,6 +139,24 @@ public function initialize(array $config): void { 'index' => ['platformAdmin', 'coAdmin'] ] ]); + + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['EnrollmentFlows', 'EnrollmentFlowSteps', 'Petitions'], + // What actions will inlcude the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'EnrollmentFlows' => ['edit', 'view'], + 'EnrollmentFlowSteps' => ['index'], + 'Petitions' => ['index'], + ], + // What model will have a counter-badge after the tab title + 'counter' => ['EnrollmentFlowSteps', 'Petitions'] + ] + ); } /** diff --git a/app/src/View/Helper/TabHelper.php b/app/src/View/Helper/TabHelper.php new file mode 100644 index 000000000..22227dc11 --- /dev/null +++ b/app/src/View/Helper/TabHelper.php @@ -0,0 +1,358 @@ +getView()->get('vv_sub_nav_attributes'); + $vv_subnavigation_action = $isNested ? + $vv_sub_nav_attributes['nested']['action'] + : $vv_sub_nav_attributes['action']; + $linkFilter = $this->getLinkFilter($tab, $curId); + + $modelName = $tab; + $controller = $modelName; + $plugin = null; + + $action = $vv_subnavigation_action[$tab][0] ?? null; + if(str_contains($tab, '.Plugin')) { + $modelName = $this->retrievePluginName($tab, (int)$curId); + [$plugin, $controller] = explode('.', $modelName); + $this->setPluginName($modelName); + } else if (str_contains($tab, '.Hierarchy')) { + $modelName = $this->retrievePluginName($tab, (int)$curId); + [$plugin, ] = explode('.', $modelName); + foreach ($this->getHasManyAssociationModels($modelName) as $association) { + [$plugin, $controller] = explode('.', $association); + break; + } + } else if (str_contains($tab, '@action')) { + // We have a plugin path + [$controller,] = explode('@', $modelName); + [, $action] = explode('.', $modelName); + } else if (str_contains($tab, '.')) { + // We have a plugin path + [$plugin, $controller] = explode('.', $modelName); + } + + // XXX Even though we use the model name above, here we need to use the tab name + // since we want to access the configuration. As a result we want the actual configuration + // and not the plugin name we calculated above + $url = [ + 'plugin' => $plugin, + 'controller' => $controller, + 'action' => $action + ]; + + if(\in_array('index', $vv_subnavigation_action[$tab], true)) { + $url['?'] = $linkFilter; + } else { + $url[] = $curId; + } + + return $url; + } + + /** + * Calculate the link Class + * + * @param string $tab + * @param bool $isNested + * + * @return string + */ + public function getLinkClass(string $tab, bool $isNested = false): string + { + $vv_sub_nav_attributes = $this->getView()->get('vv_sub_nav_attributes'); + + $curController = $this->getView()->getRequest()->getParam('controller'); + $curAction = $this->getView()->getRequest()->getParam('action'); + $plugin = $this->getView()->getPlugin(); + $fullModelName = $curController; + if(isset($plugin)) { + $fullModelName = "{$plugin}.{$curController}"; + } +// $associationDepth = 0; +// $isParent = $this->tabBelongsToModelPath($tab, $fullModelName, $associationDepth); + + // The list of Nested models is in order. Which means that the first tab is always the parent and the one + // that will be set as active + $parentModelForNested = $vv_sub_nav_attributes['nested']['tabs'][0] ?? 0; + + // Calculate Tab Style Class(es) + return match(true) { + // Matches the tab to the current controller. It addresses the simple subnavigation + // The fullModelName can either be a simple Model or a Plugin with the path. + $tab === $fullModelName && \in_array($curAction, ['index', 'edit', 'view']), + // Always mark active the parent Tab + !$isNested && $parentModelForNested !== null && $tab === $parentModelForNested, + // Matches the action tab links, e.g. FileSource/search + $tab === "{$curController}@action.{$curAction}" => 'nav-link active', + default => 'nav-link' + }; + } + + /** + * Check the belongsTo tree hierarchy + * + * @param string $tab + * @param string $modelFullName + * @param int $depth + * + * @return bool + */ + public function tabBelongsToModelPath(string $tab, string $modelFullName, int &$depth = 0): bool + { + $model = TableRegistry::getTableLocator()->get($modelFullName); + // We'll start by obtaining the set of models directly associated with the CO model. + $associations = $model->associations(); + + $depth++; + foreach($associations->getByType(['belongsTo', 'belongsToMany']) as $ta) { + if($ta->getClassName() === $tab) { + return true; + } + return $this->tabBelongsToModelPath($tab, $ta->getClassName(), $depth); + } + + return false; + } + + /** + * Check the belongsTo tree hierarchy + * + * @param string $modelName + * + * @return \Generator + */ + public function getHasManyAssociationModels(string $modelName): \Generator + { + $model = TableRegistry::getTableLocator()->get($modelName); + // We'll start by obtaining the set of models directly associated with the CO model. + $associations = $model->associations(); + + foreach($associations->getByType(['hasMany', 'hasOne']) as $ta) { + $this>$this->setAssociation($ta->getClassName()); + yield $ta->getClassName(); + } + } + + /** + * Construct the link filter + * + * @param string $tab + * @param int|string $curId + * + * @return int[]|string[] + */ + public function getLinkFilter(string $tab, int|string $curId): array + { + $mainTabList = $this->getView()->get('vv_sub_nav_attributes')['tabs']; + $fullModelsName = $tab; + $modelName = $tab; + // We have two use cases. The first one is for the Core models and the second one is for the + // plugins. In case we have a plugin we need to retrieve the name from the database + if(str_contains($tab, '.Plugin')) { + $modelName = $this->retrievePluginName($tab, (int)$curId); + $this->setPluginName($modelName); + $fullModelsName = $modelName; + } else if (str_contains($tab, '.Hierarchy')) { + $modelName = $this->retrievePluginName($tab, (int)$curId); + [$plugin, ] = explode('.', $modelName); + foreach ($this->getHasManyAssociationModels($modelName) as $association) { + $fullModelsName = $association; + break; + } + } else if(str_contains($tab, '@action')) { + [$modelName, ] = explode('@', $tab); + $fullModelsName = $modelName; + } else if(str_contains($tab, '.')) { + [$plugin, $modelName] = explode('.', $tab); + } + + $modelsTable = TableRegistry::getTableLocator()->get($fullModelsName); + $primary_link_list = $modelsTable->getPrimaryLinks(); + $primary_link = null; + if(count($primary_link_list) > 1) { + $primary_link = collection($primary_link_list) + ->filter(function($link) use ($mainTabList) { + $linkToClass = StringUtilities::foreignKeyToClassName($link); + return \in_array($linkToClass, $mainTabList, true); + })->first(); + } else if (\is_array($primary_link_list) && !empty($primary_link_list)) { + $primary_link = $primary_link_list[0]; + } + + $foreignKey = StringUtilities::classNameToForeignKey($modelName); + + // If the primary link is the co_id, it means this is a root element. As a result, we will construct + // the link filter key from the controller itself. If not, we will get the primary link directly. + return ($primary_link === 'co_id') + ? [$foreignKey => $curId] + : [$primary_link => $curId]; + } + + /** + * @return string|null + */ + public function getPluginName(): ?string + { + return $this->pluginName; + } + + /** + * @param string|null $pluginName + * + * @return void + */ + public function setPluginName(?string $pluginName): void + { + $this->pluginName = $pluginName; + } + + /** + * Get the plugin name from the database + * + * @param string $tab + * @param int $curId + * + * @return string + */ + public function retrievePluginName(string $tab, int $curId): string + { + // Get the name of the Core Model + [$coreModel, $dummy] = explode('.', $tab); + $ModelTable = TableRegistry::getTableLocator()->get($coreModel); + $response = $ModelTable + ->find() + ->select(['plugin']) + ->where(['id' => $curId]) + ->first(); + + return $response?->plugin; + } + + /** + * Get reference to Model Table + * + * @param string $modelsName + * + * @return Table + */ + public function getModelTableReference(string $modelsName): Table + { + return TableRegistry::getTableLocator()->get($modelsName); + } + + /** + * Select count(*) + * + * @param string $modelName Model name in `group_members` format + * @param array $whereClause where clause array + * + * @return int + */ + public function getModelTotalCount(string $modelName, array $whereClause): int + { + $modelsName = Inflector::camelize($modelName); + $ModelTable = TableRegistry::getTableLocator()->get($modelsName); + $count = $ModelTable->find() + ->where($whereClause) + ->count(); + + return $count; + } + + /** + * Get Person Status by ID + * @param int $personId + * + * @return string + */ + public function getPersonStatus(int $personId): string + { + $peopleTable = TableRegistry::getTableLocator()->get('people'); + $response = $peopleTable + ->find() + ->select(['status']) + ->where(['id' => $personId]) + ->first(); + + return $response?->status; + } + + /** + * @return string|null + */ + public function getAssociation(): ?string + { + return $this->association; + } + + /** + * @param string|null $association + * + * @return void + */ + public function setAssociation(?string $association): void + { + $this->association = $association; + } +} \ No newline at end of file diff --git a/app/templates/ExternalIdentitySources/fields-nav.inc b/app/templates/ExternalIdentitySources/fields-nav.inc index 9c6109179..c8b8d351b 100644 --- a/app/templates/ExternalIdentitySources/fields-nav.inc +++ b/app/templates/ExternalIdentitySources/fields-nav.inc @@ -37,8 +37,3 @@ $topLinks = [ 'class' => '' ] ]; - -$subnav = [ - 'name' => 'plugin', - 'active' => 'properties' -]; \ No newline at end of file diff --git a/app/templates/ExternalIdentitySources/retrieve.php b/app/templates/ExternalIdentitySources/retrieve.php index b7c0cca94..464aff0a6 100644 --- a/app/templates/ExternalIdentitySources/retrieve.php +++ b/app/templates/ExternalIdentitySources/retrieve.php @@ -25,16 +25,10 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -use \Cake\Utility\Inflector; +declare(strict_types = 1); - // Include the plugin subnavigation - $subnav = [ - 'name' => 'plugin', - 'active' => 'search' - ]; +use \Cake\Utility\Inflector; - // Generate the subnavigation title and tabs - print $this->element('subnavigation', $subnav); ?>