From 97751df18a01fb7f2aebaf608fcf6606bf5e7b5e Mon Sep 17 00:00:00 2001 From: Pavel Baksy Date: Mon, 22 Dec 2025 02:03:35 +0100 Subject: [PATCH] Add smart tab management and duplicate request handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Duplicate Name Detection: - Warn when saving request with existing name in same project - Show overwrite confirmation dialog with destructive styling - Update existing request on overwrite (preserves ID, updates timestamp) - Request names are unique within projects (but can duplicate across projects) Save Request Improvements: - Clear modified flag (•) when request is saved or overwritten - Pre-fill save dialog with original request name and project - Pre-fill save dialog with tab name for copy tabs - Auto-select text in name field for easy editing Smart Tab Loading: - Switch to existing tab when loading unmodified saved request - Create new tab only when needed (modified or doesn't exist) - Avoid duplicate tabs for same request - Show toast notification when switching to existing tab Copy Tab Management: - Create numbered copies when loading modified request again - Naming: "ReqA (copy)", "ReqA (copy 2)", "ReqA (copy 3)", etc. - Copy tabs are NOT linked to saved request (no saved_request_id) - Copy tabs marked as unsaved (•) to encourage saving with new name - Unique copy names prevent conflicts Tab Response Persistence: - Restore response when switching between tabs - Each tab maintains independent response state - Clear response area when switching to tab without response Project Manager: - Add find_request_by_name() for duplicate detection - Add update_request() for overwriting existing requests - Update request modified_at timestamp on save Files Modified: - src/project_manager.py: Duplicate detection and update methods - src/window.py: Smart tab management and copy handling --- src/project_manager.py | 31 +++++- src/window.py | 213 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 226 insertions(+), 18 deletions(-) diff --git a/src/project_manager.py b/src/project_manager.py index 7bcb1b5..9e6ec35 100644 --- a/src/project_manager.py +++ b/src/project_manager.py @@ -20,7 +20,7 @@ import json import uuid from pathlib import Path -from typing import List +from typing import List, Optional from datetime import datetime, timezone from .models import Project, SavedRequest, HttpRequest @@ -112,6 +112,35 @@ class ProjectManager: self.save_projects(projects) return saved_request + def find_request_by_name(self, project_id: str, name: str) -> Optional[SavedRequest]: + """Find a request by name within a project. Returns None if not found.""" + projects = self.load_projects() + for p in projects: + if p.id == project_id: + for req in p.requests: + if req.name == name: + return req + break + return None + + def update_request(self, project_id: str, request_id: str, name: str, request: HttpRequest) -> SavedRequest: + """Update an existing request.""" + projects = self.load_projects() + updated_request = None + for p in projects: + if p.id == project_id: + for req in p.requests: + if req.id == request_id: + req.name = name + req.request = request + req.modified_at = datetime.now(timezone.utc).isoformat() + updated_request = req + break + break + if updated_request: + self.save_projects(projects) + return updated_request + def delete_request(self, project_id: str, request_id: str): """Delete a saved request.""" projects = self.load_projects() diff --git a/src/window.py b/src/window.py index bfcd043..5cac53b 100644 --- a/src/window.py +++ b/src/window.py @@ -270,6 +270,57 @@ class RosterWindow(Adw.ApplicationWindow): return is_empty + def _find_tab_by_saved_request_id(self, saved_request_id): + """Find a tab by its saved_request_id. Returns None if not found.""" + for tab in self.tab_manager.tabs: + if tab.saved_request_id == saved_request_id: + return tab + return None + + def _generate_copy_name(self, base_name): + """Generate a unique copy name like 'ReqA (copy)', 'ReqA (copy 2)', etc.""" + # Check if "Name (copy)" exists + existing_names = {tab.name for tab in self.tab_manager.tabs} + + copy_name = f"{base_name} (copy)" + if copy_name not in existing_names: + return copy_name + + # Find the highest number used + counter = 2 + while f"{base_name} (copy {counter})" in existing_names: + counter += 1 + + return f"{base_name} (copy {counter})" + + def _switch_to_tab(self, tab_id): + """Switch to a specific tab.""" + tab = self.tab_manager.get_tab_by_id(tab_id) + if not tab: + return + + # Update tab manager + self.tab_manager.set_active_tab(tab_id) + self.current_tab_id = tab_id + + # Load the tab's request into UI + self._load_request_to_ui(tab.request) + + # Restore response if available + if tab.response: + self._display_response(tab.response) + else: + # Clear response area + self.status_label.set_text("Ready") + self.time_label.set_text("") + buffer = self.response_headers_textview.get_buffer() + buffer.set_text("") + buffer = self.response_body_sourceview.get_buffer() + buffer.set_text("") + + # Update tab bar + self._update_tab_bar_visibility() + def _create_new_tab(self, name="New Request", request=None, saved_request_id=None): """Create a new tab (simplified version - just clears current request for now).""" # Create tab in tab manager @@ -1072,16 +1123,40 @@ class RosterWindow(Adw.ApplicationWindow): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + # Check if current tab has a saved request (for pre-filling) + current_tab = None + preselect_project_index = 0 + prefill_name = "" + if self.current_tab_id: + current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) + if current_tab: + if current_tab.saved_request_id: + # Find which project this request belongs to + for i, p in enumerate(projects): + for req in p.requests: + if req.id == current_tab.saved_request_id: + preselect_project_index = i + prefill_name = current_tab.name + break + elif current_tab.name and current_tab.name != "New Request": + # Copy tab or named unsaved tab - prefill with tab name + prefill_name = current_tab.name + # Project dropdown project_list = Gtk.StringList() for p in projects: project_list.append(p.name) dropdown = Gtk.DropDown(model=project_list) + dropdown.set_selected(preselect_project_index) # Pre-select project box.append(dropdown) # Name entry entry = Gtk.Entry() entry.set_placeholder_text("Request name") + if prefill_name: + entry.set_text(prefill_name) # Pre-fill name + # Select all text for easy editing + entry.select_region(0, -1) box.append(entry) dialog.set_extra_child(box) @@ -1096,12 +1171,70 @@ class RosterWindow(Adw.ApplicationWindow): if name: selected = dropdown.get_selected() project = projects[selected] - self.project_manager.add_request(project.id, name, request) - self._load_projects() + # Check for duplicate name + existing = self.project_manager.find_request_by_name(project.id, name) + if existing: + # Show overwrite confirmation + self._show_overwrite_dialog(project, name, existing.id, request) + else: + # No duplicate, save normally + saved_request = self.project_manager.add_request(project.id, name, request) + self._load_projects() + self._show_toast(f"Saved as '{name}'") + + # Clear modified flag on current tab + self._mark_tab_as_saved(saved_request.id, name, request) dialog.connect("response", on_response) dialog.present(self) + def _mark_tab_as_saved(self, saved_request_id, name, request): + """Mark the current tab as saved (clear modified flag).""" + if self.current_tab_id: + current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) + if current_tab: + # Update tab metadata + current_tab.saved_request_id = saved_request_id + current_tab.name = name + current_tab.modified = False + + # Update original_request to match saved state + current_tab.original_request = HttpRequest( + method=request.method, + url=request.url, + headers=request.headers.copy(), + body=request.body, + syntax=request.syntax + ) + + # Update tab bar to remove modified indicator + self._update_tab_bar_visibility() + + def _show_overwrite_dialog(self, project, name, existing_request_id, request): + """Show dialog asking if user wants to overwrite existing request.""" + dialog = Adw.AlertDialog() + dialog.set_heading("Request Already Exists") + dialog.set_body(f"A request named '{name}' already exists in '{project.name}'.\n\nDo you want to overwrite it?") + + dialog.add_response("cancel", "Cancel") + dialog.add_response("overwrite", "Overwrite") + dialog.set_response_appearance("overwrite", Adw.ResponseAppearance.DESTRUCTIVE) + dialog.set_default_response("cancel") + dialog.set_close_response("cancel") + + def on_overwrite_response(dlg, response): + if response == "overwrite": + # Update the existing request + self.project_manager.update_request(project.id, existing_request_id, name, request) + self._load_projects() + self._show_toast(f"Updated '{name}'") + + # Clear modified flag on current tab + self._mark_tab_as_saved(existing_request_id, name, request) + + dialog.connect("response", on_overwrite_response) + dialog.present(self) + def _on_add_to_project(self, widget, project): """Save current request to specific project.""" request = self._build_request_from_ui() @@ -1126,8 +1259,19 @@ class RosterWindow(Adw.ApplicationWindow): if response == "save": name = entry.get_text().strip() if name: - self.project_manager.add_request(project.id, name, request) - self._load_projects() + # Check for duplicate name + existing = self.project_manager.find_request_by_name(project.id, name) + if existing: + # Show overwrite confirmation + self._show_overwrite_dialog(project, name, existing.id, request) + else: + # No duplicate, save normally + saved_request = self.project_manager.add_request(project.id, name, request) + self._load_projects() + self._show_toast(f"Saved as '{name}'") + + # Clear modified flag on current tab + self._mark_tab_as_saved(saved_request.id, name, request) dialog.connect("response", on_response) dialog.present(self) @@ -1136,6 +1280,26 @@ class RosterWindow(Adw.ApplicationWindow): """Load saved request - smart loading based on current tab state.""" req = saved_request.request + # First, check if this request is already open in an unmodified tab + existing_tab = self._find_tab_by_saved_request_id(saved_request.id) + if existing_tab and not existing_tab.is_modified(): + # Switch to the existing unmodified tab + self._switch_to_tab(existing_tab.id) + self._show_toast(f"Switched to '{saved_request.name}'") + return + + # Check if there's a modified tab with this saved_request_id + # If so, this is a copy and should not be linked + is_copy = existing_tab and existing_tab.is_modified() + + if is_copy: + # Generate numbered copy name + tab_name = self._generate_copy_name(saved_request.name) + link_to_saved = None + else: + tab_name = saved_request.name + link_to_saved = saved_request.id + # Check if current tab is an empty "New Request" if self._is_empty_new_request_tab(): # Replace the empty tab with this request @@ -1143,17 +1307,24 @@ class RosterWindow(Adw.ApplicationWindow): current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) if current_tab: # Update the tab - current_tab.name = saved_request.name + current_tab.name = tab_name current_tab.request = req - current_tab.saved_request_id = saved_request.id - current_tab.original_request = HttpRequest( - method=req.method, - url=req.url, - headers=req.headers.copy(), - body=req.body, - syntax=req.syntax - ) - current_tab.modified = False + current_tab.saved_request_id = link_to_saved + + if is_copy: + # This is a copy - mark as unsaved + current_tab.original_request = None + current_tab.modified = True + else: + # This is linked to saved request + current_tab.original_request = HttpRequest( + method=req.method, + url=req.url, + headers=req.headers.copy(), + body=req.body, + syntax=req.syntax + ) + current_tab.modified = False # Load into UI self._load_request_to_ui(req) @@ -1161,12 +1332,20 @@ class RosterWindow(Adw.ApplicationWindow): else: # Current tab has changes or is not a "New Request" # Create a new tab - self._create_new_tab( - name=saved_request.name, + tab_id = self._create_new_tab( + name=tab_name, request=req, - saved_request_id=saved_request.id + saved_request_id=link_to_saved ) + # If it's a copy, clear the original_request to mark as unsaved + if is_copy: + new_tab = self.tab_manager.get_tab_by_id(tab_id) + if new_tab: + new_tab.original_request = None + new_tab.modified = True + self._update_tab_bar_visibility() + # Switch to headers tab self.request_stack.set_visible_child_name("headers")