feature: ConnectionProvider integration

- Added `ConnectionProviderData`, `NavItemData`, and `NavSubItemData` DTOs for structured data handling.
- Implemented CRUD operations for `ConnectionProviderService`, including transaction-safe create, update, and delete methods.
- Introduced Livewire components for ConnectionProvider list, edit, and create pages with dynamic and reusable UI elements.
- Refactored routes to consolidate paths under the `apps` prefix and updated navigation to dynamically include new sections for providers.
- Enhanced sidebar with structured submenu for ConnectionProviders and Settings using dynamic collections.
- Updated existing Create and Edit templates to use consistent route naming and validation handling.
- Replaced hardcoded navigation items with dynamically generated structures leveraging the `NavItemData` collection.
This commit is contained in:
= 2026-05-15 09:18:40 +00:00
parent bbb00c12b0
commit 48d2c1a47e
16 changed files with 460 additions and 127 deletions

2
CONTRIBUTING.MD Normal file
View File

@ -0,0 +1,2 @@
Always use `#[NoDiscard]` attribute in function which fetches data from DB. This ensures that
db does not called unnecessarily.

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Data\ConnectedApp;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
class ConnectionProviderData extends Data
{
public function __construct(
public int $id,
public string $name,
public string $slug
) {}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\Sidebar;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\{Data, DataCollection};
final class NavItemData extends Data
{
public function __construct(
public string $label,
public string $icon,
public string $active_pattern,
public ?string $route = null,
#[DataCollectionOf(NavSubItemData::class)]
public ?DataCollection $sub_menu = null,
) {}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\Sidebar;
use Spatie\LaravelData\Data;
final class NavSubItemData extends Data
{
public function __construct(
public string $label,
public string $route,
public string $active_pattern,
) {}
}

View File

@ -4,9 +4,8 @@
namespace App\Livewire\Forms; namespace App\Livewire\Forms;
use App\Data\ConnectedApp\StoreConnectionProviderRequestData; use App\Data\ConnectedApp\ConnectionProviderData;
use App\Models\ConnectionProvider; use App\Models\ConnectionProvider;
use App\Services\ConnectionProviderService;
use Closure; use Closure;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Form; use Livewire\Form;
@ -15,6 +14,11 @@ class StoreConnectionProviderForm extends Form
{ {
public string $name = ''; public string $name = '';
public function set(ConnectionProviderData $data): void
{
$this->name = $data->name;
}
public function rules(): array public function rules(): array
{ {
return [ return [
@ -39,14 +43,4 @@ function (string $attribute, mixed $value, Closure $fail): void {
], ],
]; ];
} }
public function save(): void
{
$this->validate();
$service = app(ConnectionProviderService::class);
$data = new StoreConnectionProviderRequestData(
name: $this->name,
);
$service->create($data);
}
} }

View File

@ -4,14 +4,22 @@
namespace App\Services; namespace App\Services;
use App\Data\ConnectedApp\{ConnectionProviderData, StoreConnectionProviderRequestData};
use App\Models\ConnectionProvider; use App\Models\ConnectionProvider;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
use NoDiscard;
use Spatie\LaravelData\PaginatedDataCollection;
use Throwable;
class ConnectionProviderService class ConnectionProviderService
{ {
/** /**
* @return Collection<int, ConnectionProvider> * @return Collection<int, ConnectionProvider>
*/ */
#[NoDiscard]
public function search(string $value): Collection public function search(string $value): Collection
{ {
return ConnectionProvider::query() return ConnectionProvider::query()
@ -19,4 +27,68 @@ public function search(string $value): Collection
->orWhereLike('name', "%{$value}%") ->orWhereLike('name', "%{$value}%")
->get(); ->get();
} }
/**
* @throws Throwable
*/
public function create(StoreConnectionProviderRequestData $data): void
{
DB::transaction(
function () use ($data): void {
ConnectionProvider::query()->create($data->toArray());
}
);
}
/**
* @return PaginatedDataCollection<int, ConnectionProviderData>
*/
#[NoDiscard]
public function getAll(): PaginatedDataCollection
{
$data = ConnectionProvider::query()->select(['id', 'name', 'slug'])->paginate();
return ConnectionProviderData::collect($data, PaginatedDataCollection::class);
}
/**
* @throws InvalidArgumentException when id is invalid
* @throws Throwable when an error occurs during the update.
*/
public function update(string $slug, StoreConnectionProviderRequestData $data): void
{
DB::transaction(
fn () => ConnectionProvider::query()
->where('slug', $slug)
->update($data->toArray())
);
}
/**
* @throws Throwable when an error occurs during the deletion.
* @throws InvalidArgumentException when id is invalid
*/
public function delete(string $slug): void
{
DB::transaction(
fn () => ConnectionProvider::query()
->where('slug', $slug)
->delete()
);
}
/**
* @throws ModelNotFoundException<ConnectionProviderData> when the connected app is not found.
* @throws InvalidArgumentException when id is invalid.
*/
#[NoDiscard]
public function getProvider(string $slug): ConnectionProviderData
{
$data = ConnectionProvider::query()
->select(['id', 'name', 'slug'])
->where('slug', $slug)
->first();
return ConnectionProviderData::from($data);
}
} }

View File

@ -1,6 +1,9 @@
<?php <?php
use App\Data\Ui\Sidebar\NavItemData;
use App\Data\Ui\Sidebar\NavSubItemData;
use Livewire\Component; use Livewire\Component;
use Spatie\LaravelData\DataCollection;
new class extends Component { new class extends Component {
/** Collapsed = icons-only mode */ /** Collapsed = icons-only mode */
@ -17,29 +20,55 @@ public function toggle(): void
session(['sidebar_collapsed' => $this->collapsed]); session(['sidebar_collapsed' => $this->collapsed]);
} }
public function navItems(): array /**
* @return DataCollection<int, NavItemData>
*/
public function navItems(): DataCollection
{ {
return [ return NavItemData::collect([
[ new NavItemData(
'label' => 'Dashboard', label: 'Dashboard',
'icon' => 'lucide.house', icon: 'lucide.house',
'route' => 'dashboard', active_pattern: 'dashboard',
'active_pattern' => 'dashboard', route: 'dashboard',
], ),
[
'label' => 'Users', new NavItemData(
'icon' => 'lucide.users', label: 'Apps',
'route' => 'home', icon: 'lucide.grid-2x2-plus',
'active_pattern' => 'home', active_pattern: 'apps.*',
], sub_menu: NavSubItemData::collect([
[ new NavSubItemData(
'label' => 'Apps', label: 'All Apps',
'icon' => 'lucide.grid-2x2-plus', route: 'apps.index',
'route' => 'connected-apps.index', active_pattern: 'apps.index'
// Using a wildcard ensures the tab stays active on nested routes like connected-apps.edit ),
'active_pattern' => 'connected-apps.*', new NavSubItemData(
], label: 'Providers',
]; route: 'apps.connectionProviders.index',
active_pattern: 'apps.connectionProviders.*'
)
], DataCollection::class)
),
new NavItemData(
label: 'Settings',
icon: 'lucide.settings',
active_pattern: 'settings.*',
sub_menu: NavSubItemData::collect([
new NavSubItemData(
label: 'Profile',
route: 'profile.edit',
active_pattern: 'settings.profile',
),
new NavSubItemData(
label: 'Security',
route: 'security.edit',
active_pattern: 'settings.security',
),
], DataCollection::class)
),
], DataCollection::class);
} }
}; };
?> ?>
@ -81,15 +110,77 @@ class="ml-3 text-sm font-semibold text-gray-900 whitespace-nowrap tracking-tight
</div> </div>
<nav <nav
class="flex-1 py-3 px-2 space-y-0.5 overflow-y-auto overflow-x-hidden" class="flex-1 py-3 px-2 space-y-1 overflow-y-auto overflow-x-hidden"
aria-label="Main navigation" aria-label="Main navigation"
> >
@foreach ($this->navItems() as $item) @foreach ($this->navItems() as $item)
{{-- Check if the current route matches the pattern --}} @php $isActive = request()->routeIs($item->active_pattern); @endphp
@php $isActive = request()->routeIs($item['active_pattern']); @endphp
@if ($item->sub_menu)
{{-- ITEM WITH SUBMENU --}}
<div x-data="{ expanded: {{ $isActive ? 'true' : 'false' }} }">
<button
@click="expanded = !expanded; if(collapsed) collapsed = false"
@class([
'group relative w-full flex items-center justify-between px-2 py-2.5 rounded-lg',
'text-sm font-medium transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400',
'bg-gray-100 text-gray-900' => $isActive,
'text-gray-500 hover:bg-gray-50 hover:text-gray-800' => ! $isActive,
])
aria-haspopup="true"
:aria-expanded="String(expanded)"
>
<div class="flex items-center gap-3">
<x-mary-icon :name="$item->icon" class="w-4 h-4 shrink-0"/>
<span
x-cloak
x-show="!collapsed"
class="whitespace-nowrap overflow-hidden leading-none"
>
{{ $item->label }}
</span>
</div>
{{-- Chevron Arrow (Hidden when collapsed) --}}
<span x-cloak x-show="!collapsed" class="shrink-0 transition-transform duration-200"
:class="expanded ? 'rotate-90' : ''">
<x-mary-icon name="lucide.chevron-right" class="w-4 h-4"/>
</span>
{{-- Tooltip (Collapsed Mode) --}}
<span x-cloak x-show="collapsed" role="tooltip"
class="pointer-events-none absolute left-full ml-3 z-50 whitespace-nowrap rounded-md bg-gray-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-xl opacity-0 group-hover:opacity-100 transition-opacity duration-150 delay-75">
{{ $item->label }}
<span
class="absolute right-full top-1/2 -translate-y-1/2 border-[5px] border-transparent border-r-gray-900"
aria-hidden="true"></span>
</span>
</button>
{{-- Submenu Dropdown --}}
<div x-cloak x-show="expanded && !collapsed" x-collapse>
<div class="mt-1 space-y-1 pl-9 pr-2">
@foreach ($item->sub_menu as $subItem)
@php $isSubActive = request()->routeIs($subItem->active_pattern); @endphp
<a <a
href="{{ route($item['route']) }}" href="{{ route($subItem->route) }}"
wire:navigate
@class([
'block px-3 py-2 rounded-md text-sm font-medium transition-colors',
'bg-gray-100 text-blue-700' => $isSubActive,
'text-gray-500 hover:bg-gray-50 hover:text-gray-900' => ! $isSubActive,
])
>
{{ $subItem->label }}
</a>
@endforeach
</div>
</div>
</div>
@else
{{-- STANDARD ITEM --}}
<a
href="{{ route($item->route) }}"
wire:navigate wire:navigate
@class([ @class([
'group relative flex items-center gap-3 px-2 py-2.5 rounded-lg', 'group relative flex items-center gap-3 px-2 py-2.5 rounded-lg',
@ -99,54 +190,35 @@ class="flex-1 py-3 px-2 space-y-0.5 overflow-y-auto overflow-x-hidden"
]) ])
aria-current="{{ $isActive ? 'page' : 'false' }}" aria-current="{{ $isActive ? 'page' : 'false' }}"
> >
{{-- Active pill indicator --}}
@if ($isActive) @if ($isActive)
<span <span class="absolute left-0 inset-y-2 w-0.5 rounded-r-full bg-blue-600"
class="absolute left-0 inset-y-2 w-0.5 rounded-r-full bg-blue-600" aria-hidden="true"></span>
aria-hidden="true"
></span>
@endif @endif
<x-mary-icon :name="$item['icon']" class="w-4 h-4"/> <x-mary-icon :name="$item->icon" class="w-4 h-4 shrink-0"/>
{{-- Label hidden when collapsed --}}
<span <span
x-cloak x-cloak
x-show="!collapsed" x-show="!collapsed"
x-transition:enter="transition-opacity duration-200 delay-100"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity duration-75"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="whitespace-nowrap overflow-hidden leading-none" class="whitespace-nowrap overflow-hidden leading-none"
> >
{{ $item['label'] }} {{ $item->label }}
</span> </span>
{{-- Tooltip only rendered & visible in collapsed mode --}} {{-- Tooltip (Collapsed Mode) --}}
<span x-cloak x-show="collapsed" role="tooltip"
class="pointer-events-none absolute left-full ml-3 z-50 whitespace-nowrap rounded-md bg-gray-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-xl opacity-0 group-hover:opacity-100 transition-opacity duration-150 delay-75">
{{ $item->label }}
<span <span
x-cloak class="absolute right-full top-1/2 -translate-y-1/2 border-[5px] border-transparent border-r-gray-900"
x-show="collapsed" aria-hidden="true"></span>
role="tooltip"
class="pointer-events-none absolute left-full ml-3 z-50
whitespace-nowrap rounded-md bg-gray-900 px-2.5 py-1.5
text-xs font-medium text-white shadow-xl
opacity-0 group-hover:opacity-100
transition-opacity duration-150 delay-75"
>
{{ $item['label'] }}
{{-- Arrow --}}
<span
class="absolute right-full top-1/2 -translate-y-1/2
border-[5px] border-transparent border-r-gray-900"
aria-hidden="true"
></span>
</span> </span>
</a> </a>
@endif
@endforeach @endforeach
</nav> </nav>
{{-- Toggle Footer --}}
<div class="border-t border-gray-100 px-2 py-3 shrink-0"> <div class="border-t border-gray-100 px-2 py-3 shrink-0">
<button <button
wire:click="toggle" wire:click="toggle"
@ -159,29 +231,13 @@ class="group w-full flex items-center gap-3 px-2 py-2.5 rounded-lg
:aria-label="collapsed ? 'Expand sidebar' : 'Collapse sidebar'" :aria-label="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
> >
<span class="flex-shrink-0 flex items-center justify-center w-5 h-5" aria-hidden="true"> <span class="flex-shrink-0 flex items-center justify-center w-5 h-5" aria-hidden="true">
<svg <svg :class="collapsed ? 'rotate-180' : 'rotate-0'"
:class="collapsed ? 'rotate-180' : 'rotate-0'" class="w-4 h-4 transition-transform duration-300 ease-in-out" fill="none" viewBox="0 0 24 24"
class="w-4 h-4 transition-transform duration-300 ease-in-out" stroke-width="2" stroke="currentColor">
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/>
</svg> </svg>
</span> </span>
<span x-cloak x-show="!collapsed" class="whitespace-nowrap">
<span
x-cloak
x-show="!collapsed"
x-transition:enter="transition-opacity duration-200 delay-100"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity duration-75"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="whitespace-nowrap"
>
Collapse Collapse
</span> </span>
</button> </button>

View File

@ -10,7 +10,7 @@
<div> <div>
<x-shared.card title="Connected Apps" icon="lucide-grid-2x2-plus" class="p-0"> <x-shared.card title="Connected Apps" icon="lucide-grid-2x2-plus" class="p-0">
<x-slot:actions> <x-slot:actions>
<x-shared.button :link="route('connected-apps.create')"> <x-shared.button :link="route('apps.create')">
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<x-lucide-plus class="w-5 h-5"/> <x-lucide-plus class="w-5 h-5"/>
{{ __('Connect App') }} {{ __('Connect App') }}

View File

@ -94,7 +94,7 @@ public function create()
public function goBack(): void public function goBack(): void
{ {
$this->redirectRoute('connected-apps.index', navigate: true); $this->redirectRoute('apps.index', navigate: true);
} }
}; };
?> ?>

View File

@ -44,7 +44,7 @@ public function mount(int $id): void
$this->recordId = $id; $this->recordId = $id;
}, },
onError: function () { onError: function () {
$this->redirectRoute('connected-apps.index', navigate: true); $this->redirectRoute('apps.index', navigate: true);
}, },
showSuccess: false showSuccess: false
); );
@ -98,7 +98,7 @@ public function save(): void
connectionStatusId: $this->form->statusId, connectionStatusId: $this->form->statusId,
); );
$this->service->update($this->recordId, $serviceData); $this->service->update($this->recordId, $serviceData);
$this->redirectRoute('connected-apps.index', navigate: true); $this->redirectRoute('apps.index', navigate: true);
}, },
); );
} }
@ -139,7 +139,7 @@ public function save(): void
/> />
</div> </div>
<x-slot:actions class="pr-2"> <x-slot:actions class="pr-2">
<x-mary-button :link="route('connected-apps.index')" wire:navigate> <x-mary-button :link="route('apps.index')" wire:navigate>
{{ __('Cancel') }} {{ __('Cancel') }}
</x-mary-button> </x-mary-button>
<x-mary-button type="submit" spinner="save" class="btn-primary"> <x-mary-button type="submit" spinner="save" class="btn-primary">

View File

@ -53,7 +53,7 @@ public function getConnectedApps(): PaginatedDataCollection
public function edit(int $appId): void public function edit(int $appId): void
{ {
$this->redirectRoute('connected-apps.edit', ['id' => $appId], navigate: true); $this->redirectRoute('apps.edit', ['id' => $appId], navigate: true);
} }
public function delete(int $appId): void public function delete(int $appId): void
@ -69,7 +69,7 @@ public function delete(int $appId): void
<div> <div>
<x-shared.card title="Connected Apps" class="pb-2"> <x-shared.card title="Connected Apps" class="pb-2">
<x-slot:actions> <x-slot:actions>
<x-mary-button :link="route('connected-apps.create')" wire:navigate icon="lucide.plus">Add App <x-mary-button :link="route('apps.create')" wire:navigate icon="lucide.plus">Add App
</x-mary-button> </x-mary-button>
</x-slot:actions> </x-slot:actions>
<x-mary-table <x-mary-table
@ -86,9 +86,10 @@ public function delete(int $appId): void
@endscope @endscope
@scope('actions', $row) @scope('actions', $row)
<div class="flex"> <div class="flex">
<x-mary-button icon="lucide.edit" wire:click="edit({{$row->id}})" spinner="edit" <x-mary-button icon="lucide.edit" wire:click="edit({{$row->id}})" spinner="edit({{$row->id}})"
class="mr-2 btn-sm text-gray-500"/> class="mr-2 btn-sm text-gray-500"/>
<x-mary-button icon="lucide.trash" wire:click="delete({{$row->id}})" spinner="delete" variant="danger" <x-mary-button icon="lucide.trash" wire:click="delete({{$row->id}})" spinner="delete({{$row->id}})"
variant="danger"
class="btn-sm btn-error btn-soft"/> class="btn-sm btn-error btn-soft"/>
</div> </div>
@endscope @endscope

View File

@ -1,11 +1,11 @@
<?php <?php
use App\Concerns\HandlesOperations; use App\Concerns\HandlesOperations;
use App\Data\ConnectedApp\StoreConnectionProviderRequestData;
use App\Livewire\Forms\StoreConnectionProviderForm; use App\Livewire\Forms\StoreConnectionProviderForm;
use App\Services\ConnectionProviderService;
use Laravel\Mcp\Server\Attributes\Title; use Laravel\Mcp\Server\Attributes\Title;
use Livewire\Component; use Livewire\Component;
use Mary\Traits\Toast;
use Illuminate\Support\Facades\Log;
new new
#[Title('Create Connection Provider')] #[Title('Create Connection Provider')]
@ -13,14 +13,25 @@ class extends Component {
use HandlesOperations; use HandlesOperations;
public StoreConnectionProviderForm $form; public StoreConnectionProviderForm $form;
protected ConnectionProviderService $service;
public function boot(ConnectionProviderService $service): void
{
$this->service = $service;
}
public function save(): void public function save(): void
{ {
$this->attempt( $this->attempt(
action: function () { action: function () {
$this->form->save(); $this->form->validate();
$this->redirectRoute('dashboard', navigate: true); $data = new StoreConnectionProviderRequestData(
} name: $this->form->name,
);
$this->service->create($data);
$this->form->reset();
$this->redirectRoute('apps.connectionProviders.index', navigate: true);
},
); );
} }
}; };

View File

@ -0,0 +1,60 @@
<?php
use App\Concerns\HandlesOperations;
use App\Data\ConnectedApp\StoreConnectionProviderRequestData;
use App\Livewire\Forms\StoreConnectionProviderForm;
use App\Services\ConnectionProviderService;
use Laravel\Mcp\Server\Attributes\Title;
use Livewire\Component;
use Mary\Traits\Toast;
use Illuminate\Support\Facades\Log;
new
#[Title('Edit Connection Provider')]
class extends Component {
use HandlesOperations;
public StoreConnectionProviderForm $form;
public string $currentProviderSlug;
protected ConnectionProviderService $service;
public function boot(ConnectionProviderService $service): void
{
$this->service = $service;
}
public function mount(string $slug): void
{
$this->currentProviderSlug = $slug;
$data = $this->service->getProvider($slug);
$this->form->set($data);
}
public function save(): void
{
$this->attempt(
action: function () {
$this->form->validate();
$data = new StoreConnectionProviderRequestData(
name: $this->form->name,
);
$this->service->update($this->currentProviderSlug, $data);
$this->redirectRoute('apps.connectionProviders.index', navigate: true);
},
);
}
};
?>
<div>
<x-shared.card :title="__('Edit Connection Provider')">
<x-mary-form :no-separator="true" class="p-4" wire:submit="save">
<x-mary-input wire:model.blur="form.name" :label="__('Name')"/>
<x-slot:actions>
<x-mary-button class="btn-primary" type="submit" spinner="save" wire:click="form.store"
:label="__('Save')"/>
</x-slot:actions>
</x-mary-form>
</x-shared.card>
</div>

View File

@ -0,0 +1,79 @@
<?php
use App\Concerns\HandlesOperations;
use App\Data\Ui\TableHeader;
use App\Services\ConnectionProviderService;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
new class extends Component {
use WithPagination, HandlesOperations;
protected ConnectionProviderService $service;
#[DataCollectionOf(TableHeader::class)]
public DataCollection $headers;
public function boot(ConnectionProviderService $service): void
{
$this->service = $service;
}
public function mount(): void
{
$this->initTableHeaders();
}
private function initTableHeaders(): void
{
$this->headers = TableHeader::collect([
new TableHeader(key: 'name', label: 'Name',),
], DataCollection::class);
}
#[Computed]
public function getAllProviders()
{
return $this->service->getAll();
}
public function edit(string $providerSlug): void
{
$this->redirectRoute('apps.connectionProviders.edit', ['slug' => $providerSlug]);
}
public function delete(string $providerSlug): void
{
$this->attempt(
action: fn() => $this->service->delete($providerSlug),
successMessage: "Connection Provider deleted!"
);
}
};
?>
<div>
<x-shared.card title="Connection Providers" class="pb-2">
<x-slot:actions>
<x-mary-button :link="route('apps.connectionProviders.create')" wire:navigate icon="lucide.plus">Add
Provider
</x-mary-button>
</x-slot:actions>
<x-mary-table :headers="$headers->toArray()" :rows="$this->getAllProviders->items()" with-pagination>
@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"/>
<x-mary-button icon="lucide.trash" wire:click="delete('{{$row->slug}}')"
spinner="delete('{{$row->slug}}')"
variant="danger"
class="btn-sm btn-error btn-soft"/>
</div>
@endscope
</x-mary-table>
</x-shared.card>
</div>

View File

@ -7,7 +7,7 @@
use Laravel\Fortify\Features; use Laravel\Fortify\Features;
Route::middleware(['auth'])->group(function (): void { Route::middleware(['auth'])->group(function (): void {
Route::redirect('settings', 'settings/profile'); Route::redirect('settings', 'settings/profile')->name('settings');
Route::livewire('settings/profile', Profile::class)->name('profile.edit'); Route::livewire('settings/profile', Profile::class)->name('profile.edit');
}); });

View File

@ -9,14 +9,16 @@
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('connected-apps')->name('connected-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');
Route::livewire('/', 'pages::connected-apps.index')->name('index'); Route::livewire('/', 'pages::connected-apps.index')->name('index');
});
Route::prefix('connection-providers')->name('connectionProviders.')->group(function (): void { Route::prefix('connection-providers')->name('connectionProviders.')->group(function (): void {
Route::livewire('create', 'pages::connecton-providers.create')->name('create'); Route::livewire('create', 'pages::connecton-providers.create')->name('create');
Route::livewire('{slug}/edit', 'pages::connecton-providers.edit')->name('edit');
Route::livewire('index', 'pages::connecton-providers.index')->name('index');
});
}); });
}); });