Add NPC database table with server-side DataTable and per-NPC game notes
- Npc entity (village spreadsheet columns) + game_notes text field for
the DM's live play notes, plus updated_at touched on every change
- Race / VillageStatus / BloodCartInvolvement / MysteryImportance as
string-backed PHP enums, rendered as dropdowns on the edit form
- /npcs: server-side DataTables (paging, global search and column sort
all run in the backend via NpcRepository::findForDataTable); name cell
links to the edit page, game notes shown truncated
- /npcs/{id}: Bootstrap-themed edit form (all fields + a large game
notes textarea); Symfony forms now use the bootstrap_5 theme
- app.js: generic server-side DataTable wiring driven by data-* on the
<table>/<th> (data-datatable-src, data-row-url, data-format)
- app:npc:import console command - upserts from data/npcs.csv by
(family, name), never overwrites game_notes; --purge / --dry-run
- data/npcs.csv: 180 villagers exported from the working spreadsheet
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+51
-3
@@ -13,14 +13,48 @@ import DataTable from 'datatables.net-bs5';
|
||||
|
||||
window.bootstrap = bootstrap;
|
||||
|
||||
// Turn any <table class="datatable"> into a DataTable. Opt out per-table with
|
||||
// data-datatable="false"; pass options as a JSON object in data-datatable-options.
|
||||
const esc = (v) => {
|
||||
if (v === null || v === undefined) return '';
|
||||
return String(v).replace(/[&<>"']/g, (c) => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
}[c]));
|
||||
};
|
||||
|
||||
// Build a DataTables column definition from a <th> in the table head.
|
||||
// data-data key in the row object (required for server-side tables)
|
||||
// data-orderable "false" to disable sorting on the column
|
||||
// data-format "link" | "truncate" | (default: plain escaped text)
|
||||
// data-row-url (on the <table>) URL template with __ID__ for "link" columns
|
||||
// NB: "format" not "render" - DataTables itself consumes a data-render attribute.
|
||||
function columnFromTh(th, rowUrl) {
|
||||
const key = th.dataset.data;
|
||||
const col = { data: key || null, orderable: th.dataset.orderable !== 'false' };
|
||||
|
||||
if (th.dataset.format === 'link' && rowUrl) {
|
||||
col.render = (val, type, row) => {
|
||||
if (type !== 'display') return val ?? '';
|
||||
return `<a href="${esc(rowUrl.replace('__ID__', row.id))}">${esc(val)}</a>`;
|
||||
};
|
||||
} else if (th.dataset.format === 'truncate') {
|
||||
col.render = (val, type) => {
|
||||
if (type !== 'display') return val ?? '';
|
||||
const s = val ? String(val) : '';
|
||||
const short = s.length > 70 ? s.slice(0, 70) + '…' : s;
|
||||
return `<span title="${esc(s)}">${esc(short)}</span>`;
|
||||
};
|
||||
} else {
|
||||
col.render = (val, type) => (type === 'display' ? esc(val) : (val ?? ''));
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
function initDataTables(root = document) {
|
||||
root.querySelectorAll('table.datatable').forEach((table) => {
|
||||
if (table.dataset.datatable === 'false' || DataTable.isDataTable(table)) {
|
||||
return;
|
||||
}
|
||||
let options = { pageLength: 25, order: [] };
|
||||
|
||||
let options = { pageLength: 25, order: [], responsive: false };
|
||||
if (table.dataset.datatableOptions) {
|
||||
try {
|
||||
options = { ...options, ...JSON.parse(table.dataset.datatableOptions) };
|
||||
@@ -28,6 +62,20 @@ function initDataTables(root = document) {
|
||||
console.error('Invalid data-datatable-options on', table, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Server-side mode: the backend does the paging, sorting and searching.
|
||||
if (table.dataset.datatableSrc) {
|
||||
const ths = [...table.querySelectorAll('thead th')];
|
||||
options = {
|
||||
...options,
|
||||
serverSide: true,
|
||||
processing: true,
|
||||
ajax: { url: table.dataset.datatableSrc },
|
||||
columns: ths.map((th) => columnFromTh(th, table.dataset.rowUrl)),
|
||||
order: options.order.length ? options.order : [[0, 'asc']],
|
||||
};
|
||||
}
|
||||
|
||||
new DataTable(table, options);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user