video automations: working builder + New Automation button (own builder, not music's)
The video automations page had no way to CREATE automations and its card cog did nothing — it called the music global showAutomationBuilder(), which swaps views inside the (hidden-on-video) music page, so the builder 'opened' on the music tab instead. Now the video side has its own builder. - index.html: the video automations subpage gets its own list-view + builder-view (vauto- prefixed ids) and a '+ New Automation' button, swapping exactly like the music page. Save/Cancel/Back reuse the shared builder functions. - stats-automations.js: the builder is now context-aware. A builder context holds the element ids + blocks endpoint + owned_by + reload callback. Music context is the default and byte-identical (all 17 id lookups go through _bEl() resolving the music ids). showVideoAutomationBuilder() sets a video context (vauto- ids, /api/video/automations/blocks, owned_by='video'); editAutomation() routes the card cog to the right builder by active side. Opening clears BOTH builders' canvases so cfg-* ids can't collide. Save tags owned_by from context and calls the context's reload. A generic config_fields renderer/reader (video-gated so music keeps its bespoke renderers) drives video block config like the mode select. - video-automations.js: exposes window._reloadVideoAutomations so a save refreshes the video list. 11 wiring tests (test_video_automations_builder.py); node --check clean; music builder path unchanged.
This commit is contained in:
parent
5d488bceb4
commit
1fa3e1b599
4 changed files with 289 additions and 35 deletions
101
tests/test_video_automations_builder.py
Normal file
101
tests/test_video_automations_builder.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"""Video automation BUILDER wiring — the isolated video page must be able to
|
||||
create/edit its own automations without hijacking the music page's builder.
|
||||
|
||||
These pin the wiring contract that makes that work:
|
||||
- the video automations subpage hosts its OWN builder DOM (vauto- prefixed ids)
|
||||
+ a "New Automation" button, swapping list/builder like the music page;
|
||||
- the shared builder (stats-automations.js) is context-aware: a video context
|
||||
points at the video ids + the video-scoped blocks endpoint + owned_by='video',
|
||||
and the card cog routes to the video builder when the video side is active;
|
||||
- a save tags the row owned_by='video' so it stays off the music page;
|
||||
- a generic config_fields renderer drives video block config (so the video
|
||||
action's fields show up) and is gated to the video context (music untouched).
|
||||
|
||||
String-contract level (like tests/test_video_side_shell.py) so a refactor that
|
||||
silently breaks the coupling fails here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent.parent
|
||||
_INDEX = (_ROOT / "webui" / "index.html").read_text(encoding="utf-8", errors="replace")
|
||||
_STATS = (_ROOT / "webui" / "static" / "stats-automations.js").read_text(encoding="utf-8")
|
||||
_VAUTO = (_ROOT / "webui" / "static" / "video" / "video-automations.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# --- the video subpage builder DOM ----------------------------------------
|
||||
|
||||
def test_video_subpage_has_new_automation_button():
|
||||
# Header button + empty-state button both open the VIDEO builder.
|
||||
assert _INDEX.count('onclick="showVideoAutomationBuilder()"') >= 1
|
||||
assert 'auto-new-btn' in _INDEX
|
||||
|
||||
|
||||
def test_video_subpage_has_list_and_builder_views():
|
||||
for tok in ('id="vauto-list-view"', 'id="vauto-builder-view"'):
|
||||
assert tok in _INDEX, tok
|
||||
|
||||
|
||||
def test_video_builder_has_prefixed_ids():
|
||||
for tok in (
|
||||
'id="vauto-builder-name"', 'id="vauto-builder-group-name"',
|
||||
'id="vauto-builder-group-list"', 'id="vauto-builder-sidebar"',
|
||||
'id="vauto-builder-canvas"',
|
||||
):
|
||||
assert tok in _INDEX, tok
|
||||
|
||||
|
||||
def test_video_builder_buttons_use_shared_handlers():
|
||||
# Save / Cancel / Back reuse the SAME shared functions (ctx-driven).
|
||||
assert 'onclick="saveAutomation()"' in _INDEX
|
||||
assert 'onclick="hideAutomationBuilder()"' in _INDEX
|
||||
|
||||
|
||||
# --- the shared builder is context-aware ----------------------------------
|
||||
|
||||
def test_shared_builder_has_video_entry_point():
|
||||
assert 'function showVideoAutomationBuilder(' in _STATS
|
||||
assert 'function showAutomationBuilder(' in _STATS
|
||||
assert 'function _openAutomationBuilder(' in _STATS
|
||||
|
||||
|
||||
def test_video_context_targets_video_ids_and_endpoint():
|
||||
assert "'/api/video/automations/blocks'" in _STATS
|
||||
assert "ownedBy: 'video'" in _STATS
|
||||
# The video context must reference the vauto- prefixed element ids.
|
||||
for tok in ('vauto-builder-name', 'vauto-builder-sidebar', 'vauto-builder-canvas',
|
||||
'vauto-list-view', 'vauto-builder-view'):
|
||||
assert tok in _STATS, tok
|
||||
|
||||
|
||||
def test_card_cog_routes_by_active_side():
|
||||
# Cards call editAutomation (not showAutomationBuilder directly) so a video
|
||||
# card opens the video builder instead of the hidden music one.
|
||||
assert 'editAutomation(${a.id})' in _STATS
|
||||
assert 'function editAutomation(' in _STATS
|
||||
assert "getAttribute('data-side') === 'video'" in _STATS
|
||||
|
||||
|
||||
def test_save_tags_owned_by_from_context():
|
||||
assert 'body.owned_by = _autoBuilderCtx.ownedBy' in _STATS
|
||||
|
||||
|
||||
def test_open_clears_both_builders_to_avoid_id_collision():
|
||||
# cfg-* ids exist in both builders; opening clears both canvases/sidebars.
|
||||
assert "'vauto-builder-sidebar', 'vauto-builder-canvas'" in _STATS
|
||||
|
||||
|
||||
def test_generic_config_renderer_is_video_gated():
|
||||
# The generic config_fields renderer/reader must only run in the video
|
||||
# context so the music side keeps its bespoke renderers (byte-identical).
|
||||
assert 'function _renderGenericConfigField(' in _STATS
|
||||
assert 'function _readGenericConfigField(' in _STATS
|
||||
assert 'if (_autoBuilderCtx.ownedBy) {' in _STATS
|
||||
|
||||
|
||||
# --- the video page exposes its reload hook -------------------------------
|
||||
|
||||
def test_video_page_exposes_reload_hook():
|
||||
assert 'window._reloadVideoAutomations = load' in _VAUTO
|
||||
|
|
@ -1313,20 +1313,49 @@
|
|||
(minus Refresh Beatport Cache + user/playlist ones). Reuses the
|
||||
music .automation-* card look; built by video/video-automations.js. -->
|
||||
<section class="video-subpage" data-video-subpage="video-automations" hidden>
|
||||
<div class="page-shell automations-container">
|
||||
<div class="dashboard-header"><div class="dashboard-header-sweep" aria-hidden="true"><span></span></div>
|
||||
<div class="header-text">
|
||||
<h2 class="header-title"><img src="/static/automation.png" class="page-header-icon" alt=""><span>Automations</span></h2>
|
||||
<p class="header-subtitle">System tasks that keep your library fresh — shared with the music side</p>
|
||||
<!-- List view (swaps with the builder view below, exactly like the
|
||||
music page does inside #automations-page). -->
|
||||
<div class="automations-list-view" id="vauto-list-view">
|
||||
<div class="page-shell automations-container">
|
||||
<div class="dashboard-header"><div class="dashboard-header-sweep" aria-hidden="true"><span></span></div>
|
||||
<div class="header-text">
|
||||
<h2 class="header-title"><img src="/static/automation.png" class="page-header-icon" alt=""><span>Automations</span></h2>
|
||||
<p class="header-subtitle">Video tasks that keep your movie & TV library fresh — separate from the music side</p>
|
||||
</div>
|
||||
<div class="header-spacer"></div>
|
||||
<div class="header-actions">
|
||||
<button class="auto-new-btn" onclick="showVideoAutomationBuilder()">+ New Automation</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="automations-stats" data-vauto-stats></div>
|
||||
<div class="automations-list" data-vauto-list></div>
|
||||
<div class="automations-empty" data-vauto-empty style="display:none;">
|
||||
<div class="automations-empty-icon">⚡</div>
|
||||
<div class="automations-empty-title">No video automations yet</div>
|
||||
<div class="automations-empty-text">The video side has its own automations (library scan, cleanup, wishlist…) — separate from the music ones. Create your first one.</div>
|
||||
<button class="auto-new-btn" onclick="showVideoAutomationBuilder()">+ New Automation</button>
|
||||
</div>
|
||||
<div class="header-spacer"></div>
|
||||
</div>
|
||||
<div class="automations-stats" data-vauto-stats></div>
|
||||
<div class="automations-list" data-vauto-list></div>
|
||||
<div class="automations-empty" data-vauto-empty style="display:none;">
|
||||
<div class="automations-empty-icon">⚡</div>
|
||||
<div class="automations-empty-title">Video automations coming soon</div>
|
||||
<div class="automations-empty-text">The video side gets its own automations (library scan, cleanup, wishlist…) — separate from the music ones.</div>
|
||||
</div>
|
||||
|
||||
<!-- Builder view — mirrors the music builder markup with vauto- prefixed
|
||||
ids so the SAME shared builder functions (showAutomationBuilder core,
|
||||
saveAutomation, hideAutomationBuilder) drive it via a video context.
|
||||
Its sidebar is fed by /api/video/automations/blocks (video scope). -->
|
||||
<div class="automations-builder-view" id="vauto-builder-view" style="display:none;">
|
||||
<div class="builder-header">
|
||||
<button class="builder-back-btn" onclick="hideAutomationBuilder()" title="Back to list">←</button>
|
||||
<input type="text" id="vauto-builder-name" class="builder-name-input" placeholder="Automation Name">
|
||||
<input type="text" id="vauto-builder-group-name" class="builder-group-input" placeholder="Group (optional)" list="vauto-builder-group-list">
|
||||
<datalist id="vauto-builder-group-list"></datalist>
|
||||
<div class="builder-header-actions">
|
||||
<button class="btn-cancel" onclick="hideAutomationBuilder()">Cancel</button>
|
||||
<button class="btn-save" onclick="saveAutomation()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="builder-content">
|
||||
<div class="builder-sidebar" id="vauto-builder-sidebar"></div>
|
||||
<div class="builder-canvas" id="vauto-builder-canvas"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1703,6 +1703,48 @@ async function retryFailedMirroredDiscovery(urlHash) {
|
|||
let _autoBlocks = null; // cached block definitions from /api/automations/blocks
|
||||
let _autoBuilder = { editId: null, when: null, do: null, then: [], isSystem: false };
|
||||
|
||||
// Builder context — lets the SAME builder functions drive either the music
|
||||
// automation page or the (isolated) video automation page. Default = music, so
|
||||
// existing music behaviour is byte-identical. The video page swaps in its own
|
||||
// element ids + the video-scoped blocks endpoint + owned_by tagging before
|
||||
// opening, so a video automation is built with video triggers/actions and never
|
||||
// leaks onto the music page. Only one builder is open at a time, so a single
|
||||
// shared context is safe.
|
||||
const _AUTO_CTX_MUSIC = {
|
||||
ids: {
|
||||
listView: 'automations-list-view', builderView: 'automations-builder-view',
|
||||
name: 'builder-name', group: 'builder-group-name', groupList: 'builder-group-list',
|
||||
sidebar: 'builder-sidebar', canvas: 'builder-canvas',
|
||||
},
|
||||
blocksUrl: '/api/automations/blocks',
|
||||
ownedBy: null,
|
||||
onSaved: function () { return loadAutomations(); },
|
||||
};
|
||||
const _AUTO_CTX_VIDEO = {
|
||||
ids: {
|
||||
listView: 'vauto-list-view', builderView: 'vauto-builder-view',
|
||||
name: 'vauto-builder-name', group: 'vauto-builder-group-name', groupList: 'vauto-builder-group-list',
|
||||
sidebar: 'vauto-builder-sidebar', canvas: 'vauto-builder-canvas',
|
||||
},
|
||||
blocksUrl: '/api/video/automations/blocks',
|
||||
ownedBy: 'video',
|
||||
// Reloads the video automations list after a save (video-automations.js
|
||||
// exposes this; falls back to a no-op if the video page isn't loaded).
|
||||
onSaved: function () { return window._reloadVideoAutomations ? window._reloadVideoAutomations() : null; },
|
||||
};
|
||||
let _autoBuilderCtx = _AUTO_CTX_MUSIC;
|
||||
|
||||
// Resolve a builder element through the active context (music vs video ids).
|
||||
function _bEl(key) { return document.getElementById(_autoBuilderCtx.ids[key]); }
|
||||
|
||||
// Route a card's edit/cog to the right builder based on the active side, so a
|
||||
// video card opens the VIDEO builder (on the video page) instead of hijacking
|
||||
// the hidden music page's builder.
|
||||
function editAutomation(id) {
|
||||
if (document.body.getAttribute('data-side') === 'video') return showVideoAutomationBuilder(id);
|
||||
return showAutomationBuilder(id);
|
||||
}
|
||||
|
||||
let _autoMirroredPlaylists = null; // cached mirrored playlist list
|
||||
let _autoSpotifyAuthenticated = false; // whether Spotify is authed (for refresh filtering)
|
||||
|
||||
|
|
@ -2911,7 +2953,7 @@ async function useHubRecipe(recipeId) {
|
|||
const t = AUTO_HUB_RECIPES.find(r => r.id === recipeId);
|
||||
if (!t) return;
|
||||
await showAutomationBuilder();
|
||||
document.getElementById('builder-name').value = t.name;
|
||||
_bEl('name').value = t.name;
|
||||
_autoBuilder.when = { type: t.when.type, config: JSON.parse(JSON.stringify(t.when.config)) };
|
||||
_autoBuilder.do = { type: t.do.type, config: JSON.parse(JSON.stringify(t.do.config)) };
|
||||
_autoBuilder.then = t.then.map(th => ({ type: th.type, config: JSON.parse(JSON.stringify(th.config)) }));
|
||||
|
|
@ -3267,7 +3309,7 @@ function renderAutomationCard(a) {
|
|||
<input type="checkbox" ${a.enabled ? 'checked' : ''} onchange="toggleAutomation(${a.id})">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<button class="automation-edit-btn" title="Edit" onclick="event.stopPropagation(); showAutomationBuilder(${a.id})">⚙</button>
|
||||
<button class="automation-edit-btn" title="Edit" onclick="event.stopPropagation(); editAutomation(${a.id})">⚙</button>
|
||||
${dupeBtn}
|
||||
${groupBtn}
|
||||
${deleteBtn}
|
||||
|
|
@ -3677,7 +3719,7 @@ function _formatDuration(seconds) {
|
|||
}
|
||||
|
||||
async function saveAutomation() {
|
||||
const name = document.getElementById('builder-name').value.trim();
|
||||
const name = _bEl('name').value.trim();
|
||||
if (!name) { showToast('Name is required', 'error'); return; }
|
||||
if (!_autoBuilder.when) { showToast('Add a trigger (WHEN)', 'error'); return; }
|
||||
if (!_autoBuilder.do) { showToast('Add an action (DO)', 'error'); return; }
|
||||
|
|
@ -3697,7 +3739,7 @@ async function saveAutomation() {
|
|||
const delayVal = delayEl ? parseInt(delayEl.value) : 0;
|
||||
if (delayVal > 0) actionConfig.delay = delayVal;
|
||||
|
||||
const groupInput = document.getElementById('builder-group-name');
|
||||
const groupInput = _bEl('group');
|
||||
const groupName = groupInput ? groupInput.value.trim() : '';
|
||||
|
||||
const body = {
|
||||
|
|
@ -3707,6 +3749,9 @@ async function saveAutomation() {
|
|||
then_actions: thenActions,
|
||||
group_name: groupName || null,
|
||||
};
|
||||
// Tag the side that owns this automation (video) so it stays on the video
|
||||
// page and off the music one. Music context leaves this null (unchanged).
|
||||
if (_autoBuilderCtx.ownedBy) body.owned_by = _autoBuilderCtx.ownedBy;
|
||||
|
||||
try {
|
||||
let res;
|
||||
|
|
@ -3719,16 +3764,36 @@ async function saveAutomation() {
|
|||
if (data.error) throw new Error(data.error);
|
||||
showToast(_autoBuilder.editId ? 'Automation updated' : 'Automation created', 'success');
|
||||
hideAutomationBuilder();
|
||||
await loadAutomations();
|
||||
if (_autoBuilderCtx.onSaved) await _autoBuilderCtx.onSaved();
|
||||
} catch (err) { showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
// --- Builder View ---
|
||||
|
||||
async function showAutomationBuilder(editId) {
|
||||
// Load block definitions (always refresh)
|
||||
// Music entry point — sets the music context, then opens the shared builder.
|
||||
function showAutomationBuilder(editId) {
|
||||
_autoBuilderCtx = _AUTO_CTX_MUSIC;
|
||||
return _openAutomationBuilder(editId);
|
||||
}
|
||||
|
||||
// Video entry point — sets the video context (video ids + video-scoped blocks +
|
||||
// owned_by='video'), then opens the SAME shared builder on the video page.
|
||||
function showVideoAutomationBuilder(editId) {
|
||||
_autoBuilderCtx = _AUTO_CTX_VIDEO;
|
||||
return _openAutomationBuilder(editId);
|
||||
}
|
||||
|
||||
async function _openAutomationBuilder(editId) {
|
||||
// Clear BOTH builders' sidebar/canvas first so stale cfg-* ids from a
|
||||
// previously-open builder (music or video) can never be matched by
|
||||
// getElementById while this one is open. Cheap and bulletproof.
|
||||
['builder-sidebar', 'builder-canvas', 'vauto-builder-sidebar', 'vauto-builder-canvas'].forEach(function (id) {
|
||||
const el = document.getElementById(id); if (el) el.innerHTML = '';
|
||||
});
|
||||
|
||||
// Load block definitions for the active scope (always refresh).
|
||||
try {
|
||||
const res = await fetch('/api/automations/blocks');
|
||||
const res = await fetch(_autoBuilderCtx.blocksUrl);
|
||||
_autoBlocks = await res.json();
|
||||
} catch (e) {
|
||||
if (!_autoBlocks) { showToast('Failed to load blocks', 'error'); return; }
|
||||
|
|
@ -3744,7 +3809,7 @@ async function showAutomationBuilder(editId) {
|
|||
const allAutos = await allRes.json();
|
||||
const groupSet = new Set();
|
||||
if (Array.isArray(allAutos)) allAutos.forEach(a => { if (a.group_name) groupSet.add(a.group_name); });
|
||||
const datalist = document.getElementById('builder-group-list');
|
||||
const datalist = _bEl('groupList');
|
||||
if (datalist) datalist.innerHTML = [...groupSet].sort().map(g => `<option value="${_escAttr(g)}">`).join('');
|
||||
} catch (e) { }
|
||||
|
||||
|
|
@ -3754,8 +3819,8 @@ async function showAutomationBuilder(editId) {
|
|||
const res = await fetch('/api/automations/' + editId);
|
||||
const a = await res.json();
|
||||
if (a.error) throw new Error(a.error);
|
||||
document.getElementById('builder-name').value = a.name || '';
|
||||
const groupInput = document.getElementById('builder-group-name');
|
||||
_bEl('name').value = a.name || '';
|
||||
const groupInput = _bEl('group');
|
||||
if (groupInput) groupInput.value = a.group_name || '';
|
||||
_autoBuilder.when = { type: a.trigger_type, config: a.trigger_config || {} };
|
||||
_autoBuilder.do = { type: a.action_type, config: a.action_config || {} };
|
||||
|
|
@ -3770,34 +3835,34 @@ async function showAutomationBuilder(editId) {
|
|||
_autoBuilder.isSystem = !!a.is_system;
|
||||
} catch (err) { showToast('Failed to load automation', 'error'); return; }
|
||||
} else {
|
||||
document.getElementById('builder-name').value = '';
|
||||
const groupInput = document.getElementById('builder-group-name');
|
||||
_bEl('name').value = '';
|
||||
const groupInput = _bEl('group');
|
||||
if (groupInput) groupInput.value = '';
|
||||
}
|
||||
|
||||
// System automations: lock the name field and hide group
|
||||
document.getElementById('builder-name').readOnly = _autoBuilder.isSystem;
|
||||
const groupEl = document.getElementById('builder-group-name');
|
||||
_bEl('name').readOnly = _autoBuilder.isSystem;
|
||||
const groupEl = _bEl('group');
|
||||
if (groupEl) groupEl.style.display = _autoBuilder.isSystem ? 'none' : '';
|
||||
|
||||
_renderBuilderSidebar();
|
||||
_renderBuilderCanvas();
|
||||
|
||||
document.getElementById('automations-list-view').style.display = 'none';
|
||||
document.getElementById('automations-builder-view').style.display = '';
|
||||
_bEl('listView').style.display = 'none';
|
||||
_bEl('builderView').style.display = '';
|
||||
}
|
||||
|
||||
function hideAutomationBuilder() {
|
||||
document.getElementById('automations-builder-view').style.display = 'none';
|
||||
document.getElementById('automations-list-view').style.display = '';
|
||||
document.getElementById('builder-name').readOnly = false;
|
||||
_bEl('builderView').style.display = 'none';
|
||||
_bEl('listView').style.display = '';
|
||||
const nameEl = _bEl('name'); if (nameEl) nameEl.readOnly = false;
|
||||
_autoBuilder = { editId: null, when: null, do: null, then: [], isSystem: false };
|
||||
}
|
||||
|
||||
// --- Sidebar ---
|
||||
|
||||
function _renderBuilderSidebar() {
|
||||
const sidebar = document.getElementById('builder-sidebar');
|
||||
const sidebar = _bEl('sidebar');
|
||||
if (!sidebar || !_autoBlocks) return;
|
||||
|
||||
let html = '';
|
||||
|
|
@ -3832,7 +3897,7 @@ function _renderBuilderSidebar() {
|
|||
// --- Canvas ---
|
||||
|
||||
function _renderBuilderCanvas() {
|
||||
const canvas = document.getElementById('builder-canvas');
|
||||
const canvas = _bEl('canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
let html = '';
|
||||
|
|
@ -4218,9 +4283,48 @@ function _renderBlockConfigFields(slotKey, blockType, config) {
|
|||
</div>
|
||||
${_notifyVarHtml(slotKey)}`;
|
||||
}
|
||||
// Generic config_fields renderer — drives the VIDEO builder only (gated on
|
||||
// the video context) so the music side keeps its bespoke renderers above and
|
||||
// is byte-identical. Any video block that declares config_fields in
|
||||
// core/automation/blocks.py (select / text / number / checkbox) renders here.
|
||||
if (_autoBuilderCtx.ownedBy) {
|
||||
const def = _findBlockDef(blockType);
|
||||
if (def && Array.isArray(def.config_fields) && def.config_fields.length) {
|
||||
return def.config_fields.map(f => _renderGenericConfigField(slotKey, f, config)).join('');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// One generic config row from a block's config_fields definition.
|
||||
function _renderGenericConfigField(slotKey, f, config) {
|
||||
const id = 'cfg-' + slotKey + '-' + f.key;
|
||||
const cur = (config && config[f.key] !== undefined && config[f.key] !== null) ? config[f.key] : f.default;
|
||||
const label = _esc(f.label || f.key);
|
||||
if (f.type === 'select') {
|
||||
const opts = (f.options || []).map(o =>
|
||||
`<option value="${_escAttr(o.value)}"${String(cur) === String(o.value) ? ' selected' : ''}>${_esc(o.label)}</option>`).join('');
|
||||
return `<div class="config-row"><label>${label}</label><select id="${id}">${opts}</select></div>`;
|
||||
}
|
||||
if (f.type === 'checkbox') {
|
||||
return `<div class="config-row"><label><input type="checkbox" id="${id}"${cur ? ' checked' : ''}> ${label}</label></div>`;
|
||||
}
|
||||
if (f.type === 'number') {
|
||||
const min = (f.min !== undefined) ? ` min="${f.min}"` : '';
|
||||
return `<div class="config-row"><label>${label}</label><input type="number" id="${id}" value="${_escAttr(cur != null ? cur : '')}"${min} style="width:90px;"></div>`;
|
||||
}
|
||||
return `<div class="config-row"><label>${label}</label><input type="text" id="${id}" value="${_escAttr(cur != null ? cur : '')}" placeholder="${_escAttr(f.placeholder || '')}"></div>`;
|
||||
}
|
||||
|
||||
// Read one generic config field back from the DOM (mirror of the renderer).
|
||||
function _readGenericConfigField(slotKey, f) {
|
||||
const el = document.getElementById('cfg-' + slotKey + '-' + f.key);
|
||||
if (!el) return (f.default !== undefined) ? f.default : '';
|
||||
if (f.type === 'checkbox') return el.checked;
|
||||
if (f.type === 'number') { const n = parseFloat(el.value); return isNaN(n) ? (f.default != null ? f.default : 0) : n; }
|
||||
return el.value;
|
||||
}
|
||||
|
||||
// --- Condition Builder ---
|
||||
|
||||
function _renderConditionBuilder(slotKey, blockDef, config) {
|
||||
|
|
@ -4582,6 +4686,16 @@ function _readPlacedConfig(slotKey) {
|
|||
message: document.getElementById('cfg-' + slotKey + '-message')?.value || '',
|
||||
};
|
||||
}
|
||||
// Generic config_fields read — video builder only (mirror of the generic
|
||||
// renderer above); music keeps its bespoke readers, untouched.
|
||||
if (_autoBuilderCtx.ownedBy) {
|
||||
const def = _findBlockDef(type);
|
||||
if (def && Array.isArray(def.config_fields) && def.config_fields.length) {
|
||||
const out = {};
|
||||
def.config_fields.forEach(f => { out[f.key] = _readGenericConfigField(slotKey, f); });
|
||||
return out;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
}
|
||||
|
||||
function load() {
|
||||
getJSON('/api/automations').then(function (d) {
|
||||
return getJSON('/api/automations').then(function (d) {
|
||||
var all = Array.isArray(d) ? d : (d && d.automations) || [];
|
||||
var sys = all.filter(isVideoAutomation);
|
||||
renderStats(sys);
|
||||
|
|
@ -78,12 +78,22 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Exposed so the shared automation builder (stats-automations.js) can refresh
|
||||
// THIS list after a video automation is created/edited/saved. Lives on window
|
||||
// because the builder is global and this module is an IIFE.
|
||||
window._reloadVideoAutomations = load;
|
||||
|
||||
function onPage() {
|
||||
return document.body.getAttribute('data-side') === 'video' &&
|
||||
!!document.querySelector('[data-video-subpage="video-automations"]:not([hidden])');
|
||||
}
|
||||
|
||||
function start() {
|
||||
// Always enter on the list view — if the builder was left open from a
|
||||
// previous visit, swap back so the page doesn't greet you mid-build.
|
||||
var bv = document.getElementById('vauto-builder-view');
|
||||
var lv = document.getElementById('vauto-list-view');
|
||||
if (bv && lv && bv.style.display !== 'none') { bv.style.display = 'none'; lv.style.display = ''; }
|
||||
load();
|
||||
// Refresh the System section shortly after a run/toggle (the reused music card
|
||||
// handlers fire on the music list, not ours) — keeps our cards in sync.
|
||||
|
|
|
|||
Loading…
Reference in a new issue