From 264cc8cd587ec2d7849c689a970a78d00b2c4b19 Mon Sep 17 00:00:00 2001 From: lrnselfreliance <33263708+lrnselfreliance@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:49:15 -0600 Subject: [PATCH] Consolidate search options into a single Filter modal (#497) * Consolidate search options into a single Filter modal Reworks the file-search UI on the Dashboard, Videos, Archives, and Docs pages. The crowded row of controls (sort, tags, limit, file-type filter, date selector) is replaced with a single "Filter" button that opens one comprehensive modal. - Add SearchFilterModal + SearchFilterButton (violet + count badge when active). Sections (Sort, File Type, Tags, Results Per Page, Published Date) are shown per-page via props; layout uses a Semantic Grid. - Selections are drafted locally and applied in a single updateQuery only on close/Done, so the search isn't re-run on every field change. - Results Per Page and Sort use buttons (fewer clicks, no clipped dropdown). - Extract shared SearchControlBar (view toggle + input + Filter) for the three results pages; move the global-search view toggle onto the tab row. - Rework the Dashboard search line into one adaptive flex row; clear button only shows when there's an active search. - Remove now-dead FilesView limit/tag returns and SearchLimitDropdown. * Address review: reset limit on Clear All; use replace over replaceAll - handleClearAll now resets Results Per Page to the default page size (DEFAULT_SEARCH_LIMIT = 24) so "Clear All" actually clears every setting. - Replace start-anchored replaceAll(/^-/g) with replace(/^-/) for the sort key. - Add a test covering the Clear All limit reset. * Remove now-unused Media import from Files.js The FilesSearchView refactor dropped the last usage in this file, leaving an unused import (a build warning). Remove it. --------- Co-authored-by: Claude Opus 4.8 (1M context) --- app/src/DashboardPage.js | 74 +--- app/src/components/Archive.js | 49 +-- app/src/components/Common.js | 6 +- app/src/components/DatesSelector.js | 4 +- app/src/components/Docs.js | 50 +-- app/src/components/Files.js | 421 ++++++++++++++++--- app/src/components/Search.js | 9 +- app/src/components/SearchFilterModal.test.js | 129 ++++++ app/src/components/Videos.js | 50 +-- 9 files changed, 549 insertions(+), 243 deletions(-) create mode 100644 app/src/components/SearchFilterModal.test.js diff --git a/app/src/DashboardPage.js b/app/src/DashboardPage.js index 012080d4..10b0ea93 100644 --- a/app/src/DashboardPage.js +++ b/app/src/DashboardPage.js @@ -33,8 +33,7 @@ import {SearchView, useSearch, useSearchSuggestions} from "./components/Search"; import {OutdatedZimsMessage} from "./components/Zim"; import {useSearchFilter, useSearchRecentFiles, useWROLMode} from "./hooks/customHooks"; import {ExtensionInstallSuggestion} from "./components/admin/ExtensionInstallSuggestion"; -import {FileCards, FileSearchFilterButton} from "./components/Files"; -import {DateSelectorButton} from "./components/DatesSelector"; +import {fileMimetypeFilterOptions, FileCards, SearchFilterButton} from "./components/Files"; import {useCalculators} from "./components/Calculators"; import {classifyAndEvaluate} from "./components/calculators/mathConfig"; @@ -394,7 +393,7 @@ export function DashboardPage() { loading, setSearchStr: setSuggestionSearchStr, setSearchTags, - months, dateRange, setDates, clearDate, + clearDate, } = useSearchSuggestions(searchStr, activeTags, anyTag); React.useEffect(() => { @@ -444,7 +443,7 @@ export function DashboardPage() { } const getSearchResultsInput = (props) => { - return ; }; + // The search input grows to fill the row; the Filter button sits immediately after it. Flexbox adapts + // to the client width, so only the control size changes between mobile and larger screens. + const searchRow = (big) => { + const inputProps = {style: {flexGrow: 1, minWidth: 0, marginBottom: 0}}; + if (big) { + inputProps.size = 'big'; + } + return
+ {getSearchResultsInput(inputProps)} + +
; + }; + return - - - - - {getSearchResultsInput()} - - - - - - - - - - - - - - - - - - - - - {getSearchResultsInput({size: 'big'})} - - - - - - - - - - - - - - - {getSearchResultsInput({size: 'big'})} - - - - + {searchRow(false)} + {searchRow(true)} {!searchStr && } {!searchStr && } {body} diff --git a/app/src/components/Archive.js b/app/src/components/Archive.js index 2a83b443..67b59837 100644 --- a/app/src/components/Archive.js +++ b/app/src/components/Archive.js @@ -38,7 +38,6 @@ import { PreviewPath, resolveDataPath, SearchInput, - SortButton, TabLinks, textEllipsis, useTitle @@ -78,10 +77,10 @@ import { useSearchDomain, useSearchOrder } from "../hooks/customHooks"; -import {FileCards, FileRowTagIcon, FilesView} from "./Files"; +import {FileCards, FileRowTagIcon, FilesView, SearchControlBar} from "./Files"; import Grid from "semantic-ui-react/dist/commonjs/collections/Grid"; import _ from "lodash"; -import {Media, ThemeContext} from "../contexts/contexts"; +import {ThemeContext} from "../contexts/contexts"; import {Button, Card, darkTheme, Header, Loader, Placeholder, Popup, Segment, Statistic, Tab} from "./Theme"; import {BulkTagModal} from "./BulkTagModal"; import {taggedImageLabel, TagsSelector} from "../Tags"; @@ -1216,7 +1215,7 @@ function ArchivesPage() { /> ; - const {body, paginator, viewButton, limitDropdown, tagQuerySelector} = FilesView( + const {body, paginator, viewButton} = FilesView( { files: archives, activePage: activePage, @@ -1230,16 +1229,6 @@ function ArchivesPage() { ); - const [localSearchStr, setLocalSearchStr] = React.useState(searchStr || ''); - const searchInput = setLocalSearchStr(null)} - searchStr={localSearchStr} - onSubmit={setSearchStr} - placeholder='Search Archives...' - inputRef={searchInputRef} - />; - // Domain header with edit link when filtering by domain let header; if (domainObj && domainObj.domain) { @@ -1263,30 +1252,14 @@ function ArchivesPage() { return <> {header} - - - - {viewButton} - {limitDropdown} - {tagQuerySelector} - - - - {searchInput} - - - - - - - {viewButton} - {limitDropdown} - {tagQuerySelector} - - {searchInput} - - - + {body} {paginator} setLocalSearchStr(null)} - searchStr={localSearchStr} - onSubmit={setSearchStr} - placeholder='Search Documents...' - inputRef={searchInputRef} - />; - return <> {author &&
Author: {author}
} {subject &&
Subject: {subject}
} - - - - {viewButton} - {limitDropdown} - {tagQuerySelector} - - - - - {searchInput} - - - - - - - {viewButton} - {limitDropdown} - {tagQuerySelector} - - - {searchInput} - - - + {body} {paginator} setView('list')}/>; } -export function SearchLimitDropdown({limits = []}) { - const {limit, setLimit} = usePages(); - const limitOptions = limits.map(i => { - return {key: i, value: i, text: i.toString()} - }); - - return setLimit(value)} - /> -} - export function FilesView( { files, @@ -373,8 +364,6 @@ export function FilesView( onSelect, setPage, headlines = false, - limitOptions = [12, 24, 48, 96], - showAnyTag = false, }, ) { const {view} = useSearchView(); @@ -403,15 +392,11 @@ export function FilesView( } const viewButton = - const limitDropdown = ; - const tagQuerySelector = ; return { body, paginator, viewButton, - limitDropdown, - tagQuerySelector, } } @@ -502,33 +487,357 @@ export function TagsQuerySelector({onChange, showAny = true}) { /> } -export function FileSearchFilterButton({size = 'medium'}) { - // see `filterToMimetypes` - const filterOptions = [ - {key: 'video', text: 'Video', value: 'video'}, - {key: 'archive', text: 'Archive', value: 'archive'}, - {key: 'pdf', text: 'PDF', value: 'pdf'}, - {key: 'ebook', text: 'eBook', value: 'ebook'}, - {key: 'doc', text: 'Document', value: 'doc'}, - {key: 'audio', text: 'Audio', value: 'audio'}, - {key: 'image', text: 'Image', value: 'image'}, - {key: 'zip', text: 'ZIP', value: 'zip'}, - {key: 'model', text: '3D Model', value: 'model'}, - {key: 'software', text: 'Software', value: 'software'}, - ]; +// see `filterToMimetypes` +export const fileMimetypeFilterOptions = [ + {key: 'video', text: 'Video', value: 'video'}, + {key: 'archive', text: 'Archive', value: 'archive'}, + {key: 'pdf', text: 'PDF', value: 'pdf'}, + {key: 'ebook', text: 'eBook', value: 'ebook'}, + {key: 'doc', text: 'Document', value: 'doc'}, + {key: 'audio', text: 'Audio', value: 'audio'}, + {key: 'image', text: 'Image', value: 'image'}, + {key: 'zip', text: 'ZIP', value: 'zip'}, + {key: 'model', text: '3D Model', value: 'model'}, + {key: 'software', text: 'Software', value: 'software'}, +]; - return +export const docMimetypeFilterOptions = [ + {key: 'pdf', text: 'PDF', value: 'pdf'}, + {key: 'epub', text: 'EPUB', value: 'epub'}, + {key: 'comic', text: 'Comic', value: 'comic'}, + {key: 'mobi', text: 'MOBI', value: 'mobi'}, +]; + +export function FileSearchFilterButton({size = 'medium'}) { + return } export function DocSearchFilterButton({size = 'medium'}) { - const filterOptions = [ - {key: 'pdf', text: 'PDF', value: 'pdf'}, - {key: 'epub', text: 'EPUB', value: 'epub'}, - {key: 'comic', text: 'Comic', value: 'comic'}, - {key: 'mobi', text: 'MOBI', value: 'mobi'}, - ]; + return +} - return +// The app-wide default page size (matches usePages' fallback); used to reset Results Per Page on Clear All. +const DEFAULT_SEARCH_LIMIT = 24; + +// A single section within the comprehensive SearchFilterModal. Vertical spacing between sections is +// provided by the surrounding Grid rows. +function SearchFilterSection({header, children}) { + return <> +
{header}
+ {children} + +} + +// A comprehensive modal which combines every search option (sort, tags, file-type filter, dates, limit) +// into a single dialog. Which sections appear is controlled by the props, so each page can show only what +// is relevant. All state is URL-query driven (same hooks the individual controls use), so no extra wiring +// is required on the page. +export function SearchFilterModal( + { + open, + onClose, + sorts = null, + fileFilterOptions = null, + showDates = false, + showTags = true, + showLimit = true, + limitOptions = [12, 24, 48, 96], + }, +) { + // Committed (URL) state — read only; drafts below hold the user's in-progress edits. + const {filter} = useSearchFilter(); + const {sort} = useSearchOrder(); + const {dateRange, months} = useSearchDate(); + const {activeTags, anyTag} = useSearch(); + const {limit} = usePages(DEFAULT_SEARCH_LIMIT); + const {updateQuery} = useContext(QueryContext); + + const emptyRange = [null, null]; + const seededRange = dateRange && (dateRange[0] || dateRange[1]) ? dateRange : emptyRange; + const seededMonths = (months || []).map(i => parseInt(i)); + + // Every selection is drafted locally and only applied to the URL (which performs the search) when the + // modal is closed — via Done, the close icon, or clicking away. + const [draftSort, setDraftSort] = useState(null); + const [draftFilter, setDraftFilter] = useState(null); + const [draftTags, setDraftTags] = useState([]); + const [draftAnyTag, setDraftAnyTag] = useState(false); + const [draftLimit, setDraftLimit] = useState(null); + const [draftMonths, setDraftMonths] = useState([]); + const [draftRange, setDraftRange] = useState(emptyRange); + + // Seed the drafts from the URL each time the modal is opened. + React.useEffect(() => { + if (open) { + setDraftSort(sort || null); + setDraftFilter(filter || null); + setDraftTags(activeTags || []); + setDraftAnyTag(anyTag || false); + setDraftLimit(limit); + setDraftMonths(seededMonths); + setDraftRange(seededRange); + } + }, [open]); + + const handleClose = () => { + // Apply every changed draft in a SINGLE updateQuery — sequential setters would each merge against + // the same stale searchParams and clobber one another. Only reset pagination if something changed. + const params = {}; + if (sorts && (draftSort || null) !== (sort || null)) { + params.order = draftSort; + } + if (fileFilterOptions && (draftFilter || null) !== (filter || null)) { + params.filter = draftFilter; + } + if (showTags && (draftAnyTag !== anyTag || JSON.stringify(draftTags) !== JSON.stringify(activeTags))) { + if (draftAnyTag) { + params.tag = []; + params.anyTag = 'true'; + } else { + params.tag = draftTags; + params.anyTag = null; + } + } + if (showLimit && draftLimit !== limit) { + params.l = draftLimit; + } + if (showDates && (JSON.stringify(draftMonths) !== JSON.stringify(seededMonths) + || JSON.stringify(draftRange) !== JSON.stringify(seededRange))) { + let newFromDate = null; + let newToDate = null; + if (draftRange[0] <= draftRange[1]) { + newFromDate = draftRange[0]; + newToDate = draftRange[1]; + } + params.fromDate = newFromDate; + params.toDate = newToDate; + params.month = draftMonths; + } + if (Object.keys(params).length > 0) { + params.o = 0; + updateQuery(params); + } + if (onClose) { + onClose(); + } + } + + const handleClearAll = () => { + // Reset the form only; nothing is applied until the modal is closed. + setDraftSort(null); + setDraftFilter(null); + setDraftTags([]); + setDraftAnyTag(false); + setDraftLimit(DEFAULT_SEARCH_LIMIT); + setDraftMonths([]); + setDraftRange(emptyRange); + } + + // Sort section (operates on the draft). + const sortKey = draftSort ? draftSort.replace(/^-/, '') : (sorts ? sorts[0]['value'] : null); + const desc = draftSort ? draftSort.startsWith('-') : true; + const sortButtons = sorts && sorts.map(o => + + ); + const directionToggle = ; + + // File-type filter section (single-select radios; clicking the active one clears it). Laid out in + // columns so more options fit on each line. + const filterFields = fileFilterOptions && fileFilterOptions.map(i => + + + setDraftFilter(draftFilter === i['value'] ? null : i['value'])} + /> + + + ); + + // Results-per-page as buttons (matches the Sort By buttons and avoids a dropdown menu being clipped by + // the scrolling modal content). + const limitButtons = limitOptions.map(i => + + ); + + return + Search Filters + + + {sorts && + + + +
{directionToggle}
+
{sortButtons}
+
+
+
} + + {fileFilterOptions && + + + +
{filterFields}
+
+
+
} + + {(showTags || showLimit) && + + {showTags && + + + 0)} + selectedTagNames={draftTags} + anyTag={draftAnyTag} + filterByOverlap={true} + onChange={(tagNames, newAnyTag) => { + if (newAnyTag) { + setDraftAnyTag(true); + setDraftTags([]); + } else { + setDraftAnyTag(false); + setDraftTags(tagNames); + } + }} + style={{marginLeft: '0.3em', marginTop: '0.3em'}} + /> + + } + {showLimit && + + +
{limitButtons}
+
+
} +
} + + {showDates && + + + + + + + + + } +
+
+ + + + +
+} + +// The trigger button which opens the comprehensive SearchFilterModal. Turns violet and displays a count +// badge when any of its filters are active. +export function SearchFilterButton( + { + sorts = null, + fileFilterOptions = null, + showDates = false, + showTags = true, + showLimit = true, + limitOptions = [12, 24, 48, 96], + size = 'medium', + content = 'Filter', + }, +) { + const [open, setOpen] = useState(false); + const {filter} = useSearchFilter(); + const {sort} = useSearchOrder(); + const {dateRange, months} = useSearchDate(); + const {activeTags, anyTag} = useSearch(); + + let count = 0; + if (fileFilterOptions && filter) { + count += 1; + } + if (sorts && sort) { + count += 1; + } + if (showTags && ((activeTags && activeTags.length > 0) || anyTag)) { + count += 1; + } + if (showDates && ((months && months.length > 0) || !dateRangeIsEmpty(dateRange))) { + count += 1; + } + const active = count > 0; + + return <> + + setOpen(false)} + sorts={sorts} + fileFilterOptions={fileFilterOptions} + showDates={showDates} + showTags={showTags} + showLimit={showLimit} + limitOptions={limitOptions} + /> + +} + +// The single-line search control row shared by the Videos/Archives/Docs result pages: +// [ view toggle ] [ search input (grows) ] [ Filter button ] +// The text input is managed locally and only submits its value on enter/clear. Filter options are +// forwarded to the SearchFilterButton so each page shows only the sections relevant to it. +export function SearchControlBar( + { + searchStr, + setSearchStr, + placeholder = 'Search...', + inputRef, + viewButton, + sorts = null, + fileFilterOptions = null, + showDates = false, + }, +) { + const [localSearchStr, setLocalSearchStr] = useState(searchStr || ''); + + const searchInput = setLocalSearchStr(null)} + onSubmit={setSearchStr} + placeholder={placeholder} + inputRef={inputRef} + />; + + return
+ {viewButton} +
{searchInput}
+ +
} export function FilesSearchView({ @@ -539,7 +848,7 @@ export function FilesSearchView({ const {searchFiles, pages} = useSearchFiles(24, emptySearch, model); - const {body, paginator, viewButton, limitDropdown, tagQuerySelector} = FilesView( + const {body, paginator, viewButton} = FilesView( { files: searchFiles, activePage: pages.activePage, @@ -549,31 +858,11 @@ export function FilesSearchView({ onSelect: null, setPage: pages.setPage, headlines: true, - showAnyTag: true, }, ); return <> - - - - {showView && - {viewButton}} - {limitDropdown} - {tagQuerySelector} - - - - - - - {showView && - {viewButton}} - {limitDropdown} - {tagQuerySelector} - - - + {showView &&
{viewButton}
} {body} {paginator} diff --git a/app/src/components/Search.js b/app/src/components/Search.js index a57efd7d..2c5bf5e7 100644 --- a/app/src/components/Search.js +++ b/app/src/components/Search.js @@ -1,6 +1,6 @@ import React, {useState} from "react"; import {Link, Route, Routes, useLocation, useNavigate} from "react-router"; -import {FilesSearchView} from "./Files"; +import {FilesSearchView, SearchViewButton} from "./Files"; import {useLatestRequest, usePages, useSearchChannels, useSearchDate, useSearchFilter} from "../hooks/customHooks"; import {ShortcutHint} from "./ShortcutHint"; import {ZimSearchView} from "./Zim"; @@ -485,10 +485,13 @@ export function SearchView({suggestions, suggestionsSums, loading}) { {text: othersTabName, to: `/search/other${search}`, key: 'othersSearch'}, ]; + // The view toggle only applies to the Files tab; show it in the tab row's right slot there. + const isFilesTab = !/\/(zim|map|other)$/.test(location.pathname); + return - + : null}/> - }/> + }/> }/> }/> }/> diff --git a/app/src/components/SearchFilterModal.test.js b/app/src/components/SearchFilterModal.test.js new file mode 100644 index 00000000..711460ca --- /dev/null +++ b/app/src/components/SearchFilterModal.test.js @@ -0,0 +1,129 @@ +import React from 'react'; +import {fireEvent, render, screen} from '@testing-library/react'; +import {MemoryRouter} from 'react-router'; +import {SearchFilterButton} from './Files'; +import {QueryContext, ThemeContext} from '../contexts/contexts'; + +// Render SearchFilterButton with a controlled QueryContext so the search hooks +// (useSearchFilter/useSearchOrder/useSearchDate/useSearch) can read the URL +// query params without a real router/provider. +function renderButton(props = {}, initialParams = {}, onUpdateQuery = null) { + const params = new URLSearchParams(initialParams); + + function Wrapper({children}) { + const [searchParams, setSearchParams] = React.useState(params); + const updateQuery = (newParams) => { + if (onUpdateQuery) { + onUpdateQuery(newParams); + } + const next = new URLSearchParams(searchParams.toString()); + Object.entries(newParams).forEach(([k, v]) => { + if (v === null || v === undefined) { + next.delete(k); + } else if (Array.isArray(v)) { + next.delete(k); + v.forEach(i => next.append(k, String(i))); + } else { + next.set(k, String(v)); + } + }); + setSearchParams(next); + }; + const theme = { + i: {}, s: {}, t: {}, theme: 'light', inverted: false, setInverted: () => {}, + }; + return ( + + + '/'}}> + {children} + + + + ); + } + + return render(); +} + +const videoOrders = [ + {value: 'published_datetime', text: 'Published Date'}, + {value: 'size', text: 'Size'}, +]; + +describe('SearchFilterButton', () => { + it('renders a Filter button with no count badge when nothing is active', () => { + renderButton({sorts: videoOrders}); + expect(screen.getByText('Filter')).toBeInTheDocument(); + // No active filters -> no numeric badge. + expect(screen.queryByText('1')).toBeNull(); + }); + + it('shows a count badge summing the active filters', () => { + // order + filter + tag => count of 3. + renderButton( + {sorts: videoOrders, fileFilterOptions: [{value: 'video', text: 'Video'}]}, + {order: '-published_datetime', filter: 'video', tag: 'Food'}, + ); + expect(screen.getByText('3')).toBeInTheDocument(); + }); + + it('opens the modal and only shows the sections enabled by props', () => { + renderButton({sorts: videoOrders}); + fireEvent.click(screen.getByText('Filter')); + + // Enabled sections. + expect(screen.getByText('Sort By')).toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Results Per Page')).toBeInTheDocument(); + + // Disabled sections (no fileFilterOptions, showDates defaults false). Assert on the date range + // 'From Year' label, since the 'Published Date' header text collides with a sort option label. + expect(screen.queryByText('File Type')).toBeNull(); + expect(screen.queryByText('From Year')).toBeNull(); + }); + + it('applies changes only on close, not as fields change', () => { + const spy = jest.fn(); + renderButton({sorts: videoOrders}, {}, spy); + fireEvent.click(screen.getByText('Filter')); + + // Changing the sort field updates the draft but must NOT perform the search yet. + fireEvent.click(screen.getByText('Size')); + expect(spy).not.toHaveBeenCalled(); + + // Closing via Done commits the draft in a single query update. + fireEvent.click(screen.getByText('Done')); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0]).toMatchObject({order: '-size'}); + }); + + it('Clear All resets results-per-page to the default on close', () => { + const spy = jest.fn(); + renderButton({sorts: videoOrders}, {l: '96'}, spy); + fireEvent.click(screen.getByText('Filter')); + fireEvent.click(screen.getByText('Clear All')); + fireEvent.click(screen.getByText('Done')); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0]).toMatchObject({l: 24}); + }); + + it('does not update the query when closed without changes', () => { + const spy = jest.fn(); + renderButton({sorts: videoOrders}, {}, spy); + fireEvent.click(screen.getByText('Filter')); + fireEvent.click(screen.getByText('Done')); + expect(spy).not.toHaveBeenCalled(); + }); + + it('shows the File Type section when fileFilterOptions are provided', () => { + renderButton({ + sorts: videoOrders, + fileFilterOptions: [{value: 'pdf', text: 'PDF'}, {value: 'epub', text: 'EPUB'}], + }); + fireEvent.click(screen.getByText('Filter')); + expect(screen.getByText('File Type')).toBeInTheDocument(); + expect(screen.getByText('PDF')).toBeInTheDocument(); + expect(screen.getByText('EPUB')).toBeInTheDocument(); + }); +}); diff --git a/app/src/components/Videos.js b/app/src/components/Videos.js index 26ce1285..cfc77ba1 100644 --- a/app/src/components/Videos.js +++ b/app/src/components/Videos.js @@ -19,9 +19,7 @@ import { PageContainer, PreviewLink, scrollToTop, - SearchInput, secondsToFullDuration, - SortButton, TabLinks, textEllipsis, useTitle, @@ -47,7 +45,7 @@ import { TableCell } from "semantic-ui-react"; import {useChannel, useSearchOrder, useSearchVideos, useVideo, useVideoStatistics} from "../hooks/customHooks"; -import {FileRowTagIcon, FilesView} from "./Files"; +import {FileRowTagIcon, FilesView, SearchControlBar} from "./Files"; import Grid from "semantic-ui-react/dist/commonjs/collections/Grid"; import Icon from "semantic-ui-react/dist/commonjs/elements/Icon"; import {Button, Card, Header, Loader, Modal, Placeholder, Popup, Segment, Statistic} from "./Theme"; @@ -65,7 +63,7 @@ import { updateVideoDownloaderConfig, uploadCookies, } from "../api"; -import {Media, QueryContext, ThemeContext} from "../contexts/contexts"; +import {QueryContext, ThemeContext} from "../contexts/contexts"; import _ from "lodash"; import {defaultFileOrder, defaultSearchOrder} from "./Vars"; import {InputForm, ToggleForm, useForm} from "../hooks/useForm"; @@ -219,7 +217,7 @@ export function VideosPage() { /> ; - const {body, paginator, viewButton, limitDropdown, tagQuerySelector} = FilesView( + const {body, paginator, viewButton} = FilesView( { files: videos, activePage: activePage, @@ -233,42 +231,16 @@ export function VideosPage() { }, ); - const [localSearchStr, setLocalSearchStr] = React.useState(searchStr || ''); - const searchInput = setLocalSearchStr(null)} - onSubmit={setSearchStr} - placeholder='Search Videos...' - inputRef={searchInputRef} - />; - return <> {header} - - - - {viewButton} - {limitDropdown} - {tagQuerySelector} - - - - {searchInput} - - - - - - - {viewButton} - {limitDropdown} - {tagQuerySelector} - - {searchInput} - - - + {body} {paginator}