From 0bee7c06688ca982cbe4702a9db5efc778c83cd0 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Mon, 6 Apr 2026 14:40:37 +0300 Subject: [PATCH 1/5] Fix incorrect tab navigation link ID for plugin model index views --- app/src/Lib/Util/StringUtilities.php | 37 ++++++++++++++++++++++++++++ app/src/Lib/Util/TableUtilities.php | 5 ++-- app/src/View/Helper/TabHelper.php | 14 ++++++++--- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php index c7cce8012..bbed926cf 100644 --- a/app/src/Lib/Util/StringUtilities.php +++ b/app/src/Lib/Util/StringUtilities.php @@ -29,6 +29,7 @@ namespace App\Lib\Util; +use Cake\Core\Plugin; use Cake\ORM\TableRegistry; use \Cake\Utility\Inflector; @@ -495,6 +496,42 @@ public static function pluginModel(string $s): string { return $s; } + + /** + * Convert a foreign key into the full qualified (plugin) model name (eg: Reports.Report). + * + * @param string $foreignKey Foreign key name (eg: report_id) + * @return string Pluralized plugin model name. + * @since COmanage Registry v5.2.0 + */ + public static function foreignKeyToQualifiedModelName(string $foreignKey): string { + $primaryLinkModelName = StringUtilities::foreignKeyToClassName($foreignKey); + + return self::modelNameToQualifiedModelName($primaryLinkModelName); + } + + + /** + * Determine the qualified model name for a given model, taking into account plugins. + * + * @param string $modelName Model name to qualify with plugin if applicable. + * @return string Fully qualified plugin name or base model name if no plugin. + * @since COmanage Registry v5.2.0 + */ + public static function modelNameToQualifiedModelName(string $modelName): string { + $plugin = null; + if (!class_exists('App\\Model\\Table\\' . $modelName . 'Table')) { + foreach (Plugin::loaded() as $loadedPlugin) { + if (class_exists($loadedPlugin . '\\Model\\Table\\' . $modelName . 'Table')) { + $plugin = $loadedPlugin; + break; + } + } + } + + return StringUtilities::qualifyModelPath($modelName, $plugin); + } + /** * Determine the plugin component of a Plugin path. * diff --git a/app/src/Lib/Util/TableUtilities.php b/app/src/Lib/Util/TableUtilities.php index 66165888c..e1b9be4e0 100644 --- a/app/src/Lib/Util/TableUtilities.php +++ b/app/src/Lib/Util/TableUtilities.php @@ -230,7 +230,8 @@ public static function treeTraversalFromPrimaryLink( $collection = $db->getSchemaCollection(); $listOfTables = $collection->listTables(); - $primaryLinkModelName = StringUtilities::foreignKeyToClassName(($primaryLinkKey)); + $primaryLinkModelName = StringUtilities::foreignKeyToQualifiedModelName($primaryLinkKey); + // Check if the table exists. // We can not handle @@ -259,7 +260,7 @@ public static function treeTraversalFromPrimaryLink( && $col !== $primaryLinkKey && str_ends_with($col, '_id') ) { - $fkModel = StringUtilities::foreignKeyToClassName(($col)); + $fkModel = StringUtilities::foreignKeyToQualifiedModelName(($col)); $fk_table = Inflector::underscore($fkModel); if (\in_array($fk_table, $listOfTables, true)) { self::treeTraversalFromPrimaryLink($col, $val, $results); diff --git a/app/src/View/Helper/TabHelper.php b/app/src/View/Helper/TabHelper.php index 9ba4d85de..5bef2078f 100644 --- a/app/src/View/Helper/TabHelper.php +++ b/app/src/View/Helper/TabHelper.php @@ -119,7 +119,8 @@ public function constructLinkUrl(string $tab, string|int $curId, bool $isNested } // I will get the id from the associated ids table - $url[] = $vv_associated_ids[$controller]; + $modelPath = StringUtilities::qualifyModelPath($controller, $plugin); + $url[] = $vv_associated_ids[$modelPath]; } else { $url[] = $curId; } @@ -145,7 +146,12 @@ public function getDeepNestedId(array $linkFilter): ?int // Generate the ModelName and instantiate the linked Table $modelName = StringUtilities::foreignKeyToClassName($linkFilterForeignKey); $table = TableRegistry::getTableLocator()->get($modelName); - $linkFilterId = $vv_associated_ids[Inflector::pluralize($modelName)] ?? null; + + $pluralModelName = Inflector::pluralize($modelName); + $linkFilterId = $vv_associated_ids[$pluralModelName] + ?? current(array_filter($vv_associated_ids, fn($k) => str_ends_with($k, '.' . $pluralModelName), ARRAY_FILTER_USE_KEY)) + ?: null; + if($linkFilterId !== null) { return (int)$linkFilterId; } @@ -253,7 +259,7 @@ public function getCurrentId(string $tabName = null, bool $isNested = false): in $vv_primary_link = $this->getView()->get('vv_primary_link'); $vv_bc_title_links = $this->getView()->get('vv_bc_title_links'); $request = $this->getView()->getRequest(); - $curController = $request->getParam('controller'); + $curController = StringUtilities::getQualifiedName($request->getParam('plugin'), $request->getParam('controller')); $vv_sub_nav_attributes = $this->getView()->get('vv_sub_nav_attributes'); $tab_actions = !$isNested ? $vv_sub_nav_attributes['action'] : $vv_sub_nav_attributes['nested']['action']; $tabs = !$isNested ? $vv_sub_nav_attributes['tabs'] : $vv_sub_nav_attributes['nested']['tabs']; @@ -265,7 +271,7 @@ public function getCurrentId(string $tabName = null, bool $isNested = false): in // Get the ids of all the associated Model records $results = []; if ($request->getQuery($vv_primary_link) !== null) { - TableUtilities::treeTraversalFromPrimaryLink($vv_primary_link, (int)$tid, $results, ); + TableUtilities::treeTraversalFromPrimaryLink($vv_primary_link, (int)$tid, $results); } else { TableUtilities::treeTraversalFromId($curController, (int)$tid, $results); } From f554817430a38bdc67513095de1468295ca31f24 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 7 Apr 2026 17:07:19 +0300 Subject: [PATCH 2/5] TabHelper::getDeepNestedId() fixes --- app/src/View/Helper/TabHelper.php | 77 +++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/app/src/View/Helper/TabHelper.php b/app/src/View/Helper/TabHelper.php index 5bef2078f..c3f8db031 100644 --- a/app/src/View/Helper/TabHelper.php +++ b/app/src/View/Helper/TabHelper.php @@ -139,41 +139,68 @@ public function constructLinkUrl(string $tab, string|int $curId, bool $isNested */ public function getDeepNestedId(array $linkFilter): ?int { + // Retrieve associated IDs for the current view $vv_associated_ids = $this->getView()->get('vv_associated_ids'); - // Get the foreign from the linkFilter + // Extract the first key from the link filter array (foreign key) $linkFilterForeignKey = array_key_first($linkFilter); - // Generate the ModelName and instantiate the linked Table - $modelName = StringUtilities::foreignKeyToClassName($linkFilterForeignKey); - $table = TableRegistry::getTableLocator()->get($modelName); - $pluralModelName = Inflector::pluralize($modelName); - $linkFilterId = $vv_associated_ids[$pluralModelName] - ?? current(array_filter($vv_associated_ids, fn($k) => str_ends_with($k, '.' . $pluralModelName), ARRAY_FILTER_USE_KEY)) + // Derive the model name and qualified model name from the foreign key + $modelName = StringUtilities::foreignKeyToClassName($linkFilterForeignKey); + $qualifiedModelName = StringUtilities::modelNameToQualifiedModelName($modelName); + $table = TableRegistry::getTableLocator()->get($qualifiedModelName); + + // Attempt to retrieve the ID from associated IDs using different possible keys + $linkFilterId = + $vv_associated_ids[$qualifiedModelName] + ?? $vv_associated_ids[$modelName] + ?? current(array_filter($vv_associated_ids, fn($k) => str_ends_with($k, '.' . $modelName), ARRAY_FILTER_USE_KEY)) ?: null; - if($linkFilterId !== null) { + // If an ID is found, return it as an integer + if ($linkFilterId !== null) { return (int)$linkFilterId; } - $foreignKeyId = -1; + + // Initialize variables to track foreign key and its ID + $foreignKeyId = null; $foreignKey = null; - // This means that we are working on deep nested associations and we need - // to fetch more data - $linkFilterSchema = $table->getSchema(); - foreach($linkFilterSchema->columns() as $column) { - // Check the foreign keys - if(str_ends_with($column, '_id')) { - $foreignKeytToTableName = Inflector::pluralize(StringUtilities::foreignKeyToClassName($column)); - if(isset($vv_associated_ids[$foreignKeytToTableName])) { - $foreignKeyId = $vv_associated_ids[$foreignKeytToTableName]; - $foreignKey = $column; - break; - } + + // Iterate through all schema columns to look for a valid foreign key + foreach ($table->getSchema()->columns() as $column) { + // Skip columns that do not end with '_id' + if (!str_ends_with($column, '_id')) { + continue; + } + + // Convert the foreign key column to model names + $fkModelName = StringUtilities::foreignKeyToClassName($column); + $fkQualifiedModelName = StringUtilities::modelNameToQualifiedModelName($fkModelName); + + // Check for a match in associated IDs + $candidateId = + $vv_associated_ids[$fkQualifiedModelName] + ?? $vv_associated_ids[$fkModelName] + ?? null; + + // If a match is found, set the foreign key ID and column, and break the loop + if ($candidateId !== null) { + $foreignKeyId = (int)$candidateId; + $foreignKey = $column; + break; } } - $id = $table->find()->where([$foreignKey => $foreignKeyId])->first()->id; - return(int)$id; + // If no valid foreign key or ID was found, return null + if ($foreignKey === null || $foreignKeyId === null) { + return null; + } + + // Query the database for the associated row using the foreign key + $row = $table->find()->where([$foreignKey => $foreignKeyId])->first(); + + // Return the ID from the row if found or null otherwise + return $row ? (int)$row->id : null; } /** @@ -398,7 +425,7 @@ public function getLinkFilter( [$plugin, $modelName] = explode('.', $tab); } - $modelsTable = TableRegistry::getTableLocator()->get($fullModelsName); + $modelsTable = TableRegistry::getTableLocator()->get(StringUtilities::modelNameToQualifiedModelName($fullModelsName)); $primary_link_list = $modelsTable->getPrimaryLinks(); $primary_link = null; if(count($primary_link_list) > 1) { @@ -519,7 +546,7 @@ public function setPluginName(?string $pluginName): void public function retrievePluginName(string $tab, int $curId): string { // Get the name of the Core Model - [$coreModel, $dummy] = explode('.', $tab); + $coreModel = substr($tab, 0, strrpos($tab, '.')); $ModelTable = TableRegistry::getTableLocator()->get($coreModel); $response = $ModelTable ->find() From 128449e0567becf95cacb08088631d2345d6817a Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Wed, 8 Apr 2026 13:52:43 +0300 Subject: [PATCH 3/5] Improve breadcrumb parent links. Fix Group Membership breadcrumb in the context of person_id. --- app/resources/locales/en_US/operation.po | 2 + .../Component/BreadcrumbComponent.php | 126 +++++++++++++++--- app/src/Controller/GroupMembersController.php | 21 +++ app/src/Lib/Util/StringUtilities.php | 8 +- 4 files changed, 134 insertions(+), 23 deletions(-) diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po index 098a9b1d4..8d2d8c2d7 100644 --- a/app/resources/locales/en_US/operation.po +++ b/app/resources/locales/en_US/operation.po @@ -405,3 +405,5 @@ msgstr "View Petition {0}" msgid "view.ExternalIdentityRoles.a" msgstr "View Role {0}" +msgid "person.memberships" +msgstr "My Memberships" \ No newline at end of file diff --git a/app/src/Controller/Component/BreadcrumbComponent.php b/app/src/Controller/Component/BreadcrumbComponent.php index 503866a86..257b6ea50 100644 --- a/app/src/Controller/Component/BreadcrumbComponent.php +++ b/app/src/Controller/Component/BreadcrumbComponent.php @@ -119,9 +119,9 @@ public function beforeRender(EventInterface $event) { 'co_id' => method_exists($controller, 'getCOID') ? $controller->getCOID() : null ]; - // Force the component to use the model derived from the query param (e.g. People) - // instead of the global page model (e.g. Cos). - $modelName = StringUtilities::foreignKeyToClassName($queryParam); + // Use fully-qualified model name (handles plugin tables) + $modelName = StringUtilities::foreignKeyToQualifiedModelName($queryParam); + $this->injectPrimaryLink($link, true, null, $modelName); break; // Only inject the first matching parameter } @@ -299,25 +299,113 @@ public function injectPrimaryLink( ->contain($contain) ->firstOrFail(); - // Optional parent index breadcrumb + // Optional parent breadcrumb(s) + // This block tries to add navigation "above" the current linked entity, when the + // linked table implements findPrimaryLink(). + // + // Example chain (plugin config entity): + // LdapConnector.LdapProvisioners(id=18) has primary link provisioning_target_id=19 + // Desired breadcrumbs: + // Provisioning Targets -> /provisioning-targets?co_id=2 (index/list) + // LDEV LDAP Provisioner -> /provisioning-targets/edit/19 (specific parent entity) + // + // We intentionally generate two crumbs because the label and the target differ: + // - plural label should go to index/list + // - specific display label should go to edit/view of the parent record if ($index && method_exists($linkTable, 'findPrimaryLink')) { $parentLink = $linkTable->findPrimaryLink($linkedEntity->id); - $this->injectParents[strtolower($linkModelFqn) . ':index'] = [ - 'target' => [ - 'plugin' => $parentLink->plugin ?? StringUtilities::blankToNull(StringUtilities::pluginPlugin($linkModelFqn)) ?? null, - 'controller' => StringUtilities::pluginModel($linkModelFqn), - 'action' => 'index', - '?' => [ - $parentLink->attr => $parentLink->value - ] - ], - 'label' => StringUtilities::localizeController( - controllerName: $linkModelFqn, - pluginName: $link->plugin ?? null, - plural: true - ) - ]; + // CASE A: Parent link is NOT co_id (ie: parent is another entity, via an FK like provisioning_target_id). + // We treat this as a normal parent object relationship and generate: + // 1) parent index crumb (plural label) + // 2) parent entity crumb (display value label) + if (!empty($parentLink->attr) && $parentLink->attr !== 'co_id') { + // Derive the parent controller/table alias from the FK: + // provisioning_target_id -> ProvisioningTargets + $parentController = StringUtilities::foreignKeyToClassName($parentLink->attr); + $parentTable = \Cake\ORM\TableRegistry::getTableLocator()->get($parentController); + + // A1) Parent INDEX crumb (plural label) + // Example: + // "Provisioning Targets" -> /provisioning-targets?co_id=2 + $parentIndexTarget = [ + 'plugin' => null, + 'controller' => $parentController, + 'action' => 'index', + ]; + + // If we know the CO context, keep it on the index URL so the list doesn't jump COs. + // Example: + // /provisioning-targets?co_id=2 + if (!empty($parentLink->co_id)) { + $parentIndexTarget['?'] = ['co_id' => (int)$parentLink->co_id]; + } + + $this->injectParents[strtolower($parentController) . ':index'] = [ + 'target' => $parentIndexTarget, + 'label' => StringUtilities::localizeController( + controllerName: $parentController, + pluginName: null, + plural: true + ) + ]; + + // A2) Parent ENTITY crumb (specific display label) + // Example: + // "LDEV LDAP Provisioner" -> /provisioning-targets/edit/19 + $parentEntity = $parentTable->get((int)$parentLink->value); + + // Prefer a table-provided display generator when available (lets tables compute a friendly label) + // Fallback to Cake's displayField, then to the raw ID. + $parentDisplay = null; + + if (method_exists($parentTable, 'generateDisplayField')) { + $parentDisplay = $parentTable->generateDisplayField($parentEntity); + } + + if ($parentDisplay === null) { + $df = $parentTable->getDisplayField(); + $parentDisplay = $parentEntity->$df ?? null; + } + + if ($parentDisplay === null) { + $parentDisplay = (string)$parentLink->value; + } + + $this->injectParents[strtolower($parentController) . ':entity'] = [ + 'target' => [ + 'plugin' => null, + 'controller' => $parentController, + 'action' => 'edit', + (int)$parentLink->value + ], + 'label' => (string)$parentDisplay + ]; + } + // CASE B: Parent link is co_id (ie: this entity is rooted directly at the CO). + // In this case there isn't a meaningful parent entity page to link to; the "parent" + // is the CO context, and the most helpful breadcrumb is the linked model's index + // filtered by co_id (the standard list view in that CO). + // + // Example: + // "Email Addresses" -> /email-addresses?co_id=2 + else { + $this->injectParents[strtolower($linkModelFqn) . ':index'] = [ + 'target' => [ + 'plugin' => $parentLink->plugin ?? StringUtilities::blankToNull(StringUtilities::pluginPlugin($linkModelFqn)) ?? null, + 'controller' => StringUtilities::pluginModel($linkModelFqn), + 'action' => 'index', + '?' => [ + $parentLink->attr => $parentLink->value + ] + ], + 'label' => StringUtilities::localizeController( + controllerName: $linkModelFqn, + pluginName: $link->plugin ?? null, + plural: true + ) + ]; + } } // Determine target action for entity link diff --git a/app/src/Controller/GroupMembersController.php b/app/src/Controller/GroupMembersController.php index 30b4ae966..4655beb4f 100644 --- a/app/src/Controller/GroupMembersController.php +++ b/app/src/Controller/GroupMembersController.php @@ -42,6 +42,16 @@ class GroupMembersController extends StandardController { ] ]; + public function initialize(): void + { + parent::initialize(); + + // Build breadcrumb chain from query context: ?person_id=... + $this->Breadcrumb->configureQueryPrimaryLinks([ + 'index' => ['person_id'] + ]); + } + /** * Callback run prior to the request render. * @@ -64,6 +74,17 @@ public function beforeRender(EventInterface $event) { $this->set('vv_bc_parent_primarykey', $this->GroupMembers->$model->getPrimaryKey()); } + // If we're in a Person context (index filtered by ?person_id=...), override the page title. + if ( + in_array($this->getRequest()->getParam('action'), ['index'], true) + && $this->getRequest()->getQuery('person_id') !== null + ) { + $this->set('vv_title', __d('operation', 'person.memberships')); + + // Ensure the breadcrumb leaf is plain text (no extra title-links). + $this->set('vv_bc_title_links', []); + } + return parent::beforeRender($event); } diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php index bbed926cf..29a094227 100644 --- a/app/src/Lib/Util/StringUtilities.php +++ b/app/src/Lib/Util/StringUtilities.php @@ -101,7 +101,7 @@ public static function classNameToForeignKey(string $className): string { */ public static function columnKey( - string $modelsName, + string $modelsName, string $c, ?\DateTimeZone $tz=null, bool $useCustomClMdlLabel=false @@ -506,7 +506,7 @@ public static function pluginModel(string $s): string { */ public static function foreignKeyToQualifiedModelName(string $foreignKey): string { $primaryLinkModelName = StringUtilities::foreignKeyToClassName($foreignKey); - + return self::modelNameToQualifiedModelName($primaryLinkModelName); } @@ -628,8 +628,8 @@ public static function urlbase64decode(string $s): string { array("+", "/", "="), $s)) : ""; - } - + } + /** * base64 encode a string. * From da2f1942a18cb30c751c56095339e4b5a87b6977 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Thu, 9 Apr 2026 12:49:25 +0300 Subject: [PATCH 4/5] Improve Tab link and names/titles construction --- .../Component/RegistryAuthComponent.php | 3 +- app/src/View/Helper/FieldHelper.php | 2 +- app/src/View/Helper/TabHelper.php | 42 +++++++++++++++---- app/templates/element/form/listItem.php | 14 +++++-- .../element/subnavigation/supertitle.php | 4 +- .../element/subnavigation/tabTitle.php | 6 ++- 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/app/src/Controller/Component/RegistryAuthComponent.php b/app/src/Controller/Component/RegistryAuthComponent.php index 656f27689..603457ae2 100644 --- a/app/src/Controller/Component/RegistryAuthComponent.php +++ b/app/src/Controller/Component/RegistryAuthComponent.php @@ -411,7 +411,8 @@ protected function calculatePermissions(?int $id=null): array { ->applyOptions(['archived' => true]); // QueryModificationTrait - $getActionMethod = "get{$reqAction}Contains"; + $reqActionCapitlize = ucfirst($reqAction) ; + $getActionMethod = "get{$reqActionCapitlize}Contains"; if(method_exists($table, $getActionMethod) && $table->$getActionMethod()) { $query = $query->contain($table->$getActionMethod()); } diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index d4f332a8c..8cbd50898 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -428,7 +428,7 @@ public function formField(string $fieldName, string $fieldType = null, array $fieldSelectOptions = null, string $fieldNameAlias = null, - bool $labelIsTextOnly = null): string + bool $labelIsTextOnly = null): string { $fieldArgs = $fieldOptions ?? []; $fieldArgs['label'] = $fieldOptions['label'] ?? false; diff --git a/app/src/View/Helper/TabHelper.php b/app/src/View/Helper/TabHelper.php index c3f8db031..b692b8004 100644 --- a/app/src/View/Helper/TabHelper.php +++ b/app/src/View/Helper/TabHelper.php @@ -78,6 +78,9 @@ public function constructLinkUrl(string $tab, string|int $curId, bool $isNested if(str_ends_with($tab, '.Plugin')) { // This is always the second tab of the plugin and it is configuration $controller = $curController; + $controllerQualifiedName = StringUtilities::modelNameToQualifiedModelName($controller); + $plugin = StringUtilities::pluginPlugin($controllerQualifiedName); + $plugin = !empty($plugin) ? $plugin : null; $action = 'configure'; } else if (str_ends_with($tab, '.Hierarchy')) { $modelName = $this->retrievePluginName($tab, (int)$curId); @@ -236,7 +239,7 @@ public function getLinkClass(string $tab, bool $isNested = false, array $nesting // Always mark active the parent Tab !$isNested && $parentModelForNested !== null && $tab === $parentModelForNested && in_array($fullModelName, $nestings), // Match Configuration and Hierarchy tabs - isset($plugin) && str_contains($tab, '.Plugin') && $curAction === 'edit', + isset($plugin) && str_contains($tab, '.Plugin') && $curAction === 'configure', isset($plugin) && str_contains($tab, '.Hierarchy') && $curAction === 'index', // Matches the action tab links, e.g. FileSource/search $tab === "{$curController}@action.{$curAction}" => 'nav-link active', @@ -547,14 +550,37 @@ public function retrievePluginName(string $tab, int $curId): string { // Get the name of the Core Model $coreModel = substr($tab, 0, strrpos($tab, '.')); - $ModelTable = TableRegistry::getTableLocator()->get($coreModel); - $response = $ModelTable - ->find() - ->select(['plugin']) - ->where(['id' => $curId]) - ->first(); - return $response?->plugin; + // Ensure we resolve plugin tables correctly + $qualifiedCoreModel = StringUtilities::modelNameToQualifiedModelName($coreModel); + $ModelTable = TableRegistry::getTableLocator()->get($qualifiedCoreModel); + + // If this table actually has a "plugin" column, fetch the plugin model path from the record. + // Example (pluggable wrapper tables): + // ProvisioningTargets.plugin = "LdapConnector.LdapProvisioners" + if ($ModelTable->getSchema()->hasColumn('plugin')) { + $response = $ModelTable + ->find() + ->select(['plugin']) + ->where(['id' => $curId]) + ->first(); + + return (string)($response?->plugin ?? ''); + } + + // Otherwise, this isn't a pluggable wrapper record (no plugin column). + // Fall back to the table's registry alias, which is already in Plugin.Model form + // for plugin tables. + // + // Example: + // Table registry alias: "LdapConnector.PersonSchemas" + // This avoids querying a non-existent "plugin" column (eg person_schemas.plugin). + if (method_exists($ModelTable, 'getRegistryAlias')) { + return (string)$ModelTable->getRegistryAlias(); + } + + // Last resort: return the qualified name we used to locate the table. + return (string)$qualifiedCoreModel; } /** diff --git a/app/templates/element/form/listItem.php b/app/templates/element/form/listItem.php index 9357d3a46..50d7aa60d 100644 --- a/app/templates/element/form/listItem.php +++ b/app/templates/element/form/listItem.php @@ -34,18 +34,24 @@ // - add a prefix and create a namespace // - wrap them in an array. // We choose the latter. +use App\Lib\Util\StringUtilities; + $this->set('fieldName', $arguments['fieldName']); $fieldName = $arguments['fieldName']; $this->set('vv_field_arguments', $arguments); +$qualifiedModelName = StringUtilities::entityToPluginClassName($this->Field->getEntity()); +$plugin = StringUtilities::pluginPlugin($qualifiedModelName); +$controller = StringUtilities::pluginModel($qualifiedModelName); + // If an attribute is frozen, inject a special link to unfreeze it, since // the attribute is read-only and the admin can't simply uncheck the setting if($fieldName == 'frozen' && $this->Field->getEntity()->frozen) { $url = [ 'label' => __d('operation', 'unfreeze'), 'url' => [ - 'plugin' => null, - 'controller' => \App\Lib\Util\StringUtilities::entityToClassname($this->Field->getEntity()), + 'plugin' => !empty($plugin) ? $plugin : null, + 'controller' => $controller, 'action' => 'unfreeze', $this->Field->getEntity()->id ] @@ -63,8 +69,8 @@ $url = [ 'label' => __d('operation', 'configure.plugin'), 'url' => [ - 'plugin' => null, - 'controller' => \App\Lib\Util\StringUtilities::entityToClassname($this->Field->getEntity()), + 'plugin' => !empty($plugin) ? $plugin : null, + 'controller' => $controller, 'action' => 'configure', $this->Field->getEntity()->id ] diff --git a/app/templates/element/subnavigation/supertitle.php b/app/templates/element/subnavigation/supertitle.php index 71d6a4205..f08d75591 100644 --- a/app/templates/element/subnavigation/supertitle.php +++ b/app/templates/element/subnavigation/supertitle.php @@ -62,7 +62,7 @@ if ( (!empty($vv_obj) || !empty($$objectName)) && !empty($this->getPlugin()) - && $vv_subnavigation_tabs[0] !== StringUtilities::entityToClassName($vv_bc_parent_obj) + && $vv_subnavigation_tabs[0] !== $vv_bc_parent_obj->getSource() ) { $object = $vv_obj ?? $$objectName?->items()?->first(); if ($object === null) { @@ -73,7 +73,7 @@ // If we get here, it means that neither the request object nor its parent can give us a supertitle. // We need to fetch all the ids and get the supertitle from the root tab/node $results = []; - TableUtilities::treeTraversalFromId(StringUtilities::entityToClassName($object), (int)$object->id, $results); + TableUtilities::treeTraversalFromId($object->getSource(), (int)$object->id, $results); $superTitleModelReference = $this->Tab->getModelTableReference($vv_subnavigation_tabs[0]); $superTitleModelDisplayField = $superTitleModelReference->getDisplayField(); $superTitleModelId = $results[$vv_subnavigation_tabs[0]]; diff --git a/app/templates/element/subnavigation/tabTitle.php b/app/templates/element/subnavigation/tabTitle.php index f17c4b894..540a499ff 100644 --- a/app/templates/element/subnavigation/tabTitle.php +++ b/app/templates/element/subnavigation/tabTitle.php @@ -57,7 +57,11 @@ $tabToTableName = Inflector::tableize(Inflector::singularize($tab)); // Plugin Configuration Tab -if (str_contains($tab, '.') && in_array('edit', $navigation_action[$tab], true)) { +if ( + str_contains($tab, '.') + && in_array('edit', $navigation_action[$tab], true) + && array_search($tab, $vv_subnavigation_tabs, true) !== 0 +) { $title = __d('operation','configure.plugin'); } else if (str_contains($tab, '@action.')) { // Top Links/Actions [$modelName, ] = explode('@', $tab); From 71828202198bbdcfefa404e83810f5dc731d6786 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Fri, 10 Apr 2026 10:35:34 +0300 Subject: [PATCH 5/5] Fix Group Membership breadcrumb and subnav title for Person Canvas --- app/resources/locales/en_US/menu.po | 3 +++ app/resources/locales/en_US/operation.po | 3 --- app/src/Controller/GroupMembersController.php | 4 ++-- app/templates/GroupMembers/columns.inc | 10 ++++++++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/resources/locales/en_US/menu.po b/app/resources/locales/en_US/menu.po index ab4c885a1..0280bea78 100644 --- a/app/resources/locales/en_US/menu.po +++ b/app/resources/locales/en_US/menu.po @@ -90,6 +90,9 @@ msgstr "All Features" msgid "co.groups" msgstr "Groups" +msgid "co.groups.memberships" +msgstr "Group Memberships" + msgid "co.operations" msgstr "Operations" diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po index 8d2d8c2d7..65230a4ac 100644 --- a/app/resources/locales/en_US/operation.po +++ b/app/resources/locales/en_US/operation.po @@ -404,6 +404,3 @@ msgstr "View Petition {0}" msgid "view.ExternalIdentityRoles.a" msgstr "View Role {0}" - -msgid "person.memberships" -msgstr "My Memberships" \ No newline at end of file diff --git a/app/src/Controller/GroupMembersController.php b/app/src/Controller/GroupMembersController.php index 4655beb4f..0cad43037 100644 --- a/app/src/Controller/GroupMembersController.php +++ b/app/src/Controller/GroupMembersController.php @@ -76,10 +76,10 @@ public function beforeRender(EventInterface $event) { // If we're in a Person context (index filtered by ?person_id=...), override the page title. if ( - in_array($this->getRequest()->getParam('action'), ['index'], true) + $this->getRequest()->getParam('action') === 'index' && $this->getRequest()->getQuery('person_id') !== null ) { - $this->set('vv_title', __d('operation', 'person.memberships')); + $this->set('vv_title', __d('menu', 'co.groups.memberships')); // Ensure the breadcrumb leaf is plain text (no extra title-links). $this->set('vv_bc_title_links', []); diff --git a/app/templates/GroupMembers/columns.inc b/app/templates/GroupMembers/columns.inc index 6c40b226a..5dd5cdc2e 100644 --- a/app/templates/GroupMembers/columns.inc +++ b/app/templates/GroupMembers/columns.inc @@ -153,8 +153,14 @@ if ($this->request->getQuery('group_id') !== null) { ]; $subnav = 'people'; - $localTitle = __d('controller','Groups', 99); - + // If we're in a Person context (index filtered by ?person_id=...), do not override the subnav title. + if ( + $this->getRequest()->getParam('action') === 'index' + && empty($this->getRequest()->getQuery('person_id')) + ) { + $localTitle = __d('controller','Groups', 99); + } + // Do not show the normal add link here $suppressAddLink = true; ?>