@finos/legend-application-studio 28.19.69 → 28.19.70

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.
Files changed (22) hide show
  1. package/lib/components/editor/editor-group/project-configuration-editor/ProjectDependencyEditor.d.ts.map +1 -1
  2. package/lib/components/editor/editor-group/project-configuration-editor/ProjectDependencyEditor.js +338 -26
  3. package/lib/components/editor/editor-group/project-configuration-editor/ProjectDependencyEditor.js.map +1 -1
  4. package/lib/index.css +2 -2
  5. package/lib/index.css.map +1 -1
  6. package/lib/package.json +1 -1
  7. package/lib/stores/editor/EditorGraphState.d.ts.map +1 -1
  8. package/lib/stores/editor/EditorGraphState.js +7 -1
  9. package/lib/stores/editor/EditorGraphState.js.map +1 -1
  10. package/lib/stores/editor/editor-state/project-configuration-editor-state/ProjectConfigurationEditorState.d.ts +2 -0
  11. package/lib/stores/editor/editor-state/project-configuration-editor-state/ProjectConfigurationEditorState.d.ts.map +1 -1
  12. package/lib/stores/editor/editor-state/project-configuration-editor-state/ProjectConfigurationEditorState.js +19 -0
  13. package/lib/stores/editor/editor-state/project-configuration-editor-state/ProjectConfigurationEditorState.js.map +1 -1
  14. package/lib/stores/editor/editor-state/project-configuration-editor-state/ProjectDependencyEditorState.d.ts +16 -1
  15. package/lib/stores/editor/editor-state/project-configuration-editor-state/ProjectDependencyEditorState.d.ts.map +1 -1
  16. package/lib/stores/editor/editor-state/project-configuration-editor-state/ProjectDependencyEditorState.js +112 -1
  17. package/lib/stores/editor/editor-state/project-configuration-editor-state/ProjectDependencyEditorState.js.map +1 -1
  18. package/package.json +5 -5
  19. package/src/components/editor/editor-group/project-configuration-editor/ProjectDependencyEditor.tsx +599 -53
  20. package/src/stores/editor/EditorGraphState.ts +11 -4
  21. package/src/stores/editor/editor-state/project-configuration-editor-state/ProjectConfigurationEditorState.ts +26 -0
  22. package/src/stores/editor/editor-state/project-configuration-editor-state/ProjectDependencyEditorState.ts +178 -1
@@ -61,7 +61,10 @@ import {
61
61
  type StoreProjectData,
62
62
  SNAPSHOT_VERSION_ALIAS,
63
63
  } from '@finos/legend-server-depot';
64
- import type { ProjectDependency } from '@finos/legend-server-sdlc';
64
+ import {
65
+ type ProjectDependency,
66
+ type ProjectDependencyExclusion,
67
+ } from '@finos/legend-server-sdlc';
65
68
  import {
66
69
  ActionState,
67
70
  assertErrorThrown,
@@ -74,7 +77,7 @@ import {
74
77
  import { generateGAVCoordinates } from '@finos/legend-storage';
75
78
  import { flowResult } from 'mobx';
76
79
  import { observer } from 'mobx-react-lite';
77
- import { forwardRef, useEffect, useRef, useState } from 'react';
80
+ import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
78
81
  import { ProjectConfigurationEditorState } from '../../../../stores/editor/editor-state/project-configuration-editor-state/ProjectConfigurationEditorState.js';
79
82
  import {
80
83
  type ProjectDependencyConflictTreeNodeData,
@@ -250,9 +253,117 @@ const DependencyTreeNodeContainer: React.FC<
250
253
  >
251
254
  > = (props) => {
252
255
  const { node, level, stepPaddingInRem, onNodeSelect } = props;
256
+ const editorStore = useEditorStore();
257
+ const dependencyEditorState =
258
+ editorStore.projectConfigurationEditorState.projectDependencyEditorState;
259
+
253
260
  const isExpandable = Boolean(node.childrenIds?.length);
254
261
  const selectNode = (): void => onNodeSelect?.(node);
255
262
  const value = node.value;
263
+
264
+ const isExcluded = (): boolean => {
265
+ const coordinate = generateGAVCoordinates(
266
+ value.groupId,
267
+ value.artifactId,
268
+ undefined,
269
+ );
270
+
271
+ const treeData = dependencyEditorState.dependencyTreeData;
272
+ if (!treeData) {
273
+ return false;
274
+ }
275
+
276
+ const findRootNodeForCurrentNode = (nodeId: string): string | null => {
277
+ if (treeData.rootIds.indexOf(nodeId) !== -1) {
278
+ return nodeId;
279
+ }
280
+
281
+ const findInSubtree = (rootId: string, searchId: string): boolean => {
282
+ const rootNode = treeData.nodes.get(rootId);
283
+ if (!rootNode) {
284
+ return false;
285
+ }
286
+ const visited: { [key: string]: boolean } = {};
287
+ const queue = [rootId];
288
+
289
+ while (queue.length > 0) {
290
+ const currentId = queue.shift();
291
+ if (!currentId || visited[currentId]) {
292
+ continue;
293
+ }
294
+
295
+ visited[currentId] = true;
296
+
297
+ if (currentId === searchId) {
298
+ return true;
299
+ }
300
+
301
+ const currentNode = treeData.nodes.get(currentId);
302
+ if (currentNode?.childrenIds) {
303
+ for (let i = 0; i < currentNode.childrenIds.length; i++) {
304
+ queue.push(guaranteeNonNullable(currentNode.childrenIds[i]));
305
+ }
306
+ }
307
+ }
308
+ return false;
309
+ };
310
+
311
+ for (let i = 0; i < treeData.rootIds.length; i++) {
312
+ const rootId = guaranteeNonNullable(treeData.rootIds[i]);
313
+ if (findInSubtree(rootId, nodeId)) {
314
+ return rootId;
315
+ }
316
+ }
317
+
318
+ return null;
319
+ };
320
+
321
+ const rootNodeId = findRootNodeForCurrentNode(node.id);
322
+ if (!rootNodeId) {
323
+ return false;
324
+ }
325
+
326
+ const rootNode = treeData.nodes.get(rootNodeId);
327
+ if (!rootNode) {
328
+ return false;
329
+ }
330
+
331
+ const currentProjectConfiguration =
332
+ editorStore.projectConfigurationEditorState.currentProjectConfiguration;
333
+ const rootCoordinate = `${rootNode.value.groupId}:${rootNode.value.artifactId}`;
334
+
335
+ for (
336
+ let i = 0;
337
+ i < currentProjectConfiguration.projectDependencies.length;
338
+ i++
339
+ ) {
340
+ const projectDep = guaranteeNonNullable(
341
+ currentProjectConfiguration.projectDependencies[i],
342
+ );
343
+ const projectDepCoordinate = generateGAVCoordinates(
344
+ guaranteeNonNullable(projectDep.groupId),
345
+ guaranteeNonNullable(projectDep.artifactId),
346
+ undefined,
347
+ );
348
+
349
+ if (projectDepCoordinate === rootCoordinate) {
350
+ const exclusions = dependencyEditorState.getExclusions(
351
+ projectDep.projectId,
352
+ );
353
+
354
+ for (let j = 0; j < exclusions.length; j++) {
355
+ if (guaranteeNonNullable(exclusions[j]).coordinate === coordinate) {
356
+ return true;
357
+ }
358
+ }
359
+ break;
360
+ }
361
+ }
362
+
363
+ return false;
364
+ };
365
+
366
+ const nodeIsExcluded = isExcluded();
256
367
  const nodeExpandIcon = isExpandable ? (
257
368
  node.isOpen ? (
258
369
  <ChevronDownIcon />
@@ -277,6 +388,10 @@ const DependencyTreeNodeContainer: React.FC<
277
388
  'project-dependency-explorer-tree__node__container--selected':
278
389
  node.isSelected,
279
390
  },
391
+ {
392
+ 'project-dependency-explorer-tree__node__container--excluded':
393
+ nodeIsExcluded,
394
+ },
280
395
  )}
281
396
  style={{
282
397
  paddingLeft: `${(level - 1) * (stepPaddingInRem ?? 1)}rem`,
@@ -289,15 +404,27 @@ const DependencyTreeNodeContainer: React.FC<
289
404
  </div>
290
405
  </div>
291
406
  <button
292
- className="tree-view__node__label project-dependency-explorer-tree__node__label"
407
+ className={clsx(
408
+ 'tree-view__node__label project-dependency-explorer-tree__node__label',
409
+ {
410
+ 'project-dependency-explorer-tree__node__label--excluded':
411
+ nodeIsExcluded,
412
+ },
413
+ )}
293
414
  tabIndex={-1}
294
- title={value.id}
415
+ title={nodeIsExcluded ? `${value.id} (EXCLUDED)` : value.id}
295
416
  >
296
417
  {value.artifactId}
297
418
  </button>
298
419
  <div className="project-dependency-explorer-tree__node__version">
299
420
  <button
300
- className="project-dependency-explorer-tree__node__version-btn"
421
+ className={clsx(
422
+ 'project-dependency-explorer-tree__node__version-btn',
423
+ {
424
+ 'project-dependency-explorer-tree__node__version-btn--excluded':
425
+ nodeIsExcluded,
426
+ },
427
+ )}
301
428
  title={value.versionId}
302
429
  tabIndex={-1}
303
430
  >
@@ -364,8 +491,114 @@ const ConflictTreeNodeContainer: React.FC<
364
491
  >
365
492
  > = (props) => {
366
493
  const { node, level, stepPaddingInRem, onNodeSelect } = props;
494
+ const editorStore = useEditorStore();
495
+ const dependencyEditorState =
496
+ editorStore.projectConfigurationEditorState.projectDependencyEditorState;
497
+
367
498
  const isExpandable = Boolean(node.childrenIds?.length);
368
499
  const selectNode = (): void => onNodeSelect?.(node);
500
+
501
+ const isExcluded = (): boolean => {
502
+ if (!(node instanceof ProjectDependencyTreeNodeData)) {
503
+ return false;
504
+ }
505
+
506
+ const value = node.value;
507
+ const coordinate = `${value.groupId}:${value.artifactId}`;
508
+
509
+ const treeData = dependencyEditorState.dependencyTreeData;
510
+ if (!treeData) {
511
+ return false;
512
+ }
513
+
514
+ const findRootNodeForCurrentNode = (nodeId: string): string | null => {
515
+ if (treeData.rootIds.indexOf(nodeId) !== -1) {
516
+ return nodeId;
517
+ }
518
+
519
+ const findInSubtree = (rootId: string, searchId: string): boolean => {
520
+ const rootNode = treeData.nodes.get(rootId);
521
+ if (!rootNode) {
522
+ return false;
523
+ }
524
+ const visited: { [key: string]: boolean } = {};
525
+ const queue = [rootId];
526
+
527
+ while (queue.length > 0) {
528
+ const currentId = queue.shift();
529
+ if (!currentId || visited[currentId]) {
530
+ continue;
531
+ }
532
+ visited[currentId] = true;
533
+
534
+ if (currentId === searchId) {
535
+ return true;
536
+ }
537
+
538
+ const currentNode = treeData.nodes.get(currentId);
539
+ if (currentNode?.childrenIds) {
540
+ for (let i = 0; i < currentNode.childrenIds.length; i++) {
541
+ queue.push(guaranteeNonNullable(currentNode.childrenIds[i]));
542
+ }
543
+ }
544
+ }
545
+ return false;
546
+ };
547
+
548
+ for (let i = 0; i < treeData.rootIds.length; i++) {
549
+ const rootId = guaranteeNonNullable(treeData.rootIds[i]);
550
+ if (findInSubtree(rootId, nodeId)) {
551
+ return rootId;
552
+ }
553
+ }
554
+
555
+ return null;
556
+ };
557
+
558
+ const rootNodeId = findRootNodeForCurrentNode(node.id);
559
+ if (!rootNodeId) {
560
+ return false;
561
+ }
562
+
563
+ const rootNode = treeData.nodes.get(rootNodeId);
564
+ if (!rootNode) {
565
+ return false;
566
+ }
567
+
568
+ const currentProjectConfiguration =
569
+ editorStore.projectConfigurationEditorState.currentProjectConfiguration;
570
+ const rootCoordinate = `${rootNode.value.groupId}:${rootNode.value.artifactId}`;
571
+
572
+ for (
573
+ let i = 0;
574
+ i < currentProjectConfiguration.projectDependencies.length;
575
+ i++
576
+ ) {
577
+ const projectDep = guaranteeNonNullable(
578
+ currentProjectConfiguration.projectDependencies[i],
579
+ );
580
+ const projectDepCoordinate = generateGAVCoordinates(
581
+ guaranteeNonNullable(projectDep.groupId),
582
+ guaranteeNonNullable(projectDep.artifactId),
583
+ undefined,
584
+ );
585
+
586
+ if (projectDepCoordinate === rootCoordinate) {
587
+ const exclusions = dependencyEditorState.getExclusions(
588
+ projectDep.projectId,
589
+ );
590
+
591
+ for (let j = 0; j < exclusions.length; j++) {
592
+ if (guaranteeNonNullable(exclusions[j]).coordinate === coordinate) {
593
+ return true;
594
+ }
595
+ }
596
+ break;
597
+ }
598
+ }
599
+
600
+ return false;
601
+ };
369
602
  const nodeExpandIcon = isExpandable ? (
370
603
  node.isOpen ? (
371
604
  <ChevronDownIcon />
@@ -390,6 +623,10 @@ const ConflictTreeNodeContainer: React.FC<
390
623
  'project-dependency-explorer-tree__node__container--selected':
391
624
  node.isSelected,
392
625
  },
626
+ {
627
+ 'project-dependency-explorer-tree__node__container--excluded':
628
+ isExcluded(),
629
+ },
393
630
  )}
394
631
  style={{
395
632
  paddingLeft: `${(level - 1) * (stepPaddingInRem ?? 1)}rem`,
@@ -421,17 +658,35 @@ const ConflictTreeNodeContainer: React.FC<
421
658
  )}
422
659
  </div>
423
660
  <button
424
- className="tree-view__node__label project-dependency-explorer-tree__node__label"
661
+ className={clsx(
662
+ 'tree-view__node__label project-dependency-explorer-tree__node__label',
663
+ {
664
+ 'project-dependency-explorer-tree__node__label--excluded':
665
+ isExcluded(),
666
+ },
667
+ )}
425
668
  tabIndex={-1}
426
- title={node.description}
669
+ title={
670
+ isExcluded() ? `${node.description} (EXCLUDED)` : node.description
671
+ }
427
672
  >
428
673
  {node.label}
429
674
  </button>
430
675
  {node instanceof ProjectDependencyTreeNodeData && (
431
676
  <div className="project-dependency-explorer-tree__node__version">
432
677
  <button
433
- className="project-dependency-explorer-tree__node__version-btn"
434
- title={node.value.versionId}
678
+ className={clsx(
679
+ 'project-dependency-explorer-tree__node__version-btn',
680
+ {
681
+ 'project-dependency-explorer-tree__node__version-btn--excluded':
682
+ isExcluded(),
683
+ },
684
+ )}
685
+ title={
686
+ isExcluded()
687
+ ? `${node.value.versionId} (EXCLUDED)`
688
+ : node.value.versionId
689
+ }
435
690
  tabIndex={-1}
436
691
  >
437
692
  {node.value.versionId}
@@ -747,6 +1002,267 @@ const ProjectDependencyReportModal = observer(
747
1002
  },
748
1003
  );
749
1004
 
1005
+ interface TransitiveDependencyOption {
1006
+ label: string;
1007
+ value: string;
1008
+ groupId: string;
1009
+ artifactId: string;
1010
+ }
1011
+
1012
+ const ProjectDependencyInlineExclusionsSelector = observer(
1013
+ (props: { projectDependency: ProjectDependency; isReadOnly: boolean }) => {
1014
+ const { projectDependency, isReadOnly } = props;
1015
+ const editorStore = useEditorStore();
1016
+ const applicationStore = useApplicationStore();
1017
+ const dependencyEditorState =
1018
+ editorStore.projectConfigurationEditorState.projectDependencyEditorState;
1019
+ const [selectedTransitiveDependency, setSelectedTransitiveDependency] =
1020
+ useState<TransitiveDependencyOption | null>(null);
1021
+ const [transitiveDependencyOptions, setTransitiveDependencyOptions] =
1022
+ useState<TransitiveDependencyOption[]>([]);
1023
+
1024
+ const getTransitiveDependencies =
1025
+ useCallback((): TransitiveDependencyOption[] => {
1026
+ const dependencyReport = dependencyEditorState.dependencyReport;
1027
+ if (!dependencyReport?.graph) {
1028
+ return [];
1029
+ }
1030
+
1031
+ const transitiveDeps: { [key: string]: TransitiveDependencyOption } =
1032
+ {};
1033
+ const existingExclusionCoordinates =
1034
+ dependencyEditorState.getExclusionCoordinates(
1035
+ projectDependency.projectId,
1036
+ );
1037
+
1038
+ const visitedNodes: { [key: string]: boolean } = {};
1039
+ const traverseNode = (nodeId: string) => {
1040
+ if (visitedNodes[nodeId]) {
1041
+ return;
1042
+ }
1043
+ visitedNodes[nodeId] = true;
1044
+
1045
+ const node = dependencyReport.graph.nodes.get(nodeId);
1046
+ if (node?.dependencies) {
1047
+ for (let i = 0; i < node.dependencies.length; i++) {
1048
+ const dep = node.dependencies[i];
1049
+ if (!dep?.groupId || !dep.artifactId) {
1050
+ continue;
1051
+ }
1052
+ const coordinate = generateGAVCoordinates(
1053
+ dep.groupId,
1054
+ dep.artifactId,
1055
+ undefined,
1056
+ );
1057
+
1058
+ if (
1059
+ existingExclusionCoordinates.indexOf(coordinate) === -1 &&
1060
+ coordinate !==
1061
+ `${projectDependency.groupId}:${projectDependency.artifactId}`
1062
+ ) {
1063
+ transitiveDeps[coordinate] = {
1064
+ label: generateGAVCoordinates(
1065
+ dep.groupId,
1066
+ dep.artifactId,
1067
+ undefined,
1068
+ ),
1069
+ value: coordinate,
1070
+ groupId: dep.groupId,
1071
+ artifactId: dep.artifactId,
1072
+ };
1073
+ }
1074
+
1075
+ traverseNode(dep.id);
1076
+ }
1077
+ }
1078
+ };
1079
+
1080
+ const rootNodeId = generateGAVCoordinates(
1081
+ guaranteeNonNullable(projectDependency.groupId),
1082
+ guaranteeNonNullable(projectDependency.artifactId),
1083
+ guaranteeNonNullable(projectDependency.versionId),
1084
+ );
1085
+ traverseNode(rootNodeId);
1086
+
1087
+ const transitiveDepsArray: TransitiveDependencyOption[] = [];
1088
+ for (const coordinate in transitiveDeps) {
1089
+ if (
1090
+ Object.prototype.hasOwnProperty.call(transitiveDeps, coordinate)
1091
+ ) {
1092
+ if (!transitiveDeps[coordinate]) {
1093
+ continue;
1094
+ } else {
1095
+ transitiveDepsArray.push(transitiveDeps[coordinate]);
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ return transitiveDepsArray.sort((a, b) =>
1101
+ a.label.localeCompare(b.label),
1102
+ );
1103
+ }, [
1104
+ dependencyEditorState,
1105
+ projectDependency.projectId,
1106
+ projectDependency.groupId,
1107
+ projectDependency.artifactId,
1108
+ projectDependency.versionId,
1109
+ ]);
1110
+
1111
+ useEffect(() => {
1112
+ setTransitiveDependencyOptions(getTransitiveDependencies());
1113
+ }, [
1114
+ dependencyEditorState.dependencyReport,
1115
+ projectDependency.projectId,
1116
+ getTransitiveDependencies,
1117
+ ]);
1118
+
1119
+ const addExclusionFromDropdown = (
1120
+ option: TransitiveDependencyOption | null,
1121
+ ): void => {
1122
+ if (!option) {
1123
+ return;
1124
+ }
1125
+
1126
+ try {
1127
+ dependencyEditorState.addExclusionByCoordinate(
1128
+ projectDependency.projectId,
1129
+ option.value,
1130
+ );
1131
+ setSelectedTransitiveDependency(null);
1132
+ setTransitiveDependencyOptions(getTransitiveDependencies());
1133
+ flowResult(dependencyEditorState.fetchDependencyReport())
1134
+ .then(() => {
1135
+ setTransitiveDependencyOptions(getTransitiveDependencies());
1136
+ })
1137
+ .catch(applicationStore.alertUnhandledError);
1138
+
1139
+ applicationStore.notificationService.notifySuccess(
1140
+ `Exclusion added: ${option.value}`,
1141
+ );
1142
+ } catch (error) {
1143
+ assertErrorThrown(error);
1144
+ applicationStore.notificationService.notifyError(
1145
+ `Failed to add exclusion: ${error.message}`,
1146
+ );
1147
+ }
1148
+ };
1149
+
1150
+ if (isReadOnly) {
1151
+ return null;
1152
+ }
1153
+
1154
+ return (
1155
+ <div className="project-dependency-exclusions-selector">
1156
+ <CustomSelectorInput
1157
+ className="project-dependency-exclusions-selector__dropdown"
1158
+ placeholder="Add exclusion..."
1159
+ options={transitiveDependencyOptions}
1160
+ onChange={addExclusionFromDropdown}
1161
+ value={selectedTransitiveDependency}
1162
+ isClearable={true}
1163
+ escapeClearsValue={true}
1164
+ disabled={transitiveDependencyOptions.length === 0}
1165
+ darkMode={
1166
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
1167
+ }
1168
+ />
1169
+ </div>
1170
+ );
1171
+ },
1172
+ );
1173
+
1174
+ const ProjectDependencyExclusionsList = observer(
1175
+ (props: { projectDependency: ProjectDependency; isReadOnly: boolean }) => {
1176
+ const { projectDependency, isReadOnly } = props;
1177
+ const editorStore = useEditorStore();
1178
+ const applicationStore = useApplicationStore();
1179
+ const dependencyEditorState =
1180
+ editorStore.projectConfigurationEditorState.projectDependencyEditorState;
1181
+ const [, setForceUpdate] = useState(0);
1182
+ const exclusions = dependencyEditorState.getExclusions(
1183
+ projectDependency.projectId,
1184
+ );
1185
+
1186
+ useEffect(() => {
1187
+ setForceUpdate((prev) => prev + 1);
1188
+ }, [
1189
+ dependencyEditorState.dependencyReport,
1190
+ projectDependency.projectId,
1191
+ dependencyEditorState,
1192
+ ]);
1193
+
1194
+ useEffect(() => {
1195
+ const interval = setInterval(() => {
1196
+ const currentExclusions = dependencyEditorState.getExclusions(
1197
+ projectDependency.projectId,
1198
+ );
1199
+ if (currentExclusions.length !== exclusions.length) {
1200
+ setForceUpdate((prev) => prev + 1);
1201
+ }
1202
+ }, 1000);
1203
+
1204
+ return () => clearInterval(interval);
1205
+ }, [exclusions.length, dependencyEditorState, projectDependency.projectId]);
1206
+
1207
+ const removeExclusion = (exclusion: ProjectDependencyExclusion): void => {
1208
+ try {
1209
+ dependencyEditorState.removeExclusion(
1210
+ projectDependency.projectId,
1211
+ exclusion,
1212
+ );
1213
+
1214
+ flowResult(dependencyEditorState.fetchDependencyReport()).catch(
1215
+ applicationStore.alertUnhandledError,
1216
+ );
1217
+
1218
+ applicationStore.notificationService.notifySuccess(
1219
+ `Exclusion removed: ${exclusion.coordinate}`,
1220
+ );
1221
+ } catch (error) {
1222
+ assertErrorThrown(error);
1223
+ applicationStore.notificationService.notifyError(
1224
+ `Failed to remove exclusion: ${error.message}`,
1225
+ );
1226
+ }
1227
+ };
1228
+
1229
+ if (exclusions.length === 0) {
1230
+ return null;
1231
+ }
1232
+
1233
+ return (
1234
+ <div className="project-dependency-exclusions-list">
1235
+ <div className="project-dependency-exclusions-list__header">
1236
+ <div className="project-dependency-exclusions-list__title">
1237
+ Exclusions ({exclusions.length})
1238
+ </div>
1239
+ </div>
1240
+ <div className="project-dependency-exclusions-list__items">
1241
+ {exclusions.map((exclusion) => (
1242
+ <div
1243
+ key={exclusion.coordinate}
1244
+ className="project-dependency-exclusions-list__item"
1245
+ >
1246
+ <div className="project-dependency-exclusions-list__item__coordinate">
1247
+ {exclusion.coordinate}
1248
+ </div>
1249
+ {!isReadOnly && (
1250
+ <button
1251
+ className="project-dependency-exclusions-list__item__remove-btn btn--dark btn--caution"
1252
+ onClick={() => removeExclusion(exclusion)}
1253
+ title="Remove exclusion"
1254
+ >
1255
+ <TimesIcon />
1256
+ </button>
1257
+ )}
1258
+ </div>
1259
+ ))}
1260
+ </div>
1261
+ </div>
1262
+ );
1263
+ },
1264
+ );
1265
+
750
1266
  const ProjectVersionDependencyEditor = observer(
751
1267
  (props: {
752
1268
  projectDependency: ProjectDependency;
@@ -754,7 +1270,6 @@ const ProjectVersionDependencyEditor = observer(
754
1270
  isReadOnly: boolean;
755
1271
  projects: Map<string, StoreProjectData>;
756
1272
  }) => {
757
- // init
758
1273
  const { projectDependency, deleteValue, isReadOnly, projects } = props;
759
1274
  const projectDependencyData = projects.get(projectDependency.projectId);
760
1275
  const editorStore = useEditorStore();
@@ -776,12 +1291,14 @@ const ProjectVersionDependencyEditor = observer(
776
1291
  const projectDisabled =
777
1292
  !configState.associatedProjectsAndVersionsFetched ||
778
1293
  configState.isReadOnly;
779
- const projectsOptions = Array.from(configState.projects.values())
1294
+ const projectsArray: StoreProjectData[] = [];
1295
+ configState.projects.forEach((project: StoreProjectData) => {
1296
+ projectsArray.push(project);
1297
+ });
1298
+ const projectsOptions = projectsArray
780
1299
  .map(buildProjectOption)
781
1300
  .sort(compareLabelFn);
782
- const onProjectSelectionChange = async (
783
- val: ProjectOption | null,
784
- ): Promise<void> => {
1301
+ const onProjectSelectionChange = (val: ProjectOption | null): void => {
785
1302
  if (
786
1303
  (val !== null || selectedProjectOption !== null) &&
787
1304
  (!val ||
@@ -793,26 +1310,36 @@ const ProjectVersionDependencyEditor = observer(
793
1310
  if (val) {
794
1311
  try {
795
1312
  fetchSelectedProjectVersionsStatus.inProgress();
796
- const _versions = await editorStore.depotServerClient.getVersions(
797
- guaranteeNonNullable(projectDependency.groupId),
798
- guaranteeNonNullable(projectDependency.artifactId),
799
- true,
800
- );
801
- configState.versions.set(val.value.coordinates, _versions);
802
- if (_versions.length) {
803
- projectDependency.setVersionId(
804
- guaranteeNonNullable(_versions[_versions.length - 1]),
805
- );
806
- flowResult(dependencyEditorState.fetchDependencyReport()).catch(
807
- applicationStore.alertUnhandledError,
808
- );
809
- } else {
810
- projectDependency.setVersionId('');
811
- }
1313
+ editorStore.depotServerClient
1314
+ .getVersions(
1315
+ guaranteeNonNullable(projectDependency.groupId),
1316
+ guaranteeNonNullable(projectDependency.artifactId),
1317
+ true,
1318
+ )
1319
+ .then((_versions) => {
1320
+ configState.versions.set(val.value.coordinates, _versions);
1321
+ if (_versions.length) {
1322
+ projectDependency.setVersionId(
1323
+ guaranteeNonNullable(_versions[_versions.length - 1]),
1324
+ );
1325
+ flowResult(
1326
+ dependencyEditorState.fetchDependencyReport(),
1327
+ ).catch(applicationStore.alertUnhandledError);
1328
+ } else {
1329
+ projectDependency.setVersionId('');
1330
+ }
1331
+ fetchSelectedProjectVersionsStatus.reset();
1332
+ })
1333
+ .catch((error) => {
1334
+ assertErrorThrown(error);
1335
+ editorStore.applicationStore.notificationService.notifyError(
1336
+ error,
1337
+ );
1338
+ fetchSelectedProjectVersionsStatus.reset();
1339
+ });
812
1340
  } catch (error) {
813
1341
  assertErrorThrown(error);
814
1342
  editorStore.applicationStore.notificationService.notifyError(error);
815
- } finally {
816
1343
  fetchSelectedProjectVersionsStatus.reset();
817
1344
  }
818
1345
  }
@@ -820,19 +1347,26 @@ const ProjectVersionDependencyEditor = observer(
820
1347
  };
821
1348
  // version
822
1349
  const version = projectDependency.versionId;
823
- const versionOptions = versions
824
- .toSorted((v1, v2) => compareSemVerVersions(v2, v1))
825
- .map((v) => {
826
- if (v === MASTER_SNAPSHOT_ALIAS) {
827
- return { value: v, label: SNAPSHOT_VERSION_ALIAS };
828
- }
829
- return { value: v, label: v };
830
- });
831
- const selectedVersionOption: VersionOption | null =
832
- versionOptions.find((v) => v.value === version) ?? null;
1350
+ const sortedVersions = versions
1351
+ .slice()
1352
+ .sort((v1, v2) => compareSemVerVersions(v2, v1));
1353
+ const versionOptions = sortedVersions.map((v) => {
1354
+ if (v === MASTER_SNAPSHOT_ALIAS) {
1355
+ return { value: v, label: SNAPSHOT_VERSION_ALIAS };
1356
+ }
1357
+ return { value: v, label: v };
1358
+ });
1359
+ let selectedVersionOption: VersionOption | null = null;
1360
+ for (let i = 0; i < versionOptions.length; i++) {
1361
+ if (guaranteeNonNullable(versionOptions[i]).value === version) {
1362
+ selectedVersionOption = guaranteeNonNullable(versionOptions[i]);
1363
+ break;
1364
+ }
1365
+ }
833
1366
  const versionDisabled =
834
- Boolean(!versions.length || !projectDependency.projectId.length) ||
835
- !configState.associatedProjectsAndVersionsFetched ||
1367
+ !guaranteeNonNullable(versions.length) ||
1368
+ !guaranteeNonNullable(projectDependency.projectId.length) ||
1369
+ !guaranteeNonNullable(configState.associatedProjectsAndVersionsFetched) ||
836
1370
  isReadOnly;
837
1371
 
838
1372
  const onVersionSelectionChange = (val: VersionOption | null): void => {
@@ -901,9 +1435,7 @@ const ProjectVersionDependencyEditor = observer(
901
1435
  isClearable={true}
902
1436
  escapeClearsValue={true}
903
1437
  onChange={(val: ProjectOption | null) => {
904
- onProjectSelectionChange(val).catch(
905
- applicationStore.alertUnhandledError,
906
- );
1438
+ onProjectSelectionChange(val);
907
1439
  }}
908
1440
  value={selectedProjectOption}
909
1441
  isLoading={configState.fetchingProjectVersionsState.isInProgress}
@@ -935,6 +1467,13 @@ const ProjectVersionDependencyEditor = observer(
935
1467
  !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
936
1468
  }
937
1469
  />
1470
+ {selectedProject && selectedVersionOption && (
1471
+ <ProjectDependencyInlineExclusionsSelector
1472
+ projectDependency={projectDependency}
1473
+ isReadOnly={isReadOnly}
1474
+ />
1475
+ )}
1476
+
938
1477
  <ControlledDropdownMenu
939
1478
  className="project-dependency-editor__visit-project-btn__dropdown-trigger btn--medium"
940
1479
  content={
@@ -1013,13 +1552,20 @@ export const ProjectDependencyEditor = observer(() => {
1013
1552
  <ProjectDependencyActions dependencyEditorState={dependencyEditorState} />
1014
1553
  {currentProjectConfiguration.projectDependencies.map(
1015
1554
  (projectDependency) => (
1016
- <ProjectVersionDependencyEditor
1017
- key={projectDependency._UUID}
1018
- projectDependency={projectDependency}
1019
- deleteValue={deleteProjectDependency(projectDependency)}
1020
- isReadOnly={isReadOnly}
1021
- projects={configState.projects}
1022
- />
1555
+ <div key={projectDependency._UUID}>
1556
+ <ProjectVersionDependencyEditor
1557
+ projectDependency={projectDependency}
1558
+ deleteValue={deleteProjectDependency(projectDependency)}
1559
+ isReadOnly={isReadOnly}
1560
+ projects={configState.projects}
1561
+ />
1562
+ {/* Indented exclusions list */}
1563
+ <ProjectDependencyExclusionsList
1564
+ key={`${projectDependency.projectId}-exclusions`}
1565
+ projectDependency={projectDependency}
1566
+ isReadOnly={isReadOnly}
1567
+ />
1568
+ </div>
1023
1569
  ),
1024
1570
  )}
1025
1571
  {dependencyEditorState.reportTab && (