diff --git a/app/View/Components/Forms/InputWithSelect.php b/app/View/Components/Forms/InputWithSelect.php new file mode 100644 index 000000000..1ce139cec --- /dev/null +++ b/app/View/Components/Forms/InputWithSelect.php @@ -0,0 +1,82 @@ +canGate && $this->canResource && $this->autoDisable) { + $hasPermission = Gate::allows($this->canGate, $this->canResource); + + if (!$hasPermission) { + $this->disabled = true; + } + } + } + + public function render(): View|Closure|string + { + // Store original ID for wire:model binding (property name) + $this->modelBinding = $this->id; + + if (is_null($this->id)) { + $this->id = new Cuid2; + // Don't create wire:model binding for auto-generated IDs + $this->modelBinding = 'null'; + } + // Generate unique HTML ID by adding random suffix + // This prevents duplicate IDs when multiple forms are on the same page + if ($this->modelBinding && $this->modelBinding !== 'null') { + // Use original ID with random suffix for uniqueness + $uniqueSuffix = new Cuid2; + $this->htmlId = $this->modelBinding . '-' . $uniqueSuffix; + } else { + $this->htmlId = (string) $this->id; + } + + if (is_null($this->name)) { + $this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id; + } + + // Set default option if not provided and options exist + if (is_null($this->defaultOption) && !empty($this->options)) { + $this->defaultOption = array_key_first($this->options); + } + + return view('components.forms.input-with-select'); + } +} diff --git a/resources/views/components/forms/input-with-select.blade.php b/resources/views/components/forms/input-with-select.blade.php new file mode 100644 index 000000000..f9b1f4c9b --- /dev/null +++ b/resources/views/components/forms/input-with-select.blade.php @@ -0,0 +1,169 @@ +@php + $inputId = $htmlId !== 'null' ? $htmlId . '-input' : null; + $selectId = $htmlId !== 'null' ? $htmlId . '-select' : null; +@endphp + +
+ @if ($label) + + @endif + +
+ {{-- Input --}} + + + {{-- Select --}} + +
+ + @if (!$label && $helper) + + @endif + @error($modelBinding) + + @enderror +
+ + diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index 99ff249e9..da1cf3722 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -20,21 +20,33 @@

Limit Memory

- +
- - + +
diff --git a/tests/Unit/InputWithSelectComponentTest.php b/tests/Unit/InputWithSelectComponentTest.php new file mode 100644 index 000000000..2d7565cb0 --- /dev/null +++ b/tests/Unit/InputWithSelectComponentTest.php @@ -0,0 +1,77 @@ +required)->toBeFalse() + ->and($component->disabled)->toBeFalse() + ->and($component->readonly)->toBeFalse() + ->and($component->defaultClass)->toBe('input') + ->and($component->type)->toBe('text') + ->and($component->options)->toBe([]); +}); + +it('uses provided id', function () { + $component = new InputWithSelect(id: 'test-input-select'); + + expect($component->id)->toBe('test-input-select'); +}); + +it('accepts options array', function () { + $options = ['b' => 'B', 'k' => 'KiB', 'm' => 'MiB', 'g' => 'GiB']; + $component = new InputWithSelect(options: $options); + + expect($component->options)->toBe($options); +}); + +it('sets default option to first option when not provided', function () { + $options = ['b' => 'B', 'k' => 'KiB', 'm' => 'MiB']; + $component = new InputWithSelect(options: $options); + + // defaultOption is set in render(), so we test the logic directly + if (is_null($component->defaultOption) && !empty($component->options)) { + $component->defaultOption = array_key_first($component->options); + } + + expect($component->defaultOption)->toBe('b'); +}); + +it('uses provided default option', function () { + $options = ['b' => 'B', 'k' => 'KiB', 'm' => 'MiB']; + $component = new InputWithSelect(options: $options, defaultOption: 'm'); + + expect($component->defaultOption)->toBe('m'); +}); + +it('accepts min and max values', function () { + $component = new InputWithSelect(min: 0, max: 100); + + expect($component->min)->toBe(0.0) + ->and($component->max)->toBe(100.0); +}); + +it('accepts type parameter', function () { + $component = new InputWithSelect(type: 'number'); + + expect($component->type)->toBe('number'); +}); + +it('accepts authorization properties', function () { + $component = new InputWithSelect( + canGate: 'update', + canResource: 'resource', + autoDisable: false + ); + + expect($component->canGate)->toBe('update') + ->and($component->canResource)->toBe('resource') + ->and($component->autoDisable)->toBeFalse(); +}); + +it('can be manually disabled', function () { + $component = new InputWithSelect(disabled: true); + + expect($component->disabled)->toBeTrue(); +});