Add recode/clone dim_group sibling auto-lookup
- GET /api/sources/:id/lookup?col=X&value=Y — given a key column value, queries the source table for sibling column values in the same dim_group; returns null if no match or ambiguous - Recode and Clone panels: key columns (is_key + dim_group) trigger lookup on blur and auto-fill sibling inputs that the user hasn't already typed into - Row labels now use col_meta label field when set, falling back to cname Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2a9a3be0f0
commit
cf9bdea9a8
@ -224,6 +224,41 @@ module.exports = function(pool) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// given a key column value, look up sibling dim_group column values from source
|
||||||
|
// returns { sibling_col: value, ... } if exactly one match, null if none or ambiguous
|
||||||
|
router.get('/sources/:id/lookup', async (req, res) => {
|
||||||
|
const { col, value } = req.query;
|
||||||
|
if (!col || value == null || value === '') return res.json(null);
|
||||||
|
try {
|
||||||
|
const [srcResult, metaResult] = await Promise.all([
|
||||||
|
pool.query(`SELECT schema, tname FROM pf.source WHERE id = $1`, [req.params.id]),
|
||||||
|
pool.query(`SELECT * FROM pf.col_meta WHERE source_id = $1 ORDER BY opos`, [req.params.id])
|
||||||
|
]);
|
||||||
|
if (srcResult.rows.length === 0) return res.status(404).json({ error: 'Source not found' });
|
||||||
|
|
||||||
|
const keyCol = metaResult.rows.find(c => c.cname === col && c.is_key && c.dim_group);
|
||||||
|
if (!keyCol) return res.json(null);
|
||||||
|
|
||||||
|
const siblings = metaResult.rows.filter(c =>
|
||||||
|
c.dim_group === keyCol.dim_group && c.cname !== col
|
||||||
|
);
|
||||||
|
if (!siblings.length) return res.json(null);
|
||||||
|
|
||||||
|
const { schema, tname } = srcResult.rows[0];
|
||||||
|
const sibCols = siblings.map(c => `"${c.cname}"`).join(', ');
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT DISTINCT ${sibCols} FROM "${schema}"."${tname}" WHERE "${col}" = $1 LIMIT 2`,
|
||||||
|
[value]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length !== 1) return res.json(null);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// set or clear the default Perspective layout for a source.
|
// set or clear the default Perspective layout for a source.
|
||||||
// Body: a Perspective view config (group_by, split_by, columns, plugin_config, …).
|
// Body: a Perspective view config (group_by, split_by, columns, plugin_config, …).
|
||||||
// Pass null or {} to clear.
|
// Pass null or {} to clear.
|
||||||
|
|||||||
@ -436,6 +436,21 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou
|
|||||||
} catch (err) { flash(err.message, 'error') }
|
} catch (err) { flash(err.message, 'error') }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function lookupDerivedCols(col, value, setter) {
|
||||||
|
if (!sourceId || !value.trim()) return
|
||||||
|
const res = await fetch(`/api/sources/${sourceId}/lookup?col=${encodeURIComponent(col)}&value=${encodeURIComponent(value)}`)
|
||||||
|
if (!res.ok) return
|
||||||
|
const derived = await res.json()
|
||||||
|
if (!derived) return
|
||||||
|
setter(prev => {
|
||||||
|
const next = { ...prev }
|
||||||
|
for (const [k, v] of Object.entries(derived)) {
|
||||||
|
if (!prev[k] || prev[k] === '') next[k] = String(v ?? '')
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function buildEffectiveSlice(raw) {
|
function buildEffectiveSlice(raw) {
|
||||||
const dimCols = new Set(colMetaRef.current.filter(c => c.role === 'dimension').map(c => c.cname))
|
const dimCols = new Set(colMetaRef.current.filter(c => c.role === 'dimension').map(c => c.cname))
|
||||||
const dateCols = new Set(colMetaRef.current.filter(c => c.role === 'date').map(c => c.cname))
|
const dateCols = new Set(colMetaRef.current.filter(c => c.role === 'date').map(c => c.cname))
|
||||||
@ -842,9 +857,15 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou
|
|||||||
{activeOp === 'recode' && <>
|
{activeOp === 'recode' && <>
|
||||||
<p className="text-gray-400">New values for dimensions to replace. Leave blank to keep.</p>
|
<p className="text-gray-400">New values for dimensions to replace. Leave blank to keep.</p>
|
||||||
{dimCols.map(c => (
|
{dimCols.map(c => (
|
||||||
<Row key={c.cname} label={c.cname}>
|
<Row key={c.cname} label={c.label || c.cname}>
|
||||||
<input value={recodeSet[c.cname] || ''} onChange={e => setRecodeSet(s => ({ ...s, [c.cname]: e.target.value }))}
|
<input
|
||||||
placeholder={slice[c.cname] || '—'} className={`${inp} font-mono`} />
|
value={recodeSet[c.cname] || ''}
|
||||||
|
onChange={e => setRecodeSet(s => ({ ...s, [c.cname]: e.target.value }))}
|
||||||
|
onBlur={c.is_key && c.dim_group
|
||||||
|
? e => lookupDerivedCols(c.cname, e.target.value, setRecodeSet)
|
||||||
|
: undefined}
|
||||||
|
placeholder={slice[c.cname] || '—'}
|
||||||
|
className={`${inp} font-mono`} />
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
<Row label="Note"><input value={recodeNote} onChange={e => setRecodeNote(e.target.value)} placeholder="optional" className={inp} /></Row>
|
<Row label="Note"><input value={recodeNote} onChange={e => setRecodeNote(e.target.value)} placeholder="optional" className={inp} /></Row>
|
||||||
@ -855,9 +876,15 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou
|
|||||||
{activeOp === 'clone' && <>
|
{activeOp === 'clone' && <>
|
||||||
<p className="text-gray-400">Override dimensions on cloned rows. Leave blank to keep.</p>
|
<p className="text-gray-400">Override dimensions on cloned rows. Leave blank to keep.</p>
|
||||||
{dimCols.map(c => (
|
{dimCols.map(c => (
|
||||||
<Row key={c.cname} label={c.cname}>
|
<Row key={c.cname} label={c.label || c.cname}>
|
||||||
<input value={cloneSet[c.cname] || ''} onChange={e => setCloneSet(s => ({ ...s, [c.cname]: e.target.value }))}
|
<input
|
||||||
placeholder={slice[c.cname] || '—'} className={`${inp} font-mono`} />
|
value={cloneSet[c.cname] || ''}
|
||||||
|
onChange={e => setCloneSet(s => ({ ...s, [c.cname]: e.target.value }))}
|
||||||
|
onBlur={c.is_key && c.dim_group
|
||||||
|
? e => lookupDerivedCols(c.cname, e.target.value, setCloneSet)
|
||||||
|
: undefined}
|
||||||
|
placeholder={slice[c.cname] || '—'}
|
||||||
|
className={`${inp} font-mono`} />
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
<Row label="Scale"><input type="number" step="any" value={cloneScale} onChange={e => setCloneScale(e.target.value)} className={inp} /></Row>
|
<Row label="Scale"><input type="number" step="any" value={cloneScale} onChange={e => setCloneScale(e.target.value)} className={inp} /></Row>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user