Skip to content

Commit

Permalink
Improve breadcrumb parent links. Fix Group Membership breadcrumb in t…
Browse files Browse the repository at this point in the history
…he context of person_id.
  • Loading branch information
Ioannis committed Apr 19, 2026
1 parent f554817 commit 128449e
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 23 deletions.
2 changes: 2 additions & 0 deletions app/resources/locales/en_US/operation.po
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,5 @@ msgstr "View Petition {0}"
msgid "view.ExternalIdentityRoles.a"
msgstr "View Role {0}"

msgid "person.memberships"
msgstr "My Memberships"
126 changes: 107 additions & 19 deletions app/src/Controller/Component/BreadcrumbComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions app/src/Controller/GroupMembersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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);
}

Expand Down
8 changes: 4 additions & 4 deletions app/src/Lib/Util/StringUtilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -628,8 +628,8 @@ public static function urlbase64decode(string $s): string {
array("+", "/", "="),
$s))
: "";
}
}

/**
* base64 encode a string.
*
Expand Down

0 comments on commit 128449e

Please sign in to comment.