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:
parent
47225cf254
commit
97751df18a
@ -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()
|
||||
|
||||
193
src/window.py
193
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)
|
||||
# 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")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user