@cosmicdrift/kumiko-headless 0.334.0 → 0.336.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/form/form-controller.js +20 -24
- package/dist/form/index.d.ts +1 -1
- package/dist/form/index.js +1 -1
- package/dist/form/zod-bridge.d.ts +5 -0
- package/dist/form/zod-bridge.js +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/locale-routing/index.d.ts +3 -1
- package/dist/view-model/edit.js +6 -4
- package/dist/view-model/list.js +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createStore } from "../store/index.js";
|
|
2
|
-
import { groupIssuesByPath, zodErrorToFieldIssues } from "./zod-bridge.js";
|
|
2
|
+
import { groupIssuesByPath, relevantFieldIssues, zodErrorToFieldIssues } from "./zod-bridge.js";
|
|
3
3
|
// Paths without the prefix (hook-level errors, "(root)") pass through unchanged.
|
|
4
4
|
function stripServerFieldPathPrefix(issues, prefix) {
|
|
5
5
|
return issues.map((issue) => issue.path.startsWith(prefix) ? { ...issue, path: issue.path.slice(prefix.length) } : issue);
|
|
@@ -139,15 +139,15 @@ export function createFormController(options) {
|
|
|
139
139
|
// the scope — a step-2 validate() that comes back clean must not wipe
|
|
140
140
|
// step-1's still-unresolved errors from an earlier submit() (or vice
|
|
141
141
|
// versa when step-2 newly has issues).
|
|
142
|
-
function replaceScopedErrors(
|
|
143
|
-
if (
|
|
142
|
+
function replaceScopedErrors(isInScope, grouped) {
|
|
143
|
+
if (isInScope === undefined) {
|
|
144
144
|
errors = Object.freeze(grouped);
|
|
145
145
|
// skip: unscoped replace — wholesale overwrite, nothing left to merge
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
const merged = {};
|
|
149
149
|
for (const [path, issues] of Object.entries(errors)) {
|
|
150
|
-
if (!
|
|
150
|
+
if (!isInScope(path))
|
|
151
151
|
merged[path] = issues;
|
|
152
152
|
}
|
|
153
153
|
Object.assign(merged, grouped);
|
|
@@ -155,13 +155,15 @@ export function createFormController(options) {
|
|
|
155
155
|
}
|
|
156
156
|
function runValidate(scope, opts) {
|
|
157
157
|
// See the validate() doc comment in types.ts for the scope contract.
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
const
|
|
158
|
+
// A scope entry matches its exact path and everything below it, but not
|
|
159
|
+
// siblings: `address.city` must not pull in `address.zip` (#1898).
|
|
160
|
+
const isInScope = scope === undefined
|
|
161
|
+
? undefined
|
|
162
|
+
: (path) => scope.some((entry) => path === entry || path.startsWith(`${entry}.`));
|
|
161
163
|
const includeRoot = opts?.includeRoot === true;
|
|
162
164
|
if (!options.schema) {
|
|
163
165
|
if (Object.keys(errors).length > 0) {
|
|
164
|
-
replaceScopedErrors(
|
|
166
|
+
replaceScopedErrors(isInScope, {});
|
|
165
167
|
invalidate();
|
|
166
168
|
}
|
|
167
169
|
return true;
|
|
@@ -170,7 +172,7 @@ export function createFormController(options) {
|
|
|
170
172
|
const parsed = options.schema.safeParse(values);
|
|
171
173
|
if (parsed.success) {
|
|
172
174
|
if (Object.keys(errors).length > 0) {
|
|
173
|
-
replaceScopedErrors(
|
|
175
|
+
replaceScopedErrors(isInScope, {});
|
|
174
176
|
invalidate();
|
|
175
177
|
}
|
|
176
178
|
return true;
|
|
@@ -180,28 +182,22 @@ export function createFormController(options) {
|
|
|
180
182
|
if (!state.visible)
|
|
181
183
|
hiddenFields.add(fieldKey);
|
|
182
184
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// fields={…} cannot silently skip cross-field rules (#1907).
|
|
191
|
-
const isRootIssue = issue.path === "(root)";
|
|
192
|
-
if (scopeSet && !(includeRoot && isRootIssue) && !scopeSet.has(rootField)) {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
return true;
|
|
185
|
+
// Root-level .refine() issues use path "(root)". Wizard step validate()
|
|
186
|
+
// still drops them (#1885); submit() passes includeRoot so RenderEdit
|
|
187
|
+
// fields={…} cannot silently skip cross-field rules (#1907).
|
|
188
|
+
const relevantIssues = relevantFieldIssues(zodErrorToFieldIssues(parsed.error), {
|
|
189
|
+
hiddenFields,
|
|
190
|
+
...(scope !== undefined && { scope }),
|
|
191
|
+
includeRoot,
|
|
196
192
|
});
|
|
197
193
|
if (relevantIssues.length === 0) {
|
|
198
194
|
if (Object.keys(errors).length > 0) {
|
|
199
|
-
replaceScopedErrors(
|
|
195
|
+
replaceScopedErrors(isInScope, {});
|
|
200
196
|
invalidate();
|
|
201
197
|
}
|
|
202
198
|
return true;
|
|
203
199
|
}
|
|
204
|
-
replaceScopedErrors(
|
|
200
|
+
replaceScopedErrors(isInScope, groupIssuesByPath(relevantIssues));
|
|
205
201
|
invalidate();
|
|
206
202
|
return false;
|
|
207
203
|
}
|
package/dist/form/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { createFormController } from "./form-controller.js";
|
|
2
2
|
export type { FieldConditionPredicate, FieldConditions, FieldConditionValue, FieldState, FormController, FormControllerOptions, FormSnapshot, FormValues, SubmitConfig, SubmitPayloadMode, SubmitResult, } from "./types.js";
|
|
3
|
-
export { groupIssuesByPath, I18N_KEY_PARAM, zodErrorToFieldIssues } from "./zod-bridge.js";
|
|
3
|
+
export { groupIssuesByPath, I18N_KEY_PARAM, relevantFieldIssues, zodErrorToFieldIssues, } from "./zod-bridge.js";
|
package/dist/form/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createFormController } from "./form-controller.js";
|
|
2
|
-
export { groupIssuesByPath, I18N_KEY_PARAM, zodErrorToFieldIssues } from "./zod-bridge.js";
|
|
2
|
+
export { groupIssuesByPath, I18N_KEY_PARAM, relevantFieldIssues, zodErrorToFieldIssues, } from "./zod-bridge.js";
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ZodError } from "zod";
|
|
2
2
|
import type { FieldIssue } from "../dispatcher/index.js";
|
|
3
3
|
export declare function zodErrorToFieldIssues(error: ZodError): FieldIssue[];
|
|
4
|
+
export declare function relevantFieldIssues(issues: readonly FieldIssue[], opts: {
|
|
5
|
+
readonly hiddenFields: ReadonlySet<string>;
|
|
6
|
+
readonly scope?: readonly string[];
|
|
7
|
+
readonly includeRoot?: boolean;
|
|
8
|
+
}): FieldIssue[];
|
|
4
9
|
export declare const I18N_KEY_PARAM = "i18nKey";
|
|
5
10
|
export declare function groupIssuesByPath(issues: readonly FieldIssue[]): Record<string, readonly FieldIssue[]>;
|
package/dist/form/zod-bridge.js
CHANGED
|
@@ -44,6 +44,24 @@ export function zodErrorToFieldIssues(error) {
|
|
|
44
44
|
};
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
+
// The issues that count for validity: hidden fields never block, and a
|
|
48
|
+
// scope narrows to the listed paths (an entry matches itself and everything
|
|
49
|
+
// below it, not siblings — #1898). `includeRoot` keeps "(root)" refine issues
|
|
50
|
+
// inside a scope (submit passes it, #1907). Shared by the controller's
|
|
51
|
+
// validate() and RenderEdit's `valid` dry-run so the two cannot drift.
|
|
52
|
+
export function relevantFieldIssues(issues, opts) {
|
|
53
|
+
const { hiddenFields, scope, includeRoot } = opts;
|
|
54
|
+
return issues.filter((issue) => {
|
|
55
|
+
const rootField = issue.path.split(".")[0] ?? "";
|
|
56
|
+
if (hiddenFields.has(rootField))
|
|
57
|
+
return false;
|
|
58
|
+
if (scope === undefined)
|
|
59
|
+
return true;
|
|
60
|
+
if (includeRoot === true && issue.path === "(root)")
|
|
61
|
+
return true;
|
|
62
|
+
return scope.some((entry) => issue.path === entry || issue.path.startsWith(`${entry}.`));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
47
65
|
// Same magic key both sides of a superRefine can set to opt out of the
|
|
48
66
|
// generic `errors.validation.custom` message — see form-schema.ts.
|
|
49
67
|
export const I18N_KEY_PARAM = "i18nKey";
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { AssetResolution, AssetResolveContext, AssetResolver, BadgeProps, B
|
|
|
4
4
|
export type { BatchResult, Command, Dispatcher, DispatcherError, DispatcherStatus, FieldIssue, PendingFile, PendingWrite, QueryOpts, QueryResult, StreamOpts, WriteOpts, WriteResult, } from "./dispatcher/index.js";
|
|
5
5
|
export { StreamFrame, type StreamFrameEvent } from "./dispatcher/index.js";
|
|
6
6
|
export type { FieldConditionPredicate, FieldConditions, FieldConditionValue, FieldState, FormController, FormControllerOptions, FormSnapshot, FormValues, SubmitConfig, SubmitPayloadMode, SubmitResult, } from "./form/index.js";
|
|
7
|
-
export { createFormController, I18N_KEY_PARAM } from "./form/index.js";
|
|
7
|
+
export { createFormController, I18N_KEY_PARAM, relevantFieldIssues, zodErrorToFieldIssues, } from "./form/index.js";
|
|
8
8
|
export { applyFormatSpec, currencyDecimals, escapeHtml, escapeHtmlAttr, escapeXml, type HtmlValue, html, isSafeHref, RawHtml, raw, stripControlChars, toInstant, } from "./format/index.js";
|
|
9
9
|
export type { NavDefinition, NavNode, NavRegistrySlice, NavTree, ResolveNavigationOptions, } from "./nav/index.js";
|
|
10
10
|
export { resolveNavigation } from "./nav/index.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Parser nutzt wie der Server-Boot-Validator (Cross-Feature-Refs).
|
|
3
3
|
export { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
4
4
|
export { StreamFrame } from "./dispatcher/index.js";
|
|
5
|
-
export { createFormController, I18N_KEY_PARAM } from "./form/index.js";
|
|
5
|
+
export { createFormController, I18N_KEY_PARAM, relevantFieldIssues, zodErrorToFieldIssues, } from "./form/index.js";
|
|
6
6
|
export { applyFormatSpec, currencyDecimals, escapeHtml, escapeHtmlAttr, escapeXml, html, isSafeHref, RawHtml, raw, stripControlChars, toInstant, } from "./format/index.js";
|
|
7
7
|
export { resolveNavigation } from "./nav/index.js";
|
|
8
8
|
export { createStore, shallowEqual } from "./store/index.js";
|
|
@@ -25,7 +25,9 @@ export type WrapUrlLocaleResolverOptions = {
|
|
|
25
25
|
resolvePage: (pathname: string) => string | undefined;
|
|
26
26
|
detectLang: (pathname: string) => string;
|
|
27
27
|
pathname?: () => string | undefined;
|
|
28
|
-
/** Optional pathname-change subscription
|
|
28
|
+
/** Optional pathname-change subscription. Browser default listens to `popstate` only,
|
|
29
|
+
* which does not fire on `history.pushState`/`replaceState`: apps with a client-side
|
|
30
|
+
* router must supply this themselves. */
|
|
29
31
|
subscribePathname?: (listener: () => void) => () => void;
|
|
30
32
|
/** When set, translate against the URL-resolved locale instead of base.translate
|
|
31
33
|
* (needed for fixed-T / cloneInstance resolvers that are not locale-live). */
|
package/dist/view-model/edit.js
CHANGED
|
@@ -95,9 +95,7 @@ function deriveSelectOptionsQuery(fieldDef) {
|
|
|
95
95
|
: {};
|
|
96
96
|
}
|
|
97
97
|
function deriveSelectFieldHints(fieldDef, translate, featureName, entityName, fieldName) {
|
|
98
|
-
const options = fieldDef.type === "select" || fieldDef.type === "multiSelect"
|
|
99
|
-
? (fieldDef.options ?? [])
|
|
100
|
-
: undefined;
|
|
98
|
+
const options = fieldDef.type === "select" || fieldDef.type === "multiSelect" ? fieldDef.options : undefined;
|
|
101
99
|
const optionLabels = options !== undefined
|
|
102
100
|
? buildOptionLabels(translate, (value) => fieldOptionLabelKey(featureName, entityName, fieldName, value), options)
|
|
103
101
|
: undefined;
|
|
@@ -264,13 +262,17 @@ function deriveEmbeddedListHints(fieldDef, translate, featureName, entityName, f
|
|
|
264
262
|
};
|
|
265
263
|
}
|
|
266
264
|
function buildExtensionSectionViewModel(sectionSpec, translate) {
|
|
265
|
+
const entityNameOverride = sectionSpec.entityName?.trim();
|
|
267
266
|
return {
|
|
268
267
|
kind: "extension",
|
|
269
268
|
...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
|
|
270
269
|
title: translate(sectionSpec.title),
|
|
271
270
|
component: sectionSpec.component,
|
|
272
271
|
contributesToFormSubmit: sectionSpec.contributesToFormSubmit === true,
|
|
273
|
-
|
|
272
|
+
// Blank override must fall back to the host value: the renderer's `??`
|
|
273
|
+
// would otherwise pass "" through to the mounted extension.
|
|
274
|
+
...(entityNameOverride !== undefined &&
|
|
275
|
+
entityNameOverride.length > 0 && { entityName: entityNameOverride }),
|
|
274
276
|
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
275
277
|
};
|
|
276
278
|
}
|
package/dist/view-model/list.js
CHANGED
|
@@ -117,7 +117,7 @@ export function computeListViewModel(input) {
|
|
|
117
117
|
// form. A missing key returns the key itself per convention; the
|
|
118
118
|
// renderer then falls back to humanizeSlug.
|
|
119
119
|
const optionLabels = fieldDef.type === "select" || fieldDef.type === "multiSelect"
|
|
120
|
-
? buildOptionLabels(translate, (value) => fieldOptionLabelKey(featureName, screen.entity, normalized.field, value), fieldDef.options
|
|
120
|
+
? buildOptionLabels(translate, (value) => fieldOptionLabelKey(featureName, screen.entity, normalized.field, value), fieldDef.options)
|
|
121
121
|
: undefined;
|
|
122
122
|
const optionTones = fieldDef.type === "select" ? fieldDef.optionTones : undefined;
|
|
123
123
|
const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.336.0",
|
|
4
4
|
"description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
39
|
+
"@cosmicdrift/kumiko-framework": "0.336.0",
|
|
40
40
|
"temporal-polyfill": "^0.3.2",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|