| 1 |
|
| 2 |
* |
| 3 |
* Loaded once globally (base.html), like synckit-billing.js. Each tab partial |
| 4 |
* calls window.initSyncKitTab(config) on render to wire its create form and |
| 5 |
* pass tab-specific bits (edit-control classes, whether Regenerate shows the |
| 6 |
* new key inline, the create-form mode). All table-row buttons call the global |
| 7 |
* syncKit* functions below. No template value is ever interpolated into a JS |
| 8 |
* string: ids/slugs/names arrive via data-* attributes, the project list via a |
| 9 |
* <template> of <option>s — so titles/slugs can't break out of a string. |
| 10 |
'use strict'; |
| 11 |
|
| 12 |
(function () { |
| 13 |
|
| 14 |
|
| 15 |
var cfg = { |
| 16 |
editInputClass: 'proj-synckit-slug-input input--sm w-120', |
| 17 |
editBtnClass: 'proj-synckit-btn-row', |
| 18 |
regenInline: false, |
| 19 |
}; |
| 20 |
|
| 21 |
function netError() { showToast('Network error. Please check your connection and try again.'); } |
| 22 |
function reloadTab() { document.getElementById('tab-synckit').click(); } |
| 23 |
|
| 24 |
|
| 25 |
|
| 26 |
function makeCopyButton(text, className) { |
| 27 |
var btn = document.createElement('button'); |
| 28 |
btn.type = 'button'; |
| 29 |
btn.className = className; |
| 30 |
btn.textContent = 'Copy'; |
| 31 |
btn.addEventListener('click', function () { |
| 32 |
navigator.clipboard.writeText(text).then(function () { |
| 33 |
btn.textContent = 'Copied'; |
| 34 |
setTimeout(function () { btn.textContent = 'Copy'; }, 1500); |
| 35 |
}); |
| 36 |
}); |
| 37 |
return btn; |
| 38 |
} |
| 39 |
|
| 40 |
window.syncKitCopyKey = function (event, key) { |
| 41 |
navigator.clipboard.writeText(key).then(function () { |
| 42 |
var btn = event.target; |
| 43 |
var orig = btn.textContent; |
| 44 |
btn.textContent = 'Copied'; |
| 45 |
setTimeout(function () { btn.textContent = orig; }, 1500); |
| 46 |
}); |
| 47 |
}; |
| 48 |
|
| 49 |
window.syncKitRegenKey = function (appId) { |
| 50 |
if (!confirm('Regenerate API key? All existing clients using the current key will stop working.')) return; |
| 51 |
fetch('/api/sync/apps/' + appId + '/regenerate-key', { |
| 52 |
method: 'POST', |
| 53 |
credentials: 'same-origin', |
| 54 |
headers: csrfHeaders(), |
| 55 |
}).then(function (res) { |
| 56 |
if (!res.ok) { showToast('Failed to regenerate key.'); return; } |
| 57 |
if (!cfg.regenInline) { reloadTab(); return; } |
| 58 |
|
| 59 |
return res.json().then(function (data) { |
| 60 |
var display = document.getElementById('synckit-key-display-' + appId); |
| 61 |
if (display && data.api_key) { |
| 62 |
var parent = display.parentElement; |
| 63 |
parent.innerHTML = ''; |
| 64 |
var code = document.createElement('code'); |
| 65 |
code.className = 'proj-synckit-code--wrap'; |
| 66 |
code.textContent = data.api_key; |
| 67 |
parent.appendChild(code); |
| 68 |
parent.appendChild(document.createTextNode(' ')); |
| 69 |
parent.appendChild(makeCopyButton(data.api_key, 'btn-small btn-secondary proj-synckit-btn-row')); |
| 70 |
parent.appendChild(document.createTextNode(' ')); |
| 71 |
var warn = document.createElement('span'); |
| 72 |
warn.className = 'form-hint proj-synckit-hint-warn'; |
| 73 |
warn.textContent = 'Save this key now: it cannot be shown again.'; |
| 74 |
parent.appendChild(warn); |
| 75 |
} |
| 76 |
showToast('Key regenerated. Copy it now: it will not be shown again.'); |
| 77 |
}); |
| 78 |
}).catch(netError); |
| 79 |
}; |
| 80 |
|
| 81 |
window.syncKitDeleteApp = function (appId, name) { |
| 82 |
if (!confirm('Delete sync app "' + name + '"? This will remove all devices and sync data for this app.')) return; |
| 83 |
fetch('/api/sync/apps/' + appId, { |
| 84 |
method: 'DELETE', |
| 85 |
credentials: 'same-origin', |
| 86 |
headers: csrfHeaders(), |
| 87 |
}).then(function (res) { |
| 88 |
if (res.ok) { reloadTab(); } else { showToast('Failed to delete app.'); } |
| 89 |
}).catch(netError); |
| 90 |
}; |
| 91 |
|
| 92 |
window.syncKitShowSlugForm = function (appId, currentSlug) { |
| 93 |
var cell = document.getElementById('synckit-slug-cell-' + appId); |
| 94 |
if (!cell) return; |
| 95 |
cell.innerHTML = ''; |
| 96 |
var input = document.createElement('input'); |
| 97 |
input.type = 'text'; |
| 98 |
input.value = currentSlug; |
| 99 |
input.placeholder = 'e.g. goingson'; |
| 100 |
input.className = cfg.editInputClass; |
| 101 |
cell.appendChild(input); |
| 102 |
cell.appendChild(document.createTextNode(' ')); |
| 103 |
var saveBtn = document.createElement('button'); |
| 104 |
saveBtn.className = 'btn-small ' + cfg.editBtnClass; |
| 105 |
saveBtn.textContent = 'Save'; |
| 106 |
saveBtn.addEventListener('click', function () { |
| 107 |
var slug = input.value.trim(); |
| 108 |
if (!slug) return; |
| 109 |
fetch('/api/sync/apps/' + appId + '/slug', { |
| 110 |
method: 'PUT', |
| 111 |
credentials: 'same-origin', |
| 112 |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, |
| 113 |
body: JSON.stringify({ slug: slug }), |
| 114 |
}).then(function (res) { |
| 115 |
if (res.ok) { |
| 116 |
reloadTab(); |
| 117 |
showToast('Update slug set to "' + slug + '".'); |
| 118 |
} else { |
| 119 |
res.text().then(function (t) { showToast(t || 'Failed to set slug.'); }); |
| 120 |
} |
| 121 |
}).catch(netError); |
| 122 |
}); |
| 123 |
cell.appendChild(saveBtn); |
| 124 |
cell.appendChild(document.createTextNode(' ')); |
| 125 |
var cancelBtn = document.createElement('button'); |
| 126 |
cancelBtn.className = 'btn-small secondary ' + cfg.editBtnClass; |
| 127 |
cancelBtn.textContent = 'Cancel'; |
| 128 |
cancelBtn.addEventListener('click', reloadTab); |
| 129 |
cell.appendChild(cancelBtn); |
| 130 |
}; |
| 131 |
|
| 132 |
window.syncKitShowLinkForm = function (appId) { |
| 133 |
var cell = document.getElementById('synckit-link-cell-' + appId); |
| 134 |
if (!cell) return; |
| 135 |
cell.innerHTML = ''; |
| 136 |
var select = document.createElement('select'); |
| 137 |
select.id = 'synckit-link-select-' + appId; |
| 138 |
select.className = 'user-synckit-edit-select input--sm'; |
| 139 |
var noneOpt = document.createElement('option'); |
| 140 |
noneOpt.value = ''; |
| 141 |
noneOpt.textContent = 'None'; |
| 142 |
select.appendChild(noneOpt); |
| 143 |
|
| 144 |
|
| 145 |
var tpl = document.getElementById('synckit-projects-options'); |
| 146 |
if (tpl && tpl.content) { |
| 147 |
tpl.content.querySelectorAll('option').forEach(function (opt) { |
| 148 |
select.appendChild(opt.cloneNode(true)); |
| 149 |
}); |
| 150 |
} |
| 151 |
cell.appendChild(select); |
| 152 |
cell.appendChild(document.createTextNode(' ')); |
| 153 |
var saveBtn = document.createElement('button'); |
| 154 |
saveBtn.className = 'btn-small user-synckit-edit-btn'; |
| 155 |
saveBtn.textContent = 'Save'; |
| 156 |
saveBtn.addEventListener('click', function () { window.syncKitSaveLink(appId); }); |
| 157 |
cell.appendChild(saveBtn); |
| 158 |
cell.appendChild(document.createTextNode(' ')); |
| 159 |
var cancelBtn = document.createElement('button'); |
| 160 |
cancelBtn.className = 'btn-small secondary user-synckit-edit-btn'; |
| 161 |
cancelBtn.textContent = 'Cancel'; |
| 162 |
cancelBtn.addEventListener('click', reloadTab); |
| 163 |
cell.appendChild(cancelBtn); |
| 164 |
}; |
| 165 |
|
| 166 |
window.syncKitSaveLink = function (appId) { |
| 167 |
var sel = document.getElementById('synckit-link-select-' + appId); |
| 168 |
if (!sel) return; |
| 169 |
var projectId = sel.value; |
| 170 |
fetch('/api/sync/apps/' + appId + '/link', { |
| 171 |
method: 'PUT', |
| 172 |
credentials: 'same-origin', |
| 173 |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, |
| 174 |
body: JSON.stringify({ project_id: projectId || null, item_id: null }), |
| 175 |
}).then(function (res) { |
| 176 |
if (res.ok) { reloadTab(); } else { showToast('Failed to update link.'); } |
| 177 |
}).catch(netError); |
| 178 |
}; |
| 179 |
|
| 180 |
|
| 181 |
function showCreatedKeyInline(statusEl, apiKey) { |
| 182 |
statusEl.className = 'proj-synckit-create-status--reset'; |
| 183 |
statusEl.innerHTML = ''; |
| 184 |
var wrap = document.createElement('div'); |
| 185 |
wrap.className = 'proj-synckit-new-key'; |
| 186 |
var heading = document.createElement('p'); |
| 187 |
heading.className = 'proj-synckit-new-key-heading'; |
| 188 |
heading.textContent = 'Save this API key now: it cannot be shown again.'; |
| 189 |
wrap.appendChild(heading); |
| 190 |
var code = document.createElement('code'); |
| 191 |
code.className = 'proj-synckit-code--selectable'; |
| 192 |
code.textContent = apiKey; |
| 193 |
wrap.appendChild(code); |
| 194 |
wrap.appendChild(document.createTextNode(' ')); |
| 195 |
wrap.appendChild(makeCopyButton(apiKey, 'btn-small btn-secondary proj-synckit-btn-row')); |
| 196 |
var doneBtn = document.createElement('button'); |
| 197 |
doneBtn.type = 'button'; |
| 198 |
doneBtn.className = 'btn-small proj-synckit-btn-tight'; |
| 199 |
doneBtn.textContent = 'Done'; |
| 200 |
doneBtn.addEventListener('click', reloadTab); |
| 201 |
wrap.appendChild(doneBtn); |
| 202 |
statusEl.appendChild(wrap); |
| 203 |
} |
| 204 |
|
| 205 |
function wireCreateForm() { |
| 206 |
var form = document.getElementById('synckit-create-form'); |
| 207 |
if (!form) return; |
| 208 |
form.addEventListener('submit', function (e) { |
| 209 |
e.preventDefault(); |
| 210 |
var nameInput = document.getElementById('synckit-app-name'); |
| 211 |
var name = nameInput.value.trim(); |
| 212 |
if (!name) return; |
| 213 |
|
| 214 |
var body = { name: name }; |
| 215 |
if (cfg.create && cfg.create.mode === 'project') { |
| 216 |
if (cfg.create.projectId) body.project_id = cfg.create.projectId; |
| 217 |
} else { |
| 218 |
var projectSelect = document.getElementById('synckit-app-project'); |
| 219 |
var projectId = projectSelect ? projectSelect.value : ''; |
| 220 |
if (projectId) body.project_id = projectId; |
| 221 |
} |
| 222 |
|
| 223 |
fetch('/api/sync/apps', { |
| 224 |
method: 'POST', |
| 225 |
credentials: 'same-origin', |
| 226 |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, |
| 227 |
body: JSON.stringify(body), |
| 228 |
}).then(function (res) { |
| 229 |
var statusEl = document.getElementById('synckit-create-status'); |
| 230 |
if (res.ok) { |
| 231 |
|
| 232 |
if (!cfg.create || cfg.create.mode !== 'project') { reloadTab(); return; } |
| 233 |
return res.json().then(function (data) { |
| 234 |
if (statusEl && data.api_key) showCreatedKeyInline(statusEl, data.api_key); |
| 235 |
}); |
| 236 |
} |
| 237 |
return res.text().then(function (t) { |
| 238 |
if (!statusEl) return; |
| 239 |
statusEl.className = (cfg.create && cfg.create.errorClass) || 'proj-synckit-create-status--error'; |
| 240 |
statusEl.textContent = t || 'Failed to create app.'; |
| 241 |
}); |
| 242 |
}).catch(function () { |
| 243 |
var statusEl = document.getElementById('synckit-create-status'); |
| 244 |
if (!statusEl) return; |
| 245 |
statusEl.className = (cfg.create && cfg.create.errorClass) || 'proj-synckit-create-status--error'; |
| 246 |
statusEl.textContent = 'Network error. Please check your connection and try again.'; |
| 247 |
}); |
| 248 |
}); |
| 249 |
} |
| 250 |
|
| 251 |
window.initSyncKitTab = function (config) { |
| 252 |
cfg = config || {}; |
| 253 |
if (window.initSyncKitBilling) window.initSyncKitBilling(); |
| 254 |
wireCreateForm(); |
| 255 |
}; |
| 256 |
})(); |
| 257 |
|