From 4e054b0606cfa1b15aee4d574a6e5155ce9529cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20de=20la=20Pen=CC=83a?=
Date: Wed, 9 Sep 2026 16:57:03 +0100
Subject: [PATCH] feat: Let modules inject admin tabs when editing a user group
Co-authored-by: Cursor
---
admin/controllers/Groups.php | 71 ++++++++++++++++++++++++-
admin/views/Groups/edit.php | 36 +++++++++----
src/Interfaces/Admin/User/Group/Tab.php | 38 +++++++++++++
3 files changed, 135 insertions(+), 10 deletions(-)
create mode 100644 src/Interfaces/Admin/User/Group/Tab.php
diff --git a/admin/controllers/Groups.php b/admin/controllers/Groups.php
index 62ab23e5..61bad0bd 100644
--- a/admin/controllers/Groups.php
+++ b/admin/controllers/Groups.php
@@ -14,12 +14,16 @@
use Nails\Admin\Controller\DefaultController;
use Nails\Auth\Constants;
+use Nails\Auth\Interfaces\Admin\User\Group\Tab;
use Nails\Auth\Model\User\Group;
use Nails\Auth\Model\User\Password;
use Nails\Common\Exception\ValidationException;
+use Nails\Common\Factory\Component;
use Nails\Common\Resource;
+use Nails\Common\Service\FormValidation;
use Nails\Common\Service\Input;
use Nails\Common\Service\Uri;
+use Nails\Components;
use Nails\Config;
use Nails\Factory;
@@ -43,6 +47,13 @@ class Groups extends DefaultController
// --------------------------------------------------------------------------
+ /**
+ * @var Tab[]
+ */
+ protected array $aGroupTabs = [];
+
+ // --------------------------------------------------------------------------
+
/**
* Load data for the edit/create view
*
@@ -54,6 +65,22 @@ protected function loadEditViewData(?Resource $oItem = null): void
{
parent::loadEditViewData($oItem);
+ $this->aGroupTabs = [];
+ /** @var Component $oComponent */
+ foreach (Components::available() as $oComponent) {
+ $aClasses = $oComponent
+ ->findClasses('Auth\\Admin\\User\\Group\\Tab')
+ ->whichImplement(Tab::class);
+
+ foreach ($aClasses as $sClass) {
+ if ($sClass::isEnabled($oItem)) {
+ $this->aGroupTabs[$sClass] = new $sClass();
+ }
+ }
+ }
+
+ $this->data['aGroupTabs'] = $this->aGroupTabs;
+
// Get all available permissions
$this->data['aPermissions'] = [];
foreach ($this->data['adminControllers'] as $module => $oModuleDetails) {
@@ -107,6 +134,20 @@ protected function runFormValidation(string $sMode, array $aOverrides = []): voi
'description' => ['required'],
]
);
+
+ /** @var Input $oInput */
+ $oInput = Factory::service('Input');
+ $aRules = [];
+ /** @var Tab $oTab */
+ foreach ($this->aGroupTabs as $oTab) {
+ $aRules = array_merge($aRules, $oTab->getValidationRules($this->data['item']));
+ }
+
+ if ($aRules) {
+ /** @var FormValidation $oFormValidation */
+ $oFormValidation = Factory::service('FormValidation');
+ $oFormValidation->buildValidator($aRules, [], $oInput->post())->run();
+ }
}
// --------------------------------------------------------------------------
@@ -125,7 +166,7 @@ protected function getPostObject(): array
/** @var Password $oUserPasswordModel */
$oUserPasswordModel = Factory::model('UserPassword', Constants::MODULE_SLUG);
- return [
+ $aData = [
'slug' => $oInput->post('slug'),
'label' => $oInput->post('label'),
'description' => $oInput->post('description'),
@@ -134,6 +175,34 @@ protected function getPostObject(): array
'acl' => $oUserGroupModel->processPermissions($oInput->post('acl') ?: []),
'password_rules' => $oUserPasswordModel->processRules($oInput->post('pw') ?: []),
];
+
+ /** @var Tab $oTab */
+ foreach ($this->aGroupTabs as $oTab) {
+ $aData = array_merge($aData, $oTab->getPostData($this->data['item'], $oInput->post()));
+ }
+
+ return $aData;
+ }
+
+ // --------------------------------------------------------------------------
+
+ protected function afterCreateAndEdit(
+ $sMode,
+ ?Resource $oNewItem,
+ ?Resource $oOldItem = null
+ ): void {
+ parent::afterCreateAndEdit($sMode, $oNewItem, $oOldItem);
+
+ if (!$oNewItem instanceof \Nails\Auth\Resource\User\Group) {
+ return;
+ }
+
+ /** @var Input $oInput */
+ $oInput = Factory::service('Input');
+ /** @var Tab $oTab */
+ foreach ($this->aGroupTabs as $oTab) {
+ $oTab->afterSave($oNewItem, $oInput->post());
+ }
}
// --------------------------------------------------------------------------
diff --git a/admin/views/Groups/edit.php b/admin/views/Groups/edit.php
index 736a9c09..466d3f1e 100644
--- a/admin/views/Groups/edit.php
+++ b/admin/views/Groups/edit.php
@@ -3,6 +3,18 @@
use Nails\Factory;
$oInput = Factory::service('Input');
+
+$aRenderedGroupTabs = [];
+/** @var \Nails\Auth\Interfaces\Admin\User\Group\Tab $oTab */
+foreach ($aGroupTabs ?? [] as $oTab) {
+ $aRenderedGroupTabs[] = [
+ 'id' => 'tab-extension-' . count($aRenderedGroupTabs),
+ 'order' => $oTab->getOrder(),
+ 'label' => $oTab->getLabel(),
+ 'content' => $oTab->getBody($item ?? null),
+ ];
+}
+arraySortMulti($aRenderedGroupTabs, 'order');
?>
-
-
-
- Currently, 2FA settings are done at a code-level and apply to all users.
-
-
+
+
+ =$aTab['content']?>
+
+
@@ -280,5 +293,10 @@
echo \Nails\Admin\Helper::floatingControls($CONFIG['FLOATING_CONFIG']);
echo form_close();
+ /** @var \Nails\Auth\Interfaces\Admin\User\Group\Tab $oTab */
+ foreach ($aGroupTabs ?? [] as $oTab) {
+ echo $oTab->getAdditionalMarkup($item ?? null);
+ }
+
?>
diff --git a/src/Interfaces/Admin/User/Group/Tab.php b/src/Interfaces/Admin/User/Group/Tab.php
new file mode 100644
index 00000000..30e265d8
--- /dev/null
+++ b/src/Interfaces/Admin/User/Group/Tab.php
@@ -0,0 +1,38 @@
+