Files
webhooker/static/js/app.js
T
clawbot 57e6ecaf0f
check / check (push) Waiting to run
Targets section: one Add, then a type and Next, then its fields (closes #370)
The webhook page's targets section lists only its targets until Add is
clicked. Add shows a choice of target type with Next; Next shows only
that type's fields, with Save and Cancel. The `database` and `log`
types have no URL field, and the `slack` form gains max retries.

A refused target now shows the webhook page again with the form open on
its type, the values entered and the reason, instead of a bare text
page. Target validation returns that message rather than writing the
response; newTarget validates a whole new target for reuse by the
new-webhook page. The edit page still answers a refusal in plain text.

Model: opus-5-5
2026-10-02 16:59:48 +00:00

135 lines
4.2 KiB
JavaScript

// Webhooker client-side JavaScript
// Copy-to-clipboard, as progressive enhancement.
//
// Markup renders each copy button with the `hidden` attribute and a
// `data-copy-target` pointing at the id of the element holding the
// text. This script reveals a button only once it has both a resolvable
// target and a usable Clipboard API, so a browser without either shows
// no button at all and the text stays selectable.
(function () {
"use strict";
const revertDelayMs = 2000;
function flash(button, message) {
const original = button.getAttribute("data-copy-label");
button.textContent = message;
window.setTimeout(function () {
button.textContent = original;
}, revertDelayMs);
}
function wire(button) {
const target = document.getElementById(
button.getAttribute("data-copy-target")
);
if (!target) {
return;
}
button.setAttribute("data-copy-label", button.textContent);
button.addEventListener("click", function () {
navigator.clipboard.writeText(target.textContent.trim()).then(
function () {
flash(button, "Copied");
},
function () {
flash(button, "Copy failed");
}
);
});
button.removeAttribute("hidden");
}
function init() {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
return;
}
const buttons = document.querySelectorAll("[data-copy-target]");
buttons.forEach(wire);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
// Alpine.js components.
//
// The pages' Content-Security-Policy forbids eval, so the UI loads
// Alpine's CSP build, which cannot run expressions written in the
// markup: a directive in templates/ may only name a property or method,
// and each x-data names a component registered here. This script runs
// before Alpine, whose script tag is deferred, so this listener is in
// place when Alpine starts.
document.addEventListener("alpine:init", function () {
"use strict";
// Something a click shows and hides: the mobile menu, an add form,
// an event in the event log, a delivery's attempts.
window.Alpine.data("collapsible", function () {
return {
open: false,
toggle() {
this.open = !this.open;
},
get closed() {
return !this.open;
},
// Turns a downward caret up while open.
get caretClass() {
return { "rotate-180": this.open };
},
};
});
// The targets section's add target form, in three steps: closed,
// choosing a type, then filling in that type's fields. targetType
// is empty until Next takes it from the type select.
//
// A refused submission comes back with the type it was submitted
// with in the section's data-type, and starts on that type's fields.
window.Alpine.data("targetForm", function () {
return {
choosing: false,
targetType: "",
init() {
this.targetType = this.$root.dataset.type;
},
add() {
this.choosing = true;
},
next() {
this.targetType = this.$refs.type.value;
this.choosing = false;
},
cancel() {
this.choosing = false;
this.targetType = "";
},
get filling() {
return this.targetType !== "";
},
get closed() {
return !this.choosing && !this.filling;
},
get isHttp() {
return this.targetType === "http";
},
get isSlack() {
return this.targetType === "slack";
},
get isDatabase() {
return this.targetType === "database";
},
get isLog() {
return this.targetType === "log";
},
};
});
});