mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
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 <Media> usage in this file, leaving an unused import (a build warning). Remove it. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
36b29afef7
commit
264cc8cd58
9 changed files with 549 additions and 243 deletions
|
|
@ -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 <SearchResultsInput clearable
|
||||
return <SearchResultsInput clearable={!isEmpty}
|
||||
searchStr={localSearchStr}
|
||||
onChange={setLocalSearchStr}
|
||||
onSubmit={setSearchStr}
|
||||
|
|
@ -461,58 +460,23 @@ export function DashboardPage() {
|
|||
/>;
|
||||
};
|
||||
|
||||
// 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 <div style={{display: 'flex', alignItems: 'center', gap: '0.5em', marginBottom: '2em'}}>
|
||||
{getSearchResultsInput(inputProps)}
|
||||
<SearchFilterButton fileFilterOptions={fileMimetypeFilterOptions} showDates={true}
|
||||
size={big ? 'big' : undefined}/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
return <PageContainer>
|
||||
<Media at='mobile'>
|
||||
<Grid>
|
||||
<Grid.Row columns={2}>
|
||||
<Grid.Column width={12}>
|
||||
{getSearchResultsInput()}
|
||||
</Grid.Column>
|
||||
<Grid.Column width={1} textAlign='right' style={{padding: 0}}>
|
||||
<DateSelectorButton defaultMonthsSelected={months} defaultDateRange={dateRange}
|
||||
onClear={clearDate} onDatesChange={setDates}
|
||||
/>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={1} textAlign='right'>
|
||||
<FileSearchFilterButton/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<Media between={['tablet', 'computer']}>
|
||||
<Grid>
|
||||
<Grid.Row columns={2}>
|
||||
<Grid.Column textAlign='right' width={2}>
|
||||
<DateSelectorButton defaultMonthsSelected={months} defaultDateRange={dateRange}
|
||||
onClear={clearDate} onDatesChange={setDates}
|
||||
buttonProps={{size: 'big'}}/>
|
||||
</Grid.Column>
|
||||
<Grid.Column textAlign='right' width={2}>
|
||||
<FileSearchFilterButton size='big'/>
|
||||
</Grid.Column>
|
||||
<Grid.Column mobile={12}>
|
||||
{getSearchResultsInput({size: 'big'})}
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<Media greaterThanOrEqual='computer'>
|
||||
<Grid>
|
||||
<Grid.Row columns={2}>
|
||||
<Grid.Column textAlign='right' width={1}>
|
||||
<DateSelectorButton defaultMonthsSelected={months} defaultDateRange={dateRange}
|
||||
onClear={clearDate} onDatesChange={setDates}
|
||||
buttonProps={{size: 'big'}}/>
|
||||
</Grid.Column>
|
||||
<Grid.Column textAlign='right' width={1}>
|
||||
<FileSearchFilterButton size='big'/>
|
||||
</Grid.Column>
|
||||
<Grid.Column mobile={14}>
|
||||
{getSearchResultsInput({size: 'big'})}
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<Media at='mobile'>{searchRow(false)}</Media>
|
||||
<Media greaterThanOrEqual='tablet'>{searchRow(true)}</Media>
|
||||
{!searchStr && <FlagsMessages/>}
|
||||
{!searchStr && <ExtensionInstallSuggestion/>}
|
||||
{body}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
/>
|
||||
</div>;
|
||||
|
||||
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 = <SearchInput
|
||||
onChange={setLocalSearchStr}
|
||||
onClear={() => 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}
|
||||
<Media at='mobile'>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={2}>{viewButton}</Grid.Column>
|
||||
<Grid.Column width={4}>{limitDropdown}</Grid.Column>
|
||||
<Grid.Column width={2}>{tagQuerySelector}</Grid.Column>
|
||||
<Grid.Column width={8}><SortButton sorts={archiveOrders}/></Grid.Column>
|
||||
</Grid.Row>
|
||||
<Grid.Row width={16}>
|
||||
<Grid.Column>{searchInput}</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<Media greaterThanOrEqual='tablet'>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={1}>{viewButton}</Grid.Column>
|
||||
<Grid.Column width={2}>{limitDropdown}</Grid.Column>
|
||||
<Grid.Column width={1}>{tagQuerySelector}</Grid.Column>
|
||||
<Grid.Column width={3}><SortButton sorts={archiveOrders}/></Grid.Column>
|
||||
<Grid.Column width={9}>{searchInput}</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<SearchControlBar
|
||||
searchStr={searchStr}
|
||||
setSearchStr={setSearchStr}
|
||||
placeholder='Search Archives...'
|
||||
inputRef={searchInputRef}
|
||||
viewButton={viewButton}
|
||||
sorts={archiveOrders}
|
||||
/>
|
||||
{body}
|
||||
{paginator}
|
||||
<TaggedDeleteConfirmModal
|
||||
|
|
|
|||
|
|
@ -709,7 +709,7 @@ export function textEllipsis(str, maxLength = 100, {side = "end", ellipsis = "..
|
|||
return str;
|
||||
}
|
||||
|
||||
export function TabLinks({links}) {
|
||||
export function TabLinks({links, right}) {
|
||||
return <Menu tabular>
|
||||
{links.map((link) => <NavLink
|
||||
to={link.to}
|
||||
|
|
@ -723,6 +723,10 @@ export function TabLinks({links}) {
|
|||
>
|
||||
{link.text}
|
||||
</NavLink>)}
|
||||
{right &&
|
||||
<Menu.Menu position='right'>
|
||||
<Menu.Item style={{padding: '0.5em'}}>{right}</Menu.Item>
|
||||
</Menu.Menu>}
|
||||
</Menu>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const dateRangeIsEmpty = (dateRange) => {
|
|||
return dateRange[0] === null && dateRange[1] === null;
|
||||
}
|
||||
|
||||
function MonthsForm({monthsSelected, setMonthsSelected}) {
|
||||
export function MonthsForm({monthsSelected, setMonthsSelected}) {
|
||||
monthsSelected = monthsSelected.map(i => parseInt(i));
|
||||
|
||||
const handleWinter = (e) => {
|
||||
|
|
@ -83,7 +83,7 @@ function MonthsForm({monthsSelected, setMonthsSelected}) {
|
|||
</Form>
|
||||
}
|
||||
|
||||
function DateRangeForm({dateRange, setDateRange}) {
|
||||
export function DateRangeForm({dateRange, setDateRange}) {
|
||||
const currentYear = (new Date()).getFullYear();
|
||||
const [error, setError] = React.useState('');
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
PageContainer,
|
||||
PreviewPath,
|
||||
SearchInput,
|
||||
SortButton,
|
||||
TabLinks,
|
||||
toLocaleString,
|
||||
useTitle
|
||||
|
|
@ -22,7 +21,7 @@ import {
|
|||
import {Button, darkTheme, Header, Icon, Segment, Statistic, Tab} from "./Theme";
|
||||
import {BulkTagModal} from "./BulkTagModal";
|
||||
import {TaggedDeleteConfirmModal} from "./TaggedDeleteConfirmModal";
|
||||
import {DocSearchFilterButton, FilesView} from "./Files";
|
||||
import {docMimetypeFilterOptions, FilesView, SearchControlBar} from "./Files";
|
||||
import {useAuthors, useDoc, useOneQuery, useSearchDocs, useSubjects} from "../hooks/customHooks";
|
||||
import {TagsSelector} from "../Tags";
|
||||
import {toast} from "react-semantic-toasts-2";
|
||||
|
|
@ -116,7 +115,7 @@ function DocsPage() {
|
|||
docOrders = [{value: 'rank', text: 'Rank'}, ...docOrders];
|
||||
}
|
||||
|
||||
const {body, paginator, viewButton, limitDropdown, tagQuerySelector} = FilesView(
|
||||
const {body, paginator, viewButton} = FilesView(
|
||||
{
|
||||
files: docs,
|
||||
activePage: activePage,
|
||||
|
|
@ -128,45 +127,18 @@ function DocsPage() {
|
|||
},
|
||||
);
|
||||
|
||||
const [localSearchStr, setLocalSearchStr] = React.useState(searchStr || '');
|
||||
const searchInput = <SearchInput
|
||||
onChange={setLocalSearchStr}
|
||||
onClear={() => setLocalSearchStr(null)}
|
||||
searchStr={localSearchStr}
|
||||
onSubmit={setSearchStr}
|
||||
placeholder='Search Documents...'
|
||||
inputRef={searchInputRef}
|
||||
/>;
|
||||
|
||||
return <>
|
||||
{author && <Header as='h1'>Author: {author}</Header>}
|
||||
{subject && <Header as='h1'>Subject: {subject}</Header>}
|
||||
<Media at='mobile'>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={2}>{viewButton}</Grid.Column>
|
||||
<Grid.Column width={4}>{limitDropdown}</Grid.Column>
|
||||
<Grid.Column width={2}>{tagQuerySelector}</Grid.Column>
|
||||
<Grid.Column width={2}><DocSearchFilterButton/></Grid.Column>
|
||||
<Grid.Column width={6}><SortButton sorts={docOrders}/></Grid.Column>
|
||||
</Grid.Row>
|
||||
<Grid.Row width={16}>
|
||||
<Grid.Column>{searchInput}</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<Media greaterThanOrEqual='tablet'>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={1}>{viewButton}</Grid.Column>
|
||||
<Grid.Column width={2}>{limitDropdown}</Grid.Column>
|
||||
<Grid.Column width={1}>{tagQuerySelector}</Grid.Column>
|
||||
<Grid.Column width={1}><DocSearchFilterButton/></Grid.Column>
|
||||
<Grid.Column width={3}><SortButton sorts={docOrders}/></Grid.Column>
|
||||
<Grid.Column width={8}>{searchInput}</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<SearchControlBar
|
||||
searchStr={searchStr}
|
||||
setSearchStr={setSearchStr}
|
||||
placeholder='Search Documents...'
|
||||
inputRef={searchInputRef}
|
||||
viewButton={viewButton}
|
||||
sorts={docOrders}
|
||||
fileFilterOptions={docMimetypeFilterOptions}
|
||||
/>
|
||||
{body}
|
||||
{paginator}
|
||||
<TaggedDeleteConfirmModal
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import {
|
|||
CardMeta,
|
||||
Checkbox,
|
||||
Container,
|
||||
Dropdown,
|
||||
Form,
|
||||
Image,
|
||||
Label,
|
||||
PlaceholderLine,
|
||||
Step,
|
||||
TableCell,
|
||||
|
|
@ -29,12 +29,14 @@ import {
|
|||
PageContainer,
|
||||
Paginator,
|
||||
PreviewLink,
|
||||
SearchInput,
|
||||
TagIcon,
|
||||
textEllipsis,
|
||||
useTitle
|
||||
} from "./Common";
|
||||
import {
|
||||
usePages,
|
||||
useSearchDate,
|
||||
useSearchFiles,
|
||||
useSearchFilter,
|
||||
useSearchOrder,
|
||||
|
|
@ -47,10 +49,12 @@ import {Link, Route, Routes, useSearchParams} from "react-router";
|
|||
import {CardPlaceholder} from "./Placeholder";
|
||||
import {ArchiveCard, ArchiveRowCells} from "./Archive";
|
||||
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
|
||||
import {Media, ThemeContext} from "../contexts/contexts";
|
||||
import {QueryContext, ThemeContext} from "../contexts/contexts";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Header,
|
||||
Icon,
|
||||
Modal,
|
||||
Placeholder,
|
||||
|
|
@ -58,6 +62,7 @@ import {
|
|||
Progress,
|
||||
Segment
|
||||
} from "./Theme";
|
||||
import {DateRangeForm, dateRangeIsEmpty, MonthsForm} from "./DatesSelector";
|
||||
import {SelectableTable} from "./Tables";
|
||||
import {VideoCard, VideoRowCells} from "./Videos";
|
||||
import {FileBrowser} from "./FileBrowser";
|
||||
|
|
@ -349,20 +354,6 @@ export function SearchViewButton({headlines}) {
|
|||
return <Button icon='browser' onClick={() => setView('list')}/>;
|
||||
}
|
||||
|
||||
export function SearchLimitDropdown({limits = []}) {
|
||||
const {limit, setLimit} = usePages();
|
||||
const limitOptions = limits.map(i => {
|
||||
return {key: i, value: i, text: i.toString()}
|
||||
});
|
||||
|
||||
return <Dropdown fluid selection
|
||||
placeholder='Limit'
|
||||
options={limitOptions}
|
||||
value={limit || limitOptions[0]['value']}
|
||||
onChange={(e, {value}) => 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 = <SearchViewButton headlines={headlines}/>
|
||||
const limitDropdown = <SearchLimitDropdown limits={limitOptions}/>;
|
||||
const tagQuerySelector = <TagsQuerySelector showAny={showAnyTag}/>;
|
||||
|
||||
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 <SearchFilter filters={filterOptions} size={size}/>
|
||||
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 <SearchFilter filters={fileMimetypeFilterOptions} size={size}/>
|
||||
}
|
||||
|
||||
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 <SearchFilter filters={docMimetypeFilterOptions} size={size}/>
|
||||
}
|
||||
|
||||
return <SearchFilter filters={filterOptions} size={size}/>
|
||||
// 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 as='h4' style={{marginBottom: '0.6em'}}>{header}</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 =>
|
||||
<Button key={o['value']}
|
||||
color={o['value'] === sortKey ? 'violet' : undefined}
|
||||
onClick={() => setDraftSort(desc ? `-${o['value']}` : o['value'])}
|
||||
style={{margin: '0.2em'}}
|
||||
>{o['text']}</Button>
|
||||
);
|
||||
const directionToggle = <Button icon labelPosition='left'
|
||||
onClick={() => {
|
||||
const base = sortKey || sorts[0]['value'];
|
||||
setDraftSort(desc ? base : `-${base}`);
|
||||
}}>
|
||||
<Icon name={desc ? 'sort amount down' : 'sort amount up'}/>
|
||||
{desc ? 'Descending' : 'Ascending'}
|
||||
</Button>;
|
||||
|
||||
// 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 =>
|
||||
<Grid.Column mobile={8} tablet={5} computer={4} key={i['value']}>
|
||||
<Form.Field>
|
||||
<Checkbox radio
|
||||
label={i['text']}
|
||||
name='searchFilterRadioGroup'
|
||||
checked={draftFilter === i['value']}
|
||||
value={i['value']}
|
||||
onChange={() => setDraftFilter(draftFilter === i['value'] ? null : i['value'])}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Grid.Column>
|
||||
);
|
||||
|
||||
// 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 =>
|
||||
<Button key={i}
|
||||
color={(draftLimit || limitOptions[0]) === i ? 'violet' : undefined}
|
||||
onClick={() => setDraftLimit(i)}
|
||||
style={{margin: '0.2em'}}
|
||||
>{i}</Button>
|
||||
);
|
||||
|
||||
return <Modal open={open} onClose={handleClose} closeIcon size='small'>
|
||||
<Modal.Header>Search Filters</Modal.Header>
|
||||
<Modal.Content scrolling>
|
||||
<Grid>
|
||||
{sorts &&
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<SearchFilterSection header='Sort By'>
|
||||
<div style={{marginBottom: '1em'}}>{directionToggle}</div>
|
||||
<div>{sortButtons}</div>
|
||||
</SearchFilterSection>
|
||||
</Grid.Column>
|
||||
</Grid.Row>}
|
||||
|
||||
{fileFilterOptions &&
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<SearchFilterSection header='File Type'>
|
||||
<Form><Grid>{filterFields}</Grid></Form>
|
||||
</SearchFilterSection>
|
||||
</Grid.Column>
|
||||
</Grid.Row>}
|
||||
|
||||
{(showTags || showLimit) &&
|
||||
<Grid.Row columns={2}>
|
||||
{showTags &&
|
||||
<Grid.Column>
|
||||
<SearchFilterSection header='Tags'>
|
||||
<TagsSelector hideGroup hideEdit showAny
|
||||
active={draftAnyTag || (draftTags && draftTags.length > 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'}}
|
||||
/>
|
||||
</SearchFilterSection>
|
||||
</Grid.Column>}
|
||||
{showLimit &&
|
||||
<Grid.Column>
|
||||
<SearchFilterSection header='Results Per Page'>
|
||||
<div>{limitButtons}</div>
|
||||
</SearchFilterSection>
|
||||
</Grid.Column>}
|
||||
</Grid.Row>}
|
||||
|
||||
{showDates &&
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<SearchFilterSection header='Published Date'>
|
||||
<MonthsForm monthsSelected={draftMonths} setMonthsSelected={setDraftMonths}/>
|
||||
<Divider/>
|
||||
<DateRangeForm dateRange={draftRange} setDateRange={setDraftRange}/>
|
||||
</SearchFilterSection>
|
||||
</Grid.Column>
|
||||
</Grid.Row>}
|
||||
</Grid>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button secondary floated='left' onClick={handleClearAll}>Clear All</Button>
|
||||
<Button primary onClick={handleClose}>Done</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
// 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 <>
|
||||
<Button color={active ? 'violet' : 'grey'} size={size} onClick={() => setOpen(true)}>
|
||||
<Icon name='filter'/>
|
||||
{content}
|
||||
{count > 0 &&
|
||||
<Label circular size='tiny' style={{marginLeft: '0.7em'}}>{count}</Label>}
|
||||
</Button>
|
||||
<SearchFilterModal
|
||||
open={open}
|
||||
onClose={() => 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 = <SearchInput
|
||||
searchStr={localSearchStr}
|
||||
onChange={setLocalSearchStr}
|
||||
onClear={() => setLocalSearchStr(null)}
|
||||
onSubmit={setSearchStr}
|
||||
placeholder={placeholder}
|
||||
inputRef={inputRef}
|
||||
/>;
|
||||
|
||||
return <div style={{display: 'flex', alignItems: 'center', gap: '0.5em', marginBottom: '1em'}}>
|
||||
{viewButton}
|
||||
<div style={{flexGrow: 1, minWidth: 0}}>{searchInput}</div>
|
||||
<SearchFilterButton sorts={sorts} fileFilterOptions={fileFilterOptions} showDates={showDates}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
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 <>
|
||||
<Media at='mobile'>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
{showView &&
|
||||
<Grid.Column width={2}>{viewButton}</Grid.Column>}
|
||||
<Grid.Column width={4}>{limitDropdown}</Grid.Column>
|
||||
<Grid.Column width={2}>{tagQuerySelector}</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<Media greaterThanOrEqual='tablet'>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
{showView &&
|
||||
<Grid.Column width={1}>{viewButton}</Grid.Column>}
|
||||
<Grid.Column width={2}>{limitDropdown}</Grid.Column>
|
||||
<Grid.Column width={1}>{tagQuerySelector}</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
{showView && <div style={{marginBottom: '1em'}}>{viewButton}</div>}
|
||||
{body}
|
||||
{paginator}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 <React.Fragment>
|
||||
<TabLinks links={links}/>
|
||||
<TabLinks links={links} right={isFilesTab ? <SearchViewButton headlines/> : null}/>
|
||||
<Routes>
|
||||
<Route path='/*' element={<FilesSearchView/>}/>
|
||||
<Route path='/*' element={<FilesSearchView showView={false}/>}/>
|
||||
<Route path='/zim' exact element={<ZimSearchView suggestions={suggestions} loading={loading}/>}/>
|
||||
<Route path='/map' exact element={<MapSearchView/>}/>
|
||||
<Route path='/other' exact element={<OtherSearchView loading={loading}/>}/>
|
||||
|
|
|
|||
129
app/src/components/SearchFilterModal.test.js
Normal file
129
app/src/components/SearchFilterModal.test.js
Normal file
|
|
@ -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 (
|
||||
<MemoryRouter>
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<QueryContext.Provider value={{searchParams, updateQuery, getLocationStr: () => '/'}}>
|
||||
{children}
|
||||
</QueryContext.Provider>
|
||||
</ThemeContext.Provider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
return render(<Wrapper><SearchFilterButton {...props}/></Wrapper>);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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() {
|
|||
/>
|
||||
</div>;
|
||||
|
||||
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 = <SearchInput
|
||||
searchStr={localSearchStr}
|
||||
onChange={setLocalSearchStr}
|
||||
onClear={() => setLocalSearchStr(null)}
|
||||
onSubmit={setSearchStr}
|
||||
placeholder='Search Videos...'
|
||||
inputRef={searchInputRef}
|
||||
/>;
|
||||
|
||||
return <>
|
||||
{header}
|
||||
<Media at='mobile'>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={2}>{viewButton}</Grid.Column>
|
||||
<Grid.Column width={4}>{limitDropdown}</Grid.Column>
|
||||
<Grid.Column width={2}>{tagQuerySelector}</Grid.Column>
|
||||
<Grid.Column width={8}><SortButton sorts={videoOrders}/></Grid.Column>
|
||||
</Grid.Row>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={16}>{searchInput}</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<Media greaterThanOrEqual='tablet'>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={1}>{viewButton}</Grid.Column>
|
||||
<Grid.Column width={2}>{limitDropdown}</Grid.Column>
|
||||
<Grid.Column width={1}>{tagQuerySelector}</Grid.Column>
|
||||
<Grid.Column width={4}><SortButton sorts={videoOrders}/></Grid.Column>
|
||||
<Grid.Column width={8}>{searchInput}</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Media>
|
||||
<SearchControlBar
|
||||
searchStr={searchStr}
|
||||
setSearchStr={setSearchStr}
|
||||
placeholder='Search Videos...'
|
||||
inputRef={searchInputRef}
|
||||
viewButton={viewButton}
|
||||
sorts={videoOrders}
|
||||
/>
|
||||
{body}
|
||||
{paginator}
|
||||
<TaggedDeleteConfirmModal
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue