All files / src/components SpeedDialMenu.tsx

100% Statements 127/127
87.5% Branches 14/16
88.88% Functions 8/9
100% Lines 127/127

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 1941x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x   1x                           1x 1x 14x 14x 14x   14x               14x 14x 2x 2x 2x 1x 1x 1x 1x 1x   2x 2x   2x 2x 2x 2x   14x           14x 14x 2x   2x 2x 1x 1x 1x 1x 2x   14x         14x 14x 1x 1x 1x   14x     14x 14x   14x     14x 14x   14x 14x 14x   14x       14x 14x 1x 1x 1x   14x       14x 14x 1x 1x 1x   14x         14x 14x 14x 14x 14x 14x 2x 2x 2x 2x 2x 2x 2x 2x 14x 14x 14x 14x   14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x       14x 14x 14x 14x 8x 8x 8x 8x 14x 14x 14x           14x  
import { PANEL } from '@/constants/common';
import { useAppContext } from '@/context';
import { i18n } from '@/helpers/i18n';
import AutoDeleteIcon from '@/icons/AutoDeleteIcon';
import ClearIcon from '@/icons/ClearIcon';
import ContrastIcon from '@/icons/ContrastIcon';
import InfoOutlineIcon from '@/icons/InfoOutlineIcon';
import { showReportDialog } from '@/components/ReportDialog';
import { SupplierCache } from '@/utils/SupplierCache';
import { clearSearchResults } from '@/utils/idbCache';
import BarChartIcon from '@mui/icons-material/BarChart';
import BugReportIcon from '@mui/icons-material/BugReport';
import PriceChangeIcon from '@mui/icons-material/PriceChange';
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
import { MouseEvent, useState } from 'react';
import { useTheme } from '../themes';
import AboutModal from './AboutModal';
import HelpTooltip from './HelpTooltip';
import { Logger } from '@/utils/Logger';
import { getErrorMessage } from '@/helpers/exceptions';
 
const logger = new Logger('SpeedDialMenu');
 
/**
 * SpeedDialMenu component that provides quick access to various application actions.
 * Displays a floating action button that expands to show multiple action buttons when clicked.
 * Includes actions for clearing results, clearing cache, toggling theme, stats, and about.
 *
 * @component
 * @category Components
 * @param props - Component props
 *
 * @example
 * ```tsx
 * <SpeedDialMenu speedDialVisibility={true} />
 * ```
 * @source
 */
export default function SpeedDialMenu({ speedDialVisibility }: SpeedDialMenuProps) {
  const appContext = useAppContext();
  const { toggleTheme } = useTheme();
  const [aboutOpen, setAboutOpen] = useState(false);
 
  /**
   * Handles clearing all search results. Clears the persisted results, empties
   * the in-memory results, and also resets the search query text so the search
   * box the user returns to isn't left showing the previous query. Clearing the
   * results bounces the app back to the home search panel, whose input is
   * controlled by `searchFilters.titleQuery`, so resetting that empties the box.
   * @param event - The click event
   * @source
   */
  const handleClearResults = async (event: MouseEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    try {
      await clearSearchResults();
    } catch (error) {
      logger.warn(`Failed to clear search results from IndexedDB: ${getErrorMessage(error)}`, {
        error,
      });
    }
 
    logger.debug('[handleClearResults] Setting userSettings', { ...appContext.userSettings });
    appContext.setUserSettings({ ...appContext.userSettings });
 
    appContext.setSearchResults([]);
    // Reset the query text so the home search box doesn't keep the old query.
    appContext.setSearchFilters({ ...appContext.searchFilters, titleQuery: '' });
  };
 
  /**
   * Handles clearing the browser cache.
   * Deletes all cache entries for the application.
   *
   * @param event - The click event
   * @source
   */
  const handleClearCache = async (event: MouseEvent<HTMLAnchorElement>) => {
    event.preventDefault();
 
    try {
      await SupplierCache.clearAll();
      logger.debug('Supplier cache cleared');
    } catch (error) {
      logger.error(`Failed to clear supplier cache: ${getErrorMessage(error)}`, { error });
    }
  };
 
  /**
   * Handles toggling between light and dark themes using the new theme system.
   *
   * @param event - The click event
   * @source
   */
  const handleToggleTheme = (event: MouseEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    toggleTheme();
  };
 
  /**
   * Handles opening the about modal.
   * @source
   */
  const handleAboutOpen = () => setAboutOpen(true);
 
  /**
   * Opens the bug-report dialog, prefilled with recent-exception diagnostics.
   * @source
   */
  const handleReportBug = () => void showReportDialog();
 
  // Stats are recorded in every build; the graphs appear only once the Konami
  // hotkey unlocks advanced mode — in dev builds too, so the toggle is honest.
  const statsVisible = appContext.advancedMode;
 
  /**
   * Handles navigating to the stats panel. No-op while the stats UI is hidden,
   * so the panel can't be reached outside advanced mode.
   * @source
   */
  const handleStatsOpen = () => {
    if (!statsVisible) return;
    appContext.setPanel?.(PANEL.STATS);
  };
 
  /**
   * Handles navigating to the price history panel. No-op while the panel is
   * hidden, so it can't be reached outside advanced mode.
   * @source
   */
  const handlePriceHistoryOpen = () => {
    if (!statsVisible) return;
    appContext.setPanel?.(PANEL.PRICE_HISTORY);
  };
 
  /**
   * Array of action configurations for the speed dial menu.
   * Each action includes an icon, name, and click handler.
   * The Stats and Price History actions appear only in advanced mode.
   * @source
   */
  const actions = [
    { icon: <ClearIcon />, name: i18n('speed_dial_clear_results'), onClick: handleClearResults },
    { icon: <AutoDeleteIcon />, name: i18n('speed_dial_clear_cache'), onClick: handleClearCache },
    { icon: <ContrastIcon />, name: i18n('speed_dial_toggle_theme'), onClick: handleToggleTheme },
    ...(statsVisible
      ? [
          { icon: <BarChartIcon />, name: i18n('speed_dial_stats'), onClick: handleStatsOpen },
          {
            icon: <PriceChangeIcon />,
            name: i18n('speed_dial_price_history'),
            onClick: handlePriceHistoryOpen,
          },
        ]
      : []),
    { icon: <BugReportIcon />, name: i18n('speed_dial_report_bug'), onClick: handleReportBug },
    { icon: <InfoOutlineIcon />, name: i18n('speed_dial_about'), onClick: handleAboutOpen },
  ];
 
  return (
    <>
      <AboutModal aboutOpen={aboutOpen} setAboutOpen={setAboutOpen} />
      <SpeedDial
        id="speed-dial-menu"
        className={speedDialVisibility ? 'speed-dial-menu open' : 'speed-dial-menu'}
        FabProps={{ size: 'small' }}
        ariaLabel={i18n('speed_dial_aria')}
        sx={{ position: 'fixed', bottom: 6, right: 0 }}
        icon={
          <HelpTooltip text={i18n('speed_dial_hint')}>
            <SpeedDialIcon />
          </HelpTooltip>
        }
      >
        {actions.map((action) => (
          <SpeedDialAction
            id={action.name}
            onClick={(e: MouseEvent<HTMLDivElement>) => {
              // MUI's SpeedDialAction fires a div MouseEvent; handlers only use
              // preventDefault(), which is identical across element types.
              action.onClick(e as unknown as MouseEvent<HTMLAnchorElement>);
            }}
            key={action.name}
            icon={action.icon}
            title={action.name}
          />
        ))}
      </SpeedDial>
    </>
  );
}