@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.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/dist/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
- package/dist/page-header-slot.d.ts +2 -0
- package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3801
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -86
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -640
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1733
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -88
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -491
- package/src/components/render-edit-action-button.tsx +0 -109
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -226
- package/src/components/render-edit.tsx +0 -1871
- package/src/components/render-field.tsx +0 -951
- package/src/components/render-list.tsx +0 -593
- package/src/components/write-form-section.tsx +0 -198
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-query.test.tsx +0 -149
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -207
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -166
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -240
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -279
- package/src/primitives.tsx +0 -1567
- package/src/screen-fills-height.ts +0 -3
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// Read-only render of a content editor's format with the collection's
|
|
2
|
-
// example variableSchema values substituted in for `{{name}}` — reuses the
|
|
3
|
-
// same registered editor the collection edits with (readOnly), so "rich"
|
|
4
|
-
// renders formatted HTML and "plain"/"markdown" render as text through the
|
|
5
|
-
// exact same component, no separate render path per format.
|
|
6
|
-
|
|
7
|
-
import type { ContentEditorFormat } from "@cosmicdrift/kumiko-types/nav";
|
|
8
|
-
import { type ReactNode, useId } from "react";
|
|
9
|
-
import { useContentEditor } from "./content-editors";
|
|
10
|
-
|
|
11
|
-
const VARIABLE_PATTERN = /\{\{\s*(\w+)\s*\}\}/g;
|
|
12
|
-
|
|
13
|
-
const noop = (): void => {};
|
|
14
|
-
|
|
15
|
-
/** A name with no example value (or not in the schema at all) stays as the
|
|
16
|
-
* literal `{{name}}` placeholder — there is nothing else to show for it. */
|
|
17
|
-
export function substituteVariables(
|
|
18
|
-
content: string,
|
|
19
|
-
variables: Readonly<Record<string, string>>,
|
|
20
|
-
): string {
|
|
21
|
-
return content.replace(VARIABLE_PATTERN, (match, name: string) =>
|
|
22
|
-
Object.hasOwn(variables, name) ? (variables[name] ?? match) : match,
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function escapeHtml(value: string): string {
|
|
27
|
-
return value
|
|
28
|
-
.replace(/&/g, "&")
|
|
29
|
-
.replace(/</g, "<")
|
|
30
|
-
.replace(/>/g, ">")
|
|
31
|
-
.replace(/"/g, """)
|
|
32
|
-
.replace(/'/g, "'");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type ContentPreviewProps = {
|
|
36
|
-
readonly content: string;
|
|
37
|
-
readonly variables: Readonly<Record<string, string>>;
|
|
38
|
-
readonly contentFormat?: ContentEditorFormat;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export function ContentPreview({
|
|
42
|
-
content,
|
|
43
|
-
variables,
|
|
44
|
-
contentFormat,
|
|
45
|
-
}: ContentPreviewProps): ReactNode {
|
|
46
|
-
const Editor = useContentEditor(contentFormat);
|
|
47
|
-
const id = useId();
|
|
48
|
-
// "rich" content is HTML (see ContentCollectionDefinition.contentFormat) —
|
|
49
|
-
// an example value substituted in raw could break the markup (`<`) or
|
|
50
|
-
// render as an unescaped entity (`&`). "plain"/"markdown" content is text,
|
|
51
|
-
// no escaping wanted there.
|
|
52
|
-
const safeVariables =
|
|
53
|
-
contentFormat === "rich"
|
|
54
|
-
? Object.fromEntries(
|
|
55
|
-
Object.entries(variables).map(([name, value]) => [name, escapeHtml(value)]),
|
|
56
|
-
)
|
|
57
|
-
: variables;
|
|
58
|
-
return (
|
|
59
|
-
<Editor
|
|
60
|
-
id={id}
|
|
61
|
-
value={substituteVariables(content, safeVariables)}
|
|
62
|
-
onChange={noop}
|
|
63
|
-
variables={[]}
|
|
64
|
-
readOnly
|
|
65
|
-
/>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// Custom-Screen-Components-Map: client-side Lookup für Screens vom Typ
|
|
2
|
-
// `custom`. KumikoScreen schaut hier nach `screen.id` (oder qn) und
|
|
3
|
-
// rendert die passende Component. createKumikoApp im renderer-web sammelt
|
|
4
|
-
// alle clientFeatures.components und mountet den Provider; Tests die
|
|
5
|
-
// einzelne Custom-Screens prüfen wollen, mounten den Provider direkt.
|
|
6
|
-
|
|
7
|
-
import { type ComponentType, createContext, type ReactNode, useContext } from "react";
|
|
8
|
-
|
|
9
|
-
export type CustomScreensMap = Readonly<Record<string, ComponentType>>;
|
|
10
|
-
|
|
11
|
-
const CustomScreensContext = createContext<CustomScreensMap | undefined>(undefined);
|
|
12
|
-
|
|
13
|
-
export type CustomScreensProviderProps = {
|
|
14
|
-
readonly children: ReactNode;
|
|
15
|
-
readonly value: CustomScreensMap;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function CustomScreensProvider({ children, value }: CustomScreensProviderProps): ReactNode {
|
|
19
|
-
return <CustomScreensContext.Provider value={value}>{children}</CustomScreensContext.Provider>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Schaut die Component für ein Custom-Screen nach. Returnt undefined
|
|
23
|
-
* wenn weder Provider gemounted noch screenId in der Map ist — der
|
|
24
|
-
* Caller (KumikoScreen) zeigt dann seinen Placeholder-Banner. */
|
|
25
|
-
export function useCustomScreenComponent(screenId: string): ComponentType | undefined {
|
|
26
|
-
const map = useContext(CustomScreensContext);
|
|
27
|
-
if (map === undefined) return undefined;
|
|
28
|
-
return map[screenId];
|
|
29
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// Dashboard-Body-Injection: der KumikoScreen-Switch ist plattform-agnostisch,
|
|
2
|
-
// die Dashboard-Panels (StatCard, Charts) sind es nicht — die Implementierung
|
|
3
|
-
// kommt vom Platform-Package (renderer-web registriert seine Web-Variante in
|
|
4
|
-
// createKumikoApp). Gleiches Muster wie CustomScreensProvider.
|
|
5
|
-
|
|
6
|
-
import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
7
|
-
import type { Translate } from "@cosmicdrift/kumiko-headless";
|
|
8
|
-
import { type ComponentType, createContext, type ReactNode, useContext } from "react";
|
|
9
|
-
|
|
10
|
-
export type DashboardBodyProps = {
|
|
11
|
-
readonly featureName: string;
|
|
12
|
-
readonly screen: DashboardScreenDefinition;
|
|
13
|
-
readonly translate?: Translate;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const DashboardBodyContext = createContext<ComponentType<DashboardBodyProps> | undefined>(
|
|
17
|
-
undefined,
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
export type DashboardBodyProviderProps = {
|
|
21
|
-
readonly children: ReactNode;
|
|
22
|
-
readonly value: ComponentType<DashboardBodyProps>;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export function DashboardBodyProvider({ children, value }: DashboardBodyProviderProps): ReactNode {
|
|
26
|
-
return <DashboardBodyContext.Provider value={value}>{children}</DashboardBodyContext.Provider>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Undefined wenn kein Platform-Package einen Dashboard-Body registriert
|
|
30
|
-
* hat — KumikoScreen zeigt dann seinen Placeholder-Banner. */
|
|
31
|
-
export function useDashboardBody(): ComponentType<DashboardBodyProps> | undefined {
|
|
32
|
-
return useContext(DashboardBodyContext);
|
|
33
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
// Extension-Form-Submit-Registry — erlaubt einer Extension-Section (z.B.
|
|
3
|
-
// Custom-Fields), beim Submit des umgebenden entityEdit-Forms mitzuschreiben,
|
|
4
|
-
// statt einen eigenen Save-Button zu führen ("composed form, ein Save",
|
|
5
|
-
// Bug-Bash 3 #1). Die Section meldet (a) einen Submit-Handler und (b) ihren
|
|
6
|
-
// dirty-State an die Form; die Form aktiviert ihren Save-Button auch wenn nur
|
|
7
|
-
// eine Section dirty ist und ruft nach dem Entity-Write alle Section-Handler
|
|
8
|
-
// mit der entityId.
|
|
9
|
-
//
|
|
10
|
-
// Ohne umgebende Form (Context === null) fällt die Section auf ihren eigenen
|
|
11
|
-
// Save-Button zurück (Standalone-Backward-Compat).
|
|
12
|
-
|
|
13
|
-
import { createContext, useContext, useEffect, useId, useMemo, useRef } from "react";
|
|
14
|
-
|
|
15
|
-
export type ExtensionSubmitContext = { readonly entityId: string };
|
|
16
|
-
|
|
17
|
-
export type ExtensionSubmitResult = {
|
|
18
|
-
readonly isSuccess: boolean;
|
|
19
|
-
/** i18n-Key für die Fehlermeldung, die die Form als Banner zeigt. */
|
|
20
|
-
readonly errorKey?: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type ExtensionFormSubmitHandler = (
|
|
24
|
-
ctx: ExtensionSubmitContext,
|
|
25
|
-
) => Promise<ExtensionSubmitResult>;
|
|
26
|
-
|
|
27
|
-
type Registration = {
|
|
28
|
-
readonly key: string;
|
|
29
|
-
readonly dirty: boolean;
|
|
30
|
-
readonly handler: ExtensionFormSubmitHandler;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export type ExtensionFormRegistry = {
|
|
34
|
-
readonly upsert: (reg: Registration) => void;
|
|
35
|
-
readonly remove: (key: string) => void;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const ExtensionFormRegistryContext = createContext<ExtensionFormRegistry | null>(null);
|
|
39
|
-
|
|
40
|
-
export const ExtensionFormRegistryProvider = ExtensionFormRegistryContext.Provider;
|
|
41
|
-
|
|
42
|
-
// Host-Seite (render-edit): hält die Registrierungen in einem ref, meldet den
|
|
43
|
-
// aggregierten dirty-State via onDirtyChange hoch (damit der Save-Button
|
|
44
|
-
// re-rendert) und liefert runAll() zum Ausführen aller Handler beim Submit.
|
|
45
|
-
export type ExtensionFormHostStatus = {
|
|
46
|
-
readonly anyDirty: boolean;
|
|
47
|
-
readonly hasRegistrations: boolean;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export function useExtensionFormHost(onStatusChange: (status: ExtensionFormHostStatus) => void): {
|
|
51
|
-
readonly registry: ExtensionFormRegistry;
|
|
52
|
-
readonly runAll: (ctx: ExtensionSubmitContext) => Promise<readonly ExtensionSubmitResult[]>;
|
|
53
|
-
} {
|
|
54
|
-
const regsRef = useRef<Map<string, Registration>>(new Map());
|
|
55
|
-
const onStatusRef = useRef(onStatusChange);
|
|
56
|
-
onStatusRef.current = onStatusChange;
|
|
57
|
-
|
|
58
|
-
const registry = useMemo<ExtensionFormRegistry>(() => {
|
|
59
|
-
const emitDirty = (): void => {
|
|
60
|
-
let any = false;
|
|
61
|
-
for (const r of regsRef.current.values()) {
|
|
62
|
-
if (r.dirty) {
|
|
63
|
-
any = true;
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
onStatusRef.current({ anyDirty: any, hasRegistrations: regsRef.current.size > 0 });
|
|
68
|
-
};
|
|
69
|
-
return {
|
|
70
|
-
upsert: (reg) => {
|
|
71
|
-
regsRef.current.set(reg.key, reg);
|
|
72
|
-
emitDirty();
|
|
73
|
-
},
|
|
74
|
-
remove: (key) => {
|
|
75
|
-
regsRef.current.delete(key);
|
|
76
|
-
emitDirty();
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
const runAll = useMemo(
|
|
82
|
-
() =>
|
|
83
|
-
async (ctx: ExtensionSubmitContext): Promise<readonly ExtensionSubmitResult[]> => {
|
|
84
|
-
const results: ExtensionSubmitResult[] = [];
|
|
85
|
-
// Insertion-Order (Map) = Section-Reihenfolge im Form.
|
|
86
|
-
for (const reg of regsRef.current.values()) {
|
|
87
|
-
results.push(await reg.handler(ctx));
|
|
88
|
-
}
|
|
89
|
-
return results;
|
|
90
|
-
},
|
|
91
|
-
[],
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
return { registry, runAll };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Section-Seite: meldet Handler + dirty an die umgebende Form. Returnt true
|
|
98
|
-
// wenn eine composed-Form vorhanden ist (Section blendet dann ihren eigenen
|
|
99
|
-
// Save-Button aus); false = standalone (eigener Save-Button bleibt).
|
|
100
|
-
export function useExtensionFormSubmit(opts: {
|
|
101
|
-
readonly dirty: boolean;
|
|
102
|
-
readonly onSubmit: ExtensionFormSubmitHandler;
|
|
103
|
-
}): boolean {
|
|
104
|
-
const registry = useContext(ExtensionFormRegistryContext);
|
|
105
|
-
const key = useId();
|
|
106
|
-
// Handler über ref, damit das Re-Registrieren NICHT an jeder Handler-
|
|
107
|
-
// Identity hängt (nur an dirty); der Handler liest immer den frischen
|
|
108
|
-
// Closure-State (pending) zur Aufruf-Zeit.
|
|
109
|
-
const handlerRef = useRef(opts.onSubmit);
|
|
110
|
-
handlerRef.current = opts.onSubmit;
|
|
111
|
-
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
if (registry === null) return;
|
|
114
|
-
registry.upsert({
|
|
115
|
-
key,
|
|
116
|
-
dirty: opts.dirty,
|
|
117
|
-
handler: (ctx) => handlerRef.current(ctx),
|
|
118
|
-
});
|
|
119
|
-
return () => registry.remove(key);
|
|
120
|
-
}, [registry, key, opts.dirty]);
|
|
121
|
-
|
|
122
|
-
return registry !== null;
|
|
123
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// Extension-Section-Components-Map: client-side Lookup für entityEdit
|
|
2
|
-
// sections vom Type `extension`, List-Header-Slots UND Dashboard-`custom`-
|
|
3
|
-
// Panels. Jeder Mount-Ort löst den `__component`-Namen aus einer
|
|
4
|
-
// PlatformComponent über dieselbe Registry auf und mountet die passende
|
|
5
|
-
// Component — die Bundled-Feature-/App-Component lädt + persistiert dann
|
|
6
|
-
// ihre eigenen Daten (z.B. custom-fields, oder ein eigenständiger Chart).
|
|
7
|
-
// The entityEdit footer slot (`screen.slots.footer`) mounts through it too.
|
|
8
|
-
//
|
|
9
|
-
// Mounting analog zu CustomScreensProvider — createKumikoApp im
|
|
10
|
-
// renderer-web sammelt alle clientFeatures.extensionSectionComponents und
|
|
11
|
-
// mountet den Provider; Tests die einzelne Sections prüfen wollen
|
|
12
|
-
// mounten den Provider direkt.
|
|
13
|
-
|
|
14
|
-
import type { PlatformComponent } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
15
|
-
import { type ComponentType, createContext, type ReactNode, useContext } from "react";
|
|
16
|
-
|
|
17
|
-
/** Extrahiert den `__component`-Namen aus einer PlatformComponent. Liest
|
|
18
|
-
* react- und native-Branch (die App-Author registriert beide unter dem
|
|
19
|
-
* gleichen Namen); returnt den ersten gefundenen string. Pure narrowing
|
|
20
|
-
* via `in` — kein cast, kein assertion. */
|
|
21
|
-
export function extensionSectionName(component: PlatformComponent): string | undefined {
|
|
22
|
-
for (const branch of [component.react, component.native]) {
|
|
23
|
-
if (branch !== null && typeof branch === "object" && "__component" in branch) {
|
|
24
|
-
const candidate = branch.__component;
|
|
25
|
-
if (typeof candidate === "string") return candidate;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type ExtensionSectionProps = {
|
|
32
|
-
readonly entityName: string;
|
|
33
|
-
readonly entityId: string | null;
|
|
34
|
-
/** Bereits gespeicherte Extension-Werte der Entity (aus der geladenen
|
|
35
|
-
* detail-row durchgereicht). `undefined` im Create-Mode oder wenn der
|
|
36
|
-
* Host-Screen keine Werte liefert. Erlaubt der Section, den Bestand
|
|
37
|
-
* beim Edit anzuzeigen statt write-only zu sein. */
|
|
38
|
-
readonly initialValues?: Readonly<Record<string, unknown>>;
|
|
39
|
-
/** Nur im List-Header-Slot gesetzt (entityId ist dort null): die screen.id
|
|
40
|
-
* der Liste, damit ein Header-Control den per-Screen URL-Filter-State
|
|
41
|
-
* (useListUrlState) ansprechen kann — z.B. ein Tag-Filter der die Liste auf
|
|
42
|
-
* eine id-Menge narrowed. In entityEdit-Sections undefined. */
|
|
43
|
-
readonly screenId?: string;
|
|
44
|
-
/** Nur im Dashboard-`custom`-Panel gesetzt: der aktuell gewählte Wert des
|
|
45
|
-
* Screen-Filters (siehe DashboardFilterDefinition), gemerged wie bei jeder
|
|
46
|
-
* anderen Panel-Query. In allen anderen Mount-Orten undefined. Dashboard-
|
|
47
|
-
* Panels haben keine Entity — entityName/entityId tragen dort die
|
|
48
|
-
* screen.id bzw. null, siehe CustomPanelBody in dashboard-body.tsx. */
|
|
49
|
-
readonly filterParams?: Readonly<Record<string, unknown>>;
|
|
50
|
-
/** Current form values of the host RenderEdit (controlled mode,
|
|
51
|
-
* issue #1887/#1888) — same snapshot reference as
|
|
52
|
-
* `RenderEditChangeState.values`. Lets the section read the host
|
|
53
|
-
* form's live state (e.g. for a review step). Only set in
|
|
54
|
-
* entityEdit sections; undefined in list-header and dashboard
|
|
55
|
-
* mounts, where no host form exists. */
|
|
56
|
-
readonly values?: Readonly<Record<string, unknown>>;
|
|
57
|
-
/** Sets host RenderEdit form values from outside (e.g. a VIN-decode
|
|
58
|
-
* roundtrip that fills other fields) — same function as
|
|
59
|
-
* `RenderEditControls.patch`, merges only the given keys. `patch` must
|
|
60
|
-
* converge: `setValues` is a no-op only when the merged result is
|
|
61
|
-
* reference-equal to the current values, so an unconditional
|
|
62
|
-
* `patch(...)` from a `values`-dependent effect loops. Undefined outside
|
|
63
|
-
* entityEdit sections. */
|
|
64
|
-
readonly patch?: (partial: Readonly<Record<string, unknown>>) => void;
|
|
65
|
-
/** Validates the host form without writing — same function as
|
|
66
|
-
* `RenderEditControls.validate`; field errors land in
|
|
67
|
-
* `snapshot.errors` on the field instead of a collective message.
|
|
68
|
-
* Undefined outside entityEdit sections. */
|
|
69
|
-
readonly validate?: () => boolean;
|
|
70
|
-
/** Whether the host form has unsaved changes. Only set in the entityEdit
|
|
71
|
-
* footer slot (`screen.slots.footer`); undefined in all other mounts. */
|
|
72
|
-
readonly hasUnsavedChanges?: boolean;
|
|
73
|
-
/** Current wizard step, only set for `layout.mode === "wizard"` forms.
|
|
74
|
-
* Only set in the entityEdit footer slot (`screen.slots.footer`);
|
|
75
|
-
* undefined in all other mounts, and undefined there too for a
|
|
76
|
-
* non-wizard form. */
|
|
77
|
-
readonly wizardStep?: { readonly index: number; readonly isLast: boolean };
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type ExtensionSectionComponent = ComponentType<ExtensionSectionProps>;
|
|
81
|
-
|
|
82
|
-
export type ExtensionSectionsMap = Readonly<Record<string, ExtensionSectionComponent>>;
|
|
83
|
-
|
|
84
|
-
const ExtensionSectionsContext = createContext<ExtensionSectionsMap | undefined>(undefined);
|
|
85
|
-
|
|
86
|
-
export type ExtensionSectionsProviderProps = {
|
|
87
|
-
readonly children: ReactNode;
|
|
88
|
-
readonly value: ExtensionSectionsMap;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
export function ExtensionSectionsProvider({
|
|
92
|
-
children,
|
|
93
|
-
value,
|
|
94
|
-
}: ExtensionSectionsProviderProps): ReactNode {
|
|
95
|
-
return (
|
|
96
|
-
<ExtensionSectionsContext.Provider value={value}>{children}</ExtensionSectionsContext.Provider>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Schaut die Component für einen extension-section-Namen nach. Returnt
|
|
101
|
-
* undefined wenn weder Provider gemounted noch der Name in der Map
|
|
102
|
-
* registriert ist — der Caller (RenderEdit) zeigt dann seinen
|
|
103
|
-
* Placeholder-Banner. `name` ist optional damit Caller den Hook
|
|
104
|
-
* unkonditional aufrufen können (Rules-of-Hooks), ohne einen Stub-Key
|
|
105
|
-
* wie `""` reichen zu müssen — analog `useColumnRenderer`. */
|
|
106
|
-
export function useExtensionSectionComponent(name?: string): ExtensionSectionComponent | undefined {
|
|
107
|
-
const map = useContext(ExtensionSectionsContext);
|
|
108
|
-
if (name === undefined || name === "") return undefined;
|
|
109
|
-
if (map === undefined) return undefined;
|
|
110
|
-
return map[name];
|
|
111
|
-
}
|
package/src/app/form-schema.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EntityDefinition,
|
|
3
|
-
EntityEditScreenDefinition,
|
|
4
|
-
FieldDefinition,
|
|
5
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
|
-
import { evalFieldCondition, NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
7
|
-
import { I18N_KEY_PARAM } from "@cosmicdrift/kumiko-headless";
|
|
8
|
-
import * as z from "zod";
|
|
9
|
-
import { layoutEditFields } from "./layout-fields";
|
|
10
|
-
|
|
11
|
-
// `required` means "has a value", not "is truthy" — `false` and `0` count
|
|
12
|
-
// as present, only the actually-empty representations don't.
|
|
13
|
-
function isPresent(value: unknown): boolean {
|
|
14
|
-
if (value === undefined || value === null || value === "") return false;
|
|
15
|
-
if (Array.isArray(value) && value.length === 0) return false;
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isEmbeddedListField(field: FieldDefinition): boolean {
|
|
20
|
-
return field.type === "embedded" && field.multiple === true;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Renders raw to the user without a de+en default in i18n-defaults.ts.
|
|
24
|
-
export const REQUIRED_FIELD_I18N_KEY = "kumiko.validation.required";
|
|
25
|
-
|
|
26
|
-
// Client-side presence validation for the auto-wired entityEdit path —
|
|
27
|
-
// checks that every rendered required field HAS a value, not that the
|
|
28
|
-
// value has the right shape. Format/range/type validation stays server-
|
|
29
|
-
// authoritative (buildInsertSchema/buildUpdateSchema): the form-state
|
|
30
|
-
// representation of a value can diverge from the server-payload shape (e.g.
|
|
31
|
-
// money is `{amount,currency}`, not a bare number), so a format check here
|
|
32
|
-
// would either reject valid values or need per-representation branches that
|
|
33
|
-
// rot the moment either side changes.
|
|
34
|
-
//
|
|
35
|
-
// One `superRefine` instead of a per-field shape: a field-level `.refine()`
|
|
36
|
-
// wouldn't run at all for a key that's simply absent from `values` —
|
|
37
|
-
// `superRefine` sees the whole object and catches that case too.
|
|
38
|
-
// `.passthrough()` is load-bearing: the default `z.object({})` STRIPS every
|
|
39
|
-
// key (there's no declared shape), so `superRefine` would see an empty
|
|
40
|
-
// object regardless of what was actually submitted — every required field
|
|
41
|
-
// would misreport as missing.
|
|
42
|
-
//
|
|
43
|
-
// Iterates the screen's layout field specs, not `entity.fields` — `required`
|
|
44
|
-
// and `readOnly` are per-spec, evaluated against the current values the
|
|
45
|
-
// same way `view-model/edit.ts` resolves them for the rendered form. Not
|
|
46
|
-
// checked here: `visible`. `runValidate` already filters issues on hidden
|
|
47
|
-
// fields via `computeFieldStates(options.fields, …)` (form-controller.ts:163,181),
|
|
48
|
-
// fed by `deriveFormFields(screen)` in render-edit.tsx.
|
|
49
|
-
export function buildFormSchema(
|
|
50
|
-
entity: EntityDefinition,
|
|
51
|
-
screen: EntityEditScreenDefinition,
|
|
52
|
-
): z.ZodType {
|
|
53
|
-
const fields = layoutEditFields(screen);
|
|
54
|
-
return z
|
|
55
|
-
.object({})
|
|
56
|
-
.passthrough()
|
|
57
|
-
.superRefine((values, ctx) => {
|
|
58
|
-
// `.passthrough()` types `values` as a plain object but doesn't declare
|
|
59
|
-
// its keys — the runtime object always carries every form field.
|
|
60
|
-
// @cast-boundary form-values
|
|
61
|
-
const record = values as Record<string, unknown>;
|
|
62
|
-
for (const spec of fields) {
|
|
63
|
-
const field = entity.fields[spec.field];
|
|
64
|
-
if (!field) continue;
|
|
65
|
-
// Not operable by the user — a presence error would be unresolvable,
|
|
66
|
-
// same reason as the NO_WIDGET_FIELD_TYPES check below.
|
|
67
|
-
if (spec.readOnly !== undefined && evalFieldCondition(spec.readOnly, record)) continue;
|
|
68
|
-
// Screen-spec `required` overrides the entity default, mirroring
|
|
69
|
-
// `view-model/edit.ts` — the rendered form is the reference, and a
|
|
70
|
-
// presence check stricter than the form blocks the user for nothing.
|
|
71
|
-
const entityRequired = "required" in field && field.required === true;
|
|
72
|
-
const isRequired =
|
|
73
|
-
spec.required === undefined ? entityRequired : evalFieldCondition(spec.required, record);
|
|
74
|
-
if (!isRequired) continue;
|
|
75
|
-
// Embedded LIST fields get their own EmbeddedListField grid widget
|
|
76
|
-
// (#1838) — they're fillable, so NO_WIDGET_FIELD_TYPES's "embedded"
|
|
77
|
-
// entry must not exempt them. A statically-`required: true` field
|
|
78
|
-
// with no bound widget is also caught at boot
|
|
79
|
-
// (validateNoWidgetRequiredField in boot-validator/screens.ts).
|
|
80
|
-
if (NO_WIDGET_FIELD_TYPES.includes(field.type) && !isEmbeddedListField(field)) continue;
|
|
81
|
-
if (isPresent(record[spec.field])) continue;
|
|
82
|
-
ctx.addIssue({
|
|
83
|
-
code: "custom",
|
|
84
|
-
path: [spec.field],
|
|
85
|
-
message: `"${spec.field}" is required.`,
|
|
86
|
-
// I18N_KEY_PARAM override, see packages/headless/src/form/zod-bridge.ts.
|
|
87
|
-
params: { [I18N_KEY_PARAM]: REQUIRED_FIELD_I18N_KEY },
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}
|