@cosmicdrift/kumiko-renderer 0.114.0 → 1.0.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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": "1.0.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "1.0.0",
|
|
20
20
|
"react": "^19.2.6"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
EntityDefinition,
|
|
6
6
|
EntityEditScreenDefinition,
|
|
7
7
|
EntityListScreenDefinition,
|
|
8
|
-
ProjectionListScreenDefinition,
|
|
9
8
|
RowAction,
|
|
10
9
|
RowActionNavigate,
|
|
11
10
|
RowActionWriteHandler,
|
|
@@ -36,7 +35,6 @@ import { synthesizeConfigEditEntity, synthesizeConfigEditScreen } from "./config
|
|
|
36
35
|
import { useCustomScreenComponent } from "./custom-screens";
|
|
37
36
|
import type { FeatureSchema } from "./feature-schema";
|
|
38
37
|
import { useNav } from "./nav";
|
|
39
|
-
import { synthesizeProjectionEntity, synthesizeProjectionScreen } from "./projection-list-shim";
|
|
40
38
|
import { lastSegment } from "./qn";
|
|
41
39
|
import { dispatcherErrorText, WriteFailedError } from "./write-failed-error";
|
|
42
40
|
|
|
@@ -137,15 +135,6 @@ export function KumikoScreen({
|
|
|
137
135
|
{...(onRowClick !== undefined && { onRowClick })}
|
|
138
136
|
/>
|
|
139
137
|
);
|
|
140
|
-
case "projectionList":
|
|
141
|
-
return (
|
|
142
|
-
<ProjectionListBody
|
|
143
|
-
schema={schema}
|
|
144
|
-
screen={screen}
|
|
145
|
-
translate={translate}
|
|
146
|
-
{...(onRowClick !== undefined && { onRowClick })}
|
|
147
|
-
/>
|
|
148
|
-
);
|
|
149
138
|
case "actionForm":
|
|
150
139
|
return <ActionFormBody schema={schema} screen={screen} translate={translate} />;
|
|
151
140
|
case "configEdit":
|
|
@@ -1006,131 +995,6 @@ function EntityListBody({
|
|
|
1006
995
|
);
|
|
1007
996
|
}
|
|
1008
997
|
|
|
1009
|
-
// ---- projection-list ----
|
|
1010
|
-
|
|
1011
|
-
// Wie entityList, aber die List-Query kommt DIREKT aus `screen.query` (statt aus
|
|
1012
|
-
// der Entity abgeleitet) — dadurch cross-feature-fähig. v1 bewusst schlank:
|
|
1013
|
-
// rendert die Query-Rows mit den (explizit gelabelten) Columns + navigate-
|
|
1014
|
-
// RowActions/Row-Klick. Kein Server-Sort/-Pagination/-Facetten (eine Projection-
|
|
1015
|
-
// Query hat dafür keinen garantierten Contract) — die kommen bei Bedarf später.
|
|
1016
|
-
function ProjectionListBody({
|
|
1017
|
-
schema,
|
|
1018
|
-
screen,
|
|
1019
|
-
translate,
|
|
1020
|
-
onRowClick,
|
|
1021
|
-
}: {
|
|
1022
|
-
readonly schema: FeatureSchema;
|
|
1023
|
-
readonly screen: ProjectionListScreenDefinition;
|
|
1024
|
-
readonly translate?: Translate;
|
|
1025
|
-
readonly onRowClick?: (row: ListRowViewModel, entityName: string) => void;
|
|
1026
|
-
}): ReactNode {
|
|
1027
|
-
const { Banner } = usePrimitives();
|
|
1028
|
-
const t = useTranslation();
|
|
1029
|
-
const nav = useNav();
|
|
1030
|
-
const effectiveTranslate = translate ?? t;
|
|
1031
|
-
const entity = useMemo(() => synthesizeProjectionEntity(screen.columns), [screen.columns]);
|
|
1032
|
-
const listScreen = useMemo(() => synthesizeProjectionScreen(screen), [screen]);
|
|
1033
|
-
const rowsQuery = useQuery<PagedRows>(screen.query, {}, { live: true });
|
|
1034
|
-
|
|
1035
|
-
const runNavigate = useCallback(
|
|
1036
|
-
(action: RowActionNavigate, row: ListRowViewModel) => {
|
|
1037
|
-
const entityId =
|
|
1038
|
-
action.entityId !== undefined ? String(row.values[action.entityId] ?? "") : undefined;
|
|
1039
|
-
nav.navigate({
|
|
1040
|
-
screenId: action.screen,
|
|
1041
|
-
...(entityId !== undefined && entityId !== "" && { entityId }),
|
|
1042
|
-
});
|
|
1043
|
-
const params =
|
|
1044
|
-
action.params !== undefined ? evalRowExtractor(action.params, row.values) : undefined;
|
|
1045
|
-
if (params !== undefined) {
|
|
1046
|
-
const stringified: Record<string, string | null> = {};
|
|
1047
|
-
for (const [k, v] of Object.entries(params)) {
|
|
1048
|
-
stringified[k] = v === null || v === undefined ? null : String(v);
|
|
1049
|
-
}
|
|
1050
|
-
nav.setSearchParams(stringified);
|
|
1051
|
-
}
|
|
1052
|
-
},
|
|
1053
|
-
[nav],
|
|
1054
|
-
);
|
|
1055
|
-
|
|
1056
|
-
const rowActions = useMemo((): readonly DataTableRowAction[] | undefined => {
|
|
1057
|
-
if (screen.rowActions === undefined) return undefined;
|
|
1058
|
-
const out: DataTableRowAction[] = [];
|
|
1059
|
-
for (const action of screen.rowActions) {
|
|
1060
|
-
// v1: nur navigate (writeHandler-RowActions bräuchten den Dispatcher-Pfad).
|
|
1061
|
-
if (action.kind !== "navigate") continue;
|
|
1062
|
-
const navigateAction = action;
|
|
1063
|
-
const visible = action.visible;
|
|
1064
|
-
out.push({
|
|
1065
|
-
id: action.id,
|
|
1066
|
-
label: effectiveTranslate(action.label),
|
|
1067
|
-
...(action.style !== undefined && { style: action.style }),
|
|
1068
|
-
onTrigger: (row: ListRowViewModel) => runNavigate(navigateAction, row),
|
|
1069
|
-
...(visible !== undefined && {
|
|
1070
|
-
isVisible: (row: ListRowViewModel) => evalFieldCondition(visible, row.values),
|
|
1071
|
-
}),
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
return out.length > 0 ? out : undefined;
|
|
1075
|
-
}, [screen.rowActions, effectiveTranslate, runNavigate]);
|
|
1076
|
-
|
|
1077
|
-
const toolbarActions = useMemo((): readonly ToolbarActionButton[] | undefined => {
|
|
1078
|
-
if (screen.toolbarActions === undefined) return undefined;
|
|
1079
|
-
const out: ToolbarActionButton[] = [];
|
|
1080
|
-
for (const action of screen.toolbarActions) {
|
|
1081
|
-
// v1: nur navigate (writeHandler-Toolbar bräuchte den Dispatcher-Pfad).
|
|
1082
|
-
if (action.kind !== "navigate") continue;
|
|
1083
|
-
const target = action.screen;
|
|
1084
|
-
out.push({
|
|
1085
|
-
id: action.id,
|
|
1086
|
-
label: effectiveTranslate(action.label),
|
|
1087
|
-
...(action.style !== undefined && { style: action.style }),
|
|
1088
|
-
onTrigger: () => nav.navigate({ screenId: target }),
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
return out.length > 0 ? out : undefined;
|
|
1092
|
-
}, [screen.toolbarActions, effectiveTranslate, nav]);
|
|
1093
|
-
|
|
1094
|
-
if (rowsQuery.loading && rowsQuery.data === null) {
|
|
1095
|
-
return (
|
|
1096
|
-
<Banner padded variant="loading" testId="kumiko-screen-loading">
|
|
1097
|
-
Loading…
|
|
1098
|
-
</Banner>
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
if (rowsQuery.error) {
|
|
1102
|
-
return (
|
|
1103
|
-
<Banner padded variant="error" testId="kumiko-screen-error">
|
|
1104
|
-
{rowsQuery.error.i18nKey}
|
|
1105
|
-
</Banner>
|
|
1106
|
-
);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const rowClickAction = screen.rowActions?.find(
|
|
1110
|
-
(a): a is RowActionNavigate => a.kind === "navigate" && a.rowClick === true,
|
|
1111
|
-
);
|
|
1112
|
-
const wrappedOnRowClick = rowClickAction
|
|
1113
|
-
? (row: ListRowViewModel) => runNavigate(rowClickAction, row)
|
|
1114
|
-
: onRowClick !== undefined
|
|
1115
|
-
? (row: ListRowViewModel) => onRowClick(row, listScreen.entity)
|
|
1116
|
-
: undefined;
|
|
1117
|
-
|
|
1118
|
-
return (
|
|
1119
|
-
<RenderList
|
|
1120
|
-
screen={listScreen}
|
|
1121
|
-
entity={entity}
|
|
1122
|
-
rows={rowsQuery.data?.rows ?? []}
|
|
1123
|
-
featureName={schema.featureName}
|
|
1124
|
-
searchable={screen.searchable ?? false}
|
|
1125
|
-
sort={screen.defaultSort ?? null}
|
|
1126
|
-
{...(rowActions !== undefined && { rowActions })}
|
|
1127
|
-
{...(toolbarActions !== undefined && { toolbarActions })}
|
|
1128
|
-
{...(translate !== undefined && { translate })}
|
|
1129
|
-
{...(wrappedOnRowClick !== undefined && { onRowClick: wrappedOnRowClick })}
|
|
1130
|
-
/>
|
|
1131
|
-
);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
998
|
// ---- actionForm (Tier 2.7d) ----
|
|
1135
999
|
|
|
1136
1000
|
// Action-Form-Body — non-CRUD Write-Handler-driven Form. Re-uses
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { EditSectionViewModel, SubmitResult } from "@cosmicdrift/kumiko-headless";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// without anyone deciding that on purpose.
|
|
3
|
+
// A read-only inspector form — every field readOnly, no editable section — has
|
|
4
|
+
// nothing to submit, so the renderer drops the Save button rather than show a
|
|
5
|
+
// dead disabled one. An extension section carries its own dirty/save, so it
|
|
6
|
+
// counts as editable.
|
|
8
7
|
export function hasEditableSection(sections: readonly EditSectionViewModel[]): boolean {
|
|
9
|
-
return sections.some(
|
|
10
|
-
(s) => s.kind === "extension" || (s.kind === "fields" && s.fields.some((f) => !f.readOnly)),
|
|
11
|
-
);
|
|
8
|
+
return sections.some((s) => s.kind !== "fields" || s.fields.some((f) => !f.readOnly));
|
|
12
9
|
}
|
|
13
10
|
|
|
14
11
|
// Single source of truth for the extension-section entity-id. The section mount
|
|
@@ -247,9 +247,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
247
247
|
[screen, entity, snapshot.values, translate, featureName],
|
|
248
248
|
);
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
// section with no fields of its own too (it carries its own dirty/save).
|
|
252
|
-
const isFormEditable = hasEditableSection(vm.sections);
|
|
250
|
+
const hasEditableField = hasEditableSection(vm.sections);
|
|
253
251
|
|
|
254
252
|
// Persistiert alle composed Extension-Sections mit der aufgelösten entityId.
|
|
255
253
|
// false = eine Section schlug fehl (ihr i18n-Key landet im Banner). Ohne
|
|
@@ -357,7 +355,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
357
355
|
{translate("kumiko.actions.cancel")}
|
|
358
356
|
</Button>
|
|
359
357
|
)}
|
|
360
|
-
{
|
|
358
|
+
{hasEditableField && (
|
|
361
359
|
<Button
|
|
362
360
|
type="submit"
|
|
363
361
|
disabled={(snapshot.isUnchanged && !extensionDirty) || isSubmitting}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// Shim für ProjectionList-Renderer (analog action-form-shim.ts / config-edit-shim.ts).
|
|
2
|
-
//
|
|
3
|
-
// RenderList + computeListViewModel verlangen ein `entity: EntityDefinition` +
|
|
4
|
-
// ein `screen: EntityListScreenDefinition`-Pair, nutzen davon aber nur
|
|
5
|
-
// `entity.fields` (Spalten-Typ/Label) und die List-Felder des Screens
|
|
6
|
-
// (columns/rowActions/…). Ein projectionList-Screen ist query-getrieben und hat
|
|
7
|
-
// KEINE Entity — die zwei Helper hier shapen den Input ad-hoc um, damit die
|
|
8
|
-
// bestehende List-Maschinerie reused werden kann. Die Query selbst wird NICHT
|
|
9
|
-
// hierüber aufgelöst (der Body nimmt `screen.query` direkt).
|
|
10
|
-
//
|
|
11
|
-
// Selbe Schulden-Reservation wie bei den anderen Shims: greift die List-
|
|
12
|
-
// Maschinerie künftig auf weitere EntityDefinition-Felder (transitions, idType,
|
|
13
|
-
// derivedFields) oder cross-referenziert ein Boot-Validator die schema.entities-
|
|
14
|
-
// Map, brechen die Type-Lies hier silent — dann ist Zeit für eine echte
|
|
15
|
-
// query-native ListView-Komponente.
|
|
16
|
-
|
|
17
|
-
import type {
|
|
18
|
-
EntityDefinition,
|
|
19
|
-
EntityListScreenDefinition,
|
|
20
|
-
ListColumnSpec,
|
|
21
|
-
ProjectionListScreenDefinition,
|
|
22
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
23
|
-
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
24
|
-
|
|
25
|
-
const PROJECTION_PSEUDO_ENTITY = "__projection__";
|
|
26
|
-
|
|
27
|
-
/** Minimale EntityDefinition aus den Column-Feldern: jedes Feld ein Text-Feld,
|
|
28
|
-
* nicht sortierbar (eine Projection-Query hat keinen garantierten Server-Sort).
|
|
29
|
-
* computeListViewModel liest nur `fields[<col>].type` → Text reicht; die
|
|
30
|
-
* Präsentation kommt aus dem Column-Renderer + explizitem Label. */
|
|
31
|
-
export function synthesizeProjectionEntity(columns: readonly ListColumnSpec[]): EntityDefinition {
|
|
32
|
-
const fields: Record<string, { type: "text"; sortable: false }> = {};
|
|
33
|
-
for (const col of columns) {
|
|
34
|
-
fields[normalizeListColumn(col).field] = { type: "text", sortable: false };
|
|
35
|
-
}
|
|
36
|
-
return { fields } as unknown as EntityDefinition;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Wandelt ein ProjectionListScreenDefinition in die EntityListScreen-Shape die
|
|
40
|
-
* RenderList erwartet. `type` = "entityList" + Pseudo-Entity halten den Type-
|
|
41
|
-
* Constraint; RenderList branched nicht auf `type` und liest `entity` nur für
|
|
42
|
-
* Fehlermeldungen. Die List-Felder werden 1:1 durchgereicht. */
|
|
43
|
-
export function synthesizeProjectionScreen(
|
|
44
|
-
screen: ProjectionListScreenDefinition,
|
|
45
|
-
): EntityListScreenDefinition {
|
|
46
|
-
return {
|
|
47
|
-
id: screen.id,
|
|
48
|
-
type: "entityList",
|
|
49
|
-
entity: PROJECTION_PSEUDO_ENTITY,
|
|
50
|
-
columns: screen.columns,
|
|
51
|
-
...(screen.rowRenderer !== undefined && { rowRenderer: screen.rowRenderer }),
|
|
52
|
-
...(screen.cardRenderer !== undefined && { cardRenderer: screen.cardRenderer }),
|
|
53
|
-
...(screen.rowActions !== undefined && { rowActions: screen.rowActions }),
|
|
54
|
-
...(screen.toolbarActions !== undefined && { toolbarActions: screen.toolbarActions }),
|
|
55
|
-
...(screen.pagination !== undefined && { pagination: screen.pagination }),
|
|
56
|
-
...(screen.pageSize !== undefined && { pageSize: screen.pageSize }),
|
|
57
|
-
...(screen.defaultSort !== undefined && { defaultSort: screen.defaultSort }),
|
|
58
|
-
...(screen.searchable !== undefined && { searchable: screen.searchable }),
|
|
59
|
-
...(screen.slots !== undefined && { slots: screen.slots }),
|
|
60
|
-
...(screen.access !== undefined && { access: screen.access }),
|
|
61
|
-
};
|
|
62
|
-
}
|