refactor: simplify data objects and improve role management components

- Removed redundant custom collections and `Wireable` trait from DTOs.
- Replaced custom collection properties with `DataCollection`.
- Updated role management views to use improved components, including avatars and toggle.
- Refined Blade directives for scoped slots.
- Minor cleanup in configuration and dependencies.
This commit is contained in:
= 2026-05-11 05:51:25 +00:00
parent 105123bb74
commit ede0064b54
17 changed files with 153 additions and 253 deletions

View File

@ -4,19 +4,24 @@
namespace App\Data\Ui\ManageRoles;
use App\Data\Users\UsersData;
use App\Data\Users\UserData;
use App\Enums\RolesStatus;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataCollection;
final class RoleData extends Data implements Wireable
final class RoleData extends Data
{
use WireableData;
public function __construct(
public string $name,
public UsersData $users,
public ServiceCollection $services,
/** @var DataCollection<int, UserData> */
#[DataCollectionOf(class: UserData::class),]
public DataCollection $users,
/** @var DataCollection<int, ServiceData> */
#[DataCollectionOf(class: ServiceData::class),]
public DataCollection $services,
public RolesStatus $status,
) {}
}

View File

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ManageRoles;
use Illuminate\Support\Collection;
/**
* @extends Collection<int, RoleData>
*/
final class RolesCollection extends Collection {}

View File

@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ManageRoles;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Data;
final class RolesData extends Data implements Wireable
{
use WireableData;
public function __construct(
public RolesCollection $roles,
) {}
}

View File

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ManageRoles;
use Illuminate\Support\Collection;
/**
* @extends Collection<int, ServiceData>
*/
final class ServiceCollection extends Collection {}

View File

@ -4,15 +4,9 @@
namespace App\Data\Ui\ManageRoles;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Data;
final class ServiceData extends Data implements Wireable
final class ServiceData extends Data
{
use WireableData;
public function __construct(
public string $name,
) {}
public function __construct(public string $name) {}
}

View File

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Data\Users;
use Illuminate\Support\Collection;
/**
* @extends Collection<int, UserData>
*/
final class UserCollection extends Collection {}

View File

@ -8,9 +8,5 @@
final class UserData extends Data
{
public function __construct(
public string $name,
public string $email,
) {}
public function __construct(public string $name, public string $email) {}
}

View File

@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Data\Users;
use Spatie\LaravelData\Data;
final class UsersData extends Data
{
public function __construct(
public UserCollection $users,
) {}
}

View File

@ -25,19 +25,18 @@ public function registerScopeDirective(): void
*
* https://github.com/konradkalemba/blade-components-scoped-slots
*/
Blade::directive('scope', function ($expression) {
Blade::directive("scope", function ($expression) {
// Split the expression by `top-level` commas (not in parentheses)
$directiveArguments = preg_split("/,(?![^\(\(]*[\)\)])/", $expression);
$directiveArguments = array_map('trim', $directiveArguments);
$directiveArguments = array_map("trim", $directiveArguments);
[$name, $functionArguments] = $directiveArguments;
// Build function "uses" to inject extra external variables
$uses = Arr::except(array_flip($directiveArguments), [$name, $functionArguments]);
$uses = array_flip($uses);
$uses[] = '$__env';
$uses[] = '$__bladeCompiler';
$uses = implode(',', $uses);
$uses = implode(",", $uses);
/**
* Slot names can`t contains dot , eg: `user.city`.
@ -45,11 +44,11 @@ public function registerScopeDirective(): void
*
* Later, on component it will be replaced back.
*/
$name = str_replace('.', '___', $name);
$name = str_replace(".", "___", $name);
return "<?php \$__bladeCompiler = \$__bladeCompiler ?? null; \$__env->slot({$name}, function({$functionArguments}) use ({$uses}) { \$loop = !empty(\$__env->getLoopStack()) ? (object) \$__env->getLoopStack()[0] : null; ?>";
});
Blade::directive('endscope', fn() => '<?php }); ?>');
Blade::directive("endscope", fn() => "<?php }); ?>");
}
}

View File

@ -171,7 +171,7 @@ public function render(): View|Closure|string
@if($actions)
<th scope="col" class="px-6 py-3 w-1 whitespace-nowrap">
<span class="sr-only">Actions</span>
<span>Actions</span>
</th>
@endif
</tr>
@ -182,7 +182,8 @@ public function render(): View|Closure|string
<tr
wire:key="{{ $uuid }}-{{ $k }}"
@class([
'bg-white border-b',
'bg-white',
'border-b' => $k < $rows->count() - 1,
$rowClasses($row),
'even:bg-gray-50 ' => $striped,
'hover:bg-gray-50' => !$noHover,

View File

@ -54,7 +54,7 @@
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"lint": [
"pint --parallel"

View File

@ -26,36 +26,36 @@
* is an array, it will try to convert from the first format that works,
* and will serialize dates using the first format from the array.
*/
'date_format' => DATE_ATOM,
"date_format" => DATE_ATOM,
/*
* When transforming or casting dates, the following timezone will be used to
* convert the date to the correct timezone. If set to null no timezone will
* be passed.
*/
'date_timezone' => null,
"date_timezone" => null,
/*
* It is possible to enable certain features of the package, these would otherwise
* be breaking changes, and thus they are disabled by default. In the next major
* version of the package, these features will be enabled by default.
*/
'features' => [
'cast_and_transform_iterables' => false,
"features" => [
"cast_and_transform_iterables" => false,
/*
* When trying to set a computed property value, the package will throw an exception.
* You can disable this behaviour by setting this option to true, which will then just
* ignore the value being passed into the computed property and recalculate it.
*/
'ignore_exception_when_trying_to_set_computed_property_value' => false,
"ignore_exception_when_trying_to_set_computed_property_value" => false,
],
/*
* Global transformers will take complex types and transform them into simple
* types.
*/
'transformers' => [
"transformers" => [
DateTimeInterface::class => DateTimeInterfaceTransformer::class,
Arrayable::class => ArrayableTransformer::class,
BackedEnum::class => EnumTransformer::class,
@ -65,7 +65,7 @@
* Global casts will cast values into complex types when creating a data
* object from simple types.
*/
'casts' => [
"casts" => [
DateTimeInterface::class => DateTimeInterfaceCast::class,
BackedEnum::class => EnumCast::class,
// Enumerable::class => Spatie\LaravelData\Casts\EnumerableCast::class,
@ -76,7 +76,7 @@
* validation rules to properties of a data object based upon
* the type of the property.
*/
'rule_inferrers' => [
"rule_inferrers" => [
SometimesRuleInferrer::class,
NullableRuleInferrer::class,
RequiredRuleInferrer::class,
@ -89,7 +89,7 @@
* it cannot normalize the payload. The normalizers below are used for
* every data object, unless overridden in a specific data object class.
*/
'normalizers' => [
"normalizers" => [
ModelNormalizer::class,
// Spatie\LaravelData\Normalizers\FormRequestNormalizer::class,
ArrayableNormalizer::class,
@ -103,7 +103,7 @@
* this key can be set globally here for all data objects. You can pass in
* `null` if you want to disable wrapping.
*/
'wrap' => null,
"wrap" => null,
/*
* Adds a specific caster to the Symphony VarDumper component which hides
@ -111,7 +111,7 @@
* by `dump` or `dd`. Can be 'enabled', 'disabled' or 'development'
* which will only enable the caster locally.
*/
'var_dumper_caster_mode' => 'development',
"var_dumper_caster_mode" => "development",
/*
* It is possible to skip the PHP reflection analysis of data objects
@ -124,18 +124,18 @@
* in seconds if you want the cache to clear after a certain
* timeframe.
*/
'structure_caching' => [
'enabled' => true,
'directories' => [app_path('Data')],
'cache' => [
'store' => env('CACHE_STORE', env('CACHE_DRIVER', 'file')),
'prefix' => 'laravel-data',
'duration' => null,
"structure_caching" => [
"enabled" => true,
"directories" => [app_path("Data")],
"cache" => [
"store" => env("CACHE_STORE", env("CACHE_DRIVER", "file")),
"prefix" => "laravel-data",
"duration" => null,
],
'reflection_discovery' => [
'enabled' => true,
'base_path' => base_path(),
'root_namespace' => null,
"reflection_discovery" => [
"enabled" => true,
"base_path" => base_path(),
"root_namespace" => null,
],
],
@ -144,23 +144,23 @@
* method. By default, only when a request is passed the data is being validated. This
* behaviour can be changed to always validate or to completely disable validation.
*/
'validation_strategy' => ValidationStrategy::OnlyRequests->value,
"validation_strategy" => ValidationStrategy::OnlyRequests->value,
/*
* A data object can map the names of its properties when transforming (output) or when
* creating (input). By default, the package will not map any names. You can set a
* global strategy here, or override it on a specific data object.
*/
'name_mapping_strategy' => [
'input' => null,
'output' => null,
"name_mapping_strategy" => [
"input" => null,
"output" => null,
],
/*
* When using an invalid include, exclude, only or except partial, the package will
* throw an exception. You can disable this behaviour by setting this option to true.
*/
'ignore_invalid_partials' => false,
"ignore_invalid_partials" => false,
/*
* When transforming a nested chain of data objects, the package can end up in an infinite
@ -168,41 +168,39 @@
* set as a safety measure to prevent this from happening. When set to null, the
* package will not enforce a maximum depth.
*/
'max_transformation_depth' => null,
"max_transformation_depth" => null,
/*
* When the maximum transformation depth is reached, the package will throw an exception.
* You can disable this behaviour by setting this option to true which will return an
* empty array.
*/
'throw_when_max_transformation_depth_reached' => true,
"throw_when_max_transformation_depth_reached" => true,
/*
* When using the `make:data` command, the package will use these settings to generate
* the data classes. You can override these settings by passing options to the command.
*/
'commands' => [
"commands" => [
/*
* Provides default configuration for the `make:data` command. These settings can be overridden with options
* passed directly to the `make:data` command for generating single Data classes, or if not set they will
* automatically fall back to these defaults. See `php artisan make:data --help` for more information
*/
'make' => [
"make" => [
/*
* The default namespace for generated Data classes. This exists under the application's root namespace,
* so the default 'Data` will end up as '\App\Data', and generated Data classes will be placed in the
* app/Data/ folder. Data classes can live anywhere, but this is where `make:data` will put them.
*/
'namespace' => 'Data',
"namespace" => "Data",
/*
* This suffix will be appended to all data classes generated by make:data, so that they are less likely
* to conflict with other related classes, controllers or models with a similar name without resorting
* to adding an alias for the Data object. Set to a blank string (not null) to disable.
*/
'suffix' => 'Data',
"suffix" => "Data",
],
],
@ -211,7 +209,7 @@
* these synths will automatically handle the data objects and their
* properties when used in a Livewire component.
*/
'livewire' => [
'enable_synths' => false,
"livewire" => [
"enable_synths" => true,
],
];

45
package-lock.json generated
View File

@ -208,9 +208,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -227,9 +224,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -246,9 +240,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -265,9 +256,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -284,9 +272,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -303,9 +288,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -394,9 +376,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -528,9 +507,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -547,9 +523,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -566,9 +539,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -585,9 +555,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -1182,9 +1149,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1205,9 +1169,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1228,9 +1189,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1251,9 +1209,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [

View File

@ -8,7 +8,10 @@
use App\Data\Users\UserCollection;
use App\Data\Users\UserData;
use App\Data\Users\UsersData;
use App\Enums\RolesStatus;
use Livewire\Component;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
new class extends Component {
/**
@ -19,79 +22,61 @@
* }>
*/
public array $header;
public RolesData $rolesData;
#[DataCollectionOf(class: RoleData::class)]
public DataCollection $roles;
public function mount(): void
{
$this->header = [
[
'key' => 'name',
'label' => 'Role Name',
"key" => "name",
"label" => "Role Name",
],
[
'key' => 'users',
'label' => 'User Assigned',
"key" => "users",
"label" => "User Assigned",
],
[
'key' => 'services',
'label' => 'Services',
"key" => "services",
"label" => "Services",
],
[
'key' => 'status',
'label' => 'Status',
]
"key" => "status",
"label" => "Status",
],
];
$this->roles = RoleData::collect(
[
new RoleData(
name: "Super Admin",
$this->rolesData = new RolesData(
roles: new RolesCollection([
new RoleData(
name: 'Super Admin',
users: new UsersData(
users: new UserCollection([
new UserData(
name: 'Kushal Saha',
email: 'kushal@example.com'
),
new UserData(
name: 'Test Example',
email: 'test@example.com'
)
]
)
),
services: new ServiceCollection(
users: UserData::collect(
[
new ServiceData(
name: 'Teams'
)
]
),
status: \App\Enums\RolesStatus::Active
),
new RoleData(
name: 'Super Admin',
users: new UsersData(
users: new UserCollection([
new UserData(
name: 'Kushal Saha',
email: 'kushal@example.com'
)
]
)
),
services: new ServiceCollection(
[
new ServiceData(
name: 'Teams'
new UserData(
name: "Kushal Saha",
email: "kushal@example.com",
),
new ServiceData(
name: 'Outlook'
)
]
new UserData(
name: "Test Example",
email: "test@example.com",
),
],
DataCollection::class,
),
status: \App\Enums\RolesStatus::Active
services: ServiceData::collect(
[
new ServiceData(name: "Teams"),
new ServiceData(name: "Outlook"),
],
DataCollection::class,
),
status: RolesStatus::Active,
),
])
],
DataCollection::class,
);
}
};
@ -101,34 +86,42 @@ public function mount(): void
<div>
<x-shared.card title="Roles" icon="lucide-shield-ellipsis" class="p-0">
<x-slot:actions>
<x-shared.button>
<div class="flex items-center gap-x-2">
<x-lucide-plus class="w-5 h-5"/>
{{ __('Add role') }}
</div>
<x-shared.button icon="lucide-plus">
{{ __('Add role') }}
</x-shared.button>
</x-slot:actions>
<x-table :headers="$header" :rows="$rolesData->roles">
@scope('cell_users', $row)
@foreach($row->users->users as $user)
<span
class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-gray-200 text-gray-800 font-bold text-[10px] tracking-tight">
{{implode('', array_map(fn($w) => strtoupper($w[0]), explode(' ', $user->name)))}}
</span>
@endforeach
<x-table :headers="$header" :rows="$roles">
@scope('cell_users', $role)
@php /** @var RoleData $role */ @endphp
<div class="flex -space-x-3">
@foreach($role->users as $user)
<x-shared.avatar>
<span class="text-xs font-bold text-center text-blue-800">
{{implode('', array_map(fn($w) => strtoupper($w[0]), explode(' ', $user->name)))}}
</span>
</x-shared.avatar>
@endforeach
</div>
@endscope
@scope('cell_status', $row)
@scope('cell_status', $role)
@php /** @var RoleData $role */ @endphp
<x-shared.badge class="bg-lime-200 text-lime-800 font-bold">
{{ ucfirst($row->status->name) }}
{{ ucfirst($role->status->name) }}
</x-shared.badge>
@endscope
@scope('cell_services', $row)
@foreach($row->services as $service)
@scope('cell_services', $role)
@php /** @var RoleData $role */ @endphp
@foreach($role->services as $service)
<x-shared.badge class="bg-indigo-200 text-indigo-800 font-bold">
{{$service->name}}
</x-shared.badge>
@endforeach
@endscope
@scope('actions', $role)
<x-shared.button icon="lucide-square-pen"/>
@endscope
</x-table>
</x-shared.card>
</div>

View File

@ -0,0 +1,3 @@
<span {{$attributes->twMerge(['class' => 'bg-blue-300 w-8 h-8 border border-white rounded-full flex items-center justify-center'])}}>
{{$slot}}
</span>

View File

@ -1,3 +1,11 @@
<button class="rounded-lg border border-gray-300 px-6 py-2 text-gray-700">
{{$slot}}
@props(['icon' => null])
<button {{$attributes->twMerge(['class' => 'rounded-lg border border-gray-300 px-4 py-2 text-gray-700'])}}>
<div class="gap-2 flex items-center">
@if($icon)
<span class="block">
@svg($icon, 'w-4 h-4')
</span>
@endif
{{$slot}}
</div>
</button>

View File

@ -0,0 +1,15 @@
@props(['disabled' => false, 'label' => null])
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
{{ $disabled ? 'disabled' : '' }}
{!! $attributes->merge(['class' => 'sr-only peer']) !!}
>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
@if($label)
<span class="ml-3 text-sm font-medium text-gray-900">{{ $label }}</span>
@endif
</label>