2024-01-01 17:11:09 -07:00
import React , { useState } from "react" ;
2026-03-22 13:30:33 -06:00
import { Link , Route , Routes , useLocation , useNavigate } from "react-router" ;
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>
2026-06-23 19:49:15 -06:00
import { FilesSearchView , SearchViewButton } from "./Files" ;
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
import { useLatestRequest , usePages , useSearchChannels , useSearchDate , useSearchDeep , useSearchFilter } from "../hooks/customHooks" ;
2026-02-23 15:59:40 -07:00
import { ShortcutHint } from "./ShortcutHint" ;
2023-05-29 18:04:03 -06:00
import { ZimSearchView } from "./Zim" ;
2026-04-11 20:09:30 -06:00
import { MapSearchView } from "./MapSearchView" ;
import { searchEstimateFiles , searchEstimateMap , searchEstimateOthers , searchEstimateZims , searchSuggestions } from "../api" ;
2024-02-05 16:04:19 -07:00
import { filterToMimetypes , fuzzyMatch , normalizeEstimate , SearchResultsInput , TabLinks } from "./Common" ;
2023-11-20 11:07:19 -07:00
import _ from "lodash" ;
2023-11-21 13:01:52 -07:00
import { TagsContext } from "../Tags" ;
ui: thirty-five files off Semantic, leaving fifteen
Phase 2, part 4. Baseline 50 -> 15.
Migrated: all eight calculators, the rest of inventory and collections, the
Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage,
DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings
and Status, Download, and App.
Also the three shared modules everything imports: api.js and customHooks.js
swap the Semantic toast library for the library's shim (same call shape), and
useForm.js renders the library's Message. useForm accepts both the old
`{content, header, positive|negative}` shape and the new `{kind, title}`,
because the pages building those objects migrate separately.
Two library gaps closed, both found by call sites needing them:
ActionInput is a forwardRef now. Its ref reaches the input rather than the
wrapper, because callers use it to focus or select the field -- a ref to the
surrounding div would do neither, which is why one call site had resorted to
hand-copying the component's markup.
Header takes a `color` token name. Three separate call sites had reached for
an inline style to colour a heading that carries meaning ("3 items to add"
against "2 to remove"), which is how a hex ends up in markup.
Two files needed repair after being interrupted mid-edit: MapViewer had a
Segment left in its error branch, and VideoPlayer had lost the useContext
import its own code still used. Neither jest nor tsc noticed; only
`npm run build` did, which is the whole reason that check exists.
Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser,
FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim,
ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the
unmigrated files still import its Semantic-backed wrappers, so it can only go
once they have.
2026-07-30 12:21:30 -06:00
import { Accordion , Header , Icon , Label , Loading , Panel , Stack } from "./ui" ;
import { QueryContext } from "../contexts/contexts" ;
2025-12-26 18:59:13 -07:00
import { KeyboardShortcutsContext } from "../contexts/KeyboardShortcutsContext" ;
2023-05-29 18:04:03 -06:00
2023-11-21 21:20:09 -07:00
const SUGGESTED _APPS = [
2024-01-01 15:16:03 -07:00
{ location : '/admin' , title : 'Downloads' , description : 'View and control your downloads' } ,
{ location : '/admin/settings' , title : 'Hotspot' , description : 'Control your hotspot' } ,
{ location : '/admin/settings' , title : 'Restart' , description : 'Restart your WROLPi' } ,
2023-11-20 11:29:14 -07:00
{ location : '/admin/settings' , title : 'Settings' , description : 'View and modify your settings' } ,
2024-01-01 15:16:03 -07:00
{ location : '/admin/settings' , title : 'Shutdown' , description : 'Shutdown your WROLPi' } ,
2023-11-21 21:20:09 -07:00
{ location : '/admin/status' , title : 'Status' , description : 'View the status of this WROLPi server' } ,
2023-11-20 11:29:14 -07:00
{ location : '/admin/wrol' , title : 'WROL Mode' , description : 'Enable or disable WROL Mode' } ,
2026-03-20 15:38:32 -06:00
{ location : '/archives' , title : 'Archives' , description : 'View your Archives' } ,
{ location : '/archives/domains' , title : 'Domains' , description : 'View the domains of your Archives' } ,
2024-01-01 15:16:03 -07:00
{ location : '/files' , title : 'Files' , description : 'View your files' } ,
2023-11-21 21:20:09 -07:00
{ location : '/help' , title : 'Help' , description : 'Help documents for WROLPi' } ,
2024-01-01 15:16:03 -07:00
{ location : '/inventory' , title : 'Inventory' , description : 'Track and organize your food storage' } ,
{ location : '/map' , title : 'Map' , description : 'View your Map' } ,
{ location : '/map/manage' , title : 'Manage Map' , description : 'Manage your Map' } ,
2026-07-01 18:48:06 -06:00
{ location : '/more/calculators?calc=otp' , title : 'One Time Pad' , description : 'Encrypt and Decrypt messages' } ,
2024-01-01 15:16:03 -07:00
{ location : '/more/statistics' , title : 'Statistics' , description : 'View the statistics of your WROLPi' } ,
2026-07-01 18:48:06 -06:00
{
location : '/more/calculators?calc=vin' ,
title : 'Vin Decoder' ,
description : 'Decode and analyze vehicle VIN numbers'
} ,
2024-01-01 15:16:03 -07:00
{ location : '/videos' , title : 'Videos' , description : 'View your Videos' } ,
{ location : '/videos/channel' , title : 'Channels' , description : 'View the Channels of your Videos' } ,
{ location : '/zim' , title : 'Zim' , description : 'View your Zims' } ,
{ location : '/zim/manage' , title : 'Manage Zim' , description : 'Manage your Zims' } ,
2024-05-31 23:52:27 -06:00
{
location : '/more/calculators?calc=temperature' ,
title : 'Temperature Calculator' ,
description : 'Convert Celsius, Fahrenheit, Kelvin'
} ,
{
location : '/more/calculators?calc=electrical' ,
title : 'Electrical Calculator' ,
2024-06-25 17:05:41 -06:00
description : 'Convert Volts, Ohms, Amps, Watts, Power Loss.'
2024-05-31 23:52:27 -06:00
} ,
2024-06-20 09:51:29 -06:00
{
location : '/more/calculators?calc=antenna' ,
title : 'Antenna Calculator' ,
description : 'Calculate dipole length, wavelength.'
} ,
2026-05-28 15:05:19 -06:00
{
location : '/more/calculators?calc=drive' ,
title : 'Drive Ratio Calculator' ,
description : 'Calculate pulley, gear, and sprocket ratios, RPM, torque, belt and chain length.'
} ,
2026-05-28 17:43:48 -06:00
{
location : '/more/calculators?calc=water' ,
title : 'Water Storage Calculator' ,
description : 'Calculate water storage for your household: drinking and hygiene needs, how much to store, and how many drums, buckets, or bottles you need.'
} ,
2023-11-20 11:29:14 -07:00
] ;
2023-11-26 20:49:36 -07:00
export const useSearch = ( defaultLimit = 48 , totalPages = 0 , emptySearch = false , model ) => {
2023-11-20 11:29:14 -07:00
const navigate = useNavigate ( ) ;
2024-05-25 10:48:08 -06:00
const { dateRange , setDateRange , months , setDates , isEmpty : datesIsEmpty } = useSearchDate ( ) ;
2024-03-06 17:41:04 -07:00
const { searchParams , updateQuery , getLocationStr } = React . useContext ( QueryContext ) ;
2023-11-26 20:49:36 -07:00
// `searchStr` means actually fetch the files/zims.
2023-11-20 11:01:46 -07:00
const searchStr = searchParams . get ( 'q' ) ;
2023-11-26 20:49:36 -07:00
// User can search only by Tags, `searchStr` not required.
const activeTags = searchParams . getAll ( 'tag' ) ;
const pages = usePages ( defaultLimit , totalPages ) ;
// text/html, video*, image*, etc.
const filter = searchParams . get ( 'filter' ) ;
2026-03-17 18:35:57 -06:00
// archive/video/doc/etc.
2023-11-26 20:49:36 -07:00
const model _ = searchParams . get ( 'model' ) || model ;
2024-05-25 10:48:08 -06:00
const anySearch = ( ! datesIsEmpty ) || searchStr || ( activeTags && activeTags . length > 0 ) || filter || model ;
const isEmpty = ! anySearch ;
2023-11-26 20:49:36 -07:00
const setSearchStr = ( value ) => {
const searchQuery = { q : value , o : 0 } ;
if ( filter ) {
searchQuery [ 'filter' ] = filter ;
}
2024-03-06 17:41:04 -07:00
const location = getLocationStr ( searchQuery , '/search' ) ;
navigate ( location ) ;
2023-11-26 20:49:36 -07:00
}
const clearSearch = ( ) => {
navigate ( { pathname : window . location . pathname , search : '' } ) ;
}
const setTags = ( tags ) => {
2024-07-22 16:36:57 -06:00
updateQuery ( { tag : tags , anyTag : null } ) ;
2023-11-26 20:49:36 -07:00
}
const addTag = ( name ) => {
const newTags = [ ... activeTags , name ] ;
setTags ( newTags ) ;
}
const removeTag = ( name ) => {
const newTags = activeTags . filter ( i => i !== name ) ;
setTags ( newTags ) ;
}
2024-07-22 16:36:57 -06:00
const anyTag = searchParams . get ( 'anyTag' ) === 'true' ;
const setAnyTag = ( value ) => {
updateQuery ( { tag : [ ] , anyTag : value ? 'true' : null } ) ;
}
2023-11-26 20:49:36 -07:00
return {
2024-07-22 16:36:57 -06:00
activeTags , addTag , removeTag , anyTag , setAnyTag , setTags ,
2023-11-26 20:49:36 -07:00
filter ,
model : model _ ,
pages ,
searchParams ,
searchStr ,
setSearchStr ,
clearSearch ,
2024-04-14 14:01:07 -06:00
months , dateRange , setDateRange , setDates ,
2024-05-25 10:48:08 -06:00
anySearch , isEmpty ,
2023-11-26 20:49:36 -07:00
}
}
2023-11-20 11:01:46 -07:00
2024-07-22 16:36:57 -06:00
export function useSuggestions ( searchStr , tagNames , filter , anyTag ) {
2024-01-01 15:16:03 -07:00
const defaultSuggestions = {
fileGroups : [ ] ,
channels : [ ] ,
domains : [ ] ,
2026-03-17 18:35:57 -06:00
authors : [ ] ,
subjects : [ ] ,
2024-01-01 15:16:03 -07:00
zimsEstimates : [ ] ,
}
const [ suggestions , setSuggestions ] = React . useState ( defaultSuggestions ) ;
2024-04-14 14:01:07 -06:00
const { dateRange , months } = useSearchDate ( ) ;
2024-04-16 09:39:29 -06:00
// channels/domains.
const { data : generalData , sendRequest : sendGeneralReqeust , loading : generalLoading } = useLatestRequest ( 500 ) ;
const { data : filesData , sendRequest : sendFilesRequest , loading : filesLoading } = useLatestRequest ( 500 ) ;
2024-01-01 15:16:03 -07:00
// Zims are slow, so they are separate.
2024-01-01 17:11:09 -07:00
const { data : zimData , sendRequest : sendZimRequest , loading : zimLoading } = useLatestRequest ( 500 ) ;
2024-08-15 19:32:01 -06:00
const { data : otherData , sendRequest : sendOtherRequest , loading : otherLoading } = useLatestRequest ( 500 ) ;
2026-04-11 20:09:30 -06:00
const { data : mapData , sendRequest : sendMapRequest , loading : mapLoading } = useLatestRequest ( 500 ) ;
2024-01-01 15:16:03 -07:00
React . useEffect ( ( ) => {
2024-04-16 10:10:16 -06:00
if ( searchStr || ( tagNames && tagNames . length > 0 ) ) {
2024-02-05 16:04:19 -07:00
const mimetypes = filterToMimetypes ( filter ) ;
2024-04-16 10:04:06 -06:00
setSuggestions ( defaultSuggestions ) ;
2024-04-16 10:10:16 -06:00
if ( searchStr && searchStr . length > 0 ) {
// We can't search Channels/Domains without some string to filter by.
sendGeneralReqeust ( async ( ) => await searchSuggestions ( searchStr ) ) ;
2026-04-11 20:09:30 -06:00
sendMapRequest ( async ( ) => await searchEstimateMap ( searchStr ) ) ;
2024-04-16 10:10:16 -06:00
}
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
// The files estimate always uses deep search server-side, so users know results are possible.
2024-07-22 16:36:57 -06:00
sendFilesRequest ( async ( ) => await searchEstimateFiles ( searchStr , tagNames , mimetypes , months , dateRange , anyTag ) ) ;
2024-01-01 15:16:03 -07:00
sendZimRequest ( async ( ) => await searchEstimateZims ( searchStr , tagNames ) ) ;
2024-08-15 19:32:01 -06:00
sendOtherRequest ( async ( ) => await searchEstimateOthers ( tagNames ) ) ;
2024-01-01 15:16:03 -07:00
}
2024-03-06 17:41:04 -07:00
} , [
searchStr ,
2024-04-16 09:39:29 -06:00
sendGeneralReqeust ,
2024-03-06 17:41:04 -07:00
sendZimRequest ,
JSON . stringify ( tagNames ) ,
JSON . stringify ( months ) ,
JSON . stringify ( dateRange ) ,
2024-06-12 16:35:33 -06:00
filter ,
2024-07-22 16:36:57 -06:00
anyTag ,
2024-03-06 17:41:04 -07:00
] ) ;
2023-11-26 20:49:36 -07:00
React . useEffect ( ( ) => {
2024-04-16 09:39:29 -06:00
if ( ! _ . isEmpty ( generalData ) ) {
2024-04-16 10:04:06 -06:00
setSuggestions ( ( prevState ) => {
return {
... prevState ,
channels : generalData . channels ,
domains : generalData . domains ,
2026-03-17 18:35:57 -06:00
authors : generalData . authors ,
subjects : generalData . subjects ,
2024-04-16 10:04:06 -06:00
}
2024-01-01 15:16:03 -07:00
} ) ;
2023-11-26 20:49:36 -07:00
}
2024-04-16 10:04:06 -06:00
} , [ setSuggestions , generalData ] ) ;
2024-04-16 09:39:29 -06:00
React . useEffect ( ( ) => {
if ( ! _ . isEmpty ( filesData ) ) {
2024-04-16 10:04:06 -06:00
setSuggestions ( ( prevState ) => {
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
return { ... prevState , fileGroups : filesData . fileGroups , fileGroupsDeep : filesData . fileGroupsDeep }
2024-04-16 09:39:29 -06:00
} ) ;
}
2024-04-16 10:04:06 -06:00
} , [ setSuggestions , filesData ] ) ;
2023-11-26 20:49:36 -07:00
2024-01-01 15:16:03 -07:00
React . useEffect ( ( ) => {
if ( ! _ . isEmpty ( zimData ) ) {
2024-04-16 10:04:06 -06:00
setSuggestions ( ( prevState ) => {
return { ... prevState , zimsEstimates : zimData . zimsEstimates }
} ) ;
2024-01-01 15:16:03 -07:00
}
2024-04-16 10:04:06 -06:00
} , [ setSuggestions , zimData ] ) ;
2023-11-26 20:49:36 -07:00
2024-08-15 19:32:01 -06:00
React . useEffect ( ( ) => {
if ( ! _ . isEmpty ( otherData ) ) {
setSuggestions ( ( prevState ) => {
return { ... prevState , otherEstimates : otherData . others }
} ) ;
}
} , [ setSuggestions , otherData ] ) ;
2026-04-11 20:09:30 -06:00
React . useEffect ( ( ) => {
if ( ! _ . isEmpty ( mapData ) ) {
setSuggestions ( ( prevState ) => {
return { ... prevState , mapPlaces : mapData . mapPlaces }
} ) ;
}
} , [ setSuggestions , mapData ] ) ;
2024-04-16 09:39:29 -06:00
return { suggestions , loading : generalLoading || zimLoading || filesLoading }
2023-11-26 20:49:36 -07:00
}
2024-07-22 16:36:57 -06:00
export function useSearchSuggestions ( defaultSearchStr , defaultTagNames , anyTag ) {
2023-11-26 20:49:36 -07:00
const navigate = useNavigate ( ) ;
2024-02-05 16:04:19 -07:00
const { filter } = useSearchFilter ( ) ;
2024-01-01 11:31:17 -07:00
const [ searchStr , setSearchStr ] = React . useState ( defaultSearchStr || '' ) ;
2024-01-01 15:16:03 -07:00
const [ searchTags , setSearchTags ] = React . useState ( defaultTagNames ) ;
2023-11-26 20:49:36 -07:00
const { SingleTag , fuzzyMatchTagsByName } = React . useContext ( TagsContext ) ;
2024-04-14 14:01:07 -06:00
const { dateRange , months , setDates , clearDate } = useSearchDate ( ) ;
2024-07-22 16:36:57 -06:00
const { suggestions , loading } = useSuggestions ( searchStr , searchTags , filter , anyTag ) ;
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
const { deep } = useSearchDeep ( ) ;
2024-03-06 17:41:04 -07:00
const { getLocationStr } = React . useContext ( QueryContext ) ;
2023-11-26 20:49:36 -07:00
// The results that will be displayed by <Search>.
2023-11-22 12:19:49 -07:00
const [ suggestionsResults , setSuggestionsResults ] = useState ( { } ) ;
2023-11-26 20:49:36 -07:00
// The results summarized.
2023-11-22 12:19:49 -07:00
const [ suggestionsSums , setSuggestionsSums ] = useState ( { } ) ;
2023-11-20 11:01:46 -07:00
2025-08-26 17:44:31 -06:00
// Disable selecting a result briefly after new suggestions arrive.
const [ selectDisabled , setSelectDisabled ] = React . useState ( false ) ;
const selectDisableTimerRef = React . useRef ( null ) ;
2024-02-05 16:04:19 -07:00
const noResults = [ { title : 'No results' } ] ;
2023-11-22 12:19:49 -07:00
const normalizeSuggestionsResults = ( newSuggestions ) => {
ui: the last of Semantic UI, in name and in dead CSS
The library has been uninstalled for some time, but the codebase still read as
though it were there: identifiers named after it, rules written against its
class names, and about 180 comments explaining a rule in terms of a thing that
is no longer installed.
Renamed, keeping the vocabulary the app actually uses: semanticUIColorMap ->
navColorHexMap, semanticColorNames -> paletteColorNames, semanticNames ->
iconNameAliases, semanticSizes -> sizeAliases, and SemanticUIColors ->
NavColors in wrolpi/schema.py, which is what validates nav_color on the way in.
ColorToSemanticHexColor had no callers at all and is gone.
The baseline ratchet has done its job -- the list reached zero, so the JSON is
deleted and the guard keeps only the checks with teeth: no package, no import,
no `className="ui "`, no toast library. Its Theme.tsx check went with the
wrappers it was watching for. It also asserts its own walk found files, so a
clean result cannot mean it scanned nothing.
Deleted from App.css: every rule requiring `.ui`, `.inverted`, `.segment` or
`.modal`, none of which any component has set since the stylesheet stopped
loading, plus the form skeletons, ui-alerts and progress-text rules that
nothing renders. Two of them were doing harm rather than nothing: the drag
tints had a dark-theme variant under `.inverted` that stopped applying, leaving
15% of a fixed blue over a dark row, and now mix a token instead.
Two selectors naming markup that no longer exists were live bugs. The file
browser's filter had `.ui.input.file-browser-filter`, so it rendered at the
browser default width on a bar laid out to give it 34em -- now qualified by its
container, because `.wrolpi-action-input` sets `width: 100%` at the same
specificity and is loaded later. And SearchBox takes named props rather than a
rest spread, so the `id` DestinationForm hands it was dropped in silence and
the label it renders pointed at nothing; the download forms' Destination field
has had no associated label since the migration.
"Semantic roles" survives in four places, where it means what it says.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:15:57 -06:00
// Convert the suggestions from the Backend to what SearchBox expects.
2024-01-01 15:16:03 -07:00
const lowerSearchStr = searchStr ? searchStr . toLowerCase ( ) : '' ;
2023-11-21 21:15:24 -07:00
2023-11-22 12:19:49 -07:00
let results = { } ;
2023-11-21 21:15:24 -07:00
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
// The dropdown always suggests the deep count so users know what is findable. The search results
// page shows the active mode's count, and offers deep search when it found less than promised.
const dropdownEstimate = newSuggestions . fileGroupsDeep ? ? newSuggestions . fileGroups ;
// The tab badge shows the count matching the active search mode, so it agrees with the result list.
const fileGroupsEstimate = deep ? newSuggestions . fileGroupsDeep : newSuggestions . fileGroups ;
2023-11-21 21:15:24 -07:00
// Suggested results are ordered.
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
if ( dropdownEstimate > 0 ) {
2023-11-22 12:19:49 -07:00
results . fileGroups = {
2023-11-21 21:15:24 -07:00
name : 'Files' , results : [
2023-11-21 21:20:09 -07:00
{
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
title : dropdownEstimate . toString ( ) ,
2023-11-21 21:20:09 -07:00
type : 'files' ,
2024-02-12 12:12:07 -07:00
// Add search query onto current location.
2024-02-17 13:46:28 -07:00
location : getLocationStr ( { q : searchStr } , '/search' ) ,
2023-11-21 21:20:09 -07:00
}
2023-11-21 21:15:24 -07:00
]
} ;
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
} else if ( dropdownEstimate === 0 ) {
2024-02-05 16:04:19 -07:00
// Tell the user there are no files.
results . fileGroups = { name : 'Files' , results : noResults } ;
2023-11-21 21:15:24 -07:00
}
2023-11-22 10:35:01 -07:00
2024-01-01 15:16:03 -07:00
const zimSum = newSuggestions . zimsEstimates && newSuggestions . zimsEstimates . length > 0
? newSuggestions . zimsEstimates . reduce ( ( i , j ) => i + j . estimate , 0 )
: null ;
if ( newSuggestions && zimSum > 0 ) {
2023-11-22 12:19:49 -07:00
results . zimsSum = {
2023-11-21 21:15:24 -07:00
name : 'Zims' , results : [
2024-02-12 12:12:07 -07:00
// Navigating to Zims is not relative. We don't want to keep filters or other extra params.
2023-11-22 12:19:49 -07:00
{ title : zimSum . toString ( ) , type : 'zims' , location : ` /search/zim?q= ${ encodeURIComponent ( searchStr ) } ` }
2023-11-21 21:15:24 -07:00
] ,
2024-02-05 16:04:19 -07:00
} ;
} else if ( newSuggestions && zimSum === 0 ) {
results . zimsSum = { name : 'Zims' , results : noResults } ;
2023-11-21 21:15:24 -07:00
}
if ( newSuggestions . channels && newSuggestions . channels . length > 0 ) {
2023-11-22 12:19:49 -07:00
results . channels = {
2023-11-21 21:15:24 -07:00
name : 'Channels' , results : newSuggestions . channels . map ( i => {
2023-11-21 21:20:09 -07:00
return { type : 'channel' , title : i [ 'name' ] , id : i [ 'id' ] , location : ` /videos/channel/ ${ i . id } /video ` }
2023-11-21 21:15:24 -07:00
} )
2023-11-20 11:01:46 -07:00
}
2024-07-22 16:36:57 -06:00
} else if ( newSuggestions . channels && newSuggestions . channels . length === 0 ) {
results . channels = { name : 'Channels' , results : noResults } ;
2023-11-20 11:01:46 -07:00
}
2023-11-21 21:15:24 -07:00
if ( newSuggestions . domains && newSuggestions . domains . length > 0 ) {
2023-11-22 12:19:49 -07:00
results . domains = {
2023-11-21 21:15:24 -07:00
name : 'Domains' , results : newSuggestions . domains . map ( i => {
2023-11-21 21:20:09 -07:00
return {
type : 'domain' ,
title : i . domain ,
id : i . id ,
domain : i . domain ,
2026-03-20 15:38:32 -06:00
location : ` /archives?domain= ${ i . domain } `
2023-11-21 21:20:09 -07:00
}
2023-11-21 21:15:24 -07:00
} )
}
}
2026-03-17 18:35:57 -06:00
if ( newSuggestions . authors && newSuggestions . authors . length > 0 ) {
results . authors = {
name : 'Authors' , results : newSuggestions . authors . map ( i => {
return {
type : 'author' ,
title : i . name ,
id : i . id ,
location : ` /docs?author= ${ encodeURIComponent ( i . name ) } `
}
} )
}
}
if ( newSuggestions . subjects && newSuggestions . subjects . length > 0 ) {
results . subjects = {
name : 'Subjects' , results : newSuggestions . subjects . map ( i => {
return {
type : 'subject' ,
title : i . name ,
id : i . id ,
location : ` /docs?subject= ${ encodeURIComponent ( i . name ) } `
}
} )
}
}
2023-11-21 21:15:24 -07:00
2024-01-01 15:16:03 -07:00
// Match at most 5 Tags.
const matchingTags = searchStr ? fuzzyMatchTagsByName ( searchStr ) . slice ( 0 , 5 ) : null ;
2023-11-22 10:35:01 -07:00
if ( matchingTags && matchingTags . length > 0 ) {
2023-11-22 12:19:49 -07:00
results . tags = {
2023-11-22 10:35:01 -07:00
name : 'Tags' , results : matchingTags . map ( i => {
2023-11-22 12:19:49 -07:00
return { type : 'tag' , title : i . name , location : ` /search?tag= ${ encodeURIComponent ( i . name ) } ` }
2023-11-22 10:35:01 -07:00
} )
}
2023-11-21 21:15:24 -07:00
}
2024-08-15 19:32:01 -06:00
const otherSum = suggestions . otherEstimates ? _ . sum ( Object . values ( suggestions . otherEstimates ) ) : 0 ;
2023-11-22 10:35:01 -07:00
const matchingApps = SUGGESTED _APPS . filter ( i =>
i . title . toLowerCase ( ) . includes ( lowerSearchStr )
2024-05-31 23:52:27 -06:00
|| fuzzyMatch ( i . title . toLowerCase ( ) , lowerSearchStr )
|| i . description . toLowerCase ( ) . includes ( lowerSearchStr )
|| fuzzyMatch ( i . description . toLowerCase ( ) , lowerSearchStr ) ) ;
2023-11-21 21:15:24 -07:00
if ( matchingApps && matchingApps . length > 0 ) {
2024-01-01 15:16:03 -07:00
// Match at most 5 apps.
results . apps = { name : 'Apps' , results : matchingApps . slice ( 0 , 5 ) } ;
2023-11-21 21:15:24 -07:00
}
2026-03-29 18:58:31 -06:00
// Detect coordinates (e.g., "40.76, -111.89" or "40.76 -111.89").
const coordMatch = searchStr && searchStr . trim ( ) . match (
/^(-?\d+\.?\d*)[,\s]+(-?\d+\.?\d*)$/
) ;
2026-04-11 20:09:30 -06:00
// Map places suggestion in dropdown.
if ( newSuggestions . mapPlaces > 0 ) {
results [ 'map places' ] = {
name : 'Map Places' , results : [ {
type : 'search' ,
title : ` Map: ${ newSuggestions . mapPlaces } places ` ,
description : 'Search map locations' ,
location : ` /search/map?q= ${ encodeURIComponent ( searchStr ) } ` ,
} ]
} ;
}
2026-03-29 18:58:31 -06:00
if ( coordMatch ) {
const lat = parseFloat ( coordMatch [ 1 ] ) ;
const lon = parseFloat ( coordMatch [ 2 ] ) ;
if ( lat >= - 90 && lat <= 90 && lon >= - 180 && lon <= 180 ) {
// Derive zoom from decimal precision: 0dp=3, 1dp=7, 2dp=10, 3dp=13, 4dp=16, 5+=18
const maxDecimals = Math . max (
( coordMatch [ 1 ] . split ( '.' ) [ 1 ] || '' ) . length ,
( coordMatch [ 2 ] . split ( '.' ) [ 1 ] || '' ) . length ,
) ;
const zoom = Math . min ( [ 3 , 7 , 10 , 13 , 16 , 18 ] [ maxDecimals ] || 18 , 18 ) ;
results . map = {
name : 'Map' , results : [ {
type : 'map' ,
title : ` ${ lat } , ${ lon } ` ,
description : 'View on map' ,
location : ` /map?lat= ${ lat } &lon= ${ lon } &z= ${ zoom } ` ,
} ]
} ;
}
}
2023-11-22 12:19:49 -07:00
setSuggestionsResults ( results ) ;
setSuggestionsSums ( {
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
fileGroups : fileGroupsEstimate ,
fileGroupsDeep : newSuggestions . fileGroupsDeep ,
2023-11-22 12:19:49 -07:00
zims : zimSum ,
2024-08-15 19:32:01 -06:00
otherSum : otherSum ,
2026-04-11 20:09:30 -06:00
mapPlaces : newSuggestions . mapPlaces || 0 ,
2023-11-22 12:19:49 -07:00
channels : newSuggestions . channels . length ,
domains : newSuggestions . domains . length ,
2026-03-17 18:35:57 -06:00
authors : newSuggestions . authors ? . length ,
subjects : newSuggestions . subjects ? . length ,
2024-01-01 15:16:03 -07:00
tags : matchingTags ? . length ,
apps : matchingApps ? . length ,
2023-11-26 20:49:36 -07:00
} ) ;
2023-11-20 11:01:46 -07:00
}
2025-08-26 17:44:31 -06:00
const suggestionSelectDelay = 250 ;
2023-11-20 11:01:46 -07:00
React . useEffect ( ( ) => {
2023-11-22 12:19:49 -07:00
setSuggestionsSums ( { } ) ;
setSuggestionsResults ( { } ) ;
2023-11-20 11:01:46 -07:00
2023-11-26 20:49:36 -07:00
if ( ! _ . isEmpty ( suggestions ) ) {
2025-08-26 17:44:31 -06:00
// Start a brief window where selecting results is disabled to prevent accidental clicks
// when the dropdown re-renders with new suggestions.
if ( selectDisableTimerRef . current ) {
clearTimeout ( selectDisableTimerRef . current ) ;
}
setSelectDisabled ( true ) ;
selectDisableTimerRef . current = setTimeout ( ( ) => setSelectDisabled ( false ) , suggestionSelectDelay ) ;
2023-11-26 20:49:36 -07:00
normalizeSuggestionsResults ( suggestions ) ;
}
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
} , [ JSON . stringify ( suggestions ) , deep ] ) ;
2023-11-20 11:29:14 -07:00
2025-08-26 17:44:31 -06:00
// Clear timer on unmount.
React . useEffect ( ( ) => {
return ( ) => {
if ( selectDisableTimerRef . current ) {
clearTimeout ( selectDisableTimerRef . current ) ;
}
} ;
} , [ ] ) ;
2023-11-26 20:49:36 -07:00
// User clicked on a result in the dropdown.
2024-02-05 16:04:19 -07:00
const handleResultSelect = ( { result } ) => {
2025-08-26 17:44:31 -06:00
if ( selectDisabled ) {
console . debug ( 'Selection temporarily disabled after suggestions update' ) ;
return ;
}
2024-02-05 16:04:19 -07:00
if ( result . location ) {
2024-02-17 14:52:46 -07:00
console . info ( ` useSearchSuggestions Navigating: ${ result . location } ` )
2024-02-05 16:04:19 -07:00
navigate ( result . location ) ;
} else {
console . error ( 'No location to navigate' ) ;
}
}
2023-11-21 13:01:52 -07:00
const resultRenderer = ( { type , title , description } ) => {
if ( type === 'tag' ) {
2023-11-21 21:15:24 -07:00
return < SingleTag name = { title } / > ;
2023-11-21 13:01:52 -07:00
}
2023-11-21 21:15:24 -07:00
if ( description ) {
2023-11-21 14:46:55 -07:00
return < >
ui: thirty-five files off Semantic, leaving fifteen
Phase 2, part 4. Baseline 50 -> 15.
Migrated: all eight calculators, the rest of inventory and collections, the
Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage,
DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings
and Status, Download, and App.
Also the three shared modules everything imports: api.js and customHooks.js
swap the Semantic toast library for the library's shim (same call shape), and
useForm.js renders the library's Message. useForm accepts both the old
`{content, header, positive|negative}` shape and the new `{kind, title}`,
because the pages building those objects migrate separately.
Two library gaps closed, both found by call sites needing them:
ActionInput is a forwardRef now. Its ref reaches the input rather than the
wrapper, because callers use it to focus or select the field -- a ref to the
surrounding div would do neither, which is why one call site had resorted to
hand-copying the component's markup.
Header takes a `color` token name. Three separate call sites had reached for
an inline style to colour a heading that carries meaning ("3 items to add"
against "2 to remove"), which is how a hex ends up in markup.
Two files needed repair after being interrupted mid-edit: MapViewer had a
Segment left in its error branch, and VideoPlayer had lost the useContext
import its own code still used. Neither jest nor tsc noticed; only
`npm run build` did, which is the whole reason that check exists.
Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser,
FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim,
ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the
unmigrated files still import its Semantic-backed wrappers, so it can only go
once they have.
2026-07-30 12:21:30 -06:00
< Header as = 'h4' > { title } < / H e a d e r >
2023-11-21 14:46:55 -07:00
{ description }
< / >
}
2023-11-26 20:49:36 -07:00
// No specific renderer, use the generic.
2023-11-21 21:15:24 -07:00
return < span > { title } < / s p a n >
2023-11-21 13:01:52 -07:00
} ;
2023-11-20 11:29:14 -07:00
2024-01-01 11:31:17 -07:00
return {
suggestions ,
suggestionsResults ,
suggestionsSums ,
2024-03-06 17:41:04 -07:00
searchStr , setSearchStr ,
2024-01-01 15:16:03 -07:00
setSearchTags ,
2024-04-14 14:01:07 -06:00
months , dateRange , setDates , clearDate ,
2024-01-01 11:31:17 -07:00
handleResultSelect ,
resultRenderer ,
2024-01-01 17:11:09 -07:00
loading ,
2024-01-01 11:31:17 -07:00
}
2023-11-22 12:19:49 -07:00
}
2023-11-20 11:01:46 -07:00
2024-01-01 17:11:09 -07:00
export function SearchView ( { suggestions , suggestionsSums , loading } ) {
2026-03-22 13:30:33 -06:00
const location = useLocation ( ) ;
const search = location . search ;
2024-01-01 15:16:03 -07:00
const filesTabName = < span > Files < Label > { normalizeEstimate ( suggestionsSums ? . fileGroups ) } < / L a b e l > < / s p a n > ;
const zimsTabName = < span > Zims < Label > { normalizeEstimate ( suggestionsSums ? . zims ) } < / L a b e l > < / s p a n > ;
2026-04-11 20:09:30 -06:00
const mapTabName = < span > Map < Label > { normalizeEstimate ( suggestionsSums ? . mapPlaces ) } < / L a b e l > < / s p a n > ;
2024-08-15 19:32:01 -06:00
const othersTabName = < span > Other < Label > { normalizeEstimate ( suggestionsSums ? . otherSum ) } < / L a b e l > < / s p a n > ;
2023-05-29 18:04:03 -06:00
const links = [
2026-03-22 13:30:33 -06:00
{ text : filesTabName , to : ` /search ${ search } ` , key : 'filesSearch_' , end : true } ,
{ text : zimsTabName , to : ` /search/zim ${ search } ` , key : 'zimsSearch' } ,
2026-04-11 20:09:30 -06:00
{ text : mapTabName , to : ` /search/map ${ search } ` , key : 'mapSearch' } ,
2026-03-22 13:30:33 -06:00
{ text : othersTabName , to : ` /search/other ${ search } ` , key : 'othersSearch' } ,
2024-01-01 17:11:09 -07:00
] ;
2023-05-29 18:04:03 -06: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>
2026-06-23 19:49:15 -06:00
// 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 ) ;
2023-05-29 18:04:03 -06:00
return < React . Fragment >
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>
2026-06-23 19:49:15 -06:00
< TabLinks links = { links } right = { isFilesTab ? < SearchViewButton headlines / > : null } / >
2023-05-29 18:04:03 -06:00
< Routes >
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:08 -06:00
< Route path = '/*' element = { < FilesSearchView showView = { false }
deepEstimate = { suggestionsSums ? . fileGroupsDeep } / > } / >
2024-01-01 15:16:03 -07:00
< Route path = '/zim' exact element = { < ZimSearchView suggestions = { suggestions } loading = { loading } / > } / >
2026-04-11 20:09:30 -06:00
< Route path = '/map' exact element = { < MapSearchView / > } / >
2024-08-15 19:32:01 -06:00
< Route path = '/other' exact element = { < OtherSearchView loading = { loading } / > } / >
2023-05-29 18:04:03 -06:00
< / R o u t e s >
< / R e a c t . F r a g m e n t >
}
2024-01-01 11:31:17 -07:00
export function SearchIconButton ( ) {
ui: the corner triggers, the More button, and the search modal
Four faults, each measured in the running app before and after.
The search glyph carried Semantic's `item` class, which has had no rules at all
since Semantic was removed: no pointer cursor, no hover, and an 18.6px hit area
against a 54.4px bar. It is the same kind of thing as Help and Admin and now
takes the same class. The share glyph did compute `cursor: pointer` -- it is an
`<a href>` -- but its target was the 24px box the icon occupies, so the pointer
appeared only if you found it exactly. Corner anchors now stretch and pad like
a link.
The More button rendered with NO class at all: a bare grey UA button while the
rest of the bar took the user's colour. Menu.Target clones its child and hands
it a className of its own, and the trigger spread those cloned props after its
own `className` attribute, so the clone replaced ours. It is merged now. This
was mine, from extracting NavDropdownTrigger last commit -- and it also made the
previous commit's "measured and drawn are the same component" claim only half
true, because the hidden placeholder is not cloned and so kept its class.
The bar still wrapped, for a reason the padding fix could not reach: the corner
does not keep a constant width. The Cmd-K hint renders only once
useKeyboardDetected has seen a keypress, so it appears at an arbitrary later
moment and widens the corner by ~30px; the load, memory, temperature and drive
warnings come and go with the machine's state. None of that resizes the
container, which was the only thing observed -- so nothing recalculated. That
is the other half of "at certain widths": the same viewport wrapped or not
depending on whether the user had touched a key. The corner and the home link
are observed too now.
The search modal showed an input and the first line of the first group. The
suggestion list is absolutely positioned, which is right everywhere else -- a
list that reflows the page while you type is unusable -- but in a modal it fails
twice: an absolute list adds no height, so the panel sized itself to the search
box alone at 114px, and the panel's own `overflow: auto` then clipped the list
at that edge. In the modal the list sits in flow; the inverse is asserted so
the rule cannot leak to the pages where overlaying is correct.
Two of the new tests were wrong first and reverting the fix left them green:
they asserted hand-written markup carrying the classes I expected the components
to have, rather than the real SearchIconButton and ShareButton. They use the
real components now. Every fix was then planted against: 5 red for the More
class, 2 for the search class, 2 for the corner hit area, 1 for the observer,
2 for the modal.
1115 jest / 62 suites, 196 in ui-layout.cy.js, clean npm run build.
Confirmed live: share and search both pointer at 54.4px, More white on the bar
colour with a transparent face, and the modal's list in flow and unclipped.
2026-08-02 14:45:49 -06:00
/ *
* A single button which opens the search modal via keyboard shortcuts context .
*
ui: the last of Semantic UI, in name and in dead CSS
The library has been uninstalled for some time, but the codebase still read as
though it were there: identifiers named after it, rules written against its
class names, and about 180 comments explaining a rule in terms of a thing that
is no longer installed.
Renamed, keeping the vocabulary the app actually uses: semanticUIColorMap ->
navColorHexMap, semanticColorNames -> paletteColorNames, semanticNames ->
iconNameAliases, semanticSizes -> sizeAliases, and SemanticUIColors ->
NavColors in wrolpi/schema.py, which is what validates nav_color on the way in.
ColorToSemanticHexColor had no callers at all and is gone.
The baseline ratchet has done its job -- the list reached zero, so the JSON is
deleted and the guard keeps only the checks with teeth: no package, no import,
no `className="ui "`, no toast library. Its Theme.tsx check went with the
wrappers it was watching for. It also asserts its own walk found files, so a
clean result cannot mean it scanned nothing.
Deleted from App.css: every rule requiring `.ui`, `.inverted`, `.segment` or
`.modal`, none of which any component has set since the stylesheet stopped
loading, plus the form skeletons, ui-alerts and progress-text rules that
nothing renders. Two of them were doing harm rather than nothing: the drag
tints had a dark-theme variant under `.inverted` that stopped applying, leaving
15% of a fixed blue over a dark row, and now mix a token instead.
Two selectors naming markup that no longer exists were live bugs. The file
browser's filter had `.ui.input.file-browser-filter`, so it rendered at the
browser default width on a bar laid out to give it 34em -- now qualified by its
container, because `.wrolpi-action-input` sets `width: 100%` at the same
specificity and is loaded later. And SearchBox takes named props rather than a
rest spread, so the `id` DestinationForm hands it was dropped in silence and
the label it renders pointed at nothing; the download forms' Destination field
has had no associated label since the migration.
"Semantic roles" survives in four places, where it means what it says.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:15:57 -06:00
* ` wrolpi-navbar-link ` , not the leftover ` item ` class , which carries no rules at all
* now that the stylesheet defining it is gone : no pointer cursor , no hover highlight , and a hit area of
ui: the corner triggers, the More button, and the search modal
Four faults, each measured in the running app before and after.
The search glyph carried Semantic's `item` class, which has had no rules at all
since Semantic was removed: no pointer cursor, no hover, and an 18.6px hit area
against a 54.4px bar. It is the same kind of thing as Help and Admin and now
takes the same class. The share glyph did compute `cursor: pointer` -- it is an
`<a href>` -- but its target was the 24px box the icon occupies, so the pointer
appeared only if you found it exactly. Corner anchors now stretch and pad like
a link.
The More button rendered with NO class at all: a bare grey UA button while the
rest of the bar took the user's colour. Menu.Target clones its child and hands
it a className of its own, and the trigger spread those cloned props after its
own `className` attribute, so the clone replaced ours. It is merged now. This
was mine, from extracting NavDropdownTrigger last commit -- and it also made the
previous commit's "measured and drawn are the same component" claim only half
true, because the hidden placeholder is not cloned and so kept its class.
The bar still wrapped, for a reason the padding fix could not reach: the corner
does not keep a constant width. The Cmd-K hint renders only once
useKeyboardDetected has seen a keypress, so it appears at an arbitrary later
moment and widens the corner by ~30px; the load, memory, temperature and drive
warnings come and go with the machine's state. None of that resizes the
container, which was the only thing observed -- so nothing recalculated. That
is the other half of "at certain widths": the same viewport wrapped or not
depending on whether the user had touched a key. The corner and the home link
are observed too now.
The search modal showed an input and the first line of the first group. The
suggestion list is absolutely positioned, which is right everywhere else -- a
list that reflows the page while you type is unusable -- but in a modal it fails
twice: an absolute list adds no height, so the panel sized itself to the search
box alone at 114px, and the panel's own `overflow: auto` then clipped the list
at that edge. In the modal the list sits in flow; the inverse is asserted so
the rule cannot leak to the pages where overlaying is correct.
Two of the new tests were wrong first and reverting the fix left them green:
they asserted hand-written markup carrying the classes I expected the components
to have, rather than the real SearchIconButton and ShareButton. They use the
real components now. Every fix was then planted against: 5 red for the More
class, 2 for the search class, 2 for the corner hit area, 1 for the observer,
2 for the modal.
1115 jest / 62 suites, 196 in ui-layout.cy.js, clean npm run build.
Confirmed live: share and search both pointer at 54.4px, More white on the bar
colour with a transparent face, and the modal's list in flow and unclipped.
2026-08-02 14:45:49 -06:00
* whatever the 18 px glyph occupied rather than the height of the bar . It is the same
* kind of thing as Help and Admin and now has the same affordance .
* /
2025-12-26 18:59:13 -07:00
const { openSearchModal } = React . useContext ( KeyboardShortcutsContext ) ;
2025-01-18 10:08:11 -07:00
2025-12-26 18:59:13 -07:00
return (
ui: the corner triggers, the More button, and the search modal
Four faults, each measured in the running app before and after.
The search glyph carried Semantic's `item` class, which has had no rules at all
since Semantic was removed: no pointer cursor, no hover, and an 18.6px hit area
against a 54.4px bar. It is the same kind of thing as Help and Admin and now
takes the same class. The share glyph did compute `cursor: pointer` -- it is an
`<a href>` -- but its target was the 24px box the icon occupies, so the pointer
appeared only if you found it exactly. Corner anchors now stretch and pad like
a link.
The More button rendered with NO class at all: a bare grey UA button while the
rest of the bar took the user's colour. Menu.Target clones its child and hands
it a className of its own, and the trigger spread those cloned props after its
own `className` attribute, so the clone replaced ours. It is merged now. This
was mine, from extracting NavDropdownTrigger last commit -- and it also made the
previous commit's "measured and drawn are the same component" claim only half
true, because the hidden placeholder is not cloned and so kept its class.
The bar still wrapped, for a reason the padding fix could not reach: the corner
does not keep a constant width. The Cmd-K hint renders only once
useKeyboardDetected has seen a keypress, so it appears at an arbitrary later
moment and widens the corner by ~30px; the load, memory, temperature and drive
warnings come and go with the machine's state. None of that resizes the
container, which was the only thing observed -- so nothing recalculated. That
is the other half of "at certain widths": the same viewport wrapped or not
depending on whether the user had touched a key. The corner and the home link
are observed too now.
The search modal showed an input and the first line of the first group. The
suggestion list is absolutely positioned, which is right everywhere else -- a
list that reflows the page while you type is unusable -- but in a modal it fails
twice: an absolute list adds no height, so the panel sized itself to the search
box alone at 114px, and the panel's own `overflow: auto` then clipped the list
at that edge. In the modal the list sits in flow; the inverse is asserted so
the rule cannot leak to the pages where overlaying is correct.
Two of the new tests were wrong first and reverting the fix left them green:
they asserted hand-written markup carrying the classes I expected the components
to have, rather than the real SearchIconButton and ShareButton. They use the
real components now. Every fix was then planted against: 5 red for the More
class, 2 for the search class, 2 for the corner hit area, 1 for the observer,
2 for the modal.
1115 jest / 62 suites, 196 in ui-layout.cy.js, clean npm run build.
Confirmed live: share and search both pointer at 54.4px, More white on the bar
colour with a transparent face, and the modal's list in flow and unclipped.
2026-08-02 14:45:49 -06:00
< a className = 'wrolpi-navbar-link' onClick = { openSearchModal }
ui: fix the cypress failures, and two real defects behind them
All 43 cypress specs pass now (was 36 of 43). Five of the seven failures were
stale selectors, two were real.
Real, and both mine:
The search modal stopped focusing its input. Mantine mounts modal content on
a later tick, so the effect that focused it ran while the ref was still null;
Semantic's modal mounted synchronously, which is why focusing there had been
enough. Fixed declaratively -- the field asks for focus itself, and SearchBox
also sets `data-autofocus`, which is what Mantine's focus trap honours when it
takes focus away from a plain autoFocus.
Escape closed the suggestion list and the enclosing modal at once, so a user
correcting a mistyped query lost the whole modal. Escape now dismisses the
suggestions and stops there; a second Escape, with the list closed, still
closes the modal.
Stale selectors, all in the specs: Semantic's icon font (`i.search.icon`) is
SVG now, its `.ui.modal`/`.ui.dimmer` are the library's modal and overlay, and
one test asserted a button carried the classes `ui button secondary`. That last
one is rewritten to check what a user can actually perceive -- that Edit is a
real link pointing at the edit route -- because framework class names say
nothing and break on every library change.
Two accessibility gaps the tests walked into:
The nav's search control was an anchor with an onClick, no name and no
keyboard path. It now has a name, a role and Enter/Space activation, which is
also what let the spec stop matching on icon-font classes.
The search dialog had no accessible name at all; it has one now.
One `{force: true}` remains, with its reason recorded: Cypress judges that
field covered because it sits in a fixed modal under a sticky header, and I
verified by hand at Cypress's own viewport that it is the topmost element at
its centre, unobscured, and focused on open.
Also closed a hole in the ratchet that this uncovered. A file could drop its
`semantic-ui-react` imports, leave the baseline, and still render Semantic
through Theme.tsx's wrappers -- which is exactly how a Semantic modal survived
in the search shortcut long after that file looked migrated. A test now
forbids it, and the three files doing it (Calculators, KeyboardShortcutsProvider,
and Common.js reaching for theme names) import from the library or from
themes/names instead.
2026-07-30 12:53:19 -06:00
role = 'button' tabIndex = { 0 } aria - label = 'Search'
onKeyDown = { e => ( e . key === 'Enter' || e . key === ' ' ) && openSearchModal ( ) } >
2024-12-19 13:17:25 -07:00
< Icon name = 'search' / >
2026-02-23 15:59:40 -07:00
< ShortcutHint shortcutKey = "K" / >
2024-12-19 13:17:25 -07:00
< / a >
2025-12-26 18:59:13 -07:00
) ;
2024-01-01 11:31:17 -07:00
}
2024-08-15 19:32:01 -06:00
function SearchChannelPreview ( { channel } ) {
ui: thirty-five files off Semantic, leaving fifteen
Phase 2, part 4. Baseline 50 -> 15.
Migrated: all eight calculators, the rest of inventory and collections, the
Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage,
DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings
and Status, Download, and App.
Also the three shared modules everything imports: api.js and customHooks.js
swap the Semantic toast library for the library's shim (same call shape), and
useForm.js renders the library's Message. useForm accepts both the old
`{content, header, positive|negative}` shape and the new `{kind, title}`,
because the pages building those objects migrate separately.
Two library gaps closed, both found by call sites needing them:
ActionInput is a forwardRef now. Its ref reaches the input rather than the
wrapper, because callers use it to focus or select the field -- a ref to the
surrounding div would do neither, which is why one call site had resorted to
hand-copying the component's markup.
Header takes a `color` token name. Three separate call sites had reached for
an inline style to colour a heading that carries meaning ("3 items to add"
against "2 to remove"), which is how a hex ends up in markup.
Two files needed repair after being interrupted mid-edit: MapViewer had a
Segment left in its error branch, and VideoPlayer had lost the useContext
import its own code still used. Neither jest nor tsc noticed; only
`npm run build` did, which is the whole reason that check exists.
Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser,
FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim,
ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the
unmigrated files still import its Semantic-backed wrappers, so it can only go
once they have.
2026-07-30 12:21:30 -06:00
return < div >
< Link to = { ` /videos/channel/ ${ channel . id } /video ` } >
{ channel . name }
< / L i n k >
< / d i v >
2024-08-15 19:32:01 -06:00
}
function OtherSearchView ( { loading } ) {
const { searchParams } = React . useContext ( QueryContext ) ;
ui: thirty-five files off Semantic, leaving fifteen
Phase 2, part 4. Baseline 50 -> 15.
Migrated: all eight calculators, the rest of inventory and collections, the
Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage,
DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings
and Status, Download, and App.
Also the three shared modules everything imports: api.js and customHooks.js
swap the Semantic toast library for the library's shim (same call shape), and
useForm.js renders the library's Message. useForm accepts both the old
`{content, header, positive|negative}` shape and the new `{kind, title}`,
because the pages building those objects migrate separately.
Two library gaps closed, both found by call sites needing them:
ActionInput is a forwardRef now. Its ref reaches the input rather than the
wrapper, because callers use it to focus or select the field -- a ref to the
surrounding div would do neither, which is why one call site had resorted to
hand-copying the component's markup.
Header takes a `color` token name. Three separate call sites had reached for
an inline style to colour a heading that carries meaning ("3 items to add"
against "2 to remove"), which is how a hex ends up in markup.
Two files needed repair after being interrupted mid-edit: MapViewer had a
Segment left in its error branch, and VideoPlayer had lost the useContext
import its own code still used. Neither jest nor tsc noticed; only
`npm run build` did, which is the whole reason that check exists.
Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser,
FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim,
ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the
unmigrated files still import its Semantic-backed wrappers, so it can only go
once they have.
2026-07-30 12:21:30 -06:00
const [ activeValue , setActiveValue ] = React . useState ( 'channels' ) ;
2024-08-15 19:32:01 -06:00
const activeTags = searchParams . getAll ( 'tag' ) ;
const { channels , loading : channelsLoading } = useSearchChannels ( activeTags ) ;
if ( loading || channelsLoading ) {
ui: thirty-five files off Semantic, leaving fifteen
Phase 2, part 4. Baseline 50 -> 15.
Migrated: all eight calculators, the rest of inventory and collections, the
Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage,
DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings
and Status, Download, and App.
Also the three shared modules everything imports: api.js and customHooks.js
swap the Semantic toast library for the library's shim (same call shape), and
useForm.js renders the library's Message. useForm accepts both the old
`{content, header, positive|negative}` shape and the new `{kind, title}`,
because the pages building those objects migrate separately.
Two library gaps closed, both found by call sites needing them:
ActionInput is a forwardRef now. Its ref reaches the input rather than the
wrapper, because callers use it to focus or select the field -- a ref to the
surrounding div would do neither, which is why one call site had resorted to
hand-copying the component's markup.
Header takes a `color` token name. Three separate call sites had reached for
an inline style to colour a heading that carries meaning ("3 items to add"
against "2 to remove"), which is how a hex ends up in markup.
Two files needed repair after being interrupted mid-edit: MapViewer had a
Segment left in its error branch, and VideoPlayer had lost the useContext
import its own code still used. Neither jest nor tsc noticed; only
`npm run build` did, which is the whole reason that check exists.
Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser,
FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim,
ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the
unmigrated files still import its Semantic-backed wrappers, so it can only go
once they have.
2026-07-30 12:21:30 -06:00
return < Panel > < Loading / > < / P a n e l >
2024-08-15 19:32:01 -06:00
}
ui: thirty-five files off Semantic, leaving fifteen
Phase 2, part 4. Baseline 50 -> 15.
Migrated: all eight calculators, the rest of inventory and collections, the
Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage,
DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings
and Status, Download, and App.
Also the three shared modules everything imports: api.js and customHooks.js
swap the Semantic toast library for the library's shim (same call shape), and
useForm.js renders the library's Message. useForm accepts both the old
`{content, header, positive|negative}` shape and the new `{kind, title}`,
because the pages building those objects migrate separately.
Two library gaps closed, both found by call sites needing them:
ActionInput is a forwardRef now. Its ref reaches the input rather than the
wrapper, because callers use it to focus or select the field -- a ref to the
surrounding div would do neither, which is why one call site had resorted to
hand-copying the component's markup.
Header takes a `color` token name. Three separate call sites had reached for
an inline style to colour a heading that carries meaning ("3 items to add"
against "2 to remove"), which is how a hex ends up in markup.
Two files needed repair after being interrupted mid-edit: MapViewer had a
Segment left in its error branch, and VideoPlayer had lost the useContext
import its own code still used. Neither jest nor tsc noticed; only
`npm run build` did, which is the whole reason that check exists.
Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser,
FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim,
ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the
unmigrated files still import its Semantic-backed wrappers, so it can only go
once they have.
2026-07-30 12:21:30 -06:00
return < Accordion value = { activeValue } onChange = { setActiveValue } >
< Accordion . Item value = 'channels' >
< Accordion . Control >
< span style = { { display : 'inline-flex' , alignItems : 'center' , gap : 8 } } >
< Header as = 'h3' style = { { margin : 0 } } > Channels < / H e a d e r >
< Label > { normalizeEstimate ( channels . length ) } < / L a b e l >
< / s p a n >
< / A c c o r d i o n . C o n t r o l >
< Accordion . Panel >
< Stack gap = 'xs' >
{ ! _ . isEmpty ( channels ) ?
channels . map ( i => < SearchChannelPreview key = { i . id } channel = { i } / > )
: 'No Channels' }
< / S t a c k >
< / A c c o r d i o n . P a n e l >
< / A c c o r d i o n . I t e m >
2024-08-15 19:32:01 -06:00
< / A c c o r d i o n >
ui: thirty-five files off Semantic, leaving fifteen
Phase 2, part 4. Baseline 50 -> 15.
Migrated: all eight calculators, the rest of inventory and collections, the
Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage,
DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings
and Status, Download, and App.
Also the three shared modules everything imports: api.js and customHooks.js
swap the Semantic toast library for the library's shim (same call shape), and
useForm.js renders the library's Message. useForm accepts both the old
`{content, header, positive|negative}` shape and the new `{kind, title}`,
because the pages building those objects migrate separately.
Two library gaps closed, both found by call sites needing them:
ActionInput is a forwardRef now. Its ref reaches the input rather than the
wrapper, because callers use it to focus or select the field -- a ref to the
surrounding div would do neither, which is why one call site had resorted to
hand-copying the component's markup.
Header takes a `color` token name. Three separate call sites had reached for
an inline style to colour a heading that carries meaning ("3 items to add"
against "2 to remove"), which is how a hex ends up in markup.
Two files needed repair after being interrupted mid-edit: MapViewer had a
Segment left in its error branch, and VideoPlayer had lost the useContext
import its own code still used. Neither jest nor tsc noticed; only
`npm run build` did, which is the whole reason that check exists.
Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser,
FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim,
ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the
unmigrated files still import its Semantic-backed wrappers, so it can only go
once they have.
2026-07-30 12:21:30 -06:00
}