fix: Bearbeiten und Loeschen stehen wieder sichtbar in jeder Zeile

Mit Notiz und Tags ist die Buchungsliste breiter als der Bildschirm geworden.
Die Knopfspalte scrollte damit einfach aus dem Bild - und weil sie in einer
gespeicherten Spaltenreihenfolge auch noch mittendrin stand, war sie gar
nicht mehr zu finden. "fixed" hiess bisher nur "nicht verschiebbar", nicht
"bleibt stehen".

Angeheftete Spalten (neu: pin) kleben jetzt am rechten Rand und ruecken
immer ans Ende, egal was gespeichert ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-19 11:48:07 +02:00
co-authored by Claude Opus 5
parent 637f5f4a70
commit 15b863ff5e
3 changed files with 35 additions and 6 deletions
+11 -5
View File
@@ -11,7 +11,8 @@ import { Skeleton } from "./Skeleton";
* *
* Spalten-Def: { key, header, render(row)→JSX, filterText(row)? (aktiviert Filter), * Spalten-Def: { key, header, render(row)→JSX, filterText(row)? (aktiviert Filter),
* sortValue(row)? (Sortierung), align:"num", grow:true (füllt Restbreite), * sortValue(row)? (Sortierung), align:"num", grow:true (füllt Restbreite),
* fixed:true (kein Reorder/Resize), width, min }. * fixed:true (kein Reorder/Resize), pin:"right" (klebt am rechten Rand
* und steht immer ganz hinten), width, min }.
*/ */
const DEFAULT_WIDTH = 150; const DEFAULT_WIDTH = 150;
@@ -96,7 +97,12 @@ export default function DataTable({
return () => ro.disconnect(); return () => ro.disconnect();
}, []); }, []);
const allOrdered = order.map((k) => colByKey[k]).filter(Boolean); const gespeichert = order.map((k) => colByKey[k]).filter(Boolean);
// Angeheftete Spalten gehoeren ans Ende, egal was gespeichert ist. Sonst
// klebte etwa die Knopfspalte mitten im Scrollbereich und deckte ihre
// Nachbarn zu – und neu hinzugekommene Spalten landen ohnehin dahinter.
const allOrdered = [...gespeichert.filter((c) => !c.pin),
...gespeichert.filter((c) => c.pin)];
const orderedCols = allOrdered.filter((c) => !hidden.has(c.key)); const orderedCols = allOrdered.filter((c) => !hidden.has(c.key));
const chooserBtn = useRef(null); const chooserBtn = useRef(null);
@@ -285,7 +291,7 @@ export default function DataTable({
<tr> <tr>
{orderedCols.map((c) => ( {orderedCols.map((c) => (
<th key={c.key} <th key={c.key}
className={`dt-th ${c.align === "num" ? "num" : ""} ${dropKey === c.key ? "drop" : ""} ${sort?.key === c.key ? "sorted" : ""}`} className={`dt-th ${c.align === "num" ? "num" : ""} ${dropKey === c.key ? "drop" : ""} ${sort?.key === c.key ? "sorted" : ""} ${c.pin ? "dt-pin" : ""}`}
draggable={!c.fixed} draggable={!c.fixed}
onDragStart={(e) => onDragStart(e, c)} onDragStart={(e) => onDragStart(e, c)}
onDragOver={(e) => onDragOver(e, c)} onDragOver={(e) => onDragOver(e, c)}
@@ -301,7 +307,7 @@ export default function DataTable({
{filterShown && ( {filterShown && (
<tr className="dt-filter"> <tr className="dt-filter">
{orderedCols.map((c) => ( {orderedCols.map((c) => (
<td key={c.key}> <td key={c.key} className={c.pin ? "dt-pin" : ""}>
{(c.filterText || c.filterValues) ? ( {(c.filterText || c.filterValues) ? (
<FilterCell <FilterCell
col={c} col={c}
@@ -325,7 +331,7 @@ export default function DataTable({
onClick={onRowClick ? (e) => onRowClick(row, e) : undefined} onClick={onRowClick ? (e) => onRowClick(row, e) : undefined}
style={onRowClick ? { cursor: "pointer" } : undefined}> style={onRowClick ? { cursor: "pointer" } : undefined}>
{orderedCols.map((c) => ( {orderedCols.map((c) => (
<td key={c.key} className={c.align === "num" ? "num" : ""}>{c.render(row)}</td> <td key={c.key} className={`${c.align === "num" ? "num" : ""} ${c.pin ? "dt-pin" : ""}`}>{c.render(row)}</td>
))} ))}
</tr> </tr>
))} ))}
+20
View File
@@ -774,6 +774,26 @@ tr.row-dup {
cursor: pointer; cursor: pointer;
} }
/* Angeheftete Spalte: klebt am rechten Rand, wenn die Tabelle breiter ist als
das Fenster. Der Hintergrund muss deckend sein, sonst scrollen die anderen
Zellen sichtbar darunter durch. */
.datatable th.dt-pin,
.datatable td.dt-pin {
position: sticky;
right: 0;
z-index: 2;
background: var(--surface);
}
.datatable tbody tr:hover td.dt-pin { background: var(--surface-2); }
/* Eine Kante nach links, damit man sieht, dass dahinter weitergeht. */
.datatable th.dt-pin::before,
.datatable td.dt-pin::before {
content: "";
position: absolute;
left: 0; top: 0; bottom: 0;
border-left: 1px solid var(--border);
}
/* DataTable-Zellen einzeilig halten, damit „…" greift (Excel-artig). */ /* DataTable-Zellen einzeilig halten, damit „…" greift (Excel-artig). */
.datatable td { white-space: nowrap; vertical-align: middle; } .datatable td { white-space: nowrap; vertical-align: middle; }
.datatable td .named-text { min-width: 0; } .datatable td .named-text { min-width: 0; }
+4 -1
View File
@@ -624,7 +624,10 @@ export default function Transactions() {
}, },
}, },
{ {
key: "actions", header: "", width: 118, fixed: true, // Angeheftet: Seit Notiz und Tags dazugekommen sind, ist die Tabelle
// breiter als der Bildschirm – ohne das scrollten Bearbeiten und
// Loeschen einfach aus dem Bild.
key: "actions", header: "", width: 118, fixed: true, pin: "right",
render: (tx) => ( render: (tx) => (
<div className="nowrap" style={{ textAlign: "right" }}> <div className="nowrap" style={{ textAlign: "right" }}>
<button className="btn sm ghost" title={t("common.edit")} onClick={() => openEdit(tx)}> <button className="btn sm ghost" title={t("common.edit")} onClick={() => openEdit(tx)}>