Improve SSH key filtering and datalist component

- Add ownedAndOnlySShKeys() method to filter out git-related keys
- Update Boarding component to use new filtering method
- Enhance datalist component with better multi-select and single-select handling
- Fix Alpine.js reactivity and improve UI interactions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-10-23 16:18:20 +02:00
parent 2e71ef4f11
commit 188c86ca45
3 changed files with 238 additions and 236 deletions

View file

@ -107,7 +107,7 @@ public function mount()
if ($this->selectedServerType === 'remote') { if ($this->selectedServerType === 'remote') {
if ($this->privateKeys->isEmpty()) { if ($this->privateKeys->isEmpty()) {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); $this->privateKeys = PrivateKey::ownedAndOnlySShKeys(['name'])->where('id', '!=', 0)->get();
} }
if ($this->servers->isEmpty()) { if ($this->servers->isEmpty()) {
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); $this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
@ -186,7 +186,7 @@ public function setServerType(string $type)
return $this->validateServer('localhost'); return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') { } elseif ($this->selectedServerType === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); $this->privateKeys = PrivateKey::ownedAndOnlySShKeys(['name'])->where('id', '!=', 0)->get();
// Auto-select first key if available for better UX // Auto-select first key if available for better UX
if ($this->privateKeys->count() > 0) { if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id; $this->selectedExistingPrivateKey = $this->privateKeys->first()->id;

View file

@ -88,6 +88,16 @@ public static function ownedByCurrentTeam(array $select = ['*'])
return self::whereTeamId($teamId)->select($selectArray->all()); return self::whereTeamId($teamId)->select($selectArray->all());
} }
public static function ownedAndOnlySShKeys(array $select = ['*'])
{
$teamId = currentTeam()->id;
$selectArray = collect($select)->concat(['id']);
return self::whereTeamId($teamId)
->where('is_git_related', false)
->select($selectArray->all());
}
public static function validatePrivateKey($privateKey) public static function validatePrivateKey($privateKey)
{ {
try { try {

View file

@ -98,18 +98,15 @@
{{-- Unified Input Container with Tags Inside --}} {{-- Unified Input Container with Tags Inside --}}
<div @click="$refs.searchInput.focus()" <div @click="$refs.searchInput.focus()"
class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text px-1 focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white" class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 px-2 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text px-1 focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white"
:class="{ :class="{
'opacity-50': {{ $disabled ? 'true' : 'false' }} 'opacity-50': {{ $disabled ? 'true' : 'false' }}
}" }" wire:loading.class="opacity-50"
wire:loading.class="opacity-50"
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"> wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4">
{{-- Selected Tags Inside Input --}} {{-- Selected Tags Inside Input --}}
<template x-for="value in selected" :key="value"> <template x-for="value in selected" :key="value">
<button <button type="button" @click.stop="removeOption(value, $event)"
type="button"
@click.stop="removeOption(value, $event)"
:disabled="{{ $disabled ? 'true' : 'false' }}" :disabled="{{ $disabled ? 'true' : 'false' }}"
class="inline-flex items-center gap-1.5 px-2 py-0.5 text-xs bg-coolgray-200 dark:bg-coolgray-700 rounded whitespace-nowrap {{ $disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400' }}" class="inline-flex items-center gap-1.5 px-2 py-0.5 text-xs bg-coolgray-200 dark:bg-coolgray-700 rounded whitespace-nowrap {{ $disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400' }}"
aria-label="Remove"> aria-label="Remove">
@ -119,18 +116,14 @@ class="inline-flex items-center gap-1.5 px-2 py-0.5 text-xs bg-coolgray-200 dark
{{-- Search Input (Borderless, Inside Container) --}} {{-- Search Input (Borderless, Inside Container) --}}
<input type="text" x-model="search" x-ref="searchInput" @input="filterOptions()" @focus="open = true" <input type="text" x-model="search" x-ref="searchInput" @input="filterOptions()" @focus="open = true"
@keydown.escape="open = false" @keydown.escape="open = false" :placeholder="(Array.isArray(selected) && selected.length > 0) ? '' :
:placeholder="(Array.isArray(selected) && selected.length > 0) ? '' : {{ json_encode($placeholder ?: 'Search...') }}" @required($required) @readonly($readonly)
{{ json_encode($placeholder ?: 'Search...') }}" @disabled($disabled) @if ($autofocus) autofocus @endif
@required($required) @readonly($readonly) @disabled($disabled) @if ($autofocus) class="flex-1 min-w-[120px] text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-black dark:text-white" />
autofocus </div>
@endif
class="flex-1 min-w-[120px] text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-black dark:text-white"
/>
</div>
{{-- Dropdown Options --}} {{-- Dropdown Options --}}
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition <div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar"> class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
<template x-if="filteredOptions.length === 0"> <template x-if="filteredOptions.length === 0">
@ -149,16 +142,16 @@ class="w-4 h-4 rounded border-neutral-300 dark:border-neutral-600 bg-white dark:
<span class="text-sm flex-1" x-text="option.text"></span> <span class="text-sm flex-1" x-text="option.text"></span>
</div> </div>
</template> </template>
</div> </div>
{{-- Hidden datalist for options --}} {{-- Hidden datalist for options --}}
<datalist x-ref="datalist" style="display: none;"> <datalist x-ref="datalist" style="display: none;">
{{ $slot }} {{ $slot }}
</datalist> </datalist>
</div> </div>
@else @else
{{-- Single Selection Mode with Alpine.js --}} {{-- Single Selection Mode with Alpine.js --}}
<div x-data="{ <div x-data="{
open: false, open: false,
search: '', search: '',
selected: @entangle(($attributes->whereStartsWith('wire:model')->first() ? $attributes->wire('model')->value() : $modelBinding)).live, selected: @entangle(($attributes->whereStartsWith('wire:model')->first() ? $attributes->wire('model')->value() : $modelBinding)).live,
@ -222,7 +215,7 @@ class="w-4 h-4 rounded border-neutral-300 dark:border-neutral-600 bg-white dark:
isDefaultValue() { isDefaultValue() {
return !this.selected || this.selected === 'default' || this.selected === ''; return !this.selected || this.selected === 'default' || this.selected === '';
} }
}" @click.outside="open = false" class="relative"> }" @click.outside="open = false" class="relative">
{{-- Hidden input for form validation --}} {{-- Hidden input for form validation --}}
<input type="hidden" :value="selected" @required($required) /> <input type="hidden" :value="selected" @required($required) />
@ -232,27 +225,26 @@ class="w-4 h-4 rounded border-neutral-300 dark:border-neutral-600 bg-white dark:
class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white" class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white"
:class="{ :class="{
'opacity-50': {{ $disabled ? 'true' : 'false' }} 'opacity-50': {{ $disabled ? 'true' : 'false' }}
}" }" wire:loading.class="opacity-50" wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4">
wire:loading.class="opacity-50"
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4">
{{-- Display Selected Value or Search Input --}} {{-- Display Selected Value or Search Input --}}
<div class="flex-1 flex items-center min-w-0 px-1"> <div class="flex-1 flex items-center min-w-0 px-1">
<template x-if="!isDefaultValue() && !open"> <template x-if="!isDefaultValue() && !open">
<span class="text-sm flex-1 truncate text-black dark:text-white px-2" x-text="getSelectedText()"></span> <span class="text-sm flex-1 truncate text-black dark:text-white px-2"
x-text="getSelectedText()"></span>
</template> </template>
<input type="text" x-show="isDefaultValue() || open" x-model="search" x-ref="searchInput" <input type="text" x-show="isDefaultValue() || open" x-model="search" x-ref="searchInput"
@input="filterOptions()" @focus="open = true" @input="filterOptions()" @focus="open = true" @keydown.escape="open = false"
@keydown.escape="open = false" :placeholder="{{ json_encode($placeholder ?: 'Search...') }}" @readonly($readonly)
:placeholder="{{ json_encode($placeholder ?: 'Search...') }}" @disabled($disabled) @if ($autofocus) autofocus @endif
@readonly($readonly) @disabled($disabled) @if ($autofocus) autofocus @endif
class="flex-1 text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-black dark:text-white px-2" /> class="flex-1 text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-black dark:text-white px-2" />
</div> </div>
{{-- Dropdown Arrow --}} {{-- Dropdown Arrow --}}
<button type="button" @click.stop="open = !open" :disabled="{{ $disabled ? 'true' : 'false' }}" <button type="button" @click.stop="open = !open" :disabled="{{ $disabled ? 'true' : 'false' }}"
class="shrink-0 text-neutral-400 px-2 {{ $disabled ? 'cursor-not-allowed' : 'cursor-pointer' }}"> class="shrink-0 text-neutral-400 px-2 {{ $disabled ? 'cursor-not-allowed' : 'cursor-pointer' }}">
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': open }" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
@ -281,12 +273,12 @@ class="px-3 py-2 cursor-pointer hover:bg-neutral-100 dark:hover:bg-coolgray-200"
<datalist x-ref="datalist" style="display: none;"> <datalist x-ref="datalist" style="display: none;">
{{ $slot }} {{ $slot }}
</datalist> </datalist>
</div> </div>
@endif @endif
@error($modelBinding) @error($modelBinding)
<label class="label"> <label class="label">
<span class="text-red-500 label-text-alt">{{ $message }}</span> <span class="text-red-500 label-text-alt">{{ $message }}</span>
</label> </label>
@enderror @enderror
</div> </div>