1import { Controller } from "@hotwired/stimulus" 2 3const FILTER_DEBOUNCE_MS = 800 4 5// Update-collaborator-permissions-on-multiple-sites offcanvas: fetch the collaborator's 6// updatable sites, render checkbox rows, debounced search + environment filter, 7// select-all, and CSV-serialize the selected site ids into the hidden field on submit 8// (disabling the site_ids[] checkboxes so they are not also sent as query params). 9export default class extends Controller { 10 static targets = ["searchTerm", "testSite", "selectAll", "siteListing", "csv"] 11 // The finished endpoint URL, built server-side by the route helper. It already carries the 12 // account context and the escaped collaborator email, so this controller neither reconstructs 13 // the /a/:account_ctx prefix from window.location nor interpolates an email into a path. 14 static values = { endpoint: String } 15 16 connect() { 17 this.loadSites() 18 } 19 20 disconnect() { 21 if (this.filterTimeout) clearTimeout(this.filterTimeout) 22 } 23 24 loadSites() { 25 if (!this.endpointValue) { 26 console.error("Missing sites endpoint") 27 this.siteListingTarget.innerHTML = 28 '<div class="alert alert-warning">Unable to load sites. Please refresh the page.</div>' 29 return 30 } 31 32 fetch(this.endpointValue, { 33 method: "GET", 34 headers: { Accept: "application/json", "Content-Type": "application/json" }, 35 }) 36 .then((response) => { 37 if (!response.ok) throw new Error(`Server responded with ${response?.status}`) 38 return response.json() 39 }) 40 .then((sites) => this.renderSites(sites)) 41 .catch((error) => { 42 if (error instanceof SyntaxError) return 43 toastr.error("Could not retrieve updatable sites for this collaborator. Please try refreshing this page.") 44 }) 45 } 46 47 renderSites(sites) { 48 if (!Array.isArray(sites)) sites = [] 49 sites.sort((a, b) => (a.url > b.url ? 1 : -1)) 50 this.siteListingTarget.innerHTML = "" 51 sites.forEach((site) => this.siteListingTarget.append(this.buildSiteRow(site))) 52 } 53 54 buildSiteRow(site) { 55 const wrapper = document.createElement("div") 56 wrapper.className = "form-check" 57 wrapper.dataset.siteRow = "" 58 wrapper.dataset.sitePattern = `${site.id}||${site.url}` 59 wrapper.dataset.testEnvironment = site.testSite 60 61 const input = document.createElement("input") 62 input.className = "form-check-input" 63 input.dataset.siteCheck = "" 64 input.type = "checkbox" 65 input.id = `update-collaborator-on-multiple-sites-${site.id}` 66 input.name = "site_ids[]" 67 input.value = site.id 68 69 const label = document.createElement("label") 70 label.className = "form-check-label" 71 label.htmlFor = input.id 72 label.textContent = site.url 73 74 wrapper.append(input, label) 75 return wrapper 76 } 77 78 search() { 79 clearTimeout(this.filterTimeout) 80 this.filterTimeout = setTimeout(() => this.applyFilter(), FILTER_DEBOUNCE_MS) 81 } 82 83 applyFilter() { 84 const term = this.searchTermTarget.value.trim() 85 const environment = this.testSiteTarget.value 86 this.siteListingTarget.querySelectorAll("[data-site-row]").forEach((row) => { 87 const matchesTerm = term === "" || row.dataset.sitePattern.includes(term) 88 const matchesEnv = environment === "" || row.dataset.testEnvironment === environment 89 row.style.display = matchesTerm && matchesEnv ? "" : "none" 90 }) 91 } 92 93 selectAllSites() { 94 const checked = this.selectAllTarget.checked 95 this.siteListingTarget.querySelectorAll("[data-site-check]").forEach((checkbox) => { 96 checkbox.checked = checked 97 }) 98 } 99 100 submit(event) { 101 const checked = this.siteListingTarget.querySelectorAll("[data-site-check]:checked") 102 if (checked.length === 0) { 103 toastr.error("Please select at least one site") 104 event.preventDefault() 105 return 106 } 107 108 this.csvTarget.value = Array.from(checked).map((checkbox) => checkbox.value).join(",") 109 // disable the site_ids[] checkboxes so they are not also submitted as query params 110 checked.forEach((checkbox) => { checkbox.disabled = true }) 111 } 112}
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.