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:
parent
2e71ef4f11
commit
188c86ca45
3 changed files with 238 additions and 236 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,14 +116,10 @@ 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
|
|
||||||
@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>
|
</div>
|
||||||
|
|
||||||
{{-- Dropdown Options --}}
|
{{-- Dropdown Options --}}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue