Feat: implement role and app management
- Added services (`RoleService`, `AppRoleService`, `AppsPermissionService`) to handle role creation, app assignments, and permissions. - Introduced new Livewire forms for creating roles and assigning apps (`CreateRoleForm`, `AssignAppToRoleForm`). - Built dynamic Blade views for role and app management, including modals and reusable components. - Removed outdated `fluxui-development` skill documentation.
This commit is contained in:
parent
495c0ea2d8
commit
c680c1ae6e
@ -1,81 +0,0 @@
|
|||||||
---
|
|
||||||
name: fluxui-development
|
|
||||||
description: "Use this skill for Flux UI development in Livewire applications only. Trigger when working with <flux:*> components, building or customizing Livewire component UIs, creating forms, modals, tables, or other interactive elements. Covers: flux: components (buttons, inputs, modals, forms, tables, date-pickers, kanban, badges, tooltips, etc.), component composition, Tailwind CSS styling, Heroicons/Lucide icon integration, validation patterns, responsive design, and theming. Do not use for non-Livewire frameworks or non-component styling."
|
|
||||||
license: MIT
|
|
||||||
metadata:
|
|
||||||
author: laravel
|
|
||||||
---
|
|
||||||
|
|
||||||
# Flux UI Development
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Use `search-docs` for detailed Flux UI patterns and documentation.
|
|
||||||
|
|
||||||
## Basic Usage
|
|
||||||
|
|
||||||
This project uses the free edition of Flux UI, which includes all free components and variants but not Pro components.
|
|
||||||
|
|
||||||
Flux UI is a component library for Livewire built with Tailwind CSS. It provides components that are easy to use and customize.
|
|
||||||
|
|
||||||
Use Flux UI components when available. Fall back to standard Blade components when no Flux component exists for your needs.
|
|
||||||
|
|
||||||
<!-- Basic Button -->
|
|
||||||
```blade
|
|
||||||
<flux:button variant="primary">Click me</flux:button>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Components (Free Edition)
|
|
||||||
|
|
||||||
Available: avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, profile, radio, select, separator, skeleton, switch, text, textarea, tooltip
|
|
||||||
|
|
||||||
## Icons
|
|
||||||
|
|
||||||
Flux includes [Heroicons](https://heroicons.com/) as its default icon set. Search for exact icon names on the Heroicons site - do not guess or invent icon names.
|
|
||||||
|
|
||||||
<!-- Icon Button -->
|
|
||||||
```blade
|
|
||||||
<flux:button icon="arrow-down-tray">Export</flux:button>
|
|
||||||
```
|
|
||||||
|
|
||||||
For icons not available in Heroicons, use [Lucide](https://lucide.dev/). Import the icons you need with the Artisan command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php artisan flux:icon crown grip-vertical github
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Form Fields
|
|
||||||
|
|
||||||
<!-- Form Field -->
|
|
||||||
```blade
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>Email</flux:label>
|
|
||||||
<flux:input type="email" wire:model="email" />
|
|
||||||
<flux:error name="email" />
|
|
||||||
</flux:field>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modals
|
|
||||||
|
|
||||||
<!-- Modal -->
|
|
||||||
```blade
|
|
||||||
<flux:modal wire:model="showModal">
|
|
||||||
<flux:heading>Title</flux:heading>
|
|
||||||
<p>Content</p>
|
|
||||||
</flux:modal>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
1. Check component renders correctly
|
|
||||||
2. Test interactive states
|
|
||||||
3. Verify mobile responsiveness
|
|
||||||
|
|
||||||
## Common Pitfalls
|
|
||||||
|
|
||||||
- Trying to use Pro-only components in the free edition
|
|
||||||
- Not checking if a Flux component exists before creating custom implementations
|
|
||||||
- Forgetting to use the `search-docs` tool for component-specific documentation
|
|
||||||
- Not following existing project patterns for Flux usage
|
|
||||||
@ -18,5 +18,6 @@ public function __construct(
|
|||||||
public int $providerId,
|
public int $providerId,
|
||||||
#[MapInputName('connection_status_id')]
|
#[MapInputName('connection_status_id')]
|
||||||
public int $statusId,
|
public int $statusId,
|
||||||
|
public ?string $slug = null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
17
app/Data/Role/AssignAppsToRoleData.php
Normal file
17
app/Data/Role/AssignAppsToRoleData.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Role;
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Data;
|
||||||
|
|
||||||
|
final class AssignAppsToRoleData extends Data
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $roleId,
|
||||||
|
public int $duration,
|
||||||
|
/** @var int[] */
|
||||||
|
public array $appIds
|
||||||
|
) {}
|
||||||
|
}
|
||||||
14
app/Data/Role/CreateRoleData.php
Normal file
14
app/Data/Role/CreateRoleData.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Role;
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Data;
|
||||||
|
|
||||||
|
final class CreateRoleData extends Data
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $name,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
31
app/Data/Role/RoleAppData.php
Normal file
31
app/Data/Role/RoleAppData.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Role;
|
||||||
|
|
||||||
|
use App\Models\ConnectedApp;
|
||||||
|
use Spatie\LaravelData\Data;
|
||||||
|
|
||||||
|
final class RoleAppData extends Data
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $id,
|
||||||
|
public int $appId,
|
||||||
|
public string $appName,
|
||||||
|
public int $duration,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercept the Eloquent model and map it to the DTO properties.
|
||||||
|
*/
|
||||||
|
public static function fromModel(ConnectedApp $app): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
id: $app->pivot->id,
|
||||||
|
appId: $app->id,
|
||||||
|
appName: $app->name,
|
||||||
|
duration: $app->pivot->duration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/Data/Role/RoleData.php
Normal file
18
app/Data/Role/RoleData.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Role;
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||||
|
use Spatie\LaravelData\{Data, DataCollection};
|
||||||
|
|
||||||
|
final class RoleData extends Data
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $id,
|
||||||
|
public string $name,
|
||||||
|
#[DataCollectionOf(class: RoleAppData::class), ]
|
||||||
|
public ?DataCollection $apps = null
|
||||||
|
) {}
|
||||||
|
}
|
||||||
10
app/Enums/ConnectedAppPermissonEnum.php
Normal file
10
app/Enums/ConnectedAppPermissonEnum.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum ConnectedAppPermissonEnum: string
|
||||||
|
{
|
||||||
|
case Use = 'use';
|
||||||
|
}
|
||||||
21
app/Helpers/AppPermission.php
Normal file
21
app/Helpers/AppPermission.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
use App\Data\ConnectedApp\ConnectedAppData;
|
||||||
|
use App\Enums\ConnectedAppPermissonEnum;
|
||||||
|
|
||||||
|
class AppPermission
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generates the standard permission string.
|
||||||
|
*/
|
||||||
|
public static function name(ConnectedAppPermissonEnum $permission, string|ConnectedAppData $app): string
|
||||||
|
{
|
||||||
|
$slug = is_string($app) ? $app : $app->slug;
|
||||||
|
|
||||||
|
return "{$permission->value}-{$slug}";
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Livewire/Forms/AssignAppToRoleForm.php
Normal file
27
app/Livewire/Forms/AssignAppToRoleForm.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Livewire\Attributes\Validate;
|
||||||
|
use Livewire\Form;
|
||||||
|
|
||||||
|
class AssignAppToRoleForm extends Form
|
||||||
|
{
|
||||||
|
#[Validate([
|
||||||
|
'appIds' => 'required|array|min:1',
|
||||||
|
'appIds.*' => 'integer|exists:connected_apps,id',
|
||||||
|
])]
|
||||||
|
public array $appIds = [];
|
||||||
|
|
||||||
|
#[Validate('required|integer|min:1|max:3650')]
|
||||||
|
public int $duration = 30;
|
||||||
|
|
||||||
|
public function reset(mixed ...$properties): void
|
||||||
|
{
|
||||||
|
$this->resetErrorBag();
|
||||||
|
parent::reset(...$properties);
|
||||||
|
$this->duration = 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/Livewire/Forms/CreateRoleForm.php
Normal file
13
app/Livewire/Forms/CreateRoleForm.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Livewire\Attributes\Validate;
|
||||||
|
|
||||||
|
class CreateRoleForm extends AssignAppToRoleForm
|
||||||
|
{
|
||||||
|
#[Validate('required|string|max:255|min:3')]
|
||||||
|
public string $name = '';
|
||||||
|
}
|
||||||
111
app/Services/AppRoleService.php
Normal file
111
app/Services/AppRoleService.php
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Data\Role\{AssignAppsToRoleData, RoleAppData};
|
||||||
|
use App\Models\{AppRole, ConnectedApp};
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use NoDiscard;
|
||||||
|
use Spatie\LaravelData\DataCollection;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class AppRoleService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Assign connected apps to a role, updating the duration if already assigned.
|
||||||
|
*
|
||||||
|
* @throws Throwable when an error occurs during the assignment.
|
||||||
|
*/
|
||||||
|
public function assign(AssignAppsToRoleData $data): void
|
||||||
|
{
|
||||||
|
if (empty($data->appIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$records = array_map(fn (int $appId) => [
|
||||||
|
'role_id' => $data->roleId,
|
||||||
|
'app_id' => $appId,
|
||||||
|
'duration' => $data->duration,
|
||||||
|
], $data->appIds);
|
||||||
|
|
||||||
|
AppRole::query()->upsert(
|
||||||
|
$records,
|
||||||
|
['role_id', 'app_id'],
|
||||||
|
['duration']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an app-role assignment.
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException when id is invalid.
|
||||||
|
* @throws Throwable when an error occurs during the deletion.
|
||||||
|
*/
|
||||||
|
public function detach(int $id): void
|
||||||
|
{
|
||||||
|
if ($id <= 0) {
|
||||||
|
throw new InvalidArgumentException('Invalid id');
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($id): void {
|
||||||
|
AppRole::query()->findOrFail($id)->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all apps assigned to a given role.
|
||||||
|
*
|
||||||
|
* @return DataCollection<int, RoleAppData>
|
||||||
|
*/
|
||||||
|
#[NoDiscard]
|
||||||
|
public function getAppsForRole(int $roleId): DataCollection
|
||||||
|
{
|
||||||
|
$assignments = AppRole::query()
|
||||||
|
->with('app:id,name')
|
||||||
|
->where('role_id', $roleId)
|
||||||
|
->orderBy('id')
|
||||||
|
->get(['id', 'app_id', 'role_id', 'duration']);
|
||||||
|
|
||||||
|
return RoleAppData::collect(
|
||||||
|
$assignments->map(fn (AppRole $row): array => [
|
||||||
|
'id' => $row->id,
|
||||||
|
'appId' => $row->app_id,
|
||||||
|
'appName' => $row->app->name,
|
||||||
|
'duration' => $row->duration,
|
||||||
|
])->all(),
|
||||||
|
DataCollection::class,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search available apps for the select dropdown.
|
||||||
|
*/
|
||||||
|
#[NoDiscard]
|
||||||
|
public function searchAppsForSelect(string $search = ''): Collection
|
||||||
|
{
|
||||||
|
return ConnectedApp::query()
|
||||||
|
->select(['id', 'name'])
|
||||||
|
->when($search, function ($query, $search): void {
|
||||||
|
$query->where('name', 'like', '%'.$search.'%');
|
||||||
|
})
|
||||||
|
->limit(50) // Limit the results so the dropdown doesn't lag
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all connected app IDs for the "Select All" feature.
|
||||||
|
*
|
||||||
|
* @return array<int>
|
||||||
|
*/
|
||||||
|
#[NoDiscard]
|
||||||
|
public function getAllAppIds(): array
|
||||||
|
{
|
||||||
|
// Replace 'ConnectedApp' with your actual model name
|
||||||
|
return ConnectedApp::pluck('id')->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Services/AppsPermissionService.php
Normal file
32
app/Services/AppsPermissionService.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Data\ConnectedApp\ConnectedAppData;
|
||||||
|
use App\Enums\ConnectedAppPermissonEnum;
|
||||||
|
use App\Helpers\AppPermission;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class AppsPermissionService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new permission for the connected app.
|
||||||
|
*
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function add(ConnectedAppData $connectedApp, ConnectedAppPermissonEnum $permission): void
|
||||||
|
{
|
||||||
|
if (! $connectedApp->slug) {
|
||||||
|
$connectedApp->slug = Str::slug($connectedApp->name);
|
||||||
|
}
|
||||||
|
$permissionName = AppPermission::name($permission, $connectedApp);
|
||||||
|
DB::transaction(function () use ($permissionName): void {
|
||||||
|
Permission::firstOrCreate(['name' => $permissionName]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
use App\Data\ConnectedApp\{ConnectAppRequest, ConnectedAppData};
|
use App\Data\ConnectedApp\{ConnectAppRequest, ConnectedAppData};
|
||||||
use App\Data\Ui\ConnectedApps\ConnectedAppData as UiConnectedAppData;
|
use App\Data\Ui\ConnectedApps\ConnectedAppData as UiConnectedAppData;
|
||||||
|
use App\Enums\ConnectedAppPermissonEnum;
|
||||||
use App\Models\ConnectedApp;
|
use App\Models\ConnectedApp;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@ -15,6 +16,10 @@
|
|||||||
|
|
||||||
final class ConnectedAppService
|
final class ConnectedAppService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AppsPermissionService $permissionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new connected app.
|
* Create a new connected app.
|
||||||
*
|
*
|
||||||
@ -23,9 +28,13 @@ final class ConnectedAppService
|
|||||||
public function create(ConnectAppRequest $data): void
|
public function create(ConnectAppRequest $data): void
|
||||||
{
|
{
|
||||||
DB::transaction(
|
DB::transaction(
|
||||||
fn () => ConnectedApp::query()->create(
|
function () use ($data): void {
|
||||||
$data->toArray()
|
$connectedApp = ConnectedApp::query()->create(
|
||||||
)
|
$data->toArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->permissionService->add(ConnectedAppData::from($connectedApp), ConnectedAppPermissonEnum::Use);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
64
app/Services/RoleService.php
Normal file
64
app/Services/RoleService.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Data\Role\{CreateRoleData, RoleData};
|
||||||
|
use App\Models\Role;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use NoDiscard;
|
||||||
|
use Spatie\LaravelData\PaginatedDataCollection;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class RoleService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new role.
|
||||||
|
* Returns the created role DTO
|
||||||
|
*
|
||||||
|
* @throws Throwable when an error occurs during the creation.
|
||||||
|
*/
|
||||||
|
#[NoDiscard]
|
||||||
|
public function create(CreateRoleData $data): RoleData
|
||||||
|
{
|
||||||
|
$role = DB::transaction(fn () => Role::query()->create([
|
||||||
|
'name' => $data->name,
|
||||||
|
'guard_name' => 'web',
|
||||||
|
]));
|
||||||
|
|
||||||
|
return RoleData::from($role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException when id is invalid.
|
||||||
|
* @throws Throwable when an error occurs during the deletion.
|
||||||
|
*/
|
||||||
|
public function delete(int $id): void
|
||||||
|
{
|
||||||
|
if ($id <= 0) {
|
||||||
|
throw new InvalidArgumentException('Invalid id');
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($id): void {
|
||||||
|
Role::query()->findOrFail($id)->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Paginated Collection of roles with their connected apps DTO
|
||||||
|
*
|
||||||
|
* @return PaginatedDataCollection<int, RoleData>
|
||||||
|
*/
|
||||||
|
#[NoDiscard]
|
||||||
|
public function getAll(): PaginatedDataCollection
|
||||||
|
{
|
||||||
|
$roles = Role::query()
|
||||||
|
->with('apps')
|
||||||
|
->orderBy('id')
|
||||||
|
->paginate();
|
||||||
|
|
||||||
|
return RoleData::collect($roles, PaginatedDataCollection::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ public function up(): void
|
|||||||
$table->foreignIdFor(ConnectedApp::class, 'app_id');
|
$table->foreignIdFor(ConnectedApp::class, 'app_id');
|
||||||
$table->foreignId('role_id')->comment('role_id from spatie roles table');
|
$table->foreignId('role_id')->comment('role_id from spatie roles table');
|
||||||
$table->integer('duration')->comment('in days');
|
$table->integer('duration')->comment('in days');
|
||||||
$table->index(['app_id', 'role_id']);
|
$table->unique(['app_id', 'role_id']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,19 @@ public function navItems(): DataCollection
|
|||||||
], DataCollection::class)
|
], DataCollection::class)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
new NavItemData(
|
||||||
|
label: 'Access Manager',
|
||||||
|
icon: 'lucide.shield',
|
||||||
|
active_pattern: 'access-manager.*',
|
||||||
|
sub_menu: NavSubItemData::collect([
|
||||||
|
new NavSubItemData(
|
||||||
|
label: 'Roles and Apps',
|
||||||
|
route: 'access-manager.roles-and-apps',
|
||||||
|
active_pattern: 'access-manager.roles-and-apps',
|
||||||
|
),
|
||||||
|
], DataCollection::class)
|
||||||
|
),
|
||||||
|
|
||||||
new NavItemData(
|
new NavItemData(
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
icon: 'lucide.settings',
|
icon: 'lucide.settings',
|
||||||
|
|||||||
36
resources/views/components/shared/choices.blade.php
Normal file
36
resources/views/components/shared/choices.blade.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
@props([
|
||||||
|
'label' => 'Select Items',
|
||||||
|
'model', // Livewire property
|
||||||
|
'options', // The data collection passed from the parent
|
||||||
|
'searchFunction', // The parent method to call for searching
|
||||||
|
'selectAllAction', // The parent method to call for selecting all
|
||||||
|
'clearAction', // The parent method to call for clearing
|
||||||
|
'placeholder' => 'Search...',
|
||||||
|
'noResultText' => 'No items found.'
|
||||||
|
])
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend w-full mb-1 flex items-end justify-between">
|
||||||
|
<label class="text-xs font-semibold text-">{{ $label }}</label>
|
||||||
|
|
||||||
|
<div class="flex gap-3 text-xs">
|
||||||
|
<button type="button" wire:click="{{ $selectAllAction }}" class="text-primary hover:underline">
|
||||||
|
Select All
|
||||||
|
</button>
|
||||||
|
<button type="button" wire:click="{{ $clearAction }}" class="text-error hover:underline">
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<x-mary-choices
|
||||||
|
wire:model="{{ $model }}"
|
||||||
|
:options="$options"
|
||||||
|
search-function="{{ $searchFunction }}"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
:no-result-text="$noResultText"
|
||||||
|
:placeholder="$placeholder"
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Concerns\HandlesOperations;
|
||||||
|
use App\Livewire\Forms\CreateRoleForm;
|
||||||
|
use App\Services\AppRoleService;
|
||||||
|
use App\Data\Role\{AssignAppsToRoleData, CreateRoleData, RoleAppData};
|
||||||
|
use App\Data\Ui\TableHeader;
|
||||||
|
use App\Services\RoleService;
|
||||||
|
use Livewire\Attributes\{Computed, Layout, Title, Validate};
|
||||||
|
use Livewire\Component;
|
||||||
|
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||||
|
use Spatie\LaravelData\DataCollection;
|
||||||
|
use Spatie\LaravelData\PaginatedDataCollection;
|
||||||
|
|
||||||
|
new
|
||||||
|
#[Title('Roles and Apps')]
|
||||||
|
class extends Component {
|
||||||
|
use HandlesOperations;
|
||||||
|
|
||||||
|
protected AppRoleService $appRoleService;
|
||||||
|
public bool $showCreateModal = false;
|
||||||
|
public CreateRoleForm $form;
|
||||||
|
protected RoleService $roleService;
|
||||||
|
public array $appsSearchable = [];
|
||||||
|
|
||||||
|
#[DataCollectionOf(TableHeader::class)]
|
||||||
|
public DataCollection $headers;
|
||||||
|
|
||||||
|
public function boot(RoleService $roleService, AppRoleService $appRoleService): void
|
||||||
|
{
|
||||||
|
$this->roleService = $roleService;
|
||||||
|
$this->appRoleService = $appRoleService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->headers = TableHeader::collect([
|
||||||
|
new TableHeader(key: 'name', label: 'Name'),
|
||||||
|
new TableHeader(key: 'apps', label: 'Apps'),
|
||||||
|
new TableHeader(key: '', label: 'Users'),
|
||||||
|
], DataCollection::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function getRoles(): PaginatedDataCollection
|
||||||
|
{
|
||||||
|
return $this->roleService->getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function searchApps(string $value = ''): void
|
||||||
|
{
|
||||||
|
$this->appsSearchable = $this
|
||||||
|
->appRoleService
|
||||||
|
->searchAppsForSelect($value)
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectAllApps(): void
|
||||||
|
{
|
||||||
|
$this->form->appIds = $this->appRoleService->getAllAppIds();
|
||||||
|
$this->appsSearchable = $this->appRoleService->searchAppsForSelect('')->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearApps(): void
|
||||||
|
{
|
||||||
|
$this->form->appIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function openCreateModal(): void
|
||||||
|
{
|
||||||
|
$this->form->reset();
|
||||||
|
$this->searchApps();
|
||||||
|
$this->showCreateModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(): void
|
||||||
|
{
|
||||||
|
$this->form->validate();
|
||||||
|
$this->attempt(
|
||||||
|
action: function (): void {
|
||||||
|
$role = $this->roleService->create(new CreateRoleData(name: $this->form->name));
|
||||||
|
$this->appRoleService->assign(
|
||||||
|
new AssignAppsToRoleData(
|
||||||
|
roleId: $role->id,
|
||||||
|
duration: $this->form->duration,
|
||||||
|
appIds: $this->form->appIds
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->showCreateModal = false;
|
||||||
|
$this->form->reset();
|
||||||
|
unset($this->getRoles);
|
||||||
|
},
|
||||||
|
successMessage: 'Role created successfully!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $roleId): void
|
||||||
|
{
|
||||||
|
$this->attempt(
|
||||||
|
action: function () use ($roleId): void {
|
||||||
|
$this->roleService->delete($roleId);
|
||||||
|
unset($this->getRoles);
|
||||||
|
},
|
||||||
|
successMessage: 'Role deleted successfully!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-shared.card title="Roles" icon="lucide-shield-check" class="pb-2">
|
||||||
|
<x-slot:actions>
|
||||||
|
<x-mary-button wire:click="openCreateModal" icon="lucide.plus">
|
||||||
|
Add new role
|
||||||
|
</x-mary-button>
|
||||||
|
</x-slot:actions>
|
||||||
|
|
||||||
|
<x-mary-table
|
||||||
|
:headers="$headers->toArray()"
|
||||||
|
:rows="$this->getRoles->items()"
|
||||||
|
>
|
||||||
|
@scope('cell_name', $row)
|
||||||
|
<span class="font-medium text-gray-900">{{ $row->name }}</span>
|
||||||
|
@endscope
|
||||||
|
|
||||||
|
@scope('cell_apps', $row)
|
||||||
|
@foreach($row->apps as $app)
|
||||||
|
@php /** @var RoleAppData $app */ @endphp
|
||||||
|
<x-mary-badge class="badge-soft badge-primary" :value="$app->appName"/>
|
||||||
|
@endforeach
|
||||||
|
@endscope
|
||||||
|
|
||||||
|
@scope('actions', $row)
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.eye"
|
||||||
|
:link="route('access-manager.roles-and-apps.show', $row->id)"
|
||||||
|
wire:navigate
|
||||||
|
tooltip="View details"
|
||||||
|
class="btn-sm btn-circle btn-soft"
|
||||||
|
/>
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.trash"
|
||||||
|
wire:click="delete({{ $row->id }})"
|
||||||
|
spinner="delete({{ $row->id }})"
|
||||||
|
variant="danger"
|
||||||
|
class="btn-sm btn-circle btn-error btn-soft"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@endscope
|
||||||
|
</x-mary-table>
|
||||||
|
</x-shared.card>
|
||||||
|
|
||||||
|
<x-mary-modal wire:model="showCreateModal" title="Add New Role">
|
||||||
|
<x-mary-form wire:submit="save">
|
||||||
|
<x-mary-input
|
||||||
|
label="Role Name"
|
||||||
|
wire:model="form.name"
|
||||||
|
id="role-name-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. editor, manager"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
<x-shared.choices
|
||||||
|
model="form.appIds"
|
||||||
|
:options="$appsSearchable"
|
||||||
|
search-function="searchApps"
|
||||||
|
select-all-action="selectAllApps"
|
||||||
|
clear-action="clearApps"
|
||||||
|
label="Apps"
|
||||||
|
/>
|
||||||
|
<x-mary-input
|
||||||
|
label="Duration"
|
||||||
|
wire:model="form.duration"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</x-slot:actions>
|
||||||
|
</x-mary-form>
|
||||||
|
</x-mary-modal>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,421 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Concerns\HandlesOperations;
|
||||||
|
use App\Data\Role\{AssignAppsToRoleData, RoleAppData};
|
||||||
|
use App\Data\Ui\TableHeader;
|
||||||
|
use App\Livewire\Forms\AssignAppToRoleForm;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Services\AppRoleService;
|
||||||
|
use Livewire\Attributes\{Computed, Layout, Title};
|
||||||
|
use Livewire\Component;
|
||||||
|
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||||
|
use Spatie\LaravelData\DataCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property-read DataCollection $apps
|
||||||
|
*/
|
||||||
|
new
|
||||||
|
#[Layout('layouts.app.sidebar')]
|
||||||
|
#[Title('Role Details')]
|
||||||
|
class extends Component {
|
||||||
|
use HandlesOperations;
|
||||||
|
|
||||||
|
public int $roleId = 0;
|
||||||
|
|
||||||
|
public string $roleName = '';
|
||||||
|
|
||||||
|
public bool $showAddAppModal = false;
|
||||||
|
|
||||||
|
// UI State for Editing
|
||||||
|
public bool $isEditingApp = false;
|
||||||
|
public ?int $editingAssignmentId = null;
|
||||||
|
public string $editingAppName = '';
|
||||||
|
|
||||||
|
public AssignAppToRoleForm $assignForm;
|
||||||
|
|
||||||
|
public array $appsSearchable = [];
|
||||||
|
|
||||||
|
public bool $showAddUserModal = false;
|
||||||
|
|
||||||
|
public ?int $selectedUserId = null;
|
||||||
|
|
||||||
|
public array $usersSearchable = [];
|
||||||
|
|
||||||
|
protected AppRoleService $appRoleService;
|
||||||
|
|
||||||
|
#[DataCollectionOf(TableHeader::class)]
|
||||||
|
public DataCollection $appHeaders;
|
||||||
|
|
||||||
|
#[DataCollectionOf(TableHeader::class)]
|
||||||
|
public DataCollection $userHeaders;
|
||||||
|
|
||||||
|
public function boot(AppRoleService $appRoleService): void
|
||||||
|
{
|
||||||
|
$this->appRoleService = $appRoleService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(int $id): void
|
||||||
|
{
|
||||||
|
$this->attempt(
|
||||||
|
action: function () use ($id): void {
|
||||||
|
$role = Role::query()->findOrFail($id);
|
||||||
|
$this->roleId = $role->id;
|
||||||
|
$this->roleName = $role->name;
|
||||||
|
},
|
||||||
|
onError: function (): void {
|
||||||
|
$this->redirectRoute('access-manager.roles-and-apps', navigate: true);
|
||||||
|
},
|
||||||
|
showSuccess: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->appHeaders = TableHeader::collect([
|
||||||
|
new TableHeader(key: 'app_name', label: 'App'),
|
||||||
|
new TableHeader(key: 'duration', label: 'Duration (days)'),
|
||||||
|
], DataCollection::class);
|
||||||
|
|
||||||
|
$this->userHeaders = TableHeader::collect([
|
||||||
|
new TableHeader(key: 'name', label: 'Name'),
|
||||||
|
new TableHeader(key: 'email', label: 'Email'),
|
||||||
|
], DataCollection::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function apps(): DataCollection
|
||||||
|
{
|
||||||
|
return $this->appRoleService->getAppsForRole($this->roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Replace with a service method that returns user DTOs for this role.
|
||||||
|
*
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
#[Computed]
|
||||||
|
public function getRoleUsers(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function openAddAppModal(): void
|
||||||
|
{
|
||||||
|
$this->isEditingApp = false;
|
||||||
|
$this->editingAssignmentId = null;
|
||||||
|
$this->editingAppName = '';
|
||||||
|
|
||||||
|
$this->assignForm->reset();
|
||||||
|
$this->resetErrorBag();
|
||||||
|
$this->searchApps('');
|
||||||
|
|
||||||
|
$this->showAddAppModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openEditAppModal(int $assignmentId): void
|
||||||
|
{
|
||||||
|
$this->isEditingApp = true;
|
||||||
|
$this->editingAssignmentId = $assignmentId;
|
||||||
|
$this->resetErrorBag();
|
||||||
|
|
||||||
|
$assignment = collect($this->apps())->firstWhere('id', $assignmentId);
|
||||||
|
|
||||||
|
if ($assignment) {
|
||||||
|
$this->assignForm->appIds = [$assignment['appId'] ?? $assignment['id']];
|
||||||
|
$this->assignForm->duration = $assignment['duration'];
|
||||||
|
|
||||||
|
$this->editingAppName = $assignment['appName'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->showAddAppModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveApp(): void
|
||||||
|
{
|
||||||
|
$this->assignForm->validate();
|
||||||
|
|
||||||
|
if ($this->isEditingApp) {
|
||||||
|
$this->updateApp();
|
||||||
|
} else {
|
||||||
|
$this->assignApp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchApps(string $value = ''): void
|
||||||
|
{
|
||||||
|
$this->appsSearchable = $this
|
||||||
|
->appRoleService
|
||||||
|
->searchAppsForSelect($value)
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectAllApps(): void
|
||||||
|
{
|
||||||
|
$this->assignForm->appIds = $this->appRoleService->getAllAppIds();
|
||||||
|
$this->appsSearchable = $this->appRoleService->searchAppsForSelect('')->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearApps(): void
|
||||||
|
{
|
||||||
|
$this->assignForm->appIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assignApp(): void
|
||||||
|
{
|
||||||
|
$this->attempt(
|
||||||
|
action: function (): void {
|
||||||
|
$this->appRoleService->assign(new AssignAppsToRoleData(
|
||||||
|
roleId: $this->roleId,
|
||||||
|
duration: $this->assignForm->duration,
|
||||||
|
appIds: $this->assignForm->appIds,
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->assignForm->reset();
|
||||||
|
$this->showAddAppModal = false;
|
||||||
|
unset($this->apps);
|
||||||
|
},
|
||||||
|
successMessage: 'Apps assigned successfully!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateApp(): void
|
||||||
|
{
|
||||||
|
$this->attempt(
|
||||||
|
action: function (): void {
|
||||||
|
$this->appRoleService->assign(new AssignAppsToRoleData(
|
||||||
|
roleId: $this->roleId,
|
||||||
|
duration: $this->assignForm->duration,
|
||||||
|
appIds: $this->assignForm->appIds,
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->assignForm->reset();
|
||||||
|
$this->showAddAppModal = false;
|
||||||
|
unset($this->apps);
|
||||||
|
},
|
||||||
|
successMessage: 'Apps updated successfully!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detachApp(int $assignmentId): void
|
||||||
|
{
|
||||||
|
$this->attempt(
|
||||||
|
action: function () use ($assignmentId): void {
|
||||||
|
$this->appRoleService->detach($assignmentId);
|
||||||
|
unset($this->apps);
|
||||||
|
},
|
||||||
|
successMessage: 'App removed from role.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function openAddUserModal(): void
|
||||||
|
{
|
||||||
|
$this->reset('selectedUserId');
|
||||||
|
$this->resetErrorBag();
|
||||||
|
$this->showAddUserModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchUsers(string $value = ''): void
|
||||||
|
{
|
||||||
|
$this->usersSearchable = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assignUser(): void
|
||||||
|
{
|
||||||
|
$this->success('User assignment not implemented yet.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detachUser(int $userId): void
|
||||||
|
{
|
||||||
|
$this->success('User removal not implemented yet.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<x-mary-button
|
||||||
|
:link="route('access-manager.roles-and-apps')"
|
||||||
|
wire:navigate
|
||||||
|
icon="lucide.arrow-left"
|
||||||
|
class="btn-sm btn-ghost"
|
||||||
|
tooltip-right="Back To Roles & Apps"
|
||||||
|
/>
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">{{ $roleName }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row gap-6 w-full">
|
||||||
|
|
||||||
|
<x-shared.card title="Connected Apps" icon="heroicon-o-squares-2x2" class="pb-2 w-full h-min">
|
||||||
|
<x-slot:actions>
|
||||||
|
<x-mary-button wire:click="openAddAppModal" icon="lucide.plus" class="btn-sm">
|
||||||
|
Add App
|
||||||
|
</x-mary-button>
|
||||||
|
</x-slot:actions>
|
||||||
|
|
||||||
|
@if ($this->apps->count())
|
||||||
|
<x-mary-table
|
||||||
|
:headers="$appHeaders->toArray()"
|
||||||
|
:rows="$this->apps->toArray()"
|
||||||
|
>
|
||||||
|
@scope('cell_app_name', $row)
|
||||||
|
<span class="font-medium text-gray-900">{{ $row['appName'] }}</span>
|
||||||
|
@endscope
|
||||||
|
|
||||||
|
@scope('cell_duration', $row)
|
||||||
|
{{ $row['duration'] }} days
|
||||||
|
@endscope
|
||||||
|
|
||||||
|
@scope('actions', $row)
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.square-pen"
|
||||||
|
wire:click="openEditAppModal({{ $row['id'] }})"
|
||||||
|
spinner="openEditAppModal({{ $row['id'] }})"
|
||||||
|
class="btn-sm btn-soft btn-circle"
|
||||||
|
/>
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.trash"
|
||||||
|
wire:click="detachApp({{ $row['id'] }})"
|
||||||
|
spinner="detachApp({{ $row['id'] }})"
|
||||||
|
class="btn-sm btn-error btn-soft btn-circle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@endscope
|
||||||
|
</x-mary-table>
|
||||||
|
@else
|
||||||
|
<p class="p-6 text-center text-sm text-gray-400">
|
||||||
|
No apps assigned to this role yet.
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</x-shared.card>
|
||||||
|
|
||||||
|
<x-shared.card title="Users" icon="heroicon-o-users" class="pb-2 w-full h-min">
|
||||||
|
<x-slot:actions>
|
||||||
|
<x-mary-button wire:click="openAddUserModal" icon="lucide.plus" class="btn-sm">
|
||||||
|
Add User
|
||||||
|
</x-mary-button>
|
||||||
|
</x-slot:actions>
|
||||||
|
|
||||||
|
@if (count($this->getRoleUsers))
|
||||||
|
<x-mary-table
|
||||||
|
:headers="$userHeaders->toArray()"
|
||||||
|
:rows="$this->getRoleUsers"
|
||||||
|
>
|
||||||
|
@scope('cell_name', $row)
|
||||||
|
<span class="font-medium text-gray-900">{{ $row['name'] }}</span>
|
||||||
|
@endscope
|
||||||
|
|
||||||
|
@scope('cell_email', $row)
|
||||||
|
{{ $row['email'] }}
|
||||||
|
@endscope
|
||||||
|
|
||||||
|
@scope('actions', $row)
|
||||||
|
<x-mary-button
|
||||||
|
icon="lucide.trash"
|
||||||
|
wire:click="detachUser({{ $row['id'] }})"
|
||||||
|
spinner="detachUser({{ $row['id'] }})"
|
||||||
|
variant="danger"
|
||||||
|
class="btn-sm btn-error btn-soft btn-circle"
|
||||||
|
/>
|
||||||
|
@endscope
|
||||||
|
</x-mary-table>
|
||||||
|
@else
|
||||||
|
<p class="p-6 text-center text-sm text-gray-400">
|
||||||
|
No users assigned to this role yet.
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</x-shared.card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-mary-modal wire:model="showAddAppModal"
|
||||||
|
title="{{ $isEditingApp ? 'Edit App Duration' : 'Add Apps to ' . $roleName }}">
|
||||||
|
<x-mary-form wire:submit="saveApp">
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
@if(!$isEditingApp)
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 flex items-end justify-between">
|
||||||
|
<label class="text-sm font-semibold text-gray-700">Connected Apps</label>
|
||||||
|
<div class="flex gap-3 text-xs">
|
||||||
|
<button type="button" wire:click="selectAllApps" class="text-primary hover:underline">
|
||||||
|
Select All
|
||||||
|
</button>
|
||||||
|
<button type="button" wire:click="clearApps" class="text-error hover:underline">
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-mary-choices
|
||||||
|
wire:model="assignForm.appIds"
|
||||||
|
:options="$appsSearchable"
|
||||||
|
search-function="searchApps"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
no-result-text="No apps found."
|
||||||
|
placeholder="Search apps..."
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<x-mary-input
|
||||||
|
label="Connected App"
|
||||||
|
wire:model="editingAppName"
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
class="bg-gray-50 text-gray-500 cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<x-mary-input
|
||||||
|
label="Duration (days)"
|
||||||
|
wire:model="assignForm.duration"
|
||||||
|
id="assign-duration-input"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="3650"
|
||||||
|
placeholder="30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-slot:actions>
|
||||||
|
<x-mary-button @click="$wire.showAddAppModal = false" class="btn-ghost">
|
||||||
|
Cancel
|
||||||
|
</x-mary-button>
|
||||||
|
<x-mary-button type="submit" icon="{{ $isEditingApp ? 'lucide.save' : 'lucide.link' }}"
|
||||||
|
spinner="saveApp">
|
||||||
|
{{ $isEditingApp ? 'Update Duration' : 'Assign Apps' }}
|
||||||
|
</x-mary-button>
|
||||||
|
</x-slot:actions>
|
||||||
|
</x-mary-form>
|
||||||
|
</x-mary-modal>
|
||||||
|
|
||||||
|
<x-mary-modal wire:model="showAddUserModal" title="Add User to {{ $roleName }}">
|
||||||
|
<x-mary-form wire:submit="assignUser">
|
||||||
|
<x-mary-choices
|
||||||
|
wire:model="selectedUserId"
|
||||||
|
:options="$usersSearchable"
|
||||||
|
search-function="searchUsers"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
no-result-text="No users found."
|
||||||
|
placeholder="Search users..."
|
||||||
|
searchable
|
||||||
|
single
|
||||||
|
label="User"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<x-slot:actions>
|
||||||
|
<x-mary-button @click="$wire.showAddUserModal = false" class="btn-ghost">
|
||||||
|
Cancel
|
||||||
|
</x-mary-button>
|
||||||
|
<x-mary-button type="submit" icon="lucide.user-plus" spinner="assignUser">
|
||||||
|
Assign User
|
||||||
|
</x-mary-button>
|
||||||
|
</x-slot:actions>
|
||||||
|
</x-mary-form>
|
||||||
|
</x-mary-modal>
|
||||||
|
</div>
|
||||||
@ -9,6 +9,11 @@
|
|||||||
Route::group(['middleware' => ['auth', 'verified']], function (): void {
|
Route::group(['middleware' => ['auth', 'verified']], function (): void {
|
||||||
Route::livewire('dashboard', 'pages::dashboard')->name('dashboard');
|
Route::livewire('dashboard', 'pages::dashboard')->name('dashboard');
|
||||||
|
|
||||||
|
Route::prefix('access-manager')->name('access-manager.')->group(function (): void {
|
||||||
|
Route::livewire('roles-and-apps', 'pages::access-manager.roles-and-apps.index')->name('roles-and-apps');
|
||||||
|
Route::livewire('roles-and-apps/{id}', 'pages::access-manager.roles-and-apps.show')->name('roles-and-apps.show');
|
||||||
|
});
|
||||||
|
|
||||||
Route::prefix('apps')->name('apps.')->group(function (): void {
|
Route::prefix('apps')->name('apps.')->group(function (): void {
|
||||||
Route::livewire('create', 'pages::connected-apps.create')->name('create');
|
Route::livewire('create', 'pages::connected-apps.create')->name('create');
|
||||||
Route::livewire('{id}/edit', 'pages::connected-apps.edit')->name('edit');
|
Route::livewire('{id}/edit', 'pages::connected-apps.edit')->name('edit');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user