Add clone support for scene and automation cards, update sync clock descriptions
- Scene clone: opens capture modal with prefilled name/description/targets instead of server-side duplication; removed backend clone endpoint - Automation clone: opens editor with prefilled conditions, scene, logic, deactivation mode (webhook tokens stripped for uniqueness) - Updated sync clock i18n descriptions to reflect speed-only-on-clock model - Added entity card clone pattern documentation to server/CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,7 @@ import {
|
||||
import {
|
||||
loadAutomations, openAutomationEditor, closeAutomationEditorModal,
|
||||
saveAutomationEditor, addAutomationCondition,
|
||||
toggleAutomationEnabled, deleteAutomation, copyWebhookUrl,
|
||||
toggleAutomationEnabled, cloneAutomation, deleteAutomation, copyWebhookUrl,
|
||||
expandAllAutomationSections, collapseAllAutomationSections,
|
||||
} from './features/automations.js';
|
||||
import {
|
||||
@@ -315,6 +315,7 @@ Object.assign(window, {
|
||||
saveAutomationEditor,
|
||||
addAutomationCondition,
|
||||
toggleAutomationEnabled,
|
||||
cloneAutomation,
|
||||
deleteAutomation,
|
||||
copyWebhookUrl,
|
||||
expandAllAutomationSections,
|
||||
|
||||
@@ -185,6 +185,7 @@ function createAutomationCard(automation, sceneMap = new Map()) {
|
||||
</div>
|
||||
<div class="stream-card-props">${condPills}</div>`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" onclick="cloneAutomation('${automation.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="openAutomationEditor('${automation.id}')" title="${t('automations.edit')}">${ICON_SETTINGS}</button>
|
||||
<button class="btn btn-icon ${automation.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleAutomationEnabled('${automation.id}', ${!automation.enabled})" title="${automation.enabled ? t('automations.action.disable') : t('automations.status.active')}">
|
||||
${automation.enabled ? ICON_PAUSE : ICON_START}
|
||||
@@ -192,7 +193,7 @@ function createAutomationCard(automation, sceneMap = new Map()) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function openAutomationEditor(automationId) {
|
||||
export async function openAutomationEditor(automationId, cloneData) {
|
||||
const modal = document.getElementById('automation-editor-modal');
|
||||
const titleEl = document.getElementById('automation-editor-title');
|
||||
const idInput = document.getElementById('automation-editor-id');
|
||||
@@ -241,6 +242,26 @@ export async function openAutomationEditor(automationId) {
|
||||
showToast(e.message, 'error');
|
||||
return;
|
||||
}
|
||||
} else if (cloneData) {
|
||||
// Clone mode — create with prefilled data
|
||||
titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`;
|
||||
idInput.value = '';
|
||||
nameInput.value = (cloneData.name || '') + ' (Copy)';
|
||||
enabledInput.checked = cloneData.enabled !== false;
|
||||
logicSelect.value = cloneData.condition_logic || 'or';
|
||||
|
||||
// Clone conditions (strip webhook tokens — they must be unique)
|
||||
for (const c of (cloneData.conditions || [])) {
|
||||
const clonedCond = { ...c };
|
||||
if (clonedCond.condition_type === 'webhook') delete clonedCond.token;
|
||||
addAutomationConditionRow(clonedCond);
|
||||
}
|
||||
|
||||
_initSceneSelector('automation-scene', cloneData.scene_preset_id);
|
||||
|
||||
document.getElementById('automation-deactivation-mode').value = cloneData.deactivation_mode || 'none';
|
||||
_onDeactivationModeChange();
|
||||
_initSceneSelector('automation-fallback-scene', cloneData.deactivation_scene_preset_id);
|
||||
} else {
|
||||
titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`;
|
||||
idInput.value = '';
|
||||
@@ -729,6 +750,18 @@ export function copyWebhookUrl(btn) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function cloneAutomation(automationId) {
|
||||
try {
|
||||
const resp = await fetchWithAuth(`/automations/${automationId}`);
|
||||
if (!resp.ok) throw new Error('Failed to load automation');
|
||||
const automation = await resp.json();
|
||||
openAutomationEditor(null, automation);
|
||||
} catch (e) {
|
||||
if (e.isAuth) return;
|
||||
showToast(t('automations.error.clone_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAutomation(automationId, automationName) {
|
||||
const msg = t('automations.delete.confirm').replace('{name}', automationName);
|
||||
const confirmed = await showConfirm(msg);
|
||||
|
||||
@@ -308,21 +308,49 @@ export async function recaptureScenePreset(presetId) {
|
||||
// ===== Clone =====
|
||||
|
||||
export async function cloneScenePreset(presetId) {
|
||||
try {
|
||||
const resp = await fetchWithAuth(`/scene-presets/${presetId}/clone`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (resp.ok) {
|
||||
showToast(t('scenes.cloned'), 'success');
|
||||
_reloadScenesTab();
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
showToast(err.detail || t('scenes.error.clone_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
showToast(t('scenes.error.clone_failed'), 'error');
|
||||
const preset = scenePresetsCache.data.find(p => p.id === presetId);
|
||||
if (!preset) return;
|
||||
|
||||
// Open the capture modal in create mode, prefilled from the cloned preset
|
||||
_editingId = null;
|
||||
document.getElementById('scene-preset-editor-id').value = '';
|
||||
document.getElementById('scene-preset-editor-name').value = (preset.name || '') + ' (Copy)';
|
||||
document.getElementById('scene-preset-editor-description').value = preset.description || '';
|
||||
document.getElementById('scene-preset-editor-error').style.display = 'none';
|
||||
|
||||
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
||||
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.add'); titleEl.textContent = t('scenes.add'); }
|
||||
|
||||
// Fetch targets and populate selector, then pre-add the cloned preset's targets
|
||||
const selectorGroup = document.getElementById('scene-target-selector-group');
|
||||
const targetList = document.getElementById('scene-target-list');
|
||||
if (selectorGroup && targetList) {
|
||||
selectorGroup.style.display = '';
|
||||
targetList.innerHTML = '';
|
||||
try {
|
||||
const resp = await fetchWithAuth('/picture-targets');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
_allTargets = data.targets || [];
|
||||
|
||||
// Pre-add targets from the cloned preset
|
||||
const clonedTargetIds = (preset.targets || []).map(pt => pt.target_id || pt.id);
|
||||
for (const tid of clonedTargetIds) {
|
||||
const tgt = _allTargets.find(t => t.id === tid);
|
||||
if (!tgt) continue;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'scene-target-item';
|
||||
item.dataset.targetId = tid;
|
||||
item.innerHTML = `<span>${escapeHtml(tgt.name)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||
targetList.appendChild(item);
|
||||
}
|
||||
_refreshTargetSelect();
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
scenePresetModal.open();
|
||||
scenePresetModal.snapshot();
|
||||
}
|
||||
|
||||
// ===== Delete =====
|
||||
|
||||
Reference in New Issue
Block a user