1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["button", "progress", "bar", "message"] 5 static values = { 6 url: String, 7 label: String 8 } 9 10 connect() { 11 this.pollTimer = null 12 this.originallyDisabled = this.hasButtonTarget && this.buttonTarget.disabled 13 } 14 15 disconnect() { 16 this.stopPolling() 17 } 18 19 async start(event) { 20 event.preventDefault() 21 if (this.pollTimer) return 22 if (this.hasButtonTarget && this.buttonTarget.disabled) return 23 24 this.setRunningState() 25 try { 26 const response = await fetch(this.urlValue, { 27 method: "POST", 28 headers: this.requestHeaders(), 29 credentials: "same-origin" 30 }) 31 if (!response.ok) throw new Error(`Request failed (${response.status})`) 32 33 const data = await response.json() 34 this.pollUrl = data.poll_url 35 this.startPolling() 36 } catch (error) { 37 this.resetButton() 38 this.showMessage(this.errorText()) 39 console.error("Background job start failed:", error) 40 } 41 } 42 43 startPolling() { 44 this.pollTimer = setInterval(() => this.poll(), 1000) 45 this.poll() 46 } 47 48 stopPolling() { 49 if (this.pollTimer) { 50 clearInterval(this.pollTimer) 51 this.pollTimer = null 52 } 53 } 54 55 async poll() { 56 try { 57 const response = await fetch(this.pollUrl, { 58 headers: { Accept: "application/json" }, 59 credentials: "same-origin" 60 }) 61 if (!response.ok) throw new Error(`Poll failed (${response.status})`) 62 63 const status = await response.json() 64 this.updateProgress(status.progress, status.message) 65 66 if (status.status === "completed") { 67 this.stopPolling() 68 this.resetButton() 69 if (status.download_url) { 70 window.location.href = status.download_url 71 } else if (status.redirect_url) { 72 window.location.href = status.redirect_url 73 } 74 } else if (status.status === "failed") { 75 this.stopPolling() 76 this.resetButton() 77 this.showMessage(status.message || this.errorText()) 78 } 79 } catch (error) { 80 this.stopPolling() 81 this.resetButton() 82 this.showMessage(this.errorText()) 83 console.error("Background job poll failed:", error) 84 } 85 } 86 87 setRunningState() { 88 if (this.hasProgressTarget) this.progressTarget.classList.remove("hidden") 89 if (this.hasButtonTarget) { 90 this.buttonTarget.disabled = true 91 this.buttonTarget.textContent = this.runningText() 92 } 93 this.updateProgress(0, null) 94 } 95 96 resetButton() { 97 if (this.hasProgressTarget) this.progressTarget.classList.add("hidden") 98 if (this.hasButtonTarget) { 99 this.buttonTarget.disabled = this.originallyDisabled 100 this.buttonTarget.textContent = this.labelValue 101 } 102 if (this.hasMessageTarget) this.messageTarget.textContent = "" 103 } 104 105 updateProgress(progress, message) { 106 if (this.hasBarTarget) this.barTarget.style.width = `${progress || 0}%` 107 if (this.hasMessageTarget && message) this.messageTarget.textContent = message 108 } 109 110 showMessage(text) { 111 if (this.hasMessageTarget) this.messageTarget.textContent = text 112 } 113 114 requestHeaders() { 115 const headers = { 116 Accept: "application/json", 117 "X-Requested-With": "XMLHttpRequest" 118 } 119 const token = document.querySelector('meta[name="csrf-token"]')?.content 120 if (token) headers["X-CSRF-Token"] = token 121 return headers 122 } 123 124 runningText() { 125 return this.element.dataset.backgroundJobRunningTextValue || "Export in progress..." 126 } 127 128 errorText() { 129 return this.element.dataset.backgroundJobErrorTextValue || "Export failed. Please try again." 130 } 131};
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.