Skip to main content

max / makenotwork

12.1 KB · 262 lines History Blame Raw
1 /* Makenotwork, SyncKit dashboard tabs (project + user).
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 // Tab-specific config, refreshed on each initSyncKitTab() call (tabs are
14 // re-rendered HTMX partials; the script itself loads once).
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
23 // Re-fetch the tab this panel is showing in. It used to click `#tab-synckit`,
24 // which was the project dashboard's tab button until that strip was described
25 // (`6b24f2df` step 3) and every panel became its own region. The tab partial
26 // says where it lives; a partial that says nothing reloads the page, which is
27 // what the user-level SyncKit tab gets since nothing links to it.
28 function reloadTab() {
29 var el = document.getElementById('synckit-tab-cfg');
30 var url = el ? el.dataset.reloadUrl : '';
31 var target = el ? el.dataset.reloadTarget : '';
32 if (url && target) { htmx.ajax('GET', url, target); return; }
33 location.reload();
34 }
35 window.reloadSyncKitTab = reloadTab;
36
37 // A "Copy" button that copies `text` and briefly flips its label. Built via
38 // addEventListener so the secret key never lands in an inline onclick string.
39 function makeCopyButton(text, className) {
40 var btn = document.createElement('button');
41 btn.type = 'button';
42 btn.className = className;
43 btn.textContent = 'Copy';
44 btn.addEventListener('click', function () {
45 window.copyWithFeedback(btn, text, 'Copied');
46 });
47 return btn;
48 }
49
50 window.syncKitCopyKey = function (event, key) {
51 window.copyWithFeedback(event.target, key, 'Copied');
52 };
53
54 window.syncKitRegenKey = function (appId) {
55 if (!confirm('Regenerate API key? All existing clients using the current key will stop working.')) return;
56 fetch('/api/sync/apps/' + appId + '/regenerate-key', {
57 method: 'POST',
58 credentials: 'same-origin',
59 headers: csrfHeaders(),
60 }).then(function (res) {
61 if (!res.ok) { showToast('Failed to regenerate key.'); return; }
62 if (!cfg.regenInline) { reloadTab(); return; }
63 // Project tab: reveal the new key inline with a copy button.
64 return res.json().then(function (data) {
65 var display = document.getElementById('synckit-key-display-' + appId);
66 if (display && data.api_key) {
67 var parent = display.parentElement;
68 parent.innerHTML = '';
69 var code = document.createElement('code');
70 code.className = 'proj-synckit-code--wrap';
71 code.textContent = data.api_key;
72 parent.appendChild(code);
73 parent.appendChild(document.createTextNode(' '));
74 parent.appendChild(makeCopyButton(data.api_key, 'btn-small btn-secondary proj-synckit-btn-row'));
75 parent.appendChild(document.createTextNode(' '));
76 var warn = document.createElement('span');
77 warn.className = 'form-hint proj-synckit-hint-warn';
78 warn.textContent = 'Save this key now: it cannot be shown again.';
79 parent.appendChild(warn);
80 }
81 showToast('Key regenerated. Copy it now: it will not be shown again.');
82 });
83 }).catch(netError);
84 };
85
86 window.syncKitDeleteApp = function (appId, name) {
87 if (!confirm('Delete sync app "' + name + '"? This will remove all devices and sync data for this app.')) return;
88 fetch('/api/sync/apps/' + appId, {
89 method: 'DELETE',
90 credentials: 'same-origin',
91 headers: csrfHeaders(),
92 }).then(function (res) {
93 if (res.ok) { reloadTab(); } else { showToast('Failed to delete app.'); }
94 }).catch(netError);
95 };
96
97 window.syncKitShowSlugForm = function (appId, currentSlug) {
98 var cell = document.getElementById('synckit-slug-cell-' + appId);
99 if (!cell) return;
100 cell.innerHTML = '';
101 var input = document.createElement('input');
102 input.type = 'text';
103 input.value = currentSlug;
104 input.placeholder = 'e.g. goingson';
105 input.className = cfg.editInputClass;
106 cell.appendChild(input);
107 cell.appendChild(document.createTextNode(' '));
108 var saveBtn = document.createElement('button');
109 saveBtn.className = 'btn-small ' + cfg.editBtnClass;
110 saveBtn.textContent = 'Save';
111 saveBtn.addEventListener('click', function () {
112 var slug = input.value.trim();
113 if (!slug) return;
114 fetch('/api/sync/apps/' + appId + '/slug', {
115 method: 'PUT',
116 credentials: 'same-origin',
117 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
118 body: JSON.stringify({ slug: slug }),
119 }).then(function (res) {
120 if (res.ok) {
121 reloadTab();
122 showToast('Update slug set to "' + slug + '".');
123 } else {
124 res.text().then(function (t) { showToast(t || 'Failed to set slug.'); });
125 }
126 }).catch(netError);
127 });
128 cell.appendChild(saveBtn);
129 cell.appendChild(document.createTextNode(' '));
130 var cancelBtn = document.createElement('button');
131 cancelBtn.className = 'btn-small secondary ' + cfg.editBtnClass;
132 cancelBtn.textContent = 'Cancel';
133 cancelBtn.addEventListener('click', reloadTab);
134 cell.appendChild(cancelBtn);
135 };
136
137 window.syncKitShowLinkForm = function (appId) {
138 var cell = document.getElementById('synckit-link-cell-' + appId);
139 if (!cell) return;
140 cell.innerHTML = '';
141 var select = document.createElement('select');
142 select.id = 'synckit-link-select-' + appId;
143 select.className = 'user-synckit-edit-select input--sm';
144 var noneOpt = document.createElement('option');
145 noneOpt.value = '';
146 noneOpt.textContent = 'None';
147 select.appendChild(noneOpt);
148 // Options come from a server-rendered <template> (HTML-escaped), not from
149 // values interpolated into this script.
150 var tpl = document.getElementById('synckit-projects-options');
151 if (tpl && tpl.content) {
152 tpl.content.querySelectorAll('option').forEach(function (opt) {
153 select.appendChild(opt.cloneNode(true));
154 });
155 }
156 cell.appendChild(select);
157 cell.appendChild(document.createTextNode(' '));
158 var saveBtn = document.createElement('button');
159 saveBtn.className = 'btn-small user-synckit-edit-btn';
160 saveBtn.textContent = 'Save';
161 saveBtn.addEventListener('click', function () { window.syncKitSaveLink(appId); });
162 cell.appendChild(saveBtn);
163 cell.appendChild(document.createTextNode(' '));
164 var cancelBtn = document.createElement('button');
165 cancelBtn.className = 'btn-small secondary user-synckit-edit-btn';
166 cancelBtn.textContent = 'Cancel';
167 cancelBtn.addEventListener('click', reloadTab);
168 cell.appendChild(cancelBtn);
169 };
170
171 window.syncKitSaveLink = function (appId) {
172 var sel = document.getElementById('synckit-link-select-' + appId);
173 if (!sel) return;
174 var projectId = sel.value;
175 fetch('/api/sync/apps/' + appId + '/link', {
176 method: 'PUT',
177 credentials: 'same-origin',
178 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
179 body: JSON.stringify({ project_id: projectId || null, item_id: null }),
180 }).then(function (res) {
181 if (res.ok) { reloadTab(); } else { showToast('Failed to update link.'); }
182 }).catch(netError);
183 };
184
185 // Project tab: render the just-created app's key inline (one-time reveal).
186 function showCreatedKeyInline(statusEl, apiKey) {
187 statusEl.className = 'proj-synckit-create-status--reset';
188 statusEl.innerHTML = '';
189 var wrap = document.createElement('div');
190 wrap.className = 'proj-synckit-new-key';
191 var heading = document.createElement('p');
192 heading.className = 'proj-synckit-new-key-heading';
193 heading.textContent = 'Save this API key now: it cannot be shown again.';
194 wrap.appendChild(heading);
195 var code = document.createElement('code');
196 code.className = 'proj-synckit-code--selectable';
197 code.textContent = apiKey;
198 wrap.appendChild(code);
199 wrap.appendChild(document.createTextNode(' '));
200 wrap.appendChild(makeCopyButton(apiKey, 'btn-small btn-secondary proj-synckit-btn-row'));
201 var doneBtn = document.createElement('button');
202 doneBtn.type = 'button';
203 doneBtn.className = 'btn-small proj-synckit-btn-tight';
204 doneBtn.textContent = 'Done';
205 doneBtn.addEventListener('click', reloadTab);
206 wrap.appendChild(doneBtn);
207 statusEl.appendChild(wrap);
208 }
209
210 function wireCreateForm() {
211 var form = document.getElementById('synckit-create-form');
212 if (!form) return;
213 form.addEventListener('submit', function (e) {
214 e.preventDefault();
215 var nameInput = document.getElementById('synckit-app-name');
216 var name = nameInput.value.trim();
217 if (!name) return;
218
219 var body = { name: name };
220 if (cfg.create && cfg.create.mode === 'project') {
221 if (cfg.create.projectId) body.project_id = cfg.create.projectId;
222 } else {
223 var projectSelect = document.getElementById('synckit-app-project');
224 var projectId = projectSelect ? projectSelect.value : '';
225 if (projectId) body.project_id = projectId;
226 }
227
228 fetch('/api/sync/apps', {
229 method: 'POST',
230 credentials: 'same-origin',
231 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
232 body: JSON.stringify(body),
233 }).then(function (res) {
234 var statusEl = document.getElementById('synckit-create-status');
235 if (res.ok) {
236 // User tab just reloads; project tab reveals the key inline.
237 if (!cfg.create || cfg.create.mode !== 'project') { reloadTab(); return; }
238 return res.json().then(function (data) {
239 if (statusEl && data.api_key) showCreatedKeyInline(statusEl, data.api_key);
240 });
241 }
242 return res.text().then(function (t) {
243 if (!statusEl) return;
244 statusEl.className = (cfg.create && cfg.create.errorClass) || 'proj-synckit-create-status--error';
245 statusEl.textContent = t || 'Failed to create app.';
246 });
247 }).catch(function () {
248 var statusEl = document.getElementById('synckit-create-status');
249 if (!statusEl) return;
250 statusEl.className = (cfg.create && cfg.create.errorClass) || 'proj-synckit-create-status--error';
251 statusEl.textContent = 'Network error. Please check your connection and try again.';
252 });
253 });
254 }
255
256 window.initSyncKitTab = function (config) {
257 cfg = config || {};
258 if (window.initSyncKitBilling) window.initSyncKitBilling();
259 wireCreateForm();
260 };
261 })();
262