feat(tables): Edit YAML row-context menu item

Opens the row's backing .yaml in the browse tool's YAML editor
(preview-yaml.js — CodeMirror with syntax highlight, lint, Ctrl+S
save). Disabled on multi-row range and unsaved draft rows.

Three URL shapes resolve correctly:
  per-party row → <dir>/?file=<file>.yaml
  SSR virtual   → /<project>/archive/<party>/?file=ssr.yaml
  rollup virtual → /<project>/archive/<party>/<slot>/?file=<file>.yaml

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-19 08:31:17 -05:00
parent f3d334a221
commit 1604b62477
2 changed files with 121 additions and 25 deletions

View file

@ -150,23 +150,71 @@
return { status: 'ok', deleted: okCount, failed: failCount }; return { status: 'ok', deleted: okCount, failed: failCount };
} }
// Map a row's yamlUrl to the browse-tool deep-link that opens the
// file in the YAML editor (preview-yaml.js). Three URL shapes:
//
// /<project>/archive/<party>/<...>/<file>.yaml
// → /<project>/archive/<party>/<...>/?file=<file>.yaml
// (already canonical — just split into dir + file)
//
// /<project>/ssr/<party>.yaml (virtual SSR row)
// → /<project>/archive/<party>/?file=ssr.yaml
//
// /<project>/(mdl|rsk)/<party>__<file>.yaml (virtual rollup row)
// → /<project>/archive/<party>/(mdl|rsk)/?file=<file>.yaml
//
// Returns null when no editable URL is reachable (unsaved row, or
// a yamlUrl shape we don't recognize).
function browseEditUrl(row) {
if (!row || !row.yamlUrl) return null;
const url = row.yamlUrl;
let m = url.match(/^(\/[^/]+)\/ssr\/([^/]+)\.yaml$/);
if (m) return m[1] + '/archive/' + m[2] + '/?file=ssr.yaml';
m = url.match(/^(\/[^/]+)\/(mdl|rsk)\/([^/]+)__(.+)\.yaml$/);
if (m) return m[1] + '/archive/' + m[3] + '/' + m[2] + '/?file=' + m[4] + '.yaml';
const slash = url.lastIndexOf('/');
if (slash < 0) return null;
return url.slice(0, slash + 1) + '?file=' + url.slice(slash + 1);
}
function buildRowMenu(ctx) { function buildRowMenu(ctx) {
const rangeRows = ctx.rangeRowIds || []; const rangeRows = ctx.rangeRowIds || [];
const inRange = rangeRows.length > 1 && rangeRows.indexOf(ctx.rowId) !== -1; const inRange = rangeRows.length > 1 && rangeRows.indexOf(ctx.rowId) !== -1;
const targets = inRange ? rangeRows : [ctx.rowId]; const targets = inRange ? rangeRows : [ctx.rowId];
const label = targets.length > 1 ? 'Delete ' + targets.length + ' rows' : 'Delete row'; const items = [];
return [
{ // Edit YAML — opens the row's backing .yaml file in the browse
label: label, // tool's YAML editor. Disabled on multi-row range and unsaved
icon: '🗑', // draft rows (no file on disk yet).
danger: true, const singleRow = targets.length === 1 ? ctx.row : null;
disabled: !ctx.row || ctx.row.editable === false, const editUrl = singleRow && !singleRow.isNew ? browseEditUrl(singleRow) : null;
action: function () { items.push({
if (targets.length > 1) deleteRows(targets); label: 'Edit YAML',
else deleteRow(targets[0]); icon: '✎',
} disabled: !editUrl,
action: function () {
if (editUrl) window.location.href = editUrl;
} }
]; });
items.push({ separator: true });
const label = targets.length > 1 ? 'Delete ' + targets.length + ' rows' : 'Delete row';
items.push({
label: label,
icon: '🗑',
danger: true,
disabled: !ctx.row || ctx.row.editable === false,
action: function () {
if (targets.length > 1) deleteRows(targets);
else deleteRow(targets[0]);
}
});
return items;
} }
function onRowContext(ev) { function onRowContext(ev) {

View file

@ -1511,7 +1511,7 @@ body.is-elevated::after {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span> <span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-19 13:13:20 · cef7188-dirty</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-19 13:30:29 · f3d334a-dirty</span></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">
@ -5749,23 +5749,71 @@ body.is-elevated::after {
return { status: 'ok', deleted: okCount, failed: failCount }; return { status: 'ok', deleted: okCount, failed: failCount };
} }
// Map a row's yamlUrl to the browse-tool deep-link that opens the
// file in the YAML editor (preview-yaml.js). Three URL shapes:
//
// /<project>/archive/<party>/<...>/<file>.yaml
// → /<project>/archive/<party>/<...>/?file=<file>.yaml
// (already canonical — just split into dir + file)
//
// /<project>/ssr/<party>.yaml (virtual SSR row)
// → /<project>/archive/<party>/?file=ssr.yaml
//
// /<project>/(mdl|rsk)/<party>__<file>.yaml (virtual rollup row)
// → /<project>/archive/<party>/(mdl|rsk)/?file=<file>.yaml
//
// Returns null when no editable URL is reachable (unsaved row, or
// a yamlUrl shape we don't recognize).
function browseEditUrl(row) {
if (!row || !row.yamlUrl) return null;
const url = row.yamlUrl;
let m = url.match(/^(\/[^/]+)\/ssr\/([^/]+)\.yaml$/);
if (m) return m[1] + '/archive/' + m[2] + '/?file=ssr.yaml';
m = url.match(/^(\/[^/]+)\/(mdl|rsk)\/([^/]+)__(.+)\.yaml$/);
if (m) return m[1] + '/archive/' + m[3] + '/' + m[2] + '/?file=' + m[4] + '.yaml';
const slash = url.lastIndexOf('/');
if (slash < 0) return null;
return url.slice(0, slash + 1) + '?file=' + url.slice(slash + 1);
}
function buildRowMenu(ctx) { function buildRowMenu(ctx) {
const rangeRows = ctx.rangeRowIds || []; const rangeRows = ctx.rangeRowIds || [];
const inRange = rangeRows.length > 1 && rangeRows.indexOf(ctx.rowId) !== -1; const inRange = rangeRows.length > 1 && rangeRows.indexOf(ctx.rowId) !== -1;
const targets = inRange ? rangeRows : [ctx.rowId]; const targets = inRange ? rangeRows : [ctx.rowId];
const label = targets.length > 1 ? 'Delete ' + targets.length + ' rows' : 'Delete row'; const items = [];
return [
{ // Edit YAML — opens the row's backing .yaml file in the browse
label: label, // tool's YAML editor. Disabled on multi-row range and unsaved
icon: '🗑', // draft rows (no file on disk yet).
danger: true, const singleRow = targets.length === 1 ? ctx.row : null;
disabled: !ctx.row || ctx.row.editable === false, const editUrl = singleRow && !singleRow.isNew ? browseEditUrl(singleRow) : null;
action: function () { items.push({
if (targets.length > 1) deleteRows(targets); label: 'Edit YAML',
else deleteRow(targets[0]); icon: '✎',
} disabled: !editUrl,
action: function () {
if (editUrl) window.location.href = editUrl;
} }
]; });
items.push({ separator: true });
const label = targets.length > 1 ? 'Delete ' + targets.length + ' rows' : 'Delete row';
items.push({
label: label,
icon: '🗑',
danger: true,
disabled: !ctx.row || ctx.row.editable === false,
action: function () {
if (targets.length > 1) deleteRows(targets);
else deleteRow(targets[0]);
}
});
return items;
} }
function onRowContext(ev) { function onRowContext(ev) {