This is an automated email from the ASF dual-hosted git repository. sbp pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit 76a4d223e6c003eca9a31d29e89621309d2d2e42 Author: Sean B. Palmer <[email protected]> AuthorDate: Wed Dec 10 15:18:03 2025 +0000 Move announcement and ignore form scripts to separate files --- atr/get/announce.py | 129 +++--------------------------------- atr/get/ignores.py | 33 +-------- atr/static/js/announce-preview.js | 118 +++++++++++++++++++++++++++++++++ atr/static/js/ignore-form-change.js | 15 +++++ 4 files changed, 144 insertions(+), 151 deletions(-) diff --git a/atr/get/announce.py b/atr/get/announce.py index b8034c3..fddc8e5 100644 --- a/atr/get/announce.py +++ b/atr/get/announce.py @@ -82,6 +82,7 @@ async def selected(session: web.Committer, project_name: str, version_name: str) title=f"Announce and distribute {release.project.display_name} {release.version}", description=f"Announce and distribute {release.project.display_name} {release.version} as a release.", content=content, + javascripts=["announce-preview"], ) @@ -133,6 +134,10 @@ async def _render_page( "body": default_body, } + preview_url = util.as_url( + post.preview.announce_preview, project_name=release.project.name, version_name=release.version + ) + form.render_block( page, model_cls=shared.announce.AnnounceForm, @@ -149,7 +154,9 @@ async def _render_page( wider_widgets=True, ) - page.append(_render_javascript(release, download_path_description)) + # TODO: Would be better if we could add data-preview-url to the form + page.append(htpy.div("#announce-config.d-none", data_preview_url=preview_url)) + return page.collect() @@ -213,6 +220,7 @@ def _render_mailing_list_with_warning(choices: list[tuple[str, str]], default_va def _render_download_path_field(default_value: str, description: str) -> htm.Element: """Render the download path suffix field with custom help text.""" + base_text = description.split(" plus this suffix")[0] if " plus this suffix" in description else description return htm.div[ htpy.input( "#download_path_suffix.form-control", @@ -220,122 +228,5 @@ def _render_download_path_field(default_value: str, description: str) -> htm.Ele name="download_path_suffix", value=default_value, ), - htm.div(".form-text.text-muted.mt-2")[description], + htpy.div(".form-text.text-muted.mt-2", data_base_text=base_text)[description], ] - - -def _render_javascript(release: sql.Release, download_path_description: str) -> htm.Element: - """Render the JavaScript for email preview and path validation.""" - preview_url = util.as_url( - post.preview.announce_preview, project_name=release.project.name, version_name=release.version - ) - base_text = ( - download_path_description.split(" plus this suffix")[0] - if " plus this suffix" in download_path_description - else download_path_description - ) - - js_code = f""" - document.addEventListener("DOMContentLoaded", () => {{ - let debounceTimeout; - const debounceDelay = 500; - - const bodyTextarea = document.getElementById("body"); - const textPreviewContent = document.getElementById("announce-body-preview-content"); - const announceForm = document.querySelector("form.atr-canary"); - - if (!bodyTextarea || !textPreviewContent || !announceForm) {{ - console.error("Required elements for announce preview not found. Exiting."); - return; - }} - - const previewUrl = "{preview_url}"; - const csrfTokenInput = announceForm.querySelector('input[name="csrf_token"]'); - - if (!previewUrl || !csrfTokenInput) {{ - console.error("Required data attributes or CSRF token not found for announce preview."); - return; - }} - const csrfToken = csrfTokenInput.value; - - function fetchAndUpdateAnnouncePreview() {{ - const bodyContent = bodyTextarea.value; - - fetch(previewUrl, {{ - method: "POST", - headers: {{ - "Content-Type": "application/x-www-form-urlencoded", - "X-CSRFToken": csrfToken - }}, - body: new URLSearchParams({{ - "body": bodyContent, - "csrf_token": csrfToken - }}) - }}) - .then(response => {{ - if (!response.ok) {{ - return response.text().then(text => {{ - throw new Error(`HTTP error ${{response.status}}: ${{text}}`) - }}); - }} - return response.text(); - }}) - .then(previewText => {{ - textPreviewContent.textContent = previewText; - }}) - .catch(error => {{ - console.error("Error fetching email preview:", error); - textPreviewContent.textContent = `Error loading preview:\\n${{error.message}}`; - }}); - }} - - bodyTextarea.addEventListener("input", () => {{ - clearTimeout(debounceTimeout); - debounceTimeout = setTimeout(fetchAndUpdateAnnouncePreview, debounceDelay); - }}); - - fetchAndUpdateAnnouncePreview(); - - {render.copy_javascript()} - - const pathInput = document.getElementById("download_path_suffix"); - const pathHelpText = pathInput ? pathInput.parentElement.querySelector(".form-text") : null; - - if (pathInput && pathHelpText) {{ - const baseText = "{base_text}"; - let pathDebounce; - - function updatePathHelpText() {{ - let suffix = pathInput.value; - if (suffix.includes("..") || suffix.includes("//")) {{ - pathHelpText.textContent = "Download path suffix must not contain .. or //"; - return; - }} - if (suffix.startsWith("./")) {{ - suffix = suffix.substring(1); - }} else if (suffix === ".") {{ - suffix = "/"; - }} - if (!suffix.startsWith("/")) {{ - suffix = "/" + suffix; - }} - if (!suffix.endsWith("/")) {{ - suffix = suffix + "/"; - }} - if (suffix.includes("/.")) {{ - pathHelpText.textContent = "Download path suffix must not contain /."; - return; - }} - pathHelpText.textContent = baseText + suffix; - }} - - pathInput.addEventListener("input", () => {{ - clearTimeout(pathDebounce); - pathDebounce = setTimeout(updatePathHelpText, 10); - }}); - updatePathHelpText(); - }} - }}); - """ - - return htpy.script[markupsafe.Markup(js_code)] diff --git a/atr/get/ignores.py b/atr/get/ignores.py index 91c29bf..a6575e0 100644 --- a/atr/get/ignores.py +++ b/atr/get/ignores.py @@ -15,10 +15,6 @@ # specific language governing permissions and limitations # under the License. -from typing import Final - -import markupsafe - import atr.blueprints.get as get import atr.form as form import atr.htm as htm @@ -30,23 +26,6 @@ import atr.template as template import atr.util as util import atr.web as web -# TODO: Port to TypeScript and move to static files -_UPDATE_IGNORE_FORM: Final[str] = """ -document.querySelectorAll("table.page-details input.form-control").forEach(function (input) { - var row = input.closest("tr"); - var updateBtn = row.querySelector("button.btn-primary"); - function check() { - if (input.value !== input.dataset.value) { - updateBtn.classList.remove("disabled"); - } else { - updateBtn.classList.add("disabled"); - } - } - input.addEventListener("input", check); - check(); -}); -""" - @get.committer("/ignores/<committee_name>") async def ignores(session: web.Committer, committee_name: str) -> str | web.WerkzeugResponse: @@ -59,10 +38,9 @@ async def ignores(session: web.Committer, committee_name: str) -> str | web.Werk htm.p[f"Manage ignored checks for committee {committee_name}."], _add_ignore(committee_name), _existing_ignores(ignores), - _script_dom_loaded(_UPDATE_IGNORE_FORM), ] - return await template.blank("Ignored checks", content) + return await template.blank("Ignored checks", content, javascripts=["ignore-form-change"]) def _add_ignore(committee_name: str) -> htm.Element: @@ -133,12 +111,3 @@ def _existing_ignores(ignores: list[sql.CheckResultIgnore]) -> htm.Element: htm.h2["Existing ignores"], [_check_result_ignore_card(cri) for cri in ignores] or htm.p["No ignores found."], ] - - -def _script_dom_loaded(text: str) -> htm.Element: - script_text = markupsafe.Markup(f""" -document.addEventListener("DOMContentLoaded", function () {{ -{text} -}}); -""") - return htm.script[script_text] diff --git a/atr/static/js/announce-preview.js b/atr/static/js/announce-preview.js new file mode 100644 index 0000000..691ef95 --- /dev/null +++ b/atr/static/js/announce-preview.js @@ -0,0 +1,118 @@ +document.addEventListener("DOMContentLoaded", () => { + let debounceTimeout; + const debounceDelay = 500; + + const bodyTextarea = document.getElementById("body"); + const textPreviewContent = document.getElementById("announce-body-preview-content"); + const announceForm = document.querySelector("form.atr-canary"); + const configElement = document.getElementById("announce-config"); + + if (!bodyTextarea || !textPreviewContent || !announceForm) { + console.error("Required elements for announce preview not found. Exiting."); + return; + } + + const previewUrl = configElement ? configElement.dataset.previewUrl : null; + const csrfTokenInput = announceForm.querySelector('input[name="csrf_token"]'); + + if (!previewUrl || !csrfTokenInput) { + console.error("Required data attributes or CSRF token not found for announce preview."); + return; + } + const csrfToken = csrfTokenInput.value; + + function fetchAndUpdateAnnouncePreview() { + const bodyContent = bodyTextarea.value; + + fetch(previewUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-CSRFToken": csrfToken + }, + body: new URLSearchParams({ + "body": bodyContent, + "csrf_token": csrfToken + }) + }) + .then(response => { + if (!response.ok) { + return response.text().then(text => { + throw new Error(`HTTP error ${response.status}: ${text}`) + }); + } + return response.text(); + }) + .then(previewText => { + textPreviewContent.textContent = previewText; + }) + .catch(error => { + console.error("Error fetching email preview:", error); + textPreviewContent.textContent = `Error loading preview:\n${error.message}`; + }); + } + + bodyTextarea.addEventListener("input", () => { + clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(fetchAndUpdateAnnouncePreview, debounceDelay); + }); + + fetchAndUpdateAnnouncePreview(); + + // Copy variable button functionality + document.querySelectorAll(".copy-var-btn").forEach(btn => { + btn.addEventListener("click", () => { + const variable = btn.dataset.variable; + navigator.clipboard.writeText(variable).then(() => { + const originalText = btn.textContent; + btn.textContent = "Copied!"; + btn.classList.remove("btn-outline-secondary"); + btn.classList.add("btn-success"); + setTimeout(() => { + btn.textContent = originalText; + btn.classList.remove("btn-success"); + btn.classList.add("btn-outline-secondary"); + }, 1500); + }); + }); + }); + + // Download path suffix validation + const pathInput = document.getElementById("download_path_suffix"); + const pathHelpText = pathInput ? pathInput.parentElement.querySelector(".form-text") : null; + + if (pathInput && pathHelpText) { + const baseText = pathHelpText.dataset.baseText || ""; + let pathDebounce; + + function updatePathHelpText() { + let suffix = pathInput.value; + if (suffix.includes("..") || suffix.includes("//")) { + pathHelpText.textContent = "Download path suffix must not contain .. or //"; + return; + } + if (suffix.startsWith("./")) { + suffix = suffix.substring(1); + } else if (suffix === ".") { + suffix = "/"; + } + if (!suffix.startsWith("/")) { + suffix = "/" + suffix; + } + if (!suffix.endsWith("/")) { + suffix = suffix + "/"; + } + if (suffix.includes("/.")) { + pathHelpText.textContent = "Download path suffix must not contain /."; + return; + } + pathHelpText.textContent = baseText + suffix; + } + + pathInput.addEventListener("input", () => { + clearTimeout(pathDebounce); + pathDebounce = setTimeout(updatePathHelpText, 10); + }); + updatePathHelpText(); + } +}); diff --git a/atr/static/js/ignore-form-change.js b/atr/static/js/ignore-form-change.js new file mode 100644 index 0000000..a852346 --- /dev/null +++ b/atr/static/js/ignore-form-change.js @@ -0,0 +1,15 @@ +document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll("table.page-details input.form-control").forEach(function (input) { + var row = input.closest("tr"); + var updateBtn = row.querySelector("button.btn-primary"); + function check() { + if (input.value !== input.dataset.value) { + updateBtn.classList.remove("disabled"); + } else { + updateBtn.classList.add("disabled"); + } + } + input.addEventListener("input", check); + check(); + }); +}); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
