diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php index 98571b87f..e378bd289 100644 --- a/app/Livewire/GlobalSearch.php +++ b/app/Livewire/GlobalSearch.php @@ -81,6 +81,7 @@ public function openSearchModal() { $this->isModalOpen = true; $this->loadSearchableItems(); + $this->loadCreatableItems(); $this->dispatch('search-modal-opened'); } @@ -973,6 +974,9 @@ private function loadCreatableItems() ]); } + // Merge with services + $items = $items->merge(collect($this->services)); + $this->creatableItems = $items->toArray(); } diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php index ad9a9f6ad..95d2640a6 100644 --- a/resources/views/livewire/global-search.blade.php +++ b/resources/views/livewire/global-search.blade.php @@ -3,18 +3,74 @@ selectedIndex: -1, isSearching: false, isLoadingInitialData: false, + allSearchableItems: [], + searchQuery: '', + creatableItems: [], + isCreateMode: false, + + // Client-side search function + get searchResults() { + if (!this.searchQuery || this.searchQuery.length < 1) { + return []; + } + + const query = this.searchQuery.toLowerCase().trim(); + + const results = this.allSearchableItems.filter(item => { + if (!item.search_text) return false; + return item.search_text.toLowerCase().includes(query); + }).slice(0, 20); + + return results; + }, + + get filteredCreatableItems() { + if (!this.searchQuery || this.searchQuery.length < 1) { + return []; + } + const query = this.searchQuery.toLowerCase().trim(); + + if (query === 'new') { + return this.creatableItems; + } + + return this.creatableItems.filter(item => { + const searchText = `${item.name} ${item.description} ${item.type} ${item.category}`.toLowerCase(); + return searchText.includes(query); + }); + }, + + get groupedCreatableItems() { + const grouped = {}; + this.filteredCreatableItems.forEach(item => { + const category = item.category || 'Other'; + if (!grouped[category]) { + grouped[category] = []; + } + grouped[category].push(item); + }); + return grouped; + }, + openModal() { this.modalOpen = true; this.selectedIndex = -1; this.isLoadingInitialData = true; - // Dispatch event to load initial data - $wire.dispatch('loadInitialData'); + this.searchQuery = ''; + $wire.openSearchModal().then(() => { + this.allSearchableItems = $wire.allSearchableItems || []; + this.creatableItems = $wire.creatableItems || []; + this.isLoadingInitialData = false; + setTimeout(() => this.$refs.searchInput?.focus(), 50); + }); }, closeModal() { this.modalOpen = false; this.selectedIndex = -1; this.isSearching = false; this.isLoadingInitialData = false; + this.searchQuery = ''; + this.allSearchableItems = []; // Ensure scroll is restored document.body.style.overflow = ''; @this.closeSearchModal(); @@ -42,43 +98,39 @@ this.selectedIndex = -1; }); - // Listen for loading state changes - $wire.on('loadInitialData', () => { - this.isLoadingInitialData = true; - $wire.openSearchModal().finally(() => { - this.isLoadingInitialData = false; - // Focus input after data is loaded - setTimeout(() => this.$refs.searchInput?.focus(), 50); - }); - }); + this.$watch('searchQuery', (value) => { + this.selectedIndex = -1; + const trimmed = value.trim().toLowerCase(); - // Use Livewire lifecycle hooks for accurate loading state - const componentId = $wire.__instance.id; - - Livewire.hook('message.sent', (message, component) => { - // Only handle messages for this component instance - if (component.id !== componentId) return; - - // Check if this is a searchQuery update - if (message.updateQueue && message.updateQueue.some(update => update.payload.name === 'searchQuery')) { - this.isSearching = true; + if (trimmed === '') { + if ($wire.isSelectingResource) { + $wire.cancelResourceSelection(); + } + return; } - }); - Livewire.hook('message.processed', (message, component) => { - // Only handle messages for this component instance - if (component.id !== componentId) return; + const exactMatchCommands = [ + 'new project', 'new server', 'new team', 'new storage', 'new s3', + 'new private key', 'new privatekey', 'new key', + 'new github app', 'new github', 'new source', + 'new public', 'new public git', 'new public repo', 'new public repository', + 'new private github', 'new private gh', 'new private deploy', 'new deploy key', + 'new dockerfile', 'new docker compose', 'new compose', 'new docker image', 'new image', + 'new postgresql', 'new postgres', 'new mysql', 'new mariadb', + 'new redis', 'new keydb', 'new dragonfly', 'new mongodb', 'new mongo', 'new clickhouse' + ]; - // Check if this was a searchQuery update - if (message.updateQueue && message.updateQueue.some(update => update.payload.name === 'searchQuery')) { - this.isSearching = false; - } - }); + if (exactMatchCommands.includes(trimmed)) { + const matchingItem = this.creatableItems.find(item => { + const itemSearchText = `new ${item.name}`.toLowerCase(); + const itemType = `new ${item.type}`.toLowerCase(); + return itemSearchText === trimmed || itemType === trimmed || + (item.type && trimmed.includes(item.type.replace(/-/g, ' '))); + }); - // Also clear loading state when search is emptied - this.$watch('$wire.searchQuery', (value) => { - if (!value || value.length === 0) { - this.isSearching = false; + if (matchingItem) { + $wire.navigateToResource(matchingItem.type); + } } }); @@ -111,7 +163,7 @@ const escapeKeyHandler = (e) => { if (e.key === 'Escape' && this.modalOpen) { // If search query is empty, close the modal - if (!$wire.searchQuery || $wire.searchQuery === '') { + if (!this.searchQuery || this.searchQuery === '') { // Check if we're in a selection state using Alpine-accessible Livewire state if ($wire.isSelectingResource) { $wire.cancelResourceSelection(); @@ -122,7 +174,7 @@ } } else { // If search query has text, just clear it - $wire.searchQuery = ''; + this.searchQuery = ''; setTimeout(() => this.$refs.searchInput?.focus(), 100); } } @@ -191,13 +243,21 @@ class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen
- + + + + +
- @@ -212,547 +272,347 @@ class="pointer-events-auto px-2 py-1 text-xs font-medium text-neutral-500 dark:t
- -
-
-
- - - - - -

- Please wait while we fetch your data -

-
+ + {{--
+
+

+ ✓ Data loaded successfully! +

+

+ searchable items available +

+

+ Start typing to search... +

-
+
--}} - @if (strlen($searchQuery) >= 1) -
- -
- -
- @for ($i = 0; $i < 3; $i++) -
-
-
-
-
-
-
+
+ +
+ @if ($isSelectingResource) + +
+ + @if ($selectedServerId === null) +
+
+ +
+

+ Select Server +

+ @if ($this->selectedResourceName) +
+ for {{ $this->selectedResourceName }} +
+ @endif
-
-
- @endfor -
-
- - -
- @if ($isSelectingResource) - -
- - @if ($selectedServerId === null) -
-
+ @if ($loadingServers) +
+ + + + + + Loading + servers... +
+ @elseif (count($availableServers) > 0) + @foreach ($availableServers as $index => $server) -
-

- Select Server -

- @if ($this->selectedResourceName) -
- for {{ $this->selectedResourceName }} -
- @endif -
-
- @if ($loadingServers) -
- - - - - - Loading - servers... -
- @elseif (count($availableServers) > 0) - @foreach ($availableServers as $index => $server) - - @endforeach - @else -
-

No servers - available

-
- @endif -
- @endif - - - @if ($selectedServerId !== null && $selectedDestinationUuid === null) -
-
- -
-

- Select Destination -

- @if ($this->selectedResourceName) -
- for {{ $this->selectedResourceName }} -
- @endif -
-
- @if ($loadingDestinations) -
- - - - - - Loading - destinations... -
- @elseif (count($availableDestinations) > 0) - @foreach ($availableDestinations as $index => $destination) - - @endforeach - @else -
-

No destinations - available

-
- @endif -
- @endif - - - @if ($selectedDestinationUuid !== null && $selectedProjectUuid === null) -
-
- -
-

- Select Project -

- @if ($this->selectedResourceName) -
- for {{ $this->selectedResourceName }} -
- @endif -
-
- @if ($loadingProjects) -
- - - - - - Loading - projects... -
- @elseif (count($availableProjects) > 0) - @foreach ($availableProjects as $index => $project) - - @endforeach - @else -
-

No projects - available

-
- @endif -
- @endif - - - @if ($selectedProjectUuid !== null && $selectedEnvironmentUuid === null) -
-
- -
-

- Select Environment -

- @if ($this->selectedResourceName) -
- for {{ $this->selectedResourceName }} -
- @endif -
-
- @if ($loadingEnvironments) -
- - - - - - Loading - environments... -
- @elseif (count($availableEnvironments) > 0) - @foreach ($availableEnvironments as $index => $environment) - - @endforeach - @else -
-

No environments - available

-
- @endif -
- @endif -
- @elseif ($isCreateMode && count($this->filteredCreatableItems) > 0 && !$autoOpenResource) - -
- {{-- Show existing resources first if any match --}} - @php - $existingResources = collect($searchResults)->filter(fn($r) => !isset($r['is_creatable_suggestion']))->count(); - @endphp - @if ($existingResources > 0) - -
-

- Existing Resources -

-
- @foreach ($searchResults as $result) - @if (!isset($result['is_creatable_suggestion'])) - + wire:click="selectServer({{ $server['id'] }}, true)" + class="search-result-item w-full text-left block px-4 py-3 hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30"> - @elseif (strlen($searchQuery) >= 1 && count($searchResults) > 0) -
- @foreach ($searchResults as $index => $result) - @if (isset($result['is_creatable_suggestion']) && $result['is_creatable_suggestion']) - {{-- Creatable suggestion with yellow theme --}} - + + @endforeach @else - {{-- Regular search result --}} +
+

No servers + available

+
+ @endif +
+ @endif + + + @if ($selectedServerId !== null && $selectedDestinationUuid === null) +
+
+ +
+

+ Select Destination +

+ @if ($this->selectedResourceName) +
+ for {{ $this->selectedResourceName }} +
+ @endif +
+
+ @if ($loadingDestinations) +
+ + + + + + Loading + destinations... +
+ @elseif (count($availableDestinations) > 0) + @foreach ($availableDestinations as $index => $destination) + + @endforeach + @else +
+

No destinations + available

+
+ @endif +
+ @endif + + + @if ($selectedDestinationUuid !== null && $selectedProjectUuid === null) +
+
+ +
+

+ Select Project +

+ @if ($this->selectedResourceName) +
+ for {{ $this->selectedResourceName }} +
+ @endif +
+
+ @if ($loadingProjects) +
+ + + + + + Loading + projects... +
+ @elseif (count($availableProjects) > 0) + @foreach ($availableProjects as $index => $project) + + @endforeach + @else +
+

No projects + available

+
+ @endif +
+ @endif + + + @if ($selectedProjectUuid !== null && $selectedEnvironmentUuid === null) +
+
+ +
+

+ Select Environment +

+ @if ($this->selectedResourceName) +
+ for {{ $this->selectedResourceName }} +
+ @endif +
+
+ @if ($loadingEnvironments) +
+ + + + + + Loading + environments... +
+ @elseif (count($availableEnvironments) > 0) + @foreach ($availableEnvironments as $index => $environment) + + @endforeach + @else +
+

No environments + available

+
+ @endif +
+ @endif +
+ @elseif ($isCreateMode && count($this->filteredCreatableItems) > 0 && !$autoOpenResource) + +
+ {{-- Show existing resources first if any match --}} + @php + $existingResources = collect($searchResults) + ->filter(fn($r) => !isset($r['is_creatable_suggestion'])) + ->count(); + @endphp + @if ($existingResources > 0) + +
+

+ Existing Resources +

+
+ @foreach ($searchResults as $result) + @if (!isset($result['is_creatable_suggestion'])) - @elseif (strlen($searchQuery) >= 2 && count($searchResults) === 0 && !$autoOpenResource) -
-
-

- No results found -

-

- Try different keywords or check the spelling -

-

- 💡 Tip: Search for service names like "wordpress", "postgres", or "redis" -

+ @endif + + @php + $grouped = collect($this->filteredCreatableItems)->groupBy('category'); + @endphp + + @foreach ($grouped as $category => $items) + +
+

+ {{ $category }} +

+ + + @foreach ($items as $item) + + @endforeach + @endforeach +
+ @endif + + + + + +
- @endif +