Let a forecast be renamed
PUT /versions/:id has taken name and description since it existed; only the fallback-name fields ever called it, so the one thing a version's name could not be was changed. A This forecast panel on the Baseline page now saves both on blur. Safe at any time, closed versions included: nothing derives from the name. The forecast table is keyed on the id, as are pf.log, pf.layout and the UI's localStorage. Two edges the route left open. (source_id, name) is unique, so a rename can collide -- it now answers 409 with the same sentence the POST has always used, rather than a raw Postgres message that reads as a server fault. And the UPDATE COALESCEs a falsy name back to the existing one, so a blank box would have looked saved and changed nothing; that is refused where it is typed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b014f7815a
commit
738e177793
16
CLAUDE.md
16
CLAUDE.md
@ -323,6 +323,22 @@ together. `columns` selects which *measures* appear, not individual split
|
||||
combinations.
|
||||
|
||||
|
||||
## Renaming a version
|
||||
|
||||
Safe at any time, including on a closed one. Nothing derives from the name: the
|
||||
forecast table is `fc_{tname}_{version_id}`, `pf.log` and `pf.layout` key on the
|
||||
id, and the UI's `localStorage` keys are id-based. The Baseline page's **This
|
||||
forecast** panel saves `name` and `description` on blur through
|
||||
`PUT /api/versions/:id`, which has accepted both all along — it was only ever
|
||||
called for the fallback names.
|
||||
|
||||
Two edges the panel handles: `(source_id, name)` is unique, so the route
|
||||
answers 409 on a collision rather than a raw Postgres error; and the `UPDATE`
|
||||
COALESCEs a falsy name back to the existing one, so a blank box is refused
|
||||
client-side rather than looking saved and changing nothing.
|
||||
|
||||
---
|
||||
|
||||
## Cloning a version's definition
|
||||
|
||||
`POST /sources/:id/versions` takes `clone_from`. It copies the template
|
||||
|
||||
@ -463,6 +463,13 @@ ${colDefs},
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// (source_id, name) is unique, so a rename can collide. The POST has
|
||||
// said so for as long as it has existed; this said it with a raw
|
||||
// Postgres message, which reads as a server fault rather than a name
|
||||
// that is already taken.
|
||||
if (err.code === '23505') {
|
||||
return res.status(409).json({ error: 'A version with that name already exists for this source' });
|
||||
}
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
@ -529,9 +529,15 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio
|
||||
// The names a row falls back to when nobody has named it. Blank means "use the
|
||||
// built-in", so these save on blur like the segment fields and an empty box is
|
||||
// a meaningful value rather than a missing one.
|
||||
async function saveVersionName(field, value) {
|
||||
async function saveVersionName(field, value, opts = {}) {
|
||||
const next = value.trim()
|
||||
if (next === (selectedVersion?.[field] || '')) return
|
||||
// The route COALESCEs a falsy name back to the existing one, so a blank box
|
||||
// would look saved and change nothing. Say so instead.
|
||||
if (field === 'name' && !next) {
|
||||
flash('A version needs a name', 'error')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/versions/${versionId}`, {
|
||||
method: 'PUT',
|
||||
@ -540,7 +546,10 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio
|
||||
})
|
||||
if (!res.ok) { const d = await res.json(); flash(d.error, 'error'); return }
|
||||
await refreshVersions(sourceId)
|
||||
flash('Saved — reload the Forecast view to see it')
|
||||
// A rename shows everywhere the version is listed and needs no reload; a
|
||||
// fallback name is baked into the aggregated row, so the pivot cannot
|
||||
// re-derive it in place.
|
||||
flash(opts.message || 'Saved — reload the Forecast view to see it')
|
||||
} catch (err) { flash(err.message, 'error') }
|
||||
}
|
||||
|
||||
@ -576,6 +585,36 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Name and description. Nothing depends on the name -- the forecast table
|
||||
is fc_{tname}_{version_id}, pf.log and pf.layout key on the id, and the
|
||||
UI's localStorage keys are id-based -- so it is safe to change at any
|
||||
time, including on a closed version, where correcting a name is still
|
||||
legitimate. Saves on blur, like the fallback names below. */}
|
||||
{versionId && (
|
||||
<div className="bg-white border border-gray-200 rounded p-3 flex items-end gap-3 flex-wrap">
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wide w-full">This forecast</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-gray-500">Name</label>
|
||||
<input
|
||||
key={`name-${versionId}-${selectedVersion?.name || ''}`}
|
||||
defaultValue={selectedVersion?.name || ''}
|
||||
onBlur={e => saveVersionName('name', e.target.value, { message: 'Renamed' })}
|
||||
onKeyDown={e => { if (e.key === 'Enter') e.currentTarget.blur() }}
|
||||
className="border border-gray-200 rounded px-2 py-1 text-sm w-64" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 flex-1 min-w-[16rem]">
|
||||
<label className="text-xs text-gray-500">Description</label>
|
||||
<input
|
||||
key={`desc-${versionId}-${selectedVersion?.description || ''}`}
|
||||
defaultValue={selectedVersion?.description || ''}
|
||||
onBlur={e => saveVersionName('description', e.target.value, { message: 'Saved' })}
|
||||
onKeyDown={e => { if (e.key === 'Enter') e.currentTarget.blur() }}
|
||||
placeholder="optional"
|
||||
className="border border-gray-200 rounded px-2 py-1 text-sm w-full" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fallback names. Not part of any segment -- they are what the pivot shows
|
||||
for rows nobody has named, so they belong to the version rather than to
|
||||
a log entry. Blank falls back to the built-in in sql_generator's
|
||||
|
||||
Loading…
Reference in New Issue
Block a user