command_deck 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/command_deck/assets/css/main.css +33 -0
- data/lib/command_deck/assets/js/core/dom.js +2 -0
- data/lib/command_deck/assets/js/ui/overlay/action_form.js +187 -0
- data/lib/command_deck/assets/js/ui/overlay/actions_view.js +32 -0
- data/lib/command_deck/assets/js/ui/overlay/header_view.js +23 -0
- data/lib/command_deck/assets/js/ui/{position_manager.js → overlay/position_manager.js} +1 -1
- data/lib/command_deck/assets/js/ui/overlay/result_view.js +67 -0
- data/lib/command_deck/assets/js/ui/overlay/tabs_view.js +41 -0
- data/lib/command_deck/assets/js/ui/{theme_manager.js → overlay/theme_manager.js} +1 -1
- data/lib/command_deck/assets/js/ui/overlay.js +31 -207
- data/lib/command_deck/version.rb +1 -1
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f79a2f12ff19b218e7b02e3269ebaff3a283bc3772cdedeaffe829a7e3c9adc
|
4
|
+
data.tar.gz: 0b76ee1ba7f39d8b3a8a781ec33aefe32efdd9e5b66edcfced9e9fc77ea8dcfb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64a99c105eb4dbce276c748aca3781dbe8f74e5bec7b7ab6073e141a7e7fceac5680bfc4e8f67f1aaac25e2642914ef4d6d7df7e0d0e597614836326f652ec25
|
7
|
+
data.tar.gz: d9ce128531b79674de1d8125995969bf8d4e12df619e7b03aa52a569760f204d7a38fcc28e5cdce5604d9ae8b43fe82e2867a3e972724ddebfe9f5d71b2750b4
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## [0.1.2] - 2025-09-11
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Support for Turbolinks.
|
8
|
+
- Copy to clipboard button for results.
|
9
|
+
- Toggle results visibility button.
|
10
|
+
- Refactor overlay.js into multiple files.
|
11
|
+
|
3
12
|
## [0.1.1] - 2025-09-10
|
4
13
|
|
5
14
|
### Fixed
|
@@ -324,6 +324,39 @@
|
|
324
324
|
cursor: not-allowed;
|
325
325
|
}
|
326
326
|
|
327
|
+
/* ---------- Results Toolbar ---------- */
|
328
|
+
#cd-result-wrap {
|
329
|
+
position: relative;
|
330
|
+
margin-top: 10px;
|
331
|
+
padding-top: 28px;
|
332
|
+
min-height: 28px;
|
333
|
+
}
|
334
|
+
|
335
|
+
.cd-result-toolbar {
|
336
|
+
position: absolute;
|
337
|
+
top: 6px;
|
338
|
+
right: 6px;
|
339
|
+
display: flex;
|
340
|
+
gap: 6px;
|
341
|
+
z-index: 1;
|
342
|
+
}
|
343
|
+
|
344
|
+
.cd-icon-btn {
|
345
|
+
border: 1px solid #ddd;
|
346
|
+
background: #f9f9f9;
|
347
|
+
border-radius: 6px;
|
348
|
+
padding: 2px 6px;
|
349
|
+
cursor: pointer;
|
350
|
+
font-size: 12px;
|
351
|
+
line-height: 1;
|
352
|
+
}
|
353
|
+
|
354
|
+
#command-deck-panel.cd-theme-dark .cd-icon-btn {
|
355
|
+
background: #111;
|
356
|
+
color: #eee;
|
357
|
+
border-color: #333;
|
358
|
+
}
|
359
|
+
|
327
360
|
#command-deck-output {
|
328
361
|
white-space: pre-wrap;
|
329
362
|
background: #0b102114;
|
@@ -8,6 +8,8 @@ export function onReady(fn) {
|
|
8
8
|
}
|
9
9
|
document.addEventListener('turbo:load', run);
|
10
10
|
document.addEventListener('turbo:render', run);
|
11
|
+
document.addEventListener('turbolinks:load', run);
|
12
|
+
document.addEventListener('turbolinks:render', run);
|
11
13
|
window.addEventListener('pageshow', run);
|
12
14
|
}
|
13
15
|
|
@@ -0,0 +1,187 @@
|
|
1
|
+
import { el } from '../../core/dom.js';
|
2
|
+
import { store } from '../../core/store.js';
|
3
|
+
|
4
|
+
export class ActionForm {
|
5
|
+
constructor({ action, getCurrentPanelKey, actionsApi, onRunning, onResult, onAfterRun }) {
|
6
|
+
this.action = action;
|
7
|
+
this.getCurrentPanelKey = getCurrentPanelKey;
|
8
|
+
this.actionsApi = actionsApi;
|
9
|
+
this.onRunning = onRunning || (() => {});
|
10
|
+
this.onResult = onResult || (() => {});
|
11
|
+
this.onAfterRun = onAfterRun || (() => {});
|
12
|
+
}
|
13
|
+
|
14
|
+
mount(parentNode) {
|
15
|
+
const box = el('div', { class: 'cd-action' });
|
16
|
+
const title = el('div', { class: 'cd-action-title' }, [this.action.title]);
|
17
|
+
const form = el('div', { class: 'cd-form' });
|
18
|
+
|
19
|
+
box.appendChild(title);
|
20
|
+
box.appendChild(form);
|
21
|
+
|
22
|
+
const inputs = {};
|
23
|
+
const paramsMeta = {};
|
24
|
+
|
25
|
+
(this.action.params || []).forEach(p => {
|
26
|
+
const labelChildren = [p.label || p.name];
|
27
|
+
if (p.required === true) {
|
28
|
+
labelChildren.push(el('span', { class: 'cd-required', title: 'Required' }, ['*']));
|
29
|
+
}
|
30
|
+
const label = el('label', null, labelChildren);
|
31
|
+
let input;
|
32
|
+
if (p.type === 'boolean') {
|
33
|
+
input = el('input', { type: 'checkbox' });
|
34
|
+
} else if (p.type === 'integer') {
|
35
|
+
input = el('input', { type: 'number', step: '1' });
|
36
|
+
} else if (p.type === 'selector' || (p.choices && p.choices.length)) {
|
37
|
+
input = el('select');
|
38
|
+
if (p.include_blank) {
|
39
|
+
input.appendChild(el('option', { value: '' }, ['']));
|
40
|
+
}
|
41
|
+
(p.choices || []).forEach(ch => {
|
42
|
+
const attrs = { 'data-val': JSON.stringify(ch.value) };
|
43
|
+
if (ch.meta != null) attrs['data-meta'] = JSON.stringify(ch.meta);
|
44
|
+
const opt = el('option', attrs, [ch.label != null ? String(ch.label) : String(ch.value)]);
|
45
|
+
input.appendChild(opt);
|
46
|
+
});
|
47
|
+
|
48
|
+
const selKey = `sel:${this.getCurrentPanelKey() || ''}:${this.action.key}:${p.name}`;
|
49
|
+
const savedRaw = store.get(selKey);
|
50
|
+
if (savedRaw != null) {
|
51
|
+
let matched = false;
|
52
|
+
for (let i = 0; i < input.options.length; i++) {
|
53
|
+
const o = input.options[i];
|
54
|
+
const raw = o.getAttribute('data-val');
|
55
|
+
if (raw === savedRaw) {
|
56
|
+
input.selectedIndex = i;
|
57
|
+
matched = true;
|
58
|
+
break;
|
59
|
+
}
|
60
|
+
}
|
61
|
+
if (!matched && p.include_blank && savedRaw === '') {
|
62
|
+
for (let i = 0; i < input.options.length; i++) {
|
63
|
+
if (input.options[i].value === '') { input.selectedIndex = i; break; }
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
} else {
|
68
|
+
input = el('input', { type: 'text' });
|
69
|
+
}
|
70
|
+
if (p.required === true && input) {
|
71
|
+
input.setAttribute('aria-required', 'true');
|
72
|
+
input.setAttribute('required', '');
|
73
|
+
}
|
74
|
+
form.appendChild(label);
|
75
|
+
form.appendChild(input);
|
76
|
+
|
77
|
+
let hint;
|
78
|
+
if (input.tagName === 'SELECT') {
|
79
|
+
hint = el('div', { class: 'cd-param-hint' });
|
80
|
+
const updateHint = () => {
|
81
|
+
const sel = input.options[input.selectedIndex];
|
82
|
+
if (!sel) { hint.textContent = ''; return; }
|
83
|
+
const metaRaw = sel.getAttribute('data-meta');
|
84
|
+
if (!metaRaw) { hint.textContent = ''; return; }
|
85
|
+
try {
|
86
|
+
const meta = JSON.parse(metaRaw);
|
87
|
+
if (typeof meta.enabled === 'boolean') {
|
88
|
+
const status = meta.enabled ? 'ON' : 'OFF';
|
89
|
+
hint.textContent = 'Current: ';
|
90
|
+
const badge = el('span', { class: `cd-badge ${meta.enabled ? 'cd-on' : 'cd-off'}` }, [status]);
|
91
|
+
hint.appendChild(badge);
|
92
|
+
} else {
|
93
|
+
hint.textContent = '';
|
94
|
+
}
|
95
|
+
} catch(_) { hint.textContent = ''; }
|
96
|
+
};
|
97
|
+
input.addEventListener('change', () => {
|
98
|
+
const sel = input.options[input.selectedIndex];
|
99
|
+
const raw = sel ? sel.getAttribute('data-val') : '';
|
100
|
+
const selKey = `sel:${this.getCurrentPanelKey() || ''}:${this.action.key}:${p.name}`;
|
101
|
+
store.set(selKey, raw || '');
|
102
|
+
updateHint();
|
103
|
+
validate();
|
104
|
+
});
|
105
|
+
setTimeout(updateHint, 0);
|
106
|
+
form.appendChild(hint);
|
107
|
+
}
|
108
|
+
|
109
|
+
inputs[p.name] = input;
|
110
|
+
paramsMeta[p.name] = p;
|
111
|
+
});
|
112
|
+
|
113
|
+
const requiredParams = (this.action.params || []).filter(p => p.required === true);
|
114
|
+
const isFilled = (p) => {
|
115
|
+
const inp = inputs[p.name];
|
116
|
+
if (!inp) return false;
|
117
|
+
if (p.type === 'integer') return inp.value !== '';
|
118
|
+
if (p.type === 'selector' || (p.choices && p.choices.length)) {
|
119
|
+
if (p.include_blank) return inp.value !== '' && inp.selectedIndex > -1;
|
120
|
+
return inp.selectedIndex > -1;
|
121
|
+
}
|
122
|
+
if (p.type === 'boolean') return true;
|
123
|
+
return String(inp.value || '').trim() !== '';
|
124
|
+
};
|
125
|
+
const validate = () => {
|
126
|
+
const ok = requiredParams.every(isFilled);
|
127
|
+
const running = run.classList.contains('loading');
|
128
|
+
run.disabled = !ok || running;
|
129
|
+
};
|
130
|
+
|
131
|
+
const run = el('button', { class: 'cd-run', title: 'Run' }, ['\u25B6']);
|
132
|
+
run.addEventListener('click', () => {
|
133
|
+
if (run.disabled) return;
|
134
|
+
run.classList.add('loading');
|
135
|
+
run.setAttribute('aria-busy', 'true');
|
136
|
+
run.disabled = true;
|
137
|
+
|
138
|
+
const payload = {};
|
139
|
+
Object.keys(inputs).forEach(k => {
|
140
|
+
const inp = inputs[k];
|
141
|
+
const meta = paramsMeta[k] || {};
|
142
|
+
if (inp.tagName === 'SELECT') {
|
143
|
+
const sel = inp.options[inp.selectedIndex];
|
144
|
+
const raw = sel && sel.getAttribute('data-val');
|
145
|
+
const label = sel ? sel.textContent : '';
|
146
|
+
let value;
|
147
|
+
try { value = raw != null ? JSON.parse(raw) : inp.value; } catch(_) { value = inp.value; }
|
148
|
+
|
149
|
+
if (meta.return === 'label') {
|
150
|
+
payload[k] = label;
|
151
|
+
} else if (meta.return === 'both' || meta.return === 'object') {
|
152
|
+
payload[k] = { label: label, value: value };
|
153
|
+
} else {
|
154
|
+
payload[k] = value;
|
155
|
+
}
|
156
|
+
|
157
|
+
const selKey = `sel:${this.getCurrentPanelKey() || ''}:${this.action.key}:${k}`;
|
158
|
+
store.set(selKey, raw || '');
|
159
|
+
} else {
|
160
|
+
payload[k] = (inp.type === 'checkbox') ? !!inp.checked : inp.value;
|
161
|
+
}
|
162
|
+
});
|
163
|
+
|
164
|
+
this.onRunning();
|
165
|
+
this.actionsApi
|
166
|
+
.submit(this.action.key, payload)
|
167
|
+
.then((res) => { this.onResult(res); this.onAfterRun(); })
|
168
|
+
.catch((e) => this.onResult({ ok:false, error: String(e) }))
|
169
|
+
.finally(() => { run.classList.remove('loading'); run.removeAttribute('aria-busy'); validate(); });
|
170
|
+
});
|
171
|
+
|
172
|
+
const actionsRow = el('div', { class: 'cd-actions-row' });
|
173
|
+
actionsRow.appendChild(run);
|
174
|
+
form.appendChild(actionsRow);
|
175
|
+
|
176
|
+
// Hook up validation listeners
|
177
|
+
(this.action.params || []).forEach(p => {
|
178
|
+
const inp = inputs[p.name];
|
179
|
+
if (!inp) return;
|
180
|
+
const evt = (inp.tagName === 'SELECT' || inp.type === 'checkbox') ? 'change' : 'input';
|
181
|
+
inp.addEventListener(evt, validate);
|
182
|
+
});
|
183
|
+
validate();
|
184
|
+
|
185
|
+
parentNode.appendChild(box);
|
186
|
+
}
|
187
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { ActionForm } from './action_form.js';
|
2
|
+
|
3
|
+
export class ActionsView {
|
4
|
+
constructor({ getCurrentPanelKey, actionsApi, onRunning, onResult, onAfterRun }) {
|
5
|
+
this.getCurrentPanelKey = getCurrentPanelKey;
|
6
|
+
this.actionsApi = actionsApi;
|
7
|
+
this.onRunning = onRunning || (() => {});
|
8
|
+
this.onResult = onResult || (() => {});
|
9
|
+
this.onAfterRun = onAfterRun || (() => {});
|
10
|
+
this.container = null;
|
11
|
+
}
|
12
|
+
|
13
|
+
mount(container) {
|
14
|
+
this.container = container;
|
15
|
+
}
|
16
|
+
|
17
|
+
setActions(actions) {
|
18
|
+
if (!this.container) return;
|
19
|
+
this.container.textContent = '';
|
20
|
+
(actions || []).forEach(action => {
|
21
|
+
const form = new ActionForm({
|
22
|
+
action,
|
23
|
+
getCurrentPanelKey: this.getCurrentPanelKey,
|
24
|
+
actionsApi: this.actionsApi,
|
25
|
+
onRunning: this.onRunning,
|
26
|
+
onResult: this.onResult,
|
27
|
+
onAfterRun: this.onAfterRun
|
28
|
+
});
|
29
|
+
form.mount(this.container);
|
30
|
+
});
|
31
|
+
}
|
32
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { el } from '../../core/dom.js';
|
2
|
+
|
3
|
+
export class HeaderView {
|
4
|
+
constructor() {
|
5
|
+
this.el = el('div', { id: 'cd-header' });
|
6
|
+
this.titleEl = el('h4', null, ['Command Deck']);
|
7
|
+
this.rightEl = el('div', { id: 'cd-header-right' });
|
8
|
+
this.el.appendChild(this.titleEl);
|
9
|
+
this.el.appendChild(this.rightEl);
|
10
|
+
}
|
11
|
+
|
12
|
+
mount(parent) {
|
13
|
+
parent.appendChild(this.el);
|
14
|
+
}
|
15
|
+
|
16
|
+
setTitle(text) {
|
17
|
+
this.titleEl.textContent = text;
|
18
|
+
}
|
19
|
+
|
20
|
+
getRightContainer() {
|
21
|
+
return this.rightEl;
|
22
|
+
}
|
23
|
+
}
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import { el, jsonPretty } from '../../core/dom.js';
|
2
|
+
import { store } from '../../core/store.js';
|
3
|
+
|
4
|
+
export class ResultView {
|
5
|
+
constructor() {
|
6
|
+
this.wrap = el('div', { id: 'cd-result-wrap' });
|
7
|
+
this.toolbar = el('div', { class: 'cd-result-toolbar' });
|
8
|
+
this.copyBtn = el('button', { class: 'cd-icon-btn', id: 'cd-copy-result', title: 'Copy result' }, ['📋']);
|
9
|
+
this.toggleBtn = el('button', { class: 'cd-icon-btn', id: 'cd-toggle-result', title: 'Hide results' }, ['▾']);
|
10
|
+
this.pre = el('pre', { id: 'command-deck-output' });
|
11
|
+
|
12
|
+
this.toolbar.appendChild(this.copyBtn);
|
13
|
+
this.toolbar.appendChild(this.toggleBtn);
|
14
|
+
this.wrap.appendChild(this.toolbar);
|
15
|
+
this.wrap.appendChild(this.pre);
|
16
|
+
|
17
|
+
this.toggleBtn.addEventListener('click', () => {
|
18
|
+
const visible = this.pre.style.display === 'none' ? false : true;
|
19
|
+
this.setVisible(!visible);
|
20
|
+
});
|
21
|
+
|
22
|
+
this.copyBtn.addEventListener('click', async () => {
|
23
|
+
const text = this.pre.textContent || '';
|
24
|
+
try {
|
25
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
26
|
+
await navigator.clipboard.writeText(text);
|
27
|
+
} else {
|
28
|
+
const ta = document.createElement('textarea');
|
29
|
+
ta.value = text;
|
30
|
+
ta.style.position = 'fixed';
|
31
|
+
ta.style.opacity = '0';
|
32
|
+
document.body.appendChild(ta);
|
33
|
+
ta.select();
|
34
|
+
document.execCommand('copy');
|
35
|
+
document.body.removeChild(ta);
|
36
|
+
}
|
37
|
+
const prev = this.copyBtn.textContent;
|
38
|
+
this.copyBtn.textContent = '✓';
|
39
|
+
setTimeout(() => { this.copyBtn.textContent = prev; }, 1000);
|
40
|
+
} catch(_) { /* ignore */ }
|
41
|
+
});
|
42
|
+
|
43
|
+
const initialVis = store.get('results-visible', '1') !== '0';
|
44
|
+
this.setVisible(initialVis);
|
45
|
+
}
|
46
|
+
|
47
|
+
mount(parentNode) {
|
48
|
+
parentNode.appendChild(this.wrap);
|
49
|
+
}
|
50
|
+
|
51
|
+
setVisible(show) {
|
52
|
+
this.pre.style.display = show ? 'block' : 'none';
|
53
|
+
this.toggleBtn.textContent = show ? '▾' : '▸';
|
54
|
+
this.toggleBtn.setAttribute('title', show ? 'Hide results' : 'Show results');
|
55
|
+
store.set('results-visible', show ? '1' : '0');
|
56
|
+
}
|
57
|
+
|
58
|
+
setRunning() {
|
59
|
+
this.setVisible(true);
|
60
|
+
this.pre.textContent = 'Running...';
|
61
|
+
}
|
62
|
+
|
63
|
+
setResult(obj) {
|
64
|
+
this.setVisible(true);
|
65
|
+
this.pre.textContent = jsonPretty(obj);
|
66
|
+
}
|
67
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import { el } from '../../core/dom.js';
|
2
|
+
|
3
|
+
export class TabsView {
|
4
|
+
constructor(onSelect) {
|
5
|
+
this.onSelect = onSelect;
|
6
|
+
this.root = el('div', { id: 'cd-tabs-wrap' });
|
7
|
+
this.buttons = [];
|
8
|
+
this.currentIndex = 0;
|
9
|
+
this.tabs = [];
|
10
|
+
}
|
11
|
+
|
12
|
+
mount(container) {
|
13
|
+
container.appendChild(this.root);
|
14
|
+
}
|
15
|
+
|
16
|
+
setTabs(tabs, selectedIndex = 0) {
|
17
|
+
this.tabs = tabs || [];
|
18
|
+
this.root.textContent = '';
|
19
|
+
this.buttons = [];
|
20
|
+
this.currentIndex = selectedIndex || 0;
|
21
|
+
|
22
|
+
this.tabs.forEach((tab, i) => {
|
23
|
+
const btn = el('button', { class: 'cd-tab-btn' }, [tab.title]);
|
24
|
+
btn.addEventListener('click', () => {
|
25
|
+
this.currentIndex = i;
|
26
|
+
this.onSelect && this.onSelect(this.tabs[i], i);
|
27
|
+
});
|
28
|
+
this.root.appendChild(btn);
|
29
|
+
this.buttons.push(btn);
|
30
|
+
});
|
31
|
+
|
32
|
+
// Trigger initial selection
|
33
|
+
if (this.tabs[this.currentIndex]) {
|
34
|
+
this.onSelect && this.onSelect(this.tabs[this.currentIndex], this.currentIndex);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
setActiveIndex(i) {
|
39
|
+
this.currentIndex = i;
|
40
|
+
}
|
41
|
+
}
|
@@ -1,7 +1,11 @@
|
|
1
|
-
import { el
|
1
|
+
import { el } from '../core/dom.js';
|
2
2
|
import { store } from '../core/store.js';
|
3
|
-
import { PositionManager } from './position_manager.js';
|
4
|
-
import { ThemeManager } from './theme_manager.js';
|
3
|
+
import { PositionManager } from './overlay/position_manager.js';
|
4
|
+
import { ThemeManager } from './overlay/theme_manager.js';
|
5
|
+
import { ResultView } from './overlay/result_view.js';
|
6
|
+
import { ActionsView } from './overlay/actions_view.js';
|
7
|
+
import { HeaderView } from './overlay/header_view.js';
|
8
|
+
import { TabsView } from './overlay/tabs_view.js';
|
5
9
|
|
6
10
|
export class Overlay {
|
7
11
|
constructor({ mount, panelsApi, actionsApi }) {
|
@@ -12,7 +16,6 @@ export class Overlay {
|
|
12
16
|
}
|
13
17
|
|
14
18
|
refreshCurrentTab() {
|
15
|
-
// Re-fetch panels and re-render current panel/tab without changing selection
|
16
19
|
this.panelsApi.fetchPanels().then((data) => {
|
17
20
|
const panels = (data && data.panels) || [];
|
18
21
|
this.panels = panels;
|
@@ -26,27 +29,31 @@ export class Overlay {
|
|
26
29
|
|
27
30
|
init() {
|
28
31
|
if (this._loaded) return; this._loaded = true;
|
29
|
-
// Root pieces
|
30
32
|
this.toggle = el('div', { id: 'command-deck-toggle', title: 'Command Deck' }, ['CD']);
|
31
33
|
this.panel = el('div', { id: 'command-deck-panel', style: { display: 'none' } });
|
32
|
-
// Keep these nodes across Turbo Drive navigations
|
33
34
|
this.toggle.setAttribute('data-turbo-permanent', '');
|
34
35
|
this.panel.setAttribute('data-turbo-permanent', '');
|
36
|
+
this.toggle.setAttribute('data-turbolinks-permanent', '');
|
37
|
+
this.panel.setAttribute('data-turbolinks-permanent', '');
|
35
38
|
|
36
|
-
|
37
|
-
|
38
|
-
headerWrap.appendChild(title);
|
39
|
-
const headerRight = el('div', { id: 'cd-header-right' });
|
40
|
-
headerWrap.appendChild(headerRight);
|
41
|
-
|
42
|
-
this.tabsWrap = el('div', { id: 'cd-tabs-wrap' });
|
39
|
+
this.headerView = new HeaderView();
|
40
|
+
this.tabsView = new TabsView((tab, i) => { this.currentTabIndex = i; this.renderActions(tab); });
|
43
41
|
this.actionsWrap = el('div', { id: 'cd-actions-wrap' });
|
44
|
-
this.
|
42
|
+
this.resultView = new ResultView();
|
43
|
+
this.actionsView = new ActionsView({
|
44
|
+
getCurrentPanelKey: () => this.currentPanelKey,
|
45
|
+
actionsApi: this.actionsApi,
|
46
|
+
onRunning: () => this.resultView.setRunning(),
|
47
|
+
onResult: (res) => this.resultView.setResult(res),
|
48
|
+
onAfterRun: () => this.refreshCurrentTab()
|
49
|
+
});
|
45
50
|
|
46
|
-
this.
|
47
|
-
this.
|
51
|
+
this.headerView.mount(this.panel);
|
52
|
+
this.tabsView.mount(this.panel);
|
48
53
|
this.panel.appendChild(this.actionsWrap);
|
49
|
-
this.
|
54
|
+
this.resultView.mount(this.panel);
|
55
|
+
|
56
|
+
this.actionsView.mount(this.actionsWrap);
|
50
57
|
|
51
58
|
document.body.appendChild(this.toggle);
|
52
59
|
document.body.appendChild(this.panel);
|
@@ -63,7 +70,6 @@ export class Overlay {
|
|
63
70
|
}
|
64
71
|
|
65
72
|
attachSettings(dropdown) {
|
66
|
-
// Append settings into right side of header
|
67
73
|
const headerRight = this.panel.querySelector('#cd-header-right');
|
68
74
|
dropdown.mount(headerRight);
|
69
75
|
if (this.theme) dropdown.setTheme && dropdown.setTheme(this.theme.load());
|
@@ -80,14 +86,15 @@ export class Overlay {
|
|
80
86
|
}
|
81
87
|
|
82
88
|
setPanels(data) {
|
83
|
-
this.
|
89
|
+
if (this.tabsView && this.tabsView.root) this.tabsView.root.textContent = '';
|
84
90
|
this.actionsWrap.textContent = '';
|
85
91
|
this.panels = (data && data.panels) || [];
|
86
92
|
if (!this.panels.length) {
|
87
|
-
this.
|
93
|
+
if (this.tabsView && this.tabsView.root) {
|
94
|
+
this.tabsView.root.appendChild(el('div', null, ['No panels defined. Add files under app/command_deck/**/*.rb']))
|
95
|
+
}
|
88
96
|
return;
|
89
97
|
}
|
90
|
-
// Initialize selector
|
91
98
|
if (this.panelSelector) {
|
92
99
|
this.panelSelector.setPanels(this.panels);
|
93
100
|
}
|
@@ -110,196 +117,13 @@ export class Overlay {
|
|
110
117
|
}
|
111
118
|
|
112
119
|
renderTabs(panel, selectedIndex = 0) {
|
113
|
-
this.
|
120
|
+
if (this.tabsView && this.tabsView.root) this.tabsView.root.textContent = '';
|
114
121
|
this.actionsWrap.textContent = '';
|
115
122
|
this.currentTabIndex = selectedIndex;
|
116
|
-
(panel.tabs || []
|
117
|
-
const btn = el('button', { class: 'cd-tab-btn' }, [tab.title]);
|
118
|
-
btn.addEventListener('click', () => { this.currentTabIndex = i; this.renderActions(tab); });
|
119
|
-
this.tabsWrap.appendChild(btn);
|
120
|
-
if (i === selectedIndex) this.renderActions(tab);
|
121
|
-
});
|
123
|
+
this.tabsView.setTabs((panel && panel.tabs) || [], selectedIndex);
|
122
124
|
}
|
123
125
|
|
124
126
|
renderActions(tab) {
|
125
|
-
this.
|
126
|
-
(tab.actions || []).forEach(action => {
|
127
|
-
const box = el('div', { class: 'cd-action' });
|
128
|
-
box.appendChild(el('div', { class: 'cd-action-title' }, [action.title]));
|
129
|
-
const form = el('div', { class: 'cd-form' });
|
130
|
-
|
131
|
-
const inputs = {};
|
132
|
-
const paramsMeta = {};
|
133
|
-
(action.params || []).forEach(p => {
|
134
|
-
const labelChildren = [p.label || p.name];
|
135
|
-
if (p.required === true) {
|
136
|
-
labelChildren.push(el('span', { class: 'cd-required', title: 'Required' }, ['*']));
|
137
|
-
}
|
138
|
-
const label = el('label', null, labelChildren);
|
139
|
-
let input;
|
140
|
-
if (p.type === 'boolean') {
|
141
|
-
input = el('input', { type: 'checkbox' });
|
142
|
-
} else if (p.type === 'integer') {
|
143
|
-
input = el('input', { type: 'number', step: '1' });
|
144
|
-
} else if (p.type === 'selector' || (p.choices && p.choices.length)) {
|
145
|
-
input = el('select');
|
146
|
-
if (p.include_blank) {
|
147
|
-
input.appendChild(el('option', { value: '' }, ['']));
|
148
|
-
}
|
149
|
-
(p.choices || []).forEach(ch => {
|
150
|
-
const attrs = { 'data-val': JSON.stringify(ch.value) };
|
151
|
-
if (ch.meta != null) attrs['data-meta'] = JSON.stringify(ch.meta);
|
152
|
-
const opt = el('option', attrs, [ch.label != null ? String(ch.label) : String(ch.value)]);
|
153
|
-
input.appendChild(opt);
|
154
|
-
});
|
155
|
-
|
156
|
-
// Restore last selection (per panel/action/param)
|
157
|
-
const selKey = `sel:${this.currentPanelKey || ''}:${action.key}:${p.name}`;
|
158
|
-
const savedRaw = store.get(selKey);
|
159
|
-
if (savedRaw != null) {
|
160
|
-
let matched = false;
|
161
|
-
for (let i = 0; i < input.options.length; i++) {
|
162
|
-
const o = input.options[i];
|
163
|
-
const raw = o.getAttribute('data-val');
|
164
|
-
if (raw === savedRaw) {
|
165
|
-
input.selectedIndex = i;
|
166
|
-
matched = true;
|
167
|
-
break;
|
168
|
-
}
|
169
|
-
}
|
170
|
-
if (!matched && p.include_blank && savedRaw === '') {
|
171
|
-
for (let i = 0; i < input.options.length; i++) {
|
172
|
-
if (input.options[i].value === '') { input.selectedIndex = i; break; }
|
173
|
-
}
|
174
|
-
}
|
175
|
-
}
|
176
|
-
} else {
|
177
|
-
input = el('input', { type: 'text' });
|
178
|
-
}
|
179
|
-
if (p.required === true && input) {
|
180
|
-
input.setAttribute('aria-required', 'true');
|
181
|
-
input.setAttribute('required', '');
|
182
|
-
}
|
183
|
-
form.appendChild(label);
|
184
|
-
form.appendChild(input);
|
185
|
-
|
186
|
-
// Optional hint below inputs (used for selector current value display)
|
187
|
-
let hint;
|
188
|
-
if (input.tagName === 'SELECT') {
|
189
|
-
hint = el('div', { class: 'cd-param-hint' });
|
190
|
-
const updateHint = () => {
|
191
|
-
const sel = input.options[input.selectedIndex];
|
192
|
-
if (!sel) { hint.textContent = ''; return; }
|
193
|
-
const metaRaw = sel.getAttribute('data-meta');
|
194
|
-
if (!metaRaw) { hint.textContent = ''; return; }
|
195
|
-
try {
|
196
|
-
const meta = JSON.parse(metaRaw);
|
197
|
-
if (typeof meta.enabled === 'boolean') {
|
198
|
-
const status = meta.enabled ? 'ON' : 'OFF';
|
199
|
-
hint.textContent = 'Current: ';
|
200
|
-
const badge = el('span', { class: `cd-badge ${meta.enabled ? 'cd-on' : 'cd-off'}` }, [status]);
|
201
|
-
hint.appendChild(badge);
|
202
|
-
} else {
|
203
|
-
hint.textContent = '';
|
204
|
-
}
|
205
|
-
} catch(_) { hint.textContent = ''; }
|
206
|
-
};
|
207
|
-
input.addEventListener('change', () => {
|
208
|
-
// Persist selection
|
209
|
-
const sel = input.options[input.selectedIndex];
|
210
|
-
const raw = sel ? sel.getAttribute('data-val') : '';
|
211
|
-
const selKey = `sel:${this.currentPanelKey || ''}:${action.key}:${p.name}`;
|
212
|
-
store.set(selKey, raw || '');
|
213
|
-
updateHint();
|
214
|
-
validate();
|
215
|
-
});
|
216
|
-
// Initialize after options are appended
|
217
|
-
setTimeout(updateHint, 0);
|
218
|
-
form.appendChild(hint);
|
219
|
-
}
|
220
|
-
|
221
|
-
inputs[p.name] = input;
|
222
|
-
paramsMeta[p.name] = p;
|
223
|
-
});
|
224
|
-
|
225
|
-
// Validation helpers
|
226
|
-
const requiredParams = (action.params || []).filter(p => p.required === true);
|
227
|
-
const isFilled = (p) => {
|
228
|
-
const inp = inputs[p.name];
|
229
|
-
if (!inp) return false;
|
230
|
-
if (p.type === 'integer') return inp.value !== '';
|
231
|
-
if (p.type === 'selector' || (p.choices && p.choices.length)) {
|
232
|
-
if (p.include_blank) return inp.value !== '' && inp.selectedIndex > -1;
|
233
|
-
return inp.selectedIndex > -1; // any selection counts
|
234
|
-
}
|
235
|
-
if (p.type === 'boolean') return true; // checkbox always provides a value
|
236
|
-
// string or others
|
237
|
-
return String(inp.value || '').trim() !== '';
|
238
|
-
};
|
239
|
-
const validate = () => {
|
240
|
-
const ok = requiredParams.every(isFilled);
|
241
|
-
const running = run.classList.contains('loading');
|
242
|
-
run.disabled = !ok || running;
|
243
|
-
};
|
244
|
-
|
245
|
-
const run = el('button', { class: 'cd-run', title: 'Run' }, ['\u25B6']); // ▶
|
246
|
-
run.addEventListener('click', () => {
|
247
|
-
if (run.disabled) return;
|
248
|
-
run.classList.add('loading');
|
249
|
-
run.setAttribute('aria-busy', 'true');
|
250
|
-
run.disabled = true;
|
251
|
-
const payload = {};
|
252
|
-
Object.keys(inputs).forEach(k => {
|
253
|
-
const inp = inputs[k];
|
254
|
-
const meta = paramsMeta[k] || {};
|
255
|
-
if (inp.tagName === 'SELECT') {
|
256
|
-
const sel = inp.options[inp.selectedIndex];
|
257
|
-
const raw = sel && sel.getAttribute('data-val');
|
258
|
-
const label = sel ? sel.textContent : '';
|
259
|
-
let value;
|
260
|
-
try { value = raw != null ? JSON.parse(raw) : inp.value; } catch(_) { value = inp.value; }
|
261
|
-
|
262
|
-
if (meta.return === 'label') {
|
263
|
-
payload[k] = label;
|
264
|
-
} else if (meta.return === 'both' || meta.return === 'object') {
|
265
|
-
payload[k] = { label: label, value: value };
|
266
|
-
} else {
|
267
|
-
payload[k] = value; // default: value
|
268
|
-
}
|
269
|
-
|
270
|
-
// Persist selection on run as well
|
271
|
-
const selKey = `sel:${this.currentPanelKey || ''}:${action.key}:${k}`;
|
272
|
-
store.set(selKey, raw || '');
|
273
|
-
} else {
|
274
|
-
payload[k] = (inp.type === 'checkbox') ? !!inp.checked : inp.value;
|
275
|
-
}
|
276
|
-
});
|
277
|
-
this.setRunning();
|
278
|
-
this.actionsApi
|
279
|
-
.submit(action.key, payload)
|
280
|
-
.then((res) => { this.setResult(res); this.refreshCurrentTab(); })
|
281
|
-
.catch((e) => this.setResult({ ok:false, error: String(e) }))
|
282
|
-
.finally(() => { run.classList.remove('loading'); run.removeAttribute('aria-busy'); validate(); });
|
283
|
-
});
|
284
|
-
const actionsRow = el('div', { class: 'cd-actions-row' });
|
285
|
-
actionsRow.appendChild(run);
|
286
|
-
form.appendChild(actionsRow);
|
287
|
-
|
288
|
-
box.appendChild(form);
|
289
|
-
this.actionsWrap.appendChild(box);
|
290
|
-
|
291
|
-
// Hook up validation listeners
|
292
|
-
(action.params || []).forEach(p => {
|
293
|
-
const inp = inputs[p.name];
|
294
|
-
if (!inp) return;
|
295
|
-
const evt = (inp.tagName === 'SELECT' || inp.type === 'checkbox') ? 'change' : 'input';
|
296
|
-
inp.addEventListener(evt, validate);
|
297
|
-
});
|
298
|
-
// Initial state
|
299
|
-
validate();
|
300
|
-
});
|
127
|
+
this.actionsView.setActions((tab && tab.actions) || []);
|
301
128
|
}
|
302
|
-
|
303
|
-
setRunning(){ this.resultPre.textContent = 'Running...'; }
|
304
|
-
setResult(obj){ this.resultPre.textContent = jsonPretty(obj); }
|
305
129
|
}
|
data/lib/command_deck/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: command_deck
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- crowrojas
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-09-
|
11
|
+
date: 2025-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -65,10 +65,15 @@ files:
|
|
65
65
|
- lib/command_deck/assets/js/core/store.js
|
66
66
|
- lib/command_deck/assets/js/main.js
|
67
67
|
- lib/command_deck/assets/js/ui/overlay.js
|
68
|
+
- lib/command_deck/assets/js/ui/overlay/action_form.js
|
69
|
+
- lib/command_deck/assets/js/ui/overlay/actions_view.js
|
70
|
+
- lib/command_deck/assets/js/ui/overlay/header_view.js
|
71
|
+
- lib/command_deck/assets/js/ui/overlay/position_manager.js
|
72
|
+
- lib/command_deck/assets/js/ui/overlay/result_view.js
|
73
|
+
- lib/command_deck/assets/js/ui/overlay/tabs_view.js
|
74
|
+
- lib/command_deck/assets/js/ui/overlay/theme_manager.js
|
68
75
|
- lib/command_deck/assets/js/ui/panel_selector.js
|
69
|
-
- lib/command_deck/assets/js/ui/position_manager.js
|
70
76
|
- lib/command_deck/assets/js/ui/settings_dropdown.js
|
71
|
-
- lib/command_deck/assets/js/ui/theme_manager.js
|
72
77
|
- lib/command_deck/engine.rb
|
73
78
|
- lib/command_deck/executor.rb
|
74
79
|
- lib/command_deck/injector.rb
|