From daae9b89440fb1b82a9300f5c9afd0a18bd500df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Fri, 19 Jun 2026 13:27:05 +0200 Subject: [PATCH] Convert add_purchase.js to TypeScript; drop dead add_edition.js (issue #17) - Add ts/add_purchase.ts: typed port of add_purchase.js. Replaces getEl with document.querySelector; types the search-select:change CustomEvent detail (SearchSelectChangeDetail / SearchSelectOption) - Point add_purchase / edit_purchase views at compiled dist/add_purchase.js - Delete add_edition.js: no Edition model/view/url/template references it (feature was removed; the script was dead) - Delete the now-superseded add_game.js / add_purchase.js source files - Tighten test_rendered_pages assertions to the dist/ script paths Co-Authored-By: Claude Sonnet 4.6 --- games/static/js/add_edition.js | 24 ---------- games/static/js/add_game.js | 12 ----- games/views/purchase.py | 6 +-- tests/test_rendered_pages.py | 4 +- .../js/add_purchase.js => ts/add_purchase.ts | 48 ++++++++++++------- 5 files changed, 35 insertions(+), 59 deletions(-) delete mode 100644 games/static/js/add_edition.js delete mode 100644 games/static/js/add_game.js rename games/static/js/add_purchase.js => ts/add_purchase.ts (53%) diff --git a/games/static/js/add_edition.js b/games/static/js/add_edition.js deleted file mode 100644 index 334f069..0000000 --- a/games/static/js/add_edition.js +++ /dev/null @@ -1,24 +0,0 @@ -import { syncSelectInputUntilChanged } from "./utils.js"; - -let syncData = [ - { - source: "#id_game", - source_value: "dataset.name", - target: "#id_name", - target_value: "value", - }, - { - source: "#id_game", - source_value: "textContent", - target: "#id_sort_name", - target_value: "value", - }, - { - source: "#id_game", - source_value: "dataset.year", - target: "#id_year_released", - target_value: "value", - }, -]; - -syncSelectInputUntilChanged(syncData, "form"); diff --git a/games/static/js/add_game.js b/games/static/js/add_game.js deleted file mode 100644 index d35ade7..0000000 --- a/games/static/js/add_game.js +++ /dev/null @@ -1,12 +0,0 @@ -import { syncSelectInputUntilChanged } from "./utils.js"; - -let syncData = [ - { - source: "#id_name", - source_value: "value", - target: "#id_sort_name", - target_value: "value", - }, -]; - -syncSelectInputUntilChanged(syncData, "form"); diff --git a/games/views/purchase.py b/games/views/purchase.py index 91135ba..b7c599a 100644 --- a/games/views/purchase.py +++ b/games/views/purchase.py @@ -194,7 +194,7 @@ def _pricing_controls() -> Node: By default the form's own single Price field is the bundle price. When 2+ games are selected and "Separate price per game" is checked, the per-game inputs (the general ``selection-fields`` element) take over and the bundle - Price is hidden. Toggle/visibility wiring lives in add_purchase.js; the + Price is hidden. Toggle/visibility wiring lives in ts/add_purchase.ts; the hidden ``pricing_mode`` tells the view which path to take. """ return Div(attributes=[("id", "pricing-controls")])[ @@ -301,7 +301,7 @@ def add_purchase(request: HttpRequest, game_id: int = 0) -> HttpResponse: ), title="Add New Purchase", scripts=mark_safe( - ModuleScript("search_select.js") + ModuleScript("add_purchase.js") + ModuleScript("search_select.js") + ModuleScript("dist/add_purchase.js") ), ) @@ -319,7 +319,7 @@ def edit_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse: AddForm(form, request=request, additional_row=_purchase_additional_row()), title="Edit Purchase", scripts=mark_safe( - ModuleScript("search_select.js") + ModuleScript("add_purchase.js") + ModuleScript("search_select.js") + ModuleScript("dist/add_purchase.js") ), ) diff --git a/tests/test_rendered_pages.py b/tests/test_rendered_pages.py index 4cbe8f0..48a86f1 100644 --- a/tests/test_rendered_pages.py +++ b/tests/test_rendered_pages.py @@ -125,14 +125,14 @@ class RenderedPagesTest(TestCase): def test_add_game_form(self): html = self.get("games:add_game").content.decode() - self.assertIn("add_game.js", html) + self.assertIn("dist/add_game.js", html) self.assertIn("submit_and_redirect", html) self.assertIn("Submit & Create Purchase", html) # & correctly escaped self.assertNoEscapedTags(html) def test_add_purchase_form(self): html = self.get("games:add_purchase").content.decode() - self.assertIn("add_purchase.js", html) + self.assertIn("dist/add_purchase.js", html) self.assertIn("Submit & Create Session", html) self.assertIn("", html) self.assertNoEscapedTags(html) diff --git a/games/static/js/add_purchase.js b/ts/add_purchase.ts similarity index 53% rename from games/static/js/add_purchase.js rename to ts/add_purchase.ts index 11cfdea..b80b650 100644 --- a/games/static/js/add_purchase.js +++ b/ts/add_purchase.ts @@ -1,58 +1,70 @@ -import { getEl, disableElementsWhenTrue, onSwap } from "./utils.js"; +import { disableElementsWhenTrue, onSwap } from "./utils.js"; + +interface SearchSelectOption { + value: string; + label: string; + data: Record; +} + +interface SearchSelectChangeDetail { + name: string; + values: string[]; + last: SearchSelectOption | null; +} // Switch between a single bundle price and one price per game. The per-game // inputs are the selection-fields element; this only sets the policy: the // hidden pricing_mode the view reads, the element's "active" flag, and whether // the bundle Price field is shown. -function applyPricingMode(separate) { - const pricingMode = getEl("#id_pricing_mode"); +function applyPricingMode(separate: boolean): void { + const pricingMode = document.querySelector("#id_pricing_mode"); if (pricingMode) pricingMode.value = separate ? "per_game" : "combined"; const selectionFields = document.querySelector("selection-fields"); if (selectionFields) selectionFields.setAttribute("active", separate ? "true" : "false"); - const priceInput = getEl("#id_price"); + const priceInput = document.querySelector("#id_price"); if (priceInput) { const wrapper = priceInput.closest("div"); if (wrapper) wrapper.classList.toggle("hidden", separate); } } -// The games field is now a SearchSelect widget (a
, not a ), so we // react to its custom "search-select:change" event instead of syncing a select. document.addEventListener("search-select:change", (event) => { - if (event.detail.name !== "games") return; + const detail = (event as CustomEvent).detail; + if (detail.name !== "games") return; // Auto-fill platform from the clicked option's data-platform. - const last = event.detail.last; + const last = detail.last; const platformId = last && last.data ? last.data.platform : ""; if (platformId) { - const platformEl = getEl("#id_platform"); - if (platformEl) platformEl.value = platformId; + const platformElement = document.querySelector("#id_platform"); + if (platformElement) platformElement.value = platformId; } // The combined/per-game choice is only meaningful with 2+ games. Reveal the // checkbox there; below the threshold, fall back to a single bundle price. - const separateRow = getEl("#separate-prices-row"); - const multipleGames = event.detail.values.length >= 2; + const separateRow = document.querySelector("#separate-prices-row"); + const multipleGames = detail.values.length >= 2; if (separateRow) separateRow.classList.toggle("hidden", !multipleGames); if (!multipleGames) { - const checkbox = getEl("#id_separate_prices"); + const checkbox = document.querySelector("#id_separate_prices"); if (checkbox) checkbox.checked = false; applyPricingMode(false); } }); onSwap("#id_separate_prices", (checkbox) => { - checkbox.addEventListener("change", () => applyPricingMode(checkbox.checked)); + checkbox.addEventListener("change", () => + applyPricingMode((checkbox as HTMLInputElement).checked) + ); }); -function setupElementHandlers() { - disableElementsWhenTrue("#id_type", "game", [ - "#id_name", - "#id_related_game", - ]); +function setupElementHandlers(): void { + disableElementsWhenTrue("#id_type", "game", ["#id_name", "#id_related_game"]); } onSwap("#id_type", (typeSelect) => {