Skip to content

Commit ac02a97

Browse files
refactor(core): extract ToolbarActions and fix filter dropdown (#4128)
## Summary - Extract `CatalogLink`, `GridSizeToggle`, `ToolbarActions` components from FilterBar.tsx to eliminate code duplication - Fix filter dropdown to show all available values when a category is selected without typing a query - Update `parse_plot_path` to use current directory structure (`plots/{spec-id}/implementations/{library}.py`) ## Test plan - [ ] Verify toolbar icons (catalog link, grid toggle) work on desktop and mobile - [ ] Click on "Library" filter without typing - should show all 9 libraries - [ ] Type in filter search - fuzzy search should still work - [ ] Run unit tests for workflow_utils and workflow_cli 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f8341e0 commit ac02a97

File tree

6 files changed

+266
-232
lines changed

6 files changed

+266
-232
lines changed

app/src/components/FilterBar.tsx

Lines changed: 117 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
2-
import { Link } from 'react-router-dom';
32
import Box from '@mui/material/Box';
43
import Chip from '@mui/material/Chip';
54
import Menu from '@mui/material/Menu';
@@ -12,16 +11,14 @@ import Tooltip from '@mui/material/Tooltip';
1211
import CloseIcon from '@mui/icons-material/Close';
1312
import SearchIcon from '@mui/icons-material/Search';
1413
import AddIcon from '@mui/icons-material/Add';
15-
import ViewAgendaIcon from '@mui/icons-material/ViewAgenda';
16-
import ViewModuleIcon from '@mui/icons-material/ViewModule';
17-
import ListIcon from '@mui/icons-material/List';
1814
import useMediaQuery from '@mui/material/useMediaQuery';
1915
import { useTheme } from '@mui/material/styles';
2016

2117
import type { FilterCategory, ActiveFilters, FilterCounts } from '../types';
2218
import { FILTER_LABELS, FILTER_TOOLTIPS, FILTER_CATEGORIES } from '../types';
2319
import type { ImageSize } from '../constants';
2420
import { getAvailableValues, getAvailableValuesForGroup, getSearchResults, type SearchResult } from '../utils';
21+
import { ToolbarActions } from './ToolbarActions';
2522

2623
interface FilterBarProps {
2724
activeFilters: ActiveFilters;
@@ -250,8 +247,18 @@ export function FilterBar({
250247
return available.length > 0;
251248
})
252249
.map((cat) => ({ type: 'category' as const, category: cat }));
250+
} else if (selectedCategory && !hasQuery) {
251+
// Category selected but no query - show all available values for this category
252+
const available = getAvailableValues(filterCounts, activeFilters, selectedCategory);
253+
return available.map(([value, count]) => ({
254+
type: 'value' as const,
255+
category: selectedCategory,
256+
value,
257+
count,
258+
matchType: 'exact' as const,
259+
}));
253260
} else {
254-
// Search results or category values
261+
// Search results (with query)
255262
return searchResults.map((r) => ({ type: 'value' as const, ...r }));
256263
}
257264
}, [selectedCategory, hasQuery, filterCounts, activeFilters, searchResults]);
@@ -348,73 +355,14 @@ export function FilterBar({
348355
{scrollPercent}% · {currentTotal}
349356
</Typography>
350357
)}
351-
{/* Catalog icon + Grid size toggle - absolute right (desktop only) */}
358+
{/* Toolbar actions - absolute right (desktop only) */}
352359
{!isMobile && (
353-
<Box
354-
sx={{
355-
position: 'absolute',
356-
right: 0,
357-
display: 'flex',
358-
alignItems: 'center',
359-
gap: 0.5,
360-
}}
361-
>
362-
{/* Catalog icon */}
363-
<Tooltip title="catalog">
364-
<Box
365-
component={Link}
366-
to="/catalog"
367-
sx={{
368-
display: 'flex',
369-
alignItems: 'center',
370-
justifyContent: 'center',
371-
width: 32,
372-
height: 32,
373-
color: '#9ca3af',
374-
'&:hover': { color: '#3776AB' },
375-
}}
376-
>
377-
<ListIcon sx={{ fontSize: '1.25rem' }} />
378-
</Box>
379-
</Tooltip>
380-
{/* Grid size toggle */}
381-
<Tooltip title={imageSize === 'normal' ? 'compact view' : 'normal view'}>
382-
<Box
383-
role="button"
384-
tabIndex={0}
385-
aria-label={imageSize === 'normal' ? 'Switch to compact view' : 'Switch to normal view'}
386-
onClick={() => {
387-
const newSize = imageSize === 'normal' ? 'compact' : 'normal';
388-
onImageSizeChange(newSize);
389-
onTrackEvent('toggle_grid_size', { size: newSize });
390-
}}
391-
onKeyDown={(e) => {
392-
if (e.key === 'Enter' || e.key === ' ') {
393-
e.preventDefault();
394-
const newSize = imageSize === 'normal' ? 'compact' : 'normal';
395-
onImageSizeChange(newSize);
396-
onTrackEvent('toggle_grid_size', { size: newSize });
397-
}
398-
}}
399-
sx={{
400-
display: 'flex',
401-
alignItems: 'center',
402-
justifyContent: 'center',
403-
width: 32,
404-
height: 32,
405-
cursor: 'pointer',
406-
color: '#9ca3af',
407-
'&:hover': { color: '#3776AB' },
408-
'&:focus': { outline: '2px solid #3776AB', outlineOffset: 2 },
409-
}}
410-
>
411-
{imageSize === 'normal' ? (
412-
<ViewAgendaIcon sx={{ fontSize: '1.25rem' }} />
413-
) : (
414-
<ViewModuleIcon sx={{ fontSize: '1.25rem' }} />
415-
)}
416-
</Box>
417-
</Tooltip>
360+
<Box sx={{ position: 'absolute', right: 0 }}>
361+
<ToolbarActions
362+
imageSize={imageSize}
363+
onImageSizeChange={onImageSizeChange}
364+
onTrackEvent={onTrackEvent}
365+
/>
418366
</Box>
419367
)}
420368
{/* Active filter chips */}
@@ -594,64 +542,11 @@ export function FilterBar({
594542
) : (
595543
<Box />
596544
)}
597-
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
598-
{/* Catalog icon */}
599-
<Tooltip title="catalog">
600-
<Box
601-
component={Link}
602-
to="/catalog"
603-
sx={{
604-
display: 'flex',
605-
alignItems: 'center',
606-
justifyContent: 'center',
607-
width: 32,
608-
height: 32,
609-
color: '#9ca3af',
610-
'&:hover': { color: '#3776AB' },
611-
}}
612-
>
613-
<ListIcon sx={{ fontSize: '1.25rem' }} />
614-
</Box>
615-
</Tooltip>
616-
{/* Grid size toggle */}
617-
<Tooltip title={imageSize === 'normal' ? 'compact view' : 'normal view'}>
618-
<Box
619-
role="button"
620-
tabIndex={0}
621-
aria-label={imageSize === 'normal' ? 'Switch to compact view' : 'Switch to normal view'}
622-
onClick={() => {
623-
const newSize = imageSize === 'normal' ? 'compact' : 'normal';
624-
onImageSizeChange(newSize);
625-
onTrackEvent('toggle_grid_size', { size: newSize });
626-
}}
627-
onKeyDown={(e) => {
628-
if (e.key === 'Enter' || e.key === ' ') {
629-
e.preventDefault();
630-
const newSize = imageSize === 'normal' ? 'compact' : 'normal';
631-
onImageSizeChange(newSize);
632-
onTrackEvent('toggle_grid_size', { size: newSize });
633-
}
634-
}}
635-
sx={{
636-
display: 'flex',
637-
alignItems: 'center',
638-
justifyContent: 'center',
639-
width: 32,
640-
height: 32,
641-
cursor: 'pointer',
642-
color: '#9ca3af',
643-
'&:hover': { color: '#3776AB' },
644-
'&:focus': { outline: '2px solid #3776AB', outlineOffset: 2 },
645-
}}
646-
>
647-
{imageSize === 'normal' ? (
648-
<ViewAgendaIcon sx={{ fontSize: '1.25rem' }} />
649-
) : (
650-
<ViewModuleIcon sx={{ fontSize: '1.25rem' }} />
651-
)}
652-
</Box>
653-
</Tooltip>
654-
</Box>
545+
<ToolbarActions
546+
imageSize={imageSize}
547+
onImageSizeChange={onImageSizeChange}
548+
onTrackEvent={onTrackEvent}
549+
/>
655550
</Box>
656551
)}
657552

@@ -734,85 +629,97 @@ export function FilterBar({
734629
<Divider key="divider" />,
735630
]
736631
: []),
737-
...(searchResults.length > 0
738-
? (() => {
739-
// Split results into exact and fuzzy matches
740-
const exactResults = searchResults.filter((r) => r.matchType === 'exact');
741-
const fuzzyResults = searchResults.filter((r) => r.matchType === 'fuzzy');
742-
743-
const renderMenuItem = (result: SearchResult, idx: number) => {
744-
const { category, value, count } = result;
745-
const specTitle = category === 'spec' ? specTitles[value] : undefined;
746-
const menuItem = (
747-
<MenuItem
748-
key={`${category}-${value}`}
749-
onClick={() => handleValueSelect(category, value)}
750-
selected={idx === highlightedIndex}
751-
sx={{ fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace' }}
632+
...((() => {
633+
// Use searchResults if query exists, otherwise show all available values for selected category
634+
const resultsToShow: SearchResult[] = hasQuery
635+
? searchResults
636+
: selectedCategory
637+
? getAvailableValues(filterCounts, activeFilters, selectedCategory).map(([value, count]) => ({
638+
category: selectedCategory,
639+
value,
640+
count,
641+
matchType: 'exact' as const,
642+
}))
643+
: [];
644+
645+
if (resultsToShow.length > 0) {
646+
// Split results into exact and fuzzy matches
647+
const exactResults = resultsToShow.filter((r) => r.matchType === 'exact');
648+
const fuzzyResults = resultsToShow.filter((r) => r.matchType === 'fuzzy');
649+
650+
const renderMenuItem = (result: SearchResult, idx: number) => {
651+
const { category, value, count } = result;
652+
const specTitle = category === 'spec' ? specTitles[value] : undefined;
653+
const menuItem = (
654+
<MenuItem
655+
key={`${category}-${value}`}
656+
onClick={() => handleValueSelect(category, value)}
657+
selected={idx === highlightedIndex}
658+
sx={{ fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace' }}
659+
>
660+
<ListItemText
661+
primary={value}
662+
secondary={!selectedCategory ? FILTER_LABELS[category] : undefined}
663+
primaryTypographyProps={{
664+
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
665+
fontSize: '0.85rem',
666+
}}
667+
secondaryTypographyProps={{
668+
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
669+
fontSize: '0.7rem',
670+
color: '#9ca3af',
671+
}}
672+
/>
673+
<Typography
674+
sx={{
675+
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
676+
fontSize: '0.75rem',
677+
color: '#9ca3af',
678+
ml: 2,
679+
}}
752680
>
753-
<ListItemText
754-
primary={value}
755-
secondary={!selectedCategory ? FILTER_LABELS[category] : undefined}
756-
primaryTypographyProps={{
757-
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
758-
fontSize: '0.85rem',
759-
}}
760-
secondaryTypographyProps={{
761-
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
762-
fontSize: '0.7rem',
763-
color: '#9ca3af',
764-
}}
765-
/>
766-
<Typography
767-
sx={{
768-
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
769-
fontSize: '0.75rem',
770-
color: '#9ca3af',
771-
ml: 2,
772-
}}
773-
>
774-
({count})
775-
</Typography>
776-
</MenuItem>
777-
);
778-
return specTitle ? (
779-
<Tooltip key={`${category}-${value}`} title={specTitle} placement="right" arrow>
780-
<span>{menuItem}</span>
781-
</Tooltip>
782-
) : (
783-
menuItem
784-
);
785-
};
786-
787-
const items: React.ReactNode[] = [];
788-
// Add exact matches
789-
exactResults.forEach((result, i) => {
790-
items.push(renderMenuItem(result, i));
791-
});
792-
// Add fuzzy label/divider if there are fuzzy results
793-
if (fuzzyResults.length > 0) {
794-
items.push(
795-
<Divider key="exact-fuzzy-divider" sx={{ my: 0.5 }}>
796-
<Typography
797-
sx={{
798-
fontSize: '0.65rem',
799-
color: '#9ca3af',
800-
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
801-
px: 1,
802-
}}
803-
>
804-
fuzzy
805-
</Typography>
806-
</Divider>
807-
);
808-
}
809-
// Add fuzzy matches
810-
fuzzyResults.forEach((result, i) => {
811-
items.push(renderMenuItem(result, exactResults.length + i));
812-
});
813-
return items;
814-
})()
815-
: [
681+
({count})
682+
</Typography>
683+
</MenuItem>
684+
);
685+
return specTitle ? (
686+
<Tooltip key={`${category}-${value}`} title={specTitle} placement="right" arrow>
687+
<span>{menuItem}</span>
688+
</Tooltip>
689+
) : (
690+
menuItem
691+
);
692+
};
693+
694+
const items: React.ReactNode[] = [];
695+
// Add exact matches
696+
exactResults.forEach((result, i) => {
697+
items.push(renderMenuItem(result, i));
698+
});
699+
// Add fuzzy label/divider if there are fuzzy results
700+
if (fuzzyResults.length > 0) {
701+
items.push(
702+
<Divider key="exact-fuzzy-divider" sx={{ my: 0.5 }}>
703+
<Typography
704+
sx={{
705+
fontSize: '0.65rem',
706+
color: '#9ca3af',
707+
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
708+
px: 1,
709+
}}
710+
>
711+
fuzzy
712+
</Typography>
713+
</Divider>
714+
);
715+
}
716+
// Add fuzzy matches
717+
fuzzyResults.forEach((result, i) => {
718+
items.push(renderMenuItem(result, exactResults.length + i));
719+
});
720+
return items;
721+
} else {
722+
return [
816723
<MenuItem key="no-results" disabled>
817724
<Typography
818725
sx={{
@@ -824,7 +731,9 @@ export function FilterBar({
824731
no matches
825732
</Typography>
826733
</MenuItem>,
827-
]),
734+
];
735+
}
736+
})()),
828737
]}
829738
</Menu>
830739

0 commit comments

Comments
 (0)