@cosmicdrift/kumiko-renderer-web 0.186.2 → 0.187.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/__tests__/primitives.test.tsx +121 -0
- package/src/primitives/__tests__/date-parse.test.ts +26 -1
- package/src/primitives/__tests__/embedded-list-input.test.tsx +16 -0
- package/src/primitives/date-field.tsx +13 -5
- package/src/primitives/date-parse.ts +30 -7
- package/src/primitives/embedded-list-input.tsx +2 -1
- package/src/primitives/index.tsx +42 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.187.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.187.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.187.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.187.0",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// abhängen werden.
|
|
9
9
|
|
|
10
10
|
import { describe, expect, mock, test } from "bun:test";
|
|
11
|
+
import { type ColumnRendererProps, ColumnRenderersProvider } from "@cosmicdrift/kumiko-renderer";
|
|
11
12
|
import userEvent from "@testing-library/user-event";
|
|
13
|
+
import type { ReactNode } from "react";
|
|
12
14
|
import { defaultPrimitives, END_LABEL_MIN_ROWS } from "../primitives";
|
|
13
15
|
import { PageSection, Stack } from "../primitives/layout";
|
|
14
16
|
import { fireEvent, render, screen } from "./test-utils";
|
|
@@ -876,6 +878,125 @@ describe("DataTable", () => {
|
|
|
876
878
|
expect(onRowClick).not.toHaveBeenCalled();
|
|
877
879
|
});
|
|
878
880
|
});
|
|
881
|
+
|
|
882
|
+
describe("highlighted column", () => {
|
|
883
|
+
const cols = [
|
|
884
|
+
{ field: "name", label: "Name", type: "string", sortable: false },
|
|
885
|
+
{ field: "y2026", label: "2026", type: "number", sortable: false, highlighted: true },
|
|
886
|
+
] as const;
|
|
887
|
+
const oneRow = [{ id: "r1", values: { name: "Alice", y2026: 42 } }];
|
|
888
|
+
|
|
889
|
+
test("marks header + cell of the highlighted column, leaves others alone", () => {
|
|
890
|
+
render(<DataTable columns={cols} rows={oneRow} />);
|
|
891
|
+
expect(screen.getByTestId("column-y2026").getAttribute("data-highlighted")).toBe("true");
|
|
892
|
+
expect(screen.getByTestId("column-name").getAttribute("data-highlighted")).toBeNull();
|
|
893
|
+
expect(screen.getByTestId("cell-r1-y2026").getAttribute("data-highlighted")).toBe("true");
|
|
894
|
+
expect(screen.getByTestId("cell-r1-name").getAttribute("data-highlighted")).toBeNull();
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
describe("testId overrides", () => {
|
|
899
|
+
const cols = [{ field: "name", label: "Name", type: "string", sortable: false }] as const;
|
|
900
|
+
const oneRow = [{ id: "r1", values: { name: "Alice" } }];
|
|
901
|
+
|
|
902
|
+
test("getRowTestId/getCellTestId override the row-<id>/cell-<id>-<field> defaults", () => {
|
|
903
|
+
render(
|
|
904
|
+
<DataTable
|
|
905
|
+
columns={cols}
|
|
906
|
+
rows={oneRow}
|
|
907
|
+
getRowTestId={(row) => `own-row-${row.id}`}
|
|
908
|
+
getCellTestId={(row, field) => `own-cell-${row.id}-${field}`}
|
|
909
|
+
/>,
|
|
910
|
+
);
|
|
911
|
+
expect(screen.getByTestId("own-row-r1")).not.toBeNull();
|
|
912
|
+
expect(screen.getByTestId("own-cell-r1-name")).not.toBeNull();
|
|
913
|
+
expect(screen.queryByTestId("row-r1")).toBeNull();
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test("without overrides, defaults stay row-<id>/cell-<id>-<field>", () => {
|
|
917
|
+
render(<DataTable columns={cols} rows={oneRow} />);
|
|
918
|
+
expect(screen.getByTestId("row-r1")).not.toBeNull();
|
|
919
|
+
expect(screen.getByTestId("cell-r1-name")).not.toBeNull();
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
describe("onCellChange (editable cell)", () => {
|
|
924
|
+
function EditableSwatch({ value, column, onChange }: ColumnRendererProps): ReactNode {
|
|
925
|
+
return (
|
|
926
|
+
<input
|
|
927
|
+
data-testid={`edit-${column.field}`}
|
|
928
|
+
value={String(value)}
|
|
929
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
930
|
+
/>
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
test("column-renderer receives onChange bound to (rowId, field, value) when onCellChange is set", () => {
|
|
935
|
+
const onCellChange = mock();
|
|
936
|
+
const cols = [
|
|
937
|
+
{
|
|
938
|
+
field: "name",
|
|
939
|
+
label: "Name",
|
|
940
|
+
type: "string",
|
|
941
|
+
sortable: false,
|
|
942
|
+
renderer: { react: { __component: "EditableSwatch" } },
|
|
943
|
+
},
|
|
944
|
+
] as const;
|
|
945
|
+
render(
|
|
946
|
+
<ColumnRenderersProvider value={{ EditableSwatch }}>
|
|
947
|
+
<DataTable
|
|
948
|
+
columns={cols}
|
|
949
|
+
rows={[{ id: "r1", values: { name: "Alice" } }]}
|
|
950
|
+
onCellChange={onCellChange}
|
|
951
|
+
/>
|
|
952
|
+
</ColumnRenderersProvider>,
|
|
953
|
+
);
|
|
954
|
+
fireEvent.change(screen.getByTestId("edit-name"), { target: { value: "Bob" } });
|
|
955
|
+
expect(onCellChange).toHaveBeenCalledWith("r1", "name", "Bob");
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test("without onCellChange, the component renderer gets no onChange (read-only)", () => {
|
|
959
|
+
const cols = [
|
|
960
|
+
{
|
|
961
|
+
field: "name",
|
|
962
|
+
label: "Name",
|
|
963
|
+
type: "string",
|
|
964
|
+
sortable: false,
|
|
965
|
+
renderer: { react: { __component: "EditableSwatch" } },
|
|
966
|
+
},
|
|
967
|
+
] as const;
|
|
968
|
+
render(
|
|
969
|
+
<ColumnRenderersProvider value={{ EditableSwatch }}>
|
|
970
|
+
<DataTable columns={cols} rows={[{ id: "r1", values: { name: "Alice" } }]} />
|
|
971
|
+
</ColumnRenderersProvider>,
|
|
972
|
+
);
|
|
973
|
+
// onChange is undefined → EditableSwatch's onChange?.(...) is a no-op;
|
|
974
|
+
// rendering itself must still succeed (no crash) with a stable value.
|
|
975
|
+
expect((screen.getByTestId("edit-name") as HTMLInputElement).value).toBe("Alice");
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
test("format-spec renderer ignores onCellChange — no crash, plain formatted text", () => {
|
|
979
|
+
const onCellChange = mock();
|
|
980
|
+
const cols = [
|
|
981
|
+
{
|
|
982
|
+
field: "amount",
|
|
983
|
+
label: "Amount",
|
|
984
|
+
type: "number",
|
|
985
|
+
sortable: false,
|
|
986
|
+
renderer: { format: "currency", symbol: "€" },
|
|
987
|
+
},
|
|
988
|
+
] as const;
|
|
989
|
+
render(
|
|
990
|
+
<DataTable
|
|
991
|
+
columns={cols}
|
|
992
|
+
rows={[{ id: "r1", values: { amount: 42 } }]}
|
|
993
|
+
onCellChange={onCellChange}
|
|
994
|
+
/>,
|
|
995
|
+
);
|
|
996
|
+
expect(screen.getByTestId("cell-r1-amount").textContent).toBe("42 €");
|
|
997
|
+
expect(onCellChange).not.toHaveBeenCalled();
|
|
998
|
+
});
|
|
999
|
+
});
|
|
879
1000
|
});
|
|
880
1001
|
|
|
881
1002
|
describe("Form", () => {
|
|
@@ -6,7 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
import { describe, expect, test } from "bun:test";
|
|
8
8
|
import { Temporal } from "temporal-polyfill";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
formatDateForInput,
|
|
11
|
+
formatDatePlaceholder,
|
|
12
|
+
parseIso,
|
|
13
|
+
parseTypedDate,
|
|
14
|
+
toIso,
|
|
15
|
+
} from "../date-parse";
|
|
16
|
+
|
|
17
|
+
const dePlaceholderLetters = { year: "J", month: "M", day: "T" };
|
|
18
|
+
const enPlaceholderLetters = { year: "Y", month: "M", day: "D" };
|
|
10
19
|
|
|
11
20
|
describe("parseIso", () => {
|
|
12
21
|
test("valid yyyy-mm-dd → PlainDate (no TZ conversion)", () => {
|
|
@@ -135,3 +144,19 @@ describe("formatDateForInput", () => {
|
|
|
135
144
|
if (roundtrip !== undefined) expect(toIso(roundtrip)).toBe("2026-04-25");
|
|
136
145
|
});
|
|
137
146
|
});
|
|
147
|
+
|
|
148
|
+
// #1865: placeholder shows the locale's format pattern, not a hardcoded
|
|
149
|
+
// example date that reads as an already-filled-in value.
|
|
150
|
+
describe("formatDatePlaceholder", () => {
|
|
151
|
+
test("de-DE → day.month.year pattern with dot separator", () => {
|
|
152
|
+
expect(formatDatePlaceholder("de-DE", dePlaceholderLetters)).toBe("TT.MM.JJJJ");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("en-US → month/day/year pattern with slash separator", () => {
|
|
156
|
+
expect(formatDatePlaceholder("en-US", enPlaceholderLetters)).toBe("MM/DD/YYYY");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("en-GB → day/month/year pattern with slash separator", () => {
|
|
160
|
+
expect(formatDatePlaceholder("en-GB", enPlaceholderLetters)).toBe("DD/MM/YYYY");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -605,3 +605,19 @@ describe("EmbeddedListInput — currency (#1839)", () => {
|
|
|
605
605
|
expect(totals.textContent).toContain("€");
|
|
606
606
|
});
|
|
607
607
|
});
|
|
608
|
+
|
|
609
|
+
describe("EmbeddedListInput — desktop table width (solon#107)", () => {
|
|
610
|
+
test("the table keeps min-w-max so columns don't shrink below their width classes", () => {
|
|
611
|
+
const rows = [{ description: "A", quantity: 1, amount: 100 }];
|
|
612
|
+
renderWithLocale(<EmbeddedListInput {...baseProps({ rows })} />);
|
|
613
|
+
const desktop = screen.getByTestId("lines-desktop");
|
|
614
|
+
const table = desktop.querySelector("table");
|
|
615
|
+
if (table === null) throw new Error("expected a <table> in the desktop layout");
|
|
616
|
+
expect(table.className).toContain("min-w-max");
|
|
617
|
+
|
|
618
|
+
const headers = desktop.querySelectorAll("th");
|
|
619
|
+
expect(headers[0]?.className).toContain("min-w-[10rem]");
|
|
620
|
+
expect(headers[1]?.className).toContain("w-36");
|
|
621
|
+
expect(headers[2]?.className).toContain("w-36");
|
|
622
|
+
});
|
|
623
|
+
});
|
|
@@ -12,7 +12,14 @@ import { type ReactNode, useState } from "react";
|
|
|
12
12
|
import { Temporal } from "temporal-polyfill";
|
|
13
13
|
import { cn } from "../lib/cn";
|
|
14
14
|
import { CalendarPopover } from "./calendar-popover";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
formatDateForInput,
|
|
17
|
+
formatDatePlaceholder,
|
|
18
|
+
guessLocale,
|
|
19
|
+
parseIso,
|
|
20
|
+
parseTypedDate,
|
|
21
|
+
toIso,
|
|
22
|
+
} from "./date-parse";
|
|
16
23
|
|
|
17
24
|
// CalendarPopover wraps react-day-picker, which only accepts native Date
|
|
18
25
|
// objects — the PlainDate↔Date boundary conversion stays confined to
|
|
@@ -98,10 +105,11 @@ export function DateField({
|
|
|
98
105
|
disabled={disabled}
|
|
99
106
|
required={required}
|
|
100
107
|
aria-invalid={hasError === true ? true : undefined}
|
|
101
|
-
placeholder={
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
placeholder={formatDatePlaceholder(resolvedLocale, {
|
|
109
|
+
year: t("kumiko.field.dateField.placeholderYear"),
|
|
110
|
+
month: t("kumiko.field.dateField.placeholderMonth"),
|
|
111
|
+
day: t("kumiko.field.dateField.placeholderDay"),
|
|
112
|
+
})}
|
|
105
113
|
onChange={(e) => {
|
|
106
114
|
setDraft(e.target.value);
|
|
107
115
|
commitTyped(e.target.value);
|
|
@@ -71,19 +71,23 @@ export function formatDateForInput(d: Temporal.PlainDate, locale: string): strin
|
|
|
71
71
|
|
|
72
72
|
type DateSlot = "y" | "m" | "d";
|
|
73
73
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// millis
|
|
74
|
+
// Shared by localeDateOrder and formatDatePlaceholder — both need the
|
|
75
|
+
// locale's numeric formatToParts breakdown of the same reference date.
|
|
76
|
+
// epoch-millis input instead of a Date object (guard-compliant); timeZone:
|
|
77
77
|
// "UTC" keeps the reference from shifting to the 1st depending on the
|
|
78
78
|
// browser's TZ.
|
|
79
|
-
function
|
|
79
|
+
function localeDateParts(locale: string): Intl.DateTimeFormatPart[] {
|
|
80
80
|
const refEpochMillis = activeTemporal()
|
|
81
81
|
.PlainDate.from({ year: 2026, month: 1, day: 2 })
|
|
82
82
|
.toZonedDateTime("UTC").epochMilliseconds;
|
|
83
|
+
return new Intl.DateTimeFormat(locale, { timeZone: "UTC" }).formatToParts(refEpochMillis);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Field order of the numeric locale format. de → [d,m,y], en-US →
|
|
87
|
+
// [m,d,y], ISO-like locales → [y,m,d].
|
|
88
|
+
function localeDateOrder(locale: string): readonly DateSlot[] {
|
|
83
89
|
const order: DateSlot[] = [];
|
|
84
|
-
for (const part of
|
|
85
|
-
refEpochMillis,
|
|
86
|
-
)) {
|
|
90
|
+
for (const part of localeDateParts(locale)) {
|
|
87
91
|
if (part.type === "year") order.push("y");
|
|
88
92
|
else if (part.type === "month") order.push("m");
|
|
89
93
|
else if (part.type === "day") order.push("d");
|
|
@@ -91,6 +95,25 @@ function localeDateOrder(locale: string): readonly DateSlot[] {
|
|
|
91
95
|
return order;
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
// Locale-shaped placeholder pattern, e.g. de "TT.MM.JJJJ", en-US
|
|
99
|
+
// "MM/DD/YYYY", en-GB "DD/MM/YYYY". Slot order and separator both come
|
|
100
|
+
// from formatToParts — nothing hardcoded per locale. `letters` is one
|
|
101
|
+
// character per slot (from i18n); repeated to the slot's digit count
|
|
102
|
+
// (day/month 2, year 4).
|
|
103
|
+
export function formatDatePlaceholder(
|
|
104
|
+
locale: string,
|
|
105
|
+
letters: { readonly year: string; readonly month: string; readonly day: string },
|
|
106
|
+
): string {
|
|
107
|
+
return localeDateParts(locale)
|
|
108
|
+
.map((part) => {
|
|
109
|
+
if (part.type === "year") return letters.year.repeat(4);
|
|
110
|
+
if (part.type === "month") return letters.month.repeat(2);
|
|
111
|
+
if (part.type === "day") return letters.day.repeat(2);
|
|
112
|
+
return part.value;
|
|
113
|
+
})
|
|
114
|
+
.join("");
|
|
115
|
+
}
|
|
116
|
+
|
|
94
117
|
// Typed input → PlainDate. Accepts ISO (yyyy-mm-dd) directly, plus three
|
|
95
118
|
// numeric tokens in locale order with any separator (".", "/", "-", " ").
|
|
96
119
|
// Two-digit years → 2000s. Partial/invalid input → undefined (caller
|
|
@@ -473,7 +473,8 @@ export function EmbeddedListInput({
|
|
|
473
473
|
{!isMobile && (
|
|
474
474
|
<div data-testid={testIdFor("desktop")} className="hidden md:block">
|
|
475
475
|
<div className="overflow-hidden rounded-lg border bg-card">
|
|
476
|
-
<
|
|
476
|
+
{/* w-full on Table's <table> would shrink columns below columnWidthClass; min-w-max keeps declared widths and lets the wrapper scroll instead */}
|
|
477
|
+
<Table className="min-w-max">
|
|
477
478
|
<TableHeader className="bg-muted">
|
|
478
479
|
<TableRow className="hover:bg-transparent">
|
|
479
480
|
{columns.map((column) => (
|
package/src/primitives/index.tsx
CHANGED
|
@@ -623,6 +623,9 @@ function DefaultDataTable({
|
|
|
623
623
|
onFilterChange,
|
|
624
624
|
onFilterReset,
|
|
625
625
|
testId,
|
|
626
|
+
onCellChange,
|
|
627
|
+
getRowTestId,
|
|
628
|
+
getCellTestId,
|
|
626
629
|
}: DataTableProps): ReactNode {
|
|
627
630
|
// Toolbar-Wrapper: gemeinsamer Container für Toolbar+Tabelle damit
|
|
628
631
|
// beide visuell zusammengehören. Toolbar ist NICHT sticky — Lists
|
|
@@ -643,7 +646,18 @@ function DefaultDataTable({
|
|
|
643
646
|
// Page-Background (z.B. Cream) matchen Listen sonst nicht die Cards.
|
|
644
647
|
<div className="overflow-hidden rounded-lg border bg-card">
|
|
645
648
|
<Table data-testid={testId}>
|
|
646
|
-
{tableInner(
|
|
649
|
+
{tableInner(
|
|
650
|
+
columns,
|
|
651
|
+
rows,
|
|
652
|
+
onRowClick,
|
|
653
|
+
sort,
|
|
654
|
+
onSortChange,
|
|
655
|
+
rowActions,
|
|
656
|
+
rowActionMode,
|
|
657
|
+
onCellChange,
|
|
658
|
+
getRowTestId,
|
|
659
|
+
getCellTestId,
|
|
660
|
+
)}
|
|
647
661
|
</Table>
|
|
648
662
|
</div>
|
|
649
663
|
);
|
|
@@ -742,6 +756,9 @@ function tableInner(
|
|
|
742
756
|
onSortChange?: DataTableProps["onSortChange"],
|
|
743
757
|
rowActions?: DataTableProps["rowActions"],
|
|
744
758
|
rowActionMode?: DataTableProps["rowActionMode"],
|
|
759
|
+
onCellChange?: DataTableProps["onCellChange"],
|
|
760
|
+
getRowTestId?: DataTableProps["getRowTestId"],
|
|
761
|
+
getCellTestId?: DataTableProps["getCellTestId"],
|
|
745
762
|
): ReactNode {
|
|
746
763
|
const hasActions = rowActions !== undefined && rowActions.length > 0;
|
|
747
764
|
return (
|
|
@@ -754,6 +771,7 @@ function tableInner(
|
|
|
754
771
|
field={col.field}
|
|
755
772
|
label={col.label}
|
|
756
773
|
sortable={col.sortable === true}
|
|
774
|
+
highlighted={col.highlighted === true}
|
|
757
775
|
{...(sort !== undefined && sort !== null && { sort })}
|
|
758
776
|
{...(onSortChange !== undefined && { onSortChange })}
|
|
759
777
|
/>
|
|
@@ -775,20 +793,21 @@ function tableInner(
|
|
|
775
793
|
{rows.map((row) => (
|
|
776
794
|
<TableRow
|
|
777
795
|
key={row.id}
|
|
778
|
-
data-testid={`row-${row.id}`}
|
|
796
|
+
data-testid={getRowTestId?.(row) ?? `row-${row.id}`}
|
|
779
797
|
onClick={onRowClick !== undefined ? () => onRowClick(row) : undefined}
|
|
780
798
|
className={cn(onRowClick !== undefined && "cursor-pointer")}
|
|
781
799
|
>
|
|
782
800
|
{columns.map((col) => (
|
|
783
801
|
<TableCell
|
|
784
802
|
key={col.field}
|
|
785
|
-
data-testid={`cell-${row.id}-${col.field}`}
|
|
803
|
+
data-testid={getCellTestId?.(row, col.field) ?? `cell-${row.id}-${col.field}`}
|
|
804
|
+
data-highlighted={col.highlighted === true ? "true" : undefined}
|
|
786
805
|
// Cells truncaten lange Werte mit ellipsis statt umzu-
|
|
787
806
|
// brechen — Lists bleiben einzeilig + scannbar (Linear-
|
|
788
807
|
// Pattern). max-w-xs gibt eine vernünftige Default-
|
|
789
808
|
// Obergrenze; der Table-Container scrollt horizontal
|
|
790
809
|
// falls die Summe der Spalten zu breit wird.
|
|
791
|
-
className="max-w-xs truncate"
|
|
810
|
+
className={cn("max-w-xs truncate", col.highlighted === true && "bg-accent/40")}
|
|
792
811
|
title={cellTitle(row.values[col.field])}
|
|
793
812
|
>
|
|
794
813
|
<DataTableCell
|
|
@@ -798,6 +817,9 @@ function tableInner(
|
|
|
798
817
|
type={col.type}
|
|
799
818
|
renderer={col.renderer}
|
|
800
819
|
{...(col.optionLabels !== undefined && { optionLabels: col.optionLabels })}
|
|
820
|
+
{...(onCellChange !== undefined && {
|
|
821
|
+
onChange: (value: unknown) => onCellChange(row.id, col.field, value),
|
|
822
|
+
})}
|
|
801
823
|
/>
|
|
802
824
|
</TableCell>
|
|
803
825
|
))}
|
|
@@ -1280,12 +1302,14 @@ function SortableHeader({
|
|
|
1280
1302
|
field,
|
|
1281
1303
|
label,
|
|
1282
1304
|
sortable,
|
|
1305
|
+
highlighted,
|
|
1283
1306
|
sort,
|
|
1284
1307
|
onSortChange,
|
|
1285
1308
|
}: {
|
|
1286
1309
|
readonly field: string;
|
|
1287
1310
|
readonly label: string;
|
|
1288
1311
|
readonly sortable: boolean;
|
|
1312
|
+
readonly highlighted?: boolean;
|
|
1289
1313
|
readonly sort?: DataTableSort;
|
|
1290
1314
|
readonly onSortChange?: (next: DataTableSort | null) => void;
|
|
1291
1315
|
}): ReactNode {
|
|
@@ -1298,7 +1322,8 @@ function SortableHeader({
|
|
|
1298
1322
|
<TableHead
|
|
1299
1323
|
data-testid={`column-${field}`}
|
|
1300
1324
|
data-sortable={sortable === true ? true : undefined}
|
|
1301
|
-
|
|
1325
|
+
data-highlighted={highlighted === true ? "true" : undefined}
|
|
1326
|
+
className={cn("px-4 text-muted-foreground", highlighted === true && "bg-accent/40")}
|
|
1302
1327
|
>
|
|
1303
1328
|
{label}
|
|
1304
1329
|
</TableHead>
|
|
@@ -1312,8 +1337,9 @@ function SortableHeader({
|
|
|
1312
1337
|
<TableHead
|
|
1313
1338
|
data-testid={`column-${field}`}
|
|
1314
1339
|
data-sortable="true"
|
|
1340
|
+
data-highlighted={highlighted === true ? "true" : undefined}
|
|
1315
1341
|
aria-sort={ariaSort}
|
|
1316
|
-
className="px-4 text-muted-foreground"
|
|
1342
|
+
className={cn("px-4 text-muted-foreground", highlighted === true && "bg-accent/40")}
|
|
1317
1343
|
>
|
|
1318
1344
|
<button
|
|
1319
1345
|
type="button"
|
|
@@ -1443,6 +1469,7 @@ type DataTableCellProps = {
|
|
|
1443
1469
|
readonly type: string;
|
|
1444
1470
|
readonly renderer?: unknown;
|
|
1445
1471
|
readonly optionLabels?: Readonly<Record<string, string>>;
|
|
1472
|
+
readonly onChange?: (value: unknown) => void;
|
|
1446
1473
|
};
|
|
1447
1474
|
|
|
1448
1475
|
// Cell-Renderer als Component (statt reiner Funktion) damit der
|
|
@@ -1461,6 +1488,7 @@ function DataTableCell({
|
|
|
1461
1488
|
type,
|
|
1462
1489
|
renderer,
|
|
1463
1490
|
optionLabels,
|
|
1491
|
+
onChange,
|
|
1464
1492
|
}: DataTableCellProps): ReactNode {
|
|
1465
1493
|
const componentRef = isComponentRendererRef(renderer);
|
|
1466
1494
|
const ResolvedComponent = useColumnRenderer(componentRef?.name);
|
|
@@ -1473,7 +1501,14 @@ function DataTableCell({
|
|
|
1473
1501
|
}
|
|
1474
1502
|
if (componentRef !== undefined) {
|
|
1475
1503
|
if (ResolvedComponent !== undefined) {
|
|
1476
|
-
return
|
|
1504
|
+
return (
|
|
1505
|
+
<ResolvedComponent
|
|
1506
|
+
value={value}
|
|
1507
|
+
row={row}
|
|
1508
|
+
column={{ field }}
|
|
1509
|
+
{...(onChange !== undefined && { onChange })}
|
|
1510
|
+
/>
|
|
1511
|
+
);
|
|
1477
1512
|
}
|
|
1478
1513
|
// Renderer im Schema referenziert, aber client-side kein Map-Eintrag —
|
|
1479
1514
|
// typischer Fall: clientFeatures.columnRenderers vergessen oder
|