Feat: add role priority management

- Added `priority` column to `roles` table with migration and model updates.
- Introduced priority management methods in `RoleService` and validation logic for updates.
- Enhanced access-manager UI to display and edit role priorities via a modal.
- Updated related data classes and views to support and handle the new priority field.
This commit is contained in:
= 2026-05-25 06:34:58 +00:00
parent d7c2491fed
commit cd29191c7b
9 changed files with 163 additions and 6 deletions

View File

@ -10,5 +10,6 @@ final class CreateRoleData extends Data
{ {
public function __construct( public function __construct(
public string $name, public string $name,
public int $priority,
) {} ) {}
} }

View File

@ -13,6 +13,7 @@ final class RoleData extends Data
public function __construct( public function __construct(
public int $id, public int $id,
public string $name, public string $name,
public int $priority = 0,
#[DataCollectionOf(class: RoleAppData::class)] #[DataCollectionOf(class: RoleAppData::class)]
public ?DataCollection $apps = null, public ?DataCollection $apps = null,
#[DataCollectionOf(class: RoleUserData::class)] #[DataCollectionOf(class: RoleUserData::class)]

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Data\Role;
use Spatie\LaravelData\Data;
final class UpdateRolePriorityData extends Data
{
public function __construct(
public int $roleId,
public int $priority,
) {}
}

View File

@ -10,4 +10,7 @@ class CreateRoleForm extends AssignAppToRoleForm
{ {
#[Validate('required|string|max:255|min:3')] #[Validate('required|string|max:255|min:3')]
public string $name = ''; public string $name = '';
#[Validate('required|integer|min:0')]
public int $priority = 0;
} }

View File

@ -4,11 +4,18 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Spatie\Activitylog\Models\Concerns\LogsActivity; use Spatie\Activitylog\Models\Concerns\LogsActivity;
use Spatie\Activitylog\Support\LogOptions; use Spatie\Activitylog\Support\LogOptions;
use Spatie\Permission\Models\Role as SpatieRole; use Spatie\Permission\Models\Role as SpatieRole;
/**
* @property int $priority
* @property AppRole $pivot
* @property ConnectedApp $apps
*/
#[Fillable(['priority'])]
class Role extends SpatieRole class Role extends SpatieRole
{ {
use LogsActivity; use LogsActivity;

View File

@ -4,7 +4,7 @@
namespace App\Services; namespace App\Services;
use App\Data\Role\{CreateRoleData, RoleData}; use App\Data\Role\{CreateRoleData, RoleData, UpdateRolePriorityData};
use App\Models\Role; use App\Models\Role;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -25,6 +25,7 @@ public function create(CreateRoleData $data): RoleData
{ {
$role = DB::transaction(fn () => Role::query()->create([ $role = DB::transaction(fn () => Role::query()->create([
'name' => $data->name, 'name' => $data->name,
'priority' => $data->priority,
'guard_name' => 'web', 'guard_name' => 'web',
])); ]));
@ -93,8 +94,32 @@ public function getPermissionsGroupedByRole(array $roleIds): ?DataCollection
$roles = Role::query() $roles = Role::query()
->with('permissions:id,name') ->with('permissions:id,name')
->whereIn('id', $roleIds) ->whereIn('id', $roleIds)
->get(['id', 'name']); ->get(['id', 'name', 'priority']);
return RoleData::collect($roles, DataCollection::class); return RoleData::collect($roles, DataCollection::class);
} }
/**
* Update the priority of a role.
*
* @throws InvalidArgumentException when roleId is invalid.
* @throws Throwable when an error occurs during the update.
*/
public function updatePriority(UpdateRolePriorityData $data): RoleData
{
if ($data->roleId <= 0) {
throw new InvalidArgumentException('Invalid role id');
}
$role = DB::transaction(function () use ($data) {
$role = Role::query()->findOrFail($data->roleId);
$role->update([
'priority' => $data->priority,
]);
return $role;
});
return RoleData::from($role);
}
} }

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration
{
public function up(): void
{
Schema::table('roles', function (Blueprint $table): void {
$table->unsignedInteger('priority')->default(100)->after('guard_name');
});
}
public function down(): void
{
Schema::table('roles', function (Blueprint $table): void {
$table->dropColumn('priority');
});
}
};

View File

@ -37,6 +37,7 @@ public function mount(): void
{ {
$this->headers = TableHeader::collect([ $this->headers = TableHeader::collect([
new TableHeader(key: 'name', label: 'Name'), new TableHeader(key: 'name', label: 'Name'),
new TableHeader(key: 'priority', label: 'Priority'),
new TableHeader(key: 'apps', label: 'Apps'), new TableHeader(key: 'apps', label: 'Apps'),
new TableHeader(key: 'users', label: 'Users'), new TableHeader(key: 'users', label: 'Users'),
], DataCollection::class); ], DataCollection::class);
@ -85,7 +86,10 @@ public function save(): void
$this->form->validate(); $this->form->validate();
$this->attempt( $this->attempt(
action: function (): void { action: function (): void {
$role = $this->roleService->create(new CreateRoleData(name: $this->form->name)); $role = $this->roleService->create(new CreateRoleData(
name: $this->form->name,
priority: (int) $this->form->priority,
));
$this->appRoleService->assign(new AssignAppsToRoleData( $this->appRoleService->assign(new AssignAppsToRoleData(
roleId: $role->id, roleId: $role->id,
validUpto: $this->form->isUnlimited ? now()->addYears(15) : $this->form->validUpto, validUpto: $this->form->isUnlimited ? now()->addYears(15) : $this->form->validUpto,
@ -128,6 +132,10 @@ public function delete(int $roleId): void
<span class="font-medium text-gray-900">{{ $row->name }}</span> <span class="font-medium text-gray-900">{{ $row->name }}</span>
@endscope @endscope
@scope('cell_priority', $row)
<span class="text-gray-500 font-medium">{{ $row->priority }}</span>
@endscope
@scope('cell_apps', $row) @scope('cell_apps', $row)
@foreach($row->apps as $app) @foreach($row->apps as $app)
@php /** @var RoleAppData $app */ @endphp @php /** @var RoleAppData $app */ @endphp
@ -178,6 +186,14 @@ class="btn-sm btn-circle btn-error btn-soft"
autofocus autofocus
/> />
<x-mary-input
label="Priority"
wire:model="form.priority"
id="role-priority-input"
type="number"
placeholder="e.g. 1, 2, 3"
/>
<x-shared.choices <x-shared.choices
wire:model="form.appIds" wire:model="form.appIds"
:options="$appsSearchable" :options="$appsSearchable"

View File

@ -1,11 +1,11 @@
<?php <?php
use App\Concerns\{Confirmation, HandlesOperations}; use App\Concerns\{Confirmation, HandlesOperations};
use App\Data\Role\{AssignAppsToRoleData, AssignUsersToRoleData, RoleAppData}; use App\Data\Role\{AssignAppsToRoleData, AssignUsersToRoleData, RoleAppData, UpdateRolePriorityData};
use App\Data\Ui\TableHeader; use App\Data\Ui\TableHeader;
use App\Livewire\Forms\AssignAppToRoleForm; use App\Livewire\Forms\AssignAppToRoleForm;
use App\Models\Role; use App\Models\Role;
use App\Services\AppRoleService; use App\Services\{AppRoleService, RoleService};
use Livewire\Attributes\{Computed, Layout, Title}; use Livewire\Attributes\{Computed, Layout, Title};
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Livewire\Component; use Livewire\Component;
@ -24,6 +24,9 @@ class extends Component {
public int $roleId = 0; public int $roleId = 0;
public string $roleName = ''; public string $roleName = '';
public int $rolePriority = 0;
public bool $showEditPriorityModal = false;
public ?int $newPriority = null;
public bool $showAddAppModal = false; public bool $showAddAppModal = false;
// UI State for Editing // UI State for Editing
@ -50,9 +53,12 @@ class extends Component {
#[DataCollectionOf(TableHeader::class)] #[DataCollectionOf(TableHeader::class)]
public DataCollection $userHeaders; public DataCollection $userHeaders;
public function boot(AppRoleService $appRoleService): void protected RoleService $roleService;
public function boot(AppRoleService $appRoleService, RoleService $roleService): void
{ {
$this->appRoleService = $appRoleService; $this->appRoleService = $appRoleService;
$this->roleService = $roleService;
} }
public function mount(int $id): void public function mount(int $id): void
@ -62,6 +68,7 @@ public function mount(int $id): void
$role = Role::query()->findOrFail($id); $role = Role::query()->findOrFail($id);
$this->roleId = $role->id; $this->roleId = $role->id;
$this->roleName = $role->name; $this->roleName = $role->name;
$this->rolePriority = $role->priority;
}, },
onError: function (): void { onError: function (): void {
$this->redirectRoute('access-manager.roles-and-apps.index', navigate: true); $this->redirectRoute('access-manager.roles-and-apps.index', navigate: true);
@ -253,6 +260,32 @@ public function detachUser(int $userId): void
successMessage: 'User removed from role.', successMessage: 'User removed from role.',
); );
} }
public function openEditPriorityModal(): void
{
$this->newPriority = $this->rolePriority;
$this->resetErrorBag();
$this->showEditPriorityModal = true;
}
public function savePriority(): void
{
$this->validate([
'newPriority' => 'required|integer|min:0',
]);
$this->attempt(
action: function (): void {
$this->roleService->updatePriority(new UpdateRolePriorityData(
roleId: $this->roleId,
priority: (int) $this->newPriority,
));
$this->rolePriority = (int) $this->newPriority;
$this->showEditPriorityModal = false;
},
successMessage: 'Role priority updated successfully!',
);
}
}; };
?> ?>
@ -268,6 +301,16 @@ class="btn-sm btn-ghost"
tooltip-right="Back To Roles & Apps" tooltip-right="Back To Roles & Apps"
/> />
<h1 class="text-2xl font-semibold text-gray-900">{{ $roleName }}</h1> <h1 class="text-2xl font-semibold text-gray-900">{{ $roleName }}</h1>
<div class="flex items-center gap-1.5">
<x-mary-badge class="badge-soft badge-secondary" value="Priority: {{ $rolePriority }}"/>
<x-mary-button
icon="lucide.pencil"
wire:click="openEditPriorityModal"
spinner="openEditPriorityModal"
class="btn-sm btn-ghost btn-circle"
tooltip="Edit priority"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -440,4 +483,26 @@ class="toggle-secondary"
</x-slot:actions> </x-slot:actions>
</x-mary-form> </x-mary-form>
</x-mary-modal> </x-mary-modal>
<x-mary-modal wire:model="showEditPriorityModal" title="Edit Role Priority">
<x-mary-form wire:submit="savePriority">
<x-mary-input
label="Priority"
wire:model="newPriority"
id="role-new-priority-input"
type="number"
placeholder="e.g. 1, 2, 3"
autofocus
/>
<x-slot:actions>
<x-mary-button @click="$wire.showEditPriorityModal = false" class="btn-ghost">
Cancel
</x-mary-button>
<x-mary-button type="submit" class="btn-primary" icon="lucide.save" spinner="savePriority">
Save
</x-mary-button>
</x-slot:actions>
</x-mary-form>
</x-mary-modal>
</div> </div>