Article export + Bootstrap toggle + export change log

- patchScore() now accepts articles[] and returns { text, log } instead
  of a plain string. Dirty articles are re-serialized into the articles:
  block (block style, replacing any existing flow-style entries). Clean
  articles bypass the patcher entirely.
- article.isDirty is set on direct property edits so the exporter can
  distinguish directly changed entities from propagated dirty state.
- patchScore() log collects { level:'changed', path } for each patched
  article/instrument/bar, and a { level:'info', message } entry for the
  count of bar documents passed through unchanged.
- CP pane shows the export log above the status poller after each export.
- Article scope toggle replaced with Bootstrap form-check form-switch;
  hand-rolled .se-toggle CSS removed.
- Test suite updated for { text, log } return; 11 new assertions (188 total).
This commit is contained in:
c0dev0id
2026-06-30 06:27:12 +02:00
parent 5bec1a1d1e
commit 86b14dfaa3
6 changed files with 132 additions and 60 deletions

View File

@@ -67,7 +67,11 @@ export const PaneCP = {
try {
const raw = await fetchScoreText(props.store.credentials);
props.store.rawScoreText = raw;
const patched = patchScore(raw, props.store.scoreModel.instruments, props.store.scoreModel.bars, props.store.scoreModel.info);
const model = props.store.scoreModel;
const { text: patched, log } = patchScore(
raw, model.instruments, model.bars, model.info, model.articles ?? [],
);
props.store.exportLog = log;
await putScoreText(patched, props.store.credentials);
props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 };
} catch (e) {
@@ -134,6 +138,18 @@ export const PaneCP = {
// Export error
exportError.value ? h('div', { class: 'se-error' }, exportError.value) : null,
// Export log (shown after export, cleared on next import)
store.exportLog?.length
? h('div', { class: 'se-export-log' },
store.exportLog.map((entry, i) =>
h('div', {
key: i,
class: entry.level === 'changed' ? 'se-export-changed' : 'se-export-info',
}, entry.path ? `${entry.path}` : entry.message)
)
)
: null,
// Status poller (while running)
store.synthesisStatus && !store.synthesisStatus.frozen
? h(StatusPoller, { store }) : null,

View File

@@ -155,7 +155,7 @@ export const PaneFO = {
: null,
);
} else if (node.type === 'article') {
const markDirty = () => props.store.markDirty();
const markDirty = () => { node.isDirty = true; props.store.markDirty(); };
const rowStyle = 'display:flex;gap:0.4rem;align-items:center;padding:0.2rem 0';
children.push(
h('h4', H4, `Article: ${node.name}`),
@@ -180,14 +180,20 @@ export const PaneFO = {
style: 'flex:1;min-width:6em',
onInput: onValInput,
}),
h('button', {
class: 'se-toggle',
role: 'switch',
'aria-checked': String(isOverwrite),
onClick: flip,
}),
h('span', { class: 'se-toggle-label active', style: 'min-width:4.5em' },
isOverwrite ? 'overwrite' : 'default'),
h('div', { class: 'form-check form-switch mb-0' }, [
h('input', {
class: 'form-check-input mt-0',
type: 'checkbox',
role: 'switch',
id: `prop-scope-${idx}`,
checked: isOverwrite,
onChange: flip,
}),
h('label', {
class: 'form-check-label',
for: `prop-scope-${idx}`,
}, isOverwrite ? 'overwrite' : 'default'),
]),
h('button', {
class: 'se-btn-remove',
title: 'Remove property',