feat(ui): pattern modes, bundles, and zone content kind
Add profile/preset/sequence JSON import and export; map preset mode to wire n6 with a mode dropdown for multi-mode patterns; zone edit shows presets or sequences only with content_kind on save; update catalogue and tests for merged pattern names. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -454,11 +454,10 @@ async function addSequenceToTab(sequenceId, zoneId) {
|
||||
const tabResponse = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } });
|
||||
if (!tabResponse.ok) throw new Error('Failed to load zone');
|
||||
const tabData = await tabResponse.json();
|
||||
const kind =
|
||||
typeof window.normalizeZoneContentKind === 'function'
|
||||
? window.normalizeZoneContentKind(tabData)
|
||||
: null;
|
||||
if (kind === 'presets') {
|
||||
if (
|
||||
typeof window.zoneAllowsSequences === 'function' &&
|
||||
!window.zoneAllowsSequences(tabData)
|
||||
) {
|
||||
alert('This zone is for presets only. Add presets from the zone Edit menu instead.');
|
||||
return;
|
||||
}
|
||||
@@ -524,11 +523,10 @@ async function refreshEditTabSequencesUi(zoneId) {
|
||||
const zoneRes = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } });
|
||||
if (!zoneRes.ok) throw new Error('zone');
|
||||
const zone = await zoneRes.json();
|
||||
const kind =
|
||||
typeof window.normalizeZoneContentKind === 'function'
|
||||
? window.normalizeZoneContentKind(zone)
|
||||
: null;
|
||||
if (kind === 'presets') {
|
||||
if (
|
||||
typeof window.zoneAllowsSequences === 'function' &&
|
||||
!window.zoneAllowsSequences(zone)
|
||||
) {
|
||||
currentEl.innerHTML =
|
||||
'<span class="muted-text">This zone is for presets only. Sequences are hidden.</span>';
|
||||
addEl.innerHTML = '<span class="muted-text">—</span>';
|
||||
@@ -1092,12 +1090,31 @@ async function loadSequencesModalList() {
|
||||
const nSteps = ln.reduce((a, l) => a + l.length, 0);
|
||||
const nLanes = ln.filter((l) => l.length > 0).length || 1;
|
||||
title.textContent = `${doc.name || id} — ${nLanes} lane(s), ${nSteps} step(s)`;
|
||||
const exportBtn = document.createElement('button');
|
||||
exportBtn.type = 'button';
|
||||
exportBtn.className = 'btn btn-secondary btn-small';
|
||||
exportBtn.textContent = 'Export';
|
||||
exportBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
const response = await fetch(`/sequences/${id}/export`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) throw new Error('Export failed');
|
||||
const bundle = await response.json();
|
||||
const safeName = String(doc.name || id).replace(/[^\w.-]+/g, '_');
|
||||
window.downloadJsonFile(`sequence-${safeName}.json`, bundle);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Failed to export sequence.');
|
||||
}
|
||||
});
|
||||
const edit = document.createElement('button');
|
||||
edit.type = 'button';
|
||||
edit.className = 'btn btn-secondary btn-small';
|
||||
edit.textContent = 'Edit';
|
||||
edit.addEventListener('click', () => openSequenceEditor(id, doc));
|
||||
row.appendChild(title);
|
||||
row.appendChild(exportBtn);
|
||||
row.appendChild(edit);
|
||||
listEl.appendChild(row);
|
||||
});
|
||||
@@ -1139,6 +1156,33 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
openSequenceEditor(null, null);
|
||||
});
|
||||
}
|
||||
const importSeqBtn = document.getElementById('import-sequence-btn');
|
||||
if (importSeqBtn) {
|
||||
importSeqBtn.addEventListener('click', async () => {
|
||||
const text = await window.pickJsonFile();
|
||||
if (!text) return;
|
||||
const bundle = window.parseJsonFileText(text);
|
||||
if (!bundle || bundle.kind !== 'sequence') {
|
||||
alert('Invalid sequence bundle file.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/sequences/import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ bundle }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Import failed');
|
||||
}
|
||||
await loadSequencesModalList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(e.message || 'Failed to import sequence.');
|
||||
}
|
||||
});
|
||||
}
|
||||
const openPresetsFromSeq = document.getElementById('sequences-open-presets-btn');
|
||||
if (openPresetsFromSeq) {
|
||||
openPresetsFromSeq.addEventListener('click', () => {
|
||||
|
||||
Reference in New Issue
Block a user