From 7973dfadd266ffb1b9711dbb2b506aff2de6f777 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Fri, 5 Dec 2025 12:38:34 -0500 Subject: [PATCH] feat: Enhance Search component with session state management and UI improvements This commit introduces session storage functionality in the Search component, allowing users to persist their search state across page reloads. The UI has been updated for better clarity, including improved labels and placeholders for input fields. Additionally, the search options have been reorganized for a more intuitive user experience. Documentation has been updated to reflect these changes. --- frontend/.eslintrc.cjs | 1 + frontend/src/api/rolePermissions.ts | 1 + frontend/src/api/videos.ts | 1 + frontend/src/pages/Search.tsx | 253 ++++++++++++++++++++-------- src/web/api/role_permissions.py | 1 + src/web/api/videos.py | 1 + src/web/schemas/role_permissions.py | 1 + src/web/schemas/videos.py | 1 + 8 files changed, 194 insertions(+), 66 deletions(-) diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 47ee835..3550926 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -48,3 +48,4 @@ module.exports = { } + diff --git a/frontend/src/api/rolePermissions.ts b/frontend/src/api/rolePermissions.ts index 4aaf333..13358c6 100644 --- a/frontend/src/api/rolePermissions.ts +++ b/frontend/src/api/rolePermissions.ts @@ -35,3 +35,4 @@ export const rolePermissionsApi = { } + diff --git a/frontend/src/api/videos.ts b/frontend/src/api/videos.ts index 60855a3..7d73f8d 100644 --- a/frontend/src/api/videos.ts +++ b/frontend/src/api/videos.ts @@ -121,3 +121,4 @@ export const videosApi = { export default videosApi + diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index 1a5b61c..3fb706a 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -9,13 +9,13 @@ import peopleApi, { Person } from '../api/people' type SearchType = 'name' | 'date' | 'tags' | 'no_faces' | 'no_tags' | 'processed' | 'unprocessed' | 'favorites' const SEARCH_TYPES: { value: SearchType; label: string }[] = [ - { value: 'name', label: 'Search photos by name' }, - { value: 'date', label: 'Search photos by date' }, - { value: 'tags', label: 'Search photos by tags' }, + { value: 'name', label: 'By person name' }, + { value: 'date', label: 'By date range' }, + { value: 'tags', label: 'By tags' }, { value: 'no_faces', label: 'Photos without faces' }, { value: 'no_tags', label: 'Photos without tags' }, - { value: 'processed', label: 'Search processed photos' }, - { value: 'unprocessed', label: 'Search un-processed photos' }, + { value: 'processed', label: 'Processed photos' }, + { value: 'unprocessed', label: 'Unprocessed photos' }, { value: 'favorites', label: '⭐ Favorite photos' }, ] @@ -42,7 +42,6 @@ export default function Search() { const [configExpanded, setConfigExpanded] = useState(true) // Default to expanded // Search inputs - const [personName, setPersonName] = useState('') const [selectedTags, setSelectedTags] = useState([]) const [matchAll, setMatchAll] = useState(false) const [dateFrom, setDateFrom] = useState('') @@ -78,6 +77,12 @@ export default function Search() { const tagInputRef = useRef(null) const tagDropdownRef = useRef(null) + // SessionStorage key for persisting search state (clears when tab/window closes) + const SEARCH_STATE_KEY = 'search_page_state' + + // Track if we're restoring state to prevent clearing during restoration + const isRestoringState = useRef(false) + // Tag modal const [showTagModal, setShowTagModal] = useState(false) const [selectedTagName, setSelectedTagName] = useState('') @@ -113,11 +118,99 @@ export default function Search() { } } + // Load state from sessionStorage on mount useEffect(() => { + let restoredSearchType: SearchType | null = null + + try { + isRestoringState.current = true + const savedState = sessionStorage.getItem(SEARCH_STATE_KEY) + if (savedState) { + const state = JSON.parse(savedState) + // Restore all state values + if (state.searchType) { + restoredSearchType = state.searchType + setSearchType(state.searchType) + } + if (state.selectedTags) setSelectedTags(state.selectedTags) + if (state.matchAll !== undefined) setMatchAll(state.matchAll) + if (state.dateFrom) setDateFrom(state.dateFrom) + if (state.dateTo) setDateTo(state.dateTo) + if (state.mediaType) setMediaType(state.mediaType) + if (state.selectedPeople) setSelectedPeople(state.selectedPeople) + if (state.inputValue) setInputValue(state.inputValue) + if (state.tagsExpanded !== undefined) setTagsExpanded(state.tagsExpanded) + if (state.filtersExpanded !== undefined) setFiltersExpanded(state.filtersExpanded) + if (state.configExpanded !== undefined) setConfigExpanded(state.configExpanded) + if (state.sortColumn) setSortColumn(state.sortColumn) + if (state.sortDir) setSortDir(state.sortDir) + if (state.page) setPage(state.page) + // Restore results if they exist + if (state.results && Array.isArray(state.results)) { + setResults(state.results) + } + if (state.total !== undefined) { + setTotal(state.total) + } + } + } catch (error) { + console.error('Error loading saved search state:', error) + } finally { + // Mark restoration as complete after a short delay to allow all state updates to process + setTimeout(() => { + isRestoringState.current = false + // Re-run auto-searches after restoration completes + if (restoredSearchType && + (restoredSearchType === 'no_faces' || + restoredSearchType === 'no_tags' || + restoredSearchType === 'processed' || + restoredSearchType === 'unprocessed' || + restoredSearchType === 'favorites')) { + // Use a small delay to ensure state is fully restored + setTimeout(() => { + performSearch() + }, 150) + } + }, 100) + } + loadTags() loadAllPeople() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + // Save state to sessionStorage whenever relevant values change + useEffect(() => { + // Don't save during state restoration + if (isRestoringState.current) { + return + } + + try { + const stateToSave = { + searchType, + selectedTags, + matchAll, + dateFrom, + dateTo, + mediaType, + selectedPeople, + inputValue, + tagsExpanded, + filtersExpanded, + configExpanded, + sortColumn, + sortDir, + page, + results, + total, + } + sessionStorage.setItem(SEARCH_STATE_KEY, JSON.stringify(stateToSave)) + } catch (error) { + console.error('Error saving search state:', error) + } + }, [searchType, selectedTags, matchAll, dateFrom, dateTo, mediaType, selectedPeople, inputValue, tagsExpanded, filtersExpanded, configExpanded, sortColumn, sortDir, page, results, total]) + const performSearch = async (pageNum: number = page) => { setLoading(true) try { @@ -252,6 +345,11 @@ export default function Search() { }, []) useEffect(() => { + // Don't clear state during restoration + if (isRestoringState.current) { + return + } + // Clear results from previous search when search type changes setResults([]) setTotal(0) @@ -269,7 +367,6 @@ export default function Search() { if (searchType !== 'name') { setSelectedPeople([]) setInputValue('') - setPersonName('') } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchType]) @@ -719,21 +816,22 @@ export default function Search() { className="w-full flex items-center gap-2 p-2 hover:bg-gray-50 rounded-t-lg" > {configExpanded ? '−' : '+'} - Search Configuration + Search options {configExpanded && (
{/* Search Type Selector */} -
+
-
@@ -843,7 +943,7 @@ export default function Search() {

- Select people from dropdown or type names manually (comma-separated) + Select people from the dropdown or type names manually. Separate multiple names with commas.

)} @@ -851,40 +951,41 @@ export default function Search() { {searchType === 'date' && (
-
-
)} {searchType === 'tags' && ( -
+
-

Tags

+

Select tags

@@ -892,34 +993,47 @@ export default function Search() { {tagsExpanded && (
-
)} {/* Tag input and dropdown */}
{showTagDropdown && ( @@ -959,8 +1073,11 @@ export default function Search() { setSelectedTags([...selectedTags, tag.tag_name]) } setTagSearchInput('') - setShowTagDropdown(false) - tagInputRef.current?.focus() + // Keep dropdown open and refocus input for continuous selection + setTimeout(() => { + setShowTagDropdown(true) + tagInputRef.current?.focus() + }, 0) }} className="px-3 py-2 hover:bg-gray-100 cursor-pointer text-sm" > @@ -969,7 +1086,7 @@ export default function Search() { )) ) : (
- No tags found + {tagSearchInput.trim() ? 'No matching tags found' : 'Start typing to search for tags'}
) })()} @@ -978,16 +1095,17 @@ export default function Search() {
-
@@ -998,13 +1116,14 @@ export default function Search() { )} {/* Filters */} -
+
-

Filters

+

Additional filters

@@ -1012,17 +1131,18 @@ export default function Search() { {filtersExpanded && (
-
@@ -1034,9 +1154,10 @@ export default function Search() {
diff --git a/src/web/api/role_permissions.py b/src/web/api/role_permissions.py index e1af258..bef4d24 100644 --- a/src/web/api/role_permissions.py +++ b/src/web/api/role_permissions.py @@ -67,3 +67,4 @@ def update_role_permissions( return RolePermissionsResponse(features=features, permissions=permissions) + diff --git a/src/web/api/videos.py b/src/web/api/videos.py index 01a46d1..beebe07 100644 --- a/src/web/api/videos.py +++ b/src/web/api/videos.py @@ -336,3 +336,4 @@ def get_video_file( return response + diff --git a/src/web/schemas/role_permissions.py b/src/web/schemas/role_permissions.py index 68be603..d8c5b8b 100644 --- a/src/web/schemas/role_permissions.py +++ b/src/web/schemas/role_permissions.py @@ -41,3 +41,4 @@ class RolePermissionsUpdateRequest(BaseModel): return [RoleFeatureSchema(**feature) for feature in ROLE_FEATURES] + diff --git a/src/web/schemas/videos.py b/src/web/schemas/videos.py index de4b8ff..d78d0de 100644 --- a/src/web/schemas/videos.py +++ b/src/web/schemas/videos.py @@ -89,3 +89,4 @@ class RemoveVideoPersonResponse(BaseModel): message: str +