import { TextField, InputAdornment, IconButton } from '@mui/material'
import { ClearOutlined, SearchOutlined } from '@mui/icons-material'
import { useEffect, useRef, useCallback } from 'react'
import { widgetStoreActions } from '../../stores/widget-store'
import { useWidgetSelector } from '../../stores/use-widget-selector'
import type { SearcherProps, SearcherFilterFn, SearcherState } from './types'
import type { EchartWidgetData } from '../../echart/types'
import { LOCK_SELECTION_TOOL_ID } from '../lock-selection/lock-selection'
export const SEARCHER_TOOL_ID = 'searcher'
const DEBOUNCE_DELAY = 300
/**
* Search input component that works with SearcherToggle.
*
* Registers a transformation tool in the widget pipeline when mounted.
* Reads the enabled state from the widget store using the provided id.
* Uses a debounced search to filter data via the transformation pipeline.
* Auto-focuses when enabled becomes true.
*
* @example
* ```tsx
*
*
* ```
*/
export function Searcher({
id,
filterFn,
order = 20,
labels,
TextFieldProps,
ClearIcon,
debounceDelay = DEBOUNCE_DELAY,
}: SearcherProps) {
const inputRef = useRef(null)
const debounceTimeoutRef = useRef | null>(null)
// Read enabled state and search text from widget store
const { enabled, searchText } = useWidgetSelector(id, (w) => ({
enabled: (w as SearcherState | undefined)?.isSearchEnabled ?? false,
searchText: (w as SearcherState | undefined)?.searchText ?? '',
}))
const prevEnabledRef = useRef(enabled)
const filter = filterFn ?? defaultFilterFn
const setSearchText = useCallback(
(text: string) => {
widgetStoreActions.setWidget(id, { searchText: text })
},
[id],
)
// Register tool once — fn reads searchText from the store at execution time.
// Enabled is synced separately to avoid full re-registration on toggle.
useEffect(() => {
widgetStoreActions.registerTool(id, {
id: SEARCHER_TOOL_ID,
order,
enabled: false,
fn: async (data) => {
const widget = widgetStoreActions.getWidget(id)
const currentSearchText = widget?.searchText ?? ''
// Execute filter (can be sync or async)
const result = filter(data as EchartWidgetData, currentSearchText)
// Return result directly (pipeline will handle Promise)
return result
},
disables: [LOCK_SELECTION_TOOL_ID],
})
return () => widgetStoreActions.unregisterTool(id, SEARCHER_TOOL_ID)
}, [id, order, filter])
// Sync enabled from store — lightweight, no re-registration
useEffect(() => {
widgetStoreActions.setToolEnabled(id, SEARCHER_TOOL_ID, enabled)
}, [id, enabled])
// Trigger pipeline re-execution when search text changes (debounced).
// The fn reads searchText from the store, so we just need to trigger the pipeline.
const debouncedTriggerPipeline = useCallback(() => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current)
}
debounceTimeoutRef.current = setTimeout(() => {
widgetStoreActions.triggerToolPipeline(id)
}, debounceDelay)
}, [id, debounceDelay])
// Auto-focus when enabled becomes true
useEffect(() => {
// Transition from disabled to enabled - focus input
if (enabled && !prevEnabledRef.current && inputRef.current) {
inputRef.current.focus()
}
prevEnabledRef.current = enabled
}, [enabled])
// Cleanup debounce timeout on unmount
useEffect(() => {
return () => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current)
}
}
}, [])
const handleChange = useCallback(
(event: React.ChangeEvent) => {
const newValue = event.target.value
setSearchText(newValue)
debouncedTriggerPipeline()
},
[debouncedTriggerPipeline, setSearchText],
)
const handleClear = useCallback(() => {
setSearchText('')
widgetStoreActions.triggerToolPipeline(id)
if (inputRef.current) {
inputRef.current.focus()
}
}, [id, setSearchText])
if (!enabled) {
return null
}
const placeholder = labels?.placeholder ?? 'Search...'
const clearAriaLabel = labels?.clearAriaLabel ?? 'Clear search'
return (
),
endAdornment: searchText ? (
{ClearIcon ?? }
) : null,
}}
{...TextFieldProps}
/>
)
}
/**
* Default filter function that searches all string fields case-insensitively.
* Note: Should be synchronous for the new pipeline architecture.
*/
const defaultFilterFn: SearcherFilterFn = (
data: EchartWidgetData,
searchText: string,
) => {
if (!searchText.trim()) return Promise.resolve(data)
const lowerSearch = searchText.toLowerCase()
return Promise.resolve(
data.map((series) =>
series.filter((item) =>
Object.values(item).some((value) =>
String(value).toLowerCase().includes(lowerSearch),
),
),
),
)
}