Skip to content

Commit 9f3c202

Browse files
authored
Refactor EmptyDirectory and anchorReference strategies management (#545)
Signed-off-by: sBouzols <[email protected]>
1 parent eb1e56f commit 9f3c202

File tree

5 files changed

+99
-113
lines changed

5 files changed

+99
-113
lines changed

src/components/directory-content.tsx

Lines changed: 67 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import {
2424
import { Add as AddIcon } from '@mui/icons-material';
2525
import { AgGridReact } from 'ag-grid-react';
2626
import { SelectionForCopy } from '@gridsuite/commons-ui/dist/components/filter/filter.type';
27+
import { CellContextMenuEvent } from 'ag-grid-community';
2728
import { ContingencyListType, FilterType, NetworkModificationType } from '../utils/elementType';
2829
import * as constants from '../utils/UIconstants';
2930
import { setActiveDirectory, setSelectionForCopy } from '../redux/actions';
3031
import { elementExists, getFilterById, updateElement } from '../utils/rest-api';
32+
import { AnchorStatesType, defaultAnchorStates } from './menus/common-contextual-menu';
3133
import ContentContextualMenu from './menus/content-contextual-menu';
3234
import ContentToolbar from './toolbars/content-toolbar';
3335
import DirectoryTreeContextualMenu from './menus/directory-tree-contextual-menu';
@@ -91,11 +93,6 @@ const styles = {
9193
},
9294
};
9395

94-
const initialMousePosition = {
95-
mouseX: null,
96-
mouseY: null,
97-
};
98-
9996
const isStudyMetadata = (metadata: Metadata): metadata is StudyMetadata => metadata.name === 'Study';
10097

10198
export default function DirectoryContent() {
@@ -111,7 +108,6 @@ export default function DirectoryContent() {
111108
const [onGridReady, getRowStyle] = useHighlightSearchedElement(gridRef?.current?.api ?? null);
112109

113110
const [languageLocal] = useParameterState(PARAM_LANGUAGE);
114-
const selectedTheme = useSelector((state: AppState) => state.theme);
115111

116112
const dispatchSelectionForCopy = useCallback(
117113
(selection: SelectionForCopy) => {
@@ -150,10 +146,7 @@ export default function DirectoryContent() {
150146
const [checkedRows, setCheckedRows] = useState<ElementAttributes[]>([]);
151147

152148
/* Menu states */
153-
const [mousePosition, setMousePosition] = useState<{
154-
mouseX: number | null;
155-
mouseY: number | null;
156-
}>(initialMousePosition);
149+
const [directoryMenuAnchorStates, setDirectoryMenuAnchorStates] = useState<AnchorStatesType>(defaultAnchorStates);
157150

158151
const [openDialog, setOpenDialog] = useState(constants.DialogsId.NONE);
159152
const [elementName, setElementName] = useState('');
@@ -246,6 +239,36 @@ export default function DirectoryContent() {
246239
event.stopPropagation();
247240
};
248241

242+
const onContextMenu = useCallback(
243+
(event: any, anchorStates: AnchorStatesType = defaultAnchorStates) => {
244+
if (anchorStates.anchorReference === 'anchorPosition') {
245+
// example : right click on empty space or on an element line
246+
// then open popover on mouse position with a little shift
247+
setDirectoryMenuAnchorStates({
248+
...anchorStates,
249+
anchorPosition: {
250+
top: event.clientY + constants.VERTICAL_SHIFT,
251+
left: event.clientX + constants.HORIZONTAL_SHIFT,
252+
},
253+
});
254+
} else {
255+
// else anchorEl
256+
// example : left click on a 'create element' button
257+
// then open popover attached to the component clicked
258+
setDirectoryMenuAnchorStates(anchorStates);
259+
}
260+
// We check if the context menu was triggered from a row to prevent displaying both the directory and the content context menus
261+
const isRow = !!event.target.closest(`.${CUSTOM_ROW_CLASS}`);
262+
if (!isRow) {
263+
dispatch(setActiveDirectory(selectedDirectory?.elementUuid));
264+
handleOpenDirectoryMenu(event);
265+
} else {
266+
handleOpenContentMenu(event);
267+
}
268+
},
269+
[dispatch, selectedDirectory?.elementUuid]
270+
);
271+
249272
/* User interactions */
250273
const contextualMixPolicies = useMemo(
251274
() => ({
@@ -257,30 +280,26 @@ export default function DirectoryContent() {
257280
const contextualMixPolicy = contextualMixPolicies.ALL;
258281

259282
const onCellContextMenu = useCallback(
260-
(event: any) => {
261-
if (event.data && event.data.uploading !== null) {
262-
if (event.data.type !== 'DIRECTORY') {
283+
(cellEvent: CellContextMenuEvent) => {
284+
if (cellEvent.data && cellEvent.data.uploading !== null) {
285+
if (cellEvent.data.type !== 'DIRECTORY') {
263286
dispatch(setActiveDirectory(selectedDirectory?.elementUuid));
264287
setActiveElement({
265-
hasMetadata: childrenMetadata[event.data.elementUuid] !== undefined,
266-
specificMetadata: childrenMetadata[event.data.elementUuid]?.specificMetadata,
267-
...event.data,
288+
hasMetadata: childrenMetadata[cellEvent.data.elementUuid] !== undefined,
289+
specificMetadata: childrenMetadata[cellEvent.data.elementUuid]?.specificMetadata,
290+
...cellEvent.data,
268291
});
269292
if (contextualMixPolicy === contextualMixPolicies.BIG) {
270293
// If some elements were already selected and the active element is not in them, we deselect the already selected elements.
271-
if (isRowUnchecked(event.data, checkedRows)) {
294+
if (isRowUnchecked(cellEvent.data, checkedRows)) {
272295
gridRef.current?.api.deselectAll();
273296
}
274-
} else if (isRowUnchecked(event.data, checkedRows)) {
297+
} else if (isRowUnchecked(cellEvent.data, checkedRows)) {
275298
// If some elements were already selected, we add the active element to the selected list if not already in it.
276-
gridRef.current?.api.getRowNode(event.data.elementUuid)?.setSelected(true);
299+
gridRef.current?.api.getRowNode(cellEvent.data.elementUuid)?.setSelected(true);
277300
}
278301
}
279-
setMousePosition({
280-
mouseX: event.event.clientX + constants.HORIZONTAL_SHIFT,
281-
mouseY: event.event.clientY + constants.VERTICAL_SHIFT,
282-
});
283-
handleOpenContentMenu(event.event);
302+
onContextMenu(cellEvent.event);
284303
}
285304
},
286305
[
@@ -290,26 +309,10 @@ export default function DirectoryContent() {
290309
contextualMixPolicy,
291310
dispatch,
292311
selectedDirectory?.elementUuid,
312+
onContextMenu,
293313
]
294314
);
295315

296-
const onContextMenu = useCallback(
297-
(event: any) => {
298-
// We check if the context menu was triggered from a row to prevent displaying both the directory and the content context menus
299-
const isRow = !!event.target.closest(`.${CUSTOM_ROW_CLASS}`);
300-
if (!isRow) {
301-
dispatch(setActiveDirectory(selectedDirectory?.elementUuid));
302-
303-
setMousePosition({
304-
mouseX: event.clientX + constants.HORIZONTAL_SHIFT,
305-
mouseY: event.clientY + constants.VERTICAL_SHIFT,
306-
});
307-
handleOpenDirectoryMenu(event);
308-
}
309-
},
310-
[dispatch, selectedDirectory?.elementUuid]
311-
);
312-
313316
const handleError = useCallback(
314317
(message: string) => {
315318
snackError({
@@ -428,36 +431,20 @@ export default function DirectoryContent() {
428431
</Box>
429432
);
430433

431-
const handleMousePosition = useCallback(
432-
(coordinates: DOMRect, isEmpty: boolean): { mouseX: number | null; mouseY: number | null } => {
433-
if (isEmpty) {
434-
return {
435-
mouseX: coordinates.right,
436-
mouseY: coordinates.top + 25 * constants.VERTICAL_SHIFT,
437-
};
438-
}
439-
return {
440-
mouseX: coordinates.left,
441-
mouseY: coordinates.bottom,
442-
};
443-
},
444-
[]
445-
);
446-
447-
const handleDialog = useCallback(
448-
(mouseEvent: MouseEvent<HTMLElement>, isEmpty: boolean) => {
449-
const coordinates: DOMRect = (mouseEvent.target as HTMLElement).getBoundingClientRect();
450-
// set the contextualMenu position
451-
setMousePosition(handleMousePosition(coordinates, isEmpty));
452-
setOpenDirectoryMenu(true);
453-
454-
dispatch(setActiveDirectory(selectedDirectory?.elementUuid));
455-
},
456-
[dispatch, selectedDirectory?.elementUuid, handleMousePosition]
457-
);
458-
459434
const renderEmptyDirContent = () => (
460-
<EmptyDirectory openDialog={(mouseEvent) => handleDialog(mouseEvent, true)} theme={selectedTheme} />
435+
<EmptyDirectory
436+
onCreateElementButtonClick={(mouseEvent) =>
437+
onContextMenu(mouseEvent, {
438+
anchorReference: 'anchorEl',
439+
anchorEl: mouseEvent.currentTarget,
440+
anchorOrigin: { vertical: 'center', horizontal: 'right' },
441+
transformOrigin: {
442+
vertical: 'center',
443+
horizontal: 'left',
444+
},
445+
})
446+
}
447+
/>
461448
);
462449

463450
const renderContent = () => {
@@ -649,7 +636,14 @@ export default function DirectoryContent() {
649636
variant="contained"
650637
endIcon={<AddIcon />}
651638
sx={styles.button}
652-
onClick={(mouseEvent) => handleDialog(mouseEvent, false)}
639+
onClick={(mouseEvent) =>
640+
onContextMenu(mouseEvent, {
641+
anchorReference: 'anchorEl',
642+
anchorEl: mouseEvent.currentTarget,
643+
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
644+
transformOrigin: { vertical: 'top', horizontal: 'left' },
645+
})
646+
}
653647
>
654648
<FormattedMessage id="createElement" />
655649
</Button>
@@ -675,15 +669,7 @@ export default function DirectoryContent() {
675669
openDialog={openDialog}
676670
setOpenDialog={setOpenDialog}
677671
onClose={handleCloseContentMenu}
678-
anchorReference="anchorPosition"
679-
anchorPosition={
680-
mousePosition.mouseY !== null && mousePosition.mouseX !== null
681-
? {
682-
top: mousePosition.mouseY,
683-
left: mousePosition.mouseX,
684-
}
685-
: undefined
686-
}
672+
{...directoryMenuAnchorStates}
687673
broadcastChannel={broadcastChannel}
688674
/>
689675
)}
@@ -693,15 +679,7 @@ export default function DirectoryContent() {
693679
openDialog={openDialog}
694680
setOpenDialog={setOpenDialog}
695681
onClose={handleCloseDirectoryMenu}
696-
anchorReference="anchorPosition"
697-
anchorPosition={
698-
mousePosition.mouseY !== null && mousePosition.mouseX !== null
699-
? {
700-
top: mousePosition.mouseY,
701-
left: mousePosition.mouseX,
702-
}
703-
: undefined
704-
}
682+
{...directoryMenuAnchorStates}
705683
restrictMenuItems
706684
/>
707685
</Box>

src/components/empty-directory.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
66
*/
77

8-
import { MouseEvent } from 'react';
98
import { Box, Button, SvgIcon } from '@mui/material';
109
import { Add as AddIcon } from '@mui/icons-material';
1110
import { FormattedMessage } from 'react-intl';
1211
import { LIGHT_THEME } from '@gridsuite/commons-ui';
12+
import { useSelector } from 'react-redux';
13+
import { PARAM_THEME } from 'utils/config-params';
14+
import { AppState } from '../redux/types';
1315
import CircleIcon from './icons/circleIcon';
1416

1517
const CIRCLE_SIZE = 250;
@@ -37,13 +39,12 @@ const styles = {
3739
marginTop: theme.spacing(1),
3840
}),
3941
};
40-
41-
export interface EmptyFolderProps {
42-
openDialog: (e: MouseEvent<HTMLElement>) => void;
43-
theme: string;
42+
interface EmptyDirectoryProps {
43+
onCreateElementButtonClick: (e: React.MouseEvent<HTMLElement>) => void;
4444
}
4545

46-
export default function EmptyDirectory({ openDialog, theme }: Readonly<EmptyFolderProps>) {
46+
export default function EmptyDirectory({ onCreateElementButtonClick }: Readonly<EmptyDirectoryProps>) {
47+
const theme = useSelector((state: AppState) => state[PARAM_THEME]);
4748
return (
4849
<Box sx={styles.container}>
4950
<CircleIcon size={CIRCLE_SIZE} iconStyles={styles.circle}>
@@ -63,7 +64,12 @@ export default function EmptyDirectory({ openDialog, theme }: Readonly<EmptyFold
6364
<h3>
6465
<FormattedMessage id="emptyDirContent" />
6566
</h3>
66-
<Button variant="contained" sx={styles.button} onClick={openDialog} endIcon={<AddIcon />}>
67+
<Button
68+
variant="contained"
69+
sx={styles.button}
70+
onClick={onCreateElementButtonClick}
71+
endIcon={<AddIcon />}
72+
>
6773
<FormattedMessage id="createElement" />
6874
</Button>
6975
</Box>

src/components/menus/common-contextual-menu.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
Menu,
1616
MenuItem,
1717
MenuProps,
18+
PopoverOrigin,
1819
PopoverPosition,
20+
PopoverProps,
1921
PopoverReference,
2022
styled,
2123
} from '@mui/material';
@@ -38,14 +40,23 @@ export type MenuItemType =
3840
disabled?: boolean;
3941
};
4042

41-
export interface CommonContextualMenuProps {
43+
export interface CommonContextualMenuProps extends MenuProps {
4244
onClose?: (e?: unknown, nextSelectedDirectoryId?: string | null) => void;
43-
open: boolean;
45+
menuItems?: MenuItemType[];
46+
}
47+
48+
export interface AnchorStatesType {
49+
anchorEl?: PopoverProps['anchorEl'];
4450
anchorReference?: PopoverReference;
4551
anchorPosition?: PopoverPosition;
46-
menuItems?: MenuItemType[];
52+
anchorOrigin?: PopoverOrigin;
53+
transformOrigin?: PopoverOrigin;
4754
}
4855

56+
export const defaultAnchorStates: AnchorStatesType = {
57+
anchorReference: 'anchorPosition',
58+
};
59+
4960
export default function CommonContextualMenu(props: Readonly<CommonContextualMenuProps>) {
5061
const { menuItems, ...others } = props;
5162

src/components/menus/content-contextual-menu.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {
2727
TreeViewFinderNodeProps,
2828
useSnackMessage,
2929
} from '@gridsuite/commons-ui';
30-
import { PopoverPosition, PopoverReference } from '@mui/material';
3130
import RenameDialog from '../dialogs/rename-dialog';
3231
import DeleteDialog from '../dialogs/delete-dialog';
3332
import ReplaceWithScriptDialog from '../dialogs/replace-with-script-dialog';
@@ -47,7 +46,7 @@ import {
4746
replaceFormContingencyListWithScript,
4847
} from '../../utils/rest-api';
4948
import { ContingencyListType, FilterType } from '../../utils/elementType';
50-
import CommonContextualMenu from './common-contextual-menu';
49+
import CommonContextualMenu, { CommonContextualMenuProps } from './common-contextual-menu';
5150
import { useDeferredFetch, useMultipleDeferredFetch } from '../../utils/custom-hooks';
5251
import MoveDialog from '../dialogs/move-dialog';
5352
import { useDownloadUtils } from '../utils/downloadUtils';
@@ -58,16 +57,13 @@ import { PARAM_LANGUAGE } from '../../utils/config-params';
5857
import { handleMaxElementsExceededError } from '../utils/rest-errors';
5958
import { AppState } from '../../redux/types';
6059

61-
export interface ContentContextualMenuProps {
60+
interface ContentContextualMenuProps extends CommonContextualMenuProps {
6261
activeElement: ElementAttributes;
6362
selectedElements: ElementAttributes[];
64-
open: boolean;
6563
onClose: () => void;
6664
openDialog: string;
6765
setOpenDialog: (dialogId: string) => void;
6866
broadcastChannel: BroadcastChannel;
69-
anchorReference?: PopoverReference;
70-
anchorPosition?: PopoverPosition;
7167
}
7268

7369
export default function ContentContextualMenu(props: Readonly<ContentContextualMenuProps>) {

0 commit comments

Comments
 (0)