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 +