@cosmicdrift/kumiko-framework 0.373.0 → 0.374.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/agents/widgets.md +1 -0
- package/dist/engine/boot-validator/projection-list-screens.js +28 -37
- package/dist/engine/boot-validator/query-refs.js +26 -8
- package/dist/engine/boot-validator/screens.d.ts +1 -1
- package/dist/engine/boot-validator/screens.js +327 -41
- package/dist/engine/boot-validator/url-prefill-fields.js +3 -0
- package/dist/engine/build-app-schema.js +7 -3
- package/dist/engine/entity-list-searchability.d.ts +5 -0
- package/dist/engine/entity-list-searchability.js +35 -0
- package/dist/engine/feature-ui-extensions.js +5 -2
- package/dist/engine/index.d.ts +2 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/paged-list.d.ts +29 -0
- package/dist/engine/paged-list.js +56 -0
- package/dist/engine/project-app-schema-for-roles.js +37 -6
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +50 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/package.json +5 -5
- package/src/changes.json +61 -0
package/agents/widgets.md
CHANGED
|
@@ -118,6 +118,7 @@ Value exports of `@cosmicdrift/kumiko-renderer-web`.
|
|
|
118
118
|
| `useDraft` | Formular-State für Rechner-Screens: ein Draft-Objekt + `patch` für Teil-Updates + `field(name)` das die `{ id, name, value, onChange }`-Props fertig für die Feld-Widgets (NumberField/MoneyField/PercentField) liefert. |
|
|
119
119
|
| `useForm` | |
|
|
120
120
|
| `useIsNarrowViewport` | |
|
|
121
|
+
| `useIsViewportBelow` | |
|
|
121
122
|
| `useLiveEvents` | Hook für useQuery-like Consumer. |
|
|
122
123
|
| `useLocale` | Liefert den aktuellen LocaleResolver und abonniert automatisch Locale-Änderungen — der aufrufende Component re-rendert sobald die Sprache gewechselt wird. |
|
|
123
124
|
| `useNav` | |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isListQueryWithoutSearchableFields, resolveEntityBehindListQuery, } from "../entity-list-searchability.js";
|
|
2
|
+
import { QnTypes, qualifyEntityName } from "../qualified-name.js";
|
|
3
3
|
import { isEditLayoutScreen } from "../screen-helpers.js";
|
|
4
4
|
import { relatedListGroupHeaderLabel, relatedListGroupKey, } from "../types/index.js";
|
|
5
5
|
import { SEARCHABLE_FALSE_WHITELIST } from "./entity-list-screens.js";
|
|
@@ -28,28 +28,38 @@ function schemaAccepts(schema, key) {
|
|
|
28
28
|
const shape = getZodObjectShape(schema);
|
|
29
29
|
return shape !== undefined && key in shape;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
// sortable/paginated are derived by buildAppSchema from the query's Zod
|
|
32
|
+
// schema (fw#2165) — there is no separate wire type from the author-facing
|
|
33
|
+
// ProjectionListScreenDefinition, so hand-authoring them would otherwise be
|
|
34
|
+
// silently overwritten with no signal to the author. Reject outright.
|
|
35
|
+
function rejectHandAuthoredDerivedFlags(prefix, screen) {
|
|
36
|
+
if (screen.sortable !== undefined) {
|
|
37
|
+
throw new Error(`${prefix}: sortable is derived from the query's Zod schema, don't set it`);
|
|
38
|
+
}
|
|
39
|
+
if (screen.paginated !== undefined) {
|
|
40
|
+
throw new Error(`${prefix}: paginated is derived from the query's Zod schema, don't set it`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function validateOneProjectionListScreen(feature, screen, queryHandlers, featureMap) {
|
|
32
44
|
const prefix = `[projectionList] Feature "${feature.name}" screen "${screen.id}"`;
|
|
33
|
-
const
|
|
45
|
+
const handler = queryHandlers.get(screen.query);
|
|
46
|
+
const schema = handler?.schema;
|
|
47
|
+
// buildAppSchema derives searchable: false for these, so no whitelist entry is needed.
|
|
48
|
+
const searchDerivedOff = isListQueryWithoutSearchableFields(screen.query, handler, (name) => findEntity(featureMap, name));
|
|
34
49
|
if (screen.searchable === true && !schemaAccepts(schema, "search")) {
|
|
35
50
|
throw new Error(`${prefix}: searchable: true but query "${screen.query}" has no "search" parameter in its Zod schema. Add a "search" field to the query schema or remove searchable: true.`);
|
|
36
51
|
}
|
|
52
|
+
if (screen.searchable === true && searchDerivedOff) {
|
|
53
|
+
throw new Error(`${prefix}: searchable: true but query "${screen.query}" lists an entity with no searchable field, so a search would never match. Mark a field searchable: true on the entity or remove searchable: true.`);
|
|
54
|
+
}
|
|
37
55
|
if (screen.searchable === false &&
|
|
38
56
|
schemaAccepts(schema, "search") &&
|
|
57
|
+
!searchDerivedOff &&
|
|
39
58
|
!SEARCHABLE_FALSE_WHITELIST.has(screen.id)) {
|
|
40
59
|
throw new Error(`${prefix}: query "${screen.query}" accepts "search" but searchable: false disables it — remove searchable: false or add "${screen.id}" to SEARCHABLE_FALSE_WHITELIST`);
|
|
41
60
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// ProjectionListScreenDefinition, so hand-authoring them would otherwise be
|
|
45
|
-
// silently overwritten with no signal to the author. Reject outright.
|
|
46
|
-
if (screen.sortable !== undefined) {
|
|
47
|
-
throw new Error(`${prefix}: sortable is derived from the query's Zod schema, don't set it`);
|
|
48
|
-
}
|
|
49
|
-
if (screen.paginated !== undefined) {
|
|
50
|
-
throw new Error(`${prefix}: paginated is derived from the query's Zod schema, don't set it`);
|
|
51
|
-
}
|
|
52
|
-
const searchActive = screen.searchable !== false && schemaAccepts(schema, "search");
|
|
61
|
+
rejectHandAuthoredDerivedFlags(prefix, screen);
|
|
62
|
+
const searchActive = screen.searchable !== false && !searchDerivedOff && schemaAccepts(schema, "search");
|
|
53
63
|
const sortActive = schemaAccepts(schema, "sort");
|
|
54
64
|
if ((searchActive || sortActive) && screen.defaultSort === undefined) {
|
|
55
65
|
throw new Error(`${prefix}: defaultSort required when search or sort is active. Set defaultSort: { field, direction }.`);
|
|
@@ -132,11 +142,12 @@ function validateProjectionListFacetsSchemaAcceptance(prefix, screen, schema) {
|
|
|
132
142
|
}
|
|
133
143
|
export function validateProjectionListScreens(features) {
|
|
134
144
|
const queryHandlers = buildQueryHandlerMap(features);
|
|
145
|
+
const featureMap = new Map(features.map((f) => [f.name, f]));
|
|
135
146
|
for (const feature of features) {
|
|
136
147
|
for (const screen of Object.values(feature.screens)) {
|
|
137
148
|
if (screen.type !== "projectionList")
|
|
138
149
|
continue;
|
|
139
|
-
validateOneProjectionListScreen(feature, screen, queryHandlers);
|
|
150
|
+
validateOneProjectionListScreen(feature, screen, queryHandlers, featureMap);
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
153
|
}
|
|
@@ -155,26 +166,6 @@ function validateRelatedListFacets(prefix, section, schema) {
|
|
|
155
166
|
return;
|
|
156
167
|
throw new Error(`${prefix}: declares facets but query "${section.query}" has no "filters" parameter in its Zod schema — add filters: z.array(z.object({ field: z.string(), op: z.literal("in"), value: z.unknown() })).optional() (or reuse entityListSchema's shape) to the handler's schema.`);
|
|
157
168
|
}
|
|
158
|
-
// parentFilter resolves an entity field map to
|
|
159
|
-
// check `field` against ONLY when the query is one of the entity-convention
|
|
160
|
-
// factories (defineEntityListHandler et al., branded by
|
|
161
|
-
// ENTITY_CONVENTION_QUERY_BRAND) — a hand-written handler carries no entity,
|
|
162
|
-
// same "capability absent, no throw" policy as schemaAccepts above. The
|
|
163
|
-
// entity name comes from the handler's own `<entityName>:list` naming
|
|
164
|
-
// convention (parseHandlerName in entity-handlers.ts), recovered here from
|
|
165
|
-
// the query's fully qualified name.
|
|
166
|
-
function resolveEntityBehindQuery(queryQn, handler, featureMap) {
|
|
167
|
-
if (handler === undefined || handler[ENTITY_CONVENTION_QUERY_BRAND] !== true)
|
|
168
|
-
return undefined;
|
|
169
|
-
let entityName;
|
|
170
|
-
try {
|
|
171
|
-
entityName = parseQn(queryQn).name.split(":")[0] ?? "";
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
return undefined;
|
|
175
|
-
}
|
|
176
|
-
return entityName === "" ? undefined : findEntity(featureMap, entityName);
|
|
177
|
-
}
|
|
178
169
|
// parentFilter lets a relatedList reuse the
|
|
179
170
|
// generic `<entity>:list` query instead of a bespoke child-rows handler —
|
|
180
171
|
// same 422 footgun as facets/filter above, plus its own mutual-exclusivity
|
|
@@ -193,7 +184,7 @@ function validateRelatedListParentFilter(prefix, section, schema, handler, featu
|
|
|
193
184
|
`Zod schema — add filter: z.object({ field: z.string(), op: z.enum(["eq","ne","lt","gt","lte","gte","in"]), ` +
|
|
194
185
|
`value: z.unknown() }).optional() (or reuse entityListSchema's shape) to the handler's schema.`);
|
|
195
186
|
}
|
|
196
|
-
const entity =
|
|
187
|
+
const entity = resolveEntityBehindListQuery(section.query, handler, (name) => findEntity(featureMap, name));
|
|
197
188
|
if (entity !== undefined && entity.fields[section.parentFilter.field] === undefined) {
|
|
198
189
|
throw new Error(`${prefix}: parentFilter.field "${section.parentFilter.field}" is not a declared field on the ` +
|
|
199
190
|
`entity behind query "${section.query}". Known fields: ${Object.keys(entity.fields).sort().join(", ")}.`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs } from "../screen-helpers.js";
|
|
1
2
|
import { buildQueryHandlerMap } from "./projection-list-screens.js";
|
|
2
3
|
const NOT_REGISTERED_SUFFIX = "is not a registered query-handler. Check the QN spelling (expected " +
|
|
3
4
|
'"<feature>:query:<short>") and that the handler is declared via r.queryHandler(...).';
|
|
@@ -29,19 +30,30 @@ function checkVisibleWhenQueryRef(queryHandlers, query, buildPrefix) {
|
|
|
29
30
|
// types) keeps this collector correct if that restriction is ever lifted.
|
|
30
31
|
function checkEditLayoutQueryRefs(queryHandlers, featureName, screenId, screenType, layout) {
|
|
31
32
|
for (const section of layout.sections) {
|
|
33
|
+
if (isFieldsEditSection(section)) {
|
|
34
|
+
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
35
|
+
const { field, fillAction } = normalizeEditField(fieldSpec);
|
|
36
|
+
if (fillAction === undefined)
|
|
37
|
+
continue;
|
|
38
|
+
checkQueryRef(queryHandlers, fillAction.query, () => `[Feature ${featureName}] Screen "${screenId}" (${screenType}) field "${field}" fillAction`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
32
41
|
if (section.kind !== "relatedList")
|
|
33
42
|
continue;
|
|
34
43
|
checkQueryRef(queryHandlers, section.query, () => `[Feature ${featureName}] Screen "${screenId}" (${screenType}) relatedList section "${section.title}"`);
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
46
|
function checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
47
|
+
if (screen.record?.query !== undefined) {
|
|
48
|
+
checkQueryRef(queryHandlers, screen.record.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) record`);
|
|
49
|
+
}
|
|
38
50
|
for (const panel of screen.panels) {
|
|
39
51
|
if (panel.kind === "custom")
|
|
40
52
|
continue;
|
|
41
53
|
if (panel.visibleWhen !== undefined) {
|
|
42
54
|
checkVisibleWhenQueryRef(queryHandlers, panel.visibleWhen.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) ${panel.kind === "screen" ? "screen-panel" : "panel"} "${panel.id}" visibleWhen`);
|
|
43
55
|
}
|
|
44
|
-
if (panel.kind === "screen")
|
|
56
|
+
if (panel.kind === "screen" || panel.kind === "text")
|
|
45
57
|
continue;
|
|
46
58
|
if (panel.kind === "stat-group") {
|
|
47
59
|
for (const stat of panel.stats) {
|
|
@@ -70,6 +82,18 @@ function checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screenTyp
|
|
|
70
82
|
}
|
|
71
83
|
}
|
|
72
84
|
}
|
|
85
|
+
function checkEditFormScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
86
|
+
checkEditLayoutQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.layout);
|
|
87
|
+
if (screen.type === "actionForm" && screen.load !== undefined) {
|
|
88
|
+
checkQueryRef(queryHandlers, screen.load.query, () => `[Feature ${featureName}] Screen "${screenId}" (actionForm) load`);
|
|
89
|
+
}
|
|
90
|
+
if (screen.type !== "entityEdit") {
|
|
91
|
+
checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.fields);
|
|
92
|
+
}
|
|
93
|
+
if (screen.type === "secretMint" && screen.confirm !== undefined) {
|
|
94
|
+
checkFormFieldQueryRefs(queryHandlers, featureName, screenId, "secretMint confirm", screen.confirm.fields);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
73
97
|
function checkScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
74
98
|
if (screen.visibleWhen !== undefined) {
|
|
75
99
|
checkVisibleWhenQueryRef(queryHandlers, screen.visibleWhen.query, () => `[Feature ${featureName}] Screen "${screenId}" (${screen.type}) visibleWhen`);
|
|
@@ -90,13 +114,7 @@ function checkScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
|
90
114
|
screen.type === "actionForm" ||
|
|
91
115
|
screen.type === "configEdit" ||
|
|
92
116
|
screen.type === "secretMint") {
|
|
93
|
-
|
|
94
|
-
if (screen.type !== "entityEdit") {
|
|
95
|
-
checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.fields);
|
|
96
|
-
}
|
|
97
|
-
if (screen.type === "secretMint" && screen.confirm !== undefined) {
|
|
98
|
-
checkFormFieldQueryRefs(queryHandlers, featureName, screenId, "secretMint confirm", screen.confirm.fields);
|
|
99
|
-
}
|
|
117
|
+
checkEditFormScreenQueryRefs(queryHandlers, featureName, screenId, screen);
|
|
100
118
|
}
|
|
101
119
|
else if (screen.type === "dashboard") {
|
|
102
120
|
checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen);
|
|
@@ -3,7 +3,7 @@ import type { ConfigKeyDefinition, FeatureDefinition } from "../types/index.ts";
|
|
|
3
3
|
import type { ActionFormScreenDefinition, DashboardCustomPanel, DashboardScreenPanel, RelatedListToolbarAction, RowAction, RowFieldExtractor, ScreenDefinition, SecretMintScreenDefinition, ToolbarAction } from "../types/screen.ts";
|
|
4
4
|
export declare function rowFieldExtractorKeys(params: RowFieldExtractor): readonly string[];
|
|
5
5
|
export declare function readsNavigateParamsAsFormPrefill(target: ScreenDefinition, explicitEntityId: string | undefined, sourceScreenEntity: string | undefined): boolean;
|
|
6
|
-
export declare function validateActionHasIcon(featureName: string, screenId: string, screenType: "projectionDetail" | "entityEdit" | "entityList" | "projectionList", context: string, action: {
|
|
6
|
+
export declare function validateActionHasIcon(featureName: string, screenId: string, screenType: "projectionDetail" | "entityEdit" | "entityList" | "projectionList" | "dashboard", context: string, action: {
|
|
7
7
|
readonly id: string;
|
|
8
8
|
readonly icon?: IconKey;
|
|
9
9
|
}): void;
|