// add-row.js — new-row creation. // // Two paths, chosen by the table's schema: // - Record-tables (identity composed from required fields that aren't // columns — e.g. the risk register's tracking-number components): "+ Add // row" navigates to the compose form (/form.html), the only place // those components can be supplied. See needsComposeForm(). // - Simple tables (all required fields are columns): "+ Add row" appends a // draft row at the end of state.rows, focuses its first editable cell, and // accumulates typing into the drafts buffer like any other row. On // row-blur, save.js detects row.isNew and POSTs to /form.html; the // 201 Location swaps the synthetic url and the draft becomes a saved row. // // Synthetic identity: each new row gets a temporary "__new-" url // so rowKey() returns something unique for selection + draft tracking. // The temporary url is replaced after a successful POST. There is no // "save on click" UX — the existing row-blur trigger is the save path, // same as for edits. (function (app) { 'use strict'; let _counter = 0; function makeSyntheticKey() { _counter += 1; return '__new-' + _counter; } // Compute the form-create URL for the current page. Both // //table.html and // (default_tool: tables) shape work; // //form.html is the form handler's "create" endpoint either // way (the form handler keys off the in-dir convention, not the // visiting URL shape). function formCreateUrl() { let dir = (location.pathname || '/').replace(/\/table\.html$/, '/'); if (!dir.endsWith('/')) dir += '/'; return dir + 'form.html'; } // True when a new record's identity is composed from required fields that // AREN'T table columns (e.g. the risk register's project/discipline/ // sequence tracking-number components). Such rows can't be created by // typing into the grid — they need the compose form. Server mode only // (the form handler is server-side). function needsComposeForm() { const ctx = app.context || {}; if (!app.state || app.state.source !== 'server') return false; const req = (ctx.rowSchema && Array.isArray(ctx.rowSchema.required)) ? ctx.rowSchema.required : []; if (!req.length) return false; const colFields = {}; (ctx.columns || []).forEach(function (c) { if (c && c.field) colFields[c.field] = true; }); return req.some(function (f) { return !colFields[f]; }); } // Create-and-paint: the user-facing path. Record-tables (composed identity) // open the compose form directly — the grid can't supply their // tracking-number components; simple tables append an inline draft row. function invoke() { if (needsComposeForm()) { window.location.href = formCreateUrl(); return; } const key = createSilent(); if (typeof app.repaint === 'function') app.repaint(); focusNewRow(key); } // Push a draft row WITHOUT painting or focusing. Used by multi-row // paste (clipboard.js) to create N rows in a single batch, with one // paint at the end. Returns the synthetic url so callers can address // the new row in their draft writes. function createSilent() { const key = makeSyntheticKey(); const draftRow = { url: key, yamlUrl: null, data: {}, etag: null, editable: true, isNew: true, }; if (!Array.isArray(app.state.rows)) { app.state.rows = []; } app.state.rows.push(draftRow); return key; } function focusNewRow(key) { // After repaint, find the tr with our synthetic data-row-id and // tell the editor to select its first cell. Filtering may have // hidden the new row if a default filter excludes it; we accept // that — clearing filters surfaces it. const tbody = document.querySelector('#table-root tbody'); if (!tbody) return; const trs = tbody.querySelectorAll('tr'); for (let i = 0; i < trs.length; i++) { if (trs[i].getAttribute('data-row-id') === key) { const editor = app.modules.editor; if (editor && typeof editor.setSelected === 'function') { // Scroll into view so the user sees the new row. trs[i].scrollIntoView({ block: 'nearest', behavior: 'auto' }); editor.setSelected(i, 0); } return; } } } // Cancel-new-row helper: drop the synthetic row entirely. Used when // the user adds a row, makes no edits, and clicks Add again or // navigates away — there's nothing to save and an empty draft just // clutters the table. The save module calls this from row-blur when // it sees a new row with no drafts. function discardEmpty(rowId) { const rows = app.state.rows || []; for (let i = 0; i < rows.length; i++) { if (rows[i].isNew && rows[i].url === rowId) { rows.splice(i, 1); if (typeof app.repaint === 'function') app.repaint(); return true; } } return false; } app.modules.addRow = { invoke: invoke, createSilent: createSilent, formCreateUrl: formCreateUrl, discardEmpty: discardEmpty, }; })(window.tablesApp);