diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
index 5f5e12e0a..fa65e8bd2 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
@@ -2,8 +2,11 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable;
+use App\Models\Environment;
+use App\Models\Project;
use App\Traits\EnvironmentVariableAnalyzer;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Livewire\Attributes\Computed;
use Livewire\Component;
class Add extends Component
@@ -56,6 +59,72 @@ public function mount()
$this->problematicVariables = self::getProblematicVariablesForFrontend();
}
+ #[Computed]
+ public function availableSharedVariables(): array
+ {
+ $team = currentTeam();
+ $result = [
+ 'team' => [],
+ 'project' => [],
+ 'environment' => [],
+ ];
+
+ // Early return if no team
+ if (! $team) {
+ return $result;
+ }
+
+ // Check if user can view team variables
+ try {
+ $this->authorize('view', $team);
+ $result['team'] = $team->environment_variables()
+ ->pluck('key')
+ ->toArray();
+ } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
+ // User not authorized to view team variables
+ }
+
+ // Get project variables if we have a project_uuid in route
+ $projectUuid = data_get($this->parameters, 'project_uuid');
+ if ($projectUuid) {
+ $project = Project::where('team_id', $team->id)
+ ->where('uuid', $projectUuid)
+ ->first();
+
+ if ($project) {
+ try {
+ $this->authorize('view', $project);
+ $result['project'] = $project->environment_variables()
+ ->pluck('key')
+ ->toArray();
+
+ // Get environment variables if we have an environment_uuid in route
+ $environmentUuid = data_get($this->parameters, 'environment_uuid');
+ if ($environmentUuid) {
+ $environment = $project->environments()
+ ->where('uuid', $environmentUuid)
+ ->first();
+
+ if ($environment) {
+ try {
+ $this->authorize('view', $environment);
+ $result['environment'] = $environment->environment_variables()
+ ->pluck('key')
+ ->toArray();
+ } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
+ // User not authorized to view environment variables
+ }
+ }
+ }
+ } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
+ // User not authorized to view project variables
+ }
+ }
+ }
+
+ return $result;
+ }
+
public function submit()
{
$this->validate();
diff --git a/app/View/Components/Forms/EnvVarInput.php b/app/View/Components/Forms/EnvVarInput.php
new file mode 100644
index 000000000..7cf8ee8fa
--- /dev/null
+++ b/app/View/Components/Forms/EnvVarInput.php
@@ -0,0 +1,89 @@
+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;
+ }
+
+ $this->scopeUrls = [
+ 'team' => route('shared-variables.team.index'),
+ 'project' => route('shared-variables.project.index'),
+ 'environment' => $this->projectUuid && $this->environmentUuid
+ ? route('shared-variables.environment.show', [
+ 'project_uuid' => $this->projectUuid,
+ 'environment_uuid' => $this->environmentUuid,
+ ])
+ : route('shared-variables.environment.index'),
+ 'default' => route('shared-variables.index'),
+ ];
+
+ return view('components.forms.env-var-input');
+ }
+}
diff --git a/resources/views/components/forms/env-var-input.blade.php b/resources/views/components/forms/env-var-input.blade.php
new file mode 100644
index 000000000..0859db78d
--- /dev/null
+++ b/resources/views/components/forms/env-var-input.blade.php
@@ -0,0 +1,268 @@
+
+ @if ($label)
+
+ @endif
+
+
+
+
merge(['class' => $defaultClass]) }}
+ @required($required)
+ @readonly($readonly)
+ @if ($modelBinding !== 'null')
+ wire:model="{{ $modelBinding }}"
+ wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
+ @endif
+ wire:loading.attr="disabled"
+ type="{{ $type }}"
+ @disabled($disabled)
+ @if ($htmlId !== 'null') id={{ $htmlId }} @endif
+ name="{{ $name }}"
+ placeholder="{{ $attributes->get('placeholder') }}"
+ @if ($autofocus) autofocus @endif>
+
+ {{-- Dropdown for suggestions --}}
+
+
+
+ @if (!$label && $helper)
+
+ @endif
+ @error($modelBinding)
+
+ @enderror
+
diff --git a/resources/views/livewire/project/shared/environment-variable/add.blade.php b/resources/views/livewire/project/shared/environment-variable/add.blade.php
index 353fe02de..9bc4f06a3 100644
--- a/resources/views/livewire/project/shared/environment-variable/add.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php
@@ -1,8 +1,20 @@
+
\ No newline at end of file
diff --git a/tests/Unit/EnvVarInputComponentTest.php b/tests/Unit/EnvVarInputComponentTest.php
new file mode 100644
index 000000000..f4fc8bcb5
--- /dev/null
+++ b/tests/Unit/EnvVarInputComponentTest.php
@@ -0,0 +1,67 @@
+required)->toBeFalse()
+ ->and($component->disabled)->toBeFalse()
+ ->and($component->readonly)->toBeFalse()
+ ->and($component->defaultClass)->toBe('input')
+ ->and($component->availableVars)->toBe([]);
+});
+
+it('uses provided id', function () {
+ $component = new EnvVarInput(id: 'env-key');
+
+ expect($component->id)->toBe('env-key');
+});
+
+it('accepts available vars', function () {
+ $vars = [
+ 'team' => ['DATABASE_URL', 'API_KEY'],
+ 'project' => ['STRIPE_KEY'],
+ 'environment' => ['DEBUG'],
+ ];
+
+ $component = new EnvVarInput(availableVars: $vars);
+
+ expect($component->availableVars)->toBe($vars);
+});
+
+it('accepts required parameter', function () {
+ $component = new EnvVarInput(required: true);
+
+ expect($component->required)->toBeTrue();
+});
+
+it('accepts disabled state', function () {
+ $component = new EnvVarInput(disabled: true);
+
+ expect($component->disabled)->toBeTrue();
+});
+
+it('accepts readonly state', function () {
+ $component = new EnvVarInput(readonly: true);
+
+ expect($component->readonly)->toBeTrue();
+});
+
+it('accepts autofocus parameter', function () {
+ $component = new EnvVarInput(autofocus: true);
+
+ expect($component->autofocus)->toBeTrue();
+});
+
+it('accepts authorization properties', function () {
+ $component = new EnvVarInput(
+ canGate: 'update',
+ canResource: 'resource',
+ autoDisable: false
+ );
+
+ expect($component->canGate)->toBe('update')
+ ->and($component->canResource)->toBe('resource')
+ ->and($component->autoDisable)->toBeFalse();
+});
diff --git a/tests/Unit/Livewire/EnvironmentVariableAutocompleteTest.php b/tests/Unit/Livewire/EnvironmentVariableAutocompleteTest.php
new file mode 100644
index 000000000..19da8b43b
--- /dev/null
+++ b/tests/Unit/Livewire/EnvironmentVariableAutocompleteTest.php
@@ -0,0 +1,53 @@
+toBeTrue();
+});
+
+it('component has required properties for environment variable autocomplete', function () {
+ $component = new Add;
+
+ expect($component)->toHaveProperty('key')
+ ->and($component)->toHaveProperty('value')
+ ->and($component)->toHaveProperty('is_multiline')
+ ->and($component)->toHaveProperty('is_literal')
+ ->and($component)->toHaveProperty('is_runtime')
+ ->and($component)->toHaveProperty('is_buildtime')
+ ->and($component)->toHaveProperty('parameters');
+});
+
+it('returns empty arrays when currentTeam returns null', function () {
+ // Mock Auth facade to return null for user
+ Auth::shouldReceive('user')
+ ->andReturn(null);
+
+ $component = new Add;
+ $component->parameters = [];
+
+ $result = $component->availableSharedVariables();
+
+ expect($result)->toBe([
+ 'team' => [],
+ 'project' => [],
+ 'environment' => [],
+ ]);
+});
+
+it('availableSharedVariables method wraps authorization checks in try-catch blocks', function () {
+ // Read the source code to verify the authorization pattern
+ $reflectionMethod = new ReflectionMethod(Add::class, 'availableSharedVariables');
+ $source = file_get_contents($reflectionMethod->getFileName());
+
+ // Verify that the method contains authorization checks
+ expect($source)->toContain('$this->authorize(\'view\', $team)')
+ ->and($source)->toContain('$this->authorize(\'view\', $project)')
+ ->and($source)->toContain('$this->authorize(\'view\', $environment)')
+ // Verify authorization checks are wrapped in try-catch blocks
+ ->and($source)->toContain('} catch (\Illuminate\Auth\Access\AuthorizationException $e) {');
+});