mirror of
https://github.com/we-promise/sure.git
synced 2026-07-12 12:55:20 +00:00
* fix(settings): give the MCP copy button success feedback The MCP server URL Copy button copied to the clipboard but showed no feedback. It is a DS::Button (single icon), but clipboard_controller's showSuccess() unconditionally toggled iconDefault/iconSuccess targets — which that markup does not have — so it threw right after the copy and the user saw nothing. Guard the icon-swap path (still used by invite codes, MFA and profiles) and add a fallback that briefly flips the button's own label to Copied! via a new copiedText value. Wire it up on the MCP page. * fix(settings): capture copy button before async clipboard resolve event.currentTarget is null by the time the writeText().then() callback runs (it's only valid during event dispatch), so showSuccess received null and the label never flipped. Capture the button synchronously in copy() and pass it through. Verified in-browser: Copy -> Copied! -> Copy. * refactor(clipboard): unify feedback reset delay, harden label lookup Extract a shared RESET_DELAY_MS so the icon-swap and label-flash paths last the same duration when both copy buttons render on one page. Scope the label lookup to span.truncate (the DS::Button text node) so it ignores any future icon span. Per review feedback. --------- Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com>
70 lines
2.3 KiB
JavaScript
70 lines
2.3 KiB
JavaScript
import { Controller } from "@hotwired/stimulus";
|
|
|
|
// Single source of truth so the icon-swap and label-flash feedback last the
|
|
// same time when both copy buttons appear on one page.
|
|
const RESET_DELAY_MS = 2000;
|
|
|
|
export default class extends Controller {
|
|
static targets = ["source", "iconDefault", "iconSuccess"];
|
|
static values = { copiedText: String };
|
|
|
|
copy(event) {
|
|
event.preventDefault();
|
|
// Capture the button now: `event.currentTarget` is reset to null once the
|
|
// event finishes dispatching, so it can't be read inside the async `.then`.
|
|
const button = event.currentTarget;
|
|
const text = this.sourceTarget?.textContent;
|
|
if (!text) return;
|
|
|
|
navigator.clipboard
|
|
.writeText(text)
|
|
.then(() => {
|
|
this.showSuccess(button);
|
|
})
|
|
.catch((error) => {
|
|
console.error("Failed to copy text: ", error);
|
|
});
|
|
}
|
|
|
|
showSuccess(button) {
|
|
// Markup that ships explicit default/success icons (invite codes, MFA,
|
|
// profiles) toggles between them.
|
|
if (this.hasIconDefaultTarget && this.hasIconSuccessTarget) {
|
|
this.iconDefaultTarget.classList.add("hidden");
|
|
this.iconSuccessTarget.classList.remove("hidden");
|
|
setTimeout(() => {
|
|
this.iconDefaultTarget.classList.remove("hidden");
|
|
this.iconSuccessTarget.classList.add("hidden");
|
|
}, RESET_DELAY_MS);
|
|
return;
|
|
}
|
|
|
|
// A single-icon button (e.g. DS::Button) has no icons to swap, so confirm
|
|
// the copy by briefly flipping the button's own label.
|
|
this.flashLabel(button);
|
|
}
|
|
|
|
flashLabel(button) {
|
|
// DS::Button renders its icon as an <svg> and its text in `span.truncate`,
|
|
// so scope to that class rather than the first <span> in case an icon ever
|
|
// ships wrapped in a span.
|
|
const label = button?.querySelector("span.truncate") ?? button?.querySelector("span");
|
|
if (!label || !this.hasCopiedTextValue) return;
|
|
|
|
clearTimeout(this.labelResetTimer);
|
|
if (this.originalLabel == null) {
|
|
this.originalLabel = label.textContent;
|
|
}
|
|
|
|
label.textContent = this.copiedTextValue;
|
|
this.labelResetTimer = setTimeout(() => {
|
|
label.textContent = this.originalLabel;
|
|
this.originalLabel = null;
|
|
}, RESET_DELAY_MS);
|
|
}
|
|
|
|
disconnect() {
|
|
clearTimeout(this.labelResetTimer);
|
|
}
|
|
}
|