Skip to content

CFM-511/CMF-274 Tab navigation resolves wrong ID for plugin model index links/Breadcrumb fixes #385

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
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 @@ -404,4 +404,3 @@ msgstr "View Petition {0}"

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

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
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
43 changes: 40 additions & 3 deletions app/src/Lib/Util/StringUtilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

namespace App\Lib\Util;

use Cake\Core\Plugin;
use Cake\ORM\TableRegistry;
use \Cake\Utility\Inflector;

Expand Down Expand Up @@ -100,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 @@ -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.
*
Expand Down Expand Up @@ -591,8 +628,8 @@ public static function urlbase64decode(string $s): string {
array("+", "/", "="),
$s))
: "";
}
}

/**
* base64 encode a string.
*
Expand Down
5 changes: 3 additions & 2 deletions app/src/Lib/Util/TableUtilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion app/src/View/Helper/FieldHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading