ChemPal Documentation - v1.7.0
    Preparing search index...

    Variable DrawerSearchPanelConst

    DrawerSearchPanel: FC<
        {
            expandedAccordion: string
            | false;
            onAccordionChange: (
                panel: string,
            ) => (event: SyntheticEvent, isExpanded: boolean) => void;
        },
    > = ...

    Pre-search filter drawer. Renders the product-name field, then walks TableColumns() in column-config order and emits one accordion section per column with a meta.drawer payload (via ColumnDrawerSection). The non-column "Results Limit" section is injected just before the price entry to preserve its historical position.

    Adding, removing, or reordering a drawer-backed column in TableColumns.tsx is reflected here automatically — no edits to this file are required.

    Component props.

    • expandedAccordion - Currently open accordion's panel id (e.g. "search-country"), or false.
    • onAccordionChange - Factory from panelId to MUI Accordion's onChange handler.

    The search drawer body (title input, filter accordions, submit).

    const [expanded, setExpanded] = useState<string | false>(false);
    const handleChange = (panel: string) => (_e: SyntheticEvent, isOpen: boolean) =>
    setExpanded(isOpen ? panel : false);

    <DrawerSearchPanel expandedAccordion={expanded} onAccordionChange={handleChange} />
    // Renders (with current TableColumns config):
    // [Product name or keyword]
    // ▸ Supplier (0 selected)
    // ▸ Country (0 selected)
    // ▸ Shipping Type
    // ▸ Availability
    // ▸ Results Limit (15 per supplier) ← non-column, injected before Price
    // ▸ Price Range
    // [Search]
    const DrawerSearchPanel: FC<{
    expandedAccordion: string | false;
    onAccordionChange: (panel: string) => (event: SyntheticEvent, isExpanded: boolean) => void;
    }> = ({ expandedAccordion, onAccordionChange }) => {
    const {
    userSettings,
    setUserSettings,
    setDrawerTab,
    setPendingSearchQuery,
    searchFilters,
    setSearchFilters,
    selectedSuppliers,
    setSelectedSuppliers,
    setPanel,
    } = useAppContext();

    // How many advanced filters are set to a non-default value; gates the
    // "Clear all filters" button below.
    const activeFilterCount = countActiveSearchFilters({
    selectedSuppliers,
    searchFilters,
    userSettings,
    });

    // Re-render (and rebuild the memoized drawer configs below) when the UI
    // language changes, so the translated column/drawer labels update live.
    const locale = useLocale();

    // Collect columns that declared a drawer section in their meta, preserving
    // the order they appear in TableColumns(). The drawer configs carry i18n'd
    // labels, so recompute them when the locale changes.
    const drawerColumns = useMemo(
    () =>
    TableColumns().flatMap((column) => {
    const drawer = column.meta?.drawer;
    return drawer && column.id ? [{ id: column.id, drawer }] : [];
    }),
    [locale],
    );

    // Hydrate the title query field from the shared live search input value so
    // the drawer shows whatever the user last typed in any search input, even
    // if it hasn't been submitted yet.
    useEffect(() => {
    const loadSearchInput = async () => {
    try {
    const data = await cstorage.session.get([CACHE.SEARCH_INPUT]);
    const stored = data[CACHE.SEARCH_INPUT];
    if (typeof stored === 'string' && stored !== searchFilters.titleQuery) {
    setSearchFilters({ ...searchFilters, titleQuery: stored });
    }
    } catch (error) {
    console.warn('Failed to load search input from session storage:', { error });
    }
    };
    loadSearchInput();
    // Run once on mount; we intentionally don't depend on searchFilters to avoid loops.
    }, []);

    const handleTitleQueryChange = async (value: string) => {
    setSearchFilters({ ...searchFilters, titleQuery: value });
    try {
    await cstorage.session.set({ [CACHE.SEARCH_INPUT]: value });
    } catch (error) {
    console.warn('Failed to persist search input to session storage:', { error });
    }
    };

    const handleSearch = async (): Promise<void> => {
    const query = searchFilters.titleQuery.trim();
    if (!query) return;
    // Clear the live in-progress draft in session storage now that the query
    // has been promoted to a real search. We intentionally do NOT write
    // CACHE.QUERY or CACHE.SEARCH_IS_NEW_SEARCH here: those are consumed by
    // useSearch's mount effect, which would fire a duplicate performSearch in
    // parallel with the pendingSearchQuery path below. HistoryPanel.handleReSearch
    // follows the same single-path pattern.
    try {
    await cstorage.session.set({ [CACHE.SEARCH_INPUT]: '' });
    } catch (error) {
    console.warn('Failed to clear search input draft in session storage:', { error });
    }
    // Clear the drawer's visible field to match the cleared draft so re-opening
    // the drawer doesn't show stale text from the just-submitted query.
    setSearchFilters({ ...searchFilters, titleQuery: '' });
    // Stage the query for ResultsTable's pendingSearchQuery effect to consume.
    // This works whether SearchPanel is already mounted (effect re-fires on the
    // context update) or is about to mount (fresh mount reads pendingSearchQuery
    // from context on first render).
    setPendingSearchQuery(query);
    setDrawerTab(DRAWER_INDEX.CLOSED);
    // Switch to the results panel so SearchPanel / ResultsTable mount if the
    // user triggered the search from the home panel. When already on RESULTS,
    // this is a harmless no-op.
    setPanel?.(PANEL.RESULTS);
    };

    const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'Enter') {
    void handleSearch();
    }
    };

    // Reset every advanced filter to its default, matching the set counted by
    // countActiveSearchFilters. The title query is the search term, not a filter,
    // so it's left untouched.
    const handleClearFilters = () => {
    setSelectedSuppliers([]);
    setSearchFilters({ ...searchFilters, availability: [], country: [], shippingType: [] });
    setUserSettings({ ...userSettings, priceMin: undefined, priceMax: undefined });
    };

    const renderResultLimit = () => (
    <Accordion
    key="per-supplier-limit"
    expanded={expandedAccordion === 'per-supplier-limit'}
    onChange={onAccordionChange('per-supplier-limit')}
    >
    <StyledAccordionSummary expandIcon={<ExpandMoreIcon />}>
    <Typography>
    {i18n('drawer_results_limit')}
    {userSettings.suppliers?.resultLimit != null && (
    <span className={styles['accordion-hint']}>
    {' '}
    {i18n('drawer_results_limit_hint', [String(userSettings.suppliers.resultLimit)])}
    </span>
    )}
    </Typography>
    </StyledAccordionSummary>
    <StyledAccordionDetails>
    <TextField
    style={{ width: '100%' }}
    label={i18n('drawer_results_limit_label')}
    value={userSettings.suppliers?.resultLimit}
    onChange={(e) =>
    setUserSettings({
    ...userSettings,
    suppliers: {
    ...userSettings.suppliers,
    resultLimit: Number(e.target.value) || undefined,
    },
    })
    }
    />
    </StyledAccordionDetails>
    </Accordion>
    );

    return (
    <Box>
    {/* Title / Search Query */}
    <Box sx={{ p: '12px 16px' }}>
    <TextField
    autoFocus
    fullWidth
    label={i18n('drawer_product_name_label')}
    size="small"
    value={searchFilters.titleQuery}
    onChange={(e) => handleTitleQueryChange(e.target.value)}
    onKeyDown={handleKeyDown}
    />
    </Box>

    {drawerColumns.flatMap(({ id, drawer }) => {
    // "Results Limit" is a user setting (not a filter on a column), so it
    // has no column to anchor it to. Inject it right before the price
    // section to preserve its historical position in the drawer UX.
    const resultLimit = id === 'price' ? [renderResultLimit()] : [];
    return [
    ...resultLimit,
    <ColumnDrawerSection
    key={id}
    columnId={id}
    config={drawer}
    expandedAccordion={expandedAccordion}
    onAccordionChange={onAccordionChange}
    />,
    ];
    })}

    <Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
    <Button
    variant="text"
    fullWidth
    startIcon={<ClearFiltersIcon />}
    onClick={handleClearFilters}
    disabled={activeFilterCount === 0}
    >
    {i18n('drawer_clear_filters')}
    </Button>
    <Button
    variant="contained"
    fullWidth
    startIcon={<SearchIcon />}
    onClick={() => void handleSearch()}
    disabled={!searchFilters.titleQuery.trim()}
    >
    {i18n('drawer_search_button')}
    </Button>
    </Box>
    </Box>
    );
    };