From cd233714bf51bc1ebfd6b35ca96d071989558b67 Mon Sep 17 00:00:00 2001 From: Livio Meuli Date: Fri, 10 Apr 2026 13:39:14 +0200 Subject: [PATCH] Modals for Delete and Rename Files and Folders --- backend/managers/file_manager.py | 19 ++++ frontend/editor.py | 106 ++++++++---------- frontend/sidebar.py | 177 ++++++++++++++++++------------- frontend/state.py | 4 + 4 files changed, 170 insertions(+), 136 deletions(-) diff --git a/backend/managers/file_manager.py b/backend/managers/file_manager.py index 7cf8207..b1d223f 100644 --- a/backend/managers/file_manager.py +++ b/backend/managers/file_manager.py @@ -133,6 +133,25 @@ class FileManager: st.error(f"File not found: {old_relative_path}") return False + def delete_folder(self, relative_path) -> bool: + folder_path = (self.base_path / relative_path).resolve() + + if not str(folder_path).startswith(str(self.base_path.resolve())): + st.error(f"Access denied: {relative_path}") + return False + + if not folder_path.exists(): + st.error(f"Folder not found: {relative_path}") + return False + + try: + import shutil + shutil.rmtree(folder_path) + return True + except Exception as e: + st.error(f"Error deleting folder {relative_path}: {str(e)}") + return False + def delete_file(self, relative_path): file_path = Path(relative_path).resolve() diff --git a/frontend/editor.py b/frontend/editor.py index 681c441..4797f58 100644 --- a/frontend/editor.py +++ b/frontend/editor.py @@ -101,71 +101,57 @@ def render_editor(): if not st.session_state.open_files: st.info("Please select a file to edit.") return - else: - fm = FileManager() - try: - active_index = st.session_state.open_files.index(st.session_state.active_file) - except (ValueError, KeyError): - active_index = 0 - - st.session_state.active_tab = active_index - - tab_names = [Path(f).name for f in st.session_state.open_files] - tabs = st.tabs(tab_names) - - for idx, file_path in enumerate(st.session_state.open_files): - with tabs[idx]: - if idx == st.session_state.active_tab: - st.session_state.active_file = file_path + fm = FileManager() - if file_path not in st.session_state.files_content: - st.session_state.files_content[file_path] = fm.read_file(Path(file_path)) - - file_language = LANG_MAP.get(Path(file_path).suffix, "text") - - code = st_ace.st_ace( - value=st.session_state.files_content[file_path], - language=file_language, - theme="monokai", - key=f"code_editor_{file_path}", - auto_update=True, - height=400, - ) + # ── Tab bar via st.tabs() ───────────────────────────────────────────────── + tab_names = [Path(f).name for f in st.session_state.open_files] + tabs = st.tabs(tab_names) - if code != st.session_state.files_content[file_path]: - st.session_state.files_content[file_path] = code - - cols = st.columns([1, 1, 1, 1]) - with cols[0]: - if st.button("Save Changes", key=f"save_{file_path}"): - content = st.session_state.files_content[file_path] - if fm.save_file(file_path, content): - st.success("File saved successfully!") - - with cols[1]: - if st.button("Close File", key=f"close_{file_path}"): - st.session_state.open_files.remove(file_path) - del st.session_state.files_content[file_path] - st.success("File closed successfully!") - - if st.session_state.active_file == file_path: - st.session_state.active_file = ( - st.session_state.open_files[0] - if st.session_state.open_files - else None - ) - st.rerun() # Refresh the page to update the UI + for idx, file_path in enumerate(st.session_state.open_files): + with tabs[idx]: + if file_path not in st.session_state.files_content: + st.session_state.files_content[file_path] = fm.read_file(Path(file_path)) - with cols[2]: - if st.button("Rename File", key=f"rename_{file_path}"): - _rename_dialog(file_path) + file_language = LANG_MAP.get(Path(file_path).suffix, "text") - with cols[3]: - if st.button("Delete File", key=f"delete_{file_path}"): - _delete_dialog(file_path) - - if st.button("▶ Run Code", key="run_code"): + code = st_ace.st_ace( + value=st.session_state.files_content[file_path], + language=file_language, + theme="monokai", + key=f"code_editor_{file_path}", + auto_update=True, + height=400, + ) + + if code != st.session_state.files_content[file_path]: + st.session_state.files_content[file_path] = code + + cols = st.columns([1, 1, 1, 1]) + with cols[0]: + if st.button("Save Changes", key=f"save_{file_path}"): + if fm.save_file(file_path, code): + st.success("File saved successfully!") + + with cols[1]: + if st.button("Close File", key=f"close_{file_path}"): + st.session_state.open_files.remove(file_path) + st.session_state.files_content.pop(file_path, None) + st.session_state.active_file = ( + st.session_state.open_files[0] + if st.session_state.open_files else None + ) + st.rerun() + + with cols[2]: + if st.button("Rename File", key=f"rename_{file_path}"): + _rename_dialog(file_path) + + with cols[3]: + if st.button("Delete File", key=f"delete_{file_path}"): + _delete_dialog(file_path) + + if st.button("▶ Run Code", key="run_code"): result = run_active_file() if not result: st.stop() diff --git a/frontend/sidebar.py b/frontend/sidebar.py index bc71125..a824f09 100644 --- a/frontend/sidebar.py +++ b/frontend/sidebar.py @@ -24,6 +24,64 @@ SUFFIX_MAP = { "default": "📄" # Unbekannt } + +# ── Modals ──────────────────────────────────────────────────────────────────── + +@st.dialog("Delete Folder") +def _delete_folder_dialog(folder_rel: str, folder_name: str): + st.warning(f"Delete **{folder_name}** and all its contents? This cannot be undone.") + col1, col2 = st.columns(2) + with col1: + if st.button("Delete", type="primary", use_container_width=True): + if fm.delete_folder(folder_rel): + st.session_state.selected_folder = None + st.session_state.selected_folder_rel = None + st.rerun() + else: + st.error("Delete failed.") + with col2: + if st.button("Cancel", use_container_width=True): + st.rerun() + + +@st.dialog("Add File") +def _add_file_dialog(parent_path: str = ""): + name = st.text_input("File name:", placeholder="e.g. script.py") + col1, col2 = st.columns(2) + with col1: + if st.button("Create", type="primary", use_container_width=True): + if not name.strip(): + st.warning("Please enter a file name.") + elif "/" in name or "\\" in name: + st.warning("Name must not contain slashes.") + else: + if fm.create_file(parent_path, name.strip()): + st.rerun() + with col2: + if st.button("Cancel", use_container_width=True): + st.rerun() + + +@st.dialog("Add Folder") +def _add_folder_dialog(parent_path: str = ""): + name = st.text_input("Folder name:", placeholder="e.g. utils") + col1, col2 = st.columns(2) + with col1: + if st.button("Create", type="primary", use_container_width=True): + if not name.strip(): + st.warning("Please enter a folder name.") + elif "/" in name or "\\" in name: + st.warning("Name must not contain slashes.") + else: + if fm.create_folder(parent_path, name.strip()): + st.rerun() + with col2: + if st.button("Cancel", use_container_width=True): + st.rerun() + + +# ── File tree ───────────────────────────────────────────────────────────────── + def build_arborist_tree(tree, parent_path=Path()): nodes = [] @@ -47,7 +105,8 @@ def build_arborist_tree(tree, parent_path=Path()): "key": f"key_{node_id}" }) - return nodes + return nodes + def render_filetree_arborist(tree): data = build_arborist_tree(tree) @@ -64,30 +123,32 @@ def render_filetree_arborist(tree): return selected +# ── Sidebar ─────────────────────────────────────────────────────────────────── + def render_sidebar(): st.sidebar.title("Navigation") - + navigation_section = st.sidebar.container() file_explorer_section = st.sidebar.container() - + with navigation_section: st.radio( - "Options", - ["Chat with AI Assistant", "Code Editor"], - key=f"radio_interface_options") + "Options", + ["Chat with AI Assistant", "Code Editor"], + key="radio_interface_options") st.markdown("---") - + with file_explorer_section: st.subheader("File Explorer") tree = fm.get_file_tree() workspace = st.container() add_more = st.container() - + with workspace: with st.container(border=False): if not tree: - st.info("Workspace is empty.") + st.info("Workspace is empty.") else: selected = render_filetree_arborist(tree) @@ -97,81 +158,45 @@ def render_sidebar(): if st.session_state.last_selected != selected_path: st.session_state.last_selected = selected_path abs_path = fm.base_path / selected_path - + if abs_path.is_file(): - if str(abs_path) not in st.session_state.open_files: - st.session_state.open_files.append(str(abs_path)) - - st.session_state.active_file = str(abs_path) + st.session_state.selected_folder = None + st.session_state.selected_folder_rel = None + file_str = str(abs_path) + if file_str not in st.session_state.open_files: + st.session_state.open_files.append(file_str) + st.session_state.active_file = file_str st.rerun() elif abs_path.is_dir(): st.session_state.selected_folder = str(abs_path) - selected_folder = st.session_state.selected_folder - with st.popover( - f"📁 {Path(selected_folder).name}", - use_container_width=True, - key=f"{selected_folder}_popover_options"): - action = st.radio( - "Action", - ["New File", "New Folder"], - key=f"radio_folder_options", - label_visibility="collapsed") - - new_name = st.text_input("Name", key=f"input_folder_options") - - btn_cols = st.columns([1,1,1]) - with btn_cols[0]: - if st.button("Create", key=f"btn_create_subfile_or_subfolder"): - if "/" in new_name or "\\" in new_name: - st.warning("Do not include slashes in names!") - elif new_name: - if action == "New Folder": - fm.create_folder(selected_path, new_name) - else: - fm.create_file(selected_path, new_name) - st.success(f"{action} created!") - st.session_state.selected_folder = None - st.rerun() + st.session_state.selected_folder_rel = selected_path + st.rerun() - with btn_cols[1]: - if st.button("Cancel", key=f"cancel_folder_options"): - st.session_state.selected_folder = None - st.rerun() + # Folder actions — rendered outside the selection block so they persist across reruns + if st.session_state.get("selected_folder"): + folder_name = Path(st.session_state.selected_folder).name + folder_rel = st.session_state.selected_folder_rel + + with st.container(border=True): + st.caption(f"📁 {folder_name}") + if st.button("Add File", key="btn_add_file_in_folder", use_container_width=True): + _add_file_dialog(folder_rel) + if st.button("Add Folder", key="btn_add_folder_in_folder", use_container_width=True): + _add_folder_dialog(folder_rel) + if st.button("Delete Folder", key="btn_delete_folder", use_container_width=True): + _delete_folder_dialog(folder_rel, folder_name) - with btn_cols[2]: - if st.button("Delete Folder", key=f"delete_folder"): - try: - fm.delete_folder(selected_path) - st.success(f"Folder '{Path(selected_folder).name}' deleted successfully!") - st.session_state.selected_folder = None - st.rerun() - except Exception as e: - st.error(f"Error deleting folder: {e}") - with add_more: - with st.popover("⚙️ Explorer_Options", key=f"popover_options"): - action = st.radio( - "Action", - ["New File", "New Folder"], - key=f"radio_explorer_options", - label_visibility="collapsed") - - new_name = st.text_input("Name", key=f"input_explorer_options") - - if st.button("Create", key=f"btn_create_file_or_folder"): - if "/" in new_name or "\\" in new_name: - st.warning("Do not include slashes in names!") - elif new_name: - if action == "New Folder": - fm.create_folder("", new_name) - else: - fm.create_file("", new_name) - st.success(f"{action} created!") - st.rerun() - - return + with st.popover("⚙️ Explorer Options", key="popover_options"): + if st.button("Add File", key="btn_add_file", use_container_width=True): + _add_file_dialog("") + + if st.button("Add Folder", key="btn_add_folder", use_container_width=True): + _add_folder_dialog("") + + return + if __name__ == "__main__": render_sidebar() - \ No newline at end of file diff --git a/frontend/state.py b/frontend/state.py index eefb71a..6021fd1 100644 --- a/frontend/state.py +++ b/frontend/state.py @@ -10,6 +10,9 @@ def init_state(): if "selected_folder" not in st.session_state: st.session_state.selected_folder = None + if "selected_folder_rel" not in st.session_state: + st.session_state.selected_folder_rel = None + # Chat manager (persists across reruns) if "chat_manager" not in st.session_state: st.session_state.chat_manager = ChatManager() @@ -57,5 +60,6 @@ def init_state(): st.session_state.agent_pending_action = None + if __name__ == "__main__": init_state() -- 2.30.2