diff --git a/app/api/session.py b/app/api/session.py index 0178e38..c4c253e 100644 --- a/app/api/session.py +++ b/app/api/session.py @@ -189,6 +189,16 @@ def delete_remembered_series(): return {"items": []} +@router.delete("/remembered-series/{series_id}") +def delete_remembered_series_item(series_id: str): + service = SessionService() + try: + items = service.remove_remembered_series(series_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + return {"items": items} + + @router.get("/mapping-preview") def get_mapping_preview(session_id: str = Query("default", min_length=1)): service = SessionService() diff --git a/app/services/session_service.py b/app/services/session_service.py index 07126cb..5b6d450 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -286,6 +286,17 @@ class SessionService: with self._connect() as conn: conn.execute("DELETE FROM remembered_series") + def remove_remembered_series(self, series_id: str) -> list[dict]: + normalized = str(series_id or "").strip() + if not normalized: + raise ValueError("series_id is required") + with self._connect() as conn: + conn.execute( + "DELETE FROM remembered_series WHERE series_id = ?", + (normalized,), + ) + return self.list_remembered_series() + def _enforce_remembered_series_limit(self, limit: int) -> None: with self._connect() as conn: conn.execute( diff --git a/app/static/app.js b/app/static/app.js index 34d3000..9265b34 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -323,6 +323,17 @@ renderRememberedDropdown(); } + async function removeRememberedSeriesItem(seriesId) { + const normalizedId = String(seriesId || "").trim(); + if (!normalizedId) return; + const data = await api(`/api/session/remembered-series/${encodeURIComponent(normalizedId)}`, { + method: "DELETE", + }); + state.rememberedSeries = data.items || []; + renderRememberedDropdown(); + openRememberedDropdown(); + } + function rememberedForQuery(query) { const normalized = (query || "").trim().toLowerCase(); const rememberedItems = (state.rememberedSeries || []).map((entry) => entry.series || {}); @@ -352,16 +363,40 @@ function renderRememberedDropdown() { const query = (el.searchInput.value || "").trim(); - const items = rememberedForQuery(query); + const items = rememberedForQuery(query).slice().sort((a, b) => { + const aLabel = (a.display_name || a.name || "").toLowerCase(); + const bLabel = (b.display_name || b.name || "").toLowerCase(); + return aLabel.localeCompare(bLabel); + }); el.rememberedDropdownList.innerHTML = ""; items.forEach((item) => { const li = document.createElement("li"); const left = document.createElement("span"); + left.className = "remembered-item-title"; const label = item.display_name || item.name || "(series)"; left.textContent = label; + left.title = label; li.appendChild(left); - li.addEventListener("click", () => withHandler(() => selectSeries(item), li)); + + const removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.className = "remembered-remove-btn"; + removeBtn.textContent = "×"; + removeBtn.setAttribute("aria-label", `Remove ${label}`); + removeBtn.title = `Remove ${label}`; + removeBtn.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + withHandler(() => removeRememberedSeriesItem(item.id), removeBtn); + }); + li.appendChild(removeBtn); + + left.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + withHandler(() => selectSeries(item), left); + }); el.rememberedDropdownList.appendChild(li); }); } diff --git a/app/static/styles.css b/app/static/styles.css index 63f6f22..4afc7c9 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -141,6 +141,37 @@ body { max-height: 220px; } +#rememberedDropdownList li { + align-items: center; +} + +#rememberedDropdownList .remembered-item-title { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; +} + +#rememberedDropdownList .remembered-remove-btn { + border: none; + background: transparent; + color: #64748b; + width: 22px; + height: 22px; + line-height: 1; + border-radius: 4px; + padding: 0; + font-size: 15px; + cursor: pointer; +} + +#rememberedDropdownList .remembered-remove-btn:hover { + background: #eef2f7; + color: #0f172a; +} + .stack { display: flex; flex-direction: column; diff --git a/data/session_state.sqlite3 b/data/session_state.sqlite3 index b635bc1..63e3fae 100644 Binary files a/data/session_state.sqlite3 and b/data/session_state.sqlite3 differ