diff --git a/README.md b/README.md index 0df7b86..b22fa47 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ This package is designed to **accelerate API development** in Laravel by providi --- +## 📽️ Demo + + [![Watch the demo]](https://www.loom.com/share/631c4dbc840d475ea4516a8dafcc5b80) + ## 🖥️ Usage 1. **Access the UI** diff --git a/resources/views/components/add-files-methods.blade.php b/resources/views/components/add-files-methods.blade.php index 576e947..b518ecd 100644 --- a/resources/views/components/add-files-methods.blade.php +++ b/resources/views/components/add-files-methods.blade.php @@ -22,8 +22,8 @@
@@ -42,14 +42,8 @@
-
-
-
diff --git a/resources/views/components/add-new-field-modal.blade.php b/resources/views/components/add-new-field-modal.blade.php index 2a10bfb..3448692 100644 --- a/resources/views/components/add-new-field-modal.blade.php +++ b/resources/views/components/add-new-field-modal.blade.php @@ -1,4 +1,4 @@ -
@@ -20,15 +20,25 @@ class="w-full border rounded-md p-2 placeholder:text-gray-400 placeholder:text-[
-
- - - @error('data_type') - {{ $message }} - @enderror +
+
+ + + @error('data_type') + {{ $message }} + @enderror +
+ + +
+ + +

Enter comma-separated values for ENUM or SET.

+
@@ -47,6 +57,16 @@ class="w-full border rounded-md p-2 placeholder:text-gray-400 placeholder:text-[ @enderror
+
+
+ + Add in Fillables? +
+ @error('is_fillable') + {{ $message }} + @enderror +
+
@@ -58,7 +78,6 @@ class="w-full border rounded-md p-2 placeholder:text-gray-400 placeholder:text-[ @enderror
- @if ($this->is_foreign_key)
diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php index 7fe272e..588f71c 100644 --- a/resources/views/components/button.blade.php +++ b/resources/views/components/button.blade.php @@ -5,7 +5,7 @@ ]) @php - $buttonColor = in_array($title, ['Reset', 'Cancel','Delete']) ? 'bg-red-500' : 'bg-blue-500'; + $buttonColor = $title === 'Cancel' ? 'bg-gray-500': (in_array($title, ['Reset', 'Delete']) ? 'bg-red-500' : 'bg-blue-500'); @endphp - -
- Are You Sure you want to delete this field? -
- - -
- -
- -
- -
\ No newline at end of file diff --git a/resources/views/components/delete-modal.blade.php b/resources/views/components/delete-modal.blade.php new file mode 100644 index 0000000..6f0c427 --- /dev/null +++ b/resources/views/components/delete-modal.blade.php @@ -0,0 +1,21 @@ +@props(['isDeleteModalOpen', 'deleteModalTitle', 'deleteModalMessage', 'deleteModalAction']) + +
+ + + + +
+ {{ $deleteModalMessage }} +
+ +
+ +
+ +
+
+
\ No newline at end of file diff --git a/resources/views/components/delete-relation-modal.blade.php b/resources/views/components/delete-relation-modal.blade.php deleted file mode 100644 index c761463..0000000 --- a/resources/views/components/delete-relation-modal.blade.php +++ /dev/null @@ -1,21 +0,0 @@ -
- - - - - -
- Are You Sure you want to delete this Relation? -
- - -
- -
- -
-
-
\ No newline at end of file diff --git a/resources/views/components/eloqunet-relation-table.blade.php b/resources/views/components/eloqunet-relation-table.blade.php index aea8d38..658021a 100644 --- a/resources/views/components/eloqunet-relation-table.blade.php +++ b/resources/views/components/eloqunet-relation-table.blade.php @@ -27,7 +27,7 @@ {{$relation['intermediate_foreign_key'] ?? '-'}} {{$relation['intermediate_local_key'] ?? '-'}} - @@ -21,7 +23,7 @@ class="w-full mt-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-non @error('data') {{ $message }} @enderror -

Example: user_id: 1

+

Example: id,name

@@ -30,20 +32,32 @@ class="w-full mt-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-non class="w-full mt-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-400" /> @error('subject') {{ $message }} @enderror
- -
- - - @error('body') {{ $message }} @enderror +
+
+ + + @error('notification_blade_path') + {{ $message }} + @enderror +
- +
\ No newline at end of file diff --git a/resources/views/components/notification-table.blade.php b/resources/views/components/notification-table.blade.php new file mode 100644 index 0000000..4b8479a --- /dev/null +++ b/resources/views/components/notification-table.blade.php @@ -0,0 +1,42 @@ +@props(['notificationData']) + +
+ + + + + + + + + + + + + @if($notificationData) + @foreach ($notificationData as $notification) + + + + + + + + @endforeach + @else + + + + @endif + +
Class NameDataSubjectBlade View PathAction
{{$notification['class_name']}}{{$notification['data']}}{{$notification['subject']}}{{$notification['notification_blade_path']}} + + +
+ Any notification file not added yet. +
+
\ No newline at end of file diff --git a/resources/views/livewire/rest-api.blade.php b/resources/views/livewire/rest-api.blade.php index 25c9994..5c10725 100644 --- a/resources/views/livewire/rest-api.blade.php +++ b/resources/views/livewire/rest-api.blade.php @@ -100,6 +100,15 @@ class="fixed top-4 right-4 bg-{{ $color }}-100 border border-{{ $color }}-400 te + +
+
+

Notifications

+ +
+ +
+
@@ -112,10 +121,10 @@ class="fixed top-4 right-4 bg-{{ $color }}-100 border border-{{ $color }}-400 te
+ - - - + \ No newline at end of file diff --git a/src/Console/Commands/MakeMigration.php b/src/Console/Commands/MakeMigration.php index 90f1b9c..b56f6eb 100644 --- a/src/Console/Commands/MakeMigration.php +++ b/src/Console/Commands/MakeMigration.php @@ -125,7 +125,12 @@ protected function parseFieldsAndForeignKeys(): string $foreignLine .= ';'; $fieldLines[] = $foreignLine; - } else { + } elseif (in_array($type, ['enum', 'set']) && !empty($field['enum_values'])) { + $enumValues = array_map(fn($val) => "'".trim($val)."'", explode(',', $field['enum_values'])); + $valuesString = '[' . implode(', ', $enumValues) . ']'; + $fieldLines[] = "\$table->{$type}('{$name}', {$valuesString});"; + } + else { $fieldLines[] = "\$table->{$type}('{$name}');"; } } diff --git a/src/Http/Livewire/RestApi.php b/src/Http/Livewire/RestApi.php index c1502e7..5c93589 100644 --- a/src/Http/Livewire/RestApi.php +++ b/src/Http/Livewire/RestApi.php @@ -24,6 +24,33 @@ class RestApi extends Component public $intermediateFields = []; public $defaultFields = []; + public $files = [ + 'is_migration_file_added', + 'is_admin_crud_added', + 'is_policy_file_added', + 'is_observer_file_added', + 'is_service_file_added', + 'is_resource_file_added', + 'is_request_file_added', + 'is_factory_file_added', + 'is_model_file_added', + ]; + + public $methods = [ + 'is_store_method_added', + 'is_show_method_added', + 'is_update_method_added', + 'is_destroy_method_added', + 'is_index_method_added' + ]; + + public $traits = [ + 'is_boot_model_trait_added', + 'is_pagination_trait_added', + 'is_resource_filterable_trait_added', + 'is_has_uuid_trait_added', + 'is_has_user_action_trait_added', + ]; public $generalError = ''; public $errorMessage = ""; @@ -43,6 +70,7 @@ class RestApi extends Component public $isAddFieldModalOpen = false; public $isDeleteFieldModalOpen = false; public $isNotificationModalOpen = false; + public $isDeleteNotificationModalOpen = false; public $isResetFormModalOpen = false; // Form inputs @@ -57,7 +85,7 @@ class RestApi extends Component public $data_type, $column_name, $column_validation; // Notification properties - public $class_name, $data, $subject, $body; + public $class_name, $data, $subject, $notification_blade_path , $notificationId; // Method checkboxes public $is_index_method_added = true; @@ -72,7 +100,6 @@ class RestApi extends Component public $is_soft_delete_added = false; public $is_admin_crud_added = false; public $is_service_file_added = false; - public $is_notification_file_added = false; public $is_resource_file_added = false; public $is_request_file_added = false; public $is_trait_files_added = false; @@ -91,6 +118,14 @@ class RestApi extends Component public $is_has_uuid_trait_added = false; public $is_has_user_action_trait_added = false; public $isEditing = false; + public $is_fillable = true; + public $isDeleteModalOpen = false; + public $deleteModalTitle = ''; + public $deleteModalMessage = ''; + public $deleteModalAction = ''; + public $itemIdToDelete = null; + public $enum_values; + // Validation rules protected $rules = [ @@ -102,17 +137,17 @@ class RestApi extends Component 'related_model' => 'required|regex:/^[A-Z][A-Za-z]+$/', 'relation_type' => 'required', 'intermediate_model' => 'required|different:model_name|different:related_model|regex:/^[A-Z][A-Za-z]+$/', - 'foreign_key' => 'required|string|regex:/^[a-z]+(_[a-z]+)*$/', - 'local_key' => 'required|string|regex:/^[a-z]+(_[a-z]+)*$/', - 'intermediate_foreign_key' => 'required|string|regex:/^[a-z]+(_[a-z]+)*$/', - 'intermediate_local_key' => 'required|string|regex:/^[a-z]+(_[a-z]+)*$/', + 'foreign_key' => 'nullable|string|regex:/^[a-z]+(_[a-z]+)*$/', + 'local_key' => 'nullable|string|regex:/^[a-z]+(_[a-z]+)*$/', + 'intermediate_foreign_key' => 'nullable|string|regex:/^[a-z]+(_[a-z]+)*$/', + 'intermediate_local_key' => 'nullable|string|regex:/^[a-z]+(_[a-z]+)*$/', 'data_type' => 'required', 'column_name' => 'required|regex:/^[a-z_]+$/', 'column_validation' => 'required', 'class_name' => 'required|regex:/^[A-Z][A-Za-z]+$/', - 'data' => 'required|regex:/^[A-Za-z0-9_]+:[A-Za-z0-9_]+(?:,[A-Za-z0-9_]+:[A-Za-z0-9_]+)*$/', + 'data' => 'required|regex:/^([a-zA-Z0-9_]+)(,[a-zA-Z0-9_]+)*$/', 'subject' => 'required|regex:/^[A-Za-z_ ]+$/', - 'body' => 'required|regex:/^[A-Za-z_ ]+$/', + 'notification_blade_path' => 'nullable|regex:/^[a-zA-Z0-9_\/]+$/', 'foreign_model_name' => 'required|regex:/^[A-Za-z][a-z0-9_]*$/', 'on_delete_action' => 'nullable|in:restrict,cascade,set null,no action', 'on_update_action' => 'nullable|in:restrict,cascade,set null,no action', @@ -144,8 +179,8 @@ protected function getDefaultFields(): array { return [ ['column_name' => 'id', 'data_type' => 'auto_increment', 'column_validation' => 'required'], - ['column_name' => 'created_by', 'data_type' => 'integer', 'column_validation' => 'nullable'], - ['column_name' => 'updated_by', 'data_type' => 'integer', 'column_validation' => 'nullable'], + ['column_name' => 'created_by', 'data_type' => 'int', 'column_validation' => 'nullable'], + ['column_name' => 'updated_by', 'data_type' => 'int', 'column_validation' => 'nullable'], ['column_name' => 'created_at', 'data_type' => 'datetime', 'column_validation' => 'required'], ['column_name' => 'updated_at', 'data_type' => 'datetime', 'column_validation' => 'nullable'], ]; @@ -164,7 +199,7 @@ public function updatedIsSoftDeleteAdded() [ 'id' => 'deleted_by', 'column_name' => 'deleted_by', - 'data_type' => 'integer', + 'data_type' => 'int', 'column_validation' => 'nullable', ], ]; @@ -183,20 +218,7 @@ public function updatedIsSoftDeleteAdded() // select all files checkbox state public function updatedIsSelectAllFilesChecked($value) { - $files = [ - 'is_migration_file_added', - 'is_admin_crud_added', - 'is_policy_file_added', - 'is_observer_file_added', - 'is_service_file_added', - 'is_notification_file_added', - 'is_resource_file_added', - 'is_request_file_added', - 'is_factory_file_added', - 'is_model_file_added', - ]; - - foreach ($files as $file) { + foreach ($this->files as $file) { $this->$file = $value; } } @@ -204,15 +226,7 @@ public function updatedIsSelectAllFilesChecked($value) // select all methods checkbox state public function updatedIsSelectAllMethodsChecked($value) { - $methods = [ - 'is_store_method_added', - 'is_show_method_added', - 'is_update_method_added', - 'is_destroy_method_added', - 'is_index_method_added', - ]; - - foreach ($methods as $method) { + foreach ($this->methods as $method) { $this->$method = $value; } } @@ -220,15 +234,7 @@ public function updatedIsSelectAllMethodsChecked($value) // select all traits checkbox state public function updatedIsSelectAllTraitsChecked($value) { - $traits = [ - 'is_boot_model_trait_added', - 'is_pagination_trait_added', - 'is_resource_filterable_trait_added', - 'is_has_uuid_trait_added', - 'is_has_user_action_trait_added', - ]; - - foreach ($traits as $trait) { + foreach ($this->traits as $trait) { $this->$trait = $value; } } @@ -294,8 +300,26 @@ public function prefillQuery() public function updated($propertyName) { $this->validateOnly($propertyName); + + $this->updateCheckboxGroup($propertyName, $this->methods, 'is_select_all_methods_checked'); + $this->updateCheckboxGroup($propertyName, $this->files, 'is_select_all_files_checked'); + $this->updateCheckboxGroup($propertyName, $this->traits, 'is_select_all_traits_checked'); } + // Update checkbox group state based on individual checkbox state + private function updateCheckboxGroup($propertyName, $group, $selectAllProperty) + { + if (in_array($propertyName, $group)) { + $allChecked = true; + foreach ($group as $item) { + if (!$this->$item) { + $allChecked = false; + break; + } + } + $this->$selectAllProperty = $allChecked; + } + } // collect the model names when the modal is opened public function updatedIsAddRelModalOpen($value) @@ -309,14 +333,6 @@ public function updatedIsAddRelModalOpen($value) } } - // Update notification file checkbox state and open modal if checked - public function updatedIsNotificationFileAdded($value): void - { - if ($value) { - $this->isNotificationModalOpen = true; - } - } - public function validateFieldsAndMethods() { $this->errorMessage = ""; @@ -340,19 +356,22 @@ public function validateFieldsAndMethods() } // Open delete modal - public function openDeleteModal($id): void + public function openDeleteRelationModal($id): void { - $this->relationId = $id; - $this->isRelDeleteModalOpen = true; + $this->itemIdToDelete = $id; + $this->deleteModalTitle = "Delete Relation"; + $this->deleteModalMessage = "Are you sure you want to delete this relation?"; + $this->deleteModalAction = 'deleteRelation'; + $this->isDeleteModalOpen = true; } // Delete relation in table public function deleteRelation(): void { $this->relationData = array_filter($this->relationData, function ($relation) { - return $relation['id'] !== $this->relationId; + return $relation['id'] !== $this->itemIdToDelete; }); - $this->isRelDeleteModalOpen = false; + $this->isDeleteModalOpen = false; } // Open edit relation modal @@ -380,12 +399,14 @@ public function resetForm() public function resetModal() { $this->reset([ + 'isEditing', 'related_model', 'relation_type', 'intermediate_model', 'foreign_key', 'local_key', 'data_type', + 'enum_values', 'column_name', 'column_validation', 'fieldId', @@ -396,6 +417,10 @@ public function resetModal() 'intermediate_local_key', 'on_delete_action', 'on_update_action', + 'class_name', + 'data', + 'subject', + 'notification_blade_path', ]); $this->resetErrorBag(); } @@ -492,17 +517,20 @@ public function openEditFieldModal($fieldId): void // Opens delete Field Modal public function openDeleteFieldModal($id): void { - $this->fieldId = $id; - $this->isDeleteFieldModalOpen = true; + $this->itemIdToDelete = $id; + $this->deleteModalTitle = "Delete Field"; + $this->deleteModalMessage = "Are you sure you want to delete this field?"; + $this->deleteModalAction = "deleteField"; + $this->isDeleteModalOpen = true; } // Deletes field from table public function deleteField(): void { $this->fieldsData = array_filter($this->fieldsData, function ($field) { - return $field['id'] !== $this->fieldId; + return $field['id'] !== $this->itemIdToDelete; }); - $this->isDeleteFieldModalOpen = false; + $this->isDeleteModalOpen = false; } protected function isDuplicateColumn(): bool @@ -543,9 +571,11 @@ public function saveField(): void $this->validate($rulesToValidate); $fieldData = [ - 'data_type' => $this->data_type, 'column_name' => $this->column_name, + 'data_type' => $this->data_type, + 'enum_values' => ($this->data_type === 'enum' || $this->data_type === 'set') ? $this->enum_values : null, 'column_validation' => $this->column_validation, + 'is_fillable' => $this->is_fillable ?? false, 'is_foreign_key' => $this->is_foreign_key ?? false, 'foreign_model_name' => $this->foreign_model_name, 'referenced_column' => $this->referenced_column, @@ -567,31 +597,93 @@ public function saveField(): void } $this->isAddFieldModalOpen = false; $this->fieldId = null; - $this->reset(['column_name', 'data_type', 'column_validation', 'is_foreign_key', 'foreign_model_name', 'referenced_column', 'on_delete_action', 'on_update_action']); + $this->reset(['column_name', 'data_type','enum_values', 'column_validation', 'is_foreign_key', 'foreign_model_name', 'referenced_column', 'on_delete_action', 'on_update_action']); } // Save notification data public function saveNotification(): void - { - $this->validate([ + { + if ($this->isDuplicateNotification()) { + $this->addError('class_name', 'You have already taken this class name'); + return; + } + + $rules = [ 'class_name' => $this->rules['class_name'], 'data' => $this->rules['data'], 'subject' => $this->rules['subject'], - 'body' => $this->rules['body'], - ]); + 'notification_blade_path' => $this->rules['notification_blade_path'], + ]; + + $this->validate($rules); // Store notification data - $this->notificationData = [ - [ + $notificationData = [ 'class_name' => $this->class_name, 'data' => $this->data, 'subject' => $this->subject, - 'body' => $this->body, - ] + 'notification_blade_path' => $this->notification_blade_path, ]; + // Update existing notification or add new one + if ($this->notificationId) { + foreach ($this->notificationData as &$notification) { + if ($notification['id'] === $this->notificationId) { + $notification = ['id' => $this->notificationId] + $notificationData; + break; + } + } + unset($notification); + } else { + $this->notificationData[] = array_merge(['id' => Str::random(8)],$notificationData); + } $this->isNotificationModalOpen = false; - $this->reset(['class_name', 'data', 'subject', 'body']); + $this->reset(['class_name', 'data', 'subject', 'notification_blade_path']); + $this->notificationId = null; + } + + // Check for duplicate notification class + protected function isDuplicateNotification(): bool + { + foreach ($this->notificationData as $notification) { + if ( + $notification['class_name'] === $this->class_name && + (!$this->notificationId || $notification['id'] !== $this->notificationId) + ) { + return true; + } + } + return false; + } + + public function openDeleteNotificationModal($id): void + { + $this->itemIdToDelete = $id; + $this->deleteModalTitle = "Delete Notification Class"; + $this->deleteModalMessage = "Are you sure you want to delete this notification class?"; + $this->deleteModalAction = 'deleteNotification'; + $this->isDeleteModalOpen = true; + } + + // Open Edit Notification Modal + public function openEditNotificationModal($notificationId) + { + $this->notificationId = $notificationId; + $this->isEditing = true; + $this->isNotificationModalOpen = true; + $notification = collect($this->notificationData)->firstWhere('id', $notificationId); + if ($notification) { + $this->fill($notification); + } + } + + // Deletes field from table + public function deleteNotification(): void + { + $this->notificationData = array_filter($this->notificationData, function ($notification) { + return $notification['id'] !== $this->itemIdToDelete; + }); + $this->isDeleteModalOpen = false; } //Validate inputs before generation @@ -609,14 +701,6 @@ private function validateInputs(): bool return false; } - // Check if notification file is selected but no notification data is provided - if ($this->is_notification_file_added && empty($this->notificationData)) { - $this->errorMessage = "Please add notification data before generating files."; - session()->flash('error', $this->errorMessage); - $this->dispatch('show-toast', ['message' => $this->errorMessage, 'type' => 'error']); - return false; - } - // Check fields and methods validation if (!$this->validateFieldsAndMethods()) { session()->flash('error', $this->errorMessage); @@ -674,7 +758,7 @@ private function generateFiles(): void ]); // Format field and relation strings - $fieldString = collect($this->fieldsData)->pluck('column_name')->implode(', '); + $fieldString = collect($this->fieldsData)->where('is_fillable', true)->pluck('column_name')->implode(', '); // Generate files based on flags if ($this->is_model_file_added) { @@ -684,7 +768,6 @@ private function generateFiles(): void if ($this->is_migration_file_added) { $this->generateMigration($this->model_name, $this->fieldsData, $this->is_soft_delete_added, $this->is_overwrite_files); } - $this->generateController($this->model_name, $selectedMethods, $this->is_service_file_added, $this->is_resource_file_added, $this->is_request_file_added, $this->is_overwrite_files, $this->is_admin_crud_added); if ($this->is_policy_file_added) { @@ -699,7 +782,7 @@ private function generateFiles(): void $this->generateService($this->model_name, $this->is_overwrite_files); } - if ($this->is_notification_file_added) { + if ($this->notificationData) { $this->generateNotification($this->model_name, $this->is_overwrite_files); } @@ -800,7 +883,7 @@ private function generateNotification($modelName, $overwrite) 'className' => $notificationData['class_name'] ?? $modelName . 'Notification', '--model' => $modelName, '--data' => $notificationData['data'] ?? '', - '--body' => $notificationData['body'] ?? '', + '--view' => $notificationData['notification_blade_path'] ?? '', '--subject' => $notificationData['subject'] ?? '', '--overwrite' => $overwrite ]); @@ -813,6 +896,11 @@ private function generateResource($modelName, $overwrite) 'model' => $modelName, '--overwrite' => $overwrite ]); + + Artisan::call('code-generator:resource-collection', [ + 'model' => $modelName, + '--overwrite' => $overwrite + ]); } // Generate request file diff --git a/src/Library/Helper.php b/src/Library/Helper.php index 8a01825..a01478f 100644 --- a/src/Library/Helper.php +++ b/src/Library/Helper.php @@ -96,16 +96,42 @@ public static function parseCreateTable(string $sql): array // Extract columns if (preg_match('/\((.*)\)/s', $sql, $matches)) { - $columnsRaw = explode(',', $matches[1]); + $columnsRaw = preg_split('/,(?![^(]*\))/', $matches[1]); foreach ($columnsRaw as $col) { - if (preg_match('/`?(\w+)`?\s+(\w+)/', trim($col), $colMatch)) { - $result['fields'][] = [ - 'id' => Str::random(), - 'column_name' => Str::snake($colMatch[1]), - 'data_type' => Str::lower($colMatch[2]), - 'column_validation' => 'required', - ]; + $col = trim($col); + + // Skip foreign keys or standalone constraints + if ( + preg_match('/^(FOREIGN\s+KEY|PRIMARY\s+KEY|CONSTRAINT|UNIQUE|CHECK)/i', $col) + ) { + continue; + } + + // Match column name and data type + if (preg_match('/`?(\w+)`?\s+(\w+)(\s*\(([^)]*)\))?/i', $col, $colMatch)) { + $columnName = $colMatch[1]; + $dataType = strtolower($colMatch[2]); + + $field = [ + 'id' => Str::random(), + 'column_name' => Str::lower($columnName), + 'data_type' => $dataType, + 'is_fillable' => true, + 'column_validation' => 'required', + ]; + + // Extract ENUM or SET values + if (in_array($dataType, ['enum', 'set']) && preg_match('/\((.*?)\)/', $col, $enumMatch)) { + // Clean up values (remove quotes and spaces) + $values = array_map( + fn($v) => trim($v, " '\""), + explode(',', $enumMatch[1]) + ); + $field['enum_values'] = implode(',', $values); + } + + $result['fields'][] = $field; } } }