diff --git a/app/plugins/CoreEnroller/templates/ApprovalCollectors/dispatch.inc b/app/plugins/CoreEnroller/templates/ApprovalCollectors/dispatch.inc
index 62375a0fb..2837b5bdc 100644
--- a/app/plugins/CoreEnroller/templates/ApprovalCollectors/dispatch.inc
+++ b/app/plugins/CoreEnroller/templates/ApprovalCollectors/dispatch.inc
@@ -63,7 +63,7 @@ $this->Field->enableFormEditMode();
]
);
- print $this->element('form/listItem', [
+ print $this->element('CoreEnroller.listItem', [
'arguments' => [
'fieldName' => 'comment',
'fieldOptions' => [
diff --git a/app/plugins/CoreEnroller/templates/BasicAttributeCollectors/dispatch.inc b/app/plugins/CoreEnroller/templates/BasicAttributeCollectors/dispatch.inc
index 8333415ae..e823825e6 100644
--- a/app/plugins/CoreEnroller/templates/BasicAttributeCollectors/dispatch.inc
+++ b/app/plugins/CoreEnroller/templates/BasicAttributeCollectors/dispatch.inc
@@ -59,7 +59,7 @@ $this->Field->enableFormEditMode();
element('form/listItem', [
+ print $this->element('CoreEnroller.listItem', [
'arguments' => [
'fieldName' => 'mail',
'fieldOptions' => [
diff --git a/app/plugins/CoreEnroller/templates/InvitationAccepters/dispatch.inc b/app/plugins/CoreEnroller/templates/InvitationAccepters/dispatch.inc
index c3f704581..0e9a2b328 100644
--- a/app/plugins/CoreEnroller/templates/InvitationAccepters/dispatch.inc
+++ b/app/plugins/CoreEnroller/templates/InvitationAccepters/dispatch.inc
@@ -36,7 +36,7 @@ if($vv_action == 'dispatch') {
// Make the Form fields editable
$this->Field->enableFormEditMode();
- print $this->element('form/listItem', [
+ print $this->element('CoreEnroller.listItem', [
'arguments' => [
'fieldName' => 'accepted',
'fieldLabel' => __d('operation','accept.invitation'),
diff --git a/app/plugins/CoreEnroller/templates/element/emailVerifiers/verify.php b/app/plugins/CoreEnroller/templates/element/emailVerifiers/verify.php
index ce25aa05a..080bd9b6a 100644
--- a/app/plugins/CoreEnroller/templates/element/emailVerifiers/verify.php
+++ b/app/plugins/CoreEnroller/templates/element/emailVerifiers/verify.php
@@ -72,7 +72,7 @@
print __d('core_enroller', 'information.EmailVerifiers.code_sent', [$vv_verify_address]);
-print $this->element('form/listItem', [
+print $this->element('CoreEnroller.listItem', [
'arguments' => [
'fieldName' => 'code',
'fieldLabel' => __d('field', 'code'),
diff --git a/app/plugins/CoreEnroller/templates/element/field.php b/app/plugins/CoreEnroller/templates/element/field.php
index 80a3b8cd4..9166f9ca7 100644
--- a/app/plugins/CoreEnroller/templates/element/field.php
+++ b/app/plugins/CoreEnroller/templates/element/field.php
@@ -119,5 +119,5 @@
'formArguments' => $formArguments
]),
// Default use case
- default => $this->Field->getElementsForDisabledInput('form/listItem', $formArguments)
+ default => $this->Field->getElementsForDisabledInput('CoreEnroller.listItem', $formArguments)
};
diff --git a/app/plugins/CoreEnroller/templates/element/fieldDiv.php b/app/plugins/CoreEnroller/templates/element/fieldDiv.php
new file mode 100644
index 000000000..0f1c84764
--- /dev/null
+++ b/app/plugins/CoreEnroller/templates/element/fieldDiv.php
@@ -0,0 +1,75 @@
+
+
+
+ element('form/nameDiv');
+
+ // This configuration isn't necessary anymore.
+ if(isset($vv_field_arguments['fieldDescription'])) {
+ unset($vv_field_arguments['fieldDescription']);
+ $this->set('vv_field_arguments', $vv_field_arguments);
+ }
+
+ // Info Div
+ ?>
+
+ element('form/infoDiv/withPrefix');
+ } elseif(isset($vv_field_arguments['autocomplete'])) {
+ print $this->element('form/infoDiv/autocomplete');
+ } elseif(isset($vv_field_arguments['status'])) {
+ print $this->element('form/infoDiv/status');
+ } elseif(isset($vv_field_arguments['groupedControls'])) {
+ print $this->element('form/infoDiv/grouped');
+ } elseif(isset($vv_field_arguments['entity'])) {
+ print $this->element('form/infoDiv/source');
+ } elseif(isset($vv_field_arguments['groupmember'])) {
+ print $this->element('form/infoDiv/groupMember');
+ } else {
+ print $this->element('form/infoDiv/default');
+ }
+
+ // Insert the afterField supplement:
+ if(!empty($vv_after_field)) {
+ print $vv_after_field;
+ }
+ ?>
+
+
\ No newline at end of file
diff --git a/app/plugins/CoreEnroller/templates/element/listItem.php b/app/plugins/CoreEnroller/templates/element/listItem.php
new file mode 100644
index 000000000..7c02fd4f3
--- /dev/null
+++ b/app/plugins/CoreEnroller/templates/element/listItem.php
@@ -0,0 +1,88 @@
+set('fieldName', $arguments['fieldName']);
+ $fieldName = $arguments['fieldName'];
+ $this->set('vv_field_arguments', $arguments);
+
+ // Pass along the field supplements if they are configured.
+ $this->set('vv_before_field', $beforeField ?? '');
+ $this->set('vv_after_field', $afterField ?? '');
+
+ // If an attribute is frozen, inject a special link to unfreeze it, since
+ // the attribute is read-only and the admin can't simply uncheck the setting
+ if($fieldName == 'frozen' && $this->Field->getEntity()->frozen) {
+ $url = [
+ 'label' => __d('operation', 'unfreeze'),
+ 'url' => [
+ 'plugin' => null,
+ 'controller' => \App\Lib\Util\StringUtilities::entityToClassname($this->Field->getEntity()),
+ 'action' => 'unfreeze',
+ $this->Field->getEntity()->id
+ ]
+ ];
+ $arguments = [
+ ...$arguments,
+ 'status' => __d('field', 'frozen'),
+ 'link' => $url,
+ ];
+ $this->set('vv_field_arguments', $arguments);
+ }
+
+ // If an attribute is a plugin, return the link to its configuration
+ if($fieldName == 'plugin' && $vv_action == 'edit') {
+ $url = [
+ 'label' => __d('operation', 'configure.plugin'),
+ 'url' => [
+ 'plugin' => null,
+ 'controller' => \App\Lib\Util\StringUtilities::entityToClassname($this->Field->getEntity()),
+ 'action' => 'configure',
+ $this->Field->getEntity()->id
+ ]
+ ];
+ $arguments = [
+ ...$arguments,
+ 'status' => $this->Field->getEntity()->$fieldName,
+ 'link' => $url,
+ ];
+ $this->set('vv_field_arguments', $arguments);
+ }
+
+?>
+
+
+ = $this->element('CoreEnroller.fieldDiv')?>
+
diff --git a/app/plugins/CoreEnroller/templates/element/unorderedList.php b/app/plugins/CoreEnroller/templates/element/unorderedList.php
new file mode 100644
index 000000000..d0633c6f1
--- /dev/null
+++ b/app/plugins/CoreEnroller/templates/element/unorderedList.php
@@ -0,0 +1,68 @@
+
+
+
+ $v) {
+ print $this->Form->hidden($attr, ['value' => $v]);
+ }
+ }
\ No newline at end of file
diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php
index 76450899b..a8f59a5e5 100644
--- a/app/src/Controller/AppController.php
+++ b/app/src/Controller/AppController.php
@@ -273,14 +273,10 @@ public function getCOID(): ?int {
*/
public function getCurrentTable(): \Cake\ORM\Table
{
- /** @var string $modelsName */
$modelsName = $this->getName();
+ $plugin = $this->getPlugin();
- $alias = $this->getPlugin() !== null
- ? $this->getPlugin() . '.' . $modelsName
- : $modelsName;
-
- return $this->fetchTable($alias);
+ return $this->fetchTable(StringUtilities::getQualifiedName($plugin, $modelsName));
}
diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php
index 4a3326b36..d0a13334c 100644
--- a/app/src/Controller/StandardController.php
+++ b/app/src/Controller/StandardController.php
@@ -686,7 +686,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/StandardEnrollerController.php b/app/src/Controller/StandardEnrollerController.php
index c7f6e2124..b95dc8dcd 100644
--- a/app/src/Controller/StandardEnrollerController.php
+++ b/app/src/Controller/StandardEnrollerController.php
@@ -74,11 +74,45 @@ public function beforeRender(\Cake\Event\EventInterface $event) {
if(!empty($link->value)) {
$currentTable = $this->getCurrentTable();
- $efsTable = $currentTable->getAssociation('EnrollmentFlowSteps')->getTarget();
-
- $this->set('vv_bc_parent_obj', $efsTable->get($link->value));
- $this->set('vv_bc_parent_displayfield', $efsTable->getDisplayField());
- $this->set('vv_bc_parent_primarykey', $efsTable->getPrimaryKey());
+ // Not all enroller plugin tables have a direct EnrollmentFlowSteps association
+ if (method_exists($currentTable, 'hasAssociation')
+ && $currentTable->hasAssociation('EnrollmentFlowSteps')) {
+
+ $efsTable = $currentTable->getAssociation('EnrollmentFlowSteps')->getTarget();
+
+ $this->set('vv_bc_parent_obj', $efsTable->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $efsTable->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $efsTable->getPrimaryKey());
+ } else {
+ // Two-hop case: foreign key from $link (e.g. attribute_collector_id)
+ // -> AttributeCollectors
+ // -> EnrollmentFlowSteps
+ // Useful for deeply nested enrollment configuration objects.
+ if (!empty($link->attr)) {
+ // Derive the table class name from the foreign key name
+ $fkClassName = StringUtilities::foreignKeyToClassName($link->attr); // e.g. 'AttributeCollectors'
+
+ // Qualify with plugin if present
+ $tableAlias = !empty($link->plugin)
+ ? $link->plugin . '.' . $fkClassName // e.g. 'CoreEnroller.AttributeCollectors'
+ : $fkClassName;
+
+ $attributeCollectorsTable = TableRegistry::getTableLocator()->get($tableAlias);
+
+ // Load the intermediate object (AttributeCollector, in your case)
+ $collector = $attributeCollectorsTable->get((int)$link->value);
+
+ // From the AttributeCollector, go up to the EnrollmentFlowStep
+ if (!empty($collector->enrollment_flow_step_id)) {
+ $efsTable = TableRegistry::getTableLocator()->get('EnrollmentFlowSteps');
+ $step = $efsTable->get((int)$collector->enrollment_flow_step_id);
+
+ $this->set('vv_bc_parent_obj', $step);
+ $this->set('vv_bc_parent_displayfield', $efsTable->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $efsTable->getPrimaryKey());
+ }
+ }
+ }
}
return parent::beforeRender($event);
diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php
index 0fda489e5..1f24cf1ca 100644
--- a/app/src/Lib/Util/StringUtilities.php
+++ b/app/src/Lib/Util/StringUtilities.php
@@ -204,6 +204,17 @@ public static function entityToClassName($entity): string {
// $classPath will be something like App\Model\Entity\Name, but we want to return "Names"
$classPath = get_class($entity);
+ return self::classPathClassName($classPath);
+ }
+
+ /**
+ * Extracts and pluralizes the class name from a fully qualified class path.
+ *
+ * @param string $classPath Fully qualified class path (eg: App\Model\Entity\Name)
+ * @return string Pluralized class name (eg: Names)
+ * @since COmanage Registry v5.2.0
+ */
+ public static function classPathClassName(string $classPath): string {
return Inflector::pluralize(substr($classPath, strrpos($classPath, '\\')+1));
}
@@ -285,13 +296,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]), '', ''];
+ // 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"
@@ -308,25 +321,20 @@ public static function entityAndActionToTitle($entity,
// If the entity actually belongs to a different model than the provided $modelsName,
// switch to that table and adjust the default message id pattern accordingly.
// This is necessary for TAB oriented views
- if(Inflector::singularize(self::entityToClassName($entity)) !== Inflector::singularize($modelsName)) {
+ if(
+ $entity !== null
+ && Inflector::singularize(self::entityToClassName($entity)) !== Inflector::singularize($modelsName)
+ ) {
$linkTable = TableRegistry::getTableLocator()->get(self::entityToClassName($entity));
// If modelPath and action are equal, don’t concatenate (preserve legacy behavior)
$msgId = $modelPath === $action ? $modelPath : "{$modelPath}.{$action}";
}
- // 2) No action → default to the controller label for the model (singular)
+ // No action → default to the controller label for the model (singular)
if($action === null) {
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.
@@ -338,7 +346,7 @@ public static function entityAndActionToTitle($entity,
$display = $entity->$field ?? null;
}
- // 6) Edit/View-like case for an existing entity with a usable display
+ // Edit/View-like case for an existing entity with a usable display
// Title: translate with override key first; if not found, fall back to default key.
// Super/Sub titles: set to the display (needed for External IDs in UI).
if (
@@ -354,7 +362,7 @@ public static function entityAndActionToTitle($entity,
return [$title, $supertitle, $subtitle];
}
- // 7) Fallbacks:
+ // Fallbacks:
// - New entities (no id),
// - Add/Delete actions,
// - Or we simply lack a display.
@@ -372,7 +380,7 @@ public static function entityAndActionToTitle($entity,
* @param string $domain Translation domain to use
* @param string $overrideKey Primary translation key to try first
* @param string $fallbackKey Fallback translation key if override not found
- * @param string $value Value to substitute in translation
+ * @param string|int $value Value to substitute in translation
* @return string Translated string using either override or fallback key
* @since COmanage Registry v5.2.0
*/
@@ -415,6 +423,22 @@ public static function foreignKeyToController(string $s): string {
return Inflector::underscore(Inflector::pluralize(substr($s, 0, strlen($s)-3)));
}
+
+ /**
+ * Get the fully qualified name by combining plugin and name with a dot separator.
+ *
+ * @param string|null $plugin Plugin name, or null if no plugin
+ * @param string $name Base name to qualify
+ * @return string Qualified name in format "Plugin.Name" or just "Name" if no plugin
+ * @since COmanage Registry v5.2.0
+ */
+ public static function getQualifiedName(?string $plugin, string $name): string
+ {
+ return $plugin !== null && $plugin !== ''
+ ? $plugin . '.' . $name
+ : $name;
+ }
+
/**
* Localize a controller name, accounting for plugins.
*
@@ -452,7 +476,7 @@ public static function qualifyModelPath(string $modelPath, ?string $plugin): str
if (empty($plugin) || str_starts_with($modelPath, $plugin . '.')) {
return $modelPath;
}
- return $plugin . '.' . $modelPath;
+ return self::getQualifiedName($plugin, $modelPath);;
}
/**
diff --git a/app/templates/EnrollmentFlows/start.inc b/app/templates/EnrollmentFlows/start.inc
index b4941bc5a..6026703ea 100644
--- a/app/templates/EnrollmentFlows/start.inc
+++ b/app/templates/EnrollmentFlows/start.inc
@@ -38,14 +38,11 @@ $modelTable = $this->Tab->getModelTableReference($modelsName);
// For now, we just request the enrollee email. We're only called if
// collect_enrollee_email is true, so we don't need to check it, though
// if we ever add more fields to the start form we should.
-print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => 'enrollee_email',
- 'fieldOptions' => [
- 'required' => true
- ],
+$fields = [
+ 'enrollee_email' => [
+ 'required' => true,
'fieldType' => 'string'
]
-]);
+];
// Any hidden fields??
diff --git a/app/templates/EnrollmentFlows/start.php b/app/templates/EnrollmentFlows/start.php
index 7dd28ede7..b3ead8908 100644
--- a/app/templates/EnrollmentFlows/start.php
+++ b/app/templates/EnrollmentFlows/start.php
@@ -27,6 +27,11 @@
declare(strict_types = 1);
+// Enrollment Flow Start has its own file of fields
+$fields = [];
+$modelsName = $this->getName();
+$templatePath = $vv_template_path ?? ROOT . DS . "templates" . DS . $modelsName;
+include($templatePath . DS . 'start.inc');
?>
@@ -38,8 +43,6 @@
= $this->element('flash') // Flash messages ?>
set('vv_fields_inc', 'start.inc');
// Set form edit ability
$this->set('vv_is_editable', true);
@@ -49,7 +52,7 @@
'type' => 'post',
]);
// Form body
-print $this->element('form/unorderedList');
+print $this->element('form/unorderedList', ['vv_fields' => $fields]);
// Close the Form
print $this->Form->end();
diff --git a/app/templates/Standard/dispatch.php b/app/templates/Standard/dispatch.php
index 84d2e3352..c69be9295 100644
--- a/app/templates/Standard/dispatch.php
+++ b/app/templates/Standard/dispatch.php
@@ -81,7 +81,7 @@
// Form body
print '
';
-print $this->element('form/unorderedList');
+print $this->element('CoreEnroller.unorderedList');
print '
';
// Inject the Petition ID into the form, though it will most likely