1import { Controller } from '@hotwired/stimulus'; 2 3/** 4 * docs-sync-writer 5 * 6 * Drives the "Generate" button on each drift-finding card in 7 * /admin/developer/docs-sync. One controller wraps the entire findings list; 8 * individual Generate buttons pass the finding index via data attributes. 9 * 10 * Targets: modal, draftTitle, draftContent, saveBtn 11 * Values: generateUrl (String), writeUrl (String), csrfToken (String), days (Number) 12 */ 13export default class extends Controller { 14 static targets = ['modal', 'draftTitle', 'draftContent', 'saveBtn']; 15 static values = { 16 generateUrl: String, 17 writeUrl: String, 18 csrfToken: String, 19 days: Number, 20 }; 21 22 // Retained between generate â save 23 #currentPath = ''; 24 #currentDraft = ''; 25 26 // ââ Generate ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 27 28 async generate(event) { 29 const btn = event.currentTarget;
30 const findingIndex = btn.dataset.findingIndex; 31 const moduleName = btn.dataset.module; 32 const playbookPath = btn.dataset.playbookPath; 33 34 btn.disabled = true; 35 btn.dataset.orig = btn.innerHTML; 36 btn.innerHTML = '<span class="loading loading-spinner loading-xs"></span> Generatingâ¦'; 37 38 try { 39 const resp = await fetch(this.generateUrlValue, { 40 method: 'POST', 41 headers: { 42 'Content-Type': 'application/json', 43 'X-Requested-With': 'XMLHttpRequest', 44 }, 45 body: JSON.stringify({ finding_index: Number(findingIndex), days: this.daysValue }), 46 }); 47 48 const data = await resp.json(); 49 50 if (!resp.ok) { 51 this.#showDraft(`Error: ${data.error ?? 'Generation failed'}`, moduleName, playbookPath, true); 52 return; 53 } 54 55 this.#currentDraft = data.draft; 56 this.#currentPath = data.playbook_path ?? playbookPath; 57 this.#showDraft(data.draft, moduleName, this.#currentPath, false); 58 59 } catch (err) { 60 this.#showDraft(`Network error: ${err.message}`, moduleName, playbookPath, true); 61 } finally { 62 btn.disabled = false; 63 btn.innerHTML = btn.dataset.orig; 64 } 65 } 66 67 // ââ Save âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 68 69 async save() { 70 const btn = this.saveBtnTarget; 71 btn.disabled = true; 72 btn.innerHTML = '<span class="loading loading-spinner loading-xs"></span> Savingâ¦'; 73 74 try { 75 const resp = await fetch(this.writeUrlValue, { 76 method: 'POST', 77 headers: { 78 'Content-Type': 'application/json', 79 'X-Requested-With': 'XMLHttpRequest', 80 }, 81 body: JSON.stringify({ 82 playbook_path: this.#currentPath, 83 content: this.#currentDraft, 84 _token: this.csrfTokenValue, 85 }), 86 }); 87 88 const data = await resp.json(); 89 90 if (!resp.ok) { 91 btn.innerHTML = data.error ?? 'Error'; 92 btn.classList.add('btn-error'); 93 btn.disabled = false; 94 return; 95 } 96 97 btn.innerHTML = 'â Saved'; 98 btn.classList.remove('btn-primary'); 99 btn.classList.add('btn-success'); 100 setTimeout(() => this.modalTarget.close(), 1800); 101 102 } catch (err) { 103 btn.innerHTML = 'Error'; 104 btn.classList.add('btn-error'); 105 btn.disabled = false; 106 } 107 } 108 109 // ââ Close âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 110 111 close() { 112 this.modalTarget.close(); 113 } 114 115 // ââ Private âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 116 117 #showDraft(draft, moduleName, path, isError) { 118 this.draftTitleTarget.textContent = isError 119 ? `Error â ${moduleName}` 120 : `Generated Playbook â ${moduleName}`; 121 this.draftContentTarget.textContent = draft; 122 this.saveBtnTarget.disabled = isError; 123 this.saveBtnTarget.classList.remove('btn-success', 'btn-error'); 124 this.saveBtnTarget.classList.add('btn-primary'); 125 this.saveBtnTarget.innerHTML = `Save to ${path}`; 126 this.modalTarget.showModal(); 127 } 128}
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.