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>
249 lines
9.3 KiB
JavaScript
249 lines
9.3 KiB
JavaScript
// row-ops.js — row-level operations (delete, future: duplicate,
|
|
// copy-to-table, etc.). Surfaced via a right-click context menu on
|
|
// table rows; the editor's selection state determines which row the
|
|
// action targets when the menu is invoked from the keyboard or from a
|
|
// future toolbar button.
|
|
//
|
|
// The shared context-menu primitive (window.zddc.menu) drives the
|
|
// rendering and keyboard behaviour. This module owns the menu spec
|
|
// and the action handlers.
|
|
(function (app) {
|
|
'use strict';
|
|
|
|
function findRowById(rowId) {
|
|
const all = (app.state && app.state.rows) || [];
|
|
for (let i = 0; i < all.length; i++) {
|
|
const editor = app.modules.editor;
|
|
const key = editor ? editor.rowKey(all[i]) : (all[i].url || '');
|
|
if (key === rowId) return all[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function removeRowFromState(row) {
|
|
const all = app.state.rows || [];
|
|
const idx = all.indexOf(row);
|
|
if (idx >= 0) all.splice(idx, 1);
|
|
// Drop any drafts keyed on the row's url.
|
|
if (app.state.drafts && row.url) {
|
|
delete app.state.drafts[row.url];
|
|
}
|
|
}
|
|
|
|
function rowDisplayName(row) {
|
|
if (!row) return '(unknown)';
|
|
if (row.isNew) return '(unsaved new row)';
|
|
if (row.yamlUrl) {
|
|
const m = row.yamlUrl.match(/[^/]+$/);
|
|
if (m) return m[0];
|
|
}
|
|
return row.url || '(row)';
|
|
}
|
|
|
|
async function deleteRow(rowId) {
|
|
const row = findRowById(rowId);
|
|
if (!row) return { status: 'noop' };
|
|
if (row.editable === false) return { status: 'readonly' };
|
|
|
|
// Unsaved new row: just drop it. Nothing to call.
|
|
if (row.isNew) {
|
|
removeRowFromState(row);
|
|
if (typeof app.repaint === 'function') app.repaint();
|
|
return { status: 'ok-local' };
|
|
}
|
|
|
|
if (!row.yamlUrl) {
|
|
// file:// or fixture context — nothing to delete server-side.
|
|
removeRowFromState(row);
|
|
if (typeof app.repaint === 'function') app.repaint();
|
|
return { status: 'ok-local' };
|
|
}
|
|
|
|
const ok = window.confirm('Delete row "' + rowDisplayName(row) + '"?\n\nThis cannot be undone.');
|
|
if (!ok) return { status: 'cancelled' };
|
|
|
|
const headers = {};
|
|
if (row.etag) headers['If-Match'] = '"' + row.etag + '"';
|
|
let resp;
|
|
try {
|
|
resp = await fetch(row.yamlUrl, {
|
|
method: 'DELETE',
|
|
headers: headers,
|
|
credentials: 'same-origin'
|
|
});
|
|
} catch (err) {
|
|
window.alert('Delete failed: ' + (err && err.message ? err.message : err));
|
|
return { status: 'network-error', error: err };
|
|
}
|
|
if (resp.status === 200 || resp.status === 204) {
|
|
removeRowFromState(row);
|
|
if (typeof app.repaint === 'function') app.repaint();
|
|
return { status: 'ok' };
|
|
}
|
|
if (resp.status === 412) {
|
|
window.alert('Cannot delete: this row was changed since you loaded it. Reload to see the latest version.');
|
|
return { status: 'conflict' };
|
|
}
|
|
let body = '';
|
|
try { body = await resp.text(); } catch (_) { /* ignore */ }
|
|
window.alert('Delete failed (' + resp.status + '): ' + body);
|
|
return { status: 'http-error', code: resp.status };
|
|
}
|
|
|
|
// Returns the list of visible-row indices currently included in
|
|
// the editor's range selection. Empty when no range is active.
|
|
function rangeRowIndices() {
|
|
const range = app.state && app.state.range;
|
|
if (!range) return [];
|
|
const r0 = Math.min(range.anchor.row, range.focus.row);
|
|
const r1 = Math.max(range.anchor.row, range.focus.row);
|
|
const out = [];
|
|
for (let r = r0; r <= r1; r++) out.push(r);
|
|
return out;
|
|
}
|
|
|
|
// Map a visible-row index to its data-row-id (synthetic or real).
|
|
function rowIdAtIndex(idx) {
|
|
const trs = document.querySelectorAll('#table-root tbody > tr');
|
|
const tr = trs[idx];
|
|
return tr ? tr.getAttribute('data-row-id') : null;
|
|
}
|
|
|
|
async function deleteRows(rowIds) {
|
|
if (!rowIds || rowIds.length === 0) return { status: 'noop' };
|
|
if (rowIds.length === 1) return deleteRow(rowIds[0]);
|
|
const ok = window.confirm('Delete ' + rowIds.length + ' rows?\n\nThis cannot be undone.');
|
|
if (!ok) return { status: 'cancelled' };
|
|
// Walk back-to-front so removing by index from state.rows
|
|
// doesn't shift the indices of pending deletes.
|
|
let okCount = 0, failCount = 0;
|
|
for (let i = rowIds.length - 1; i >= 0; i--) {
|
|
const row = findRowById(rowIds[i]);
|
|
if (!row) continue;
|
|
if (row.isNew || !row.yamlUrl) {
|
|
removeRowFromState(row);
|
|
okCount++;
|
|
continue;
|
|
}
|
|
const headers = {};
|
|
if (row.etag) headers['If-Match'] = '"' + row.etag + '"';
|
|
try {
|
|
const resp = await fetch(row.yamlUrl, {
|
|
method: 'DELETE',
|
|
headers: headers,
|
|
credentials: 'same-origin'
|
|
});
|
|
if (resp.status === 200 || resp.status === 204) {
|
|
removeRowFromState(row);
|
|
okCount++;
|
|
} else {
|
|
failCount++;
|
|
}
|
|
} catch (_err) {
|
|
failCount++;
|
|
}
|
|
}
|
|
if (typeof app.repaint === 'function') app.repaint();
|
|
if (failCount > 0) {
|
|
window.alert('Deleted ' + okCount + ' row(s); ' + failCount + ' failed.');
|
|
}
|
|
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) {
|
|
const rangeRows = ctx.rangeRowIds || [];
|
|
const inRange = rangeRows.length > 1 && rangeRows.indexOf(ctx.rowId) !== -1;
|
|
const targets = inRange ? rangeRows : [ctx.rowId];
|
|
const items = [];
|
|
|
|
// Edit YAML — opens the row's backing .yaml file in the browse
|
|
// tool's YAML editor. Disabled on multi-row range and unsaved
|
|
// draft rows (no file on disk yet).
|
|
const singleRow = targets.length === 1 ? ctx.row : null;
|
|
const editUrl = singleRow && !singleRow.isNew ? browseEditUrl(singleRow) : null;
|
|
items.push({
|
|
label: 'Edit YAML',
|
|
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) {
|
|
const tr = ev.target.closest('tr[data-row-id]');
|
|
if (!tr) return;
|
|
const rowId = tr.getAttribute('data-row-id');
|
|
const row = findRowById(rowId);
|
|
if (!row) return;
|
|
ev.preventDefault();
|
|
const menu = window.zddc && window.zddc.menu;
|
|
if (!menu || typeof menu.open !== 'function') return;
|
|
const rangeRowIds = rangeRowIndices().map(rowIdAtIndex).filter(Boolean);
|
|
menu.open({
|
|
x: ev.clientX,
|
|
y: ev.clientY,
|
|
items: buildRowMenu({ row: row, rowId: rowId, rangeRowIds: rangeRowIds }),
|
|
context: { row: row, rowId: rowId, rangeRowIds: rangeRowIds }
|
|
});
|
|
}
|
|
|
|
function attach() {
|
|
const tbody = document.querySelector('#table-root tbody');
|
|
if (!tbody) return;
|
|
tbody.addEventListener('contextmenu', onRowContext);
|
|
}
|
|
|
|
app.modules.rowOps = {
|
|
attach: attach,
|
|
deleteRow: deleteRow,
|
|
deleteRows: deleteRows,
|
|
};
|
|
})(window.tablesApp);
|