@@ -109,6 +109,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
109109 const $location = $injector . get ( '$location' ) ;
110110 const $q = $injector . get ( '$q' ) ;
111111 const $routeParams = $injector . get ( '$routeParams' ) ;
112+ const connectionGroupService = $injector . get ( 'connectionGroupService' ) ;
112113 const connectionParseService = $injector . get ( 'connectionParseService' ) ;
113114 const connectionService = $injector . get ( 'connectionService' ) ;
114115 const guacNotification = $injector . get ( 'guacNotification' ) ;
@@ -117,9 +118,12 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
117118 const userGroupService = $injector . get ( 'userGroupService' ) ;
118119
119120 // Required types
121+ const Connection = $injector . get ( 'Connection' ) ;
122+ const ConnectionGroup = $injector . get ( 'ConnectionGroup' ) ;
120123 const ConnectionImportConfig = $injector . get ( 'ConnectionImportConfig' ) ;
121124 const DirectoryPatch = $injector . get ( 'DirectoryPatch' ) ;
122125 const Error = $injector . get ( 'Error' ) ;
126+ const ImportConnection = $injector . get ( 'ImportConnection' ) ;
123127 const ParseError = $injector . get ( 'ParseError' ) ;
124128 const PermissionSet = $injector . get ( 'PermissionSet' ) ;
125129 const User = $injector . get ( 'User' ) ;
@@ -140,6 +144,13 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
140144 */
141145 $scope . patchFailure = null ; ;
142146
147+ /**
148+ * The specific array of patches sent to the API. This is
149+ * required to correctly map API errors back to file rows.
150+ * @type {Patch[] }
151+ */
152+ $scope . patches = null ;
153+
143154 /**
144155 * True if the file is fully uploaded and ready to be processed, or false
145156 * otherwise.
@@ -345,6 +356,174 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
345356 return $q . all ( { ...userRequests , ...groupRequests } ) ;
346357 }
347358
359+ /**
360+ * Ensures all connection groups specified in the import file exist within
361+ * the given data source. This function flattens the existing group tree,
362+ * identifies missing path segments, and creates them sequentially to
363+ * maintain hierarchical integrity. It also updates the connection objects
364+ * with the correct internal parent identifiers.
365+ *
366+ * @param {DataSource } dataSource
367+ * The data source to which the connection groups should be added.
368+ *
369+ * @param {ParseResult } parseResult
370+ * The result of parsing the user-supplied import file, containing
371+ * the connection objects and their desired group paths. It may be
372+ * modified inside the function to actualize connection parent
373+ * identifiers.
374+ *
375+ * @returns {Promise }
376+ * A promise which resolves to a map of all full group paths to
377+ * their corresponding internal identifiers.
378+ */
379+ function ensureConnectionGroups ( dataSource , parseResult ) {
380+
381+ if ( ! $scope . importConfig . createMissingGroups )
382+ return $q . resolve ( { } ) ;
383+
384+ return connectionGroupService . getConnectionGroupTree ( dataSource ) . then ( rootGroup => {
385+
386+ const pathMap = { } ;
387+ const flatten = ( group , prefix = "" ) => {
388+ // The first node is always "ROOT"
389+ const currentPath = prefix ? ( prefix + "/" + group . name ) : "ROOT" ;
390+
391+ // Map the path string to the REAL system identifier
392+ if ( group . identifier ) {
393+ pathMap [ currentPath ] = group . identifier ;
394+ }
395+
396+ if ( group . childConnectionGroups ) {
397+ group . childConnectionGroups . forEach ( c => flatten ( c , currentPath ) ) ;
398+ }
399+ } ;
400+
401+ flatten ( rootGroup ) ;
402+
403+ let sequence = $q . resolve ( ) ;
404+
405+ // Iterate through connections
406+ _ . forEach ( parseResult . connectionObjects , ( connectionObject ) => {
407+ const fullPath = connectionObject . group ; // e.g., "ROOT/Folder/Subfolder"
408+ if ( ! fullPath ) return ;
409+
410+ const segments = fullPath . split ( '/' ) ;
411+ let currentPath = "ROOT" ;
412+
413+ segments . forEach ( segment => {
414+ // Ignore empty segments and the literal "ROOT" segment in the path string
415+ if ( ! segment || segment === 'ROOT' ) return ;
416+
417+ const parentPath = currentPath ;
418+ const nextPath = parentPath + "/" + segment ;
419+ currentPath = nextPath ;
420+
421+ sequence = sequence . then ( ( ) => {
422+ // If this absolute path already exists in our map, skip creation
423+ if ( pathMap [ nextPath ] ) return ;
424+
425+ // Create the new group under the parent's REAL identifier
426+ const newGroup = new ConnectionGroup ( {
427+ name : segment ,
428+ parentIdentifier : pathMap [ parentPath ]
429+ } ) ;
430+
431+ return connectionGroupService . saveConnectionGroup ( dataSource , newGroup )
432+ . then ( ( ) => {
433+ // Update the map with the identifier returned by the server
434+ pathMap [ nextPath ] = newGroup . identifier ;
435+ } ) ;
436+ } ) ;
437+ } ) ;
438+ } ) ;
439+
440+ return sequence . then ( ( ) => {
441+ // Now that all groups are created and pathMap is full,
442+ // link each connection to its parent identifier.
443+ parseResult . connectionObjects . forEach ( connectionObject => {
444+ if ( ! connectionObject . parentIdentifier ) {
445+ connectionObject . parentIdentifier =
446+ pathMap [ connectionObject . group ] || pathMap [ 'ROOT' ] ;
447+ }
448+ } ) ;
449+
450+ return pathMap ;
451+ } ) ;
452+ } ) ;
453+ }
454+
455+ /**
456+ * Converts the parsed connection data into a set of administrative
457+ * patches. Each patch represents an atomic operation to create or
458+ * update a connection, including its attributes, parameters, and
459+ * its placement within the connection group hierarchy.
460+ *
461+ * @param {ImportConfig } importConfig
462+ * The configuration object defining how the import should be
463+ * processed and which fields should be prioritized.
464+ *
465+ * @param {ParseResult } parseResult
466+ * The result of parsing the user-supplied import file, used as
467+ * the source data for the generated patches.
468+ *
469+ * @returns {Patch[] }
470+ * An array of patches representing the operations required to
471+ * synchronize the state of the data source with the import file.
472+ */
473+ function createConnectionPatches ( importConfig , parseResult ) {
474+
475+ const patches = [ ] ;
476+
477+ parseResult . connectionObjects . forEach ( connectionObject => {
478+
479+ // The value for the patch is a full-fledged Connection
480+ const value = new Connection ( connectionObject ) ;
481+
482+ // If a new connection is being created
483+ if ( connectionObject . importMode
484+ === ImportConnection . ImportMode . CREATE )
485+
486+ // Add a patch for creating the connection
487+ patches . push ( new DirectoryPatch ( {
488+ op : DirectoryPatch . Operation . ADD ,
489+ path : '/' ,
490+ value
491+ } ) ) ;
492+
493+ // The connection is being replaced, and permissions are only being
494+ // added, not replaced
495+ else if ( importConfig . existingPermissionMode ===
496+ ConnectionImportConfig . ExistingPermissionMode . PRESERVE )
497+
498+ // Add a patch for replacing the connection
499+ patches . push ( new DirectoryPatch ( {
500+ op : DirectoryPatch . Operation . REPLACE ,
501+ path : '/' + connectionObject . identifier ,
502+ value
503+ } ) ) ;
504+
505+ // The connection is being replaced, and permissions are also being
506+ // replaced
507+ else {
508+
509+ // Add a patch for removing the existing connection
510+ patches . push ( new DirectoryPatch ( {
511+ op : DirectoryPatch . Operation . REMOVE ,
512+ path : '/' + connectionObject . identifier
513+ } ) ) ;
514+
515+ // Add a second patch for creating the replacement connection
516+ patches . push ( new DirectoryPatch ( {
517+ op : DirectoryPatch . Operation . ADD ,
518+ path : '/' ,
519+ value
520+ } ) ) ;
521+
522+ }
523+ } ) ;
524+ return patches ;
525+ } ;
526+
348527 /**
349528 * Process a successfully parsed import file, creating any specified
350529 * connections, creating and granting permissions to any specified users
@@ -368,54 +547,56 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
368547
369548 const dataSource = $routeParams . dataSource ;
370549
371- // First, attempt to create the connections
372- connectionService . patchConnections ( dataSource , parseResult . patches )
373- . then ( connectionResponse =>
374-
375- // If connection creation is successful, create users and groups
376- createUsersAndGroups ( parseResult ) . then ( ( ) =>
377-
378- // Grant any new permissions to users and groups. NOTE: Any
379- // existing permissions for updated connections will NOT be
380- // removed - only new permissions will be added.
381- grantConnectionPermissions ( parseResult , connectionResponse )
382- . then ( ( ) => {
383-
384- $scope . processing = false ;
385-
386- // Display a success message if everything worked
387- guacNotification . showStatus ( {
388- className : 'success' ,
389- title : 'IMPORT.DIALOG_HEADER_SUCCESS' ,
390- text : {
391- key : 'IMPORT.INFO_CONNECTIONS_IMPORTED_SUCCESS' ,
392- variables : { NUMBER : parseResult . connectionCount }
393- } ,
394-
395- // Add a button to acknowledge and redirect to
396- // the connection listing page
397- actions : [ {
398- name : 'IMPORT.ACTION_ACKNOWLEDGE' ,
399- callback : ( ) => {
400-
401- // Close the notification
402- guacNotification . showStatus ( false ) ;
403-
404- // Redirect to connection list page
405- $location . url ( '/settings/' + dataSource + '/connections' ) ;
406- }
407- } ]
408- } ) ;
409- } ) )
410-
411- // If an error occurs while trying to create users or groups,
412- // display the error to the user.
413- . catch ( handleError )
414- )
550+ $scope . processing = true ;
415551
552+ // 1. Create missing groups
553+ ensureConnectionGroups ( dataSource , parseResult )
554+ . then ( pathMap => {
555+
556+ // 2. Create the patches using the freshly created group IDs
557+ $scope . patches = createConnectionPatches ( $scope . importConfig ,
558+ parseResult , pathMap ) ;
559+
560+ // 3. Import the connections
561+ return connectionService . patchConnections ( dataSource , $scope . patches )
562+ . then ( connectionResponse => {
563+
564+ // 4. Create Users/Groups and Grant Permissions
565+ return createUsersAndGroups ( parseResult ) . then ( ( ) => {
566+
567+ // 5. Grant permissions
568+ return grantConnectionPermissions ( parseResult , connectionResponse )
569+ . then ( ( ) => {
570+
571+ $scope . processing = false ;
572+ guacNotification . showStatus ( {
573+ className : 'success' ,
574+ title : 'IMPORT.DIALOG_HEADER_SUCCESS' ,
575+ text : {
576+ key : 'IMPORT.INFO_CONNECTIONS_IMPORTED_SUCCESS' ,
577+ variables : { NUMBER : parseResult . connectionCount }
578+ } ,
579+ actions : [ {
580+ name : 'IMPORT.ACTION_ACKNOWLEDGE' ,
581+ callback : ( ) => {
582+ guacNotification . showStatus ( false ) ;
583+ $location . url ( '/settings/' + dataSource + '/connections' ) ;
584+ }
585+ } ]
586+ } ) ;
587+ } ) ;
588+ } )
589+ // If an error occurs while trying to create users or groups,
590+ // display the error to the user.
591+ . catch ( error => {
592+ $scope . processing = false ;
593+ handleError ( error ) ;
594+ } ) ;
595+ } ) ;
596+ } )
416597 // If an error occurred when the call to create the connections was made,
417598 // skip any further processing - the user will have a chance to fix the
418- // problems and try again
599+ // problems and try again.
419600 . catch ( patchFailure => {
420601 $scope . processing = false ;
421602 $scope . patchFailure = patchFailure ;
0 commit comments