1// app/javascript/controllers/form_recipients_controller.js 2import { Controller } from "@hotwired/stimulus" 3 4// Manages the list of recipient-email rows on a form. 5// Create/Edit modal: rows submit with the surrounding form (autoSave off). 6// Builder settings drawer: autoSave on â add/remove/blur persist immediately 7// via POST /forms/:id/update_recipients (mirrors hidden_tags_controller). 8export default class extends Controller { 9 static targets = ["template", "container", "row", "saveAlert", "errorAlert"] 10 static values = { 11 formId: String, 12 autoSave: { type: Boolean, default: false } 13 } 14 15 add(event) { 16 event.preventDefault() 17 this.containerTarget.insertAdjacentHTML("beforeend", this.templateTarget.innerHTML) 18 const input = this.containerTarget.lastElementChild?.querySelector("input[type='email']") 19 if (input) input.focus() 20 } 21 22 remove(event) { 23 event.preventDefault() 24 event.currentTarget.closest(".form-recipient-row")?.remove() 25 if (this.autoSaveValue) this.save() 26 } 27 28 save() { 29 if (!this.autoSaveValue) return 30 if (!this.allValid()) { 31 this.showError("Fix the highlighted email address â changes are not saved yet.") 32 return 33 } 34 35 // Snapshot the payload now, but chain the POST behind any in-flight save so 36 // overlapping blur + remove saves can't land out of order on the server. 37 const payload = JSON.stringify({ recipient_emails: this.emails() }) 38 this.pendingSave = (this.pendingSave || Promise.resolve()).then(() => this.postRecipients(payload)) 39 } 40 41 postRecipients(payload) { 42 return fetch(`/forms/${this.formIdValue}/update_recipients`, { 43 method: "POST", 44 headers: { 45 "Content-Type": "application/json", 46 // Optional chain: csrf_meta_tags renders nothing when forgery protection is off (test env) 47 "X-CSRF-Token": document.querySelector("meta[name='csrf-token']")?.content || "", 48 "Accept": "application/json" 49 }, 50 body: payload 51 }) 52 .then((response) => response.json()) 53 .then((data) => { 54 if (data.success) { 55 this.showSaved() 56 } else { 57 this.showError((data.errors || []).join(", ")) 58 } 59 }) 60 .catch(() => this.showError("Could not save recipients.")) 61 } 62 63 emails() { 64 return this.inputs().map((input) => input.value.trim()).filter((value) => value !== "") 65 } 66 67 inputs() { 68 return Array.from(this.containerTarget.querySelectorAll("input[type='email']")) 69 } 70 71 allValid() { 72 let valid = true 73 this.inputs().forEach((input) => { 74 const ok = input.value.trim() === "" || input.checkValidity() 75 input.classList.toggle("input-error", !ok) 76 if (!ok) valid = false 77 }) 78 return valid 79 } 80 81 showSaved() { 82 if (this.hasErrorAlertTarget) this.errorAlertTarget.classList.add("hidden") 83 if (!this.hasSaveAlertTarget) return 84 85 this.saveAlertTarget.classList.remove("hidden") 86 setTimeout(() => this.saveAlertTarget.classList.add("hidden"), 2000) 87 } 88 89 showError(message) { 90 if (!this.hasErrorAlertTarget) return 91 92 this.errorAlertTarget.textContent = message 93 this.errorAlertTarget.classList.remove("hidden") 94 } 95};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.