nowaikit-utils 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/popup/popup.js ADDED
@@ -0,0 +1,414 @@
1
+ /**
2
+ * NowAIKit Utils — Popup Script
3
+ *
4
+ * Handles: instance detection, quick actions, feature toggles,
5
+ * integration status, and hierarchical saved-instance management.
6
+ */
7
+
8
+ document.addEventListener('DOMContentLoaded', () => {
9
+ // Detect OS for modifier key labels
10
+ const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
11
+ if (isMac) {
12
+ document.querySelectorAll('.mod-key').forEach((el) => {
13
+ el.textContent = '\u2318';
14
+ });
15
+ }
16
+
17
+ // ─── Theme Toggle ──────────────────────────────────────────────────
18
+ const themeBtn = document.getElementById('themeToggle');
19
+ const themeIcon = document.getElementById('themeIcon');
20
+ const sunSVG = '<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>';
21
+ const moonSVG = '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>';
22
+
23
+ function applyPopupTheme(theme) {
24
+ document.body.classList.toggle('light', theme === 'light');
25
+ if (themeIcon) themeIcon.innerHTML = theme === 'dark' ? sunSVG : moonSVG;
26
+ if (themeBtn) themeBtn.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
27
+ }
28
+
29
+ // Load saved theme
30
+ chrome.storage.local.get({ nowaikitTheme: 'dark' }, (data) => {
31
+ applyPopupTheme(data.nowaikitTheme || 'dark');
32
+ });
33
+
34
+ if (themeBtn) {
35
+ themeBtn.addEventListener('click', () => {
36
+ const isLight = document.body.classList.contains('light');
37
+ const newTheme = isLight ? 'dark' : 'light';
38
+ chrome.storage.local.set({ nowaikitTheme: newTheme });
39
+ applyPopupTheme(newTheme);
40
+ // Also notify content script to update page theme
41
+ chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
42
+ if (tabs[0]?.id) chrome.tabs.sendMessage(tabs[0].id, { action: 'nowaikit-set-theme', theme: newTheme });
43
+ });
44
+ });
45
+ }
46
+
47
+ // ─── Collapsible Keyboard Shortcuts ──────────────────────────────────
48
+ const shortcutsToggle = document.getElementById('shortcutsToggle');
49
+ const shortcutsSection = document.getElementById('shortcutsSection');
50
+ if (shortcutsToggle && shortcutsSection) {
51
+ shortcutsToggle.addEventListener('click', () => {
52
+ const isCollapsed = shortcutsSection.classList.toggle('collapsed');
53
+ shortcutsToggle.classList.toggle('expanded', !isCollapsed);
54
+ });
55
+ }
56
+
57
+ // ─── Current Tab Detection ────────────────────────────────────────────
58
+
59
+ chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
60
+ const tab = tabs[0];
61
+ if (!tab?.url) return;
62
+
63
+ const isServiceNow = tab.url.includes('.service-now.com') || tab.url.includes('.servicenow.com');
64
+ const statusDot = document.getElementById('statusDot');
65
+ const instanceName = document.getElementById('instanceName');
66
+
67
+ if (isServiceNow) {
68
+ statusDot.classList.add('active');
69
+ instanceName.textContent = new URL(tab.url).hostname.split('.')[0];
70
+
71
+ const doMatch = tab.url.match(/\/([a-z_][a-z0-9_]*)\.do(?:\?|#|$)/);
72
+ const sysIdMatch = tab.url.match(/[?&]sys_id=([a-f0-9]{32})/);
73
+ const listMatch = tab.url.match(/\/([a-z_][a-z0-9_]*)_list\.do/);
74
+ const wsMatch = tab.url.match(/\/now\/(?:sow|workspace)[^/]*\/(?:.*\/)?record\/([a-z_][a-z0-9_]*)\/([a-f0-9]{32})/);
75
+
76
+ const pageInfo = document.getElementById('pageInfo');
77
+ const tableEl = document.getElementById('tableName');
78
+ const sysIdEl = document.getElementById('sysId');
79
+
80
+ if (doMatch && sysIdMatch) {
81
+ pageInfo.style.display = 'block';
82
+ tableEl.textContent = doMatch[1];
83
+ sysIdEl.textContent = sysIdMatch[1].substring(0, 12) + '...';
84
+ sysIdEl.dataset.full = sysIdMatch[1];
85
+ tableEl.dataset.full = doMatch[1];
86
+ } else if (wsMatch) {
87
+ pageInfo.style.display = 'block';
88
+ tableEl.textContent = wsMatch[1];
89
+ sysIdEl.textContent = wsMatch[2].substring(0, 12) + '...';
90
+ sysIdEl.dataset.full = wsMatch[2];
91
+ tableEl.dataset.full = wsMatch[1];
92
+ } else if (listMatch) {
93
+ pageInfo.style.display = 'block';
94
+ tableEl.textContent = listMatch[1];
95
+ sysIdEl.textContent = 'N/A (list)';
96
+ tableEl.dataset.full = listMatch[1];
97
+ } else if (doMatch) {
98
+ pageInfo.style.display = 'block';
99
+ tableEl.textContent = doMatch[1];
100
+ sysIdEl.textContent = 'N/A';
101
+ tableEl.dataset.full = doMatch[1];
102
+ }
103
+
104
+ tableEl?.addEventListener('click', () => {
105
+ if (tableEl.dataset.full) {
106
+ navigator.clipboard.writeText(tableEl.dataset.full).then(() => {
107
+ const orig = tableEl.dataset.full;
108
+ tableEl.textContent = 'Copied!';
109
+ setTimeout(() => { tableEl.textContent = orig; }, 1000);
110
+ }).catch(() => { /* clipboard denied */ });
111
+ }
112
+ });
113
+ sysIdEl?.addEventListener('click', () => {
114
+ if (sysIdEl.dataset.full) {
115
+ navigator.clipboard.writeText(sysIdEl.dataset.full).then(() => {
116
+ sysIdEl.textContent = 'Copied!';
117
+ setTimeout(() => { sysIdEl.textContent = sysIdEl.dataset.full.substring(0, 12) + '...'; }, 1000);
118
+ }).catch(() => { /* clipboard denied */ });
119
+ }
120
+ });
121
+ } else {
122
+ instanceName.textContent = 'Not a ServiceNow page';
123
+ }
124
+ });
125
+
126
+ // ─── Quick Actions ────────────────────────────────────────────────────
127
+
128
+ function sendAction(action) {
129
+ chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
130
+ if (tabs[0]?.id) chrome.tabs.sendMessage(tabs[0].id, { action });
131
+ });
132
+ }
133
+
134
+ document.getElementById('btnCopySysId').addEventListener('click', () => sendAction('nowaikit-copy-sysid'));
135
+ document.getElementById('btnCopyUrl').addEventListener('click', () => sendAction('nowaikit-copy-record-url'));
136
+ document.getElementById('btnOpenList').addEventListener('click', () => sendAction('nowaikit-open-list'));
137
+ document.getElementById('btnViewXml').addEventListener('click', () => sendAction('nowaikit-xml-view'));
138
+ document.getElementById('btnViewJson').addEventListener('click', () => sendAction('nowaikit-json-view'));
139
+ document.getElementById('btnSchema').addEventListener('click', () => sendAction('nowaikit-open-schema'));
140
+ document.getElementById('btnSwitchNode').addEventListener('click', () => sendAction('nowaikit-switch-node'));
141
+ document.getElementById('btnCodeTemplates').addEventListener('click', () => sendAction('nowaikit-open-code-templates'));
142
+ document.getElementById('btnAISidebar').addEventListener('click', () => sendAction('nowaikit-toggle-ai-sidebar'));
143
+ document.getElementById('btnMainPanel').addEventListener('click', () => sendAction('nowaikit-toggle-main-panel'));
144
+
145
+ // ─── Integration Status ───────────────────────────────────────────────
146
+
147
+ chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
148
+ if (!tabs[0]?.id) return;
149
+ chrome.tabs.sendMessage(tabs[0].id, { action: 'nowaikit-get-bridge-status' }, (response) => {
150
+ const builderDot = document.getElementById('builderDot');
151
+ const builderStatus = document.getElementById('builderStatus');
152
+ const mcpDot = document.getElementById('mcpDot');
153
+ const mcpStatus = document.getElementById('mcpStatus');
154
+
155
+ if (chrome.runtime.lastError || !response) {
156
+ builderStatus.textContent = 'Not detected';
157
+ mcpStatus.textContent = 'Not available';
158
+ return;
159
+ }
160
+ if (response.connected) {
161
+ builderDot.style.background = 'var(--teal)';
162
+ builderDot.style.boxShadow = '0 0 4px rgba(0,212,170,0.4)';
163
+ builderStatus.textContent = response.instanceName || 'Connected';
164
+ builderStatus.style.color = 'var(--teal)';
165
+ mcpDot.style.background = 'var(--teal)';
166
+ mcpDot.style.boxShadow = '0 0 4px rgba(0,212,170,0.4)';
167
+ mcpStatus.textContent = 'Available';
168
+ mcpStatus.style.color = 'var(--teal)';
169
+ } else {
170
+ builderStatus.textContent = 'Not running';
171
+ mcpStatus.textContent = 'Not available';
172
+ }
173
+ });
174
+ });
175
+
176
+ // ─── Feature Toggles ─────────────────────────────────────────────────
177
+
178
+ const toggleMap = {
179
+ toggleTechNames: 'showTechnicalNames',
180
+ toggleUpdateSet: 'showUpdateSetBanner',
181
+ toggleFieldCopy: 'enableFieldCopy',
182
+ toggleQuickNav: 'enableQuickNav',
183
+ };
184
+
185
+ chrome.storage.sync.get({
186
+ showTechnicalNames: false,
187
+ showUpdateSetBanner: false,
188
+ enableFieldCopy: false,
189
+ enableQuickNav: false,
190
+ }, (settings) => {
191
+ for (const [toggleId, key] of Object.entries(toggleMap)) {
192
+ const el = document.getElementById(toggleId);
193
+ if (el) el.checked = settings[key];
194
+ }
195
+ });
196
+
197
+ for (const [toggleId, key] of Object.entries(toggleMap)) {
198
+ const el = document.getElementById(toggleId);
199
+ if (el) {
200
+ el.addEventListener('change', () => {
201
+ chrome.storage.sync.set({ [key]: el.checked });
202
+ });
203
+ }
204
+ }
205
+
206
+ // ─── Saved Instances — Hierarchical Tree ──────────────────────────────
207
+
208
+ let collapsedState = {};
209
+
210
+ function escapeHtml(str) {
211
+ const d = document.createElement('div');
212
+ d.textContent = str;
213
+ return d.innerHTML;
214
+ }
215
+
216
+ /** Validate an instance URL slug is safe (alphanumeric, hyphens, underscores only) */
217
+ function isValidInstanceSlug(slug) {
218
+ return /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(slug) && slug.length <= 100;
219
+ }
220
+
221
+ function loadInstances() {
222
+ chrome.storage.sync.get({ savedInstances: [] }, (syncData) => {
223
+ chrome.storage.local.get({ collapsedGroups: {} }, (localData) => {
224
+ collapsedState = localData.collapsedGroups || {};
225
+ renderTree(syncData.savedInstances);
226
+ updateSuggestions(syncData.savedInstances);
227
+ });
228
+ });
229
+ }
230
+
231
+ function saveCollapsedState() {
232
+ chrome.storage.local.set({ collapsedGroups: collapsedState });
233
+ }
234
+
235
+ function toggleCollapse(key) {
236
+ collapsedState[key] = !collapsedState[key];
237
+ saveCollapsedState();
238
+ loadInstances();
239
+ }
240
+
241
+ function updateSuggestions(instances) {
242
+ const groups = new Set();
243
+ instances.forEach((inst) => {
244
+ if (inst.group) groups.add(inst.group);
245
+ });
246
+
247
+ const gList = document.getElementById('groupSuggestions');
248
+ gList.innerHTML = '';
249
+ groups.forEach((g) => { const o = document.createElement('option'); o.value = g; gList.appendChild(o); });
250
+ }
251
+
252
+ function renderTree(instances) {
253
+ const tree = document.getElementById('instanceTree');
254
+ tree.innerHTML = '';
255
+
256
+ if (instances.length === 0) {
257
+ tree.innerHTML = '<div class="empty-state"><span>&#128203;</span>No saved instances yet.<br>Add your ServiceNow instances below.</div>';
258
+ return;
259
+ }
260
+
261
+ // Build hierarchy: { groupName: [instances] }
262
+ const grouped = {};
263
+ const ungrouped = [];
264
+
265
+ instances.forEach((inst, idx) => {
266
+ const item = { ...inst, _idx: idx };
267
+ if (inst.group) {
268
+ if (!grouped[inst.group]) grouped[inst.group] = [];
269
+ grouped[inst.group].push(item);
270
+ } else {
271
+ ungrouped.push(item);
272
+ }
273
+ });
274
+
275
+ // Render grouped
276
+ const groupNames = Object.keys(grouped).sort();
277
+ for (const groupName of groupNames) {
278
+ const insts = grouped[groupName];
279
+ const isCollapsed = !!collapsedState[groupName];
280
+
281
+ // Group header
282
+ const gh = document.createElement('div');
283
+ gh.className = 'inst-group';
284
+ gh.innerHTML =
285
+ '<span class="inst-chevron' + (isCollapsed ? ' collapsed' : '') + '">\u25BE</span>' +
286
+ '<span class="inst-group-icon">\uD83D\uDCC1</span>' +
287
+ '<span class="inst-group-name">' + escapeHtml(groupName) + '</span>' +
288
+ '<span class="inst-group-count">' + insts.length + '</span>';
289
+ gh.addEventListener('click', () => toggleCollapse(groupName));
290
+ tree.appendChild(gh);
291
+
292
+ // Children container
293
+ const children = document.createElement('div');
294
+ children.className = 'inst-children' + (isCollapsed ? ' collapsed' : '');
295
+ insts.forEach((inst) => children.appendChild(createInstanceRow(inst, 1)));
296
+ tree.appendChild(children);
297
+ }
298
+
299
+ // Render ungrouped
300
+ if (ungrouped.length > 0 && groupNames.length > 0) {
301
+ // Add a subtle separator if there are also groups
302
+ const sep = document.createElement('div');
303
+ sep.style.cssText = 'height:1px;background:var(--border);margin:4px 0;';
304
+ tree.appendChild(sep);
305
+ }
306
+ ungrouped.forEach((inst) => tree.appendChild(createInstanceRow(inst, 0)));
307
+ }
308
+
309
+ function createInstanceRow(inst, depth) {
310
+ const row = document.createElement('div');
311
+ row.className = 'inst-row depth-' + depth;
312
+
313
+ const envLabel = inst.env || 'other';
314
+ const display = inst.alias || inst.url;
315
+
316
+ row.innerHTML =
317
+ '<div class="inst-env-dot ' + escapeHtml(envLabel) + '"></div>' +
318
+ '<div class="inst-details">' +
319
+ '<div class="inst-name">' + escapeHtml(display) + '</div>' +
320
+ (inst.alias ? '<div class="inst-alias">' + escapeHtml(inst.url) + '</div>' : '') +
321
+ '</div>' +
322
+ '<span class="inst-env-badge ' + escapeHtml(envLabel) + '">' + escapeHtml(envLabel) + '</span>' +
323
+ '<div class="inst-actions">' +
324
+ '<button class="btn-open" title="Open in new tab">\u2197</button>' +
325
+ '<button class="btn-remove" title="Remove">\u00D7</button>' +
326
+ '</div>';
327
+
328
+ // Open instance (validate URL slug first)
329
+ row.querySelector('.btn-open').addEventListener('click', (e) => {
330
+ e.stopPropagation();
331
+ if (isValidInstanceSlug(inst.url)) {
332
+ window.open('https://' + inst.url + '.service-now.com', '_blank');
333
+ }
334
+ });
335
+
336
+ // Remove instance
337
+ row.querySelector('.btn-remove').addEventListener('click', (e) => {
338
+ e.stopPropagation();
339
+ removeInstance(inst._idx);
340
+ });
341
+
342
+ // Click row to open (validate URL slug first)
343
+ row.addEventListener('click', () => {
344
+ if (isValidInstanceSlug(inst.url)) {
345
+ window.open('https://' + inst.url + '.service-now.com', '_blank');
346
+ }
347
+ });
348
+
349
+ return row;
350
+ }
351
+
352
+ function removeInstance(idx) {
353
+ chrome.storage.sync.get({ savedInstances: [] }, (data) => {
354
+ data.savedInstances.splice(idx, 1);
355
+ chrome.storage.sync.set({ savedInstances: data.savedInstances }, loadInstances);
356
+ });
357
+ }
358
+
359
+ // ─── Add Instance Form ────────────────────────────────────────────────
360
+
361
+ document.getElementById('btnAddInstance').addEventListener('click', () => {
362
+ const form = document.getElementById('addInstanceForm');
363
+ form.style.display = form.style.display === 'flex' ? 'none' : 'flex';
364
+ if (form.style.display === 'flex') {
365
+ document.getElementById('newInstanceUrl').focus();
366
+ }
367
+ });
368
+
369
+ document.getElementById('btnSaveInstance').addEventListener('click', saveNewInstance);
370
+
371
+ // Enter key in any form field saves
372
+ document.getElementById('addInstanceForm').addEventListener('keydown', (e) => {
373
+ if (e.key === 'Enter') saveNewInstance();
374
+ });
375
+
376
+ function saveNewInstance() {
377
+ const url = document.getElementById('newInstanceUrl').value.trim();
378
+ if (!url) return;
379
+
380
+ // Validate the instance URL slug
381
+ if (!isValidInstanceSlug(url)) {
382
+ document.getElementById('newInstanceUrl').style.borderColor = '#f87171';
383
+ setTimeout(() => { document.getElementById('newInstanceUrl').style.borderColor = ''; }, 1500);
384
+ return;
385
+ }
386
+
387
+ const env = document.getElementById('newInstanceEnv').value;
388
+ const alias = document.getElementById('newInstanceAlias').value.trim();
389
+ const group = document.getElementById('newInstanceGroup').value.trim();
390
+
391
+ const instance = { url, env, alias };
392
+ if (group) instance.group = group;
393
+
394
+ chrome.storage.sync.get({ savedInstances: [] }, (data) => {
395
+ // Prevent duplicates
396
+ if (data.savedInstances.some((i) => i.url === url)) {
397
+ document.getElementById('newInstanceUrl').style.borderColor = '#f87171';
398
+ setTimeout(() => { document.getElementById('newInstanceUrl').style.borderColor = ''; }, 1500);
399
+ return;
400
+ }
401
+
402
+ data.savedInstances.push(instance);
403
+ chrome.storage.sync.set({ savedInstances: data.savedInstances }, () => {
404
+ loadInstances();
405
+ document.getElementById('addInstanceForm').style.display = 'none';
406
+ document.getElementById('newInstanceUrl').value = '';
407
+ document.getElementById('newInstanceAlias').value = '';
408
+ document.getElementById('newInstanceGroup').value = '';
409
+ });
410
+ });
411
+ }
412
+
413
+ loadInstances();
414
+ });