From 441383ad54a8e021e15ff02031aac6a1f9bf5517 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/views/Groups/edit.php | 34 ++++++++----
admin/views/Groups/edit/2fa.php | 3 --
src/Admin/Controller/Groups.php | 71 ++++++++++++++++++++++++-
src/Interfaces/Admin/User/Group/Tab.php | 38 +++++++++++++
4 files changed, 131 insertions(+), 15 deletions(-)
delete mode 100644 admin/views/Groups/edit/2fa.php
create mode 100644 src/Interfaces/Admin/User/Group/Tab.php
diff --git a/admin/views/Groups/edit.php b/admin/views/Groups/edit.php
index 8a85df7b..6da1465a 100644
--- a/admin/views/Groups/edit.php
+++ b/admin/views/Groups/edit.php
@@ -1,36 +1,48 @@
0,
'label' => 'Basic Details',
'content' => function () use ($oView) {
return \Nails\Admin\Helper::loadInlineView('edit/basic', [], true);
},
],
[
+ 'order' => 1,
'label' => 'Password',
'content' => function () use ($oView) {
return \Nails\Admin\Helper::loadInlineView('edit/password', [], true);
},
],
[
- 'label' => '2FA',
- 'content' => function () use ($oView) {
- return \Nails\Admin\Helper::loadInlineView('edit/2fa', [], true);
- },
- ],
- [
+ 'order' => 2,
'label' => 'Permissions',
'content' => function () use ($oView) {
return \Nails\Admin\Helper::loadInlineView('edit/permissions', [], true);
},
],
-]);
+];
+
+/** @var \Nails\Auth\Interfaces\Admin\User\Group\Tab $oTab */
+foreach ($aGroupTabs ?? [] as $oTab) {
+ $aTabs[] = [
+ 'order' => $oTab->getOrder(),
+ 'label' => $oTab->getLabel(),
+ 'content' => $oTab->getBody($item ?? null),
+ ];
+}
+
+arraySortMulti($aTabs, 'order');
+echo \Nails\Admin\Helper::tabs($aTabs);
echo \Nails\Admin\Helper::floatingControls();
echo form_close();
+
+/** @var \Nails\Auth\Interfaces\Admin\User\Group\Tab $oTab */
+foreach ($aGroupTabs ?? [] as $oTab) {
+ echo $oTab->getAdditionalMarkup($item ?? null);
+}
diff --git a/admin/views/Groups/edit/2fa.php b/admin/views/Groups/edit/2fa.php
deleted file mode 100644
index 4781de49..00000000
--- a/admin/views/Groups/edit/2fa.php
+++ /dev/null
@@ -1,3 +0,0 @@
-
- Currently, 2FA settings are done at a code-level and apply to all users.
-
diff --git a/src/Admin/Controller/Groups.php b/src/Admin/Controller/Groups.php
index 76d58c2e..831d8875 100644
--- a/src/Admin/Controller/Groups.php
+++ b/src/Admin/Controller/Groups.php
@@ -16,12 +16,16 @@
use Nails\Admin\Controller\DefaultController;
use Nails\Auth\Admin\Permission;
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;
@@ -47,6 +51,13 @@ class Groups extends DefaultController
// --------------------------------------------------------------------------
+ /**
+ * @var Tab[]
+ */
+ protected array $aGroupTabs = [];
+
+ // --------------------------------------------------------------------------
+
/**
* Load data for the edit/create view
*
@@ -58,6 +69,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;
+
/** @var \Nails\Admin\Service\Permission $oPermissionService */
$oPermissionService = Factory::service('Permission', \Nails\Admin\Constants::MODULE_SLUG);
$this->data['aPermissions'] = $oPermissionService->getGrouped();
@@ -87,6 +114,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();
+ }
}
// --------------------------------------------------------------------------
@@ -105,7 +146,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'),
@@ -122,6 +163,34 @@ protected function getPostObject(): array
),
'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/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 @@
+