@gridsuite/commons-ui 0.117.0 → 0.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -180,27 +180,27 @@ function DirectoryItemSelector({
180
180
  const fetchDirectoryChildren = useCallback(
181
181
  (nodeId) => {
182
182
  const typeList = types.includes(ElementType.DIRECTORY) ? [] : types;
183
- fetchDirectoryContent(nodeId, typeList).then((children) => {
183
+ return fetchDirectoryContent(nodeId, typeList).then((children) => {
184
184
  const childrenMatchedTypes = children.filter(
185
185
  (item) => contentFilter().has(item.type)
186
186
  );
187
187
  if (childrenMatchedTypes.length > 0 && equipmentTypes && equipmentTypes.length > 0) {
188
- fetchElementsInfos(
188
+ return fetchElementsInfos(
189
189
  childrenMatchedTypes.map((e) => e.elementUuid),
190
190
  types,
191
191
  equipmentTypes
192
192
  ).then((childrenWithMetadata) => {
193
- const filtredChildren = itemFilter ? childrenWithMetadata.filter((val) => {
193
+ const filteredChildren = itemFilter ? childrenWithMetadata.filter((val) => {
194
194
  if (val.type === ElementType.DIRECTORY) {
195
195
  return true;
196
196
  }
197
197
  return itemFilter(val);
198
198
  }) : childrenWithMetadata;
199
- addToDirectory(nodeId, filtredChildren);
199
+ addToDirectory(nodeId, filteredChildren);
200
200
  });
201
- } else {
202
- addToDirectory(nodeId, childrenMatchedTypes);
203
201
  }
202
+ addToDirectory(nodeId, childrenMatchedTypes);
203
+ return Promise.resolve();
204
204
  }).catch((error) => {
205
205
  console.warn(`Could not update subs (and content) of '${nodeId}' : ${error.message}`);
206
206
  });
@@ -208,13 +208,11 @@ function DirectoryItemSelector({
208
208
  [types, equipmentTypes, itemFilter, contentFilter, addToDirectory]
209
209
  );
210
210
  const fetchNodeChildrenIfNeeded = useCallback(
211
- (nodeId, delay = 0) => {
212
- setTimeout(() => {
213
- const node = nodeMap.current[nodeId];
214
- if (node && (!node.children || node.children.length === 0) && node.type === ElementType.DIRECTORY) {
215
- fetchDirectoryChildren(nodeId);
216
- }
217
- }, delay);
211
+ async (nodeId) => {
212
+ const node = nodeMap.current[nodeId];
213
+ if (node && (!node.children || node.children.length === 0) && node.type === ElementType.DIRECTORY) {
214
+ await fetchDirectoryChildren(nodeId);
215
+ }
218
216
  },
219
217
  [fetchDirectoryChildren]
220
218
  );
@@ -224,15 +222,15 @@ function DirectoryItemSelector({
224
222
  }
225
223
  const expandedArray = await getExpansionPathsForSelected(selected, expanded);
226
224
  setAutoExpandedNodes(expandedArray);
227
- fetchChildrenForExpandedNodes(expandedArray, fetchNodeChildrenIfNeeded);
225
+ await fetchChildrenForExpandedNodes(expandedArray, fetchNodeChildrenIfNeeded);
228
226
  return true;
229
227
  }, [selected, expanded, fetchNodeChildrenIfNeeded]);
230
- const handleProvidedExpansion = useCallback(() => {
228
+ const handleProvidedExpansion = useCallback(async () => {
231
229
  if (!expanded || expanded.length === 0) {
232
230
  return false;
233
231
  }
234
232
  setAutoExpandedNodes(expanded);
235
- fetchChildrenForExpandedNodes(expanded, fetchNodeChildrenIfNeeded);
233
+ await fetchChildrenForExpandedNodes(expanded, fetchNodeChildrenIfNeeded);
236
234
  return true;
237
235
  }, [expanded, fetchNodeChildrenIfNeeded]);
238
236
  const handleLastSelectedExpansion = useCallback(async () => {
@@ -241,14 +239,18 @@ function DirectoryItemSelector({
241
239
  return false;
242
240
  }
243
241
  setAutoExpandedNodes(expandPath);
244
- fetchChildrenForExpandedNodes(expandPath, fetchNodeChildrenIfNeeded);
242
+ await fetchChildrenForExpandedNodes(expandPath, fetchNodeChildrenIfNeeded);
245
243
  return true;
246
244
  }, [fetchNodeChildrenIfNeeded]);
247
245
  const initializeExpansion = useCallback(async () => {
248
246
  const selectedSuccess = await handleSelectedExpansion();
249
- if (selectedSuccess) return;
250
- const expandedSuccess = handleProvidedExpansion();
251
- if (expandedSuccess) return;
247
+ if (selectedSuccess) {
248
+ return;
249
+ }
250
+ const expandedSuccess = await handleProvidedExpansion();
251
+ if (expandedSuccess) {
252
+ return;
253
+ }
252
254
  await handleLastSelectedExpansion();
253
255
  }, [handleSelectedExpansion, handleProvidedExpansion, handleLastSelectedExpansion]);
254
256
  useEffect(() => {
@@ -26,10 +26,14 @@ export declare function fetchDirectoryPathSafe(directoryId: UUID): Promise<Eleme
26
26
  */
27
27
  export declare function initializeFromLastSelected(): Promise<UUID[] | null>;
28
28
  /**
29
- * Fetches expansion paths for multiple selected items
29
+ * Fetches expansion paths for multiple selected items, collecting unique parent directories
30
+ * (excluding the selected items themselves) and sorting them by their minimum depth across
31
+ * all paths. This ensures parents appear before their descendants in the returned array,
32
+ * which is crucial for sequential fetching to avoid loading children before parents are
33
+ * fully populated in the node map.
30
34
  * @param selectedIds Array of selected item UUIDs
31
35
  * @param expanded Optional existing expanded nodes
32
- * @returns Promise resolving to combined expansion array
36
+ * @returns Promise resolving to combined expansion array sorted by depth
33
37
  */
34
38
  export declare function getExpansionPathsForSelected(selectedIds: UUID[], expanded?: UUID[]): Promise<UUID[]>;
35
39
  /**
@@ -44,9 +48,8 @@ export declare function saveLastSelectedDirectoryFromNode(node: {
44
48
  }>;
45
49
  }): Promise<void>;
46
50
  /**
47
- * Fetches children for expanded nodes with staggered delay to avoid overwhelming the server
51
+ * Fetches children for expanded nodes sequentially to ensure parent nodes are loaded before children
48
52
  * @param expandedNodes Array of node UUIDs to fetch children for
49
53
  * @param fetchChildrenCallback Function to fetch children for a single node
50
- * @param delayBetweenRequests Delay in milliseconds between requests (default: 100ms)
51
54
  */
52
- export declare function fetchChildrenForExpandedNodes(expandedNodes: UUID[], fetchChildrenCallback: (nodeId: UUID, delay: number) => void, delayBetweenRequests?: number): void;
55
+ export declare function fetchChildrenForExpandedNodes(expandedNodes: UUID[], fetchChildrenCallback: (nodeId: UUID) => Promise<void>): Promise<void>;
@@ -54,18 +54,23 @@ async function initializeFromLastSelected() {
54
54
  }
55
55
  async function getExpansionPathsForSelected(selectedIds, expanded = []) {
56
56
  const expandedSet = new Set(expanded);
57
- const fetchPromises = selectedIds.map(async (selectedId) => {
58
- const path = await fetchDirectoryPathSafe(selectedId);
59
- if (path && path.length > 0) {
60
- path.forEach((element, index) => {
61
- if (index < path.length - 1) {
62
- expandedSet.add(element.elementUuid);
57
+ const idToMinIndex = /* @__PURE__ */ new Map();
58
+ const paths = await Promise.all(selectedIds.map(fetchDirectoryPathSafe));
59
+ paths.filter((p) => !!p && p.length > 0).forEach((path) => {
60
+ path.forEach((element, index) => {
61
+ if (index < path.length - 1) {
62
+ const id = element.elementUuid;
63
+ expandedSet.add(id);
64
+ if (!idToMinIndex.has(id) || index < idToMinIndex.get(id)) {
65
+ idToMinIndex.set(id, index);
63
66
  }
64
- });
65
- }
67
+ }
68
+ });
66
69
  });
67
- await Promise.all(fetchPromises);
68
- return Array.from(expandedSet);
70
+ const expandedArray = Array.from(expandedSet).sort(
71
+ (a, b) => (idToMinIndex.get(a) ?? Infinity) - (idToMinIndex.get(b) ?? Infinity)
72
+ );
73
+ return expandedArray;
69
74
  }
70
75
  async function saveLastSelectedDirectoryFromNode(node) {
71
76
  let lastSelectedDirId;
@@ -78,10 +83,11 @@ async function saveLastSelectedDirectoryFromNode(node) {
78
83
  await saveLastSelectedDirectory(lastSelectedDirId);
79
84
  }
80
85
  }
81
- function fetchChildrenForExpandedNodes(expandedNodes, fetchChildrenCallback, delayBetweenRequests = 100) {
82
- expandedNodes.forEach((nodeId, index) => {
83
- fetchChildrenCallback(nodeId, index * delayBetweenRequests);
84
- });
86
+ async function fetchChildrenForExpandedNodes(expandedNodes, fetchChildrenCallback) {
87
+ await expandedNodes.reduce(async (promise, nodeId) => {
88
+ await promise;
89
+ return fetchChildrenCallback(nodeId);
90
+ }, Promise.resolve());
85
91
  }
86
92
  export {
87
93
  clearLastSelectedDirectory,
@@ -23,6 +23,7 @@ interface DndTableProps {
23
23
  uploadButtonMessageId?: string;
24
24
  handleResetButton?: () => void;
25
25
  resetButtonMessageId?: string;
26
+ maxRows?: number;
26
27
  }
27
28
  export declare function DndTable(props: Readonly<DndTableProps>): import("react/jsx-runtime").JSX.Element;
28
29
  export {};
@@ -5,7 +5,7 @@ import { Grid, TableContainer, Table, TableHead, TableRow, TableCell, Box, Table
5
5
  import { DragIndicator } from "@mui/icons-material";
6
6
  import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
7
7
  import { useIntl } from "react-intl";
8
- import { SELECTED, MAX_ROWS_NUMBER, DndColumnType } from "./dnd-table.type.js";
8
+ import { MAX_ROWS_NUMBER, SELECTED, DndColumnType } from "./dnd-table.type.js";
9
9
  import { DndTableBottomLeftButtons } from "./dnd-table-bottom-left-buttons.js";
10
10
  import { DndTableBottomRightButtons } from "./dnd-table-bottom-right-buttons.js";
11
11
  import { DndTableAddRowsDialog } from "./dnd-table-add-rows-dialog.js";
@@ -29,9 +29,8 @@ import "localized-countries/data/fr";
29
29
  import "localized-countries/data/en";
30
30
  import { TableNumericalInput } from "../inputs/reactHookForm/tableInputs/table-numerical-input.js";
31
31
  import { TableTextInput } from "../inputs/reactHookForm/tableInputs/table-text-input.js";
32
- import "../../utils/types/equipmentType.js";
33
- import "notistack";
34
32
  import "../../utils/conversionUtils.js";
33
+ import "../../utils/types/equipmentType.js";
35
34
  import "../../utils/yupConfig.js";
36
35
  import "@react-querybuilder/material";
37
36
  import "../filter/expert/expertFilterConstants.js";
@@ -163,7 +162,8 @@ function DndTable(props) {
163
162
  handleUploadButton = void 0,
164
163
  uploadButtonMessageId = void 0,
165
164
  handleResetButton = void 0,
166
- resetButtonMessageId = void 0
165
+ resetButtonMessageId = void 0,
166
+ maxRows = MAX_ROWS_NUMBER
167
167
  } = props;
168
168
  const intl = useIntl();
169
169
  const { getValues, setValue, setError, clearErrors } = useFormContext();
@@ -192,7 +192,7 @@ function DndTable(props) {
192
192
  );
193
193
  }
194
194
  const addNewRows = (numberOfRows) => {
195
- if (currentRows.length + numberOfRows > MAX_ROWS_NUMBER) {
195
+ if (currentRows.length + numberOfRows > maxRows) {
196
196
  setError(arrayFormName, {
197
197
  type: "custom",
198
198
  message: intl.formatMessage(
@@ -200,7 +200,7 @@ function DndTable(props) {
200
200
  id: "MaximumRowNumberError"
201
201
  },
202
202
  {
203
- value: MAX_ROWS_NUMBER
203
+ value: maxRows
204
204
  }
205
205
  )
206
206
  });
@@ -14,6 +14,6 @@ export interface UniqueNameInputProps {
14
14
  fullWidth?: boolean;
15
15
  }
16
16
  /**
17
- * Input component that constantly check if the field's value is available or not
17
+ * Input component that constantly checks if the field's value is available or not
18
18
  */
19
19
  export declare function UniqueNameInput({ name, label, elementType, autoFocus, onManualChangeCallback, formProps, currentName, activeDirectory, sx, fullWidth, }: Readonly<UniqueNameInputProps>): import("react/jsx-runtime").JSX.Element;
@@ -1,19 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect } from "react";
3
2
  import { FormattedMessage } from "react-intl";
4
3
  import { TextField, InputAdornment, CircularProgress } from "@mui/material";
5
4
  import { Check } from "@mui/icons-material";
6
- import { useController, useFormContext } from "react-hook-form";
7
- import "../../../../utils/types/equipmentType.js";
8
- import { elementAlreadyExists } from "../../../../services/directory.js";
9
- import { useDebounce } from "../../../../hooks/useDebounce.js";
10
- import "localized-countries";
11
- import "localized-countries/data/fr";
12
- import "localized-countries/data/en";
13
- import "notistack";
14
- import { FieldConstants } from "../../../../utils/constants/fieldConstants.js";
15
- import "../../../../utils/conversionUtils.js";
16
- import "../../../../utils/yupConfig.js";
5
+ import { useController } from "react-hook-form";
6
+ import { useUniqueNameValidation } from "../../../../hooks/use-unique-name-validation.js";
17
7
  function UniqueNameInput({
18
8
  name,
19
9
  label,
@@ -26,77 +16,18 @@ function UniqueNameInput({
26
16
  sx,
27
17
  fullWidth = true
28
18
  }) {
29
- var _a;
30
19
  const {
31
20
  field: { onChange, onBlur, value, ref },
32
- fieldState: { error, isDirty }
21
+ fieldState: { error }
33
22
  } = useController({
34
23
  name
35
24
  });
36
- const {
37
- field: { value: selectedDirectory }
38
- } = useController({
39
- name: FieldConstants.DIRECTORY
25
+ const { isValidating } = useUniqueNameValidation({
26
+ name,
27
+ currentName,
28
+ elementType,
29
+ activeDirectory
40
30
  });
41
- const {
42
- setError,
43
- clearErrors,
44
- trigger,
45
- formState: { errors }
46
- } = useFormContext();
47
- const isValidating = (_a = errors.root) == null ? void 0 : _a.isValidating;
48
- const directory = selectedDirectory || activeDirectory;
49
- const handleCheckName = useCallback(
50
- (nameValue) => {
51
- if (nameValue !== currentName && directory) {
52
- elementAlreadyExists(directory, nameValue, elementType).then((alreadyExist) => {
53
- if (alreadyExist) {
54
- setError(name, {
55
- type: "validate",
56
- message: "nameAlreadyUsed"
57
- });
58
- }
59
- }).catch((e) => {
60
- setError(name, {
61
- type: "validate",
62
- message: "nameValidityCheckErrorMsg"
63
- });
64
- console.error(e == null ? void 0 : e.message);
65
- }).finally(() => {
66
- clearErrors("root.isValidating");
67
- trigger("root.isValidating");
68
- });
69
- } else {
70
- trigger("root.isValidating");
71
- }
72
- },
73
- [currentName, directory, elementType, setError, name, clearErrors, trigger]
74
- );
75
- const debouncedHandleCheckName = useDebounce(handleCheckName, 700);
76
- useEffect(() => {
77
- const trimmedValue = value.trim();
78
- if (selectedDirectory) {
79
- debouncedHandleCheckName(trimmedValue);
80
- }
81
- if (!isDirty) {
82
- clearErrors(name);
83
- return;
84
- }
85
- if (trimmedValue) {
86
- clearErrors(name);
87
- setError("root.isValidating", {
88
- type: "validate",
89
- message: "cantSubmitWhileValidating"
90
- });
91
- debouncedHandleCheckName(trimmedValue);
92
- } else {
93
- clearErrors("root.isValidating");
94
- setError(name, {
95
- type: "validate",
96
- message: "nameEmpty"
97
- });
98
- }
99
- }, [debouncedHandleCheckName, setError, clearErrors, name, value, isDirty, selectedDirectory]);
100
31
  const handleManualChange = (e) => {
101
32
  onChange(e.target.value);
102
33
  onManualChangeCallback == null ? void 0 : onManualChangeCallback();
@@ -8,12 +8,8 @@ import "@mui/icons-material";
8
8
  import "react-hook-form";
9
9
  import "../../../inputs/reactHookForm/provider/CustomFormProvider.js";
10
10
  import * as yup from "yup";
11
- import "../../../../utils/types/equipmentType.js";
12
- import "localized-countries";
13
- import "localized-countries/data/fr";
14
- import "localized-countries/data/en";
15
- import "notistack";
16
11
  import "../../../../utils/conversionUtils.js";
12
+ import "../../../../utils/types/equipmentType.js";
17
13
  import "../../../filter/HeaderFilterForm.js";
18
14
  import { getNameElementEditorSchema } from "../name-element-editor/name-element-editor-utils.js";
19
15
  const LIMIT_REDUCTIONS_FORM = "limitReductionsForm";
@@ -0,0 +1,16 @@
1
+ import { UUID } from 'crypto';
2
+ import { ElementType } from '../utils';
3
+ interface UseUniqueNameValidationParams {
4
+ name: string;
5
+ currentName?: string;
6
+ elementType: ElementType;
7
+ activeDirectory?: string;
8
+ elementExists?: (directory: UUID, name: string, type: ElementType) => Promise<boolean>;
9
+ }
10
+ export declare function useUniqueNameValidation({ name, currentName, elementType, activeDirectory, elementExists, }: UseUniqueNameValidationParams): {
11
+ isValidating: Partial<{
12
+ type: string | number;
13
+ message: import('react-hook-form').Message;
14
+ }> | undefined;
15
+ };
16
+ export {};
@@ -0,0 +1,92 @@
1
+ import { useCallback, useEffect } from "react";
2
+ import { useFormContext, useController } from "react-hook-form";
3
+ import { FieldConstants } from "../utils/constants/fieldConstants.js";
4
+ import "../utils/conversionUtils.js";
5
+ import "react/jsx-runtime";
6
+ import "@mui/icons-material";
7
+ import "../utils/types/equipmentType.js";
8
+ import "../utils/yupConfig.js";
9
+ import { useDebounce } from "./useDebounce.js";
10
+ import { elementAlreadyExists } from "../services/directory.js";
11
+ function useUniqueNameValidation({
12
+ name,
13
+ currentName = "",
14
+ elementType,
15
+ activeDirectory,
16
+ elementExists = elementAlreadyExists
17
+ }) {
18
+ var _a;
19
+ const {
20
+ setError,
21
+ clearErrors,
22
+ trigger,
23
+ formState: { errors, defaultValues }
24
+ } = useFormContext();
25
+ const {
26
+ field: { value },
27
+ fieldState: { isDirty }
28
+ } = useController({ name });
29
+ const {
30
+ field: { value: selectedDirectory }
31
+ } = useController({
32
+ name: FieldConstants.DIRECTORY
33
+ });
34
+ const defaultFieldValue = defaultValues == null ? void 0 : defaultValues[name];
35
+ const directory = selectedDirectory || activeDirectory;
36
+ const isValidating = (_a = errors.root) == null ? void 0 : _a.isValidating;
37
+ const handleCheckName = useCallback(
38
+ (nameValue) => {
39
+ if (nameValue !== currentName && directory) {
40
+ elementExists(directory, nameValue, elementType).then((alreadyExist) => {
41
+ if (alreadyExist) {
42
+ setError(name, {
43
+ type: "validate",
44
+ message: "nameAlreadyUsed"
45
+ });
46
+ }
47
+ }).catch(() => {
48
+ setError(name, {
49
+ type: "validate",
50
+ message: "nameValidityCheckErrorMsg"
51
+ });
52
+ }).finally(() => {
53
+ clearErrors("root.isValidating");
54
+ trigger("root.isValidating");
55
+ });
56
+ } else {
57
+ trigger("root.isValidating");
58
+ }
59
+ },
60
+ [currentName, directory, elementType, name, setError, clearErrors, trigger, elementExists]
61
+ );
62
+ const debouncedHandleCheckName = useDebounce(handleCheckName, 700);
63
+ useEffect(() => {
64
+ var _a2, _b;
65
+ const trimmedValue = (_a2 = value == null ? void 0 : value.trim) == null ? void 0 : _a2.call(value);
66
+ if (!trimmedValue) {
67
+ clearErrors("root.isValidating");
68
+ setError(name, {
69
+ type: "validate",
70
+ message: "nameEmpty"
71
+ });
72
+ return;
73
+ }
74
+ if (!isDirty || ((_b = defaultFieldValue == null ? void 0 : defaultFieldValue.trim) == null ? void 0 : _b.call(defaultFieldValue)) === trimmedValue) {
75
+ clearErrors(name);
76
+ return;
77
+ }
78
+ setError("root.isValidating", {
79
+ type: "validate",
80
+ message: "cantSubmitWhileValidating"
81
+ });
82
+ if (directory) {
83
+ debouncedHandleCheckName(trimmedValue);
84
+ }
85
+ }, [value, debouncedHandleCheckName, setError, clearErrors, name, isDirty, defaultFieldValue, directory]);
86
+ return {
87
+ isValidating
88
+ };
89
+ }
90
+ export {
91
+ useUniqueNameValidation
92
+ };
@@ -54,6 +54,7 @@ const useModificationLabelComputer = () => {
54
54
  });
55
55
  case MODIFICATION_TYPES.CREATE_COUPLING_DEVICE.type:
56
56
  case MODIFICATION_TYPES.CREATE_VOLTAGE_LEVEL_TOPOLOGY.type:
57
+ case MODIFICATION_TYPES.CREATE_VOLTAGE_LEVEL_SECTION.type:
57
58
  return modificationMetadata.voltageLevelId;
58
59
  default:
59
60
  return modificationMetadata.equipmentId || "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gridsuite/commons-ui",
3
- "version": "0.117.0",
3
+ "version": "0.118.0",
4
4
  "description": "common react components for gridsuite applications",
5
5
  "author": "gridsuite team",
6
6
  "homepage": "https://github.com/gridsuite",