@cosmicdrift/kumiko-renderer 0.186.3 → 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/column-renderers.tsx +4 -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__/embedded-list-field.test.tsx +15 -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/embedded-list-field.tsx +18 -5
- 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 +21 -0
- package/src/index.ts +8 -1
- package/src/primitives.tsx +23 -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.",
|
|
@@ -31,6 +37,9 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
31
37
|
|
|
32
38
|
// Field — aria-Labels der Date/Timestamp-Primitives.
|
|
33
39
|
"kumiko.field.open-calendar": "Kalender öffnen",
|
|
40
|
+
"kumiko.field.dateField.placeholderYear": "J",
|
|
41
|
+
"kumiko.field.dateField.placeholderMonth": "M",
|
|
42
|
+
"kumiko.field.dateField.placeholderDay": "T",
|
|
34
43
|
"kumiko.field.time": "Uhrzeit",
|
|
35
44
|
"kumiko.field.timezone": "Zeitzone",
|
|
36
45
|
"kumiko.field.locatedTzHint": "Zeit lokal am angegebenen Ort",
|
|
@@ -127,6 +136,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
127
136
|
"kumiko.form.error.version-conflict":
|
|
128
137
|
"Datensatz wurde zwischenzeitlich geändert. Lade neu und versuche es erneut.",
|
|
129
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?",
|
|
130
141
|
|
|
131
142
|
// Validation — Default-Reason-Codes aus dem Framework. App-Code
|
|
132
143
|
// kann eigene Codes via Validation-Hooks reinwerfen; die hier sind
|
|
@@ -189,12 +200,20 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
189
200
|
"kumiko.actions.edit": "Edit",
|
|
190
201
|
"kumiko.actions.copyLink": "Copy link",
|
|
191
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}",
|
|
192
208
|
|
|
193
209
|
"kumiko.version.update-available": "A new version is available.",
|
|
194
210
|
|
|
195
211
|
"kumiko.toast.learn-more": "Learn more",
|
|
196
212
|
|
|
197
213
|
"kumiko.field.open-calendar": "Open calendar",
|
|
214
|
+
"kumiko.field.dateField.placeholderYear": "Y",
|
|
215
|
+
"kumiko.field.dateField.placeholderMonth": "M",
|
|
216
|
+
"kumiko.field.dateField.placeholderDay": "D",
|
|
198
217
|
"kumiko.field.time": "Time",
|
|
199
218
|
"kumiko.field.timezone": "Time zone",
|
|
200
219
|
"kumiko.field.locatedTzHint": "Time local to the given location",
|
|
@@ -277,6 +296,8 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
277
296
|
"kumiko.form.error.version-conflict":
|
|
278
297
|
"Record was modified in the meantime. Reload and try again.",
|
|
279
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?",
|
|
280
301
|
|
|
281
302
|
"kumiko.validation.required": "Required.",
|
|
282
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
|
@@ -548,6 +548,18 @@ export type DataTableProps = {
|
|
|
548
548
|
* "Ende der Liste"-Hinweis statt des Sentinels. Default true. */
|
|
549
549
|
readonly hasMore?: boolean;
|
|
550
550
|
readonly testId?: string;
|
|
551
|
+
/** Wires an edit path into a component column-renderer: `DataTableCell`
|
|
552
|
+
* passes this down as `ColumnRendererProps.onChange` for that cell,
|
|
553
|
+
* bound to `(rowId, column.field)`. Renderers without a component
|
|
554
|
+
* renderer (format-spec, type-default) ignore it — read-only stays
|
|
555
|
+
* read-only unless the column has a custom renderer that reads
|
|
556
|
+
* `onChange`. */
|
|
557
|
+
readonly onCellChange?: (rowId: string, field: string, value: unknown) => void;
|
|
558
|
+
/** Overrides the row `data-testid` (default `row-${row.id}`) — for a
|
|
559
|
+
* screen that already has its own DOM-test/e2e naming scheme. */
|
|
560
|
+
readonly getRowTestId?: (row: ListRowViewModel) => string;
|
|
561
|
+
/** Overrides the cell `data-testid` (default `cell-${row.id}-${field}`). */
|
|
562
|
+
readonly getCellTestId?: (row: ListRowViewModel, field: string) => string;
|
|
551
563
|
};
|
|
552
564
|
|
|
553
565
|
// ---- EmbeddedListInput (createEmbeddedListField widget) ----
|
|
@@ -851,6 +863,13 @@ export type CardProps = {
|
|
|
851
863
|
readonly testId?: string;
|
|
852
864
|
};
|
|
853
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
|
+
|
|
854
873
|
// ---- Core-Registry (Kumiko-eigene Primitives) ----
|
|
855
874
|
|
|
856
875
|
export type CorePrimitives = {
|
|
@@ -876,6 +895,10 @@ export type CorePrimitives = {
|
|
|
876
895
|
readonly ConfigSourceBadge: ComponentType<ConfigSourceBadgeProps>;
|
|
877
896
|
readonly ConfigCascadeView: ComponentType<ConfigCascadeViewProps>;
|
|
878
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>;
|
|
879
902
|
};
|
|
880
903
|
|
|
881
904
|
/** Offene Extension-Zone für App-eigene Primitives. Devs erweitern
|