Skip to content

Commit 3c6a6ce

Browse files
authored
Highlight searched element (#443)
Signed-off-by: LE SAULNIER Kevin <[email protected]>
1 parent 8594ad9 commit 3c6a6ce

File tree

9 files changed

+158
-28
lines changed

9 files changed

+158
-28
lines changed

src/components/app-wrapper.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ let lightTheme = createTheme({
8888
},
8989
aggrid: {
9090
theme: 'ag-theme-alpine',
91-
highlightColor: '#8e9c9b',
91+
highlightColor: '#E8E8E8',
9292
},
9393
agGridBackground: {
9494
color: 'white',
@@ -145,7 +145,7 @@ let darkTheme = createTheme({
145145
},
146146
aggrid: {
147147
theme: 'ag-theme-alpine-dark',
148-
highlightColor: '#545c5b',
148+
highlightColor: '#272727',
149149
},
150150
agGridBackground: {
151151
color: '#383838',

src/components/directory-content-table.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
import { defaultColumnDefinition } from './utils/directory-content-utils';
99
import {
1010
CustomAGGrid,
11-
ElementType,
1211
ElementAttributes,
12+
ElementType,
1313
} from '@gridsuite/commons-ui';
14-
import { AgGridReact } from 'ag-grid-react';
14+
import { AgGridReact, AgGridReactProps } from 'ag-grid-react';
1515
import {
1616
ColDef,
1717
RowClassParams,
@@ -20,7 +20,11 @@ import {
2020
} from 'ag-grid-community';
2121
import { RefObject } from 'react';
2222

23-
interface DirectoryContentTableProps {
23+
interface DirectoryContentTableProps
24+
extends Pick<
25+
AgGridReactProps<ElementAttributes>,
26+
'getRowStyle' | 'onGridReady'
27+
> {
2428
gridRef: RefObject<AgGridReact<ElementAttributes>>;
2529
rows: ElementAttributes[];
2630
handleCellContextualMenu: () => void;
@@ -37,7 +41,7 @@ const recomputeOverFlowableCells = ({ api }: AgGridEvent) =>
3741

3842
export const CUSTOM_ROW_CLASS = 'custom-row-class';
3943

40-
const getRowStyle = (cellData: RowClassParams<ElementAttributes>) => {
44+
const getClickableRowStyle = (cellData: RowClassParams<ElementAttributes>) => {
4145
const style: Record<string, string> = { fontSize: '1rem' };
4246
if (
4347
cellData.data &&
@@ -57,11 +61,20 @@ const getRowStyle = (cellData: RowClassParams<ElementAttributes>) => {
5761
export const DirectoryContentTable = ({
5862
gridRef,
5963
rows,
64+
getRowStyle,
6065
handleCellContextualMenu,
6166
handleRowSelected,
6267
handleCellClick,
68+
onGridReady,
6369
colDef,
6470
}: DirectoryContentTableProps) => {
71+
const getCustomRowStyle = (cellData: RowClassParams<ElementAttributes>) => {
72+
return {
73+
...getClickableRowStyle(cellData),
74+
...getRowStyle?.(cellData),
75+
};
76+
};
77+
6578
return (
6679
<CustomAGGrid
6780
ref={gridRef}
@@ -70,13 +83,14 @@ export const DirectoryContentTable = ({
7083
defaultColDef={defaultColumnDefinition}
7184
rowSelection="multiple"
7285
suppressRowClickSelection
86+
onGridReady={onGridReady}
7387
onCellContextMenu={handleCellContextualMenu}
7488
onCellClicked={handleCellClick}
7589
onRowSelected={handleRowSelected}
7690
onGridSizeChanged={recomputeOverFlowableCells}
7791
animateRows={true}
7892
columnDefs={colDef}
79-
getRowStyle={getRowStyle}
93+
getRowStyle={getCustomRowStyle}
8094
//We set a custom className for rows in order to easily determine if a context menu event is happening on a row or not
8195
rowClass={CUSTOM_ROW_CLASS}
8296
/>

src/components/directory-content.jsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
DirectoryContentTable,
5050
CUSTOM_ROW_CLASS,
5151
} from './directory-content-table';
52+
import { useHighlightSearchedElement } from './search/use-highlight-searched-element';
5253

5354
const circularProgressSize = '70px';
5455

@@ -69,6 +70,16 @@ const styles = {
6970
centeredCircularProgress: {
7071
alignSelf: 'center',
7172
},
73+
highlightedElementAnimation: (theme) => ({
74+
'@keyframes highlighted-element': {
75+
'from, 24%': {
76+
backgroundColor: 'inherit',
77+
},
78+
'12%, 36%, to': {
79+
backgroundColor: theme.row.hover,
80+
},
81+
},
82+
}),
7283
};
7384

7485
const initialMousePosition = {
@@ -84,6 +95,8 @@ const DirectoryContent = () => {
8495
const selectionForCopy = useSelector((state) => state.selectionForCopy);
8596
const activeDirectory = useSelector((state) => state.activeDirectory);
8697

98+
const [onGridReady, getRowStyle] = useHighlightSearchedElement();
99+
87100
const [languageLocal] = useParameterState(PARAM_LANGUAGE);
88101

89102
const dispatchSelectionForCopy = useCallback(
@@ -550,6 +563,8 @@ const DirectoryContent = () => {
550563
handleRowSelected={handleRowSelected}
551564
handleCellClick={handleCellClick}
552565
colDef={getColumnsDefinition(childrenMetadata, intl)}
566+
getRowStyle={getRowStyle}
567+
onGridReady={onGridReady}
553568
/>
554569
);
555570
};
@@ -678,7 +693,12 @@ const DirectoryContent = () => {
678693
/>
679694
)
680695
}
681-
<Grid xs={12} onContextMenu={onContextMenu}>
696+
<Grid
697+
item
698+
sx={styles.highlightedElementAnimation}
699+
xs={12}
700+
onContextMenu={onContextMenu}
701+
>
682702
{renderContent()}
683703
</Grid>
684704
<div

src/components/search/search-bar.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,19 @@ import {
1414
fetchDirectoryContent,
1515
} from '@gridsuite/commons-ui';
1616
import { useDispatch, useSelector } from 'react-redux';
17-
import { setSelectedDirectory, setTreeData } from '../../redux/actions';
17+
import {
18+
setSearchedElement,
19+
setSelectedDirectory,
20+
setTreeData,
21+
} from '../../redux/actions';
1822
import { updatedTree } from '../tree-views-container';
19-
import { MatchingElementProps, SearchItem } from './search-item';
20-
import { IDirectory, ITreeData, ReduxState } from '../../redux/reducer.type';
23+
import { SearchItem } from './search-item';
24+
import {
25+
ElementAttributesES,
26+
IDirectory,
27+
ITreeData,
28+
ReduxState,
29+
} from '../../redux/reducer.type';
2130
import { RenderElementProps } from '@gridsuite/commons-ui/dist/components/ElementSearchDialog/element-search-input';
2231
import { TextFieldProps } from '@mui/material';
2332
import { SearchBarRenderInput } from './search-bar-render-input';
@@ -29,9 +38,8 @@ interface SearchBarProps {
2938
inputRef: RefObject<TextFieldProps>;
3039
}
3140

32-
const fetchElements: (
33-
newSearchTerm: string
34-
) => Promise<MatchingElementProps[]> = searchElementsInfos;
41+
const fetchElements: (newSearchTerm: string) => Promise<ElementAttributesES[]> =
42+
searchElementsInfos;
3543

3644
export const SearchBar: FunctionComponent<SearchBarProps> = ({ inputRef }) => {
3745
const dispatch = useDispatch();
@@ -46,7 +54,7 @@ export const SearchBar: FunctionComponent<SearchBarProps> = ({ inputRef }) => {
4654
});
4755

4856
const renderOptionItem = useCallback(
49-
(props: RenderElementProps<MatchingElementProps>) => {
57+
(props: RenderElementProps<ElementAttributesES>) => {
5058
const { element, inputValue } = props;
5159
const matchingElement = elementsFound.find(
5260
(e) => e.id === element.id
@@ -97,9 +105,9 @@ export const SearchBar: FunctionComponent<SearchBarProps> = ({ inputRef }) => {
97105
);
98106

99107
const handleMatchingElement = useCallback(
100-
async (data: MatchingElementProps | string | null) => {
108+
async (data: ElementAttributesES | string | null) => {
101109
const matchingElement = elementsFound.find(
102-
(element: MatchingElementProps) => element === data
110+
(element: ElementAttributesES) => element === data
103111
);
104112
if (matchingElement !== undefined) {
105113
const elementUuidPath = matchingElement?.pathUuid;
@@ -121,9 +129,16 @@ export const SearchBar: FunctionComponent<SearchBarProps> = ({ inputRef }) => {
121129
}
122130
const lastElement = elementUuidPath.pop();
123131
handleDispatchDirectory(lastElement);
132+
dispatch(setSearchedElement(data));
124133
}
125134
},
126-
[elementsFound, handleDispatchDirectory, updateMapData, snackError]
135+
[
136+
elementsFound,
137+
handleDispatchDirectory,
138+
updateMapData,
139+
snackError,
140+
dispatch,
141+
]
127142
);
128143

129144
return (

src/components/search/search-item.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
* License, v. 2.0. If a copy of the MPL was not distributed with this
55
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
66
*/
7-
import { ElementType, getFileIcon } from '@gridsuite/commons-ui';
7+
import { getFileIcon } from '@gridsuite/commons-ui';
88
import Grid from '@mui/material/Grid';
99
import Typography from '@mui/material/Typography';
1010
import { FormattedMessage } from 'react-intl';
1111
import { FunctionComponent } from 'react';
1212
import { Theme } from '@mui/material';
13+
import { ElementAttributesES } from 'redux/reducer.type';
1314

1415
const styles = {
1516
icon: (theme: Theme) => ({
@@ -37,18 +38,10 @@ interface HighlightedTextProps {
3738
}
3839

3940
interface SearchItemProps {
40-
matchingElement: MatchingElementProps;
41+
matchingElement: ElementAttributesES;
4142
inputValue: string;
4243
}
4344

44-
export interface MatchingElementProps {
45-
id: string;
46-
name: string;
47-
type: ElementType;
48-
pathName: string[];
49-
pathUuid: string[];
50-
}
51-
5245
export const HighlightedText: FunctionComponent<HighlightedTextProps> = ({
5346
text,
5447
highlight,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Copyright (c) 2024, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
import { ElementAttributes } from '@gridsuite/commons-ui';
9+
import { GridReadyEvent, RowClassParams, RowStyle } from 'ag-grid-community';
10+
import { useCallback, useRef } from 'react';
11+
import { useDispatch, useSelector } from 'react-redux';
12+
import { setSearchedElement } from '../../redux/actions';
13+
import { ReduxState } from '../../redux/reducer.type';
14+
15+
const SEARCH_HIGHLIGHT_DURATION_S = 4;
16+
17+
export const useHighlightSearchedElement = () => {
18+
const searchedElement = useSelector(
19+
(state: ReduxState) => state.searchedElement
20+
);
21+
const dispatch = useDispatch();
22+
const timeout = useRef<ReturnType<typeof setTimeout>>();
23+
24+
const onGridReady = useCallback(
25+
({ api }: GridReadyEvent<ElementAttributes>) => {
26+
// if there is a searched element, we scroll to it, style it for SEARCH_HIGHTLIGHT_DURATION, then remove it from searchedElement to go back to previous style
27+
if (!searchedElement) {
28+
return;
29+
}
30+
const searchedElementRow = api.getRowNode(searchedElement.id);
31+
if (
32+
searchedElementRow?.rowIndex != null &&
33+
searchedElementRow?.rowIndex >= 0
34+
) {
35+
api.ensureIndexVisible(searchedElementRow.rowIndex, 'top');
36+
clearTimeout(timeout.current);
37+
timeout.current = setTimeout(() => {
38+
dispatch(setSearchedElement(null));
39+
}, SEARCH_HIGHLIGHT_DURATION_S * 1000);
40+
}
41+
},
42+
[searchedElement, dispatch]
43+
);
44+
45+
const getRowStyle = useCallback(
46+
(cellData: RowClassParams<ElementAttributes, any>) => {
47+
const style: RowStyle = { fontSize: '1rem' };
48+
if (cellData?.data?.elementUuid === searchedElement?.id) {
49+
// keyframe "highlighted-element" has to be defined in css containing highlighted element
50+
style[
51+
'animation'
52+
] = `highlighted-element ${SEARCH_HIGHLIGHT_DURATION_S}s`;
53+
}
54+
return style;
55+
},
56+
[searchedElement?.id]
57+
);
58+
59+
return [onGridReady, getRowStyle];
60+
};

src/redux/actions.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,12 @@ export function setTreeData(treeData) {
110110
treeData: treeData,
111111
};
112112
}
113+
114+
export const SEARCHED_ELEMENT = 'SEARCHED_ELEMENT';
115+
116+
export function setSearchedElement(searchedElement) {
117+
return {
118+
type: SEARCHED_ELEMENT,
119+
searchedElement: searchedElement,
120+
};
121+
}

src/redux/reducer.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
DIRECTORY_UPDATED,
3030
TREE_DATA,
3131
SELECTION_FOR_COPY,
32+
SEARCHED_ELEMENT,
3233
} from './actions';
3334

3435
import {
@@ -53,6 +54,7 @@ const initialState = {
5354
currentChildren: null,
5455
selectedDirectory: null,
5556
activeDirectory: null,
57+
searchedElement: null,
5658
currentPath: [],
5759
user: null,
5860
signInCallbackError: null,
@@ -138,6 +140,10 @@ export const reducer = createReducer(initialState, (builder) => {
138140
state.activeDirectory = action.activeDirectory;
139141
});
140142

143+
builder.addCase(SEARCHED_ELEMENT, (state, action) => {
144+
state.searchedElement = action.searchedElement;
145+
});
146+
141147
builder.addCase(CURRENT_PATH, (state, action) => {
142148
state.currentPath = action.currentPath;
143149
});

src/redux/reducer.type.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ export type IDirectory = ElementAttributes & {
2828
type: ElementType.DIRECTORY;
2929
};
3030

31+
export interface ElementAttributesES {
32+
id: UUID;
33+
name: string;
34+
parentId: UUID;
35+
type: ElementType;
36+
owner: string;
37+
subdirectoriesCount: number;
38+
lastModificationDate: string;
39+
pathName: string[];
40+
pathUuid: UUID[];
41+
}
42+
3143
export interface ITreeData {
3244
rootDirectories: IDirectory[];
3345
mapData: Record<string, IDirectory>;
@@ -37,6 +49,7 @@ export interface ReduxState {
3749
activeDirectory: UUID;
3850
currentChildren: ElementAttributes[];
3951
selectedDirectory: ElementAttributes;
52+
searchedElement: ElementAttributesES;
4053
treeData: ITreeData;
4154
user: IUser;
4255
}

0 commit comments

Comments
 (0)