"""APIClient - Agent — Collections Sidebar.""" from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QPushButton, QInputDialog, QMenu, QLineEdit, QLabel, QMessageBox ) from PyQt6.QtCore import pyqtSignal, Qt from PyQt6.QtGui import QColor, QFont, QBrush from app.ui.theme import Colors, method_color from app.core import storage from app.models import HttpRequest class CollectionsSidebar(QWidget): request_selected = pyqtSignal(object) def __init__(self, parent=None): super().__init__(parent) self.setObjectName("sidebar") self.setMinimumWidth(240) self.setMaximumWidth(380) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # ── Header ──────────────────────────────────────────────────────────── header = QWidget() header.setObjectName("sidebarHeader") header.setFixedHeight(44) h_layout = QHBoxLayout(header) h_layout.setContentsMargins(12, 0, 8, 0) title = QLabel("COLLECTIONS") title.setObjectName("sectionLabel") self.add_col_btn = QPushButton("+") self.add_col_btn.setObjectName("ghost") self.add_col_btn.setFixedSize(28, 28) self.add_col_btn.setToolTip("New Collection (Ctrl+Shift+N)") self.add_col_btn.clicked.connect(self._add_collection) h_layout.addWidget(title) h_layout.addStretch() h_layout.addWidget(self.add_col_btn) layout.addWidget(header) # ── Filter ──────────────────────────────────────────────────────────── search_wrap = QWidget() search_wrap.setObjectName("sidebarSearch") sw = QHBoxLayout(search_wrap) sw.setContentsMargins(10, 6, 10, 6) self.search_input = QLineEdit() self.search_input.setObjectName("filterInput") self.search_input.setPlaceholderText("Filter collections…") self.search_input.textChanged.connect(self._filter) sw.addWidget(self.search_input) layout.addWidget(search_wrap) # ── Tree ───────────────────────────────────────────────────────────── self.tree = QTreeWidget() self.tree.setHeaderHidden(True) self.tree.setIndentation(16) self.tree.setAnimated(True) self.tree.itemDoubleClicked.connect(self._on_double_click) self.tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.tree.customContextMenuRequested.connect(self._context_menu) layout.addWidget(self.tree) # History root (always last in tree) self._history_root = QTreeWidgetItem([" History"]) self._history_root.setForeground(0, QBrush(QColor(Colors.TEXT_MUTED))) self._history_root.setFont(0, self._section_font()) self.tree.addTopLevelItem(self._history_root) self._load_collections() self._load_history() # ── Helpers ─────────────────────────────────────────────────────────────── def _section_font(self) -> QFont: f = QFont() f.setPointSize(9) f.setWeight(QFont.Weight.Bold) return f def _make_req_item(self, req: dict) -> QTreeWidgetItem: method = req.get("method", "GET") name = req.get("name") or req.get("url", "Untitled") item = QTreeWidgetItem() item.setText(0, f" {method} {name}") item.setForeground(0, QBrush(QColor(method_color(method)))) item.setData(0, Qt.ItemDataRole.UserRole, {"type": "request", "req": req}) return item def _make_collection_item(self, col: dict) -> QTreeWidgetItem: item = QTreeWidgetItem([f" {col['name']}"]) item.setForeground(0, QBrush(QColor(Colors.TEXT_PRIMARY))) item.setFont(0, self._section_font()) item.setData(0, Qt.ItemDataRole.UserRole, {"type": "collection", "id": col["id"]}) return item def _make_folder_item(self, folder: dict) -> QTreeWidgetItem: item = QTreeWidgetItem([f" ▸ {folder['name']}"]) item.setForeground(0, QBrush(QColor(Colors.TEXT_SECONDARY))) item.setData( 0, Qt.ItemDataRole.UserRole, {"type": "folder", "id": folder["id"], "col_id": folder["collection_id"]} ) return item def _dict_to_request(self, r: dict) -> HttpRequest: return HttpRequest( method = r.get("method") or "GET", url = r.get("url") or "", headers = r.get("headers") or {}, params = r.get("params") or {}, body = r.get("body") or "", body_type = r.get("body_type") or "raw", content_type = r.get("content_type") or "", auth_type = r.get("auth_type") or "none", auth_data = r.get("auth_data") or {}, pre_request_script = r.get("pre_request_script") or "", test_script = r.get("test_script") or "", name = r.get("name") or "", id = r.get("id"), timeout = r.get("timeout") or 30, ssl_verify = bool(r.get("ssl_verify", 1)), ) # ── Data loading ────────────────────────────────────────────────────────── def _load_collections(self, filter_text: str = ""): for i in range(self.tree.topLevelItemCount() - 1, -1, -1): if self.tree.topLevelItem(i) is not self._history_root: self.tree.takeTopLevelItem(i) for col in storage.get_collections(): if filter_text and filter_text.lower() not in col["name"].lower(): continue col_item = self._make_collection_item(col) for folder in storage.get_folders(col["id"]): folder_item = self._make_folder_item(folder) for req in storage.get_requests(col["id"], folder["id"]): folder_item.addChild(self._make_req_item(req)) col_item.addChild(folder_item) for req in storage.get_requests(col["id"]): col_item.addChild(self._make_req_item(req)) self.tree.insertTopLevelItem(0, col_item) col_item.setExpanded(True) def _load_history(self): self._history_root.takeChildren() for h in storage.get_history(30): item = QTreeWidgetItem() method = h.get("method", "GET") url = h.get("url", "") item.setText(0, f" {method} {url}") item.setForeground(0, QBrush(QColor(method_color(method)))) item.setData(0, Qt.ItemDataRole.UserRole, {"type": "history", "req": h}) self._history_root.addChild(item) def _filter(self, text: str): self._load_collections(filter_text=text) def refresh(self): self._load_collections(self.search_input.text()) self._load_history() # ── Interaction ─────────────────────────────────────────────────────────── def _on_double_click(self, item, _column): data = item.data(0, Qt.ItemDataRole.UserRole) if not data or data["type"] not in ("request", "history"): return self.request_selected.emit(self._dict_to_request(data["req"])) def _context_menu(self, pos): item = self.tree.itemAt(pos) if not item: return data = item.data(0, Qt.ItemDataRole.UserRole) if not data: return menu = QMenu(self) t = data["type"] if t == "collection": menu.addAction("Add Folder", lambda: self._add_folder(data["id"])) menu.addAction("Rename", lambda: self._rename_collection(item, data["id"])) menu.addSeparator() menu.addAction("Delete", lambda: self._delete_collection(item, data["id"])) elif t == "folder": menu.addAction("Rename", lambda: self._rename_folder(item, data["id"])) menu.addSeparator() menu.addAction("Delete", lambda: self._delete_folder(data["id"])) elif t == "request": menu.addAction("Open in Tab", lambda: self.request_selected.emit( self._dict_to_request(data["req"]) )) menu.addSeparator() menu.addAction("Delete", lambda: self._delete_request(data["req"].get("id"))) elif t == "history": menu.addAction("Open in Tab", lambda: self.request_selected.emit( self._dict_to_request(data["req"]) )) menu.addSeparator() menu.addAction("Clear All History", self._clear_history) menu.exec(self.tree.mapToGlobal(pos)) # ── CRUD actions ────────────────────────────────────────────────────────── def _add_collection(self): name, ok = QInputDialog.getText(self, "New Collection", "Collection name:") if ok and name.strip(): storage.add_collection(name.strip()) self._load_collections() def _add_folder(self, col_id: int): name, ok = QInputDialog.getText(self, "New Folder", "Folder name:") if ok and name.strip(): storage.add_folder(col_id, name.strip()) self._load_collections() def _rename_collection(self, item, col_id: int): current = item.text(0).strip() name, ok = QInputDialog.getText(self, "Rename Collection", "Name:", text=current) if ok and name.strip(): storage.rename_collection(col_id, name.strip()) self._load_collections() def _rename_folder(self, item, folder_id: int): current = item.text(0).strip().lstrip("▸ ").strip() name, ok = QInputDialog.getText(self, "Rename Folder", "Name:", text=current) if ok and name.strip(): storage.rename_folder(folder_id, name.strip()) self._load_collections() def _delete_collection(self, item, col_id: int): name = item.text(0).strip() reply = QMessageBox.question( self, "Delete Collection", f"Delete '{name}' and all its requests? This cannot be undone.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel ) if reply == QMessageBox.StandardButton.Yes: storage.delete_collection(col_id) self._load_collections() def _delete_folder(self, folder_id: int): reply = QMessageBox.question( self, "Delete Folder", "Delete this folder and all its requests?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel ) if reply == QMessageBox.StandardButton.Yes: storage.delete_folder(folder_id) self._load_collections() def _delete_request(self, req_id): if req_id: storage.delete_request(req_id) self._load_collections() def _clear_history(self): storage.clear_history() self._load_history()