@@ -145,6 +145,35 @@ def _get_connection_config_from_node(self, node: Any) -> ConnectionConfig | None
145145 data = getattr (node , "data" , None )
146146 return self ._get_connection_config_from_data (data )
147147
148+ def _find_connection_node_by_name (self : ConnectionMixinHost , name : str ) -> Any | None :
149+ if not name :
150+ return None
151+ stack = [self .object_tree .root ]
152+ while stack :
153+ node = stack .pop ()
154+ for child in node .children :
155+ config = self ._get_connection_config_from_node (child )
156+ if config and config .name == name :
157+ return child
158+ stack .append (child )
159+ return None
160+
161+ def _get_connection_folder_path (self : ConnectionMixinHost , node : Any ) -> str | None :
162+ if not node or self ._get_node_kind (node ) != "connection_folder" :
163+ return None
164+ parts : list [str ] = []
165+ current = node
166+ while current and current != self .object_tree .root :
167+ if self ._get_node_kind (current ) == "connection_folder" :
168+ data = getattr (current , "data" , None )
169+ name = getattr (data , "name" , None )
170+ if isinstance (name , str ) and name :
171+ parts .append (name )
172+ current = current .parent
173+ if not parts :
174+ return None
175+ return "/" .join (reversed (parts ))
176+
148177 def connect_to_server (self : ConnectionMixinHost , config : ConnectionConfig ) -> None :
149178 """Connect to a database (async, non-blocking).
150179
@@ -369,11 +398,9 @@ def _select_connected_node(self: ConnectionMixinHost) -> None:
369398 cursor_config = self ._get_connection_config_from_node (cursor )
370399 if not cursor_config or cursor_config .name != self .current_config .name :
371400 return
372- for node in self .object_tree .root .children :
373- config = self ._get_connection_config_from_node (node )
374- if config and config .name == self .current_config .name :
375- self .object_tree .move_cursor (node )
376- break
401+ node = self ._find_connection_node_by_name (self .current_config .name )
402+ if node is not None :
403+ self .object_tree .move_cursor (node )
377404
378405 def action_disconnect (self : ConnectionMixinHost ) -> None :
379406 """Disconnect from current database."""
@@ -619,6 +646,179 @@ def action_toggle_connection_favorite(self: ConnectionMixinHost) -> None:
619646 if credentials_error :
620647 self .push_screen (ErrorScreen ("Keyring Error" , str (credentials_error )))
621648
649+ def action_move_connection_to_folder (self : ConnectionMixinHost ) -> None :
650+ from sqlit .domains .connections .app .credentials import CredentialsPersistError
651+ from sqlit .domains .connections .domain .config import normalize_folder_path
652+ from sqlit .domains .connections .ui .screens import FolderInputScreen
653+ from sqlit .shared .ui .screens .error import ErrorScreen
654+
655+ node = self .object_tree .cursor_node
656+ if not node :
657+ return
658+
659+ config = self ._get_connection_config_from_node (node )
660+ if not config :
661+ return
662+
663+ if not any (c .name == config .name for c in self .connections ):
664+ self .notify ("Only saved connections can be moved" , severity = "warning" )
665+ return
666+
667+ current_path = getattr (config , "folder_path" , "" )
668+
669+ def on_result (value : str | None ) -> None :
670+ if value is None :
671+ return
672+ new_path = normalize_folder_path (value )
673+ if new_path == current_path :
674+ return
675+ config .folder_path = new_path
676+ credentials_error : CredentialsPersistError | None = None
677+
678+ try :
679+ self .services .connection_store .save_all (self .connections )
680+ except CredentialsPersistError as exc :
681+ credentials_error = exc
682+ except Exception as exc :
683+ config .folder_path = current_path
684+ self .notify (f"Failed to move connection: { exc } " , severity = "error" )
685+ return
686+
687+ if not self .services .connection_store .is_persistent :
688+ self .notify ("Connections are not persisted in this session" , severity = "warning" )
689+
690+ self ._refresh_connection_tree ()
691+ if new_path :
692+ self .notify (f"Moved to folder '{ new_path } '" )
693+ else :
694+ self .notify ("Moved to root" )
695+ if credentials_error :
696+ self .push_screen (ErrorScreen ("Keyring Error" , str (credentials_error )))
697+
698+ self .push_screen (
699+ FolderInputScreen (config .name , current_value = current_path ),
700+ on_result ,
701+ )
702+
703+ def action_rename_connection_folder (self : ConnectionMixinHost ) -> None :
704+ from sqlit .domains .connections .app .credentials import CredentialsPersistError
705+ from sqlit .domains .connections .domain .config import normalize_folder_path
706+ from sqlit .domains .connections .ui .screens import FolderInputScreen
707+ from sqlit .shared .ui .screens .error import ErrorScreen
708+
709+ node = self .object_tree .cursor_node
710+ folder_path = self ._get_connection_folder_path (node )
711+ if not folder_path :
712+ return
713+
714+ parent_path = "/" .join (folder_path .split ("/" )[:- 1 ])
715+ current_name = folder_path .split ("/" )[- 1 ]
716+
717+ def on_result (value : str | None ) -> None :
718+ if value is None :
719+ return
720+ new_segment = normalize_folder_path (value )
721+ if not new_segment :
722+ self .notify ("Folder name cannot be empty" , severity = "warning" )
723+ return
724+ new_path = f"{ parent_path } /{ new_segment } " if parent_path else new_segment
725+ if new_path == folder_path :
726+ return
727+
728+ updated = False
729+ for conn in self .connections :
730+ path = getattr (conn , "folder_path" , "" ) or ""
731+ if path == folder_path or path .startswith (f"{ folder_path } /" ):
732+ remainder = "" if path == folder_path else path [len (folder_path ) + 1 :]
733+ conn .folder_path = f"{ new_path } /{ remainder } " if remainder else new_path
734+ updated = True
735+
736+ if not updated :
737+ return
738+
739+ credentials_error : CredentialsPersistError | None = None
740+ try :
741+ self .services .connection_store .save_all (self .connections )
742+ except CredentialsPersistError as exc :
743+ credentials_error = exc
744+ except Exception as exc :
745+ self .notify (f"Failed to rename folder: { exc } " , severity = "error" )
746+ return
747+
748+ if not self .services .connection_store .is_persistent :
749+ self .notify ("Connections are not persisted in this session" , severity = "warning" )
750+
751+ self ._refresh_connection_tree ()
752+ self .notify (f"Renamed folder to '{ new_path } '" )
753+ if credentials_error :
754+ self .push_screen (ErrorScreen ("Keyring Error" , str (credentials_error )))
755+
756+ self .push_screen (
757+ FolderInputScreen (
758+ current_name ,
759+ current_value = current_name ,
760+ title = "Rename Folder" ,
761+ description = f"Rename folder '{ folder_path } ' (use / for nesting):" ,
762+ ),
763+ on_result ,
764+ )
765+
766+ def action_delete_connection_folder (self : ConnectionMixinHost ) -> None :
767+ from sqlit .domains .connections .app .credentials import CredentialsPersistError
768+ from sqlit .shared .ui .screens .confirm import ConfirmScreen
769+ from sqlit .shared .ui .screens .error import ErrorScreen
770+
771+ node = self .object_tree .cursor_node
772+ folder_path = self ._get_connection_folder_path (node )
773+ if not folder_path :
774+ return
775+
776+ parent_path = "/" .join (folder_path .split ("/" )[:- 1 ])
777+
778+ def do_delete (confirmed : bool | None ) -> None :
779+ if not confirmed :
780+ return
781+
782+ updated = False
783+ for conn in self .connections :
784+ path = getattr (conn , "folder_path" , "" ) or ""
785+ if path == folder_path or path .startswith (f"{ folder_path } /" ):
786+ remainder = "" if path == folder_path else path [len (folder_path ) + 1 :]
787+ if parent_path :
788+ new_path = f"{ parent_path } /{ remainder } " if remainder else parent_path
789+ else :
790+ new_path = remainder
791+ conn .folder_path = new_path
792+ updated = True
793+
794+ if not updated :
795+ return
796+
797+ credentials_error : CredentialsPersistError | None = None
798+ try :
799+ self .services .connection_store .save_all (self .connections )
800+ except CredentialsPersistError as exc :
801+ credentials_error = exc
802+ except Exception as exc :
803+ self .notify (f"Failed to delete folder: { exc } " , severity = "error" )
804+ return
805+
806+ if not self .services .connection_store .is_persistent :
807+ self .notify ("Connections are not persisted in this session" , severity = "warning" )
808+
809+ self ._refresh_connection_tree ()
810+ self .notify (f"Deleted folder '{ folder_path } '" )
811+ if credentials_error :
812+ self .push_screen (ErrorScreen ("Keyring Error" , str (credentials_error )))
813+
814+ self .push_screen (
815+ ConfirmScreen (
816+ f"Delete folder '{ folder_path } '?" ,
817+ "Connections will move to the parent folder." ,
818+ ),
819+ do_delete ,
820+ )
821+
622822 def action_delete_connection (self : ConnectionMixinHost ) -> None :
623823 from sqlit .shared .ui .screens .confirm import ConfirmScreen
624824
@@ -708,15 +908,13 @@ def _handle_connection_picker_result(self: ConnectionMixinHost, result: Any) ->
708908 matching_config = next ((c for c in self .connections if c .name == config .name ), None )
709909 if matching_config :
710910 config = matching_config
711- for node in self .object_tree .root .children :
712- node_config = self ._get_connection_config_from_node (node )
713- if node_config and node_config .name == config .name :
714- self ._emit_debug (
715- "connection_picker.select_node" ,
716- connection = config .name ,
717- )
718- self .object_tree .move_cursor (node )
719- break
911+ node = self ._find_connection_node_by_name (config .name )
912+ if node is not None :
913+ self ._emit_debug (
914+ "connection_picker.select_node" ,
915+ connection = config .name ,
916+ )
917+ self .object_tree .move_cursor (node )
720918 if self .current_config and self .current_config .name == config .name :
721919 self ._emit_debug ("connection_picker.already_connected" , connection = config .name )
722920 self .notify (f"Already connected to { config .name } " )
@@ -728,12 +926,10 @@ def _handle_connection_picker_result(self: ConnectionMixinHost, result: Any) ->
728926 selected_config = next ((c for c in self .connections if c .name == result ), None )
729927 if selected_config :
730928 self ._emit_debug ("connection_picker.result" , result = "name" , connection = selected_config .name )
731- for node in self .object_tree .root .children :
732- node_config = self ._get_connection_config_from_node (node )
733- if node_config and node_config .name == result :
734- self ._emit_debug ("connection_picker.select_node" , connection = selected_config .name )
735- self .object_tree .move_cursor (node )
736- break
929+ node = self ._find_connection_node_by_name (result )
930+ if node is not None :
931+ self ._emit_debug ("connection_picker.select_node" , connection = selected_config .name )
932+ self .object_tree .move_cursor (node )
737933
738934 if self .current_config and self .current_config .name == selected_config .name :
739935 self ._emit_debug ("connection_picker.already_connected" , connection = selected_config .name )
0 commit comments