Skip to content

Commit 800cdee

Browse files
committed
feat: search by shortcut
- introduce ctrl/cmd+f shortcut to search for resources, attributions, signals depending on the focused context - use React context for search-ref because Redux can only handle serializable data - use context for Virtuoso comp. props as they cannot be inlined: petyosi/react-virtuoso#566 closes #2587 Signed-off-by: Maxim Stykow <maxim.stykow@tngtech.com>
1 parent b820106 commit 800cdee

File tree

17 files changed

+230
-80
lines changed

17 files changed

+230
-80
lines changed

src/Frontend/Components/AttributionPanels/AttributionsPanel/AttributionsList/AttributionsList.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useAttributionIdsForReplacement } from '../../../../state/variables/use
1212
import { isPackageInfoIncomplete } from '../../../../util/is-important-attribution-information-missing';
1313
import { List, ListItemContentProps } from '../../../List/List';
1414
import { PackageCard } from '../../../PackageCard/PackageCard';
15+
import { SearchList } from '../../../SearchList/SearchList';
1516
import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel';
1617

1718
export const AttributionsList: React.FC<PackagesPanelChildrenProps> = ({
@@ -31,6 +32,7 @@ export const AttributionsList: React.FC<PackagesPanelChildrenProps> = ({
3132
<List
3233
renderItemContent={renderAttributionCard}
3334
data={activeAttributionIds}
35+
components={{ List: SearchList }}
3436
selectedId={selectedAttributionId}
3537
loading={loading}
3638
sx={{ transition: TRANSITION, height: contentHeight }}

src/Frontend/Components/AttributionPanels/PackagesPanel/PackagesPanel.tsx

+14-11
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,20 @@ export const PackagesPanel = ({
123123
[attributionIds, multiSelectedAttributionIds, selectedAttributionId],
124124
);
125125

126-
const areAllAttributionsSelected = useMemo(
127-
() =>
128-
!!attributionIds?.length &&
129-
!difference(
130-
attributionIds.filter(
131-
(id) => attributions?.[id].relation === activeRelation,
132-
),
133-
multiSelectedAttributionIds,
134-
).length,
135-
[activeRelation, attributionIds, attributions, multiSelectedAttributionIds],
136-
);
126+
const areAllAttributionsSelected = useMemo(() => {
127+
const activeAttributionIds = attributionIds?.filter(
128+
(id) => attributions?.[id].relation === activeRelation,
129+
);
130+
return (
131+
!!activeAttributionIds?.length &&
132+
!difference(activeAttributionIds, multiSelectedAttributionIds).length
133+
);
134+
}, [
135+
activeRelation,
136+
attributionIds,
137+
attributions,
138+
multiSelectedAttributionIds,
139+
]);
137140

138141
const effectiveSelectedIds = useMemo(
139142
() => intersection(attributionIds, multiSelectedAttributionIds),

src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsList/SignalsList.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '../../../GroupedList/GroupedList';
2121
import { SourceIcon } from '../../../Icons/Icons';
2222
import { PackageCard } from '../../../PackageCard/PackageCard';
23+
import { SearchList } from '../../../SearchList/SearchList';
2324
import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel';
2425
import { GroupName } from './SignalsList.style';
2526

@@ -71,6 +72,7 @@ export const SignalsList: React.FC<PackagesPanelChildrenProps> = ({
7172
grouped={groupedIds}
7273
selectedId={selectedAttributionId}
7374
renderItemContent={renderAttributionCard}
75+
components={{ List: SearchList }}
7476
renderGroupName={(sourceName) => (
7577
<>
7678
<SourceIcon noTooltip />

src/Frontend/Components/NoResults/NoResults.tsx src/Frontend/Components/EmptyPlaceholder/EmptyPlaceholder.tsx

+18-7
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,28 @@ import MuiBox from '@mui/material/Box';
77
import MuiTypography from '@mui/material/Typography';
88

99
import { text } from '../../../shared/text';
10+
import { useVirtuosoComponentContext } from '../VirtuosoComponentContext/VirtuosoComponentContext';
1011

11-
export const NoResults = styled((props) => (
12-
<MuiBox {...props}>
13-
<MuiTypography sx={{ textTransform: 'uppercase' }}>
14-
{text.generic.noResults}
15-
</MuiTypography>
16-
</MuiBox>
17-
))({
12+
const Container = styled(MuiBox)({
1813
display: 'flex',
1914
height: '100%',
2015
justifyContent: 'center',
2116
alignItems: 'center',
2217
opacity: 0.5,
2318
});
19+
20+
export const EmptyPlaceholder: React.FC = () => {
21+
const { showNoResults } = useVirtuosoComponentContext();
22+
23+
if (!showNoResults) {
24+
return null;
25+
}
26+
27+
return (
28+
<Container>
29+
<MuiTypography sx={{ textTransform: 'uppercase' }}>
30+
{text.generic.noResults}
31+
</MuiTypography>
32+
</Container>
33+
);
34+
};

src/Frontend/Components/GroupedList/GroupedList.tsx

+42-31
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import {
2020
import { text } from '../../../shared/text';
2121
import { OpossumColors } from '../../shared-styles';
2222
import { useVirtuosoRefs } from '../../util/use-virtuoso-refs';
23+
import { EmptyPlaceholder } from '../EmptyPlaceholder/EmptyPlaceholder';
2324
import { LoadingMask } from '../LoadingMask/LoadingMask';
24-
import { NoResults } from '../NoResults/NoResults';
25+
import { VirtuosoComponentContext } from '../VirtuosoComponentContext/VirtuosoComponentContext';
2526
import { GroupContainer, StyledLinearProgress } from './GroupedList.style';
2627

2728
export interface GroupedListItemContentProps {
@@ -53,6 +54,7 @@ export function GroupedList({
5354
selectedId,
5455
sx,
5556
testId,
57+
components,
5658
...props
5759
}: GroupedListProps & Omit<GroupedVirtuosoProps<string, unknown>, 'selected'>) {
5860
const [{ startIndex, endIndex }, setRange] = useState<{
@@ -80,6 +82,7 @@ export function GroupedList({
8082
focusedIndex,
8183
setIsVirtuosoFocused,
8284
selectedIndex,
85+
isVirtuosoFocused,
8386
} = useVirtuosoRefs<GroupedVirtuosoHandle>({
8487
data: groups?.ids,
8588
selectedId,
@@ -94,37 +97,45 @@ export function GroupedList({
9497
>
9598
{loading && <StyledLinearProgress data-testid={'loading'} />}
9699
{groups && (
97-
<GroupedVirtuoso
98-
ref={ref}
99-
onFocus={() => setIsVirtuosoFocused(true)}
100-
onBlur={() => setIsVirtuosoFocused(false)}
101-
components={{
102-
EmptyPlaceholder:
103-
loading || groups.ids.length ? undefined : () => <NoResults />,
100+
// Virtuoso components must not be inlined: https://github.com/petyosi/react-virtuoso/issues/566
101+
<VirtuosoComponentContext.Provider
102+
value={{
103+
isVirtuosoFocused,
104+
showNoResults: !loading && !groups.ids.length,
104105
}}
105-
scrollerRef={scrollerRef}
106-
rangeChanged={setRange}
107-
groupCounts={groups?.counts}
108-
groupContent={(index) => (
109-
<GroupContainer role={'group'}>
110-
<MuiBox sx={{ display: 'flex' }}>
111-
{renderJumpUp(index)}
112-
{renderJumpDown(index)}
113-
</MuiBox>
114-
{renderGroupName?.(groups.keys[index]) || (
115-
<MuiBox sx={{ flex: 1 }} />
116-
)}
117-
</GroupContainer>
118-
)}
119-
itemContent={(index) =>
120-
renderItemContent(groups.ids[index], {
121-
index,
122-
selected: index === selectedIndex,
123-
focused: index === focusedIndex,
124-
})
125-
}
126-
{...props}
127-
/>
106+
>
107+
<GroupedVirtuoso
108+
ref={ref}
109+
onFocus={() => setIsVirtuosoFocused(true)}
110+
onBlur={() => setIsVirtuosoFocused(false)}
111+
components={{
112+
EmptyPlaceholder,
113+
...components,
114+
}}
115+
scrollerRef={scrollerRef}
116+
rangeChanged={setRange}
117+
groupCounts={groups?.counts}
118+
groupContent={(index) => (
119+
<GroupContainer role={'group'}>
120+
<MuiBox sx={{ display: 'flex' }}>
121+
{renderJumpUp(index)}
122+
{renderJumpDown(index)}
123+
</MuiBox>
124+
{renderGroupName?.(groups.keys[index]) || (
125+
<MuiBox sx={{ flex: 1 }} />
126+
)}
127+
</GroupContainer>
128+
)}
129+
itemContent={(index) =>
130+
renderItemContent(groups.ids[index], {
131+
index,
132+
selected: index === selectedIndex,
133+
focused: index === focusedIndex,
134+
})
135+
}
136+
{...props}
137+
/>
138+
</VirtuosoComponentContext.Provider>
128139
)}
129140
</LoadingMask>
130141
);

src/Frontend/Components/List/List.tsx

+28-20
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { SxProps } from '@mui/system';
66
import { Virtuoso, VirtuosoHandle, VirtuosoProps } from 'react-virtuoso';
77

88
import { useVirtuosoRefs } from '../../util/use-virtuoso-refs';
9+
import { EmptyPlaceholder } from '../EmptyPlaceholder/EmptyPlaceholder';
910
import { LoadingMask } from '../LoadingMask/LoadingMask';
10-
import { NoResults } from '../NoResults/NoResults';
11+
import { VirtuosoComponentContext } from '../VirtuosoComponentContext/VirtuosoComponentContext';
1112
import { StyledLinearProgress } from './List.style';
1213

1314
export interface ListItemContentProps {
@@ -37,6 +38,7 @@ export function List({
3738
selectedId,
3839
sx,
3940
testId,
41+
components,
4042
...props
4143
}: ListProps & Omit<VirtuosoProps<string, unknown>, 'data' | 'selected'>) {
4244
const {
@@ -45,6 +47,7 @@ export function List({
4547
scrollerRef,
4648
setIsVirtuosoFocused,
4749
selectedIndex,
50+
isVirtuosoFocused,
4851
} = useVirtuosoRefs<VirtuosoHandle>({
4952
data,
5053
selectedId,
@@ -59,25 +62,30 @@ export function List({
5962
>
6063
{loading && <StyledLinearProgress data-testid={'loading'} />}
6164
{data && (
62-
<Virtuoso
63-
ref={ref}
64-
onFocus={() => setIsVirtuosoFocused(true)}
65-
onBlur={() => setIsVirtuosoFocused(false)}
66-
components={{
67-
EmptyPlaceholder:
68-
loading || data.length ? undefined : () => <NoResults />,
69-
}}
70-
scrollerRef={scrollerRef}
71-
data={data}
72-
itemContent={(index) =>
73-
renderItemContent(data[index], {
74-
index,
75-
selected: index === selectedIndex,
76-
focused: index === focusedIndex,
77-
})
78-
}
79-
{...props}
80-
/>
65+
// Virtuoso components must not be inlined: https://github.com/petyosi/react-virtuoso/issues/566
66+
<VirtuosoComponentContext.Provider
67+
value={{ isVirtuosoFocused, showNoResults: !loading && !data.length }}
68+
>
69+
<Virtuoso
70+
ref={ref}
71+
onFocus={() => setIsVirtuosoFocused(true)}
72+
onBlur={() => setIsVirtuosoFocused(false)}
73+
components={{
74+
EmptyPlaceholder,
75+
...components,
76+
}}
77+
scrollerRef={scrollerRef}
78+
data={data}
79+
itemContent={(index) =>
80+
renderItemContent(data[index], {
81+
index,
82+
selected: index === selectedIndex,
83+
focused: index === focusedIndex,
84+
})
85+
}
86+
{...props}
87+
/>
88+
</VirtuosoComponentContext.Provider>
8189
)}
8290
</LoadingMask>
8391
);

src/Frontend/Components/ReportView/TableConfig.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const tableConfigs: Array<TableConfig> = [
8484
},
8585
];
8686

87-
// table components must not be inlined: https://github.com/petyosi/react-virtuoso/issues/566
87+
// Virtuoso components must not be inlined: https://github.com/petyosi/react-virtuoso/issues/566
8888
export const TABLE_COMPONENTS: TableComponents<PackageInfo> = {
8989
Scroller: forwardRef((props, ref) => <TableContainer {...props} ref={ref} />),
9090
Table: (props) => (

src/Frontend/Components/ResizePanels/ResizePanels.style.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
/* eslint-disable @typescript-eslint/no-magic-numbers */
77
import ClearIcon from '@mui/icons-material/Clear';
8-
import { alpha, InputBase, styled } from '@mui/material';
8+
import { alpha, styled } from '@mui/material';
9+
import MuiInputBase from '@mui/material/InputBase';
910
import MuiPaper from '@mui/material/Paper';
1011
import MuiTypography from '@mui/material/Typography';
1112

@@ -81,7 +82,7 @@ export const ClearButton = styled(ClearIcon)({
8182
},
8283
});
8384

84-
export const StyledInputBase = styled(InputBase)(({ value }) => ({
85+
export const StyledInputBase = styled(MuiInputBase)(({ value }) => ({
8586
color: 'white',
8687
maxWidth: '144px',
8788
height: '24px',

src/Frontend/Components/ResizePanels/ResizePanels.tsx

+23-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'react';
88

99
import { TRANSITION } from '../../shared-styles';
1010
import { ResizableBox } from '../ResizableBox/ResizableBox';
11+
import { SearchRefContext } from '../SearchRefContext/SearchRefContext';
1112
import { ResizeButton } from './ResizeButton/ResizeButton';
1213
import {
1314
ClearButton,
@@ -66,6 +67,8 @@ export const ResizePanels: React.FC<ResizePanelsProps> = ({
6667
const fraction = FRACTIONS[main];
6768
const [isResizing, setIsResizing] = useState(true);
6869
const containerRef = useRef<Resizable>(null);
70+
const upperSearchRef = useRef<HTMLInputElement>(null);
71+
const lowerSearchRef = useRef<HTMLInputElement>(null);
6972

7073
const isLowerCollapsed = effectiveHeight <= HEADER_HEIGHT;
7174
const isUpperCollapsed =
@@ -115,8 +118,14 @@ export const ResizePanels: React.FC<ResizePanelsProps> = ({
115118

116119
return (
117120
<>
118-
{renderHeader({ ...upperPanel, showSearch: !isUpperCollapsed })}
119-
{upperPanel.component}
121+
{renderHeader({
122+
...upperPanel,
123+
showSearch: !isUpperCollapsed,
124+
searchRef: upperSearchRef,
125+
})}
126+
<SearchRefContext.Provider value={upperSearchRef}>
127+
{upperPanel.component}
128+
</SearchRefContext.Provider>
120129
</>
121130
);
122131
}
@@ -151,8 +160,11 @@ export const ResizePanels: React.FC<ResizePanelsProps> = ({
151160
...lowerPanel,
152161
showResizeButtons: true,
153162
showSearch: !isLowerCollapsed,
163+
searchRef: lowerSearchRef,
154164
})}
155-
{lowerPanel.component}
165+
<SearchRefContext.Provider value={lowerSearchRef}>
166+
{lowerPanel.component}
167+
</SearchRefContext.Provider>
156168
</ResizableBox>
157169
);
158170
}
@@ -162,17 +174,19 @@ export const ResizePanels: React.FC<ResizePanelsProps> = ({
162174
search,
163175
setSearch,
164176
showResizeButtons,
177+
searchRef,
165178
showSearch,
166179
headerTestId,
167180
}: Pick<ResizePanel, 'title' | 'search' | 'setSearch' | 'headerTestId'> & {
168181
title: string;
169182
showResizeButtons?: boolean;
170183
showSearch: boolean;
184+
searchRef: React.RefObject<HTMLInputElement>;
171185
}) {
172186
return (
173187
<Header data-testid={headerTestId} square>
174188
<HeaderText color={'ghostwhite'}>{title}</HeaderText>
175-
{showSearch && renderSearchButton({ search, setSearch })}
189+
{showSearch && renderSearchButton({ search, setSearch, searchRef })}
176190
{showResizeButtons && renderDownResizeButton()}
177191
{showResizeButtons && renderUpResizeButton()}
178192
</Header>
@@ -182,7 +196,10 @@ export const ResizePanels: React.FC<ResizePanelsProps> = ({
182196
function renderSearchButton({
183197
search,
184198
setSearch,
185-
}: Pick<ResizePanel, 'search' | 'setSearch'>) {
199+
searchRef,
200+
}: Pick<ResizePanel, 'search' | 'setSearch'> & {
201+
searchRef: React.RefObject<HTMLInputElement>;
202+
}) {
186203
return (
187204
<Search hasValue={!!search}>
188205
<SearchIconWrapper>
@@ -193,6 +210,7 @@ export const ResizePanels: React.FC<ResizePanelsProps> = ({
193210
onChange={(event) => setSearch(event.target.value)}
194211
spellCheck={false}
195212
type={'search'}
213+
inputRef={searchRef}
196214
/>
197215
{!!search && (
198216
<ClearIconWrapper>

0 commit comments

Comments
 (0)