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 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()
|
||||||
|
|||||||
193
src/window.py
193
src/window.py
@ -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
|
||||||
|
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._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
|
||||||
|
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._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,9 +1307,16 @@ 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
|
||||||
|
|
||||||
|
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(
|
current_tab.original_request = HttpRequest(
|
||||||
method=req.method,
|
method=req.method,
|
||||||
url=req.url,
|
url=req.url,
|
||||||
@ -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")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user