/** * OxiCloud - Search Module (thin client) * * All search processing (filtering, scoring, sorting, categorization, * icon mapping, size formatting) is performed on the Rust backend. * This module is a pure rendering client — it sends requests and * displays the enriched results returned by the server. */ import { loadFiles, renderItems } from '../../app/filesView.js'; import { app } from '../../app/state.js'; import { ui } from '../../app/ui.js'; import { i18n } from '../../core/i18n.js'; import { getAuthHeaders } from './fileOperations.js'; /** * @import {SearchCriteria, SearchResults} from '../../core/types.js'} */ const search = { /** * Perform a search using query parameters. * The backend handles all processing and returns enriched results with * relevance_score, icon_class, category, size_formatted, etc. * * @param {string} query - Search query * @param {SearchCriteria} [options] - Additional search options * @returns {Promise} - Enriched search results from backend */ async searchFiles(query, options) { try { const params = new URLSearchParams(); params.append('query', query); if (options.folder_id) params.append('folder_id', options.folder_id); if (options.recursive !== undefined) params.append('recursive', String(options.recursive)); if (options.file_types) { options.file_types.forEach((file_type) => { params.append('type', file_type); }); } if (options.min_size) params.append('min_size', String(options.min_size)); if (options.max_size) params.append('max_size', String(options.max_size)); if (options.created_after) params.append('created_after', String(options.created_after)); if (options.created_before) params.append('created_before', String(options.created_before)); if (options.modified_after) params.append('modified_after', String(options.modified_after)); if (options.modified_before) params.append('modified_before', String(options.modified_before)); if (options.limit) params.append('limit', String(options.limit)); if (options.offset) params.append('offset', String(options.offset)); if (options.sort_by) params.append('sort_by', options.sort_by); const url = `/api/search?${params.toString()}`; console.log(`[search] GET ${url}`); const response = await fetch(url, { headers: getAuthHeaders() }); if (response.ok) { /** @type {SearchResults} */ return await response.json(); } else { let errorText = ''; try { const errorJson = await response.json(); errorText = errorJson.error || response.statusText; } catch (_e) { errorText = response.statusText; } console.error(`Search error: ${errorText}`); throw new Error(`Search failed: ${errorText}`); } } catch (error) { console.error('Error performing search:', error); ui.showNotification(i18n.t('notif.errorTitle'), i18n.t('notif.searchError')); return { files: [], folders: [], total_count: 0, query_time_ms: 0, sort_by: 'relevance', limit: 0, offset: 0, has_more: false }; } }, /** * Get autocomplete suggestions from the backend. * Returns lightweight name suggestions without full search overhead. * * @param {string} query - Prefix to search for * @param {SearchCriteria} [options] - { folder_id, limit } * @returns {Promise} - { suggestions: [...], query_time_ms } */ async getSuggestions(query, options) { try { const params = new URLSearchParams(); params.append('query', query); if (options.folder_id) params.append('folder_id', options.folder_id); if (options.limit) params.append('limit', String(options.limit)); const url = `/api/search/suggest?${params.toString()}`; const response = await fetch(url, { headers: getAuthHeaders() }); if (response.ok) { return await response.json(); } else { return { suggestions: [], query_time_ms: 0 }; } } catch (error) { console.error('Error getting suggestions:', error); return { suggestions: [], query_time_ms: 0 }; } }, /** * Display search results in the UI. * * Uses server-computed enriched data: * - icon_class: Font Awesome class for file type icon * - size_formatted: Human-readable file size (e.g. "2.5 MB") * - category: Content category (image, video, document, code, archive, audio, other) * - relevance_score: 0-100 match quality * - query_time_ms: Server-side query execution time * - sort_by: Active sort order * * @param {SearchResults} results - Enriched search results from backend */ displaySearchResults(results) { ui.resetFilesList(); // ensure also list visible & error hidden //FIXME: move into action rather bage sticky header + hide bread crumb ? + note also that search result does not consider section const pageStickyHeader = document.getElementById('page-sticky-header'); // Search results header with query time and sort controls const totalCount = results.total_count || results.files.length + results.folders.length; const queryTimeText = results.query_time_ms !== undefined ? ` (${results.query_time_ms}ms)` : ''; const searchHeader = document.createElement('div'); searchHeader.className = 'search-results-header'; searchHeader.innerHTML = `

Search results (${totalCount})${queryTimeText}

`; const previousSearchHeader = document.querySelector('.search-results-header'); if (previousSearchHeader) { previousSearchHeader.replaceWith(searchHeader); } else { pageStickyHeader?.appendChild(searchHeader); } // Sort dropdown — re-searches with new sort order (server-side) const sortSelect = /** @type {HTMLSelectElement} */ (document.getElementById('search-sort-select')); if (sortSelect) { sortSelect.addEventListener('change', () => { const searchInput = /** @type {HTMLInputElement} */ (document.querySelector('.search-container input')); if (searchInput?.value.trim()) { const event = new CustomEvent('search-resort', { detail: { sort_by: sortSelect.value } }); document.dispatchEvent(event); } }); } // Clear search const clearSearchBtn = document.getElementById('clear-search-btn'); if (clearSearchBtn) { clearSearchBtn.addEventListener('click', () => { const searchInput = /** @type {HTMLInputElement} */ (document.querySelector('.search-container input')); searchInput.value = ''; app.currentPath = ''; app.isSearchMode = false; document.querySelector('.search-results-header')?.remove(); ui.updateBreadcrumb(); loadFiles(); }); } // Empty state if (results.files.length === 0 && results.folders.length === 0) { ui.showError(`

No results found for this search

`); return; } // Render the whole result set (server-provided enriched data) in one // batched pass — per-item inserts would trigger a DOM scan, a smooth // scroll and a highlight pulse for each of up to 100 rows. renderItems([...results.folders, ...results.files]); }, /** * Clear the search cache on the server * @returns {Promise} */ async clearSearchCache() { try { const response = await fetch('/api/search/cache', { method: 'DELETE', headers: getAuthHeaders() }); if (response.ok) { ui.showNotification(i18n.t('notif.cacheCleared'), i18n.t('notif.cacheClearedBody')); return true; } else { ui.showNotification(i18n.t('notif.errorTitle'), i18n.t('notif.cacheClearError')); return false; } } catch (error) { console.error('Error clearing search cache:', error); ui.showNotification(i18n.t('notif.errorTitle'), i18n.t('notif.cacheClearError')); return false; } } }; export { search };