Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/resources/locales/en_US/menu.po
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ msgstr "All Features"
msgid "co.groups"
msgstr "Groups"

msgid "co.groups.memberships"
msgstr "Group Memberships"

msgid "co.operations"
msgstr "Operations"

Expand Down
1 change: 0 additions & 1 deletion app/resources/locales/en_US/operation.po
Original file line number Diff line number Diff line change
Expand Up @@ -407,4 +407,3 @@ msgstr "View Petition {0}"

msgid "view.ExternalIdentityRoles.a"
msgstr "View Role {0}"

127 changes: 108 additions & 19 deletions app/src/Controller/Component/BreadcrumbComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ 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
$requesterModel = StringUtilities::getQualifiedName($request->getParam('plugin'), $request->getParam('controller'));
$modelName = StringUtilities::foreignKeyToQualifiedModelName($queryParam, $requesterModel);

$this->injectPrimaryLink($link, true, null, $modelName);
break; // Only inject the first matching parameter
}
Expand Down Expand Up @@ -299,25 +300,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
3 changes: 2 additions & 1 deletion app/src/Controller/Component/RegistryAuthComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
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 (
$this->getRequest()->getParam('action') === 'index'
&& $this->getRequest()->getQuery('person_id') !== null
) {
$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', []);
}

return parent::beforeRender($event);
}

Expand Down
Loading