Inline-edit the type badge directly in the table
Clicking a row's type badge swaps it for a native <select> in place; picking a new value POSTs to a new endpoint (LedgerActionsController::updateType) instead of opening the full edit modal for this one field. The endpoint goes through the normal node save() lifecycle, so figli_compta_ledger_node_presave() still forces a proper revision and still enforces the répartition invariant -- nothing here bypasses that. It also clears a stale field_entree_liee when the new type is no longer linkable (versement/achat/hébergement), mirroring the full form's #states visibility rule. CSRF-protected via core's own /session/token, scoped to CsrfRequestHeaderAccessCheck::TOKEN_KEY to match what that endpoint actually generates. Verified end-to-end via the real click flow: correct revision (user + timestamp), correct optimistic UI update, correct field_entree_liee clearing, and 400/403 on invalid type / missing CSRF respectively.
This commit is contained in:
@@ -129,6 +129,23 @@
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// POST /lignes/{nid}/type -- change field_type_ligne without opening the
|
||||
// full edit form. Fetches a fresh CSRF token each time (core's
|
||||
// /session/token) rather than caching one -- this is an occasional
|
||||
// action, not worth the complexity of handling a stale cached token.
|
||||
async function updateLigneType(nid, type) {
|
||||
const tokenRes = await fetch('/session/token');
|
||||
const token = await tokenRes.text();
|
||||
const res = await fetch('/lignes/' + nid + '/type', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token },
|
||||
body: JSON.stringify({ type }),
|
||||
});
|
||||
const json = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(json.error || ('/lignes/' + nid + '/type a répondu ' + res.status));
|
||||
return json;
|
||||
}
|
||||
|
||||
async function fetchYearTotals(annee) {
|
||||
const res = await fetch('/lignes/api/totaux?annee=' + encodeURIComponent(annee), { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error('/lignes/api/totaux a répondu ' + res.status);
|
||||
@@ -202,6 +219,10 @@
|
||||
onlyErrors: false,
|
||||
hoverCol: null,
|
||||
filterEntreeId: null,
|
||||
// Which row's type badge is currently showing its inline <select>
|
||||
// instead of the badge -- only one at a time.
|
||||
editingTypeId: null,
|
||||
typeUpdateError: null,
|
||||
// Fenêtre glissante.
|
||||
windowStart: null,
|
||||
windowEnd: null,
|
||||
@@ -396,6 +417,36 @@
|
||||
toggleEntreeFilter(id) {
|
||||
this.filterEntreeId = this.filterEntreeId === id ? null : id;
|
||||
},
|
||||
startEditType(item) {
|
||||
this.typeUpdateError = null;
|
||||
this.editingTypeId = item.id;
|
||||
},
|
||||
// Optimistically patches the row in `rows` (not the transient
|
||||
// `item` from groupedRows -- that object is rebuilt from scratch on
|
||||
// every computed re-evaluation, so mutating it wouldn't stick) for
|
||||
// instant feedback, then reconciles with the server in the
|
||||
// background via reloadWindow() (picks up anything else the save
|
||||
// touched, e.g. a cleared field_entree_liee).
|
||||
async saveType(item, event) {
|
||||
const newType = event.target.value;
|
||||
this.editingTypeId = null;
|
||||
if (newType === item.type) return;
|
||||
try {
|
||||
const result = await updateLigneType(item.nid, newType);
|
||||
const row = this.rows.find((r) => r.id === item.id);
|
||||
if (row) {
|
||||
row.type = newType;
|
||||
row.linkable = LINKABLE_TYPES.includes(newType);
|
||||
if (result.entree_liee_cleared) {
|
||||
row.entreeLieeId = null;
|
||||
row.entreeLieeLabel = null;
|
||||
}
|
||||
}
|
||||
this.reloadWindow();
|
||||
} catch (err) {
|
||||
this.typeUpdateError = err.message;
|
||||
}
|
||||
},
|
||||
onCellHover(evt) {
|
||||
const cell = evt.target.closest('td, th');
|
||||
if (!cell) return;
|
||||
|
||||
Reference in New Issue
Block a user