@cosmicdrift/kumiko-renderer 0.400.0 → 0.401.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.
@@ -0,0 +1,6 @@
1
+ import { type PublishedReferenceChunk, type PublishedReferenceLookup, type ReferenceColumnSpec, type ReferenceLookupRequest } from "./render-list-reference-columns.ts";
2
+ export declare function useReferenceColumnLookups(rows: readonly Readonly<Record<string, unknown>>[], referenceColumns: readonly ReferenceColumnSpec[]): {
3
+ referenceRequests: readonly ReferenceLookupRequest[];
4
+ handleLookupMap: (field: string, chunkIndex: number, lookup: PublishedReferenceChunk) => void;
5
+ referenceLookups: Record<string, PublishedReferenceLookup>;
6
+ };
@@ -0,0 +1,36 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import { REFERENCE_LIST_LOOKUP_LIMIT } from "../hooks/reference-limits.js";
3
+ import { mergeChunkLookups, referenceIdChunks, } from "./render-list-reference-columns.js";
4
+ // Bridge components (renderReferenceLookupBridges) publish each id chunk's
5
+ // lookup through handleLookupMap; the merged result feeds enrichReferenceColumns.
6
+ export function useReferenceColumnLookups(rows, referenceColumns) {
7
+ const referenceRequests = useMemo(() => referenceColumns.map((column) => ({
8
+ ...column,
9
+ idChunks: referenceIdChunks(rows, column.field, REFERENCE_LIST_LOOKUP_LIMIT),
10
+ })), [referenceColumns, rows]);
11
+ const [chunkLookups, setChunkLookups] = useState({});
12
+ const handleLookupMap = useCallback((field, chunkIndex, lookup) => {
13
+ setChunkLookups((prev) => {
14
+ // Skip the update when the lookup is identical. Otherwise the bridge
15
+ // would re-render and send the same map back until React entered
16
+ // an infinite loop.
17
+ const current = prev[field]?.[chunkIndex];
18
+ const unchanged = current?.map === lookup.map &&
19
+ current.complete === lookup.complete &&
20
+ current.idsKey === lookup.idsKey;
21
+ if (unchanged)
22
+ return prev;
23
+ return { ...prev, [field]: { ...prev[field], [chunkIndex]: lookup } };
24
+ });
25
+ }, []);
26
+ const referenceLookups = useMemo(() => {
27
+ const merged = {};
28
+ for (const request of referenceRequests) {
29
+ const lookup = mergeChunkLookups(request.idChunks, chunkLookups[request.field]);
30
+ if (lookup !== undefined)
31
+ merged[request.field] = lookup;
32
+ }
33
+ return merged;
34
+ }, [referenceRequests, chunkLookups]);
35
+ return { referenceRequests, handleLookupMap, referenceLookups };
36
+ }
@@ -1,7 +1,6 @@
1
1
  import type { EntityDefinition, EntityListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
2
  import { type ScreenLinkResolver } from "../app/reference-link.ts";
3
3
  import type { ListColumnViewModel, ListViewModel, Translate } from "../headless/index.ts";
4
- import { type PublishedReferenceChunk, type ReferenceLookupRequest } from "./render-list-reference-columns.ts";
5
4
  export declare function withScreenLinks(columns: readonly ListColumnViewModel[], screen: EntityListScreenDefinition, resolveScreenLink: ScreenLinkResolver): readonly ListColumnViewModel[];
6
5
  type RenderListColumnsInput = {
7
6
  readonly screen: EntityListScreenDefinition;
@@ -11,8 +10,8 @@ type RenderListColumnsInput = {
11
10
  readonly translate: Translate;
12
11
  };
13
12
  export declare function useRenderListColumns(input: RenderListColumnsInput): {
14
- referenceRequests: readonly ReferenceLookupRequest[];
15
- handleLookupMap: (field: string, chunkIndex: number, lookup: PublishedReferenceChunk) => void;
13
+ referenceRequests: readonly import("./render-list-reference-columns.ts").ReferenceLookupRequest[];
14
+ handleLookupMap: (field: string, chunkIndex: number, lookup: import("./render-list-reference-columns.ts").PublishedReferenceChunk) => void;
16
15
  enrichedVm: Omit<ListViewModel, "columns"> & {
17
16
  columns: readonly ListColumnViewModel[];
18
17
  };
@@ -1,9 +1,9 @@
1
1
  import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
2
- import { useCallback, useMemo, useState } from "react";
2
+ import { useMemo } from "react";
3
3
  import { useReferenceLinkResolver, useScreenLinkResolver, } from "../app/reference-link.js";
4
4
  import { computeListViewModel } from "../headless/index.js";
5
- import { REFERENCE_LIST_LOOKUP_LIMIT } from "../hooks/reference-limits.js";
6
- import { enrichReferenceColumns, mergeChunkLookups, referenceIdChunks, selectReferenceColumns, } from "./render-list-reference-columns.js";
5
+ import { enrichReferenceColumns, selectReferenceColumns } from "./render-list-reference-columns.js";
6
+ import { useReferenceColumnLookups } from "./use-reference-column-lookups.js";
7
7
  // A column whose rows name their own target screen links each cell there.
8
8
  export function withScreenLinks(columns, screen, resolveScreenLink) {
9
9
  const linkFields = new Map();
@@ -38,34 +38,7 @@ export function useRenderListColumns(input) {
38
38
  // through onMap. Each bridge makes a constant number of hook calls (one),
39
39
  // and React manages its lifecycle when the schema changes via key.
40
40
  const referenceColumns = useMemo(() => selectReferenceColumns(vm.columns, featureName), [vm.columns, featureName]);
41
- const referenceRequests = useMemo(() => referenceColumns.map((column) => ({
42
- ...column,
43
- idChunks: referenceIdChunks(rows, column.field, REFERENCE_LIST_LOOKUP_LIMIT),
44
- })), [referenceColumns, rows]);
45
- const [chunkLookups, setChunkLookups] = useState({});
46
- const handleLookupMap = useCallback((field, chunkIndex, lookup) => {
47
- setChunkLookups((prev) => {
48
- // Skip the update when the lookup is identical. Otherwise the bridge
49
- // would re-render and send the same map back until React entered
50
- // an infinite loop.
51
- const current = prev[field]?.[chunkIndex];
52
- const unchanged = current?.map === lookup.map &&
53
- current.complete === lookup.complete &&
54
- current.idsKey === lookup.idsKey;
55
- if (unchanged)
56
- return prev;
57
- return { ...prev, [field]: { ...prev[field], [chunkIndex]: lookup } };
58
- });
59
- }, []);
60
- const referenceLookups = useMemo(() => {
61
- const merged = {};
62
- for (const request of referenceRequests) {
63
- const lookup = mergeChunkLookups(request.idChunks, chunkLookups[request.field]);
64
- if (lookup !== undefined)
65
- merged[request.field] = lookup;
66
- }
67
- return merged;
68
- }, [referenceRequests, chunkLookups]);
41
+ const { referenceRequests, handleLookupMap, referenceLookups } = useReferenceColumnLookups(rows, referenceColumns);
69
42
  const resolveReferenceLink = useReferenceLinkResolver();
70
43
  const resolveScreenLink = useScreenLinkResolver();
71
44
  const enrichedColumns = useMemo(() => withScreenLinks(enrichReferenceColumns({
@@ -16,7 +16,7 @@ function panelSpanClassName(panel) {
16
16
  return HALF_PANEL;
17
17
  return WIDE_PANEL;
18
18
  }
19
- function PanelBody({ panel, label, screenId, screenParams, translate, }) {
19
+ function PanelBody({ panel, label, featureName, screenId, screenParams, translate, }) {
20
20
  if (panel.kind === "stat-group") {
21
21
  return (_jsx(StatGroupPanelBody, { panel: panel, label: label, screenId: screenId, screenParams: screenParams, translate: translate }));
22
22
  }
@@ -36,7 +36,7 @@ function PanelBody({ panel, label, screenId, screenParams, translate, }) {
36
36
  return (_jsx(ChartPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
37
37
  }
38
38
  if (panel.kind === "list") {
39
- return (_jsx(ListPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
39
+ return (_jsx(ListPanelBody, { panel: panel, label: text, featureName: featureName, screenParams: screenParams, translate: translate }));
40
40
  }
41
41
  if (panel.kind === "feed") {
42
42
  return (_jsx(FeedPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
@@ -51,7 +51,7 @@ function renderDashboardPanel(panel, featureName, screenId, screenParams, transl
51
51
  return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, recordId: screenParams.recordId, translate: translate }, panel.id));
52
52
  }
53
53
  const label = panel.kind === "custom" || panel.label === undefined ? undefined : translate(panel.label);
54
- return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, screenId: screenId, screenParams: screenParams, translate: translate }) }, panel.id));
54
+ return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, featureName: featureName, screenId: screenId, screenParams: screenParams, translate: translate }) }, panel.id));
55
55
  }
56
56
  export function WebDashboardBody({ featureName, screen, translate, }) {
57
57
  const t = useTranslation();
@@ -23,9 +23,10 @@ export declare function ChartPanelBody({ panel, label, screenParams, translate,
23
23
  readonly screenParams: ScreenParams;
24
24
  readonly translate: Translate;
25
25
  }): ReactNode;
26
- export declare function ListPanelBody({ panel, label, screenParams, translate, }: {
26
+ export declare function ListPanelBody({ panel, label, featureName, screenParams, translate, }: {
27
27
  readonly panel: DashboardListPanel;
28
28
  readonly label: string;
29
+ readonly featureName: string;
29
30
  readonly screenParams: ScreenParams;
30
31
  readonly translate: Translate;
31
32
  }): ReactNode;
@@ -8,13 +8,13 @@ import { EMPTY_VALUE_PLACEHOLDER } from "../../headless/index.js";
8
8
  import { extensionSectionName, lastSegment, useExtensionSectionComponent, useNav, usePrimitives, useTranslation, useUserRoles, } from "../../index.js";
9
9
  import { initialWindowSelection, } from "../widgets/chart-window.js";
10
10
  import { chartToneColor, SegmentBarChart, StackedAreaChart, StackedBarChart, TimeseriesChart, } from "../widgets/charts.js";
11
- import { DashboardListTable } from "../widgets/dashboard-list.js";
12
11
  import { FeedList } from "../widgets/feed-list.js";
13
12
  import { ModeSwitch } from "../widgets/mode-switch.js";
14
13
  import { ProgressList } from "../widgets/progress-list.js";
15
14
  import { SectionCard } from "../widgets/section-card.js";
16
15
  import { STAT_CARD_GRID_CLASSES, StatCard, StatStripCell, } from "../widgets/stat.js";
17
16
  import { EmptyState, LoadingState } from "../widgets/states.js";
17
+ import { DashboardReferenceListTable } from "./dashboard-reference-list-table.js";
18
18
  import { DAY_MS, HOUR_MS, PanelError, PanelShell, panelPayload, QueryPanel, useDashboardFormats, useDashboardQuery, useReportPanelLoaded, } from "./dashboard-shared.js";
19
19
  import { isRecord, resolveDashboardText } from "./dashboard-text.js";
20
20
  function isSparkPoint(value) {
@@ -222,7 +222,7 @@ export function ChartPanelBody({ panel, label, screenParams, translate, }) {
222
222
  }), ariaLabel: label, testId: testId }));
223
223
  } }));
224
224
  }
225
- export function ListPanelBody({ panel, label, screenParams, translate, }) {
225
+ export function ListPanelBody({ panel, label, featureName, screenParams, translate, }) {
226
226
  const { formatPercent, formatNumber, formatDateTime } = useDashboardFormats();
227
227
  const columns = panel.columns.map((c) => {
228
228
  const normalized = normalizeListColumn(c);
@@ -233,9 +233,17 @@ export function ListPanelBody({ panel, label, screenParams, translate, }) {
233
233
  ...(normalized.badgeToneField !== undefined && {
234
234
  badgeToneField: normalized.badgeToneField,
235
235
  }),
236
+ ...(normalized.refEntity !== undefined && {
237
+ reference: {
238
+ refEntity: normalized.refEntity,
239
+ ...(normalized.refLabelField !== undefined && {
240
+ refLabelField: normalized.refLabelField,
241
+ }),
242
+ },
243
+ }),
236
244
  };
237
245
  });
238
- return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data) => (_jsx(DashboardListTable, { columns: columns, rows: data.rows ?? [], formatPercent: formatPercent, formatNumber: formatNumber, formatDateTime: formatDateTime, translate: translate })) }));
246
+ return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data) => (_jsx(DashboardReferenceListTable, { columns: columns, rows: data.rows ?? [], featureName: featureName, formatPercent: formatPercent, formatNumber: formatNumber, formatDateTime: formatDateTime, translate: translate })) }));
239
247
  }
240
248
  export function FeedPanelBody({ panel, label, screenParams, translate, }) {
241
249
  return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data, formats) => {
@@ -0,0 +1,17 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ import type { Translate } from "../../headless/index.ts";
3
+ import { type DashboardListColumn, DashboardListTable } from "../widgets/dashboard-list.tsx";
4
+ export type DashboardReferenceTarget = {
5
+ readonly refEntity: string;
6
+ readonly refLabelField?: string;
7
+ };
8
+ export type DashboardListTableColumn = DashboardListColumn & {
9
+ readonly reference?: DashboardReferenceTarget;
10
+ };
11
+ type Props = Omit<ComponentProps<typeof DashboardListTable>, "columns"> & {
12
+ readonly columns: readonly DashboardListTableColumn[];
13
+ readonly featureName: string;
14
+ readonly translate: Translate;
15
+ };
16
+ export declare function DashboardReferenceListTable({ columns, rows, featureName, translate, ...tableProps }: Props): ReactNode;
17
+ export {};
@@ -0,0 +1,47 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
3
+ import { useMemo } from "react";
4
+ import { useReferenceLinkResolver } from "../../app/reference-link.js";
5
+ import { renderReferenceLookupBridges } from "../../components/render-list-reference-bridge.js";
6
+ import { enrichReferenceColumns, selectReferenceColumns, } from "../../components/render-list-reference-columns.js";
7
+ import { useReferenceColumnLookups } from "../../components/use-reference-column-lookups.js";
8
+ import { DashboardListTable } from "../widgets/dashboard-list.js";
9
+ function toReferenceViewColumn(column, featureName) {
10
+ if (column.reference === undefined)
11
+ return undefined;
12
+ const target = parseRefTarget(column.reference.refEntity, featureName);
13
+ return {
14
+ field: column.field,
15
+ label: column.label,
16
+ type: "reference",
17
+ sortable: false,
18
+ refEntity: target.entityName,
19
+ refFeature: target.featureName,
20
+ refLabelField: column.reference.refLabelField ?? "id",
21
+ };
22
+ }
23
+ // Dashboard list rows carry raw ids; reference columns resolve them to the target's label
24
+ // the way entity lists do.
25
+ export function DashboardReferenceListTable({ columns, rows, featureName, translate, ...tableProps }) {
26
+ const viewColumns = useMemo(() => columns.flatMap((column) => {
27
+ const viewColumn = toReferenceViewColumn(column, featureName);
28
+ return viewColumn === undefined ? [] : [viewColumn];
29
+ }), [columns, featureName]);
30
+ const referenceColumns = useMemo(() => selectReferenceColumns(viewColumns, featureName), [viewColumns, featureName]);
31
+ const { referenceRequests, handleLookupMap, referenceLookups } = useReferenceColumnLookups(rows, referenceColumns);
32
+ const resolveReferenceLink = useReferenceLinkResolver();
33
+ const enriched = useMemo(() => enrichReferenceColumns({
34
+ columns: viewColumns,
35
+ referenceColumns,
36
+ referenceLookups,
37
+ resolveReferenceLink,
38
+ featureName,
39
+ translate,
40
+ }), [viewColumns, referenceColumns, referenceLookups, resolveReferenceLink, featureName, translate]);
41
+ const tableColumns = columns.map((column) => {
42
+ const renderer = enriched.find((c) => c.field === column.field)?.renderer;
43
+ const { reference: _reference, ...base } = column;
44
+ return typeof renderer === "function" ? { ...base, renderValue: renderer } : base;
45
+ });
46
+ return (_jsxs(_Fragment, { children: [renderReferenceLookupBridges(referenceRequests, handleLookupMap), _jsx(DashboardListTable, { columns: tableColumns, rows: rows, translate: translate, ...tableProps })] }));
47
+ }
@@ -5,6 +5,8 @@ export type DashboardListColumn = {
5
5
  readonly label: string;
6
6
  readonly display?: "bar" | "badge" | "datetime";
7
7
  readonly badgeToneField?: string;
8
+ /** Resolves the raw cell value to display text, e.g. a reference id to its label. */
9
+ readonly renderValue?: (value: unknown, row: Readonly<Record<string, unknown>>) => string;
8
10
  };
9
11
  type Row = Readonly<Record<string, unknown>>;
10
12
  /** Compact 40px-row table for dashboard lists with optional bar/badge
@@ -31,6 +31,8 @@ function Cell({ column, row, formatPercent, formatNumber, formatDateTime, transl
31
31
  return EMPTY_VALUE_PLACEHOLDER;
32
32
  return _jsx("span", { className: "tabular-nums", children: formatDateTime(atMs) });
33
33
  }
34
+ if (column.renderValue !== undefined)
35
+ return column.renderValue(raw, row);
34
36
  if (typeof raw === "number")
35
37
  return _jsx("span", { className: "tabular-nums", children: formatNumber(raw) });
36
38
  if (raw === undefined || raw === null || raw === "")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.400.0",
3
+ "version": "0.401.0",
4
4
  "description": "React renderer for Kumiko screens. The root export holds the platform-agnostic logic (primitives contract, hooks, KumikoScreen, navigation and SSE abstractions); ./headless the DOM-free UI logic, ./live the HTTP dispatcher, ./web the browser bindings (HTML primitives, history navigation, EventSource live events, createKumikoApp).",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -48,8 +48,8 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@cosmicdrift/kumiko-framework": "0.400.0",
52
- "@cosmicdrift/kumiko-types": "0.400.0",
51
+ "@cosmicdrift/kumiko-framework": "0.401.0",
52
+ "@cosmicdrift/kumiko-types": "0.401.0",
53
53
  "@types/qrcode": "^1.5.5",
54
54
  "class-variance-authority": "^0.7.1",
55
55
  "clsx": "^2.1.1",
@@ -79,8 +79,8 @@
79
79
  }
80
80
  },
81
81
  "devDependencies": {
82
- "@cosmicdrift/kumiko-locale-de": "0.400.0",
83
- "@cosmicdrift/kumiko-testing": "0.400.0",
82
+ "@cosmicdrift/kumiko-locale-de": "0.401.0",
83
+ "@cosmicdrift/kumiko-testing": "0.401.0",
84
84
  "@tailwindcss/cli": "^4.3.3",
85
85
  "@testing-library/react": "^16.3.3",
86
86
  "@testing-library/user-event": "^14.6.8",
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.401.0",
4
+ "type": "fix",
5
+ "title": "Dashboard list panels show the target's label for reference columns",
6
+ "detail": "A reference column in a dashboard list panel showed the stored id. It now resolves the target record's label and links to it, using the same lookup as entity lists."
7
+ },
2
8
  {
3
9
  "version": "0.399.0",
4
10
  "type": "improvement",