@cosmicdrift/kumiko-renderer 0.328.0 → 0.329.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/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 +1355 -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,404 +0,0 @@
|
|
|
1
|
-
// fw#2548: secretMint mints a one-time secret from a write-handler's own
|
|
2
|
-
// success payload. This renders the real path (KumikoScreen → SecretMintBody
|
|
3
|
-
// → RenderEdit) under a stub dispatcher, proving: (1) the secret is absent
|
|
4
|
-
// before submit, (2) only the whitelisted reveal.fields survive the submit —
|
|
5
|
-
// a stray "id" in the payload never leaks, (3) confirming clears the reveal
|
|
6
|
-
// from the DOM, (4) no query call ever runs that could reload the secret.
|
|
7
|
-
|
|
8
|
-
import { describe, expect, test } from "bun:test";
|
|
9
|
-
import type {
|
|
10
|
-
SecretMintScreenDefinition,
|
|
11
|
-
TextFieldDef,
|
|
12
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
13
|
-
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
14
|
-
import { fireEvent, render, screen as rtlScreen, waitFor } from "@testing-library/react";
|
|
15
|
-
import type { ComponentType, ReactNode } from "react";
|
|
16
|
-
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
17
|
-
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
18
|
-
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
19
|
-
import {
|
|
20
|
-
type BannerProps,
|
|
21
|
-
type CorePrimitives,
|
|
22
|
-
PrimitivesProvider,
|
|
23
|
-
type SecretRevealProps,
|
|
24
|
-
type SectionProps,
|
|
25
|
-
type TextProps,
|
|
26
|
-
} from "../../primitives";
|
|
27
|
-
import type { FeatureSchema } from "../feature-schema";
|
|
28
|
-
import { KumikoScreen } from "../kumiko-screen";
|
|
29
|
-
import { NavProvider, type NavTarget } from "../nav";
|
|
30
|
-
|
|
31
|
-
const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
|
|
32
|
-
const noop = () => null;
|
|
33
|
-
|
|
34
|
-
const testButton: ComponentType<{
|
|
35
|
-
children?: ReactNode;
|
|
36
|
-
onClick?: () => void;
|
|
37
|
-
testId?: string;
|
|
38
|
-
type?: "button" | "submit";
|
|
39
|
-
disabled?: boolean;
|
|
40
|
-
}> = ({ children, onClick, testId, type, disabled }) => (
|
|
41
|
-
<button type={type ?? "button"} data-testid={testId} onClick={onClick} disabled={disabled}>
|
|
42
|
-
{children}
|
|
43
|
-
</button>
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const testForm: ComponentType<{
|
|
47
|
-
children?: ReactNode;
|
|
48
|
-
actions?: ReactNode;
|
|
49
|
-
secondaryActions?: ReactNode;
|
|
50
|
-
onSubmit?: () => void;
|
|
51
|
-
}> = ({ children, actions, secondaryActions, onSubmit }) => (
|
|
52
|
-
<form
|
|
53
|
-
onSubmit={(e) => {
|
|
54
|
-
e.preventDefault();
|
|
55
|
-
onSubmit?.();
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
{children}
|
|
59
|
-
{secondaryActions}
|
|
60
|
-
{actions}
|
|
61
|
-
</form>
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const testInput: ComponentType<{
|
|
65
|
-
name?: string;
|
|
66
|
-
value?: unknown;
|
|
67
|
-
onChange?: (v: unknown) => void;
|
|
68
|
-
}> = ({ name = "field", value, onChange }) => (
|
|
69
|
-
<input
|
|
70
|
-
aria-label={name}
|
|
71
|
-
data-testid={`input-${name}`}
|
|
72
|
-
value={typeof value === "string" ? value : ""}
|
|
73
|
-
onChange={(e) => onChange?.(e.target.value)}
|
|
74
|
-
/>
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const testSection: ComponentType<SectionProps> = ({ testId, children }) => (
|
|
78
|
-
<div data-testid={testId}>{children}</div>
|
|
79
|
-
);
|
|
80
|
-
const testBanner: ComponentType<BannerProps> = ({ children, testId }) => (
|
|
81
|
-
<div data-testid={testId}>{children}</div>
|
|
82
|
-
);
|
|
83
|
-
const testText: ComponentType<TextProps> = ({ children, testId }) => (
|
|
84
|
-
<span data-testid={testId}>{children}</span>
|
|
85
|
-
);
|
|
86
|
-
// Renders every value's raw text plus a `data-qr` marker — lets tests assert
|
|
87
|
-
// both DOM-leak absence (no query needed to find rendered text) and that
|
|
88
|
-
// display: "qr" reaches the primitive as `qr: true`.
|
|
89
|
-
const testSecretReveal: ComponentType<SecretRevealProps> = ({ values, testId }) => (
|
|
90
|
-
<div data-testid={testId}>
|
|
91
|
-
{values.map((v) => (
|
|
92
|
-
<div key={v.label} data-testid={`secret-value-${v.label}`} data-qr={String(v.qr === true)}>
|
|
93
|
-
{v.value}
|
|
94
|
-
</div>
|
|
95
|
-
))}
|
|
96
|
-
</div>
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const testPrimitives: CorePrimitives = {
|
|
100
|
-
Button: testButton,
|
|
101
|
-
Banner: testBanner,
|
|
102
|
-
Field: passChildren,
|
|
103
|
-
Input: testInput,
|
|
104
|
-
DataTable: noop,
|
|
105
|
-
Form: testForm,
|
|
106
|
-
Section: testSection,
|
|
107
|
-
Card: passChildren,
|
|
108
|
-
Grid: passChildren,
|
|
109
|
-
GridCell: passChildren,
|
|
110
|
-
Text: testText,
|
|
111
|
-
Heading: passChildren,
|
|
112
|
-
Dialog: noop,
|
|
113
|
-
Modal: noop,
|
|
114
|
-
Lightbox: noop,
|
|
115
|
-
ConfigSourceBadge: noop,
|
|
116
|
-
ConfigCascadeView: noop,
|
|
117
|
-
Link: noop,
|
|
118
|
-
SecretReveal: testSecretReveal,
|
|
119
|
-
} as unknown as CorePrimitives;
|
|
120
|
-
|
|
121
|
-
const mintScreen: SecretMintScreenDefinition = {
|
|
122
|
-
id: "mint-token",
|
|
123
|
-
type: "secretMint",
|
|
124
|
-
handler: "shop:write:token:mint",
|
|
125
|
-
fields: { label: { type: "text" } as TextFieldDef },
|
|
126
|
-
layout: { sections: [{ title: "Mint", fields: ["label"] }] },
|
|
127
|
-
reveal: { fields: [{ field: "token", label: "Token" }] },
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
function buildSchema(screen: SecretMintScreenDefinition): FeatureSchema {
|
|
131
|
-
return {
|
|
132
|
-
featureName: "shop",
|
|
133
|
-
entities: {},
|
|
134
|
-
screens: [screen],
|
|
135
|
-
} as FeatureSchema;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function stubDispatcher(writeData: unknown): {
|
|
139
|
-
dispatcher: Dispatcher;
|
|
140
|
-
queryCalls: unknown[];
|
|
141
|
-
} {
|
|
142
|
-
const queryCalls: unknown[] = [];
|
|
143
|
-
const dispatcher: Dispatcher = {
|
|
144
|
-
write: (async () => ({ isSuccess: true, data: writeData })) as unknown as Dispatcher["write"],
|
|
145
|
-
query: (async (type: string, payload: unknown) => {
|
|
146
|
-
queryCalls.push({ type, payload });
|
|
147
|
-
return { isSuccess: true, data: {} };
|
|
148
|
-
}) as unknown as Dispatcher["query"],
|
|
149
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
150
|
-
statusStore: {
|
|
151
|
-
getState: () => "online",
|
|
152
|
-
subscribe: () => () => {},
|
|
153
|
-
} as unknown as Dispatcher["statusStore"],
|
|
154
|
-
async *stream() {},
|
|
155
|
-
pendingWrites: () => [],
|
|
156
|
-
pendingFiles: () => [],
|
|
157
|
-
};
|
|
158
|
-
return { dispatcher, queryCalls };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Keyed stub — one write-response per handler QN, and every write call
|
|
162
|
-
// recorded with its full payload — for scenarios with more than one
|
|
163
|
-
// write-handler in play (mint + confirm).
|
|
164
|
-
function stubMultiWriteDispatcher(responses: Readonly<Record<string, unknown>>): {
|
|
165
|
-
dispatcher: Dispatcher;
|
|
166
|
-
writeCalls: Array<{ readonly command: string; readonly payload: unknown }>;
|
|
167
|
-
} {
|
|
168
|
-
const writeCalls: Array<{ readonly command: string; readonly payload: unknown }> = [];
|
|
169
|
-
const dispatcher: Dispatcher = {
|
|
170
|
-
write: (async (command: string, payload: unknown) => {
|
|
171
|
-
writeCalls.push({ command, payload });
|
|
172
|
-
return { isSuccess: true, data: responses[command] ?? {} };
|
|
173
|
-
}) as unknown as Dispatcher["write"],
|
|
174
|
-
query: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["query"],
|
|
175
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
176
|
-
statusStore: {
|
|
177
|
-
getState: () => "online",
|
|
178
|
-
subscribe: () => () => {},
|
|
179
|
-
} as unknown as Dispatcher["statusStore"],
|
|
180
|
-
async *stream() {},
|
|
181
|
-
pendingWrites: () => [],
|
|
182
|
-
pendingFiles: () => [],
|
|
183
|
-
};
|
|
184
|
-
return { dispatcher, writeCalls };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function renderMintScreen(
|
|
188
|
-
dispatcher: Dispatcher,
|
|
189
|
-
screen: SecretMintScreenDefinition = mintScreen,
|
|
190
|
-
navigate: (target: NavTarget) => void = () => {},
|
|
191
|
-
) {
|
|
192
|
-
const qn = `shop:screen:${screen.id}`;
|
|
193
|
-
return render(
|
|
194
|
-
<LocaleProvider
|
|
195
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
196
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
197
|
-
>
|
|
198
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
199
|
-
<NavProvider
|
|
200
|
-
value={{
|
|
201
|
-
route: { screenId: qn },
|
|
202
|
-
navigate,
|
|
203
|
-
replace: () => {},
|
|
204
|
-
hrefFor: () => "",
|
|
205
|
-
searchParams: {},
|
|
206
|
-
setSearchParams: () => {},
|
|
207
|
-
}}
|
|
208
|
-
>
|
|
209
|
-
<PrimitivesProvider value={testPrimitives}>
|
|
210
|
-
<KumikoScreen schema={buildSchema(screen)} qn={qn} />
|
|
211
|
-
</PrimitivesProvider>
|
|
212
|
-
</NavProvider>
|
|
213
|
-
</DispatcherProvider>
|
|
214
|
-
</LocaleProvider>,
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
describe("SecretMintBody (fw#2548)", () => {
|
|
219
|
-
test("before submit: the secret is not in the DOM", () => {
|
|
220
|
-
const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
|
|
221
|
-
renderMintScreen(dispatcher);
|
|
222
|
-
expect(rtlScreen.queryByText("kpat_secret")).toBeNull();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
test("after a successful submit: only the whitelisted reveal field appears — not the payload's other fields", async () => {
|
|
226
|
-
const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
|
|
227
|
-
renderMintScreen(dispatcher);
|
|
228
|
-
|
|
229
|
-
fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
|
|
230
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
231
|
-
|
|
232
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
|
|
233
|
-
expect(rtlScreen.queryByText("x")).toBeNull();
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test("confirming the reveal clears the secret from the DOM", async () => {
|
|
237
|
-
const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
|
|
238
|
-
renderMintScreen(dispatcher);
|
|
239
|
-
|
|
240
|
-
fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
|
|
241
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
242
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
|
|
243
|
-
|
|
244
|
-
fireEvent.click(rtlScreen.getByTestId("kumiko-screen-secret-mint-confirm"));
|
|
245
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).toBeNull());
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
test("the secret is never fetched via a query — it only ever comes from the write result", async () => {
|
|
249
|
-
const { dispatcher, queryCalls } = stubDispatcher({ token: "kpat_secret", id: "x" });
|
|
250
|
-
renderMintScreen(dispatcher);
|
|
251
|
-
|
|
252
|
-
fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
|
|
253
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
254
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
|
|
255
|
-
|
|
256
|
-
expect(queryCalls.length).toBe(0);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("acknowledging without a confirm step and without a redirect shows the done banner", async () => {
|
|
260
|
-
const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
|
|
261
|
-
renderMintScreen(dispatcher);
|
|
262
|
-
|
|
263
|
-
fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
|
|
264
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
265
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
|
|
266
|
-
|
|
267
|
-
fireEvent.click(rtlScreen.getByTestId("kumiko-screen-secret-mint-confirm"));
|
|
268
|
-
await waitFor(() =>
|
|
269
|
-
expect(rtlScreen.queryByTestId("kumiko-screen-secret-mint-done")).not.toBeNull(),
|
|
270
|
-
);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("acknowledging with a redirect navigates instead of showing the done banner", async () => {
|
|
274
|
-
const { dispatcher } = stubDispatcher({ token: "kpat_secret", id: "x" });
|
|
275
|
-
const navigateCalls: NavTarget[] = [];
|
|
276
|
-
renderMintScreen(dispatcher, { ...mintScreen, redirect: "after-mint" }, (target) =>
|
|
277
|
-
navigateCalls.push(target),
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
|
|
281
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
282
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
|
|
283
|
-
|
|
284
|
-
fireEvent.click(rtlScreen.getByTestId("kumiko-screen-secret-mint-confirm"));
|
|
285
|
-
await waitFor(() => expect(navigateCalls.length).toBe(1));
|
|
286
|
-
expect(navigateCalls[0]).toEqual({ screenId: "after-mint" });
|
|
287
|
-
expect(rtlScreen.queryByTestId("kumiko-screen-secret-mint-done")).toBeNull();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test("a reveal field with display 'qr' reaches the SecretReveal primitive with qr: true", async () => {
|
|
291
|
-
const qrScreen: SecretMintScreenDefinition = {
|
|
292
|
-
...mintScreen,
|
|
293
|
-
reveal: { fields: [{ field: "token", label: "Token", display: "qr" }] },
|
|
294
|
-
};
|
|
295
|
-
const { dispatcher } = stubDispatcher({ token: "otpauth://totp/x?secret=ABC", id: "x" });
|
|
296
|
-
renderMintScreen(dispatcher, qrScreen);
|
|
297
|
-
|
|
298
|
-
fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
|
|
299
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
300
|
-
|
|
301
|
-
await waitFor(() =>
|
|
302
|
-
expect(rtlScreen.getByTestId("secret-value-Token").getAttribute("data-qr")).toBe("true"),
|
|
303
|
-
);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test("an input-less secretMint (no fields) has an active submit button and dispatches the mint handler on click", async () => {
|
|
307
|
-
const inputLessScreen: SecretMintScreenDefinition = {
|
|
308
|
-
id: "mint-token",
|
|
309
|
-
type: "secretMint",
|
|
310
|
-
handler: "shop:write:token:mint",
|
|
311
|
-
fields: {},
|
|
312
|
-
layout: { sections: [] },
|
|
313
|
-
reveal: { fields: [{ field: "token", label: "Token" }] },
|
|
314
|
-
};
|
|
315
|
-
const { dispatcher, writeCalls } = stubMultiWriteDispatcher({
|
|
316
|
-
"shop:write:token:mint": { token: "kpat_secret" },
|
|
317
|
-
});
|
|
318
|
-
renderMintScreen(dispatcher, inputLessScreen);
|
|
319
|
-
|
|
320
|
-
const submit = rtlScreen.getByTestId("render-edit-submit") as HTMLButtonElement;
|
|
321
|
-
expect(submit.disabled).toBe(false);
|
|
322
|
-
fireEvent.click(submit);
|
|
323
|
-
|
|
324
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
|
|
325
|
-
expect(writeCalls.map((c) => c.command)).toEqual(["shop:write:token:mint"]);
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
describe("SecretMintBody confirm step (fw#2838)", () => {
|
|
330
|
-
const mintScreenWithConfirm: SecretMintScreenDefinition = {
|
|
331
|
-
...mintScreen,
|
|
332
|
-
confirm: {
|
|
333
|
-
handler: "shop:write:token:confirm",
|
|
334
|
-
fields: { code: { type: "text" } as TextFieldDef },
|
|
335
|
-
layout: { sections: [{ title: "Confirm", fields: ["code"] }] },
|
|
336
|
-
carry: ["setupToken"],
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
// The confirm form mounts from an async post-mint update, so its form-store subscription can
|
|
341
|
-
// still be pending when the change fires — clicking before that subscription re-renders hits a disabled submit.
|
|
342
|
-
async function enterConfirmCodeAndSubmit(code: string): Promise<void> {
|
|
343
|
-
fireEvent.change(rtlScreen.getByLabelText(/code/i), { target: { value: code } });
|
|
344
|
-
await waitFor(() =>
|
|
345
|
-
expect(rtlScreen.getByTestId<HTMLButtonElement>("render-edit-submit").disabled).toBe(false),
|
|
346
|
-
);
|
|
347
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
test("renders the confirm form instead of the acknowledge button, and submits the confirm form's own values merged with the carried mint-payload field", async () => {
|
|
351
|
-
const { dispatcher, writeCalls } = stubMultiWriteDispatcher({
|
|
352
|
-
"shop:write:token:mint": { token: "kpat_secret", id: "x", setupToken: "stok_123" },
|
|
353
|
-
"shop:write:token:confirm": {},
|
|
354
|
-
});
|
|
355
|
-
renderMintScreen(dispatcher, mintScreenWithConfirm);
|
|
356
|
-
|
|
357
|
-
fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
|
|
358
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
359
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
|
|
360
|
-
|
|
361
|
-
// No bare acknowledge button — the confirm form takes its place.
|
|
362
|
-
expect(rtlScreen.queryByTestId("kumiko-screen-secret-mint-confirm")).toBeNull();
|
|
363
|
-
|
|
364
|
-
await enterConfirmCodeAndSubmit("123456");
|
|
365
|
-
|
|
366
|
-
await waitFor(() =>
|
|
367
|
-
expect(writeCalls.some((c) => c.command === "shop:write:token:confirm")).toBe(true),
|
|
368
|
-
);
|
|
369
|
-
const confirmCall = writeCalls.find((c) => c.command === "shop:write:token:confirm");
|
|
370
|
-
expect(confirmCall?.payload).toEqual({ code: "123456", setupToken: "stok_123" });
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test("a carried field that is not in reveal.fields never appears in the DOM", async () => {
|
|
374
|
-
const { dispatcher } = stubMultiWriteDispatcher({
|
|
375
|
-
"shop:write:token:mint": { token: "kpat_secret", id: "x", setupToken: "stok_123" },
|
|
376
|
-
});
|
|
377
|
-
renderMintScreen(dispatcher, mintScreenWithConfirm);
|
|
378
|
-
|
|
379
|
-
fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
|
|
380
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
381
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
|
|
382
|
-
|
|
383
|
-
expect(rtlScreen.queryByText("stok_123")).toBeNull();
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
test("without a redirect, a successful confirm shows the done banner", async () => {
|
|
387
|
-
const { dispatcher } = stubMultiWriteDispatcher({
|
|
388
|
-
"shop:write:token:mint": { token: "kpat_secret", id: "x", setupToken: "stok_123" },
|
|
389
|
-
"shop:write:token:confirm": {},
|
|
390
|
-
});
|
|
391
|
-
renderMintScreen(dispatcher, mintScreenWithConfirm);
|
|
392
|
-
|
|
393
|
-
fireEvent.change(rtlScreen.getByLabelText(/label/i), { target: { value: "My token" } });
|
|
394
|
-
fireEvent.click(rtlScreen.getByTestId("render-edit-submit"));
|
|
395
|
-
await waitFor(() => expect(rtlScreen.queryByText("kpat_secret")).not.toBeNull());
|
|
396
|
-
|
|
397
|
-
await enterConfirmCodeAndSubmit("123456");
|
|
398
|
-
|
|
399
|
-
await waitFor(() =>
|
|
400
|
-
expect(rtlScreen.queryByTestId("kumiko-screen-secret-mint-done")).not.toBeNull(),
|
|
401
|
-
);
|
|
402
|
-
expect(rtlScreen.queryByText("kpat_secret")).toBeNull();
|
|
403
|
-
});
|
|
404
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
-
import type { ComponentType, ReactNode } from "react";
|
|
4
|
-
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
5
|
-
import { type ButtonProps, type CorePrimitives, PrimitivesProvider } from "../../primitives";
|
|
6
|
-
import { VariableChips } from "../variable-chips";
|
|
7
|
-
|
|
8
|
-
const captureButton: ComponentType<ButtonProps> = ({ children, onClick, ariaLabel, disabled }) => (
|
|
9
|
-
<button type="button" aria-label={ariaLabel} onClick={() => void onClick?.()} disabled={disabled}>
|
|
10
|
-
{children}
|
|
11
|
-
</button>
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
const noop = (): ReactNode => null;
|
|
15
|
-
const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
|
|
16
|
-
|
|
17
|
-
const testPrimitives: CorePrimitives = {
|
|
18
|
-
Button: captureButton,
|
|
19
|
-
Banner: passChildren,
|
|
20
|
-
Field: passChildren,
|
|
21
|
-
Input: noop,
|
|
22
|
-
DataTable: noop,
|
|
23
|
-
Form: passChildren,
|
|
24
|
-
Section: passChildren,
|
|
25
|
-
Card: passChildren,
|
|
26
|
-
Grid: passChildren,
|
|
27
|
-
GridCell: passChildren,
|
|
28
|
-
Text: passChildren,
|
|
29
|
-
Heading: noop,
|
|
30
|
-
Dialog: noop,
|
|
31
|
-
Modal: noop,
|
|
32
|
-
Lightbox: noop,
|
|
33
|
-
ConfigSourceBadge: noop,
|
|
34
|
-
ConfigCascadeView: noop,
|
|
35
|
-
Link: noop,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
function Wrapper({ children }: { readonly children: ReactNode }): ReactNode {
|
|
39
|
-
return (
|
|
40
|
-
<LocaleProvider resolver={createStaticLocaleResolver({ locale: "en" })}>
|
|
41
|
-
<PrimitivesProvider value={testPrimitives}>{children}</PrimitivesProvider>
|
|
42
|
-
</LocaleProvider>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe("VariableChips", () => {
|
|
47
|
-
test("renders no chips when there are no variables", () => {
|
|
48
|
-
render(<VariableChips variables={[]} onInsert={() => {}} />, { wrapper: Wrapper });
|
|
49
|
-
expect(screen.queryAllByRole("button")).toHaveLength(0);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("renders one chip per variable and reports a click by name", () => {
|
|
53
|
-
const inserted: string[] = [];
|
|
54
|
-
render(
|
|
55
|
-
<VariableChips
|
|
56
|
-
variables={["customerName", "orderId"]}
|
|
57
|
-
onInsert={(name) => inserted.push(name)}
|
|
58
|
-
/>,
|
|
59
|
-
{ wrapper: Wrapper },
|
|
60
|
-
);
|
|
61
|
-
expect(screen.getAllByRole("button")).toHaveLength(2);
|
|
62
|
-
fireEvent.click(screen.getByText("{{orderId}}"));
|
|
63
|
-
expect(inserted).toEqual(["orderId"]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("disabled propagates to every chip", () => {
|
|
67
|
-
render(<VariableChips variables={["a"]} onInsert={() => {}} disabled />, { wrapper: Wrapper });
|
|
68
|
-
expect((screen.getByRole("button") as HTMLButtonElement).disabled).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Cross-feature schema access for components that need to resolve a
|
|
2
|
-
// screen owned by a DIFFERENT feature than the one they're rendering
|
|
3
|
-
// under — e.g. a reference field opening the target entity's create
|
|
4
|
-
// screen (kumiko-framework#1681). `createKumikoApp` provides the full
|
|
5
|
-
// `app.features` list; components elsewhere in the render tree default
|
|
6
|
-
// to an empty list (no cross-feature schema known, e.g. outside
|
|
7
|
-
// createKumikoApp or in isolated tests).
|
|
8
|
-
|
|
9
|
-
import { createContext, type ReactNode, useContext } from "react";
|
|
10
|
-
import type { FeatureSchema } from "./feature-schema";
|
|
11
|
-
|
|
12
|
-
const AppFeaturesContext = createContext<readonly FeatureSchema[]>([]);
|
|
13
|
-
|
|
14
|
-
export type AppFeaturesProviderProps = {
|
|
15
|
-
readonly features: readonly FeatureSchema[];
|
|
16
|
-
readonly children: ReactNode;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function AppFeaturesProvider({ features, children }: AppFeaturesProviderProps): ReactNode {
|
|
20
|
-
return <AppFeaturesContext.Provider value={features}>{children}</AppFeaturesContext.Provider>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function useAppFeatures(): readonly FeatureSchema[] {
|
|
24
|
-
return useContext(AppFeaturesContext);
|
|
25
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// Column-Renderer-Map: client-side Lookup für ListColumn-Spalten die im
|
|
2
|
-
// Schema einen PlatformComponent-Renderer (`{ react: { __component: "Name" } }`)
|
|
3
|
-
// statt einer String-Funktion angeben. createKumikoApp im renderer-web sammelt
|
|
4
|
-
// alle clientFeatures.columnRenderers und mountet den Provider; der DataTable-
|
|
5
|
-
// Cell-Renderer schlägt den Component über `useColumnRenderer(name)` nach.
|
|
6
|
-
//
|
|
7
|
-
// Analog zu CustomScreensProvider — der Renderer-Web-Bootstrap mounted beide
|
|
8
|
-
// Provider an derselben Stelle. Schemas selbst bleiben serializable: der
|
|
9
|
-
// Component wird nur per String-Key referenziert, die Map liegt allein
|
|
10
|
-
// client-seitig.
|
|
11
|
-
|
|
12
|
-
import { type ComponentType, createContext, type ReactNode, useContext } from "react";
|
|
13
|
-
|
|
14
|
-
// Props die ein Column-Renderer-Component bekommt. `value` ist der
|
|
15
|
-
// Cell-Value für die Spalte (rohes Field-Value aus der Row), `row` ist
|
|
16
|
-
// die ganze Row als plain object — nice-to-have für Renderer die auf
|
|
17
|
-
// andere Spalten zugreifen wollen (z.B. Status-Badge der den
|
|
18
|
-
// Erstellungs-Zeitpunkt aus der Row mitnimmt). `column.field` ist der
|
|
19
|
-
// Field-Name; hilfreich für generische Renderer (JSON-Pretty-Printer
|
|
20
|
-
// oder Debug-Renderer die den Feldnamen anzeigen).
|
|
21
|
-
export type ColumnRendererProps = {
|
|
22
|
-
readonly value: unknown;
|
|
23
|
-
readonly row: Readonly<Record<string, unknown>>;
|
|
24
|
-
readonly column: {
|
|
25
|
-
readonly field: string;
|
|
26
|
-
};
|
|
27
|
-
/** Set only when the caller (DataTable's `onCellChange`) wired an
|
|
28
|
-
* editable path for this cell — undefined means read-only. A renderer
|
|
29
|
-
* can branch its own markup (input vs. plain text) on presence alone. */
|
|
30
|
-
readonly onChange?: (value: unknown) => void;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export type ColumnRendererComponent = ComponentType<ColumnRendererProps>;
|
|
34
|
-
|
|
35
|
-
export type ColumnRenderersMap = Readonly<Record<string, ColumnRendererComponent>>;
|
|
36
|
-
|
|
37
|
-
const ColumnRenderersContext = createContext<ColumnRenderersMap | undefined>(undefined);
|
|
38
|
-
|
|
39
|
-
export type ColumnRenderersProviderProps = {
|
|
40
|
-
readonly children: ReactNode;
|
|
41
|
-
readonly value: ColumnRenderersMap;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export function ColumnRenderersProvider({
|
|
45
|
-
children,
|
|
46
|
-
value,
|
|
47
|
-
}: ColumnRenderersProviderProps): ReactNode {
|
|
48
|
-
return (
|
|
49
|
-
<ColumnRenderersContext.Provider value={value}>{children}</ColumnRenderersContext.Provider>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Schaut die Component für einen Renderer-Key nach. Returnt undefined
|
|
54
|
-
* wenn `name` leer/undefined ist (Caller hat keinen __component-Renderer)
|
|
55
|
-
* oder weder Provider gemounted noch der Key in der Map ist. Der Caller
|
|
56
|
-
* (DataTable.renderCell) loggt dann eine Warnung und fällt auf den
|
|
57
|
-
* Default-Type-Renderer zurück.
|
|
58
|
-
*
|
|
59
|
-
* `name` ist optional damit Caller den Hook unkonditional aufrufen
|
|
60
|
-
* können — Rules-of-Hooks verbieten einen Function-Branch der den Hook
|
|
61
|
-
* überspringt. So bleibt der Aufruf eine einzige Zeile, ohne dass der
|
|
62
|
-
* Caller einen Stub-Key wie `""` reichen muss. */
|
|
63
|
-
export function useColumnRenderer(name?: string): ColumnRendererComponent | undefined {
|
|
64
|
-
const map = useContext(ColumnRenderersContext);
|
|
65
|
-
if (name === undefined || name === "") return undefined;
|
|
66
|
-
if (map === undefined) return undefined;
|
|
67
|
-
return map[name];
|
|
68
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// Content-Editor-Components-Map: client-side lookup by `contentFormat`
|
|
2
|
-
// ("plain" | "rich" | "markdown") for r.contentCollection() entries. A collection
|
|
3
|
-
// declares its contentFormat in the schema; the client resolves it to a
|
|
4
|
-
// React component here. Same registry shape as columnRenderers/
|
|
5
|
-
// extensionSectionComponents — String-key in the schema, Component in the
|
|
6
|
-
// client bundle, last-wins merge in createKumikoApp.
|
|
7
|
-
//
|
|
8
|
-
// No entry registered for a format → TextareaContentEditor, so a missing
|
|
9
|
-
// editor is never an empty panel.
|
|
10
|
-
|
|
11
|
-
import type { ContentEditorFormat } from "@cosmicdrift/kumiko-types/nav";
|
|
12
|
-
import { type ComponentType, createContext, type ReactNode, useContext } from "react";
|
|
13
|
-
import { usePrimitives } from "../primitives";
|
|
14
|
-
|
|
15
|
-
export type ContentEditorProps = {
|
|
16
|
-
/** DOM id every registered editor must render onto its own focusable
|
|
17
|
-
* root element. Callers that wrap an editor in a `Field` (label +
|
|
18
|
-
* htmlFor) pass a stable id here so the label stays associated with
|
|
19
|
-
* whatever element actually renders — a fixed constant would break as
|
|
20
|
-
* soon as a registered editor swaps in for the textarea fallback. */
|
|
21
|
-
readonly id: string;
|
|
22
|
-
readonly value: string;
|
|
23
|
-
readonly onChange: (value: string) => void;
|
|
24
|
-
/** Variable names insertable as chips (from the collection's
|
|
25
|
-
* variableSchema). Empty until the variable-chip step wires it up —
|
|
26
|
-
* editors must accept it now, that's the point of fixing the signature
|
|
27
|
-
* here. */
|
|
28
|
-
readonly variables: readonly string[];
|
|
29
|
-
readonly readOnly: boolean;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type ContentEditorComponent = ComponentType<ContentEditorProps>;
|
|
33
|
-
|
|
34
|
-
/** Default id value for callers that don't generate their own (e.g. a
|
|
35
|
-
* standalone TextareaContentEditor render outside a multi-editor page).
|
|
36
|
-
* Registered editors don't apply this automatically — every caller passes
|
|
37
|
-
* its own id via ContentEditorProps.id. */
|
|
38
|
-
export const CONTENT_EDITOR_ELEMENT_ID = "content-editor-textarea";
|
|
39
|
-
|
|
40
|
-
export type ContentEditorsMap = Partial<Record<ContentEditorFormat, ContentEditorComponent>>;
|
|
41
|
-
|
|
42
|
-
const ContentEditorsContext = createContext<ContentEditorsMap>({});
|
|
43
|
-
|
|
44
|
-
export type ContentEditorsProviderProps = {
|
|
45
|
-
readonly children: ReactNode;
|
|
46
|
-
readonly value: ContentEditorsMap;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export function ContentEditorsProvider({
|
|
50
|
-
children,
|
|
51
|
-
value,
|
|
52
|
-
}: ContentEditorsProviderProps): ReactNode {
|
|
53
|
-
return <ContentEditorsContext.Provider value={value}>{children}</ContentEditorsContext.Provider>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Plain-textarea fallback for a contentFormat with no registered editor.
|
|
57
|
-
* Uses the primitives Input like every other form field, so it renders on
|
|
58
|
-
* every platform without requiring the platform's DOM/native equivalent. */
|
|
59
|
-
export function TextareaContentEditor({
|
|
60
|
-
id,
|
|
61
|
-
value,
|
|
62
|
-
onChange,
|
|
63
|
-
readOnly,
|
|
64
|
-
}: ContentEditorProps): ReactNode {
|
|
65
|
-
const { Input } = usePrimitives();
|
|
66
|
-
return (
|
|
67
|
-
<Input
|
|
68
|
-
kind="textarea"
|
|
69
|
-
id={id}
|
|
70
|
-
name={CONTENT_EDITOR_ELEMENT_ID}
|
|
71
|
-
value={value}
|
|
72
|
-
onChange={onChange}
|
|
73
|
-
readOnly={readOnly}
|
|
74
|
-
rows={14}
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Resolves the editor for a contentFormat, falling back to the plain
|
|
80
|
-
* textarea when no clientFeature registered one. `contentFormat`
|
|
81
|
-
* undefined (collection didn't declare one) behaves like "plain". */
|
|
82
|
-
export function useContentEditor(contentFormat?: ContentEditorFormat): ContentEditorComponent {
|
|
83
|
-
const map = useContext(ContentEditorsContext);
|
|
84
|
-
return map[contentFormat ?? "plain"] ?? TextareaContentEditor;
|
|
85
|
-
}
|