Feat: integrate reusable confirmation logic across components

- Applied `Confirmation` trait to multiple Livewire components for unified confirmation handling.
- Replaced inline confirmation modals with `requireConfirmation` method for streamlined interaction.
- Updated button styles and added reusable confirmation prompts for delete/detach actions.
- Enhanced `AppRoleService` and `ConnectionProviderService` to support deletion with confirmation.
This commit is contained in:
= 2026-05-22 12:59:12 +00:00
parent 5eca6d3602
commit 42737a6a2f
9 changed files with 85 additions and 59 deletions

View File

@ -63,9 +63,23 @@ private function assignAppPermissionsRole(Role $role, array $appIds): void
}, $appIds);
}
/**
* Detach an app from a role;
*
* @param array<int> $appIds Apps to be detached.
*/
public function detachApps(array $appIds): void
{
AppRole::query()
->whereIn('app_id', $appIds)
->delete();
}
/**
* Remove an app-role assignment.
*
* @param int $id The ID of the pivot table record.
*
* @throws InvalidArgumentException when id is invalid.
* @throws Throwable when an error occurs during the deletion.
*/

View File

@ -7,7 +7,7 @@
use App\Data\ConnectedApp\{ConnectAppRequest, ConnectedAppData};
use App\Data\Ui\ConnectedApps\ConnectedAppData as UiConnectedAppData;
use App\Enums\ConnectedAppPermissonEnum;
use App\Models\ConnectedApp;
use App\Models\{AppRole, ConnectedApp};
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
@ -69,6 +69,7 @@ public function delete(int $id): void
DB::transaction(
function () use ($id): void {
$app = ConnectedApp::query()->findOrFail($id);
AppRole::query()->where('app_id', $app->id)->delete();
$app->delete();
}
);

View File

@ -16,6 +16,10 @@
class ConnectionProviderService
{
public function __construct(
private readonly AppRoleService $appRoleService,
) {}
/**
* @return Collection<int, ConnectionProvider>
*/
@ -85,6 +89,7 @@ function () use ($slug): void {
->get()
->toArray();
$this->appRoleService->detachApps(array_column($deletedApps, 'id'));
// Note: This performs a mass soft-delete query.
// Model events for ConnectedApp will NOT fire.
$provider->connectedApps()->delete();

View File

@ -137,3 +137,7 @@ .mary-table-pagination {
}
}
/*set background color for table header*/
thead {
@apply bg-gray-50
}

View File

@ -1,5 +1,6 @@
<?php
use App\Concerns\Confirmation;
use App\Concerns\HandlesOperations;
use App\Livewire\Forms\CreateRoleForm;
use App\Services\AppRoleService;
@ -15,7 +16,7 @@
new
#[Title('Roles and Apps')]
class extends Component {
use HandlesOperations;
use HandlesOperations, Confirmation;
protected AppRoleService $appRoleService;
public bool $showCreateModal = false;
@ -67,6 +68,10 @@ public function clearApps(): void
$this->form->appIds = [];
}
public function test()
{
dd('hi');
}
public function openCreateModal(): void
{
@ -152,8 +157,8 @@ class="btn-sm btn-circle btn-soft"
/>
<x-mary-button
icon="lucide.trash"
wire:click="delete({{ $row->id }})"
spinner="delete({{ $row->id }})"
wire:click="requireConfirmation('delete', {{ $row->id }})"
spinner="requireConfirmation('delete', {{ $row->id }})"
variant="danger"
class="btn-sm btn-circle btn-error btn-soft"
/>
@ -181,17 +186,32 @@ class="btn-sm btn-circle btn-error btn-soft"
clear-action="clearApps"
label="Apps"
/>
<x-mary-input
label="Duration"
wire:model="form.duration"
type="number"
/>
<div x-data="{ isUnlimited: @entangle('assignForm.isUnlimited') }"
class="flex gap-2 items-baseline-last">
<div class="flex-1 transition-opacity duration-200"
x-bind:class="isUnlimited ? 'opacity-50 pointer-events-none' : ''">
<x-mary-datepicker
label="Date"
wire:model="form.validUpto"
icon="lucide.calendar"
x-bind:disabled="isUnlimited"
x-bind:readonly="isUnlimited"
/>
</div>
<x-mary-toggle
class="toggle-secondary"
label="Unlimited"
x-model="isUnlimited"
/>
</div>
<x-slot:actions>
<x-mary-button @click="$wire.showCreateModal = false" class="btn-ghost">
Cancel
</x-mary-button>
<x-mary-button type="submit" icon="lucide.save" spinner="save">
Save Role
<x-mary-button type="submit" class="btn-primary" icon="lucide.save" spinner="save">
Save
</x-mary-button>
</x-slot:actions>
</x-mary-form>

View File

@ -1,6 +1,6 @@
<?php
use App\Concerns\HandlesOperations;
use App\Concerns\{Confirmation, HandlesOperations};
use App\Data\Role\{AssignAppsToRoleData, AssignUsersToRoleData, RoleAppData};
use App\Data\Ui\TableHeader;
use App\Livewire\Forms\AssignAppToRoleForm;
@ -19,12 +19,11 @@
#[Layout('layouts.app.sidebar')]
#[Title('Role Details')]
class extends Component {
use HandlesOperations;
use HandlesOperations, Confirmation;
public int $roleId = 0;
public string $roleName = '';
public bool $showAddAppModal = false;
// UI State for Editing
@ -194,11 +193,11 @@ public function updateApp(): void
);
}
public function detachApp(int $appId): void
public function detachApp(int $id): void
{
$this->attempt(
action: function () use ($appId): void {
$this->appRoleService->detach($appId);
action: function () use ($id): void {
$this->appRoleService->detach($id);
unset($this->apps);
},
successMessage: 'App removed from role.',
@ -305,8 +304,8 @@ class="btn-sm btn-soft btn-circle"
/>
<x-mary-button
icon="lucide.trash"
wire:click="detachApp({{ $row['id'] }})"
spinner="detachApp({{ $row['id'] }})"
wire:click="requireConfirmation('detachApp', {{ $row['id'] }})"
spinner="requireConfirmation('detachApp', {{ $row['id'] }})"
class="btn-sm btn-error btn-soft btn-circle"
/>
</div>
@ -321,7 +320,8 @@ class="btn-sm btn-error btn-soft btn-circle"
<x-shared.card title="Users" icon="lucide-users-round" class="pb-2 w-full h-min">
<x-slot:actions>
<x-mary-button wire:click="openAddUserModal" icon="lucide.plus" class="btn-sm">
<x-mary-button wire:click="openAddUserModal" spinner="openAddUserModal" icon="lucide.plus"
class="btn-sm">
Add User
</x-mary-button>
</x-slot:actions>
@ -338,8 +338,8 @@ class="btn-sm btn-error btn-soft btn-circle"
@scope('actions', $row)
<x-mary-button
icon="lucide.trash"
wire:click="detachUser({{ $row['id'] }})"
spinner="detachUser({{ $row['id'] }})"
wire:click="requireConfirmation('detachUser', {{ $row['id'] }})"
spinner="requireConfirmation('detachUser', {{ $row['id'] }})"
variant="danger"
class="btn-sm btn-error btn-soft btn-circle"
/>

View File

@ -1,24 +1,21 @@
<?php
use App\Concerns\HandlesOperations;
use App\Concerns\{Confirmation, HandlesOperations};
use App\Data\Ui\TableHeader;
use App\Models\User;
use App\Services\UserService;
use App\Services\{UserService, RoleService};
use Livewire\Attributes\{Computed, Layout, Title};
use Livewire\Component;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\PaginatedDataCollection;
use App\Data\Role\RoleData;
use App\Data\Role\RoleAppData;
use Spatie\LaravelData\{DataCollection, PaginatedDataCollection};
use App\Data\Role\{RoleData, RoleAppData};
use App\Livewire\Forms\AssignRolesForm;
use App\Services\RoleService;
use App\Data\Permissions\DeactivatedPermissionData;
new
#[Title('Users')]
class extends Component {
use HandlesOperations;
use HandlesOperations, Confirmation;
protected UserService $userService;
protected RoleService $roleService;
@ -253,8 +250,8 @@ class="btn-sm btn-circle btn-soft"
/>
<x-mary-button
icon="lucide.trash"
wire:click="delete({{ $row->id }})"
spinner="delete({{ $row->id }})"
wire:click="requireConfirmation('delete', {{ $row->id }});"
spinner="requireConfirmation('delete', {{ $row->id }});"
variant="danger"
:tooltip="__('Delete user')"
class="btn-sm btn-circle btn-error btn-soft"

View File

@ -1,5 +1,6 @@
<?php
use App\Concerns\Confirmation;
use App\Concerns\HandlesOperations;
use App\Data\Ui\ConnectedApps\ConnectedAppData;
use App\Data\Ui\TableHeader;
@ -15,7 +16,7 @@
new
#[Title('Connected Apps')]
class extends Component {
use WithPagination, HandlesOperations;
use WithPagination, HandlesOperations, Confirmation;
protected ConnectedAppService $service;
@ -87,10 +88,11 @@ public function delete(int $appId): void
@scope('actions', $row)
<div class="flex">
<x-mary-button icon="lucide.edit" wire:click="edit({{$row->id}})" spinner="edit({{$row->id}})"
class="mr-2 btn-sm text-gray-500"/>
<x-mary-button icon="lucide.trash" wire:click="delete({{$row->id}})" spinner="delete({{$row->id}})"
class="mr-2 btn-sm btn-circle text-gray-500"/>
<x-mary-button icon="lucide.trash" wire:click="requireConfirmation('delete', {{$row->id}})"
spinner="requireConfirmation('delete', {{$row->id}})"
variant="danger"
class="btn-sm btn-error btn-soft"/>
class="btn-sm btn-circle btn-error btn-soft"/>
</div>
@endscope
</x-mary-table>

View File

@ -1,5 +1,6 @@
<?php
use App\Concerns\Confirmation;
use App\Concerns\HandlesOperations;
use App\Data\Ui\TableHeader;
use App\Services\ConnectionProviderService;
@ -10,14 +11,13 @@
use Spatie\LaravelData\DataCollection;
new class extends Component {
use WithPagination, HandlesOperations;
use WithPagination, HandlesOperations, Confirmation;
protected ConnectionProviderService $service;
#[DataCollectionOf(TableHeader::class)]
public DataCollection $headers;
public bool $cnfModal = false;
public string $selectedProvider = '';
public function boot(ConnectionProviderService $service): void
@ -50,10 +50,10 @@ public function edit(string $providerSlug): void
public function delete(): void
{
$this->cnfModal = false;
$this->attempt(
action: function () {
$this->service->delete($this->selectedProvider);
$this->selectedProvider = '';
}
);
}
@ -61,13 +61,7 @@ public function delete(): void
public function showModal(string $providerSlug): void
{
$this->selectedProvider = $providerSlug;
$this->cnfModal = true;
}
public function cancelModal(): void
{
$this->selectedProvider = '';
$this->cnfModal = false;
$this->requireConfirmation('delete', description: 'This will delete all the connected apps of this provider.');
}
};
?>
@ -83,24 +77,13 @@ public function cancelModal(): void
@scope('actions', $row)
<div class="flex">
<x-mary-button icon="lucide.edit" wire:click="edit('{{$row->slug}}')" spinner="edit('{{$row->slug}}')"
class="mr-2 btn-sm text-gray-500"/>
class="mr-2 btn-sm btn-circle text-gray-500"/>
<x-mary-button icon="lucide.trash" wire:click="showModal('{{$row->slug}}')"
spinner="showModal('{{$row->slug}}')"
variant="danger"
class="btn-sm btn-error btn-soft"/>
class="btn-sm btn-error btn-circle btn-soft"/>
</div>
@endscope
</x-mary-table>
</x-shared.card>
<x-mary-modal wire:model="cnfModal" title="Delete Confirmation" persistent separator>
<x-mary-alert title="This will delete all the connected apps of this provider."
icon="o-exclamation-triangle"
class="alert-warning"
/>
<x-slot:actions>
<x-mary-button label="Cancel" wire:click="cancelModal"/>
<x-mary-button label="Proceed" wire:click="delete" spinner="delete" class="btn-error"/>
</x-slot:actions>
</x-mary-modal>
</div>