From 18c6edd6c2d68e44352b4e0856bba9ce08baa8ec Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Wed, 8 Apr 2026 13:52:43 +0300 Subject: [PATCH] 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 e34ca74ea..ce9ebf7e3 100644 --- a/app/src/Controller/Component/BreadcrumbComponent.php +++ b/app/src/Controller/Component/BreadcrumbComponent.php @@ -117,9 +117,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 } @@ -297,25 +297,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. *