diff --git a/app/availableplugins/PipelineToolkit/src/Model/Table/IdentifierMappersTable.php b/app/availableplugins/PipelineToolkit/src/Model/Table/IdentifierMappersTable.php index 5cb331933..80f494689 100644 --- a/app/availableplugins/PipelineToolkit/src/Model/Table/IdentifierMappersTable.php +++ b/app/availableplugins/PipelineToolkit/src/Model/Table/IdentifierMappersTable.php @@ -38,6 +38,7 @@ class IdentifierMappersTable extends Table { use \App\Lib\Traits\LabeledLogTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; @@ -68,6 +69,19 @@ public function initialize(array $config): void { $this->setPrimaryLink(['flange_id']); $this->setRequiresCO(true); + $this->setViewContains([ + 'Flanges', + ]); + + $this->setEditContains([ + 'Flanges', + ]); + + // Required for deep Breadcrumb calculations + $this->setIndexContains([ + 'Flanges', + ]); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) 'entity' => [ @@ -135,6 +149,17 @@ public function buildRelatedAttributes( return $retdata; } + /** + * Table specific logic to generate a display field. + * + * @since COmanage Registry v5.2.0 + * @param \PipelineToolkit\Model\Entity\IdentifierMapper $entity Entity to generate display field for + * @return string Display field + */ + public function generateDisplayField(\PipelineToolkit\Model\Entity\IdentifierMapper $entity): string { + return $entity->name ?? $entity->description ?? $entity->flange->description; + } + /** * Set validation rules. * diff --git a/app/availableplugins/PipelineToolkit/src/Model/Table/LoginIdentifierTypesTable.php b/app/availableplugins/PipelineToolkit/src/Model/Table/LoginIdentifierTypesTable.php index a32eded1d..2b0daa643 100644 --- a/app/availableplugins/PipelineToolkit/src/Model/Table/LoginIdentifierTypesTable.php +++ b/app/availableplugins/PipelineToolkit/src/Model/Table/LoginIdentifierTypesTable.php @@ -40,6 +40,7 @@ class LoginIdentifierTypesTable extends Table { use \App\Lib\Traits\LabeledLogTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; @@ -75,6 +76,14 @@ public function initialize(array $config): void { ] ]); + $this->setViewContains([ + 'IdentifierMappers' => ['Flanges'], + ]); + + $this->setEditContains([ + 'IdentifierMappers' => ['Flanges'], + ]); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) 'entity' => [ @@ -109,6 +118,17 @@ public function buildRules(RulesChecker $rules): RulesChecker { return $rules; } + /** + * Table specific logic to generate a display field. + * + * @since COmanage Registry v5.2.0 + * @param \PipelineToolkit\Model\Entity\LoginIdentifierType $entity Entity to generate display field for + * @return string Display field + */ + public function generateDisplayField(\PipelineToolkit\Model\Entity\LoginIdentifierType $entity): string { + return __d('pipeline_toolkit', 'controller.LoginIdentifierTypes', [1]); + } + /** * Application Rule to determine if a specific Type is already configured. * diff --git a/app/availableplugins/SqlConnector/src/Model/Table/SqlSourcesTable.php b/app/availableplugins/SqlConnector/src/Model/Table/SqlSourcesTable.php index 9d71f7d51..a311196e1 100644 --- a/app/availableplugins/SqlConnector/src/Model/Table/SqlSourcesTable.php +++ b/app/availableplugins/SqlConnector/src/Model/Table/SqlSourcesTable.php @@ -249,7 +249,8 @@ protected function getChanges( * @return string Display field */ public function generateDisplayField(\SqlConnector\Model\Entity\SqlSource $entity): string { - return __d('sql_connector', 'display.SqlSource', [$entity->external_identity_source->description]); + $value = $entity->name ?? $entity->description ?? $entity->external_identity_source->description; + return __d('sql_connector', 'display.SqlSource', [$value]); } diff --git a/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php b/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php index 398a231eb..2eec1c233 100644 --- a/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php +++ b/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php @@ -173,7 +173,8 @@ public function initialize(array $config): void { * @return string Display field */ public function generateDisplayField(\EnvSource\Model\Entity\EnvSource $entity): string { - return __d('env_source', 'display.EnvSource', [$entity->external_identity_source->description]); + $value = $entity->name ?? $entity->description ?? $entity->external_identity_source->description; + return __d('env_source', 'display.EnvSource', [$value]); } /** diff --git a/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php b/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php index 1a6978365..979ac48ae 100644 --- a/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php +++ b/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php @@ -106,7 +106,7 @@ public function initialize(array $config): void { * @return string Display field */ public function generateDisplayField(\SshKeyAuthenticator\Model\Entity\SshKeyAuthenticator $entity): string { - return $entity->authenticator->description; + return $entity->name ?? $entity->description ?? $entity->authenticator->description; } /** diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index b4549e98a..ce8781e9c 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -36,12 +36,12 @@ use App\Lib\Util\StringUtilities; use Cake\Controller\Controller; use Cake\Datasource\Exception\RecordNotFoundException; -use Cake\Http\Exception\UnauthorizedException; use Cake\Event\EventManager; +use Cake\Http\Exception\UnauthorizedException; use Cake\ORM\TableRegistry; +use Cake\Routing\Router; use Cake\Utility\Hash; - /** * @property \App\Controller\Component\RegistryAuthComponent $RegistryAuth * @property \App\Controller\Component\BreadcrumbComponent $Breadcrumb @@ -273,16 +273,98 @@ public function getCOID(): ?int { */ public function getCurrentTable(): \Cake\ORM\Table { - /** @var string $modelsName */ - $modelsName = $this->getName(); + return $this->fetchTable($this->getQualifiedName()); + } + + /** + * Get the fully-qualified controller/model name with plugin prefix when present. + * + * Examples: + * - Core controller "Users" => "Users" + * - Plugin controller "MyPlugin.Users" => "MyPlugin.Users" + * + * @since COmanage Registry v5.2.0 + * @return string + */ + public function getQualifiedName(): string + { + $name = $this->getName(); + $plugin = $this->getPlugin(); + + return $plugin !== null && $plugin !== '' + ? $plugin . '.' . $name + : $name; + } - $alias = $this->getPlugin() !== null - ? $this->getPlugin() . '.' . $modelsName - : $modelsName; - return $this->fetchTable($alias); + /** + * Check if a given route/URL target is valid and can be resolved. + * + * @param array $target Route parameters to validate + * @return bool True if route is valid and can be resolved + * @since COmanage Registry v5.2.0 + */ + public function isValidRoute(array $target): bool + { + try { + // Normalize plugin nulls and query args + $params = $target; + if (array_key_exists('plugin', $params) && $params['plugin'] === null) { + unset($params['plugin']); + } + if (isset($params['?'])) { + $params['query'] = $params['?']; + unset($params['?']); + } + + Router::url($params, true); // will throw if unresolvable + return true; + } catch (\Throwable $e) { + return false; + } } + /** + * Check if target corresponds to an active application route (is routable). + * + * @param array $target e.g. ['plugin'=>'PipelineToolkit','controller'=>'IdentifierMappers','action'=>'index','?'=>['flange_id'=>3]] + * @return bool + * @since COmanage Registry v5.2.0 + */ + public function isActiveRoute(array $target): bool + { + try { + // Normalize params for Router::match + $params = $target; + + // Query params should not affect route matching + if (isset($params['?'])) { + unset($params['?']); + } + if (isset($params['query'])) { + unset($params['query']); + } + + // Default request method and extension are not required for match() + // Build URL first to ensure consistency (optional) + Router::url($target, false); + + // Router::match expects a request-style array; provide defaults + $params += [ + 'plugin' => $params['plugin'] ?? null, + 'prefix' => $params['prefix'] ?? null, + 'controller' => $params['controller'] ?? null, + 'action' => $params['action'] ?? null, + 'pass' => array_values(array_filter($params, 'is_int', ARRAY_FILTER_USE_KEY)) ?: [], + ]; + + // If a matching route exists, match() returns an array; otherwise it throws + Router::getRouteCollection()->match($params, []); + return true; + } catch (\Throwable $e) { + return false; + } + } /** * @param string $potentialPrimaryLink diff --git a/app/src/Controller/Component/BreadcrumbComponent.php b/app/src/Controller/Component/BreadcrumbComponent.php index 71bf699c5..c4a9e437d 100644 --- a/app/src/Controller/Component/BreadcrumbComponent.php +++ b/app/src/Controller/Component/BreadcrumbComponent.php @@ -31,8 +31,7 @@ use \Cake\Controller\Component; use \Cake\Event\EventInterface; -use \Cake\ORM\TableRegistry; -use \Cake\Utility\Inflector; +use \Cake\Routing\Router; use \App\Lib\Util\StringUtilities; class BreadcrumbComponent extends Component { @@ -57,6 +56,9 @@ class BreadcrumbComponent extends Component { protected $skipConfigPaths = []; // Don't render the parent links protected $skipParentPaths = []; + // Skip target index/entity links for paths where the target has no view + protected $skipNoViewPaths = []; + // Inject parent links (these render before the index link, if set) // The parent links are constructed as part of the injectPrimaryLink function, as well as in the StandardController and // in the StandardPluginController, MVEAController, ProvisioningHistoryRecordController, etc. These controllers are @@ -250,21 +252,41 @@ public function injectPrimaryLink(object $link, bool $index = true, string $link 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 - ) + $targetIndexRoute = [ + 'plugin' => $parentLink->plugin ?? StringUtilities::blankToNull(StringUtilities::pluginPlugin($linkModelFqn)) ?? null, + 'controller' => StringUtilities::pluginModel($linkModelFqn), + 'action' => 'index', + '?' => [ + $parentLink->attr => $parentLink->value + ] ]; + + // Build absolute URL string for regex matching against injected route + $targetIndexUrl = Router::url($targetIndexRoute, false); + + // If the injected route matches any configured "no view" patterns, skip injecting it + $skipInjectedIndex = false; + foreach ($this->skipNoViewPaths as $pattern) { + if (preg_match($pattern, $targetIndexUrl)) { + $skipInjectedIndex = true; + break; + } + } + + if($controller->isValidRoute($targetIndexRoute) && $controller->isActiveRoute($targetIndexRoute)) { + if(!$skipInjectedIndex) { + $this->injectParents[strtolower($linkModelFqn) . ':index'] = [ + 'target' => $targetIndexRoute, + 'label' => StringUtilities::localizeController( + controllerName: $linkModelFqn, + pluginName: $link->plugin ?? null, + plural: true + ) + ]; + } + } else { + $this->llog('debug', "Breadcrumbs: invalid route for $linkModelFqn {$parentLink->attr}={$parentLink->value}"); + } } // Determine target action for entity link @@ -279,16 +301,34 @@ public function injectPrimaryLink(object $link, bool $index = true, string $link $title = StringUtilities::stripActionPrefix($title); - // Inject the entity breadcrumb (unique per table:id) - $this->injectParents[$this->composeEntityKey($linkTable->getTable(), (int)$linkedEntity->id)] = [ - 'target' => [ - 'plugin' => $parentLink->plugin ?? StringUtilities::blankToNull(StringUtilities::pluginPlugin($linkModelFqn)) ?? null, - 'controller' => StringUtilities::pluginModel($linkModelFqn), - 'action' => $breadcrumbAction, - (int)$linkedEntity->id - ], - 'label' => $linkLabel ?? $title, + $targetRoute = [ + 'plugin' => $parentLink->plugin ?? StringUtilities::blankToNull(StringUtilities::pluginPlugin($linkModelFqn)) ?? null, + 'controller' => StringUtilities::pluginModel($linkModelFqn), + 'action' => $breadcrumbAction, + (int)$linkedEntity->id ]; + + // Build absolute URL for entity link and check skip + $entityUrl = Router::url($targetRoute, false); + $skipInjectedEntity = false; + foreach ($this->skipNoViewPaths as $pattern) { + if (preg_match($pattern, $entityUrl)) { + $skipInjectedEntity = true; + break; + } + } + + if($controller->isValidRoute($targetIndexRoute) && $controller->isActiveRoute($targetIndexRoute)) { + if(!$skipInjectedEntity) { + // Inject the entity breadcrumb (unique per table:id) + $this->injectParents[$this->composeEntityKey($linkTable->getTable(), (int)$linkedEntity->id)] = [ + 'target' => $targetRoute, + 'label' => $linkLabel ?? $title, + ]; + } + } else { + $this->llog('debug', "Breadcrumbs: invalid route for $linkModelFqn {$parentLink->attr}={$parentLink->value}"); + } } catch (\Cake\Datasource\Exception\RecordNotFoundException $e) { $this->llog('error', "Breadcrumbs: linked entity not found for $linkModelFqn {$link->attr}={$link->value}"); @@ -369,6 +409,18 @@ public function skipParents(array $skipPaths): void $this->skipParentPaths = $skipPaths; } + /** + * Set the set of paths where the target entity has no dedicated view. + * For these paths, skip generating index/entity breadcrumb links to non-existent views. + * + * @since COmanage Registry v5.2.0 + * @param array $skipPaths Array of regular expressions describing paths + */ + public function skipTargetsWithoutView(array $skipPaths): void + { + $this->skipNoViewPaths = $skipPaths; + } + /** * Resolves the contain list for a given table and mapped action * diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index b80d637d4..e921eb2c4 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -635,7 +635,7 @@ protected function getRequiredFields() { public function index() { /** var string $modelsName */ - $modelsName = $this->getName(); + $modelsName = $this->getQualifiedName(); /** var Cake\ORM\Table $table */ $table = $this->getCurrentTable(); // $tableName = models @@ -671,7 +671,7 @@ public function index() { $this->set('vv_permission_set', $this->RegistryAuth->calculatePermissionsForResultSet($resultSet)); // Default index view title is model name - [$title, , ] = StringUtilities::entityAndActionToTitle($resultSet, $modelsName, 'index'); + [$title, , ] = StringUtilities::entityAndActionToTitle(null, $modelsName, 'index'); $this->set('vv_title', $title); // Let the view render diff --git a/app/src/Controller/StandardPluginController.php b/app/src/Controller/StandardPluginController.php index 0bab00bc6..c9ab7382f 100644 --- a/app/src/Controller/StandardPluginController.php +++ b/app/src/Controller/StandardPluginController.php @@ -35,8 +35,31 @@ use \App\Lib\Util\StringUtilities; use \App\Lib\Enum\SuspendableStatusEnum; +use \PipelineToolkit\Model\Table\IdentifierMappersTable; +use Symfony\Component\Filesystem\Exception\RuntimeException; class StandardPluginController extends StandardController { + use \App\Lib\Traits\BreadcrumbsTrait; + + /** + * Perform Controller initialization. + * + * @since COmanage Registry v5.2.0 + */ + + public function initialize(): void { + parent::initialize(); + + // Configure breadcrumb rendering + // https://comanage.org/registry-pe/pipeline-toolkit/identifier-mappers?flange_id=3 + // https://comanage.org/registry-pe/pipeline-toolkit/person-role-mappers?flange_id=3 + // This is wrong because the view does not exist. + $this->Breadcrumb->skipTargetsWithoutView([ + '#^(.*?)/pipeline-toolkit/identifier-mappers(?:\?.*)?$#', + '#^(.*?)/pipeline-toolkit/person-role-mappers(?:\?.*)?$#', + ]); + } + /** * Callback run prior to the request action. * @@ -92,6 +115,7 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { public function beforeRender(\Cake\Event\EventInterface $event) { $link = $this->getPrimaryLink(true); + $request = $this->getRequest(); if(!empty($link->value) && !empty($link->model_name)) { // This might be a plugin table in Plugin.Model notation @@ -111,6 +135,74 @@ public function beforeRender(\Cake\Event\EventInterface $event) { $this->set('vv_title', $title); } + // Check for missing Breadcrumbs + // For the Pipeline Toolkit + if($this->getPlugin() == 'PipelineToolkit') { + $identifierMapperTable = $this->fetchTable(IdentifierMappersTable::class); + $identifierMapperEntity = null; + + // Edit/View + if ($this->viewBuilder()->getVar('vv_obj') && $request->getParam('action') !== 'add') { + $obj = $this->viewBuilder()->getVar('vv_obj'); + // Load IdentifierMapper if identifier_mapper_id is set + if (empty($obj->identifier_mapper_id) && $obj->getSource() !== 'PipelineToolkit.IdentifierMappers') { + throw new \RuntimeException('Identifier Mapper ID is not set'); + } + $identifierMapperEntity = empty($obj->identifier_mapper_id) ? $obj : $identifierMapperTable->get($obj->identifier_mapper_id); + } else if ($this->request->getQuery('identifier_mapper_id')) { + $identifierMapperEntity = $identifierMapperTable->get($this->request->getQuery('identifier_mapper_id')); + } else if ($this->request->getQuery('flange_id')) { + $identifierMapperEntity = $identifierMapperTable->find() + ->where(['flange_id' => $this->request->getQuery('flange_id')]) + ->firstOrFail(); + } + + if(empty($identifierMapperEntity)) { + throw new \RuntimeException('Identifier Mapper Record not found'); + } + $this->set('vv_identifier_mapper', $identifierMapperEntity); + + // Always add pipeline/flange breadcrumbs from the mapper entity + $pipelineCrumbs = $this->buildPipelineBreadcrumbs($identifierMapperEntity); + + // Conditionally add the two extra crumbs ONLY when the current entity has identifier_mapper_id + $extraCrumbs = []; + $vvObj = $this->viewBuilder()->getVar('vv_obj'); + if ($request->getParam('action') === 'edit' + && $vvObj instanceof \Cake\Datasource\EntityInterface + && (int)($vvObj->get('identifier_mapper_id') ?? 0) > 0) { + + $identifierMapperId = (int)$vvObj->get('identifier_mapper_id'); + + // Identifier Mapper edit + $extraCrumbs['identifier_mappers:' . $identifierMapperId] = [ + 'label' => __('Identifier Mappers'), + 'target' => [ + 'plugin' => 'PipelineToolkit', + 'controller' => 'IdentifierMappers', + 'action' => 'edit', + $identifierMapperId, + ], + ]; + + // Current entity index filtered by identifier_mapper_id + $controller = (string)$request->getParam('controller'); + $plugin = (string)($request->getParam('plugin') ?? ''); + $extraCrumbs[strtolower($controller) . ':index:' . $identifierMapperId] = [ + 'label' => \App\Lib\Util\StringUtilities::localizeController($controller, $plugin ?: null, true), + 'target' => [ + 'plugin' => $plugin ?: null, + 'controller' => $controller, + 'action' => 'index', + '?' => ['identifier_mapper_id' => $identifierMapperId], + ], + ]; + } + + $vv_bc_parents = (array)$this->viewBuilder()->getVar('vv_bc_parents'); + $this->set('vv_bc_parents', [...$pipelineCrumbs, ...$extraCrumbs, ...$vv_bc_parents]); + } + return parent::beforeRender($event); } diff --git a/app/src/Lib/Traits/BreadcrumbsTrait.php b/app/src/Lib/Traits/BreadcrumbsTrait.php index 2432812da..be4614cbf 100644 --- a/app/src/Lib/Traits/BreadcrumbsTrait.php +++ b/app/src/Lib/Traits/BreadcrumbsTrait.php @@ -30,6 +30,7 @@ namespace App\Lib\Traits; use App\Lib\Util\StringUtilities; +use Cake\Datasource\EntityInterface; trait BreadcrumbsTrait { /** @@ -43,6 +44,7 @@ trait BreadcrumbsTrait { * - oauth2_server_id → CoreServer.Oauth2Servers * * @return array Breadcrumb parents to prepend + * @since COmanage Registry v5.2.0 */ protected function buildServerParamBreadcrumbs(): array { @@ -137,6 +139,7 @@ protected function buildServerParamBreadcrumbs(): array * @param int $personId The person ID to build crumbs for. * @param bool $includePeopleIndex Whether to include the People index crumb. * @return array + * @since COmanage Registry v5.2.0 */ protected function buildPersonBreadcrumbs(int $personId, bool $includePeopleIndex = true): array { @@ -179,4 +182,119 @@ protected function buildPersonBreadcrumbs(int $personId, bool $includePeopleInde return $parents; } -} \ No newline at end of file + + + /** + * Build the pipeline-related breadcrumb parents. + * + * Returns only the parents to prepend (does not overwrite existing `vv_bc_parents`). + * Safely de-duplicates by checking currently set parents. + * + * @param \Cake\Datasource\EntityInterface $entity The entity containing flange information + * @return array Breadcrumb parents to prepend + * @since COmanage Registry v5.2.0 + */ + protected function buildPipelineBreadcrumbs(EntityInterface $entity): array + { + // 0) If the given entity is itself a Flange, skip flangeId calculations + if ($entity instanceof \App\Model\Entity\Flange) { + $flangeId = (int)$entity->get('id'); + if ($flangeId <= 0) { + return []; + } + } else { + // 1) Get the flange id + $flangeId = $entity->get('flange')->id + ?? $entity->get('flange_id') + ?? null; + + if (!$flangeId) { + return []; + } + } + + $coId = method_exists($this, 'getCOID') ? (int)$this->getCOID() : 0; + + try { + // 2) Get the Flange to obtain pipeline_id + $Flanges = $this->fetchTable('Flanges'); + $flange = $Flanges->get($flangeId); + $pipelineId = (int)($flange->pipeline_id ?? 0); + + if ($pipelineId <= 0) { + return []; + } + + $vv_bc_parents = (array)$this->viewBuilder()->getVar('vv_bc_parents'); + $parents = []; + + // 3) Pipelines index + $pipelinesIndexKey = 'pipelines:' . $coId; + if ($coId > 0 && empty($vv_bc_parents[$pipelinesIndexKey])) { + $parents[$pipelinesIndexKey] = [ + 'label' => __('Pipelines'), + 'target' => [ + 'plugin' => null, + 'controller' => 'Pipelines', + 'action' => 'index', + '?' => ['co_id' => $coId], + ], + ]; + } + + // 4) Current Pipeline + $Pipelines = $this->fetchTable('Pipelines'); + $pipeline = $Pipelines->get($pipelineId); + $pipelineKey = 'pipelines:' . $pipelineId; + + if (empty($vv_bc_parents[$pipelineKey])) { + $parents[$pipelineKey] = [ + 'label' => method_exists($Pipelines, 'generateDisplayField') + ? $Pipelines->generateDisplayField($pipeline) + : ($pipeline->{$Pipelines->getDisplayField()} ?? ('Pipeline #' . $pipelineId)), + 'target' => [ + 'plugin' => null, + 'controller' => 'Pipelines', + 'action' => 'edit', + $pipelineId, + ], + ]; + } + + // 5) Flanges index for this pipeline + $flangesIndexKey = 'flanges:index'; + if (empty($vv_bc_parents[$flangesIndexKey])) { + $parents[$flangesIndexKey] = [ + 'label' => __('Flanges'), + 'target' => [ + 'plugin' => null, + 'controller' => 'Flanges', + 'action' => 'index', + '?' => ['pipeline_id' => $pipelineId], + ], + ]; + } + + // 6) Current Flange + $flangeKey = 'flanges:' . $flangeId; + if (empty($vv_bc_parents[$flangeKey])) { + $parents[$flangeKey] = [ + 'label' => method_exists($Flanges, 'generateDisplayField') + ? $Flanges->generateDisplayField($flange) + : ($flange->{$Flanges->getDisplayField()} ?? ('Flange #' . $flangeId)), + 'target' => [ + 'plugin' => null, + 'controller' => 'Flanges', + 'action' => 'edit', + $flangeId, + ], + ]; + } + } catch (\Throwable $e) { + // Fail-soft: no pipeline → no crumbs + return []; + } + + return $parents; + } +} diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php index 60b9f5227..090e70cda 100644 --- a/app/src/Lib/Util/StringUtilities.php +++ b/app/src/Lib/Util/StringUtilities.php @@ -259,13 +259,15 @@ public static function entityAndActionToTitle($entity, [$plugin, $modelsName] = explode('.', $modelPath, 2); } - if($entity == null && !empty($plugin)) { - $count = $action == 'index' ? 99 : 1; - return [__d($domain, "controller.$modelsName", [$count]), '', '']; - } elseif($entity === null) { - $count = $action == 'index' ? 99 : 1; - return [__d($domain, "{$modelPath}.{$action}", [$count]), '', '']; + // 3) Index view → use the controller plural form (token 99 convention) + if($action === 'index') { + if(!empty($plugin)) { + $domain = StringUtilities::pluginToTextDomain($plugin); + return [__d($domain, "controller.$modelsName", [99]), '', '']; + } + return [__d('controller', $modelsName, [99]), '', '']; } + // Base table and default message IDs for translation $linkTable = TableRegistry::getTableLocator()->get($modelPath); $msgId = "{$action}.a"; // eg: "edit.a" @@ -293,14 +295,6 @@ public static function entityAndActionToTitle($entity, return [__d('controller', $modelsName), '', '']; } - // 3) Index view → use the controller plural form (token 99 convention) - if($action === 'index') { - if(!empty($plugin)) { - return [__d($domain, "controller.$modelsName", [99]), '', '']; - } - return [__d('controller', $modelsName, [99]), '', '']; - } - // Add/Edit/View // The MVEA Models have an entityId. The one from the parent model. // We need to have a condition for this and exclude it. diff --git a/app/src/Model/Table/FlangesTable.php b/app/src/Model/Table/FlangesTable.php index 3bda87ded..ee5a12592 100644 --- a/app/src/Model/Table/FlangesTable.php +++ b/app/src/Model/Table/FlangesTable.php @@ -43,9 +43,10 @@ class FlangesTable extends Table { use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PluggableModelTrait; use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + /** * Perform Cake Model initialization. * @@ -72,6 +73,9 @@ public function initialize(array $config): void { $this->setPrimaryLink('pipeline_id'); $this->setRequiresCO(true); + $this->setEditContains(['Pipelines']); + $this->setViewContains(['Pipelines']); + $this->setAutoViewVars([ 'plugins' => [ 'type' => 'plugin', diff --git a/app/src/Model/Table/PetitionsTable.php b/app/src/Model/Table/PetitionsTable.php index 4ee4ecac6..402120408 100644 --- a/app/src/Model/Table/PetitionsTable.php +++ b/app/src/Model/Table/PetitionsTable.php @@ -150,7 +150,7 @@ public function initialize(array $config): void { 'type' => 'enum', 'class' => 'PetitionStatusEnum' ], - 'couIds' => [ + 'cous' => [ 'type' => 'select', 'model' => 'Cous' ]