/* The project-list import component — T5.4's machinery, extracted (D6 / T8.6). One implementation of paste-or-file → server-side import with dry-run → a report that names every rejected row with its SOURCE line number → an editable list whose entries deactivate rather than delete. The location list (CR-005) and the material list (D6) are both instances of this; building the material path "the same way and against the same component, not beside it" is the T8.6 instruction, and extracting the component is what makes that literally true rather than a copy with the names changed. The page supplies what differs: the API base, the sample, how a row renders, and what the add-row collects. Everything generic — the file reader feeding the paste box (one parser, on the server), the dry-run wiring, the report roles (a report that lost rows interrupts; a clean one does not, per T4.5) — lives here once. Classic script, no modules: exposes window.WPListImport. */ 'use strict'; (function () { function el(id) { return document.getElementById(id); } window.WPListImport = function (cfg) { // cfg.prefix DOM id prefix: '
-paste', '
-file', '
-file-btn', // '
-check-btn', '
-import-btn', '
-sample-btn', // '
-report', '
-add-name', '
-add-btn', '
-add-err', // '
-tool', '
-noproject' // cfg.api(suffix) URL builder for the project-scoped routes // cfg.projectId() current project id ('' = not opened from a project) // cfg.sample text the sample button loads // cfg.loadKey response key holding the rows ('nodes' | 'items') // cfg.render() paints the current list from state.rows // cfg.rejectedRow(r)
' + res.read + ' row' + (res.read === 1 ? '' : 's') + ' read. ' + nCreated + ' value' + (nCreated === 1 ? '' : 's') + ' ' + verb + ((res.reactivated || []).length ? ', ' + res.reactivated.length + ' brought back into use' : '') + '.
'); var rowList = function (title, rows, fmt) { if (!rows || !rows.length) return ''; return 'Nothing to do — every row is already on this project.
'); } say(bits.join(''), problem); } function importText(dryRun) { var text = (el(p + '-paste') || {}).value || ''; if (!text.trim()) { say('Paste some rows or choose a CSV file first.', true); return; } if (!cfg.projectId()) { load(); return; } say('Checking…', false); fetch(cfg.api('/import'), { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ text: text, dry_run: !!dryRun }), }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, status: r.status, body: j }; }); }) .then(function (res) { if (!res.ok) { say('⚠ Import refused — ' + esc((res.body && res.body.detail) || ('HTTP ' + res.status)), true); return; } report(res.body); if (!dryRun) return load(true); }) .catch(function (err) { say('⚠ Could not reach the server — ' + esc((err && err.message) || 'offline') + '. Nothing was imported.', true); }); } function add() { var read = cfg.addPayload(); if (read.error) { setAddError(read.error); var input = el(p + '-add-name'); if (input) input.focus(); return; } setAddError(''); fetch(cfg.api(''), { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(read.payload), }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, status: r.status, body: j }; }); }) .then(function (res) { if (!res.ok) { setAddError((res.body && res.body.detail) || ('Could not add it (HTTP ' + res.status + ')')); return; } var input = el(p + '-add-name'); if (input) input.value = ''; say(cfg.addedMessage(res.body), false); return load(true); }) .catch(function (err) { setAddError('Could not reach the server — ' + ((err && err.message) || 'offline')); }); } function patch(id, patchBody, describe) { return fetch(cfg.api('/' + encodeURIComponent(id)), { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(patchBody), }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, status: r.status, body: j }; }); }) .then(function (res) { if (!res.ok) { say('⚠ ' + esc((res.body && res.body.detail) || ('HTTP ' + res.status)), true); return load(true); } say(describe(res.body), false); return load(true); }) .catch(function (err) { say('⚠ Could not reach the server — ' + esc((err && err.message) || 'offline'), true); }); } function wire() { var paste = el(p + '-paste'); var file = el(p + '-file'); var btn = function (suffix) { return el(p + suffix); }; if (btn('-file-btn')) btn('-file-btn').addEventListener('click', function () { if (file) file.click(); }); if (file) file.addEventListener('change', function (ev) { var f = ev.target.files && ev.target.files[0]; if (!f) return; var reader = new FileReader(); reader.onload = function () { // One parser, on the server. Reading the file here and posting its text // is what stops "what does a blank column mean" having two answers. if (paste) paste.value = String(reader.result || ''); say('Read ' + esc(f.name) + '. Check it, then import.', false); }; reader.onerror = function () { say('⚠ Could not read that file.', true); }; reader.readAsText(f); ev.target.value = ''; }); if (btn('-check-btn')) btn('-check-btn').addEventListener('click', function () { importText(true); }); if (btn('-import-btn')) btn('-import-btn').addEventListener('click', function () { importText(false); }); if (btn('-sample-btn')) btn('-sample-btn').addEventListener('click', function () { if (paste) paste.value = cfg.sample; say('Sample values loaded into the box — obviously fake, and safe to import ' + 'on a throwaway project.', false); }); if (btn('-add-btn')) btn('-add-btn').addEventListener('click', add); } return { state: state, say: say, setAddError: setAddError, load: load, importText: importText, add: add, patch: patch, wire: wire }; }; })();