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:
vesp 2025-12-22 02:03:35 +01:00
parent 0d5bb837f5
commit 3633117c5f
2 changed files with 226 additions and 18 deletions

View File

@ -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()

View File

@ -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)
# 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)
# 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,9 +1307,16 @@ 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.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,
@ -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")