diff --git a/routes/log.js b/routes/log.js index 130dd41..917f4ff 100644 --- a/routes/log.js +++ b/routes/log.js @@ -131,9 +131,12 @@ module.exports = function(pool) { // a closed version, where relabelling history is still legitimate. router.patch('/log/:logid', async (req, res) => { const logId = parseInt(req.params.logid); - const { note, tag, bucket, seq } = req.body; - if (note === undefined && tag === undefined && bucket === undefined && seq === undefined) { - return res.status(400).json({ error: 'Nothing to update — send note, tag, bucket and/or seq' }); + const { note, tag, bucket, seq, label } = req.body; + if (note === undefined && tag === undefined && bucket === undefined + && seq === undefined && label === undefined) { + return res.status(400).json({ + error: 'Nothing to update — send note, tag, bucket, label and/or seq' + }); } try { // COALESCE on the flag, not the value: an explicit null or '' must be @@ -143,7 +146,8 @@ module.exports = function(pool) { note = CASE WHEN $2::bool THEN $3::text ELSE note END, tag = CASE WHEN $4::bool THEN $5::text ELSE tag END, bucket = CASE WHEN $6::bool THEN $7::text ELSE bucket END, - seq = CASE WHEN $8::bool THEN $9::int ELSE seq END + seq = CASE WHEN $8::bool THEN $9::int ELSE seq END, + label = CASE WHEN $10::bool THEN $11::text ELSE label END WHERE id = $1 RETURNING *`, [ logId, @@ -152,6 +156,7 @@ module.exports = function(pool) { bucket !== undefined, bucket === undefined ? null : (String(bucket).trim() || null), seq !== undefined, (seq === undefined || seq === null || seq === '') ? null : parseInt(seq), + label !== undefined, label === undefined ? null : (String(label).trim() || null), ] ); if (!result.rows.length) return res.status(404).json({ error: 'Log entry not found' }); diff --git a/setup_sql/01_schema.sql b/setup_sql/01_schema.sql index a2e7b9c..cd66384 100644 --- a/setup_sql/01_schema.sql +++ b/setup_sql/01_schema.sql @@ -92,6 +92,15 @@ WHERE TRUE -- bucket_order lives on the version rather than the source because the Baseline -- page, where it is maintained, is version-scoped. log.seq orders the segments -- within that. +-- The segment's display name in the pivot, falling back to tag then note. +-- +-- Separate from both because those have jobs already -- tag groups adjustments +-- into initiatives for the bridge, note is free commentary -- and because the +-- label carries the sort order. Perspective orders column groups by the value +-- string, so a leading "01 - " is how ordering is expressed; putting that in the +-- note would put it in every note. +ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS label text; + ALTER TABLE pf.version ADD COLUMN IF NOT EXISTS bucket_order jsonb; ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS seq integer; diff --git a/ui/src/views/Baseline.jsx b/ui/src/views/Baseline.jsx index a41b18e..d193697 100644 --- a/ui/src/views/Baseline.jsx +++ b/ui/src/views/Baseline.jsx @@ -103,6 +103,10 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio const [rawSql, setRawSql] = useState('') const [offset, setOffset] = useState('0 days') const [segNote, setSegNote] = useState('') + // Presentation, not definition: what the segment counts toward and how it is + // labelled in the pivot. Safe to set at any time, unlike its filters. + const [segBucket, setSegBucket] = useState('') + const [segLabel, setSegLabel] = useState('') const [submitting, setSubmitting] = useState(false) const [editingLogId, setEditingLogId] = useState(null) const [showAddForm, setShowAddForm] = useState(false) @@ -140,6 +144,25 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio const [seqs, setSeqs] = useState({}) const [bucketOrder, setBucketOrder] = useState([]) + // Label and bucket are presentation, not definition: they change what the pivot + // shows and what the segment counts toward, never which rows were loaded. So + // they stay editable after adjustments exist, unlike the filters and offset, + // where an edit would silently recalibrate scales sized against the old rows. + async function saveLogField(entry, field, value) { + const next = value.trim() + if (next === (entry[field] || '')) return + try { + const res = await fetch(`/api/log/${entry.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ [field]: next }), + }) + if (!res.ok) { const d = await res.json(); flash(d.error, 'error'); return } + loadLog() + flash('Saved') + } catch (err) { flash(err.message, 'error') } + } + async function saveBucket(entry, value) { const next = value.trim() if (next === (entry.bucket || '')) return @@ -236,6 +259,8 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio where_clause: clause, note: description || segNote, date_offset: offsetStr, + ...(segBucket.trim() ? { bucket: segBucket.trim() } : {}), + ...(segLabel.trim() ? { label: segLabel.trim() } : {}), ...(useRaw ? { raw_where: clause } : { filters }), } setSubmitting(true) @@ -462,6 +487,7 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio