Skip to content

Commit 17b2940

Browse files
authored
Implement last selected directory functionality (#814)
Signed-off-by: achour94 <[email protected]>
1 parent 3c70021 commit 17b2940

File tree

7 files changed

+398
-47
lines changed

7 files changed

+398
-47
lines changed

src/components/dialogs/elementSaveDialog/ElementSaveDialog.tsx

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { DirectoryItemSelector } from '../../directoryItemSelector';
1818
import { CustomMuiDialog } from '../customMuiDialog/CustomMuiDialog';
1919
import { ElementAttributes, ElementType, FieldConstants, MAX_CHAR_DESCRIPTION } from '../../../utils';
2020
import { useSnackMessage } from '../../../hooks';
21-
import { fetchDirectoryElementPath } from '../../../services';
21+
import { DirectoryInitConfig, initializeDirectory } from './utils';
2222

2323
// Define operation types
2424
enum OperationType {
@@ -131,6 +131,7 @@ export function ElementSaveDialog({
131131
const [selectedItem, setSelectedItem] = useState<
132132
TreeViewFinderNodeProps & { parentFolderId: UUID; fullPath: string }
133133
>();
134+
const [expanded, setExpanded] = useState<UUID[]>([]);
134135

135136
// Form handling with conditional defaultValues
136137
const formMethods = useForm({
@@ -155,9 +156,42 @@ export function ElementSaveDialog({
155156
const disableSave =
156157
Object.keys(errors).length > 0 || (isCreateMode && !destinationFolder) || (!isCreateMode && !selectedItem);
157158

159+
const setDestinationFolderWithPath = useCallback(
160+
(elementUuid: UUID, elementName: string, path?: ElementAttributes[]) => {
161+
setDestinationFolder({
162+
id: elementUuid,
163+
name: elementName,
164+
});
165+
166+
if (path && path.length > 0) {
167+
// Set expanded path to show the selected directory
168+
const expandPath = path.map((element) => element.elementUuid);
169+
setExpanded(expandPath);
170+
}
171+
},
172+
[]
173+
);
174+
175+
const initializeDestinationFolder = useCallback(async () => {
176+
const config: DirectoryInitConfig = {
177+
studyUuid,
178+
initDirectory,
179+
onError: (messageTxt, headerId) => {
180+
snackError({ messageTxt, headerId });
181+
},
182+
};
183+
184+
const result = await initializeDirectory(config);
185+
186+
if (result) {
187+
setDestinationFolderWithPath(result.element.elementUuid, result.element.elementName, result.path);
188+
}
189+
}, [studyUuid, initDirectory, snackError, setDestinationFolderWithPath]);
190+
158191
// Handle cancellation
159192
const onCancel = useCallback(() => {
160193
reset({ ...emptyFormData, [FieldConstants.OPERATION_TYPE]: initialOperation });
194+
setExpanded([]);
161195
onClose();
162196
}, [onClose, reset, initialOperation]);
163197

@@ -179,36 +213,14 @@ export function ElementSaveDialog({
179213
}
180214
}, [prefixIdForGeneratedName, intl, reset, isCreateMode]);
181215

182-
// Fetch study directory for creation if needed
216+
// Destination folder initialization for create mode
183217
useEffect(() => {
184-
if (open && isCreateMode && studyUuid) {
185-
fetchDirectoryElementPath(studyUuid).then((res) => {
186-
if (!res || res.length < 2) {
187-
snackError({
188-
messageTxt: 'unknown study directory',
189-
headerId: 'studyDirectoryFetchingError',
190-
});
191-
return;
192-
}
193-
const parentFolderIndex = res.length - 2;
194-
const { elementUuid, elementName } = res[parentFolderIndex];
195-
setDestinationFolder({
196-
id: elementUuid,
197-
name: elementName,
198-
});
199-
});
218+
if (!open || !isCreateMode) {
219+
return;
200220
}
201-
}, [studyUuid, open, snackError, isCreateMode]);
202221

203-
// Set initial directory for creation if provided
204-
useEffect(() => {
205-
if (open && isCreateMode && initDirectory) {
206-
setDestinationFolder({
207-
id: initDirectory.elementUuid,
208-
name: initDirectory.elementName,
209-
});
210-
}
211-
}, [initDirectory, open, isCreateMode]);
222+
initializeDestinationFolder();
223+
}, [open, isCreateMode, initializeDestinationFolder]);
212224

213225
// Open selector dialog
214226
const handleChangeFolder = useCallback(() => {
@@ -223,6 +235,7 @@ export function ElementSaveDialog({
223235
if (items?.length > 0 && items[0].id !== destinationFolder?.id) {
224236
const { id, name } = items[0];
225237
setDestinationFolder({ id, name });
238+
setExpanded([]);
226239
}
227240
} else if (items?.length > 0 && items[0].id !== selectedItem?.id) {
228241
// Handle item selection for update
@@ -357,6 +370,7 @@ export function ElementSaveDialog({
357370
types={isCreateMode ? [ElementType.DIRECTORY] : [type]}
358371
onlyLeaves={isCreateMode ? false : undefined}
359372
multiSelect={false}
373+
expanded={isCreateMode ? expanded : []}
360374
validationButtonText={intl.formatMessage({
361375
id: 'validate',
362376
})}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Copyright (c) 2025, 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 { UUID } from 'crypto';
9+
import { ElementAttributes } from '../../../utils';
10+
import {
11+
clearLastSelectedDirectory,
12+
fetchDirectoryPathSafe,
13+
getLastSelectedDirectoryId,
14+
} from '../../directoryItemSelector/utils';
15+
16+
/**
17+
* Generic directory initialization configuration
18+
*/
19+
export interface DirectoryInitConfig {
20+
studyUuid?: UUID;
21+
initDirectory?: ElementAttributes;
22+
onError?: (message: string, headerId: string) => void;
23+
}
24+
25+
/**
26+
* Generic destination directory initialization that follows the standard priority:
27+
* 1. Last selected directory from localStorage
28+
* 2. Study UUID (if provided)
29+
* 3. Initial directory (if provided)
30+
*
31+
* @param config Configuration object
32+
* @returns Promise resolving to { element, path } or null if all methods fail
33+
*/
34+
export async function initializeDirectory(
35+
config: DirectoryInitConfig
36+
): Promise<{ element: ElementAttributes; path?: ElementAttributes[] } | null> {
37+
const { studyUuid, initDirectory, onError } = config;
38+
39+
// Priority 1: Try last selected directory from localStorage
40+
const lastSelectedDirId = getLastSelectedDirectoryId();
41+
if (lastSelectedDirId) {
42+
const path = await fetchDirectoryPathSafe(lastSelectedDirId);
43+
if (path && path.length > 0) {
44+
const targetElement = path[path.length - 1];
45+
return { element: targetElement, path };
46+
}
47+
// Clear invalid last selected directory
48+
await clearLastSelectedDirectory();
49+
}
50+
51+
// Priority 2: Try study UUID
52+
if (studyUuid) {
53+
const path = await fetchDirectoryPathSafe(studyUuid);
54+
if (path && path.length >= 2) {
55+
const parentFolderIndex = path.length - 2;
56+
const parentElement = path[parentFolderIndex];
57+
return {
58+
element: parentElement,
59+
path: path.slice(0, parentFolderIndex + 1),
60+
};
61+
}
62+
onError?.('unknown study directory', 'studyDirectoryFetchingError');
63+
return null;
64+
}
65+
66+
// Priority 3: Try initial directory
67+
if (initDirectory) {
68+
return { element: initDirectory };
69+
}
70+
71+
return null;
72+
}

src/components/directoryItemSelector/DirectoryItemSelector.tsx

Lines changed: 106 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import { TreeViewFinder, TreeViewFinderNodeProps, TreeViewFinderProps } from '..
1414
import { useSnackMessage } from '../../hooks/useSnackMessage';
1515
import { fetchDirectoryContent, fetchElementsInfos, fetchRootFolders } from '../../services';
1616
import { ElementAttributes } from '../../utils';
17+
import {
18+
fetchChildrenForExpandedNodes,
19+
getExpansionPathsForSelected,
20+
initializeFromLastSelected,
21+
saveLastSelectedDirectoryFromNode,
22+
} from './utils';
1723

1824
const styles = {
1925
icon: (theme: Theme) => ({
@@ -173,10 +179,13 @@ export function DirectoryItemSelector({
173179
itemFilter,
174180
expanded,
175181
selected,
182+
onClose,
176183
...otherTreeViewFinderProps
177184
}: Readonly<DirectoryItemSelectorProps>) {
178185
const [data, setData] = useState<TreeViewFinderNodeProps[]>([]);
179186
const [rootDirectories, setRootDirectories] = useState<ElementAttributes[]>([]);
187+
const [isRootsLoaded, setIsRootsLoaded] = useState(false);
188+
const [autoExpandedNodes, setAutoExpandedNodes] = useState<UUID[]>([]);
180189
const nodeMap = useRef<Record<UUID, ElementAttributes>>({});
181190
const dataRef = useRef<TreeViewFinderNodeProps[]>([]);
182191
dataRef.current = data;
@@ -231,6 +240,7 @@ export function DirectoryItemSelector({
231240
);
232241

233242
const updateRootDirectories = useCallback(() => {
243+
setIsRootsLoaded(false);
234244
fetchRootFolders(types)
235245
.then((newData) => {
236246
const [nrs, mdr] = updatedTree(rootsRef.current, nodeMap.current, null, newData);
@@ -243,6 +253,9 @@ export function DirectoryItemSelector({
243253
messageTxt: error.message,
244254
headerId: 'DirectoryItemSelector',
245255
});
256+
})
257+
.finally(() => {
258+
setIsRootsLoaded(true);
246259
});
247260
}, [convertRoots, types, snackError]);
248261

@@ -286,39 +299,113 @@ export function DirectoryItemSelector({
286299
[types, equipmentTypes, itemFilter, contentFilter, addToDirectory]
287300
);
288301

289-
// In this useEffect, we fetch the path (expanded array) of every selected node
290-
useEffect(() => {
291-
if (open && expanded && selected) {
292-
// we check if every selected item is already fetched
293-
const isSelectedItemFetched = selected.every((id) => nodeMap.current[id]);
294-
if (!isSelectedItemFetched) {
295-
expanded.forEach((nodeId) => {
296-
const node = nodeMap.current[nodeId];
297-
// we check that the node exist before fetching the children
298-
// And we check if there is already children (Because we are trying to reach a selected element, we know every node has at least one child)
299-
if (node?.children && node.children.length === 0) {
300-
fetchDirectoryChildren(nodeId);
301-
}
302-
});
303-
}
302+
// Helper function to fetch children for a node if not already loaded
303+
const fetchNodeChildrenIfNeeded = useCallback(
304+
(nodeId: UUID, delay: number = 0) => {
305+
setTimeout(() => {
306+
const node = nodeMap.current[nodeId];
307+
if (node && (!node.children || node.children.length === 0) && node.type === ElementType.DIRECTORY) {
308+
fetchDirectoryChildren(nodeId);
309+
}
310+
}, delay);
311+
},
312+
[fetchDirectoryChildren]
313+
);
314+
315+
// Handle expansion from selected items
316+
const handleSelectedExpansion = useCallback(async (): Promise<boolean> => {
317+
if (!selected || selected.length === 0) {
318+
return false;
304319
}
305-
}, [open, expanded, fetchDirectoryChildren, selected, data]);
306320

321+
const expandedArray = await getExpansionPathsForSelected(selected, expanded);
322+
setAutoExpandedNodes(expandedArray);
323+
fetchChildrenForExpandedNodes(expandedArray, fetchNodeChildrenIfNeeded);
324+
return true;
325+
}, [selected, expanded, fetchNodeChildrenIfNeeded]);
326+
327+
// Handle expansion from provided expanded prop
328+
const handleProvidedExpansion = useCallback((): boolean => {
329+
if (!expanded || expanded.length === 0) {
330+
return false;
331+
}
332+
333+
setAutoExpandedNodes(expanded);
334+
fetchChildrenForExpandedNodes(expanded, fetchNodeChildrenIfNeeded);
335+
336+
return true;
337+
}, [expanded, fetchNodeChildrenIfNeeded]);
338+
339+
// Handle expansion from last selected directory
340+
const handleLastSelectedExpansion = useCallback(async (): Promise<boolean> => {
341+
const expandPath = await initializeFromLastSelected();
342+
343+
if (!expandPath) {
344+
return false;
345+
}
346+
347+
setAutoExpandedNodes(expandPath);
348+
fetchChildrenForExpandedNodes(expandPath, fetchNodeChildrenIfNeeded);
349+
350+
return true;
351+
}, [fetchNodeChildrenIfNeeded]);
352+
353+
// Main expansion orchestrator
354+
const initializeExpansion = useCallback(async () => {
355+
// Priority 1: Handle selected items
356+
const selectedSuccess = await handleSelectedExpansion();
357+
if (selectedSuccess) return;
358+
359+
// Priority 2: Handle provided expanded items
360+
const expandedSuccess = handleProvidedExpansion();
361+
if (expandedSuccess) return;
362+
363+
// Priority 3: Fall back to last selected directory
364+
await handleLastSelectedExpansion();
365+
}, [handleSelectedExpansion, handleProvidedExpansion, handleLastSelectedExpansion]);
366+
367+
// Handle root loading and expansion initialization
307368
useEffect(() => {
308-
if (open) {
369+
if (!open) {
370+
setIsRootsLoaded(false);
371+
setAutoExpandedNodes([]);
372+
return;
373+
}
374+
375+
// Phase 1: Load root directories if not already loaded
376+
if (!isRootsLoaded) {
309377
updateRootDirectories();
378+
return;
310379
}
311-
}, [open, updateRootDirectories]);
380+
381+
// Phase 2: Initialize expansion once roots are loaded
382+
initializeExpansion();
383+
}, [open, isRootsLoaded, updateRootDirectories, initializeExpansion]);
384+
385+
const handleClose = useCallback(
386+
(nodes: TreeViewFinderNodeProps[]) => {
387+
if (nodes && nodes.length > 0) {
388+
const lastSelectedNode = nodes[0];
389+
saveLastSelectedDirectoryFromNode(lastSelectedNode);
390+
}
391+
392+
setAutoExpandedNodes([]);
393+
394+
onClose(nodes);
395+
},
396+
[onClose]
397+
);
312398

313399
return (
314400
<TreeViewFinder
315401
onTreeBrowse={fetchDirectoryChildren as (NodeId: string) => void}
316402
sortMethod={sortHandlingDirectories}
317403
multiSelect // defaulted to true
318404
open={open}
319-
expanded={expanded as string[]}
405+
expanded={autoExpandedNodes}
320406
onlyLeaves // defaulted to true
321407
selected={selected}
408+
onClose={handleClose}
322409
{...otherTreeViewFinderProps}
323410
data={data}
324411
/>

0 commit comments

Comments
 (0)