From c0086ddb6a922cb1a01ca1590002603ac4185abd Mon Sep 17 00:00:00 2001 From: = Date: Thu, 14 May 2026 14:03:53 +0000 Subject: [PATCH] feature: implement ConnectedApp management with creation and editing support - Added `ConnectedAppData` DTO for structured data representation. - Implemented Livewire components for ConnectedApp list (`index`) and edit (`edit`). - Integrated `ConnectedAppService` with CRUD operations. - Updated routing to include ConnectedApp management paths. - Enhanced UI with reusable components and dynamic dropdowns for protocols, providers, and statuses. --- app/Concerns/HandlesOperations.php | 7 +- app/Data/ConnectedApp/ConnectAppRequest.php | 1 - app/Data/ConnectedApp/ConnectedAppData.php | 22 +++ app/Data/Ui/README.md | 3 + app/Livewire/Forms/README.md | 7 +- app/Livewire/Forms/StoreConnectedAppFrom.php | 30 ++-- app/Services/ConnectedAppService.php | 44 ++++-- app/Services/ConnectionProviderService.php | 9 -- resources/css/app.css | 25 +-- .../components/dashboard-sidebar.blade.php | 39 +++-- .../pages/connected-apps/⚡edit.blade.php | 147 ++++++++++++++++++ .../pages/connected-apps/⚡index.blade.php | 76 +++++++++ routes/web.php | 4 +- 13 files changed, 342 insertions(+), 72 deletions(-) create mode 100644 app/Data/ConnectedApp/ConnectedAppData.php create mode 100644 app/Data/Ui/README.md create mode 100644 resources/views/pages/connected-apps/⚡edit.blade.php create mode 100644 resources/views/pages/connected-apps/⚡index.blade.php diff --git a/app/Concerns/HandlesOperations.php b/app/Concerns/HandlesOperations.php index e41dc1e..afd8c1c 100644 --- a/app/Concerns/HandlesOperations.php +++ b/app/Concerns/HandlesOperations.php @@ -13,12 +13,14 @@ trait HandlesOperations { use Toast; - public function attempt(callable $action, ?callable $onError = null, string $successMessage = 'Success !', string $errorMessage = 'Something gone wrong!'): void + public function attempt(callable $action, ?callable $onError = null, string $successMessage = 'Success !', string $errorMessage = 'Something gone wrong!', bool $showSuccess = true): void { try { // call the action callback $action(); - $this->success($successMessage); + if ($showSuccess) { + $this->success($successMessage); + } } catch (ValidationException $exception) { /** @@ -36,7 +38,6 @@ public function attempt(callable $action, ?callable $onError = null, string $suc } $this->error($errorMessage); Log::error($exception->getMessage()); - Log::error($exception->getTraceAsString()); } } } diff --git a/app/Data/ConnectedApp/ConnectAppRequest.php b/app/Data/ConnectedApp/ConnectAppRequest.php index aed2aa1..db4607d 100644 --- a/app/Data/ConnectedApp/ConnectAppRequest.php +++ b/app/Data/ConnectedApp/ConnectAppRequest.php @@ -15,7 +15,6 @@ public function __construct( public string $name, public int $connectionProviderId, public int $connectionProtocolId, - public ?string $connectionService = null, public ?string $slug = null, public ?int $connectionStatusId = null, ) {} diff --git a/app/Data/ConnectedApp/ConnectedAppData.php b/app/Data/ConnectedApp/ConnectedAppData.php new file mode 100644 index 0000000..464de8b --- /dev/null +++ b/app/Data/ConnectedApp/ConnectedAppData.php @@ -0,0 +1,22 @@ +validate(); + protected ConnectedAppService $service; - $serviceData = new ConnectAppRequest( - name: $this->name, - connectionProviderId: $this->providerId, - connectionProtocolId: $this->protocolId, - slug: str($this->name)->slug()->toString(), - connectionStatusId: $this->statusId, - ); - app(ConnectedAppService::class)->create($serviceData); - $this->reset(); + public function boot(ConnectedAppService $service): void + { + $this->service = $service; + } + + /** + * This sets the form to edit mode, after fetching the connected app of + * the given id. + */ + public function set(ConnectedAppData $data): void + { + $this->name = $data->name; + $this->protocolId = $data->protocolId; + $this->providerId = $data->providerId; + $this->statusId = $data->statusId; } } diff --git a/app/Services/ConnectedAppService.php b/app/Services/ConnectedAppService.php index 094f829..015353f 100644 --- a/app/Services/ConnectedAppService.php +++ b/app/Services/ConnectedAppService.php @@ -4,11 +4,13 @@ namespace App\Services; -use App\Data\ConnectedApp\ConnectAppRequest; -use App\Data\Ui\ConnectedApps\ConnectedAppData; +use App\Data\ConnectedApp\{ConnectAppRequest, ConnectedAppData}; +use App\Data\Ui\ConnectedApps\ConnectedAppData as UiConnectedAppData; use App\Models\ConnectedApp; +use http\Exception\InvalidArgumentException; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Facades\DB; -use Spatie\LaravelData\DataCollection; +use Spatie\LaravelData\PaginatedDataCollection; use Throwable; final class ConnectedAppService @@ -27,13 +29,37 @@ public function create(ConnectAppRequest $data): void ); } - /** - * @return DataCollection - */ - public function getAll(): DataCollection + public function update(int $id, ConnectAppRequest $data): void { - $data = ConnectedApp::with('provider', 'protocol', 'status')->get(); + if (0 === $id) { + throw new InvalidArgumentException('Invalid id'); + } + DB::transaction( + fn () => ConnectedApp::query() + ->where('id', $id) + ->update($data->toArray()) + ); + } - return ConnectedAppData::collect($data, DataCollection::class); + /** + * @return PaginatedDataCollection + */ + public function getAll(): PaginatedDataCollection + { + $data = ConnectedApp::with('provider:name,id', 'protocol:id,name', 'status:id,name') + ->paginate(); + + return UiConnectedAppData::collect($data, PaginatedDataCollection::class); + } + + /** + * @throws ModelNotFoundException + */ + public function getConnectedApp(int $id): ConnectedAppData + { + $data = ConnectedApp::with('provider:id', 'protocol:id', 'status:id') + ->findOrFail($id); + + return ConnectedAppData::from($data); } } diff --git a/app/Services/ConnectionProviderService.php b/app/Services/ConnectionProviderService.php index e1ee03f..3e7412a 100644 --- a/app/Services/ConnectionProviderService.php +++ b/app/Services/ConnectionProviderService.php @@ -4,20 +4,11 @@ namespace App\Services; -use App\Data\ConnectedApp\StoreConnectionProviderRequestData; use App\Models\ConnectionProvider; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\DB; class ConnectionProviderService { - public function create(StoreConnectionProviderRequestData $requestData): void - { - DB::transaction(function () use ($requestData): void { - ConnectionProvider::query()->create($requestData->toArray()); - }); - } - /** * @return Collection */ diff --git a/resources/css/app.css b/resources/css/app.css index 195d05c..5b70b22 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -118,15 +118,22 @@ @plugin "daisyui/theme" { /* Theme toggle */ @custom-variant dark (&:where(.dark, .dark *)); -/** -* Paginator - Traditional style -* Because Laravel defaults does not match well the design of daisyUI. -*/ +/*pagination style*/ +.mary-table-pagination { + button, span { + @apply text-xs border-none + } -.mary-table-pagination span[aria-current="page"] > span { - @apply bg-primary text-base-100 + button { + @apply cursor-pointer + } + + span[aria-current="page"] > span { + @apply bg-gray-200! + } + + button, span[aria-current="page"] > span, span[aria-disabled="true"] span { + @apply py-1 px-2 bg-gray-100 + } } -.mary-table-pagination button { - @apply cursor-pointer -} diff --git a/resources/views/components/dashboard-sidebar.blade.php b/resources/views/components/dashboard-sidebar.blade.php index 9dff3e1..7238a1b 100644 --- a/resources/views/components/dashboard-sidebar.blade.php +++ b/resources/views/components/dashboard-sidebar.blade.php @@ -1,18 +1,13 @@ active = $active; $this->collapsed = session('sidebar_collapsed', false); } @@ -26,16 +21,23 @@ public function navItems(): array { return [ [ - 'key' => 'dashboard', 'label' => 'Dashboard', - 'icon' => 'lucide-house', - 'route' => 'home', + 'icon' => 'lucide.house', + 'route' => 'dashboard', + 'active_pattern' => 'dashboard', ], [ - 'key' => 'users', 'label' => 'Users', - 'icon' => 'lucide-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.*', ], ]; } @@ -50,7 +52,6 @@ class="relative flex flex-col h-screen bg-white border-r border-gray-100 >
- - - @svg('lucide-shield-cog') - + @foreach ($this->navItems() as $item) - @php $isActive = $active === $item['key']; @endphp + {{-- Check if the current route matches the pattern --}} + @php $isActive = request()->routeIs($item['active_pattern']); @endphp @endif - - @svg($item['icon']) - + {{-- Label — hidden when collapsed --}}
- diff --git a/resources/views/pages/connected-apps/⚡edit.blade.php b/resources/views/pages/connected-apps/⚡edit.blade.php new file mode 100644 index 0000000..2ba9cc4 --- /dev/null +++ b/resources/views/pages/connected-apps/⚡edit.blade.php @@ -0,0 +1,147 @@ +service = $service; + } + + public function mount(int $id): void + { + $this->attempt( + action: function () use ($id) { + $record = $this->service->getConnectedApp($id); + $this->form->set($record); + $this->recordId = $id; + }, + onError: function () { + $this->redirectRoute('connected-apps.index', navigate: true); + }, + showSuccess: false + ); + } + + #[Computed] + public function providers() + { + return ConnectionProvider::query()->get(); + } + + + #[Computed] + public function protocols(): Collection + { + $protocolService = app(ConnectionProtocolService::class); + + return $protocolService->getAll()->map(function (ConnectionProtocol $protocol) { + $protocol->name = strtoupper($protocol->name); + return $protocol; + }); + } + + /** + * @returns DataCollection + */ + #[Computed] + public function connectionStatuses(): DataCollection + { + $statusService = app(ConnectionStatusService::class); + + return $statusService->getAll(); + } + + public function searchProvider(string $name): void + { + $providerService = app(ConnectionProviderService::class); + $this->providers = $providerService->search($name); + } + + public function save(): void + { + $this->attempt( + action: function () { + $this->form->validate(); + $serviceData = new ConnectAppRequest( + name: $this->form->name, + connectionProviderId: $this->form->providerId, + connectionProtocolId: $this->form->protocolId, + slug: str($this->form->name)->slug()->toString(), + connectionStatusId: $this->form->statusId, + ); + $this->service->update($this->recordId, $serviceData); + $this->redirectRoute('connected-apps.index', navigate: true); + }, + ); + } +}; +?> + +
+ +
+ + + + +
+ Cancel + + Save + +
+ +
+
diff --git a/resources/views/pages/connected-apps/⚡index.blade.php b/resources/views/pages/connected-apps/⚡index.blade.php new file mode 100644 index 0000000..39b2f2e --- /dev/null +++ b/resources/views/pages/connected-apps/⚡index.blade.php @@ -0,0 +1,76 @@ +initTableHeaders(); + } + + private function initTableHeaders(): void + { + $this->headers = TableHeader::collect([ + new TableHeader(key: 'name', label: 'Name',), + new TableHeader(key: 'provider', label: 'Provider'), + new TableHeader(key: 'protocol', label: 'Protocol'), + new TableHeader(key: 'status', label: 'Status'), + ], DataCollection::class); + } + + /** + * @return PaginatedDataCollection + */ + #[Computed] + public function getConnectedApps(): PaginatedDataCollection + { + $service = app(ConnectedAppService::class); + return $service->getAll(); + } + + public function edit(int $appId): void + { + $this->redirectRoute('connected-apps.edit', ['id' => $appId], navigate: true); + } +}; +?> + +
+ + + Add App + + + @scope('cell_protocol', $row) + {{$row->protocol->name}} + @endscope + + @scope('cell_status', $row) + + @endscope + @scope('actions', $row) +
+ + +
+ @endscope +
+
+
diff --git a/routes/web.php b/routes/web.php index 6365b45..c262f97 100644 --- a/routes/web.php +++ b/routes/web.php @@ -11,9 +11,11 @@ Route::prefix('connected-apps')->name('connected-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('users.')->group(function (): void { + Route::prefix('connection-providers')->name('connectionProviders.')->group(function (): void { Route::livewire('create', 'pages::connecton-providers.create')->name('create'); }); });