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 string $name,
public int $priority,
) {}
}

View File

@ -13,6 +13,7 @@ final class RoleData extends Data
public function __construct(
public int $id,
public string $name,
public int $priority = 0,
#[DataCollectionOf(class: RoleAppData::class)]
public ?DataCollection $apps = null,
#[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')]
public string $name = '';
#[Validate('required|integer|min:0')]
public int $priority = 0;
}

View File

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

View File

@ -4,7 +4,7 @@
namespace App\Services;
use App\Data\Role\{CreateRoleData, RoleData};
use App\Data\Role\{CreateRoleData, RoleData, UpdateRolePriorityData};
use App\Models\Role;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
@ -25,6 +25,7 @@ public function create(CreateRoleData $data): RoleData
{
$role = DB::transaction(fn () => Role::query()->create([
'name' => $data->name,
'priority' => $data->priority,
'guard_name' => 'web',
]));
@ -93,8 +94,32 @@ public function getPermissionsGroupedByRole(array $roleIds): ?DataCollection
$roles = Role::query()
->with('permissions:id,name')
->whereIn('id', $roleIds)
->get(['id', 'name']);
->get(['id', 'name', 'priority']);
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([
new TableHeader(key: 'name', label: 'Name'),
new TableHeader(key: 'priority', label: 'Priority'),
new TableHeader(key: 'apps', label: 'Apps'),
new TableHeader(key: 'users', label: 'Users'),
], DataCollection::class);
@ -85,7 +86,10 @@ public function save(): void
$this->form->validate();
$this->attempt(
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(
roleId: $role->id,
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>
@endscope
@scope('cell_priority', $row)
<span class="text-gray-500 font-medium">{{ $row->priority }}</span>
@endscope
@scope('cell_apps', $row)
@foreach($row->apps as $app)
@php /** @var RoleAppData $app */ @endphp
@ -178,6 +186,14 @@ class="btn-sm btn-circle btn-error btn-soft"
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
wire:model="form.appIds"
:options="$appsSearchable"

View File

@ -1,11 +1,11 @@
<?php
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\Livewire\Forms\AssignAppToRoleForm;
use App\Models\Role;
use App\Services\AppRoleService;
use App\Services\{AppRoleService, RoleService};
use Livewire\Attributes\{Computed, Layout, Title};
use Illuminate\Support\Carbon;
use Livewire\Component;
@ -24,6 +24,9 @@ class extends Component {
public int $roleId = 0;
public string $roleName = '';
public int $rolePriority = 0;
public bool $showEditPriorityModal = false;
public ?int $newPriority = null;
public bool $showAddAppModal = false;
// UI State for Editing
@ -50,9 +53,12 @@ class extends Component {
#[DataCollectionOf(TableHeader::class)]
public DataCollection $userHeaders;
public function boot(AppRoleService $appRoleService): void
protected RoleService $roleService;
public function boot(AppRoleService $appRoleService, RoleService $roleService): void
{
$this->appRoleService = $appRoleService;
$this->roleService = $roleService;
}
public function mount(int $id): void
@ -62,6 +68,7 @@ public function mount(int $id): void
$role = Role::query()->findOrFail($id);
$this->roleId = $role->id;
$this->roleName = $role->name;
$this->rolePriority = $role->priority;
},
onError: function (): void {
$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.',
);
}
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"
/>
<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>
@ -440,4 +483,26 @@ class="toggle-secondary"
</x-slot:actions>
</x-mary-form>
</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>