@cosmicdrift/kumiko-renderer 0.187.0 → 0.188.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 +5 -4
- package/src/__tests__/merge-search-params-into-initial.test.ts +13 -1
- package/src/app/__tests__/form-schema.test.ts +165 -0
- package/src/app/extension-sections.tsx +17 -0
- package/src/app/form-schema.ts +82 -0
- package/src/app/kumiko-screen.tsx +62 -27
- package/src/app/layout-fields.ts +27 -0
- package/src/components/__tests__/render-edit-logic.test.ts +48 -0
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +189 -0
- package/src/components/reference-create-dialog.tsx +4 -1
- package/src/components/render-edit-logic.ts +27 -0
- package/src/components/render-edit.tsx +516 -58
- package/src/components/render-field.tsx +36 -6
- package/src/context/draft-storage-context.tsx +39 -0
- package/src/i18n-defaults.ts +15 -0
- package/src/index.ts +8 -1
- package/src/primitives.tsx +11 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
currencyDecimals,
|
|
4
|
+
type EditFieldViewModel,
|
|
5
|
+
type FieldIssue,
|
|
6
|
+
} from "@cosmicdrift/kumiko-headless";
|
|
3
7
|
import { type ReactNode, useCallback, useMemo, useState } from "react";
|
|
4
8
|
import { useAppFeatures } from "../app/app-features-context";
|
|
5
9
|
import { toKebab } from "../app/qn";
|
|
@@ -330,15 +334,15 @@ function renderInput({
|
|
|
330
334
|
/>
|
|
331
335
|
);
|
|
332
336
|
case "money": {
|
|
333
|
-
const
|
|
337
|
+
const currency = field.currency ?? "EUR";
|
|
334
338
|
return (
|
|
335
339
|
<Input
|
|
336
340
|
kind="money"
|
|
337
341
|
{...common}
|
|
338
|
-
value={
|
|
339
|
-
onChange={(v) => onChange(v)}
|
|
340
|
-
|
|
341
|
-
locale={
|
|
342
|
+
value={moneyMinorValue(field.value, currency)}
|
|
343
|
+
onChange={(v) => onChange(moneyPayload(v, currency))}
|
|
344
|
+
currency={currency}
|
|
345
|
+
locale={appLocale}
|
|
342
346
|
/>
|
|
343
347
|
);
|
|
344
348
|
}
|
|
@@ -479,6 +483,32 @@ function numberValue(v: unknown): number | "" {
|
|
|
479
483
|
return typeof v === "number" ? v : Number(v);
|
|
480
484
|
}
|
|
481
485
|
|
|
486
|
+
// Read/initial value → MoneyInput's minor-units contract. The server's
|
|
487
|
+
// read shape is `{amount, currency, amountMinor}` in MAJOR units — deliberately
|
|
488
|
+
// NOT reading `amountMinor` here: the server derives it via a flat
|
|
489
|
+
// MINOR_UNIT_SCALE=100 (money.ts), which disagrees with currencyDecimals for
|
|
490
|
+
// zero-decimal currencies like JPY, so it would double-scale JPY amounts.
|
|
491
|
+
// Deriving from `amount * 10**currencyDecimals(currency)` keeps this the
|
|
492
|
+
// exact inverse of moneyPayload below. A bare number is passed through
|
|
493
|
+
// unchanged — legacy/malformed data, already in minor units.
|
|
494
|
+
function moneyMinorValue(v: unknown, currency: string): number | "" {
|
|
495
|
+
if (v === undefined || v === null || v === "") return "";
|
|
496
|
+
if (typeof v === "number") return v;
|
|
497
|
+
if (typeof v === "object") {
|
|
498
|
+
const amount = (v as { amount?: unknown }).amount;
|
|
499
|
+
if (typeof amount === "number") return Math.round(amount * 10 ** currencyDecimals(currency));
|
|
500
|
+
}
|
|
501
|
+
return "";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// MoneyInput's minor-units onChange → the server payload shape
|
|
505
|
+
// (`z.object({amount, currency})`, schema-builder.ts) — MAJOR units, no
|
|
506
|
+
// `amountMinor` (the server derives that itself on write).
|
|
507
|
+
function moneyPayload(minorUnits: number | undefined, currency: string): unknown {
|
|
508
|
+
if (minorUnits === undefined) return undefined;
|
|
509
|
+
return { amount: minorUnits / 10 ** currencyDecimals(currency), currency };
|
|
510
|
+
}
|
|
511
|
+
|
|
482
512
|
// locatedTimestamp-Feldwert: das Read-Wrapper liefert `{ at, tz, utc }`; leer
|
|
483
513
|
// (noch nicht gesetzt) → "" als Empty-Sentinel, analog money/timestamp.
|
|
484
514
|
function locatedValue(v: unknown): { at: string; tz: string; utc?: string } | "" {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
// Client-side draftId storage for RenderEdit's create-mode draftKey (issue
|
|
4
|
+
// #1913) — `${screen.id}:new:${draftId}` needs the same `draftId` across a
|
|
5
|
+
// same-tab reload so the wizard resumes the right row instead of the leader
|
|
6
|
+
// on the last-write-wins upsert. Contract only, platform-neutral — the
|
|
7
|
+
// concrete impl (window.sessionStorage on web) is injected by the platform
|
|
8
|
+
// package, same pattern as NavApi/window.history in app/nav.tsx. This
|
|
9
|
+
// package touches no browser storage itself.
|
|
10
|
+
export type DraftStorage = {
|
|
11
|
+
readonly getDraftId: (screenId: string) => string | null;
|
|
12
|
+
readonly setDraftId: (screenId: string, draftId: string) => void;
|
|
13
|
+
readonly clearDraftId: (screenId: string) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// No-op default: without a mounted provider (unwired platform, or a test
|
|
17
|
+
// that doesn't care about drafts) same-tab reload just loses draftId
|
|
18
|
+
// persistence — RenderEdit falls back to its `form-draft:query:list`
|
|
19
|
+
// resume path, the same one a cleared sessionStorage takes on the web.
|
|
20
|
+
const noopDraftStorage: DraftStorage = {
|
|
21
|
+
getDraftId: () => null,
|
|
22
|
+
setDraftId: () => {},
|
|
23
|
+
clearDraftId: () => {},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DraftStorageContext = createContext<DraftStorage>(noopDraftStorage);
|
|
27
|
+
|
|
28
|
+
export type DraftStorageProviderProps = {
|
|
29
|
+
readonly value: DraftStorage;
|
|
30
|
+
readonly children: ReactNode;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function DraftStorageProvider({ value, children }: DraftStorageProviderProps): ReactNode {
|
|
34
|
+
return <DraftStorageContext value={value}>{children}</DraftStorageContext>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useDraftStorage(): DraftStorage {
|
|
38
|
+
return useContext(DraftStorageContext);
|
|
39
|
+
}
|
package/src/i18n-defaults.ts
CHANGED
|
@@ -22,6 +22,12 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
22
22
|
"kumiko.actions.edit": "Bearbeiten",
|
|
23
23
|
"kumiko.actions.copyLink": "Link kopieren",
|
|
24
24
|
"kumiko.actions.copyLinkCopied": "Kopiert!",
|
|
25
|
+
"kumiko.actions.next": "Weiter",
|
|
26
|
+
"kumiko.actions.back": "Zurück",
|
|
27
|
+
"kumiko.actions.finish": "Abschließen",
|
|
28
|
+
|
|
29
|
+
// Wizard step chrome (RenderEdit layout.mode="wizard").
|
|
30
|
+
"kumiko.wizard.step": "Schritt {current} von {total}",
|
|
25
31
|
|
|
26
32
|
// Version — Update-Awareness-Banner (UpdateChecker).
|
|
27
33
|
"kumiko.version.update-available": "Eine neue Version ist verfügbar.",
|
|
@@ -130,6 +136,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
130
136
|
"kumiko.form.error.version-conflict":
|
|
131
137
|
"Datensatz wurde zwischenzeitlich geändert. Lade neu und versuche es erneut.",
|
|
132
138
|
"kumiko.form.extension.save-failed": "Ein Zusatzfeld konnte nicht gespeichert werden.",
|
|
139
|
+
"kumiko.form.draft.resume-multiple":
|
|
140
|
+
"Mehrere offene Entwürfe für dieses Formular gefunden. Welchen möchtest du fortsetzen?",
|
|
133
141
|
|
|
134
142
|
// Validation — Default-Reason-Codes aus dem Framework. App-Code
|
|
135
143
|
// kann eigene Codes via Validation-Hooks reinwerfen; die hier sind
|
|
@@ -192,6 +200,11 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
192
200
|
"kumiko.actions.edit": "Edit",
|
|
193
201
|
"kumiko.actions.copyLink": "Copy link",
|
|
194
202
|
"kumiko.actions.copyLinkCopied": "Copied!",
|
|
203
|
+
"kumiko.actions.next": "Next",
|
|
204
|
+
"kumiko.actions.back": "Back",
|
|
205
|
+
"kumiko.actions.finish": "Finish",
|
|
206
|
+
|
|
207
|
+
"kumiko.wizard.step": "Step {current} of {total}",
|
|
195
208
|
|
|
196
209
|
"kumiko.version.update-available": "A new version is available.",
|
|
197
210
|
|
|
@@ -283,6 +296,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
283
296
|
"kumiko.form.error.version-conflict":
|
|
284
297
|
"Record was modified in the meantime. Reload and try again.",
|
|
285
298
|
"kumiko.form.extension.save-failed": "A custom field could not be saved.",
|
|
299
|
+
"kumiko.form.draft.resume-multiple":
|
|
300
|
+
"Found multiple open drafts for this form. Which one do you want to resume?",
|
|
286
301
|
|
|
287
302
|
"kumiko.validation.required": "Required.",
|
|
288
303
|
"kumiko.validation.invalid": "Invalid value.",
|
package/src/index.ts
CHANGED
|
@@ -69,7 +69,11 @@ export { lastSegment } from "./app/qn";
|
|
|
69
69
|
export type { VariableChipsProps } from "./app/variable-chips";
|
|
70
70
|
export { VariableChips } from "./app/variable-chips";
|
|
71
71
|
export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error";
|
|
72
|
-
export type {
|
|
72
|
+
export type {
|
|
73
|
+
RenderEditChangeState,
|
|
74
|
+
RenderEditControls,
|
|
75
|
+
RenderEditProps,
|
|
76
|
+
} from "./components/render-edit";
|
|
73
77
|
export { RenderEdit } from "./components/render-edit";
|
|
74
78
|
export type { RenderFieldProps } from "./components/render-field";
|
|
75
79
|
export { RenderField } from "./components/render-field";
|
|
@@ -82,6 +86,8 @@ export {
|
|
|
82
86
|
useDispatcherStatus,
|
|
83
87
|
useOptionalDispatcher,
|
|
84
88
|
} from "./context/dispatcher-context";
|
|
89
|
+
export type { DraftStorage, DraftStorageProviderProps } from "./context/draft-storage-context";
|
|
90
|
+
export { DraftStorageProvider, useDraftStorage } from "./context/draft-storage-context";
|
|
85
91
|
export type { UserRolesProviderProps } from "./context/user-roles-context";
|
|
86
92
|
export { UserRolesProvider, useUserRoles } from "./context/user-roles-context";
|
|
87
93
|
export { formatWhen } from "./format-when";
|
|
@@ -169,6 +175,7 @@ export type {
|
|
|
169
175
|
ModalProps,
|
|
170
176
|
PrimitivesProviderProps,
|
|
171
177
|
PrimitivesRegistry,
|
|
178
|
+
ProgressProps,
|
|
172
179
|
RuntimeRenderer,
|
|
173
180
|
SectionProps,
|
|
174
181
|
TextProps,
|
package/src/primitives.tsx
CHANGED
|
@@ -863,6 +863,13 @@ export type CardProps = {
|
|
|
863
863
|
readonly testId?: string;
|
|
864
864
|
};
|
|
865
865
|
|
|
866
|
+
/** Determinate progress bar (e.g. wizard step progress). `value` is a
|
|
867
|
+
* 0..1 fraction, not a percentage — implementations scale for display. */
|
|
868
|
+
export type ProgressProps = {
|
|
869
|
+
readonly value: number;
|
|
870
|
+
readonly testId?: string;
|
|
871
|
+
};
|
|
872
|
+
|
|
866
873
|
// ---- Core-Registry (Kumiko-eigene Primitives) ----
|
|
867
874
|
|
|
868
875
|
export type CorePrimitives = {
|
|
@@ -888,6 +895,10 @@ export type CorePrimitives = {
|
|
|
888
895
|
readonly ConfigSourceBadge: ComponentType<ConfigSourceBadgeProps>;
|
|
889
896
|
readonly ConfigCascadeView: ComponentType<ConfigCascadeViewProps>;
|
|
890
897
|
readonly Link: ComponentType<LinkProps>;
|
|
898
|
+
/** Optional (unlike the other Core-Primitives) so existing partial
|
|
899
|
+
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
900
|
+
* a new primitive shouldn't force every test double to grow a stub. */
|
|
901
|
+
readonly Progress?: ComponentType<ProgressProps>;
|
|
891
902
|
};
|
|
892
903
|
|
|
893
904
|
/** Offene Extension-Zone für App-eigene Primitives. Devs erweitern
|