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:
parent
bbb00c12b0
commit
48d2c1a47e
2
CONTRIBUTING.MD
Normal file
2
CONTRIBUTING.MD
Normal file
@ -0,0 +1,2 @@
|
||||
Always use `#[NoDiscard]` attribute in function which fetches data from DB. This ensures that
|
||||
db does not called unnecessarily.
|
||||
20
app/Data/ConnectedApp/ConnectionProviderData.php
Normal file
20
app/Data/ConnectedApp/ConnectionProviderData.php
Normal 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
|
||||
) {}
|
||||
|
||||
}
|
||||
20
app/Data/Ui/Sidebar/NavItemData.php
Normal file
20
app/Data/Ui/Sidebar/NavItemData.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
16
app/Data/Ui/Sidebar/NavSubItemData.php
Normal file
16
app/Data/Ui/Sidebar/NavSubItemData.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
@ -4,9 +4,8 @@
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use App\Data\ConnectedApp\StoreConnectionProviderRequestData;
|
||||
use App\Data\ConnectedApp\ConnectionProviderData;
|
||||
use App\Models\ConnectionProvider;
|
||||
use App\Services\ConnectionProviderService;
|
||||
use Closure;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Form;
|
||||
@ -15,6 +14,11 @@ class StoreConnectionProviderForm extends Form
|
||||
{
|
||||
public string $name = '';
|
||||
|
||||
public function set(ConnectionProviderData $data): void
|
||||
{
|
||||
$this->name = $data->name;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,22 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Data\ConnectedApp\{ConnectionProviderData, StoreConnectionProviderRequestData};
|
||||
use App\Models\ConnectionProvider;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use InvalidArgumentException;
|
||||
use NoDiscard;
|
||||
use Spatie\LaravelData\PaginatedDataCollection;
|
||||
use Throwable;
|
||||
|
||||
class ConnectionProviderService
|
||||
{
|
||||
/**
|
||||
* @return Collection<int, ConnectionProvider>
|
||||
*/
|
||||
#[NoDiscard]
|
||||
public function search(string $value): Collection
|
||||
{
|
||||
return ConnectionProvider::query()
|
||||
@ -19,4 +27,68 @@ public function search(string $value): Collection
|
||||
->orWhereLike('name', "%{$value}%")
|
||||
->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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Data\Ui\Sidebar\NavItemData;
|
||||
use App\Data\Ui\Sidebar\NavSubItemData;
|
||||
use Livewire\Component;
|
||||
use Spatie\LaravelData\DataCollection;
|
||||
|
||||
new class extends Component {
|
||||
/** Collapsed = icons-only mode */
|
||||
@ -17,29 +20,55 @@ public function toggle(): void
|
||||
session(['sidebar_collapsed' => $this->collapsed]);
|
||||
}
|
||||
|
||||
public function navItems(): array
|
||||
/**
|
||||
* @return DataCollection<int, NavItemData>
|
||||
*/
|
||||
public function navItems(): DataCollection
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => 'Dashboard',
|
||||
'icon' => 'lucide.house',
|
||||
'route' => 'dashboard',
|
||||
'active_pattern' => 'dashboard',
|
||||
],
|
||||
[
|
||||
'label' => 'Users',
|
||||
'icon' => 'lucide.users',
|
||||
'route' => 'home',
|
||||
'active_pattern' => 'home',
|
||||
],
|
||||
[
|
||||
'label' => 'Apps',
|
||||
'icon' => 'lucide.grid-2x2-plus',
|
||||
'route' => 'connected-apps.index',
|
||||
// Using a wildcard ensures the tab stays active on nested routes like connected-apps.edit
|
||||
'active_pattern' => 'connected-apps.*',
|
||||
],
|
||||
];
|
||||
return NavItemData::collect([
|
||||
new NavItemData(
|
||||
label: 'Dashboard',
|
||||
icon: 'lucide.house',
|
||||
active_pattern: 'dashboard',
|
||||
route: 'dashboard',
|
||||
),
|
||||
|
||||
new NavItemData(
|
||||
label: 'Apps',
|
||||
icon: 'lucide.grid-2x2-plus',
|
||||
active_pattern: 'apps.*',
|
||||
sub_menu: NavSubItemData::collect([
|
||||
new NavSubItemData(
|
||||
label: 'All Apps',
|
||||
route: 'apps.index',
|
||||
active_pattern: 'apps.index'
|
||||
),
|
||||
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,72 +110,115 @@ class="ml-3 text-sm font-semibold text-gray-900 whitespace-nowrap tracking-tight
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
@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
|
||||
|
||||
<a
|
||||
href="{{ route($item['route']) }}"
|
||||
wire:navigate
|
||||
@class([
|
||||
'group relative flex items-center gap-3 px-2 py-2.5 rounded-lg',
|
||||
'text-sm font-medium transition-colors duration-150',
|
||||
'bg-gray-100 text-gray-900' => $isActive,
|
||||
'text-gray-500 hover:bg-gray-50 hover:text-gray-800' => ! $isActive,
|
||||
])
|
||||
aria-current="{{ $isActive ? 'page' : 'false' }}"
|
||||
>
|
||||
{{-- Active pill indicator --}}
|
||||
@if ($isActive)
|
||||
<span
|
||||
class="absolute left-0 inset-y-2 w-0.5 rounded-r-full bg-blue-600"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
@endif
|
||||
@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>
|
||||
|
||||
<x-mary-icon :name="$item['icon']" class="w-4 h-4"/>
|
||||
{{-- 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>
|
||||
|
||||
{{-- Label — hidden when collapsed --}}
|
||||
<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 overflow-hidden leading-none"
|
||||
{{-- 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
|
||||
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
|
||||
@class([
|
||||
'group relative flex items-center gap-3 px-2 py-2.5 rounded-lg',
|
||||
'text-sm font-medium transition-colors duration-150',
|
||||
'bg-gray-100 text-gray-900' => $isActive,
|
||||
'text-gray-500 hover:bg-gray-50 hover:text-gray-800' => ! $isActive,
|
||||
])
|
||||
aria-current="{{ $isActive ? 'page' : 'false' }}"
|
||||
>
|
||||
{{ $item['label'] }}
|
||||
</span>
|
||||
@if ($isActive)
|
||||
<span class="absolute left-0 inset-y-2 w-0.5 rounded-r-full bg-blue-600"
|
||||
aria-hidden="true"></span>
|
||||
@endif
|
||||
|
||||
<x-mary-icon :name="$item->icon" class="w-4 h-4 shrink-0"/>
|
||||
|
||||
{{-- Tooltip — only rendered & visible in 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'] }}
|
||||
{{-- 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>
|
||||
</a>
|
||||
x-cloak
|
||||
x-show="!collapsed"
|
||||
class="whitespace-nowrap overflow-hidden leading-none"
|
||||
>
|
||||
{{ $item->label }}
|
||||
</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>
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
{{-- Toggle Footer --}}
|
||||
<div class="border-t border-gray-100 px-2 py-3 shrink-0">
|
||||
<button
|
||||
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'"
|
||||
>
|
||||
<span class="flex-shrink-0 flex items-center justify-center w-5 h-5" aria-hidden="true">
|
||||
<svg
|
||||
: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"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<svg :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"
|
||||
stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<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"
|
||||
>
|
||||
<span x-cloak x-show="!collapsed" class="whitespace-nowrap">
|
||||
Collapse
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<div>
|
||||
<x-shared.card title="Connected Apps" icon="lucide-grid-2x2-plus" class="p-0">
|
||||
<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">
|
||||
<x-lucide-plus class="w-5 h-5"/>
|
||||
{{ __('Connect App') }}
|
||||
|
||||
@ -94,7 +94,7 @@ public function create()
|
||||
|
||||
public function goBack(): void
|
||||
{
|
||||
$this->redirectRoute('connected-apps.index', navigate: true);
|
||||
$this->redirectRoute('apps.index', navigate: true);
|
||||
}
|
||||
};
|
||||
?>
|
||||
|
||||
@ -44,7 +44,7 @@ public function mount(int $id): void
|
||||
$this->recordId = $id;
|
||||
},
|
||||
onError: function () {
|
||||
$this->redirectRoute('connected-apps.index', navigate: true);
|
||||
$this->redirectRoute('apps.index', navigate: true);
|
||||
},
|
||||
showSuccess: false
|
||||
);
|
||||
@ -98,7 +98,7 @@ public function save(): void
|
||||
connectionStatusId: $this->form->statusId,
|
||||
);
|
||||
$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>
|
||||
<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') }}
|
||||
</x-mary-button>
|
||||
<x-mary-button type="submit" spinner="save" class="btn-primary">
|
||||
|
||||
@ -53,7 +53,7 @@ public function getConnectedApps(): PaginatedDataCollection
|
||||
|
||||
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
|
||||
@ -69,7 +69,7 @@ public function delete(int $appId): void
|
||||
<div>
|
||||
<x-shared.card title="Connected Apps" class="pb-2">
|
||||
<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-slot:actions>
|
||||
<x-mary-table
|
||||
@ -86,9 +86,10 @@ public function delete(int $appId): void
|
||||
@endscope
|
||||
@scope('actions', $row)
|
||||
<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"/>
|
||||
<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"/>
|
||||
</div>
|
||||
@endscope
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<?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('Create Connection Provider')]
|
||||
@ -13,14 +13,25 @@ class extends Component {
|
||||
use HandlesOperations;
|
||||
|
||||
public StoreConnectionProviderForm $form;
|
||||
protected ConnectionProviderService $service;
|
||||
|
||||
public function boot(ConnectionProviderService $service): void
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->attempt(
|
||||
action: function () {
|
||||
$this->form->save();
|
||||
$this->redirectRoute('dashboard', navigate: true);
|
||||
}
|
||||
$this->form->validate();
|
||||
$data = new StoreConnectionProviderRequestData(
|
||||
name: $this->form->name,
|
||||
);
|
||||
$this->service->create($data);
|
||||
$this->form->reset();
|
||||
$this->redirectRoute('apps.connectionProviders.index', navigate: true);
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
60
resources/views/pages/connecton-providers/⚡edit.blade.php
Normal file
60
resources/views/pages/connecton-providers/⚡edit.blade.php
Normal 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>
|
||||
79
resources/views/pages/connecton-providers/⚡index.blade.php
Normal file
79
resources/views/pages/connecton-providers/⚡index.blade.php
Normal 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>
|
||||
@ -7,7 +7,7 @@
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
@ -9,14 +9,16 @@
|
||||
Route::group(['middleware' => ['auth', 'verified']], function (): void {
|
||||
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('{id}/edit', 'pages::connected-apps.edit')->name('edit');
|
||||
Route::livewire('/', 'pages::connected-apps.index')->name('index');
|
||||
});
|
||||
|
||||
Route::prefix('connection-providers')->name('connectionProviders.')->group(function (): void {
|
||||
Route::livewire('create', 'pages::connecton-providers.create')->name('create');
|
||||
Route::prefix('connection-providers')->name('connectionProviders.')->group(function (): void {
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user