Add smart tab management and duplicate request handling

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
This commit is contained in:
Pavel Baksy 2025-12-22 02:03:35 +01:00
parent 47225cf254
commit 97751df18a
2 changed files with 226 additions and 18 deletions

View File

@ -20,7 +20,7 @@
import json import json
import uuid import uuid
from pathlib import Path from pathlib import Path
from typing import List from typing import List, Optional
from datetime import datetime, timezone from datetime import datetime, timezone
from .models import Project, SavedRequest, HttpRequest from .models import Project, SavedRequest, HttpRequest
@ -112,6 +112,35 @@ class ProjectManager:
self.save_projects(projects) self.save_projects(projects)
return saved_request 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): def delete_request(self, project_id: str, request_id: str):
"""Delete a saved request.""" """Delete a saved request."""
projects = self.load_projects() projects = self.load_projects()

View File

@ -270,6 +270,57 @@ class RosterWindow(Adw.ApplicationWindow):
return is_empty 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): 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 a new tab (simplified version - just clears current request for now)."""
# Create tab in tab manager # Create tab in tab manager
@ -1072,16 +1123,40 @@ class RosterWindow(Adw.ApplicationWindow):
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) 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 dropdown
project_list = Gtk.StringList() project_list = Gtk.StringList()
for p in projects: for p in projects:
project_list.append(p.name) project_list.append(p.name)
dropdown = Gtk.DropDown(model=project_list) dropdown = Gtk.DropDown(model=project_list)
dropdown.set_selected(preselect_project_index) # Pre-select project
box.append(dropdown) box.append(dropdown)
# Name entry # Name entry
entry = Gtk.Entry() entry = Gtk.Entry()
entry.set_placeholder_text("Request name") 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) box.append(entry)
dialog.set_extra_child(box) dialog.set_extra_child(box)
@ -1096,12 +1171,70 @@ class RosterWindow(Adw.ApplicationWindow):
if name: if name:
selected = dropdown.get_selected() selected = dropdown.get_selected()
project = projects[selected] project = projects[selected]
self.project_manager.add_request(project.id, name, request) # Check for duplicate name
self._load_projects() 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.connect("response", on_response)
dialog.present(self) 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): def _on_add_to_project(self, widget, project):
"""Save current request to specific project.""" """Save current request to specific project."""
request = self._build_request_from_ui() request = self._build_request_from_ui()
@ -1126,8 +1259,19 @@ class RosterWindow(Adw.ApplicationWindow):
if response == "save": if response == "save":
name = entry.get_text().strip() name = entry.get_text().strip()
if name: if name:
self.project_manager.add_request(project.id, name, request) # Check for duplicate name
self._load_projects() 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.connect("response", on_response)
dialog.present(self) dialog.present(self)
@ -1136,6 +1280,26 @@ class RosterWindow(Adw.ApplicationWindow):
"""Load saved request - smart loading based on current tab state.""" """Load saved request - smart loading based on current tab state."""
req = saved_request.request 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" # Check if current tab is an empty "New Request"
if self._is_empty_new_request_tab(): if self._is_empty_new_request_tab():
# Replace the empty tab with this request # 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) current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id)
if current_tab: if current_tab:
# Update the tab # Update the tab
current_tab.name = saved_request.name current_tab.name = tab_name
current_tab.request = req current_tab.request = req
current_tab.saved_request_id = saved_request.id current_tab.saved_request_id = link_to_saved
current_tab.original_request = HttpRequest(
method=req.method, if is_copy:
url=req.url, # This is a copy - mark as unsaved
headers=req.headers.copy(), current_tab.original_request = None
body=req.body, current_tab.modified = True
syntax=req.syntax else:
) # This is linked to saved request
current_tab.modified = False 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 # Load into UI
self._load_request_to_ui(req) self._load_request_to_ui(req)
@ -1161,12 +1332,20 @@ class RosterWindow(Adw.ApplicationWindow):
else: else:
# Current tab has changes or is not a "New Request" # Current tab has changes or is not a "New Request"
# Create a new tab # Create a new tab
self._create_new_tab( tab_id = self._create_new_tab(
name=saved_request.name, name=tab_name,
request=req, 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 # Switch to headers tab
self.request_stack.set_visible_child_name("headers") self.request_stack.set_visible_child_name("headers")