@cosmicdrift/kumiko-renderer 0.203.0 → 0.204.1
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.204.1",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.204.1",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.204.1",
|
|
20
20
|
"react": "^19.2.6",
|
|
21
21
|
"temporal-polyfill": "^0.3.2",
|
|
22
22
|
"zod": "^4.4.3"
|
package/src/app/layout-fields.ts
CHANGED
|
@@ -2,19 +2,19 @@ import type {
|
|
|
2
2
|
EditFieldSpec,
|
|
3
3
|
EntityEditScreenDefinition,
|
|
4
4
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
|
-
import {
|
|
5
|
+
import { isFieldsEditSection, normalizeEditField } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
6
|
|
|
7
7
|
// Normalized field specs actually rendered by the screen's layout, extension
|
|
8
|
-
// sections skipped. Both this and `layoutFieldNames` key
|
|
9
|
-
// the layout" for the same reason: a field the user never
|
|
10
|
-
// chance to review/correct a value nor to fix a presence error
|
|
8
|
+
// (and relatedList) sections skipped. Both this and `layoutFieldNames` key
|
|
9
|
+
// off "rendered by the layout" for the same reason: a field the user never
|
|
10
|
+
// sees gets no chance to review/correct a value nor to fix a presence error
|
|
11
11
|
// (search-param merge, #1708; presence schema in form-schema.ts).
|
|
12
12
|
export function layoutEditFields(
|
|
13
13
|
screen: EntityEditScreenDefinition,
|
|
14
14
|
): readonly Exclude<EditFieldSpec, string>[] {
|
|
15
15
|
const specs: Exclude<EditFieldSpec, string>[] = [];
|
|
16
16
|
for (const section of screen.layout.sections) {
|
|
17
|
-
if (
|
|
17
|
+
if (!isFieldsEditSection(section)) continue;
|
|
18
18
|
for (const spec of section.fields) {
|
|
19
19
|
specs.push(normalizeEditField(spec));
|
|
20
20
|
}
|
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
ProjectionDetailScreenDefinition,
|
|
28
28
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
29
29
|
import {
|
|
30
|
-
|
|
30
|
+
isFieldsEditSection,
|
|
31
31
|
normalizeEditField,
|
|
32
32
|
PROJECTION_DETAIL_ENTITY as PROJECTION_DETAIL_PSEUDO_ENTITY,
|
|
33
33
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
@@ -38,7 +38,8 @@ import {
|
|
|
38
38
|
export function synthesizeProjectionDetailEntity(layout: EditLayout): EntityDefinition {
|
|
39
39
|
const fields: Record<string, { type: "text" }> = {};
|
|
40
40
|
for (const section of layout.sections) {
|
|
41
|
-
|
|
41
|
+
// relatedList and extension sections carry no `fields` to synthesize.
|
|
42
|
+
if (!isFieldsEditSection(section)) continue;
|
|
42
43
|
for (const spec of section.fields) {
|
|
43
44
|
fields[normalizeEditField(spec).field] = { type: "text" };
|
|
44
45
|
}
|
|
@@ -52,7 +53,8 @@ export function synthesizeProjectionDetailScreen(
|
|
|
52
53
|
screen: ProjectionDetailScreenDefinition,
|
|
53
54
|
): EntityEditScreenDefinition {
|
|
54
55
|
const sections = screen.layout.sections.map((section) => {
|
|
55
|
-
|
|
56
|
+
// relatedList and extension sections pass through unchanged.
|
|
57
|
+
if (!isFieldsEditSection(section)) return section;
|
|
56
58
|
return {
|
|
57
59
|
...section,
|
|
58
60
|
fields: section.fields.map((spec) => ({ ...normalizeEditField(spec), readOnly: true })),
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EntityDefinition,
|
|
3
|
+
EntityListScreenDefinition,
|
|
4
|
+
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
|
+
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
|
+
import type {
|
|
7
|
+
EditRelatedListSectionViewModel,
|
|
8
|
+
ListRowViewModel,
|
|
9
|
+
Translate,
|
|
10
|
+
} from "@cosmicdrift/kumiko-headless";
|
|
11
|
+
import { type ReactNode, useMemo } from "react";
|
|
12
|
+
import { useNav } from "../app/nav";
|
|
13
|
+
import { dispatcherErrorText } from "../app/write-failed-error";
|
|
14
|
+
import { useQuery } from "../hooks/use-query";
|
|
15
|
+
import { useTranslation } from "../i18n";
|
|
16
|
+
import { usePrimitives } from "../primitives";
|
|
17
|
+
import { RenderList } from "./render-list";
|
|
18
|
+
|
|
19
|
+
const RELATED_LIST_PSEUDO_ENTITY = "__related-list__";
|
|
20
|
+
|
|
21
|
+
// Same paged envelope as ProjectionListBody's PagedRows (kumiko-screen.tsx) —
|
|
22
|
+
// duplicated locally because that type isn't exported (projectionList and
|
|
23
|
+
// relatedList are independent query call-sites, not a shared abstraction).
|
|
24
|
+
type PagedRows = {
|
|
25
|
+
readonly rows: Readonly<Record<string, unknown>>[];
|
|
26
|
+
readonly nextCursor: string | null;
|
|
27
|
+
readonly total?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Minimal EntityDefinition from the section's own columns — same shape as
|
|
31
|
+
// projection-list-shim's synthesizeProjectionEntity, but relatedList columns
|
|
32
|
+
// aren't sortable (no sort UI on this section, see RelatedListSection below).
|
|
33
|
+
function synthesizeRelatedListEntity(
|
|
34
|
+
columns: EditRelatedListSectionViewModel["columns"],
|
|
35
|
+
): EntityDefinition {
|
|
36
|
+
const fields: Record<string, { type: "text" }> = {};
|
|
37
|
+
for (const col of columns) {
|
|
38
|
+
fields[normalizeListColumn(col).field] = { type: "text" };
|
|
39
|
+
}
|
|
40
|
+
return { fields } as unknown as EntityDefinition;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function RelatedListSection({
|
|
44
|
+
section,
|
|
45
|
+
parentId,
|
|
46
|
+
featureName,
|
|
47
|
+
translate,
|
|
48
|
+
}: {
|
|
49
|
+
readonly section: EditRelatedListSectionViewModel;
|
|
50
|
+
readonly parentId: string;
|
|
51
|
+
readonly featureName: string;
|
|
52
|
+
readonly translate?: Translate;
|
|
53
|
+
}): ReactNode {
|
|
54
|
+
const { Banner, Section } = usePrimitives();
|
|
55
|
+
const t = useTranslation();
|
|
56
|
+
const effectiveTranslate = translate ?? t;
|
|
57
|
+
const nav = useNav();
|
|
58
|
+
|
|
59
|
+
const entity = useMemo(() => synthesizeRelatedListEntity(section.columns), [section.columns]);
|
|
60
|
+
const listScreen = useMemo(
|
|
61
|
+
(): EntityListScreenDefinition => ({
|
|
62
|
+
// Empty id → RenderList's own toolbarTitle resolves to "" (its
|
|
63
|
+
// `screen:${id}.title` lookup misses and falls back to `id`) — this
|
|
64
|
+
// component renders the visible heading itself via `Section` below,
|
|
65
|
+
// so RenderList's toolbar carries none.
|
|
66
|
+
id: "",
|
|
67
|
+
type: "entityList",
|
|
68
|
+
entity: RELATED_LIST_PSEUDO_ENTITY,
|
|
69
|
+
columns: section.columns,
|
|
70
|
+
}),
|
|
71
|
+
[section.columns],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const payload = useMemo(
|
|
75
|
+
() => ({
|
|
76
|
+
[section.parentParam ?? "id"]: parentId,
|
|
77
|
+
...(section.pageSize !== undefined && { limit: section.pageSize }),
|
|
78
|
+
}),
|
|
79
|
+
[section.parentParam, section.pageSize, parentId],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const rowsQuery = useQuery<PagedRows>(section.query, payload);
|
|
83
|
+
|
|
84
|
+
const rowClick = section.rowClick;
|
|
85
|
+
const onRowClick =
|
|
86
|
+
rowClick !== undefined
|
|
87
|
+
? (row: ListRowViewModel) => {
|
|
88
|
+
const id = String(row.values[rowClick.idColumn ?? "id"] ?? "");
|
|
89
|
+
if (id === "") return;
|
|
90
|
+
nav.navigate({ entity: rowClick.entity, id });
|
|
91
|
+
}
|
|
92
|
+
: undefined;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Section title={section.title} testId={`related-list-${section.title}`}>
|
|
96
|
+
{rowsQuery.loading && rowsQuery.data === null ? (
|
|
97
|
+
<Banner padded variant="loading" testId="related-list-loading">
|
|
98
|
+
Loading…
|
|
99
|
+
</Banner>
|
|
100
|
+
) : rowsQuery.error ? (
|
|
101
|
+
<Banner padded variant="error" testId="related-list-error">
|
|
102
|
+
{dispatcherErrorText(rowsQuery.error, effectiveTranslate)}
|
|
103
|
+
</Banner>
|
|
104
|
+
) : (
|
|
105
|
+
<RenderList
|
|
106
|
+
screen={listScreen}
|
|
107
|
+
entity={entity}
|
|
108
|
+
rows={rowsQuery.data?.rows ?? []}
|
|
109
|
+
featureName={featureName}
|
|
110
|
+
translate={effectiveTranslate}
|
|
111
|
+
{...(onRowClick !== undefined && { onRowClick })}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
</Section>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -40,9 +40,9 @@ export function shouldNotifyCaller(
|
|
|
40
40
|
return !(result.isSuccess && !extensionsPersisted);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Extension sections skip the `fields` filter (
|
|
44
|
-
//
|
|
45
|
-
// filtering is dropped, not rendered empty.
|
|
43
|
+
// Extension and relatedList sections skip the `fields` filter (neither has a
|
|
44
|
+
// `field`-name set that filtering applies to); a `fields` section left with
|
|
45
|
+
// zero fields after filtering is dropped, not rendered empty.
|
|
46
46
|
export function filterEditSections(
|
|
47
47
|
sections: readonly EditSectionViewModel[],
|
|
48
48
|
fieldsFilter: readonly string[] | undefined,
|
|
@@ -51,7 +51,7 @@ export function filterEditSections(
|
|
|
51
51
|
const filterSet = new Set(fieldsFilter);
|
|
52
52
|
const result: EditSectionViewModel[] = [];
|
|
53
53
|
for (const section of sections) {
|
|
54
|
-
if (section.kind === "extension") {
|
|
54
|
+
if (section.kind === "extension" || section.kind === "relatedList") {
|
|
55
55
|
result.push(section);
|
|
56
56
|
continue;
|
|
57
57
|
}
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
6
|
import {
|
|
7
7
|
evalFieldCondition,
|
|
8
|
-
|
|
8
|
+
isFieldsEditSection,
|
|
9
9
|
normalizeEditField,
|
|
10
10
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
11
11
|
import type {
|
|
@@ -31,6 +31,7 @@ import { formatWhen } from "../format-when";
|
|
|
31
31
|
import { useForm } from "../hooks/use-form";
|
|
32
32
|
import { useTranslation } from "../i18n";
|
|
33
33
|
import { usePrimitives } from "../primitives";
|
|
34
|
+
import { RelatedListSection } from "./related-list-section";
|
|
34
35
|
import {
|
|
35
36
|
filterEditSections,
|
|
36
37
|
hasEditableSection,
|
|
@@ -253,7 +254,8 @@ function deriveFormFields<TValues extends FormValues, TCtx>(
|
|
|
253
254
|
): Record<string, FieldConditions<TValues, TCtx>> {
|
|
254
255
|
const out: Record<string, FieldConditions<TValues, TCtx>> = {};
|
|
255
256
|
for (const section of screen.layout.sections) {
|
|
256
|
-
|
|
257
|
+
// relatedList carries no `fields` for the form-condition map either.
|
|
258
|
+
if (!isFieldsEditSection(section)) continue;
|
|
257
259
|
for (const spec of section.fields) {
|
|
258
260
|
const normalized = normalizeEditField(spec);
|
|
259
261
|
out[normalized.field] = {
|
|
@@ -786,14 +788,19 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
786
788
|
const filteredSections = useMemo(
|
|
787
789
|
// A fully-hidden "fields" section (every field in it currently
|
|
788
790
|
// condition-hidden) must not occupy a wizard step; it would render
|
|
789
|
-
// empty and block Back/Next on nothing. Extension
|
|
790
|
-
// `visible` (they own their own
|
|
791
|
-
// pass through. A section with no
|
|
792
|
-
// step) has `visible: fields.some(...)`
|
|
793
|
-
// fields to hide", not "hidden", so it
|
|
791
|
+
// empty and block Back/Next on nothing. Extension and relatedList
|
|
792
|
+
// sections carry no `visible` (they own their own lifecycle / run
|
|
793
|
+
// their own query), so they always pass through. A section with no
|
|
794
|
+
// fields at all (e.g. a review-only step) has `visible: fields.some(...)`
|
|
795
|
+
// = false vacuously; that's "no fields to hide", not "hidden", so it
|
|
796
|
+
// stays too (fw#1901).
|
|
794
797
|
() =>
|
|
795
798
|
filterEditSections(vm.sections, fieldsFilter).filter(
|
|
796
|
-
(section) =>
|
|
799
|
+
(section) =>
|
|
800
|
+
section.kind === "extension" ||
|
|
801
|
+
section.kind === "relatedList" ||
|
|
802
|
+
section.fields.length === 0 ||
|
|
803
|
+
section.visible,
|
|
797
804
|
),
|
|
798
805
|
[vm.sections, fieldsFilter],
|
|
799
806
|
);
|
|
@@ -1302,6 +1309,22 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
1302
1309
|
</WizardStepGroup>
|
|
1303
1310
|
);
|
|
1304
1311
|
}
|
|
1312
|
+
if (section.kind === "relatedList") {
|
|
1313
|
+
// parentId is the displayed record's id — without it there's no
|
|
1314
|
+
// parent row whose related rows could be queried (fw#2166).
|
|
1315
|
+
// Rejected at boot in wizard layouts, so no WizardStepGroup here.
|
|
1316
|
+
const parentId = resolveExtensionEntityId(entityIdProp, vm.id);
|
|
1317
|
+
if (parentId === null) return null;
|
|
1318
|
+
return (
|
|
1319
|
+
<RelatedListSection
|
|
1320
|
+
key={section.title}
|
|
1321
|
+
section={section}
|
|
1322
|
+
parentId={parentId}
|
|
1323
|
+
featureName={featureName}
|
|
1324
|
+
translate={translate}
|
|
1325
|
+
/>
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1305
1328
|
if (!section.visible) return null;
|
|
1306
1329
|
// Section-Header unterdrücken wenn er den Form-Titel der
|
|
1307
1330
|
// Action-Bar 1:1 wiederholen würde (typisch bei Single-Section-
|
package/src/index.ts
CHANGED
|
@@ -77,6 +77,7 @@ export { lastSegment } from "./app/qn";
|
|
|
77
77
|
export type { VariableChipsProps } from "./app/variable-chips";
|
|
78
78
|
export { VariableChips } from "./app/variable-chips";
|
|
79
79
|
export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error";
|
|
80
|
+
export { RelatedListSection } from "./components/related-list-section";
|
|
80
81
|
export type {
|
|
81
82
|
RenderEditAction,
|
|
82
83
|
RenderEditChangeState,
|