tabs: extract scheduleField() helper — collapse 8 label/caption sites

Every field on the new- and edit-schedule forms (plus buildIntervalComposer
and buildTargetChips) builds the same wrapper shape:

    el('label', { class: 'schedule-field' },
      el('span', { class: 'schedule-field-label' }, '…'))

then appends an input. Eight identical-modulo-text sites. Extracting one
small helper:

    function scheduleField(labelText, ...children) {
      return el('label', { class: 'schedule-field' },
        el('span', { class: 'schedule-field-label' }, labelText),
        ...children);
    }

drops the per-site cost to a single `form_.append(scheduleField('caption',
input));` line. -10 LOC net; no behaviour change.
This commit is contained in:
iris 2026-05-28 12:36:01 +02:00 committed by Mara
commit bc1c910044

View file

@ -2004,6 +2004,18 @@ window.marked = marked;
}
return n;
}
// Label + caption wrapper shared by every field on the schedule
// forms — `<label class="schedule-field"><span
// class="schedule-field-label">…</span>…children…</label>`. Variadic
// children land inside the label after the caption span, which is
// what every caller wants (the input + any preset/parts/preview
// sub-rows live next to the caption).
function scheduleField(labelText, ...children) {
return el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, labelText),
...children);
}
// Interval composer (#466) — shared between the new-schedule form
// and the edit-schedule form (#474). Builds the preset chip row +
// d/h/m/s sub-fields + live preview, and returns helpers to read
@ -2017,8 +2029,7 @@ window.marked = marked;
namePrefix = 'interval_',
initialSeconds = 0,
} = {}) {
const wrapper = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, label));
const wrapper = scheduleField(label);
const presets = [
['1m', 60], ['5m', 300], ['15m', 900], ['30m', 1800],
['1h', 3600], ['6h', 21600], ['12h', 43200],
@ -2142,8 +2153,6 @@ window.marked = marked;
// explicitly uncheckable. New-schedule callers pass `[]`.
for (const n of extraNames) if (!candidates.includes(n)) candidates.push(n);
const label = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'targets'));
const box = el('div', { class: 'schedule-targets' });
for (const name of candidates) {
const id_ = idPrefix + name;
@ -2154,8 +2163,7 @@ window.marked = marked;
box.append(el('label', { class: 'schedule-target-chip', for: id_ },
cb, el('span', {}, name)));
}
label.append(box);
return label;
return scheduleField('targets', box);
}
function renderScheduleNewForm() {
@ -2181,17 +2189,12 @@ window.marked = marked;
checked: new Set(carry.targets),
}));
const bodyLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'prompt body'));
const bodyInput = el('textarea', {
name: 'body', rows: '4', required: 'required', placeholder: 'message delivered to each target at fire time',
});
bodyInput.value = carry.body;
bodyLabel.append(bodyInput);
form_.append(bodyLabel);
form_.append(scheduleField('prompt body', bodyInput));
const firstFireLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'first fire'));
const firstFireInput = el('input', {
type: 'datetime-local', name: 'first_fire', required: 'required',
});
@ -2200,8 +2203,7 @@ window.marked = marked;
// immediately on a stale-clock accident).
firstFireInput.value = carry.first_fire
|| isoForDatetimeLocal(new Date(Date.now() + 5 * 60 * 1000));
firstFireLabel.append(firstFireInput);
form_.append(firstFireLabel);
form_.append(scheduleField('first fire', firstFireInput));
// Interval composer (#466). See `buildIntervalComposer` below —
// shared between the new-schedule form and the edit-schedule form
@ -2227,14 +2229,11 @@ window.marked = marked;
}
intervalCx.updatePreview();
const descLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'description (optional)'));
const descInput = el('input', {
type: 'text', name: 'description', placeholder: 'shown on the schedule card',
});
descInput.value = carry.description;
descLabel.append(descInput);
form_.append(descLabel);
form_.append(scheduleField('description (optional)', descInput));
const actions = el('div', { class: 'schedule-actions' });
const submit = el('button', { type: 'submit', class: 'btn btn-spawn' }, ' qu3ue prompt');
@ -2470,32 +2469,23 @@ window.marked = marked;
const carry = scheduleEditCarry.get(s.id) || {};
const bodyLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'body'));
const bodyInput = el('textarea', { name: 'body', rows: '4', required: 'required' });
bodyInput.value = carry.body !== undefined ? carry.body : s.body;
bodyLabel.append(bodyInput);
form_.append(bodyLabel);
form_.append(scheduleField('body', bodyInput));
const descLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'description (blank to clear)'));
const descInput = el('input', { type: 'text', name: 'description' });
descInput.value = carry.description !== undefined
? carry.description
: (s.description || '');
descLabel.append(descInput);
form_.append(descLabel);
form_.append(scheduleField('description (blank to clear)', descInput));
const firstFireLabel = el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, 'next fire'));
const firstFireInput = el('input', {
type: 'datetime-local', name: 'next_fire', required: 'required',
});
firstFireInput.value = carry.next_fire !== undefined
? carry.next_fire
: isoForDatetimeLocal(new Date(s.next_fire_at_unix * 1000));
firstFireLabel.append(firstFireInput);
form_.append(firstFireLabel);
form_.append(scheduleField('next fire', firstFireInput));
const intervalCx = buildIntervalComposer({
label: 'interval (blank / all-zero = flip to one-shot)',