@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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.md +338 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +446 -0
- package/agents/examples/anonymous-access.md +288 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +319 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +315 -0
- package/agents/examples/custom-handlers.md +444 -0
- package/agents/examples/dashboard-stats.md +229 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +414 -0
- package/agents/examples/error-contract.md +535 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +274 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +215 -0
- package/agents/examples/ledger-invoicing.md +236 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +346 -0
- package/agents/examples/ownership.md +404 -0
- package/agents/examples/pdf-audit-export.md +306 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +250 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +418 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +75 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +7 -0
- package/dist/api/auth-routes.d.ts +0 -1
- package/dist/api/auth-routes.js +49 -46
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/routes.js +46 -17
- package/dist/api/server.d.ts +4 -3
- package/dist/api/server.js +39 -10
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/connection.js +3 -16
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +15 -3
- package/dist/bun-db/query.js +76 -24
- package/dist/crypto/owner-transition-rekey.d.ts +1 -0
- package/dist/crypto/owner-transition-rekey.js +7 -3
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +9 -2
- package/dist/crypto/subject-resolver.d.ts +1 -0
- package/dist/crypto/subject-resolver.js +7 -0
- package/dist/db/api.d.ts +4 -3
- package/dist/db/apply-entity-event.js +26 -2
- package/dist/db/blind-index-cleanup.js +2 -1
- package/dist/db/bun-provider.js +9 -18
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +2 -0
- package/dist/db/eagerload.js +4 -3
- package/dist/db/event-store-executor-context.d.ts +6 -6
- package/dist/db/event-store-executor-context.js +25 -22
- package/dist/db/event-store-executor-read.js +31 -16
- package/dist/db/event-store-executor-write.d.ts +1 -1
- package/dist/db/event-store-executor-write.js +84 -63
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +2 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/migrate-runner.d.ts +1 -0
- package/dist/db/migrate-runner.js +64 -38
- package/dist/db/queries/backfill-pii.js +63 -25
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +2 -1
- package/dist/db/queries/event-store-transfer.js +15 -4
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +11 -10
- package/dist/db/queries/pii-backfill-state.js +3 -2
- package/dist/db/queries/postgres-array-literal.d.ts +2 -0
- package/dist/db/queries/postgres-array-literal.js +6 -0
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.js +3 -2
- package/dist/db/queries/shadow-swap.d.ts +1 -1
- package/dist/db/queries/shadow-swap.js +12 -8
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/retired-framework-tables.js +22 -4
- package/dist/db/table-builder.js +6 -2
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +4 -0
- package/dist/db/tenant-db.js +50 -5
- package/dist/derivatives/delete-stored-file.d.ts +4 -0
- package/dist/derivatives/delete-stored-file.js +9 -0
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/index.d.ts +1 -1
- package/dist/derivatives/index.js +1 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +87 -35
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -8
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +14 -2
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +153 -95
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/dedupe-features.js +31 -4
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +8 -1
- package/dist/engine/entity-handlers.js +14 -7
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/render.js +10 -2
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +1 -1
- package/dist/engine/schema-builder.js +2 -1
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +2 -2
- package/dist/engine/soft-delete-cleanup.js +10 -3
- package/dist/engine/steps/_read-source.js +5 -2
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/engine/tier-resolver-extension.d.ts +1 -0
- package/dist/engine/tier-resolver-extension.js +6 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +2 -2
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/docs-url.js +6 -6
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/subject-erased.d.ts +1 -0
- package/dist/errors/subject-erased.js +6 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/admin-api.js +3 -2
- package/dist/event-store/errors.d.ts +9 -0
- package/dist/event-store/errors.js +20 -0
- package/dist/event-store/event-store.d.ts +6 -0
- package/dist/event-store/event-store.js +28 -6
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +24 -1
- package/dist/files/create-file-ref.js +67 -25
- package/dist/files/dispose-removed-file-refs.js +21 -6
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +12 -0
- package/dist/files/file-routes.js +10 -6
- package/dist/files/index.d.ts +3 -3
- package/dist/files/index.js +2 -2
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +46 -21
- package/dist/files/resolve-content-type.js +40 -4
- package/dist/files/storage-tracking.js +5 -4
- package/dist/files/types.js +5 -8
- package/dist/files/zip-entry-names.d.ts +1 -0
- package/dist/files/zip-entry-names.js +46 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +5 -1
- package/dist/jobs/job-runner.js +124 -37
- package/dist/observability/escape-hatch-report.d.ts +2 -1
- package/dist/observability/escape-hatch-report.js +48 -7
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -0
- package/dist/observability/metrics-handle.js +7 -0
- package/dist/observability/standard-metrics.js +4 -4
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.d.ts +2 -1
- package/dist/pipeline/dispatch-query.js +6 -6
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +136 -48
- package/dist/pipeline/dispatch-stream.js +10 -4
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/dispatcher.js +3 -1
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +11 -9
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +7 -1
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/notify-scope.js +3 -1
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +29 -9
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/schema-cli.js +12 -1
- package/dist/search/meilisearch-adapter.js +36 -9
- package/dist/stack/db.d.ts +2 -2
- package/dist/stack/test-stack.js +2 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/multipart-helper.d.ts +6 -0
- package/dist/testing/multipart-helper.js +1 -1
- package/dist/testing/zip-fixture.d.ts +1 -0
- package/dist/testing/zip-fixture.js +33 -0
- package/dist/ui-types/card-column-roles.js +4 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +46 -13
- package/package.json +7 -5
- package/src/changes.json +512 -8
|
@@ -24,7 +24,7 @@ function validateAtMostOneRowClick(featureName, screenId, screenType, rowActions
|
|
|
24
24
|
const rowClickActions = rowActions.filter((a) => a.kind === "navigate" && a.rowClick === true);
|
|
25
25
|
if (rowClickActions.length > 1) {
|
|
26
26
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) has ${rowClickActions.length} ` +
|
|
27
|
-
"rowActions marked rowClick:true — at most one may fire on a row-body click.");
|
|
27
|
+
"rowActions marked rowClick:true — at most one may fire on a row-body click. Mark at most one rowAction with rowClick: true.");
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
// A field type in NO_WIDGET_FIELD_TYPES renders read-only on the auto-wired
|
|
@@ -45,7 +45,7 @@ function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
|
|
|
45
45
|
if (listSpec !== undefined) {
|
|
46
46
|
if (fieldDef.type !== "embedded" || fieldDef.multiple !== true) {
|
|
47
47
|
throw new Error(`${where} sets embeddedList, which only applies to an embedded list ` +
|
|
48
|
-
`(createEmbeddedListField); the field is type "${fieldDef.type}".`);
|
|
48
|
+
`(createEmbeddedListField); the field is type "${fieldDef.type}". Remove embeddedList or point it at an embedded list field.`);
|
|
49
49
|
}
|
|
50
50
|
for (const cell of listSpec.readOnlyCells ?? []) {
|
|
51
51
|
if (fieldDef.schema[cell] === undefined) {
|
|
@@ -60,7 +60,7 @@ function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
|
|
|
60
60
|
return;
|
|
61
61
|
if (prefill.kind === "today") {
|
|
62
62
|
if (fieldDef.type !== "date") {
|
|
63
|
-
throw new Error(`${where} prefill "today" needs a date field; the field is type "${fieldDef.type}".`);
|
|
63
|
+
throw new Error(`${where} prefill "today" needs a date field; the field is type "${fieldDef.type}". Use a date field or a different prefill.`);
|
|
64
64
|
}
|
|
65
65
|
// skip: today carries no property, nothing further to check
|
|
66
66
|
return;
|
|
@@ -68,7 +68,7 @@ function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
|
|
|
68
68
|
const allowsUserId = fieldDef.type === "text" || fieldDef.type === "reference";
|
|
69
69
|
if (prefill.property === "id" ? !allowsUserId : fieldDef.type !== "text") {
|
|
70
70
|
throw new Error(`${where} prefill currentUser.${prefill.property} does not fit a field of type ` +
|
|
71
|
-
`"${fieldDef.type}" (displayName and email need text; id needs text or a reference).`);
|
|
71
|
+
`"${fieldDef.type}" (displayName and email need text; id needs text or a reference). Use a fitting field type or a different prefill property.`);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
function validateNoWidgetRequiredField(featureName, screenId, entityDef, fieldSpec) {
|
|
@@ -121,7 +121,7 @@ export function readsNavigateParamsAsFormPrefill(target, explicitEntityId, sourc
|
|
|
121
121
|
// for the value to land, and a params extractor whose keys don't include
|
|
122
122
|
// the filter's id would silently miss it — both are boot errors instead of
|
|
123
123
|
// a silently-empty dashboard.
|
|
124
|
-
function validateRowActionNavigateParams(featureName, screenId, screenType, screenEntity, action, target) {
|
|
124
|
+
function validateRowActionNavigateParams(featureName, screenId, screenType, screenEntity, action, target, actionLabel = "rowAction") {
|
|
125
125
|
// skip: not a navigate-with-params action — nothing to validate here.
|
|
126
126
|
if (action.kind !== "navigate" || action.params === undefined)
|
|
127
127
|
return;
|
|
@@ -137,14 +137,14 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
|
|
|
137
137
|
const targetDescriptor = action.screen !== undefined ? `"${action.screen}"` : `entity "${action.entity}"`;
|
|
138
138
|
const filter = target.screen.filter;
|
|
139
139
|
if (filter === undefined) {
|
|
140
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType})
|
|
140
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" sets ` +
|
|
141
141
|
`params on navigate-target ${targetDescriptor} (dashboard) — target dashboard declares no ` +
|
|
142
142
|
`filter — params would be a no-op. Remove the params extractor, or add a \`filter\` to the ` +
|
|
143
143
|
`target dashboard so it has somewhere to read the value from.`);
|
|
144
144
|
}
|
|
145
145
|
const extractedKeys = rowFieldExtractorKeys(action.params);
|
|
146
146
|
if (!extractedKeys.includes(filter.id)) {
|
|
147
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType})
|
|
147
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" sets ` +
|
|
148
148
|
`params [${extractedKeys.join(", ")}] on navigate-target ${targetDescriptor} (dashboard) whose ` +
|
|
149
149
|
`filter id is "${filter.id}" — none of the extracted keys match, so the filter would stay ` +
|
|
150
150
|
`unset. Fix the params extractor to produce a "${filter.id}" key, or remove it.`);
|
|
@@ -160,7 +160,7 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
|
|
|
160
160
|
: `same entity "${screenEntity}" auto-fills row["id"]`})`
|
|
161
161
|
: `screen type "${target.screen.type}"`;
|
|
162
162
|
const targetDescriptor = action.screen !== undefined ? `"${action.screen}"` : `entity "${action.entity}"`;
|
|
163
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType})
|
|
163
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
164
164
|
`sets params on navigate-target ${targetDescriptor} which ${reason} — only actionForm, ` +
|
|
165
165
|
`secretMint and entityEdit-create targets read URL search params as initial values. Remove ` +
|
|
166
166
|
`the params extractor or retarget to an actionForm / secretMint / cross-entity ` +
|
|
@@ -169,7 +169,7 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
|
|
|
169
169
|
const formTarget = target.screen;
|
|
170
170
|
if ((formTarget.type === "actionForm" || formTarget.type === "secretMint") &&
|
|
171
171
|
formTarget.submitPrefilled === true) {
|
|
172
|
-
validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenType})
|
|
172
|
+
validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}"`, "navigate-target", action.screen ?? `entity "${action.entity}"`, formTarget, action.params);
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
// Shared by projectionDetail and entityEdit: a tab strip needs something to
|
|
@@ -197,7 +197,7 @@ function validateTabSections(featureName, screenId, screenType, sections) {
|
|
|
197
197
|
}
|
|
198
198
|
if (tabIds.has(section.id)) {
|
|
199
199
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) has duplicate tab id ` +
|
|
200
|
-
`"${section.id}" (sections[${index}]).`);
|
|
200
|
+
`"${section.id}" (sections[${index}]). Give each tab section a unique id.`);
|
|
201
201
|
}
|
|
202
202
|
tabIds.add(section.id);
|
|
203
203
|
});
|
|
@@ -399,7 +399,7 @@ function validateDrawerTargetAction(featureName, screenId, screenKind, actionLab
|
|
|
399
399
|
return;
|
|
400
400
|
validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenKind}) ${actionLabel} "${action.id}"`, "drawer-target", action.screen, target, action.params);
|
|
401
401
|
}
|
|
402
|
-
function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, params) {
|
|
402
|
+
export function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, params) {
|
|
403
403
|
const renderedFieldNames = new Set();
|
|
404
404
|
for (const section of target.layout.sections) {
|
|
405
405
|
if (!isFieldsEditSection(section))
|
|
@@ -419,7 +419,7 @@ function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, p
|
|
|
419
419
|
("format" in targetField && targetField.format === "password"));
|
|
420
420
|
if (isSensitiveOrPassword) {
|
|
421
421
|
throw new Error(`${where} params prefills "${fieldName}", which ${targetLabel} "${targetScreenId}" marks as ` +
|
|
422
|
-
`sensitive or password — the renderer never prefills those fields and would leave the form empty.`);
|
|
422
|
+
`sensitive or password — the renderer never prefills those fields and would leave the form empty. Remove that param or prefill a non-sensitive field.`);
|
|
423
423
|
}
|
|
424
424
|
if (!renderedFieldNames.has(fieldName)) {
|
|
425
425
|
throw new Error(`${where} params prefills "${fieldName}", which ${targetLabel} "${targetScreenId}" does not render in ` +
|
|
@@ -435,12 +435,12 @@ function validateRowActionNavigateTab(featureName, screenId, screenType, actionL
|
|
|
435
435
|
return;
|
|
436
436
|
if (target === undefined) {
|
|
437
437
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
438
|
-
`sets tab "${action.tab}", but its navigate-target could not be resolved to a screen.`);
|
|
438
|
+
`sets tab "${action.tab}", but its navigate-target could not be resolved to a screen. Point the navigate-target at a registered screen.`);
|
|
439
439
|
}
|
|
440
440
|
if (target.screen.type !== "projectionDetail" || target.screen.layout.mode !== "tabs") {
|
|
441
441
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
442
442
|
`sets tab "${action.tab}", but navigate-target screen "${target.screen.id}" is not a ` +
|
|
443
|
-
`projectionDetail with layout.mode "tabs" — only those read the tab param.`);
|
|
443
|
+
`projectionDetail with layout.mode "tabs" — only those read the tab param. Remove the tab or navigate to a projectionDetail screen with layout.mode "tabs".`);
|
|
444
444
|
}
|
|
445
445
|
const sectionIds = target.screen.layout.sections
|
|
446
446
|
.map((section) => section.id)
|
|
@@ -471,7 +471,7 @@ function resolveRowActionNavigateTarget(featureName, screenId, screenType, actio
|
|
|
471
471
|
if (detail === undefined) {
|
|
472
472
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
473
473
|
`navigate-target entity "${action.entity}" has no screen declaring ` +
|
|
474
|
-
`detailFor: "${action.entity}".`);
|
|
474
|
+
`detailFor: "${action.entity}". Add detailFor: "${action.entity}" to a screen or navigate to a screen directly.`);
|
|
475
475
|
}
|
|
476
476
|
validateRowActionNavigateTab(featureName, screenId, screenType, actionLabel, action, detail);
|
|
477
477
|
return detail;
|
|
@@ -483,7 +483,7 @@ function resolveRowActionNavigateTarget(featureName, screenId, screenType, actio
|
|
|
483
483
|
const candidateQn = qualifyEntityName(featureName, "screen", action.screen);
|
|
484
484
|
if (!allScreenQns.has(candidateQn) && !navTargetShortIds.has(action.screen)) {
|
|
485
485
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
486
|
-
`navigate-target "${action.screen}" does not resolve to a registered screen in any feature.`);
|
|
486
|
+
`navigate-target "${action.screen}" does not resolve to a registered screen in any feature. Use "<feature>:screen:<id>" of an existing screen.`);
|
|
487
487
|
}
|
|
488
488
|
const target = screensByShortId.get(action.screen)?.[0];
|
|
489
489
|
validateRowActionNavigateTab(featureName, screenId, screenType, actionLabel, action, target);
|
|
@@ -494,7 +494,7 @@ function resolveRowActionNavigateTarget(featureName, screenId, screenType, actio
|
|
|
494
494
|
// same registration requirement.
|
|
495
495
|
function validateWriteHandlerRegistered(featureName, screenId, context, handler, allWriteHandlerQns) {
|
|
496
496
|
if (!handler || typeof handler !== "string") {
|
|
497
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) has empty or non-string handler.`);
|
|
497
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) has empty or non-string handler. Set handler to a non-empty handler name.`);
|
|
498
498
|
}
|
|
499
499
|
if (!allWriteHandlerQns.has(handler)) {
|
|
500
500
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) handler "${handler}" ` +
|
|
@@ -540,7 +540,7 @@ function validateFormFieldsMap(featureName, screenId, context, fields, allowEmpt
|
|
|
540
540
|
function validateOptionsQueryFieldRefs(where, fieldName, payload, siblingFieldNames) {
|
|
541
541
|
for (const ref of optionsQueryFieldRefs(payload)) {
|
|
542
542
|
if (ref === fieldName) {
|
|
543
|
-
throw new Error(`${where} optionsQueryPayload references itself ({ field: "${ref}" })
|
|
543
|
+
throw new Error(`${where} optionsQueryPayload references itself ({ field: "${ref}" }). Reference a different field of the form.`);
|
|
544
544
|
}
|
|
545
545
|
if (!siblingFieldNames.has(ref)) {
|
|
546
546
|
throw new Error(`${where} optionsQueryPayload references unknown field "${ref}" — it must be another field of the same form`);
|
|
@@ -568,30 +568,33 @@ function validateFormSelectOptions(featureName, screenId, context, fieldName, fd
|
|
|
568
568
|
if (fdef.type === "select") {
|
|
569
569
|
const where = `[Feature ${featureName}] Screen "${screenId}" (${context}) select field "${fieldName}"`;
|
|
570
570
|
if (fdef.conditionalOptions !== undefined) {
|
|
571
|
-
throw new Error(`${where} declares conditionalOptions — ${CONDITIONAL_OPTIONS_ENTITY_ONLY}
|
|
571
|
+
throw new Error(`${where} declares conditionalOptions — ${CONDITIONAL_OPTIONS_ENTITY_ONLY}. Remove conditionalOptions from this field.`);
|
|
572
572
|
}
|
|
573
573
|
if (fdef.optionsQuery !== undefined) {
|
|
574
574
|
if (fdef.optionsQuery.length === 0) {
|
|
575
|
-
throw new Error(`${where} has an empty optionsQuery
|
|
575
|
+
throw new Error(`${where} has an empty optionsQuery. Set it to a query name or remove it.`);
|
|
576
576
|
}
|
|
577
577
|
if (fdef.options.length > 0) {
|
|
578
578
|
throw new Error(`${where} declares both options and optionsQuery — pick one`);
|
|
579
579
|
}
|
|
580
|
+
if (fdef.optionsAvailabilityQuery !== undefined) {
|
|
581
|
+
throw new Error(`${where} declares both optionsQuery and optionsAvailabilityQuery — availability only applies to static options`);
|
|
582
|
+
}
|
|
580
583
|
validateOptionsQueryFieldRefs(where, fieldName, fdef.optionsQueryPayload, siblingFieldNames);
|
|
581
584
|
}
|
|
582
585
|
else if (fdef.optionsAvailabilityQuery !== undefined) {
|
|
583
586
|
if (fdef.optionsAvailabilityQuery.length === 0) {
|
|
584
|
-
throw new Error(`${where} has an empty optionsAvailabilityQuery
|
|
587
|
+
throw new Error(`${where} has an empty optionsAvailabilityQuery. Set it to a query name or remove it.`);
|
|
585
588
|
}
|
|
586
589
|
validateOptionsQueryFieldRefs(where, fieldName, fdef.optionsQueryPayload, siblingFieldNames);
|
|
587
590
|
}
|
|
588
591
|
else if (fdef.optionsQueryPayload !== undefined) {
|
|
589
|
-
throw new Error(`${where} has optionsQueryPayload without optionsQuery
|
|
592
|
+
throw new Error(`${where} has optionsQueryPayload without optionsQuery. Set optionsQuery or remove optionsQueryPayload.`);
|
|
590
593
|
}
|
|
591
594
|
}
|
|
592
595
|
else if (fdef.type === "multiSelect" && fdef.optionsAvailabilityQuery !== undefined) {
|
|
593
596
|
if (fdef.optionsAvailabilityQuery.length === 0) {
|
|
594
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) multiSelect field "${fieldName}" has an empty optionsAvailabilityQuery
|
|
597
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) multiSelect field "${fieldName}" has an empty optionsAvailabilityQuery. Set it to a query name or remove it.`);
|
|
595
598
|
}
|
|
596
599
|
}
|
|
597
600
|
}
|
|
@@ -664,11 +667,11 @@ function validateFormLayoutSections(featureName, screenId, context, layout, fiel
|
|
|
664
667
|
if (section.kind === "relatedList") {
|
|
665
668
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) relatedList section ` +
|
|
666
669
|
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
667
|
-
`primitive (fw#2166).`);
|
|
670
|
+
`primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
|
|
668
671
|
}
|
|
669
672
|
if (isWriteFormEditSection(section)) {
|
|
670
673
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) writeForm section ` +
|
|
671
|
-
`"${section.title}" is not supported — writeForm is a projectionDetail-only primitive.`);
|
|
674
|
+
`"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
|
|
672
675
|
}
|
|
673
676
|
validateFieldsXorGroups(`[Feature ${featureName}] Screen "${screenId}" (${context})`, section);
|
|
674
677
|
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
@@ -700,7 +703,7 @@ function validateRedirectTarget(feature, screenId, screenKind, redirect, lookups
|
|
|
700
703
|
// never matches and the navigation silently lands without an entityId.
|
|
701
704
|
if (typeof redirect !== "string" && redirect.idFrom !== redirect.idFrom.trim()) {
|
|
702
705
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screenKind}) redirect.idFrom "${redirect.idFrom}" ` +
|
|
703
|
-
`has leading/trailing whitespace — it would never match a payload field.`);
|
|
706
|
+
`has leading/trailing whitespace — it would never match a payload field. Remove the whitespace from idFrom.`);
|
|
704
707
|
}
|
|
705
708
|
validateScreenNavTarget(feature.name, screenId, screenKind, "redirect", redirectTarget, allScreenQns, feature.screens);
|
|
706
709
|
// idFrom only means something to a target that carries a record id; the
|
|
@@ -758,10 +761,10 @@ function validateScreenVisibleWhen(feature, screenId, screen, allScreenQns, feat
|
|
|
758
761
|
const { visibleWhen, fallback } = screen;
|
|
759
762
|
const context = `[Feature ${feature.name}] Screen "${screenId}" (${screen.type})`;
|
|
760
763
|
if (visibleWhen === undefined && fallback !== undefined) {
|
|
761
|
-
throw new Error(`${context} declares fallback without visibleWhen.`);
|
|
764
|
+
throw new Error(`${context} declares fallback without visibleWhen. Add visibleWhen or remove fallback.`);
|
|
762
765
|
}
|
|
763
766
|
if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
|
|
764
|
-
throw new Error(`${context} visibleWhen needs a non-empty query and field.`);
|
|
767
|
+
throw new Error(`${context} visibleWhen needs a non-empty query and field. Set both visibleWhen.query and visibleWhen.field.`);
|
|
765
768
|
}
|
|
766
769
|
if (fallback !== undefined) {
|
|
767
770
|
validateScreenFallback(feature, screenId, screen.type, fallback, allScreenQns, featureMap);
|
|
@@ -772,7 +775,7 @@ function validateScreenFallback(feature, screenId, screenType, fallback, allScre
|
|
|
772
775
|
validateScreenNavTarget(feature.name, screenId, screenType, "fallback", fallback, allScreenQns, feature.screens);
|
|
773
776
|
const fallbackQn = resolveScreenTargetQn(feature.name, fallback);
|
|
774
777
|
if (fallbackQn === qualifyEntityName(feature.name, "screen", screenId)) {
|
|
775
|
-
throw new Error(`${context} fallback points at the screen itself.`);
|
|
778
|
+
throw new Error(`${context} fallback points at the screen itself. Point fallback at a different screen.`);
|
|
776
779
|
}
|
|
777
780
|
// A gated fallback could hide in turn and chain or cycle (A→B→A); one hop keeps the
|
|
778
781
|
// runtime gate trivially terminating.
|
|
@@ -836,7 +839,7 @@ function validateSecretMintConfirm(feature, screenId, screen, allWriteHandlerQns
|
|
|
836
839
|
for (const carryField of confirm.carry) {
|
|
837
840
|
if (typeof carryField !== "string" || carryField.trim() === "") {
|
|
838
841
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${context}) has an empty or non-string ` +
|
|
839
|
-
`entry in "carry".`);
|
|
842
|
+
`entry in "carry". Use non-empty strings in carry.`);
|
|
840
843
|
}
|
|
841
844
|
if (fieldNames.has(carryField)) {
|
|
842
845
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${context}) carry field "${carryField}" ` +
|
|
@@ -915,7 +918,7 @@ function validateScreenHasNavArea(feature, screenId, screen, featureMap) {
|
|
|
915
918
|
function validateRelatedListSection(args) {
|
|
916
919
|
const { featureName, featureMap, where, section } = args;
|
|
917
920
|
if (!section.query || typeof section.query !== "string") {
|
|
918
|
-
throw new Error(`[Feature ${featureName}] ${where} has empty or non-string query.`);
|
|
921
|
+
throw new Error(`[Feature ${featureName}] ${where} has empty or non-string query. Set query to a registered query name.`);
|
|
919
922
|
}
|
|
920
923
|
if (section.entity !== undefined) {
|
|
921
924
|
assertRefTargetRegistered(`[Feature ${featureName}] ${where}`, "entity", section.entity, featureName, featureMap);
|
|
@@ -946,23 +949,51 @@ function validateRelatedListSection(args) {
|
|
|
946
949
|
validateRelatedListToolbarActions(args, section.toolbarActions);
|
|
947
950
|
}
|
|
948
951
|
}
|
|
952
|
+
// An empty list has no row, so params resolve against an empty record.
|
|
953
|
+
function rejectEmptyStateActionParams(ctx, actionLabel, action) {
|
|
954
|
+
const params = action.kind === "navigate" || action.kind === "drawer" ? action.params : undefined;
|
|
955
|
+
if (params !== undefined) {
|
|
956
|
+
throw new Error(`[Feature ${ctx.featureName}] ${ctx.where} ${actionLabel} "${action.id}" sets params — an empty list ` +
|
|
957
|
+
`has no row to read them from, so they would resolve against an empty record. Remove the params.`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// Target checks shared by list/related-list rowActions and emptyState.action:
|
|
961
|
+
// navigate target + params, drawer target, write-handler registration.
|
|
962
|
+
function validateListActionTargets(ctx, actionLabel, action) {
|
|
963
|
+
const { featureName, screenId, screenType } = ctx;
|
|
964
|
+
rejectRedirectOnListRowAction(featureName, screenId, screenType, action);
|
|
965
|
+
if (actionLabel === "emptyState action")
|
|
966
|
+
rejectEmptyStateActionParams(ctx, actionLabel, action);
|
|
967
|
+
if (action.kind === "navigate") {
|
|
968
|
+
const target = resolveRowActionNavigateTarget(featureName, screenId, screenType, actionLabel, action, ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
|
|
969
|
+
validateRowActionNavigateParams(featureName, screenId, screenType, ctx.screenEntity, action, target, actionLabel);
|
|
970
|
+
}
|
|
971
|
+
else if (action.kind === "drawer") {
|
|
972
|
+
validateDrawerTargetAction(featureName, screenId, screenType, actionLabel, action, ctx.screens);
|
|
973
|
+
}
|
|
974
|
+
else if (!ctx.allWriteHandlerQns.has(action.handler)) {
|
|
975
|
+
throw new Error(`[Feature ${featureName}] ${ctx.where} ` +
|
|
976
|
+
`${actionLabel} "${action.id}" handler "${action.handler}" ` +
|
|
977
|
+
`is not a registered write-handler. Check the QN spelling (expected ` +
|
|
978
|
+
`"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
949
981
|
function validateRelatedListRowActions(args, rowActions) {
|
|
950
982
|
const { featureName, screenId, screenType, where, section, screens, allWriteHandlerQns, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens, } = args;
|
|
951
983
|
for (const action of rowActions) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
984
|
+
validateListActionTargets({
|
|
985
|
+
featureName,
|
|
986
|
+
screenId,
|
|
987
|
+
screenType,
|
|
988
|
+
screenEntity: undefined,
|
|
989
|
+
where,
|
|
990
|
+
screens,
|
|
991
|
+
allWriteHandlerQns,
|
|
992
|
+
allScreenQns,
|
|
993
|
+
navTargetShortIds,
|
|
994
|
+
screensByShortId,
|
|
995
|
+
detailForScreens,
|
|
996
|
+
}, "rowAction", action);
|
|
966
997
|
}
|
|
967
998
|
// section.rowClick (legacy, navigate-only) and a rowActions entry
|
|
968
999
|
// marked rowClick:true both claim the row-body click — same
|
|
@@ -982,7 +1013,7 @@ function validateRelatedListDefaultSort(args, defaultSort) {
|
|
|
982
1013
|
const col = section.columns.find((c) => normalizeListColumn(c).field === sortField);
|
|
983
1014
|
if (col === undefined) {
|
|
984
1015
|
throw new Error(`[Feature ${featureName}] ${where} ` +
|
|
985
|
-
`defaultSort.field "${sortField}" is not a listed column.`);
|
|
1016
|
+
`defaultSort.field "${sortField}" is not a listed column. Use a listed column as defaultSort.field.`);
|
|
986
1017
|
}
|
|
987
1018
|
if (normalizeListColumn(col).sortable !== true) {
|
|
988
1019
|
throw new Error(`[Feature ${featureName}] ${where} ` +
|
|
@@ -1032,7 +1063,16 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1032
1063
|
if ((screen.type === "entityList" || screen.type === "projectionList") &&
|
|
1033
1064
|
screen.emptyState?.action !== undefined) {
|
|
1034
1065
|
validateActionHasIcon(feature.name, screenId, screen.type, "emptyState action", screen.emptyState.action);
|
|
1035
|
-
|
|
1066
|
+
validateListActionTargets({
|
|
1067
|
+
featureName: feature.name,
|
|
1068
|
+
screenId,
|
|
1069
|
+
screenType: screen.type,
|
|
1070
|
+
screenEntity: undefined,
|
|
1071
|
+
where: `Screen "${screenId}" (${screen.type})`,
|
|
1072
|
+
screens: feature.screens,
|
|
1073
|
+
allWriteHandlerQns,
|
|
1074
|
+
...navigateLookups,
|
|
1075
|
+
}, "emptyState action", screen.emptyState.action);
|
|
1036
1076
|
}
|
|
1037
1077
|
// Output of buildAppSchema, which overwrites it — an authored value would be a silent no-op.
|
|
1038
1078
|
if ((screen.type === "entityEdit" ||
|
|
@@ -1063,7 +1103,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1063
1103
|
// Column-Felder können nicht gegen eine Entity gecheckt werden; sie
|
|
1064
1104
|
// werden zur Render-Zeit gegen die Projection-Rows aufgelöst).
|
|
1065
1105
|
if (!screen.query || typeof screen.query !== "string") {
|
|
1066
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has empty or non-string query.`);
|
|
1106
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has empty or non-string query. Set query to a registered query name.`);
|
|
1067
1107
|
}
|
|
1068
1108
|
if (screen.columns.length === 0) {
|
|
1069
1109
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has an empty columns list — ` +
|
|
@@ -1085,7 +1125,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1085
1125
|
screen.filter.op === "in" &&
|
|
1086
1126
|
!Array.isArray(screen.filter.value)) {
|
|
1087
1127
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) filter.op "in" requires ` +
|
|
1088
|
-
`filter.value to be a readonly array.`);
|
|
1128
|
+
`filter.value to be a readonly array. Pass an array as filter.value.`);
|
|
1089
1129
|
}
|
|
1090
1130
|
if (screen.facets !== undefined) {
|
|
1091
1131
|
validateListFacets(`[Feature ${feature.name}] Screen "${screenId}" (projectionList)`, screen.facets, screen.columns, feature.name, featureMap);
|
|
@@ -1123,7 +1163,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1123
1163
|
// Columns. Kein Entity-Check möglich — Felder werden render-seitig
|
|
1124
1164
|
// gegen die Query-Response aufgelöst, nicht gegen eine Entity.
|
|
1125
1165
|
if (!screen.query || typeof screen.query !== "string") {
|
|
1126
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has empty or non-string query.`);
|
|
1166
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has empty or non-string query. Set query to a registered query name.`);
|
|
1127
1167
|
}
|
|
1128
1168
|
if (screen.singleton === true && screen.idParam !== undefined) {
|
|
1129
1169
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
|
|
@@ -1176,7 +1216,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1176
1216
|
if (navigate.tab !== undefined &&
|
|
1177
1217
|
!screen.layout.sections.some((section) => section.id === navigate.tab)) {
|
|
1178
1218
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) metric "${field}" ` +
|
|
1179
|
-
`navigates to tab "${navigate.tab}", which is not a section id on this screen.`);
|
|
1219
|
+
`navigates to tab "${navigate.tab}", which is not a section id on this screen. Navigate to an existing section id or remove navigate.tab.`);
|
|
1180
1220
|
}
|
|
1181
1221
|
}
|
|
1182
1222
|
else {
|
|
@@ -1205,14 +1245,14 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1205
1245
|
if (Array.isArray(screen.header?.subtitle)) {
|
|
1206
1246
|
if (screen.header.subtitleHref !== undefined) {
|
|
1207
1247
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets header.subtitleHref ` +
|
|
1208
|
-
`together with a multi-part header.subtitle — subtitleHref only works with the string form.`);
|
|
1248
|
+
`together with a multi-part header.subtitle — subtitleHref only works with the string form. Use a single-field subtitle or remove subtitleHref.`);
|
|
1209
1249
|
}
|
|
1210
1250
|
const seenPartFields = new Set();
|
|
1211
1251
|
for (const part of screen.header.subtitle) {
|
|
1212
1252
|
const partField = typeof part === "string" ? part : part.field;
|
|
1213
1253
|
if (seenPartFields.has(partField)) {
|
|
1214
1254
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) header.subtitle ` +
|
|
1215
|
-
`lists field "${partField}" twice.`);
|
|
1255
|
+
`lists field "${partField}" twice. List each field once.`);
|
|
1216
1256
|
}
|
|
1217
1257
|
seenPartFields.add(partField);
|
|
1218
1258
|
const navigate = typeof part === "string" ? undefined : part.navigate;
|
|
@@ -1244,7 +1284,16 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1244
1284
|
}
|
|
1245
1285
|
if (section.kind === "relatedList" && section.emptyState?.action !== undefined) {
|
|
1246
1286
|
validateActionHasIcon(feature.name, screenId, "projectionDetail", `section "${sectionLabel}" emptyState action`, section.emptyState.action);
|
|
1247
|
-
|
|
1287
|
+
validateListActionTargets({
|
|
1288
|
+
featureName: feature.name,
|
|
1289
|
+
screenId,
|
|
1290
|
+
screenType: "projectionDetail",
|
|
1291
|
+
screenEntity: undefined,
|
|
1292
|
+
where: `Screen "${screenId}" (projectionDetail) section "${sectionLabel}"`,
|
|
1293
|
+
screens: feature.screens,
|
|
1294
|
+
allWriteHandlerQns,
|
|
1295
|
+
...navigateLookups,
|
|
1296
|
+
}, "emptyState action", section.emptyState.action);
|
|
1248
1297
|
}
|
|
1249
1298
|
if (isExtensionEditSection(section)) {
|
|
1250
1299
|
// projectionDetail is read-only (no composed form submit) — an
|
|
@@ -1318,7 +1367,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1318
1367
|
if (section.fieldDefs[fieldName] === undefined) {
|
|
1319
1368
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
1320
1369
|
`(writeForm) field "${fieldName}" has no entry in fieldDefs — every rendered field needs ` +
|
|
1321
|
-
`a type declared there.`);
|
|
1370
|
+
`a type declared there. Add the field to fieldDefs.`);
|
|
1322
1371
|
}
|
|
1323
1372
|
}
|
|
1324
1373
|
if (screen.layout.mode === "wizard") {
|
|
@@ -1411,11 +1460,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1411
1460
|
if (section.kind === "relatedList") {
|
|
1412
1461
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) relatedList section ` +
|
|
1413
1462
|
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
1414
|
-
`primitive (fw#2166).`);
|
|
1463
|
+
`primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
|
|
1415
1464
|
}
|
|
1416
1465
|
if (isWriteFormEditSection(section)) {
|
|
1417
1466
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) writeForm section ` +
|
|
1418
|
-
`"${section.title}" is not supported — writeForm is a projectionDetail-only primitive.`);
|
|
1467
|
+
`"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
|
|
1419
1468
|
}
|
|
1420
1469
|
validateFieldsXorGroups(`[Feature ${feature.name}] Screen "${screenId}" (configEdit)`, section);
|
|
1421
1470
|
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
@@ -1432,15 +1481,15 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1432
1481
|
for (const fname of fieldNames) {
|
|
1433
1482
|
const qualified = screen.configKeys[fname];
|
|
1434
1483
|
if (qualified === undefined) {
|
|
1435
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}"
|
|
1436
|
-
`
|
|
1437
|
-
`
|
|
1484
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" has ` +
|
|
1485
|
+
`no entry in the configKeys map. Every declared field needs a mapping to a ` +
|
|
1486
|
+
`qualified config key (\`<feature>:config:<short>\`). Add one to configKeys.`);
|
|
1438
1487
|
}
|
|
1439
1488
|
const configKeyDef = configKeyDefsByQn.get(qualified);
|
|
1440
1489
|
if (configKeyDef === undefined) {
|
|
1441
1490
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" → ` +
|
|
1442
|
-
`Config
|
|
1443
|
-
`
|
|
1491
|
+
`Config key "${qualified}" is not declared in any feature registry. Typo? ` +
|
|
1492
|
+
`Expected format: "<feature>:config:<short>". Known keys: ${[...configKeyDefsByQn.keys()].sort().join(", ") || "(none)"}`);
|
|
1444
1493
|
}
|
|
1445
1494
|
if (isWriteOnlyField(screen.fields[fname]) && !isEncryptedAtRest(configKeyDef)) {
|
|
1446
1495
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" declares writeOnly ` +
|
|
@@ -1464,7 +1513,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1464
1513
|
for (const revealField of screen.reveal.fields) {
|
|
1465
1514
|
if (typeof revealField.field !== "string" || revealField.field.trim() === "") {
|
|
1466
1515
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) has a reveal.fields entry ` +
|
|
1467
|
-
`with an empty or non-string "field".`);
|
|
1516
|
+
`with an empty or non-string "field". Set field to a non-empty string.`);
|
|
1468
1517
|
}
|
|
1469
1518
|
if (revealFieldNames.has(revealField.field)) {
|
|
1470
1519
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) reveal.fields has a ` +
|
|
@@ -1473,7 +1522,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1473
1522
|
revealFieldNames.add(revealField.field);
|
|
1474
1523
|
if (typeof revealField.label !== "string" || revealField.label.trim() === "") {
|
|
1475
1524
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) reveal.fields entry ` +
|
|
1476
|
-
`"${revealField.field}" has an empty or non-string "label".`);
|
|
1525
|
+
`"${revealField.field}" has an empty or non-string "label". Set label to a non-empty string.`);
|
|
1477
1526
|
}
|
|
1478
1527
|
}
|
|
1479
1528
|
validateSecretMintConfirm(feature, screenId, screen, allWriteHandlerQns);
|
|
@@ -1608,7 +1657,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1608
1657
|
}
|
|
1609
1658
|
if (screen.filter.op === "in" && !Array.isArray(screen.filter.value)) {
|
|
1610
1659
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter.op "in" requires ` +
|
|
1611
|
-
`filter.value to be a readonly array.`);
|
|
1660
|
+
`filter.value to be a readonly array. Pass an array as filter.value.`);
|
|
1612
1661
|
}
|
|
1613
1662
|
}
|
|
1614
1663
|
// Tier 2.7e-1: rowActions pinnen — navigate-target existiert (selbes
|
|
@@ -1679,7 +1728,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1679
1728
|
const candidateQn = qualifyEntityName(feature.name, "screen", action.screen);
|
|
1680
1729
|
if (!allScreenQns.has(candidateQn) && !navTargetShortIds.has(action.screen)) {
|
|
1681
1730
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) toolbarAction "${action.id}" ` +
|
|
1682
|
-
`navigate-target "${action.screen}" does not resolve to a registered screen in any feature.`);
|
|
1731
|
+
`navigate-target "${action.screen}" does not resolve to a registered screen in any feature. Use "<feature>:screen:<id>" of an existing screen.`);
|
|
1683
1732
|
}
|
|
1684
1733
|
if (action.tab !== undefined) {
|
|
1685
1734
|
resolveRowActionNavigateTarget(feature.name, screenId, "entityList", "toolbarAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
|
|
@@ -1747,11 +1796,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1747
1796
|
if (section.kind === "relatedList") {
|
|
1748
1797
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) relatedList section ` +
|
|
1749
1798
|
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
1750
|
-
`primitive (fw#2166).`);
|
|
1799
|
+
`primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
|
|
1751
1800
|
}
|
|
1752
1801
|
if (isWriteFormEditSection(section)) {
|
|
1753
1802
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) writeForm section ` +
|
|
1754
|
-
`"${section.title}" is not supported — writeForm is a projectionDetail-only primitive.`);
|
|
1803
|
+
`"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
|
|
1755
1804
|
}
|
|
1756
1805
|
validateFieldsXorGroups(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit)`, section);
|
|
1757
1806
|
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
@@ -1819,7 +1868,7 @@ function validateDashboardScreen(featureName, screenId, screen, featureMap) {
|
|
|
1819
1868
|
const panelIds = new Set();
|
|
1820
1869
|
const addPanelId = (id, context) => {
|
|
1821
1870
|
if (panelIds.has(id)) {
|
|
1822
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) has duplicate panel id "${id}" (${context}).`);
|
|
1871
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) has duplicate panel id "${id}" (${context}). Give each panel a unique id.`);
|
|
1823
1872
|
}
|
|
1824
1873
|
panelIds.add(id);
|
|
1825
1874
|
};
|
|
@@ -1848,9 +1897,9 @@ function validateDashboardScreen(featureName, screenId, screen, featureMap) {
|
|
|
1848
1897
|
function validateDashboardTimeRange(featureName, screenId, timeRange, filterId) {
|
|
1849
1898
|
const prefix = `[Feature ${featureName}] Screen "${screenId}" (dashboard) timeRange`;
|
|
1850
1899
|
if (timeRange.id.length === 0)
|
|
1851
|
-
throw new Error(`${prefix} has an empty id.`);
|
|
1900
|
+
throw new Error(`${prefix} has an empty id. Set a non-empty id.`);
|
|
1852
1901
|
if (timeRange.id === filterId) {
|
|
1853
|
-
throw new Error(`${prefix} id "${timeRange.id}" collides with the filter id — both live in the URL search params.`);
|
|
1902
|
+
throw new Error(`${prefix} id "${timeRange.id}" collides with the filter id — both live in the URL search params. Use an id different from the filter id.`);
|
|
1854
1903
|
}
|
|
1855
1904
|
if (timeRange.options.length === 0) {
|
|
1856
1905
|
throw new Error(`${prefix}.options is empty — declare at least one option.`);
|
|
@@ -1858,15 +1907,15 @@ function validateDashboardTimeRange(featureName, screenId, timeRange, filterId)
|
|
|
1858
1907
|
const values = new Set();
|
|
1859
1908
|
for (const option of timeRange.options) {
|
|
1860
1909
|
if (option.value.length === 0 || option.label.length === 0) {
|
|
1861
|
-
throw new Error(`${prefix} option needs a non-empty value and label.`);
|
|
1910
|
+
throw new Error(`${prefix} option needs a non-empty value and label. Give every option both.`);
|
|
1862
1911
|
}
|
|
1863
1912
|
if (values.has(option.value)) {
|
|
1864
|
-
throw new Error(`${prefix}.options has duplicate value "${option.value}".`);
|
|
1913
|
+
throw new Error(`${prefix}.options has duplicate value "${option.value}". Use each option value once.`);
|
|
1865
1914
|
}
|
|
1866
1915
|
values.add(option.value);
|
|
1867
1916
|
}
|
|
1868
1917
|
if (!values.has(timeRange.default)) {
|
|
1869
|
-
throw new Error(`${prefix}.default "${timeRange.default}" is not among the options (${[...values].join(", ")}).`);
|
|
1918
|
+
throw new Error(`${prefix}.default "${timeRange.default}" is not among the options (${[...values].join(", ")}). Set default to one of the options.`);
|
|
1870
1919
|
}
|
|
1871
1920
|
}
|
|
1872
1921
|
const EMBEDDABLE_SCREEN_TYPES = new Set([
|
|
@@ -1902,7 +1951,7 @@ export function validateDashboardScreenPanel(featureName, screenId, panel, featu
|
|
|
1902
1951
|
}
|
|
1903
1952
|
function validateDashboardPanelVisibleWhen(context, visibleWhen) {
|
|
1904
1953
|
if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
|
|
1905
|
-
throw new Error(`${context} visibleWhen needs a non-empty query and field.`);
|
|
1954
|
+
throw new Error(`${context} visibleWhen needs a non-empty query and field. Set both visibleWhen.query and visibleWhen.field.`);
|
|
1906
1955
|
}
|
|
1907
1956
|
}
|
|
1908
1957
|
function validateDashboardPanelSpan(featureName, screenId, panel) {
|
|
@@ -1913,10 +1962,10 @@ function validateDashboardPanelSpan(featureName, screenId, panel) {
|
|
|
1913
1962
|
function validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelId) {
|
|
1914
1963
|
const context = `[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}"`;
|
|
1915
1964
|
if (panel.stats.length === 0) {
|
|
1916
|
-
throw new Error(`${context} has an empty stats list.`);
|
|
1965
|
+
throw new Error(`${context} has an empty stats list. Add at least one stat.`);
|
|
1917
1966
|
}
|
|
1918
1967
|
if (panel.subtitle !== undefined && panel.label === undefined) {
|
|
1919
|
-
throw new Error(`${context} sets subtitle without label — an unlabeled group renders as a KPI strip without a header.`);
|
|
1968
|
+
throw new Error(`${context} sets subtitle without label — an unlabeled group renders as a KPI strip without a header. Set a label or remove subtitle.`);
|
|
1920
1969
|
}
|
|
1921
1970
|
validateDashboardPanelSpan(featureName, screenId, panel);
|
|
1922
1971
|
validateDashboardPanelVisibleWhen(context, panel.visibleWhen);
|
|
@@ -1926,22 +1975,27 @@ function validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelI
|
|
|
1926
1975
|
throw new Error(`${context} child "${stat.id}" sets visibleWhen — gate the whole group instead; a hidden cell would leave a gap in the strip.`);
|
|
1927
1976
|
}
|
|
1928
1977
|
if (!stat.query || typeof stat.query !== "string") {
|
|
1929
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty or non-string query.`);
|
|
1978
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty or non-string query. Set query to a registered query name.`);
|
|
1930
1979
|
}
|
|
1931
1980
|
if (stat.valueField.length === 0) {
|
|
1932
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty valueField.`);
|
|
1981
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty valueField. Set valueField to a field of the query result.`);
|
|
1933
1982
|
}
|
|
1934
1983
|
validateDashboardValueFormat(featureName, screenId, stat.id, stat.valueFormat);
|
|
1935
1984
|
}
|
|
1936
1985
|
}
|
|
1986
|
+
function validateFromTodayWindowReach(where, panel) {
|
|
1987
|
+
if (panel.initialWindow === "from-today" && panel.ranges === undefined && panel.brush !== true) {
|
|
1988
|
+
throw new Error(`${where}.initialWindow "from-today" needs ranges or brush, otherwise data before today is unreachable.`);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1937
1991
|
function validateDashboardChartDisplay(featureName, screenId, panel) {
|
|
1938
1992
|
validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
|
|
1939
1993
|
const where = `[Feature ${featureName}] Screen "${screenId}" (dashboard) chart-panel "${panel.id}"`;
|
|
1940
1994
|
if (panel.scrollable !== undefined && typeof panel.scrollable !== "boolean") {
|
|
1941
|
-
throw new Error(`${where} has a non-boolean scrollable.`);
|
|
1995
|
+
throw new Error(`${where} has a non-boolean scrollable. Use true or false.`);
|
|
1942
1996
|
}
|
|
1943
1997
|
if (panel.scrollable === true && panel.chart !== "stacked-area") {
|
|
1944
|
-
throw new Error(`${where} sets scrollable on chart "${panel.chart}" — only "stacked-area" supports it.`);
|
|
1998
|
+
throw new Error(`${where} sets scrollable on chart "${panel.chart}" — only "stacked-area" supports it. Remove scrollable or use chart "stacked-area".`);
|
|
1945
1999
|
}
|
|
1946
2000
|
validateStackedAreaOnlyProps(where, panel);
|
|
1947
2001
|
validateStackedAreaEnumProp(where, "dateFormat", panel.dateFormat, ["day", "month"]);
|
|
@@ -1952,8 +2006,9 @@ function validateDashboardChartDisplay(featureName, screenId, panel) {
|
|
|
1952
2006
|
validateStackedAreaEnumProp(where, "markerLegend", panel.markerLegend, ["list", "legend"]);
|
|
1953
2007
|
const nonBooleanProp = ["brush", "legendTotals", "markerLabels"].find((prop) => panel[prop] !== undefined && typeof panel[prop] !== "boolean");
|
|
1954
2008
|
if (nonBooleanProp !== undefined) {
|
|
1955
|
-
throw new Error(`${where} has a non-boolean ${nonBooleanProp}.`);
|
|
2009
|
+
throw new Error(`${where} has a non-boolean ${nonBooleanProp}. Use true or false.`);
|
|
1956
2010
|
}
|
|
2011
|
+
validateFromTodayWindowReach(where, panel);
|
|
1957
2012
|
for (const [key, color] of Object.entries(panel.seriesColors ?? {})) {
|
|
1958
2013
|
if (typeof color !== "string" || color.trim() === "") {
|
|
1959
2014
|
throw new Error(`${where} seriesColors["${key}"] must be a non-empty CSS color.`);
|
|
@@ -1988,7 +2043,7 @@ function validateStackedAreaOnlyProps(where, panel) {
|
|
|
1988
2043
|
"markerLabels",
|
|
1989
2044
|
].filter((prop) => panel[prop] !== undefined);
|
|
1990
2045
|
if (stackedAreaOnly.length > 0) {
|
|
1991
|
-
throw new Error(`${where} sets ${stackedAreaOnly.join(", ")} on chart "${panel.chart}" — only "stacked-area" supports it.`);
|
|
2046
|
+
throw new Error(`${where} sets ${stackedAreaOnly.join(", ")} on chart "${panel.chart}" — only "stacked-area" supports it. Remove these props or use chart "stacked-area".`);
|
|
1992
2047
|
}
|
|
1993
2048
|
}
|
|
1994
2049
|
const DASHBOARD_CHART_TONES = new Set([
|
|
@@ -1999,7 +2054,7 @@ const DASHBOARD_CHART_TONES = new Set([
|
|
|
1999
2054
|
]);
|
|
2000
2055
|
function validateDashboardChartMarkerKind(where, look) {
|
|
2001
2056
|
if (look.tone === undefined && look.color === undefined) {
|
|
2002
|
-
throw new Error(`${where} needs a tone or a color.`);
|
|
2057
|
+
throw new Error(`${where} needs a tone or a color. Set tone or color.`);
|
|
2003
2058
|
}
|
|
2004
2059
|
if (look.tone !== undefined && !DASHBOARD_CHART_TONES.has(look.tone)) {
|
|
2005
2060
|
throw new Error(`${where}.tone "${String(look.tone)}" is not one of ${[...DASHBOARD_CHART_TONES].join(", ")}.`);
|
|
@@ -2018,10 +2073,10 @@ function validateDashboardChartRanges(where, ranges) {
|
|
|
2018
2073
|
const values = new Set();
|
|
2019
2074
|
for (const option of ranges.options) {
|
|
2020
2075
|
if (option.value.length === 0 || option.label.length === 0) {
|
|
2021
|
-
throw new Error(`${where} option needs a non-empty value and label.`);
|
|
2076
|
+
throw new Error(`${where} option needs a non-empty value and label. Give every option both.`);
|
|
2022
2077
|
}
|
|
2023
2078
|
if (values.has(option.value)) {
|
|
2024
|
-
throw new Error(`${where}.options has duplicate value "${option.value}".`);
|
|
2079
|
+
throw new Error(`${where}.options has duplicate value "${option.value}". Use each option value once.`);
|
|
2025
2080
|
}
|
|
2026
2081
|
if (option.months !== undefined && (!Number.isInteger(option.months) || option.months <= 0)) {
|
|
2027
2082
|
throw new Error(`${where} option "${option.value}" months "${String(option.months)}" must be a positive integer.`);
|
|
@@ -2029,7 +2084,7 @@ function validateDashboardChartRanges(where, ranges) {
|
|
|
2029
2084
|
values.add(option.value);
|
|
2030
2085
|
}
|
|
2031
2086
|
if (!values.has(ranges.default)) {
|
|
2032
|
-
throw new Error(`${where}.default "${ranges.default}" is not among the options (${[...values].join(", ")}).`);
|
|
2087
|
+
throw new Error(`${where}.default "${ranges.default}" is not among the options (${[...values].join(", ")}). Set default to one of the options.`);
|
|
2033
2088
|
}
|
|
2034
2089
|
}
|
|
2035
2090
|
const CURRENCY_CODE_RE = /^[A-Z]{3}$/;
|
|
@@ -2039,6 +2094,9 @@ function validateDashboardValueFormat(featureName, screenId, panelId, valueForma
|
|
|
2039
2094
|
if (valueFormat === undefined)
|
|
2040
2095
|
return;
|
|
2041
2096
|
const where = `[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panelId}" valueFormat`;
|
|
2097
|
+
if (valueFormat.kind !== "currency") {
|
|
2098
|
+
throw new Error(`${where}.kind "${String(valueFormat.kind)}" must be "currency".`);
|
|
2099
|
+
}
|
|
2042
2100
|
if (!CURRENCY_CODE_RE.test(valueFormat.currency)) {
|
|
2043
2101
|
throw new Error(`${where}.currency "${String(valueFormat.currency)}" must be a 3-letter uppercase ISO currency code.`);
|
|
2044
2102
|
}
|
|
@@ -2056,12 +2114,12 @@ export function validateDashboardCustomPanel(featureName, screenId, panel) {
|
|
|
2056
2114
|
}
|
|
2057
2115
|
function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
|
|
2058
2116
|
if (!panel.query || typeof panel.query !== "string") {
|
|
2059
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has empty or non-string query.`);
|
|
2117
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has empty or non-string query. Set query to a registered query name.`);
|
|
2060
2118
|
}
|
|
2061
2119
|
validateDashboardPanelSpan(featureName, screenId, panel);
|
|
2062
2120
|
if (panel.kind === "stat") {
|
|
2063
2121
|
if (panel.valueField.length === 0) {
|
|
2064
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField.`);
|
|
2122
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField. Set valueField to a field of the query result.`);
|
|
2065
2123
|
}
|
|
2066
2124
|
validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
|
|
2067
2125
|
}
|
|
@@ -2070,7 +2128,7 @@ function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
|
|
|
2070
2128
|
validateDashboardChartDisplay(featureName, screenId, panel);
|
|
2071
2129
|
if (panel.kind === "list") {
|
|
2072
2130
|
if (panel.columns.length === 0) {
|
|
2073
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) list-panel "${panel.id}" has an empty columns list.`);
|
|
2131
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) list-panel "${panel.id}" has an empty columns list. Add at least one column.`);
|
|
2074
2132
|
}
|
|
2075
2133
|
for (const col of panel.columns) {
|
|
2076
2134
|
const normalized = normalizeListColumn(col);
|
|
@@ -2083,10 +2141,10 @@ function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
|
|
|
2083
2141
|
}
|
|
2084
2142
|
function validateDashboardFilterDefinition(featureName, screenId, filter) {
|
|
2085
2143
|
if (filter.id.length === 0) {
|
|
2086
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty id.`);
|
|
2144
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty id. Set a non-empty id.`);
|
|
2087
2145
|
}
|
|
2088
2146
|
if (filter.label.length === 0) {
|
|
2089
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty label.`);
|
|
2147
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty label. Set a non-empty label.`);
|
|
2090
2148
|
}
|
|
2091
2149
|
const hasOptions = filter.options !== undefined;
|
|
2092
2150
|
const hasOptionsQuery = filter.optionsQuery !== undefined;
|
|
@@ -2099,7 +2157,7 @@ function validateDashboardFilterDefinition(featureName, screenId, filter) {
|
|
|
2099
2157
|
`declare at least one option or use optionsQuery instead.`);
|
|
2100
2158
|
}
|
|
2101
2159
|
if (hasOptionsQuery && filter.optionsQuery?.length === 0) {
|
|
2102
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter.optionsQuery is empty.`);
|
|
2160
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter.optionsQuery is empty. Set optionsQuery to a query name or remove it.`);
|
|
2103
2161
|
}
|
|
2104
2162
|
}
|
|
2105
2163
|
// Form-check für ListColumn-Renderer in der PlatformComponent-Form
|
|
@@ -2120,7 +2178,7 @@ function rejectInertColumnSortable(featureName, screenId, screenType, column) {
|
|
|
2120
2178
|
? "set sortable: true on the entity field instead"
|
|
2121
2179
|
: "sorting is derived from the query's Zod schema; only sortable: false is honored";
|
|
2122
2180
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) column "${column.field}" sets sortable — ` +
|
|
2123
|
-
`it is ignored here; ${source}.`);
|
|
2181
|
+
`it is ignored here; ${source}. Remove sortable from this column.`);
|
|
2124
2182
|
}
|
|
2125
2183
|
export function validateColumnRendererForm(featureName, screenId, column) {
|
|
2126
2184
|
const renderer = column.renderer;
|
|
@@ -2191,7 +2249,7 @@ function validateListFacets(prefix, facets, columns, currentFeatureName, feature
|
|
|
2191
2249
|
const seenFacetFields = new Set();
|
|
2192
2250
|
for (const facet of facets) {
|
|
2193
2251
|
if (seenFacetFields.has(facet.field)) {
|
|
2194
|
-
throw new Error(`${prefix} declares facet "${facet.field}" more than once.`);
|
|
2252
|
+
throw new Error(`${prefix} declares facet "${facet.field}" more than once. Declare each facet field once.`);
|
|
2195
2253
|
}
|
|
2196
2254
|
seenFacetFields.add(facet.field);
|
|
2197
2255
|
if (facet.type !== "reference" && !columnFieldNames.has(facet.field)) {
|