@cosmicdrift/kumiko-framework 0.330.2 → 0.331.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/engine/boot-validator/action-wiring.js +3 -0
- package/dist/engine/boot-validator/config-deps.d.ts +1 -0
- package/dist/engine/boot-validator/config-deps.js +24 -0
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +20 -0
- package/dist/engine/boot-validator/index.js +4 -2
- package/dist/engine/boot-validator/projection-list-screens.js +11 -6
- package/dist/engine/boot-validator/query-output-columns.js +8 -0
- package/dist/engine/boot-validator/query-refs.js +23 -0
- package/dist/engine/boot-validator/screens.js +147 -76
- package/dist/engine/boot-validator/url-prefill-fields.js +11 -1
- package/dist/engine/build-app-schema.js +3 -0
- package/dist/engine/build-config-feature-schema.js +9 -0
- package/dist/engine/config-helpers.d.ts +2 -0
- package/dist/engine/config-helpers.js +9 -0
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/project-app-schema-for-roles.js +10 -1
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +27 -10
- package/dist/ui-types/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/changes.json +14 -0
|
@@ -38,6 +38,9 @@ export function validateActionWiring(feature) {
|
|
|
38
38
|
for (const action of screen.toolbarActions ?? []) {
|
|
39
39
|
validateActionNoFunctions(feature.name, screen.id, "toolbarAction", action);
|
|
40
40
|
}
|
|
41
|
+
if (screen.type === "entityList" && screen.expandableRow !== undefined) {
|
|
42
|
+
validateColumnsNoFunctions(feature.name, screen.id, "entityList", screen.expandableRow.columns);
|
|
43
|
+
}
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
// EditFieldSpec's visible/readOnly/required are the same FieldCondition DSL
|
|
@@ -4,6 +4,7 @@ export declare function validateConfigKeyBounds(feature: FeatureDefinition): voi
|
|
|
4
4
|
export declare function validateConfigKeyComputed(feature: FeatureDefinition): void;
|
|
5
5
|
export declare function validateConfigKeyRequired(feature: FeatureDefinition): void;
|
|
6
6
|
export declare function validateConfigKeyAllowPerRequest(feature: FeatureDefinition): void;
|
|
7
|
+
export declare function validateConfigKeyOptionsQuery(feature: FeatureDefinition): void;
|
|
7
8
|
export declare function validateConfigKeyBacking(feature: FeatureDefinition): void;
|
|
8
9
|
export declare function validateConfigKeyPiiEncrypted(feature: FeatureDefinition): void;
|
|
9
10
|
export declare function validateConfigReads(features: readonly FeatureDefinition[], allConfigKeys: ReadonlySet<string>): void;
|
|
@@ -101,6 +101,30 @@ export function validateConfigKeyAllowPerRequest(feature) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
// --- Config key optionsQuery compatibility ---
|
|
105
|
+
export function validateConfigKeyOptionsQuery(feature) {
|
|
106
|
+
for (const [keyName, keyDef] of Object.entries(feature.configKeys)) {
|
|
107
|
+
const prefix = `[Feature ${feature.name}] Config key "${keyName}"`;
|
|
108
|
+
if (keyDef.optionsQuery === undefined) {
|
|
109
|
+
if (keyDef.optionsQueryPayload !== undefined) {
|
|
110
|
+
throw new Error(`${prefix} has optionsQueryPayload without optionsQuery`);
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (keyDef.optionsQuery.length === 0) {
|
|
115
|
+
throw new Error(`${prefix} has an empty optionsQuery`);
|
|
116
|
+
}
|
|
117
|
+
if (keyDef.type !== "select") {
|
|
118
|
+
throw new Error(`${prefix} has optionsQuery but type="${keyDef.type}" — optionsQuery is only valid on select keys`);
|
|
119
|
+
}
|
|
120
|
+
if (keyDef.options !== undefined && keyDef.options.length > 0) {
|
|
121
|
+
throw new Error(`${prefix} declares both options and optionsQuery — pick one`);
|
|
122
|
+
}
|
|
123
|
+
if (keyDef.allowPerRequest) {
|
|
124
|
+
throw new Error(`${prefix} has optionsQuery AND allowPerRequest=true — a per-request value would bypass the option set unchecked`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
104
128
|
// --- Config key storage backing × scope matrix ---
|
|
105
129
|
export function validateConfigKeyBacking(feature) {
|
|
106
130
|
for (const [keyName, keyDef] of Object.entries(feature.configKeys)) {
|
|
@@ -16,6 +16,7 @@ export declare function validateEncryptedFields(feature: FeatureDefinition): boo
|
|
|
16
16
|
export declare function validateFileFields(feature: FeatureDefinition): boolean;
|
|
17
17
|
export declare function validateReferenceFields(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>, queryHandlers: ReadonlyMap<string, QueryHandlerDef>): void;
|
|
18
18
|
export declare function validateEmbeddedFields(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>, queryHandlers: ReadonlyMap<string, QueryHandlerDef>): void;
|
|
19
|
+
export declare function validateEntitySelectOptionsQuery(feature: FeatureDefinition): void;
|
|
19
20
|
export declare function validateMultiSelectFields(feature: FeatureDefinition): void;
|
|
20
21
|
export declare function validateImageVariants(feature: FeatureDefinition): void;
|
|
21
22
|
export declare function validateTransitions(feature: FeatureDefinition): void;
|
|
@@ -593,6 +593,26 @@ function validateCheckboxDisplayOptions(field, fieldName, entityName) {
|
|
|
593
593
|
throw new Error(`MultiSelect field "${fieldName}" on entity "${entityName}" sets maxRows, which is only valid with display: "checkboxes".`);
|
|
594
594
|
}
|
|
595
595
|
}
|
|
596
|
+
export function validateEntitySelectOptionsQuery(feature) {
|
|
597
|
+
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
598
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
599
|
+
assertNoSelectOptionsQuery(field, `${entityName}.${fieldName}`, feature.name);
|
|
600
|
+
if (field.type !== "embedded")
|
|
601
|
+
continue;
|
|
602
|
+
for (const [subName, subField] of Object.entries(field.schema ?? {})) {
|
|
603
|
+
assertNoSelectOptionsQuery(subField, `${entityName}.${fieldName}.${subName}`, feature.name);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function assertNoSelectOptionsQuery(field, path, featureName) {
|
|
609
|
+
// @cast-boundary schema-walk — embedded sub-field types don't declare optionsQuery, an untyped author may still set it
|
|
610
|
+
const { optionsQuery, optionsQueryPayload } = field;
|
|
611
|
+
const declaresOptionsQuery = optionsQuery !== undefined || optionsQueryPayload !== undefined;
|
|
612
|
+
if (field.type === "select" && declaresOptionsQuery) {
|
|
613
|
+
throw new Error(`[Feature ${featureName}] Entity select field "${path}" declares optionsQuery — not supported on entity fields because the stored value would not be validated against the query result. Use static options, or a reference field.`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
596
616
|
export function validateMultiSelectFields(feature) {
|
|
597
617
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
598
618
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
@@ -7,9 +7,9 @@ import { warnOnUniqueAccessRoles } from "./access-roles.js";
|
|
|
7
7
|
import { validateActionWiring, validateFieldWiring } from "./action-wiring.js";
|
|
8
8
|
import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext.js";
|
|
9
9
|
import { validateFeatureBootChecks } from "./boot-check.js";
|
|
10
|
-
import { validateCircularDeps, validateConfigKeyAllowPerRequest, validateConfigKeyBacking, validateConfigKeyBounds, validateConfigKeyComputed, validateConfigKeyPiiEncrypted, validateConfigKeyRequired, validateConfigReads, warnOnToggleableDependencies, } from "./config-deps.js";
|
|
10
|
+
import { validateCircularDeps, validateConfigKeyAllowPerRequest, validateConfigKeyBacking, validateConfigKeyBounds, validateConfigKeyComputed, validateConfigKeyOptionsQuery, validateConfigKeyPiiEncrypted, validateConfigKeyRequired, validateConfigReads, warnOnToggleableDependencies, } from "./config-deps.js";
|
|
11
11
|
import { collectDetailForScreens } from "./detail-screens.js";
|
|
12
|
-
import { validateDerivedFieldCollisions, validateEmbeddedFields, validateEncryptedFields, validateEntityIndexes, validateExtendSchemaCollisions, validateExtensionPreSaveWiring, validateFileFields, validateHandlerAccess, validateImageVariants, validateLocatedTimestamps, validateMultiSelectFields, validateMultiStreamProjections, validateReferenceFields, validateTransitions, } from "./entity-handler.js";
|
|
12
|
+
import { validateDerivedFieldCollisions, validateEmbeddedFields, validateEncryptedFields, validateEntityIndexes, validateEntitySelectOptionsQuery, validateExtendSchemaCollisions, validateExtensionPreSaveWiring, validateFileFields, validateHandlerAccess, validateImageVariants, validateLocatedTimestamps, validateMultiSelectFields, validateMultiStreamProjections, validateReferenceFields, validateTransitions, } from "./entity-handler.js";
|
|
13
13
|
import { validateEntityListScreens } from "./entity-list-screens.js";
|
|
14
14
|
import { validateGdprStoragePersistence } from "./gdpr-storage.js";
|
|
15
15
|
import { validateGlobalTenancyEntities } from "./global-tenancy.js";
|
|
@@ -122,6 +122,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
122
122
|
validatePiiAndRetention(feature);
|
|
123
123
|
validateRecordOwnedSubjects(feature);
|
|
124
124
|
validateApiExposureMatching(feature, allExposedApis, featureMap);
|
|
125
|
+
validateEntitySelectOptionsQuery(feature);
|
|
125
126
|
validateEmbeddedFields(feature, featureMap, queryHandlerQns);
|
|
126
127
|
validateMultiSelectFields(feature);
|
|
127
128
|
validateImageVariants(feature);
|
|
@@ -139,6 +140,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
139
140
|
validateConfigKeyComputed(feature);
|
|
140
141
|
validateConfigKeyAllowPerRequest(feature);
|
|
141
142
|
validateConfigKeyBacking(feature);
|
|
143
|
+
validateConfigKeyOptionsQuery(feature);
|
|
142
144
|
validateConfigKeyPiiEncrypted(feature);
|
|
143
145
|
validateOwnershipRules(feature, allClaimKeys, knownRoles);
|
|
144
146
|
validateParentRefs(feature, featureMap);
|
|
@@ -200,19 +200,24 @@ function validateRelatedListParentFilter(prefix, section, schema, handler, featu
|
|
|
200
200
|
export function validateRelatedListSectionQueries(features) {
|
|
201
201
|
const queryHandlers = buildQueryHandlerMap(features);
|
|
202
202
|
const featureMap = new Map(features.map((f) => [f.name, f]));
|
|
203
|
+
const validateSectionQuery = (prefix, section) => {
|
|
204
|
+
const handler = queryHandlers.get(section.query);
|
|
205
|
+
const schema = handler?.schema;
|
|
206
|
+
validateRelatedListSearchable(prefix, section, schema);
|
|
207
|
+
validateRelatedListFacets(prefix, section, schema);
|
|
208
|
+
validateRelatedListParentFilter(prefix, section, schema, handler, featureMap);
|
|
209
|
+
};
|
|
203
210
|
for (const feature of features) {
|
|
204
211
|
for (const screen of Object.values(feature.screens)) {
|
|
212
|
+
if (screen.type === "entityList" && screen.expandableRow !== undefined) {
|
|
213
|
+
validateSectionQuery(`[Feature ${feature.name}] Screen "${screen.id}" (entityList) expandableRow "${screen.expandableRow.title}"`, screen.expandableRow);
|
|
214
|
+
}
|
|
205
215
|
if (!isEditLayoutScreen(screen))
|
|
206
216
|
continue;
|
|
207
217
|
for (const section of screen.layout.sections) {
|
|
208
218
|
if (section.kind !== "relatedList")
|
|
209
219
|
continue;
|
|
210
|
-
|
|
211
|
-
const handler = queryHandlers.get(section.query);
|
|
212
|
-
const schema = handler?.schema;
|
|
213
|
-
validateRelatedListSearchable(prefix, section, schema);
|
|
214
|
-
validateRelatedListFacets(prefix, section, schema);
|
|
215
|
-
validateRelatedListParentFilter(prefix, section, schema, handler, featureMap);
|
|
220
|
+
validateSectionQuery(`[Feature ${feature.name}] Screen "${screen.id}" (${screen.type}) relatedList section "${section.title}"`, section);
|
|
216
221
|
}
|
|
217
222
|
}
|
|
218
223
|
}
|
|
@@ -139,6 +139,14 @@ function checkScreenOutputColumns(queryHandlers, featureName, screenId, screen)
|
|
|
139
139
|
if (screen.type === "projectionList") {
|
|
140
140
|
checkProjectionListOutputColumns(queryHandlers, featureName, screenId, screen);
|
|
141
141
|
}
|
|
142
|
+
else if (screen.type === "entityList") {
|
|
143
|
+
if (screen.expandableRow !== undefined) {
|
|
144
|
+
const rowShape = getZodRowShape(queryHandlers.get(screen.expandableRow.query)?.outputSchema);
|
|
145
|
+
for (const column of screen.expandableRow.columns) {
|
|
146
|
+
checkColumnField(rowShape, column, (field) => `[Feature ${featureName}] Screen "${screenId}" (entityList) expandableRow "${screen.expandableRow?.title}" column "${field}" is not present in query "${screen.expandableRow?.query}"'s outputSchema — check for a typo, or add a "label" to mark it a virtual/computed column.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
142
150
|
else if (screen.type === "projectionDetail") {
|
|
143
151
|
checkProjectionDetailOutputFields(queryHandlers, featureName, screenId, screen);
|
|
144
152
|
checkEditLayoutOutputColumns(queryHandlers, featureName, screenId, "projectionDetail", screen.layout);
|
|
@@ -44,10 +44,22 @@ function checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
|
44
44
|
checkQueryRef(queryHandlers, screen.filter.optionsQuery, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) filter "${screen.filter?.id}"`);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
function checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screenType, fields) {
|
|
48
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
49
|
+
if (field.type !== "select" || field.optionsQuery === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
checkQueryRef(queryHandlers, field.optionsQuery, () => `[Feature ${featureName}] Screen "${screenId}" (${screenType}) select field "${fieldName}" optionsQuery`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
47
54
|
function checkScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
48
55
|
if (screen.type === "projectionList") {
|
|
49
56
|
checkQueryRef(queryHandlers, screen.query, () => `[Feature ${featureName}] Screen "${screenId}" (projectionList)`);
|
|
50
57
|
}
|
|
58
|
+
else if (screen.type === "entityList") {
|
|
59
|
+
if (screen.expandableRow !== undefined) {
|
|
60
|
+
checkQueryRef(queryHandlers, screen.expandableRow.query, () => `[Feature ${featureName}] Screen "${screenId}" (entityList) expandableRow "${screen.expandableRow?.title}"`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
51
63
|
else if (screen.type === "projectionDetail") {
|
|
52
64
|
checkQueryRef(queryHandlers, screen.query, () => `[Feature ${featureName}] Screen "${screenId}" (projectionDetail)`);
|
|
53
65
|
checkEditLayoutQueryRefs(queryHandlers, featureName, screenId, "projectionDetail", screen.layout);
|
|
@@ -57,6 +69,12 @@ function checkScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
|
57
69
|
screen.type === "configEdit" ||
|
|
58
70
|
screen.type === "secretMint") {
|
|
59
71
|
checkEditLayoutQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.layout);
|
|
72
|
+
if (screen.type !== "entityEdit") {
|
|
73
|
+
checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.fields);
|
|
74
|
+
}
|
|
75
|
+
if (screen.type === "secretMint" && screen.confirm !== undefined) {
|
|
76
|
+
checkFormFieldQueryRefs(queryHandlers, featureName, screenId, "secretMint confirm", screen.confirm.fields);
|
|
77
|
+
}
|
|
60
78
|
}
|
|
61
79
|
else if (screen.type === "dashboard") {
|
|
62
80
|
checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen);
|
|
@@ -65,6 +83,11 @@ function checkScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
|
65
83
|
export function validateQueryRefs(features) {
|
|
66
84
|
const queryHandlers = buildQueryHandlerMap(features);
|
|
67
85
|
for (const feature of features) {
|
|
86
|
+
for (const [keyName, keyDef] of Object.entries(feature.configKeys)) {
|
|
87
|
+
if (keyDef.optionsQuery === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
checkQueryRef(queryHandlers, keyDef.optionsQuery, () => `[Feature ${feature.name}] Config key "${keyName}" optionsQuery`);
|
|
90
|
+
}
|
|
68
91
|
for (const [screenId, screen] of Object.entries(feature.screens)) {
|
|
69
92
|
checkScreenQueryRefs(queryHandlers, feature.name, screenId, screen);
|
|
70
93
|
}
|
|
@@ -471,9 +471,28 @@ function validateFormFieldsMap(featureName, screenId, context, fields, allowEmpt
|
|
|
471
471
|
if (ftype === "money") {
|
|
472
472
|
validateFormMoneyCurrency(featureName, screenId, context, fname, fdef);
|
|
473
473
|
}
|
|
474
|
+
validateFormSelectOptions(featureName, screenId, context, fname, fdef);
|
|
474
475
|
}
|
|
475
476
|
return fieldNames;
|
|
476
477
|
}
|
|
478
|
+
// Static options XOR optionsQuery on inline-form select fields. The QN
|
|
479
|
+
// existence check lives in query-refs.ts with the other query refs.
|
|
480
|
+
function validateFormSelectOptions(featureName, screenId, context, fieldName, fdef) {
|
|
481
|
+
if (fdef.type === "select") {
|
|
482
|
+
const where = `[Feature ${featureName}] Screen "${screenId}" (${context}) select field "${fieldName}"`;
|
|
483
|
+
if (fdef.optionsQuery !== undefined) {
|
|
484
|
+
if (fdef.optionsQuery.length === 0) {
|
|
485
|
+
throw new Error(`${where} has an empty optionsQuery`);
|
|
486
|
+
}
|
|
487
|
+
if (fdef.options.length > 0) {
|
|
488
|
+
throw new Error(`${where} declares both options and optionsQuery — pick one`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else if (fdef.optionsQueryPayload !== undefined) {
|
|
492
|
+
throw new Error(`${where} has optionsQueryPayload without optionsQuery`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
477
496
|
// Fail-closed currency-source gate (fw#2839), the runtime half of the
|
|
478
497
|
// narrowed `FormFieldDefinition` — an untyped JS consumer has no compiler to
|
|
479
498
|
// stop it. An inline form screen has no entity, so a money field there can't
|
|
@@ -681,6 +700,98 @@ function validateScreenHasNavArea(feature, screenId, screen, featureMap) {
|
|
|
681
700
|
`a standalone r.nav() pointing at it, or set dormant: true if it's intentionally reachable ` +
|
|
682
701
|
`only via a direct link/redirect or a consuming app's own r.nav().`);
|
|
683
702
|
}
|
|
703
|
+
// Shared by projectionDetail relatedList sections and entityList.expandableRow:
|
|
704
|
+
// both render a relatedList whose rows come from an arbitrary query.
|
|
705
|
+
function validateRelatedListSection(args) {
|
|
706
|
+
const { featureName, featureMap, where, section } = args;
|
|
707
|
+
if (!section.query || typeof section.query !== "string") {
|
|
708
|
+
throw new Error(`[Feature ${featureName}] ${where} has empty or non-string query.`);
|
|
709
|
+
}
|
|
710
|
+
if (section.entity !== undefined) {
|
|
711
|
+
assertRefTargetRegistered(`[Feature ${featureName}] ${where}`, "entity", section.entity, featureName, featureMap);
|
|
712
|
+
}
|
|
713
|
+
for (const col of section.columns) {
|
|
714
|
+
const normalizedCol = normalizeListColumn(col);
|
|
715
|
+
if (normalizedCol.refEntity !== undefined) {
|
|
716
|
+
assertRefTargetRegistered(`[Feature ${featureName}] ${where}`, `column "${normalizedCol.field}" (refEntity)`, normalizedCol.refEntity, featureName, featureMap);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (section.rowClick !== undefined) {
|
|
720
|
+
const targetEntity = section.rowClick.entity;
|
|
721
|
+
const hasDetailScreen = [...featureMap.values()].some((f) => Object.values(f.screens).some((s) => s.detailFor === targetEntity));
|
|
722
|
+
if (!hasDetailScreen) {
|
|
723
|
+
throw new Error(`[Feature ${featureName}] ${where} ` +
|
|
724
|
+
`rowClick targets entity "${targetEntity}", but no screen declares ` +
|
|
725
|
+
`detailFor: "${targetEntity}". Add detailFor: "${targetEntity}" to the screen that shows it.`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (section.rowActions !== undefined)
|
|
729
|
+
validateRelatedListRowActions(args, section.rowActions);
|
|
730
|
+
if (section.defaultSort !== undefined)
|
|
731
|
+
validateRelatedListDefaultSort(args, section.defaultSort);
|
|
732
|
+
if (section.facets !== undefined) {
|
|
733
|
+
validateListFacets(`[Feature ${featureName}] ${where}`, section.facets, section.columns, featureName, featureMap);
|
|
734
|
+
}
|
|
735
|
+
if (section.toolbarActions !== undefined) {
|
|
736
|
+
validateRelatedListToolbarActions(args, section.toolbarActions);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function validateRelatedListRowActions(args, rowActions) {
|
|
740
|
+
const { featureName, screenId, screenType, where, section, screens, allWriteHandlerQns, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens, } = args;
|
|
741
|
+
for (const action of rowActions) {
|
|
742
|
+
if (action.kind === "navigate") {
|
|
743
|
+
const target = resolveRowActionNavigateTarget(featureName, screenId, screenType, "rowAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
|
|
744
|
+
validateRowActionNavigateParams(featureName, screenId, screenType, undefined, action, target);
|
|
745
|
+
}
|
|
746
|
+
else if (action.kind === "drawer") {
|
|
747
|
+
validateDrawerTargetAction(featureName, screenId, screenType, "rowAction", action, screens);
|
|
748
|
+
}
|
|
749
|
+
else if (!allWriteHandlerQns.has(action.handler)) {
|
|
750
|
+
throw new Error(`[Feature ${featureName}] ${where} ` +
|
|
751
|
+
`rowAction "${action.id}" handler "${action.handler}" ` +
|
|
752
|
+
`is not a registered write-handler. Check the QN spelling (expected ` +
|
|
753
|
+
`"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// section.rowClick (legacy, navigate-only) and a rowActions entry
|
|
757
|
+
// marked rowClick:true both claim the row-body click — same
|
|
758
|
+
// at-most-one constraint as entityList/projectionList's
|
|
759
|
+
// validateAtMostOneRowClick, just spanning two fields instead of one.
|
|
760
|
+
const rowClickActionCount = rowActions.filter((a) => a.kind === "navigate" && a.rowClick === true).length;
|
|
761
|
+
const legacyRowClickCount = section.rowClick !== undefined ? 1 : 0;
|
|
762
|
+
if (rowClickActionCount + legacyRowClickCount > 1) {
|
|
763
|
+
throw new Error(`[Feature ${featureName}] ${where} ` +
|
|
764
|
+
`has both a rowClick and ${rowClickActionCount} rowActions marked ` +
|
|
765
|
+
`rowClick:true — at most one may fire on a row-body click.`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function validateRelatedListDefaultSort(args, defaultSort) {
|
|
769
|
+
const { featureName, where, section } = args;
|
|
770
|
+
const sortField = defaultSort.field;
|
|
771
|
+
const col = section.columns.find((c) => normalizeListColumn(c).field === sortField);
|
|
772
|
+
if (col === undefined) {
|
|
773
|
+
throw new Error(`[Feature ${featureName}] ${where} ` +
|
|
774
|
+
`defaultSort.field "${sortField}" is not a listed column.`);
|
|
775
|
+
}
|
|
776
|
+
if (normalizeListColumn(col).sortable !== true) {
|
|
777
|
+
throw new Error(`[Feature ${featureName}] ${where} ` +
|
|
778
|
+
`defaultSort.field "${sortField}" is not sortable. Set sortable: true on ` +
|
|
779
|
+
`the column or pick another field.`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// Only drawer-kind and navigate actions that set tab are validated here;
|
|
783
|
+
// plain navigate (no tab) and writeHandler toolbarActions have no boot check yet.
|
|
784
|
+
function validateRelatedListToolbarActions(args, toolbarActions) {
|
|
785
|
+
const { featureName, screenId, screenType, screens, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens, } = args;
|
|
786
|
+
for (const action of toolbarActions) {
|
|
787
|
+
if (action.kind === "drawer") {
|
|
788
|
+
validateDrawerTargetAction(featureName, screenId, screenType, "toolbarAction", action, screens);
|
|
789
|
+
}
|
|
790
|
+
if (action.kind === "navigate" && action.tab !== undefined) {
|
|
791
|
+
resolveRowActionNavigateTarget(featureName, screenId, screenType, "toolbarAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
684
795
|
export function validateScreens(feature, featureMap, allWriteHandlerQns, allScreenQns, allConfigKeyQns, screensByShortId, detailForScreens) {
|
|
685
796
|
// navigate-Targets (rowAction/toolbarAction) dürfen cross-feature zeigen —
|
|
686
797
|
// der Runtime-Router (create-app) löst eine bare screenId app-weit über ALLE
|
|
@@ -876,87 +987,25 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
876
987
|
continue;
|
|
877
988
|
}
|
|
878
989
|
if (section.kind === "relatedList") {
|
|
879
|
-
if (!section.query || typeof section.query !== "string") {
|
|
880
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
881
|
-
`(relatedList) has empty or non-string query.`);
|
|
882
|
-
}
|
|
883
|
-
for (const col of section.columns) {
|
|
884
|
-
const normalizedCol = normalizeListColumn(col);
|
|
885
|
-
if (normalizedCol.refEntity !== undefined) {
|
|
886
|
-
assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" (relatedList)`, `column "${normalizedCol.field}" (refEntity)`, normalizedCol.refEntity, feature.name, featureMap);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
990
|
if (screen.layout.mode === "wizard") {
|
|
890
991
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
891
992
|
`is kind "relatedList" in a wizard layout — a read-only list is not supported in a ` +
|
|
892
993
|
`stepped form. Remove mode: "wizard" or drop the relatedList section.`);
|
|
893
994
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
else if (action.kind === "drawer") {
|
|
910
|
-
validateDrawerTargetAction(feature.name, screenId, "projectionDetail", "rowAction", action, feature.screens);
|
|
911
|
-
}
|
|
912
|
-
else if (!allWriteHandlerQns.has(action.handler)) {
|
|
913
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section ` +
|
|
914
|
-
`"${section.title}" (relatedList) rowAction "${action.id}" handler "${action.handler}" ` +
|
|
915
|
-
`is not a registered write-handler. Check the QN spelling (expected ` +
|
|
916
|
-
`"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
// section.rowClick (legacy, navigate-only) and a rowActions entry
|
|
920
|
-
// marked rowClick:true both claim the row-body click — same
|
|
921
|
-
// at-most-one constraint as entityList/projectionList's
|
|
922
|
-
// validateAtMostOneRowClick, just spanning two fields instead of one.
|
|
923
|
-
const rowClickActionCount = section.rowActions.filter((a) => a.kind === "navigate" && a.rowClick === true).length;
|
|
924
|
-
const legacyRowClickCount = section.rowClick !== undefined ? 1 : 0;
|
|
925
|
-
if (rowClickActionCount + legacyRowClickCount > 1) {
|
|
926
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
927
|
-
`(relatedList) has both a rowClick and ${rowClickActionCount} rowActions marked ` +
|
|
928
|
-
`rowClick:true — at most one may fire on a row-body click.`);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
if (section.defaultSort !== undefined) {
|
|
932
|
-
const sortField = section.defaultSort.field;
|
|
933
|
-
const col = section.columns.find((c) => normalizeListColumn(c).field === sortField);
|
|
934
|
-
if (col === undefined) {
|
|
935
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
936
|
-
`(relatedList) defaultSort.field "${sortField}" is not a listed column.`);
|
|
937
|
-
}
|
|
938
|
-
if (normalizeListColumn(col).sortable !== true) {
|
|
939
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
940
|
-
`(relatedList) defaultSort.field "${sortField}" is not sortable. Set sortable: true on ` +
|
|
941
|
-
`the column or pick another field.`);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
if (section.facets !== undefined) {
|
|
945
|
-
validateListFacets(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) relatedList section "${section.title}"`, section.facets, section.columns, feature.name, featureMap);
|
|
946
|
-
}
|
|
947
|
-
// Only drawer-kind and navigate actions that set tab are validated
|
|
948
|
-
// here — plain navigate (no tab) and writeHandler toolbarActions
|
|
949
|
-
// have no boot check yet, same gap as above.
|
|
950
|
-
if (section.toolbarActions !== undefined) {
|
|
951
|
-
for (const action of section.toolbarActions) {
|
|
952
|
-
if (action.kind === "drawer") {
|
|
953
|
-
validateDrawerTargetAction(feature.name, screenId, "projectionDetail", "toolbarAction", action, feature.screens);
|
|
954
|
-
}
|
|
955
|
-
if (action.kind === "navigate" && action.tab !== undefined) {
|
|
956
|
-
resolveRowActionNavigateTarget(feature.name, screenId, "projectionDetail", "toolbarAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
995
|
+
validateRelatedListSection({
|
|
996
|
+
featureName: feature.name,
|
|
997
|
+
featureMap,
|
|
998
|
+
screenId,
|
|
999
|
+
screenType: "projectionDetail",
|
|
1000
|
+
where: `Screen "${screenId}" (projectionDetail) section "${section.title}" (relatedList)`,
|
|
1001
|
+
section,
|
|
1002
|
+
screens: feature.screens,
|
|
1003
|
+
allWriteHandlerQns,
|
|
1004
|
+
allScreenQns,
|
|
1005
|
+
navTargetShortIds,
|
|
1006
|
+
screensByShortId,
|
|
1007
|
+
detailForScreens,
|
|
1008
|
+
});
|
|
960
1009
|
continue;
|
|
961
1010
|
}
|
|
962
1011
|
if (isWriteFormEditSection(section)) {
|
|
@@ -1048,6 +1097,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1048
1097
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" has no ` +
|
|
1049
1098
|
`\`type\` set. Each field must declare a type (e.g. "text", "number", "select").`);
|
|
1050
1099
|
}
|
|
1100
|
+
validateFormSelectOptions(feature.name, screenId, "configEdit", fname, fdef);
|
|
1051
1101
|
}
|
|
1052
1102
|
if (screen.layout.sections.length === 0) {
|
|
1053
1103
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) has an empty sections list — ` +
|
|
@@ -1340,6 +1390,27 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1340
1390
|
validateActionFieldRefs(feature.name, screenId, "toolbarAction", action.id, action, fieldNames, rowMeta);
|
|
1341
1391
|
}
|
|
1342
1392
|
}
|
|
1393
|
+
if (screen.expandableRow !== undefined) {
|
|
1394
|
+
const expandableRow = screen.expandableRow;
|
|
1395
|
+
validateRelatedListSection({
|
|
1396
|
+
featureName: feature.name,
|
|
1397
|
+
featureMap,
|
|
1398
|
+
screenId,
|
|
1399
|
+
screenType: "entityList expandableRow",
|
|
1400
|
+
where: `Screen "${screenId}" (entityList) expandableRow "${expandableRow.title}"`,
|
|
1401
|
+
section: expandableRow,
|
|
1402
|
+
screens: feature.screens,
|
|
1403
|
+
allWriteHandlerQns,
|
|
1404
|
+
allScreenQns,
|
|
1405
|
+
navTargetShortIds,
|
|
1406
|
+
screensByShortId,
|
|
1407
|
+
detailForScreens,
|
|
1408
|
+
});
|
|
1409
|
+
// Toolbar actions of the expansion evaluate against the host row.
|
|
1410
|
+
for (const action of expandableRow.toolbarActions ?? []) {
|
|
1411
|
+
validateActionFieldRefs(feature.name, screenId, "toolbarAction", action.id, action, fieldNames, rowMeta);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1343
1414
|
}
|
|
1344
1415
|
else {
|
|
1345
1416
|
// Same rationale as the columns check: an entityEdit layout with zero
|
|
@@ -28,7 +28,17 @@ function projectionDetailNavigateActions(screen) {
|
|
|
28
28
|
function navigateActionsOf(screen) {
|
|
29
29
|
switch (screen.type) {
|
|
30
30
|
case "entityList":
|
|
31
|
-
return
|
|
31
|
+
return [
|
|
32
|
+
...scopeActions(screen.rowActions, screen.entity),
|
|
33
|
+
...(screen.expandableRow === undefined
|
|
34
|
+
? []
|
|
35
|
+
: [
|
|
36
|
+
...scopeActions(screen.expandableRow.rowActions, undefined),
|
|
37
|
+
...scopeActions(screen.expandableRow.toolbarActions, undefined, {
|
|
38
|
+
pick: [relatedListParentParamField(screen.expandableRow)],
|
|
39
|
+
}),
|
|
40
|
+
]),
|
|
41
|
+
];
|
|
32
42
|
case "projectionList":
|
|
33
43
|
return scopeActions(screen.rowActions, undefined);
|
|
34
44
|
case "entityEdit":
|
|
@@ -394,6 +394,9 @@ function projectField(fieldDef) {
|
|
|
394
394
|
out["entity"] = def["entity"];
|
|
395
395
|
if (typeof def["labelField"] === "string")
|
|
396
396
|
out["labelField"] = def["labelField"];
|
|
397
|
+
// fw#2780: author-supplied picker source — without it the client falls back to the list handler.
|
|
398
|
+
if (typeof def["optionsQuery"] === "string")
|
|
399
|
+
out["optionsQuery"] = def["optionsQuery"];
|
|
397
400
|
if (typeof def["multiple"] === "boolean")
|
|
398
401
|
out["multiple"] = def["multiple"];
|
|
399
402
|
// MultiSelect: display picks checkboxes vs. combobox in the renderer,
|
|
@@ -504,6 +504,15 @@ function deriveField(def, selectorPluginIds) {
|
|
|
504
504
|
case "boolean":
|
|
505
505
|
return createBooleanField();
|
|
506
506
|
case "select":
|
|
507
|
+
if (def.optionsQuery !== undefined) {
|
|
508
|
+
return createSelectField({
|
|
509
|
+
options: [],
|
|
510
|
+
optionsQuery: def.optionsQuery,
|
|
511
|
+
...(def.optionsQueryPayload !== undefined && {
|
|
512
|
+
optionsQueryPayload: def.optionsQueryPayload,
|
|
513
|
+
}),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
507
516
|
return def.options !== undefined && def.options.length > 0
|
|
508
517
|
? createSelectField({ options: def.options })
|
|
509
518
|
: createTextField({ personal: false, reason: "system_metadata" });
|
|
@@ -18,6 +18,8 @@ type ConfigKeyOptions<T extends ConfigKeyType> = {
|
|
|
18
18
|
encrypted?: boolean;
|
|
19
19
|
piiEncrypted?: T extends "text" ? boolean : never;
|
|
20
20
|
options?: readonly string[];
|
|
21
|
+
optionsQuery?: T extends "select" ? string : never;
|
|
22
|
+
optionsQueryPayload?: T extends "select" ? Readonly<Record<string, string | number | boolean>> : never;
|
|
21
23
|
bounds?: T extends "number" ? ConfigBounds : never;
|
|
22
24
|
pattern?: T extends "text" ? {
|
|
23
25
|
regex: string;
|
|
@@ -35,6 +35,14 @@ const SCOPE_DEFAULTS = {
|
|
|
35
35
|
user: { write: access.all, read: access.all },
|
|
36
36
|
};
|
|
37
37
|
// --- Factory ---
|
|
38
|
+
function optionsQueryProps(opts) {
|
|
39
|
+
return {
|
|
40
|
+
...(opts.optionsQuery !== undefined && { optionsQuery: opts.optionsQuery }),
|
|
41
|
+
...(opts.optionsQueryPayload !== undefined && {
|
|
42
|
+
optionsQueryPayload: opts.optionsQueryPayload,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
38
46
|
function createConfigKey(scope, type, opts = {}) {
|
|
39
47
|
const defaults = SCOPE_DEFAULTS[scope];
|
|
40
48
|
return {
|
|
@@ -48,6 +56,7 @@ function createConfigKey(scope, type, opts = {}) {
|
|
|
48
56
|
...(opts.encrypted ? { encrypted: true } : {}),
|
|
49
57
|
...(opts.piiEncrypted ? { piiEncrypted: true } : {}),
|
|
50
58
|
...(opts.options ? { options: opts.options } : {}),
|
|
59
|
+
...optionsQueryProps(opts),
|
|
51
60
|
bounds: opts.bounds, // @cast-boundary schema-walk
|
|
52
61
|
...(opts.pattern ? { pattern: opts.pattern } : {}),
|
|
53
62
|
computed: opts.computed,
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.
|
|
|
71
71
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
72
72
|
export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
|
|
73
73
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
74
|
-
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyFactory, NotifyFn, NotifyOptions, NotifyPriority, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RecordHeaderSpec, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
|
|
74
|
+
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyFactory, NotifyFn, NotifyOptions, NotifyPriority, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RecordHeaderSpec, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
|
|
75
75
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, HookPhases, isAgentVisibleScreen, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
76
76
|
export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
|
|
77
77
|
export { runValidation } from "./validation.js";
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
// - WorkspaceSchema.definition.nav — the raw explicit nav-QN list,
|
|
56
56
|
// verbatim in JSON, distinct from the computed/filtered navMembers
|
|
57
57
|
// - entityList.createScreen (same-feature target, dropped when not kept)
|
|
58
|
+
// - entityList.expandableRow (a relatedList: same projection as a relatedList section)
|
|
58
59
|
// - listScreenId (projectionDetail/entityEdit/actionForm/secretMint/custom —
|
|
59
60
|
// the breadcrumb "back to list" target). It's serialized, so a visible
|
|
60
61
|
// screen would otherwise disclose a denied screen's short id. Resolved
|
|
@@ -433,12 +434,20 @@ function projectListScreen(original, feature, indices, keptScreenQns) {
|
|
|
433
434
|
const { rowActions, toolbarActions, ...rest } = screen;
|
|
434
435
|
const nextRowActions = projectRowActionArray(rowActions, feature, indices, keptScreenQns);
|
|
435
436
|
const nextToolbarActions = projectToolbarActionArray(toolbarActions, feature, indices, keptScreenQns);
|
|
436
|
-
|
|
437
|
+
const expandableRow = screen.type === "entityList" ? screen.expandableRow : undefined;
|
|
438
|
+
const nextExpandableRow = expandableRow === undefined
|
|
439
|
+
? undefined
|
|
440
|
+
: projectRelatedListSection(expandableRow, feature, indices, keptScreenQns);
|
|
441
|
+
if (nextRowActions === rowActions &&
|
|
442
|
+
nextToolbarActions === toolbarActions &&
|
|
443
|
+
nextExpandableRow === expandableRow) {
|
|
437
444
|
return screen;
|
|
445
|
+
}
|
|
438
446
|
return {
|
|
439
447
|
...rest,
|
|
440
448
|
...(nextRowActions !== undefined && { rowActions: nextRowActions }),
|
|
441
449
|
...(nextToolbarActions !== undefined && { toolbarActions: nextToolbarActions }),
|
|
450
|
+
...(nextExpandableRow !== undefined && { expandableRow: nextExpandableRow }),
|
|
442
451
|
};
|
|
443
452
|
}
|
|
444
453
|
function projectProjectionDetailScreen(screen, feature, indices, keptScreenQns) {
|
|
@@ -17,7 +17,7 @@ export type { ContentCollectionDefinition, NavDefinition, NavIconKey, } from "@c
|
|
|
17
17
|
export type { FromRule, FromRuleKind, OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule, SqlFragment, WhereRule, WhereRuleContext, } from "@cosmicdrift/kumiko-types/ownership";
|
|
18
18
|
export type { EntityProjectionExtension, MspErrorMode, MspErrorPolicy, MultiStreamApplyFn, MultiStreamProjectionDefinition, ProjectionDefinition, ProjectionTable, SingleStreamApplyFn, } from "@cosmicdrift/kumiko-types/projection";
|
|
19
19
|
export type { BelongsToRelation, EntityRelations, HasManyRelation, ManyToManyRelation, RelationDefinition, } from "@cosmicdrift/kumiko-types/relations";
|
|
20
|
-
export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
20
|
+
export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
21
21
|
export { metricField } from "@cosmicdrift/kumiko-types/screen";
|
|
22
22
|
export type { TargetRef } from "@cosmicdrift/kumiko-types/target-ref";
|
|
23
23
|
export type { Subscribe, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, } from "@cosmicdrift/kumiko-types/tree-node";
|
|
@@ -107,6 +107,30 @@ function pushToolbarActionKeys(out, action) {
|
|
|
107
107
|
// toolbar button AND the Drawer title, see ToolbarDrawerHost) is
|
|
108
108
|
// already pushed above.
|
|
109
109
|
}
|
|
110
|
+
function pushRelatedListSectionKeys(out, section) {
|
|
111
|
+
pushKey(out, section.title);
|
|
112
|
+
pushKey(out, section.description);
|
|
113
|
+
pushKey(out, section.itemNoun);
|
|
114
|
+
for (const col of section.columns) {
|
|
115
|
+
const normalized = normalizeListColumn(col);
|
|
116
|
+
if (normalized.label !== undefined)
|
|
117
|
+
pushKey(out, normalized.label);
|
|
118
|
+
}
|
|
119
|
+
for (const action of section.rowActions ?? [])
|
|
120
|
+
pushRowActionKeys(out, action);
|
|
121
|
+
}
|
|
122
|
+
function pushExpandableRowKeys(out, section) {
|
|
123
|
+
pushRelatedListSectionKeys(out, section);
|
|
124
|
+
for (const action of section.actions ?? [])
|
|
125
|
+
pushRowActionKeys(out, action);
|
|
126
|
+
for (const action of section.toolbarActions ?? [])
|
|
127
|
+
pushToolbarActionKeys(out, action);
|
|
128
|
+
pushKey(out, section.emptyState?.title);
|
|
129
|
+
pushKey(out, section.emptyState?.description);
|
|
130
|
+
if (section.emptyState?.action !== undefined) {
|
|
131
|
+
pushRowActionKeys(out, section.emptyState.action);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
110
134
|
export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
111
135
|
const { treatDotFormAsKey = false } = options;
|
|
112
136
|
const out = new Set();
|
|
@@ -129,6 +153,8 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
129
153
|
pushRowActionKeys(out, action);
|
|
130
154
|
for (const action of list.toolbarActions ?? [])
|
|
131
155
|
pushToolbarActionKeys(out, action);
|
|
156
|
+
if (list.expandableRow !== undefined)
|
|
157
|
+
pushExpandableRowKeys(out, list.expandableRow);
|
|
132
158
|
break;
|
|
133
159
|
}
|
|
134
160
|
case "projectionList": {
|
|
@@ -301,16 +327,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
301
327
|
continue;
|
|
302
328
|
}
|
|
303
329
|
if (section.kind === "relatedList") {
|
|
304
|
-
|
|
305
|
-
pushKey(out, section.description);
|
|
306
|
-
pushKey(out, section.itemNoun);
|
|
307
|
-
for (const col of section.columns) {
|
|
308
|
-
const normalized = normalizeListColumn(col);
|
|
309
|
-
if (normalized.label !== undefined)
|
|
310
|
-
pushKey(out, normalized.label);
|
|
311
|
-
}
|
|
312
|
-
for (const action of section.rowActions ?? [])
|
|
313
|
-
pushRowActionKeys(out, action);
|
|
330
|
+
pushRelatedListSectionKeys(out, section);
|
|
314
331
|
continue;
|
|
315
332
|
}
|
|
316
333
|
if (isWriteFormEditSection(section)) {
|
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefin
|
|
|
9
9
|
export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.js";
|
|
10
10
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
11
11
|
export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.js";
|
|
12
|
-
export type { ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartPanel, DashboardChartTone, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardTimeRangeDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
|
|
12
|
+
export type { ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartPanel, DashboardChartTone, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardTimeRangeDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
|
|
13
13
|
export { metricField } from "../engine/types/screen.js";
|
|
14
14
|
export type { TargetRef } from "../engine/types/target-ref.js";
|
|
15
15
|
export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.331.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -206,8 +206,8 @@
|
|
|
206
206
|
"./package.json": "./package.json"
|
|
207
207
|
},
|
|
208
208
|
"dependencies": {
|
|
209
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
210
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
209
|
+
"@cosmicdrift/kumiko-http": "0.331.0",
|
|
210
|
+
"@cosmicdrift/kumiko-types": "0.331.0",
|
|
211
211
|
"bullmq": "^5.76.7",
|
|
212
212
|
"bun-types": "^1.3.13",
|
|
213
213
|
"hono": "^4.13.1",
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
"zod": "^4.4.3"
|
|
224
224
|
},
|
|
225
225
|
"devDependencies": {
|
|
226
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
226
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.331.0",
|
|
227
227
|
"bun-types": "^1.3.13",
|
|
228
228
|
"pino-pretty": "^13.1.3"
|
|
229
229
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.331.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "A reference field's optionsQuery now reaches the client schema, so the picker uses the declared handler",
|
|
6
|
+
"detail": "`ReferenceFieldDef.optionsQuery` was dropped by the client-schema projection, so the picker ignored the declared handler and fell back to the entity's list handler. The projection now keeps it.",
|
|
7
|
+
"migration": "No code change needed."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.331.0",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "Select fields in configEdit and actionForm screens and select config keys can load their options from a query (optionsQuery)",
|
|
13
|
+
"detail": "A `select` config key and a `select` field on configEdit and actionForm screens accept `optionsQuery` (a query QN returning `{ rows: { value, label }[] }`) plus an optional static `optionsQueryPayload`. The Settings-Hub derives the field from the config key, the renderer mounts the query and shows the returned labels as they are. The boot validator rejects dead QNs, static `options` together with `optionsQuery`, `allowPerRequest` keys, and `optionsQuery` on entity fields. The write side does not check the value against the query result.",
|
|
14
|
+
"migration": "No code change needed. Existing select fields and config keys keep their static options."
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "0.330.2",
|
|
4
18
|
"type": "fix",
|