@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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 +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -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 +323 -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 +314 -0
- package/agents/examples/custom-handlers.md +397 -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 +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -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 +214 -0
- package/agents/examples/ledger-invoicing.md +235 -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 +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -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 +259 -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 +410 -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 +74 -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 +14 -5
- package/dist/api/auth-routes.js +5 -3
- 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/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +48 -19
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- 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/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +12 -5
- package/dist/bun-db/query.js +88 -94
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +29 -27
- package/dist/db/event-store-executor-read.js +40 -25
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +6 -4
- package/dist/db/migrate-runner.js +32 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- 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 +5 -2
- package/dist/db/tenant-db.js +34 -14
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/variant-key.js +2 -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 +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +3 -3
- 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 +9 -9
- 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/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +155 -96
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +11 -7
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +16 -5
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/feature-ui-extensions.js +2 -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 -2
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +3 -4
- package/dist/engine/soft-delete-cleanup.js +12 -4
- package/dist/engine/steps/call-feature.js +1 -3
- 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 +4 -15
- package/dist/engine/template-variables.d.ts +32 -0
- package/dist/engine/template-variables.js +80 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- 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 +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +2 -2
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/local-provider.js +2 -1
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +13 -9
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +142 -34
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +2 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- 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/search/meilisearch-adapter.js +39 -13
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +7 -5
- package/src/changes.json +251 -8
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
7
7
|
import { NO_WIDGET_FIELD_TYPES, optionsQueryFieldRefs, SELECT_OPTION_TONES, } from "@cosmicdrift/kumiko-types/fields";
|
|
8
8
|
import { NAV_ICON_KEYS } from "@cosmicdrift/kumiko-types/nav-icon";
|
|
9
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
9
10
|
import { rowMetaFieldNames } from "../../db/table-builder.js";
|
|
10
11
|
import { REFERENCE_LOOKUP_SOURCES } from "../../ui-types/list-row-meta.js";
|
|
11
12
|
import { isEncryptedAtRest } from "../config-helpers.js";
|
|
@@ -23,7 +24,7 @@ function validateAtMostOneRowClick(featureName, screenId, screenType, rowActions
|
|
|
23
24
|
const rowClickActions = rowActions.filter((a) => a.kind === "navigate" && a.rowClick === true);
|
|
24
25
|
if (rowClickActions.length > 1) {
|
|
25
26
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) has ${rowClickActions.length} ` +
|
|
26
|
-
"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.");
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
// A field type in NO_WIDGET_FIELD_TYPES renders read-only on the auto-wired
|
|
@@ -44,7 +45,7 @@ function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
|
|
|
44
45
|
if (listSpec !== undefined) {
|
|
45
46
|
if (fieldDef.type !== "embedded" || fieldDef.multiple !== true) {
|
|
46
47
|
throw new Error(`${where} sets embeddedList, which only applies to an embedded list ` +
|
|
47
|
-
`(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.`);
|
|
48
49
|
}
|
|
49
50
|
for (const cell of listSpec.readOnlyCells ?? []) {
|
|
50
51
|
if (fieldDef.schema[cell] === undefined) {
|
|
@@ -59,7 +60,7 @@ function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
|
|
|
59
60
|
return;
|
|
60
61
|
if (prefill.kind === "today") {
|
|
61
62
|
if (fieldDef.type !== "date") {
|
|
62
|
-
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.`);
|
|
63
64
|
}
|
|
64
65
|
// skip: today carries no property, nothing further to check
|
|
65
66
|
return;
|
|
@@ -67,7 +68,7 @@ function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
|
|
|
67
68
|
const allowsUserId = fieldDef.type === "text" || fieldDef.type === "reference";
|
|
68
69
|
if (prefill.property === "id" ? !allowsUserId : fieldDef.type !== "text") {
|
|
69
70
|
throw new Error(`${where} prefill currentUser.${prefill.property} does not fit a field of type ` +
|
|
70
|
-
`"${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.`);
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
function validateNoWidgetRequiredField(featureName, screenId, entityDef, fieldSpec) {
|
|
@@ -120,7 +121,7 @@ export function readsNavigateParamsAsFormPrefill(target, explicitEntityId, sourc
|
|
|
120
121
|
// for the value to land, and a params extractor whose keys don't include
|
|
121
122
|
// the filter's id would silently miss it — both are boot errors instead of
|
|
122
123
|
// a silently-empty dashboard.
|
|
123
|
-
function validateRowActionNavigateParams(featureName, screenId, screenType, screenEntity, action, target) {
|
|
124
|
+
function validateRowActionNavigateParams(featureName, screenId, screenType, screenEntity, action, target, actionLabel = "rowAction") {
|
|
124
125
|
// skip: not a navigate-with-params action — nothing to validate here.
|
|
125
126
|
if (action.kind !== "navigate" || action.params === undefined)
|
|
126
127
|
return;
|
|
@@ -136,14 +137,14 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
|
|
|
136
137
|
const targetDescriptor = action.screen !== undefined ? `"${action.screen}"` : `entity "${action.entity}"`;
|
|
137
138
|
const filter = target.screen.filter;
|
|
138
139
|
if (filter === undefined) {
|
|
139
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType})
|
|
140
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" sets ` +
|
|
140
141
|
`params on navigate-target ${targetDescriptor} (dashboard) — target dashboard declares no ` +
|
|
141
142
|
`filter — params would be a no-op. Remove the params extractor, or add a \`filter\` to the ` +
|
|
142
143
|
`target dashboard so it has somewhere to read the value from.`);
|
|
143
144
|
}
|
|
144
145
|
const extractedKeys = rowFieldExtractorKeys(action.params);
|
|
145
146
|
if (!extractedKeys.includes(filter.id)) {
|
|
146
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType})
|
|
147
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" sets ` +
|
|
147
148
|
`params [${extractedKeys.join(", ")}] on navigate-target ${targetDescriptor} (dashboard) whose ` +
|
|
148
149
|
`filter id is "${filter.id}" — none of the extracted keys match, so the filter would stay ` +
|
|
149
150
|
`unset. Fix the params extractor to produce a "${filter.id}" key, or remove it.`);
|
|
@@ -159,7 +160,7 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
|
|
|
159
160
|
: `same entity "${screenEntity}" auto-fills row["id"]`})`
|
|
160
161
|
: `screen type "${target.screen.type}"`;
|
|
161
162
|
const targetDescriptor = action.screen !== undefined ? `"${action.screen}"` : `entity "${action.entity}"`;
|
|
162
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType})
|
|
163
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
163
164
|
`sets params on navigate-target ${targetDescriptor} which ${reason} — only actionForm, ` +
|
|
164
165
|
`secretMint and entityEdit-create targets read URL search params as initial values. Remove ` +
|
|
165
166
|
`the params extractor or retarget to an actionForm / secretMint / cross-entity ` +
|
|
@@ -168,7 +169,7 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
|
|
|
168
169
|
const formTarget = target.screen;
|
|
169
170
|
if ((formTarget.type === "actionForm" || formTarget.type === "secretMint") &&
|
|
170
171
|
formTarget.submitPrefilled === true) {
|
|
171
|
-
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);
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
// Shared by projectionDetail and entityEdit: a tab strip needs something to
|
|
@@ -196,7 +197,7 @@ function validateTabSections(featureName, screenId, screenType, sections) {
|
|
|
196
197
|
}
|
|
197
198
|
if (tabIds.has(section.id)) {
|
|
198
199
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) has duplicate tab id ` +
|
|
199
|
-
`"${section.id}" (sections[${index}]).`);
|
|
200
|
+
`"${section.id}" (sections[${index}]). Give each tab section a unique id.`);
|
|
200
201
|
}
|
|
201
202
|
tabIds.add(section.id);
|
|
202
203
|
});
|
|
@@ -275,7 +276,7 @@ function validateWizardLayout(featureName, screenId, screenType, layout, feature
|
|
|
275
276
|
// for an unregistered key instead of crashing — this check makes the same
|
|
276
277
|
// gap fail loudly at boot instead of silently at render.
|
|
277
278
|
function isRegisteredIcon(icon) {
|
|
278
|
-
return NAV_ICON_KEYS
|
|
279
|
+
return isOneOf(NAV_ICON_KEYS, icon);
|
|
279
280
|
}
|
|
280
281
|
export function validateActionHasIcon(featureName, screenId, screenType, context, action) {
|
|
281
282
|
const resolved = resolveActionIcon(action.id, action.icon);
|
|
@@ -398,7 +399,7 @@ function validateDrawerTargetAction(featureName, screenId, screenKind, actionLab
|
|
|
398
399
|
return;
|
|
399
400
|
validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenKind}) ${actionLabel} "${action.id}"`, "drawer-target", action.screen, target, action.params);
|
|
400
401
|
}
|
|
401
|
-
function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, params) {
|
|
402
|
+
export function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, params) {
|
|
402
403
|
const renderedFieldNames = new Set();
|
|
403
404
|
for (const section of target.layout.sections) {
|
|
404
405
|
if (!isFieldsEditSection(section))
|
|
@@ -418,7 +419,7 @@ function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, p
|
|
|
418
419
|
("format" in targetField && targetField.format === "password"));
|
|
419
420
|
if (isSensitiveOrPassword) {
|
|
420
421
|
throw new Error(`${where} params prefills "${fieldName}", which ${targetLabel} "${targetScreenId}" marks as ` +
|
|
421
|
-
`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.`);
|
|
422
423
|
}
|
|
423
424
|
if (!renderedFieldNames.has(fieldName)) {
|
|
424
425
|
throw new Error(`${where} params prefills "${fieldName}", which ${targetLabel} "${targetScreenId}" does not render in ` +
|
|
@@ -434,12 +435,12 @@ function validateRowActionNavigateTab(featureName, screenId, screenType, actionL
|
|
|
434
435
|
return;
|
|
435
436
|
if (target === undefined) {
|
|
436
437
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
437
|
-
`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.`);
|
|
438
439
|
}
|
|
439
440
|
if (target.screen.type !== "projectionDetail" || target.screen.layout.mode !== "tabs") {
|
|
440
441
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
441
442
|
`sets tab "${action.tab}", but navigate-target screen "${target.screen.id}" is not a ` +
|
|
442
|
-
`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".`);
|
|
443
444
|
}
|
|
444
445
|
const sectionIds = target.screen.layout.sections
|
|
445
446
|
.map((section) => section.id)
|
|
@@ -470,7 +471,7 @@ function resolveRowActionNavigateTarget(featureName, screenId, screenType, actio
|
|
|
470
471
|
if (detail === undefined) {
|
|
471
472
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
472
473
|
`navigate-target entity "${action.entity}" has no screen declaring ` +
|
|
473
|
-
`detailFor: "${action.entity}".`);
|
|
474
|
+
`detailFor: "${action.entity}". Add detailFor: "${action.entity}" to a screen or navigate to a screen directly.`);
|
|
474
475
|
}
|
|
475
476
|
validateRowActionNavigateTab(featureName, screenId, screenType, actionLabel, action, detail);
|
|
476
477
|
return detail;
|
|
@@ -482,7 +483,7 @@ function resolveRowActionNavigateTarget(featureName, screenId, screenType, actio
|
|
|
482
483
|
const candidateQn = qualifyEntityName(featureName, "screen", action.screen);
|
|
483
484
|
if (!allScreenQns.has(candidateQn) && !navTargetShortIds.has(action.screen)) {
|
|
484
485
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
|
|
485
|
-
`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.`);
|
|
486
487
|
}
|
|
487
488
|
const target = screensByShortId.get(action.screen)?.[0];
|
|
488
489
|
validateRowActionNavigateTab(featureName, screenId, screenType, actionLabel, action, target);
|
|
@@ -493,7 +494,7 @@ function resolveRowActionNavigateTarget(featureName, screenId, screenType, actio
|
|
|
493
494
|
// same registration requirement.
|
|
494
495
|
function validateWriteHandlerRegistered(featureName, screenId, context, handler, allWriteHandlerQns) {
|
|
495
496
|
if (!handler || typeof handler !== "string") {
|
|
496
|
-
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.`);
|
|
497
498
|
}
|
|
498
499
|
if (!allWriteHandlerQns.has(handler)) {
|
|
499
500
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) handler "${handler}" ` +
|
|
@@ -539,7 +540,7 @@ function validateFormFieldsMap(featureName, screenId, context, fields, allowEmpt
|
|
|
539
540
|
function validateOptionsQueryFieldRefs(where, fieldName, payload, siblingFieldNames) {
|
|
540
541
|
for (const ref of optionsQueryFieldRefs(payload)) {
|
|
541
542
|
if (ref === fieldName) {
|
|
542
|
-
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.`);
|
|
543
544
|
}
|
|
544
545
|
if (!siblingFieldNames.has(ref)) {
|
|
545
546
|
throw new Error(`${where} optionsQueryPayload references unknown field "${ref}" — it must be another field of the same form`);
|
|
@@ -567,30 +568,33 @@ function validateFormSelectOptions(featureName, screenId, context, fieldName, fd
|
|
|
567
568
|
if (fdef.type === "select") {
|
|
568
569
|
const where = `[Feature ${featureName}] Screen "${screenId}" (${context}) select field "${fieldName}"`;
|
|
569
570
|
if (fdef.conditionalOptions !== undefined) {
|
|
570
|
-
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.`);
|
|
571
572
|
}
|
|
572
573
|
if (fdef.optionsQuery !== undefined) {
|
|
573
574
|
if (fdef.optionsQuery.length === 0) {
|
|
574
|
-
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.`);
|
|
575
576
|
}
|
|
576
577
|
if (fdef.options.length > 0) {
|
|
577
578
|
throw new Error(`${where} declares both options and optionsQuery — pick one`);
|
|
578
579
|
}
|
|
580
|
+
if (fdef.optionsAvailabilityQuery !== undefined) {
|
|
581
|
+
throw new Error(`${where} declares both optionsQuery and optionsAvailabilityQuery — availability only applies to static options`);
|
|
582
|
+
}
|
|
579
583
|
validateOptionsQueryFieldRefs(where, fieldName, fdef.optionsQueryPayload, siblingFieldNames);
|
|
580
584
|
}
|
|
581
585
|
else if (fdef.optionsAvailabilityQuery !== undefined) {
|
|
582
586
|
if (fdef.optionsAvailabilityQuery.length === 0) {
|
|
583
|
-
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.`);
|
|
584
588
|
}
|
|
585
589
|
validateOptionsQueryFieldRefs(where, fieldName, fdef.optionsQueryPayload, siblingFieldNames);
|
|
586
590
|
}
|
|
587
591
|
else if (fdef.optionsQueryPayload !== undefined) {
|
|
588
|
-
throw new Error(`${where} has optionsQueryPayload without optionsQuery
|
|
592
|
+
throw new Error(`${where} has optionsQueryPayload without optionsQuery. Set optionsQuery or remove optionsQueryPayload.`);
|
|
589
593
|
}
|
|
590
594
|
}
|
|
591
595
|
else if (fdef.type === "multiSelect" && fdef.optionsAvailabilityQuery !== undefined) {
|
|
592
596
|
if (fdef.optionsAvailabilityQuery.length === 0) {
|
|
593
|
-
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.`);
|
|
594
598
|
}
|
|
595
599
|
}
|
|
596
600
|
}
|
|
@@ -663,11 +667,11 @@ function validateFormLayoutSections(featureName, screenId, context, layout, fiel
|
|
|
663
667
|
if (section.kind === "relatedList") {
|
|
664
668
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) relatedList section ` +
|
|
665
669
|
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
666
|
-
`primitive (fw#2166).`);
|
|
670
|
+
`primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
|
|
667
671
|
}
|
|
668
672
|
if (isWriteFormEditSection(section)) {
|
|
669
673
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) writeForm section ` +
|
|
670
|
-
`"${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.`);
|
|
671
675
|
}
|
|
672
676
|
validateFieldsXorGroups(`[Feature ${featureName}] Screen "${screenId}" (${context})`, section);
|
|
673
677
|
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
@@ -699,7 +703,7 @@ function validateRedirectTarget(feature, screenId, screenKind, redirect, lookups
|
|
|
699
703
|
// never matches and the navigation silently lands without an entityId.
|
|
700
704
|
if (typeof redirect !== "string" && redirect.idFrom !== redirect.idFrom.trim()) {
|
|
701
705
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screenKind}) redirect.idFrom "${redirect.idFrom}" ` +
|
|
702
|
-
`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.`);
|
|
703
707
|
}
|
|
704
708
|
validateScreenNavTarget(feature.name, screenId, screenKind, "redirect", redirectTarget, allScreenQns, feature.screens);
|
|
705
709
|
// idFrom only means something to a target that carries a record id; the
|
|
@@ -757,10 +761,10 @@ function validateScreenVisibleWhen(feature, screenId, screen, allScreenQns, feat
|
|
|
757
761
|
const { visibleWhen, fallback } = screen;
|
|
758
762
|
const context = `[Feature ${feature.name}] Screen "${screenId}" (${screen.type})`;
|
|
759
763
|
if (visibleWhen === undefined && fallback !== undefined) {
|
|
760
|
-
throw new Error(`${context} declares fallback without visibleWhen.`);
|
|
764
|
+
throw new Error(`${context} declares fallback without visibleWhen. Add visibleWhen or remove fallback.`);
|
|
761
765
|
}
|
|
762
766
|
if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
|
|
763
|
-
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.`);
|
|
764
768
|
}
|
|
765
769
|
if (fallback !== undefined) {
|
|
766
770
|
validateScreenFallback(feature, screenId, screen.type, fallback, allScreenQns, featureMap);
|
|
@@ -771,7 +775,7 @@ function validateScreenFallback(feature, screenId, screenType, fallback, allScre
|
|
|
771
775
|
validateScreenNavTarget(feature.name, screenId, screenType, "fallback", fallback, allScreenQns, feature.screens);
|
|
772
776
|
const fallbackQn = resolveScreenTargetQn(feature.name, fallback);
|
|
773
777
|
if (fallbackQn === qualifyEntityName(feature.name, "screen", screenId)) {
|
|
774
|
-
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.`);
|
|
775
779
|
}
|
|
776
780
|
// A gated fallback could hide in turn and chain or cycle (A→B→A); one hop keeps the
|
|
777
781
|
// runtime gate trivially terminating.
|
|
@@ -835,7 +839,7 @@ function validateSecretMintConfirm(feature, screenId, screen, allWriteHandlerQns
|
|
|
835
839
|
for (const carryField of confirm.carry) {
|
|
836
840
|
if (typeof carryField !== "string" || carryField.trim() === "") {
|
|
837
841
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${context}) has an empty or non-string ` +
|
|
838
|
-
`entry in "carry".`);
|
|
842
|
+
`entry in "carry". Use non-empty strings in carry.`);
|
|
839
843
|
}
|
|
840
844
|
if (fieldNames.has(carryField)) {
|
|
841
845
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${context}) carry field "${carryField}" ` +
|
|
@@ -914,7 +918,7 @@ function validateScreenHasNavArea(feature, screenId, screen, featureMap) {
|
|
|
914
918
|
function validateRelatedListSection(args) {
|
|
915
919
|
const { featureName, featureMap, where, section } = args;
|
|
916
920
|
if (!section.query || typeof section.query !== "string") {
|
|
917
|
-
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.`);
|
|
918
922
|
}
|
|
919
923
|
if (section.entity !== undefined) {
|
|
920
924
|
assertRefTargetRegistered(`[Feature ${featureName}] ${where}`, "entity", section.entity, featureName, featureMap);
|
|
@@ -945,23 +949,51 @@ function validateRelatedListSection(args) {
|
|
|
945
949
|
validateRelatedListToolbarActions(args, section.toolbarActions);
|
|
946
950
|
}
|
|
947
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
|
+
}
|
|
948
981
|
function validateRelatedListRowActions(args, rowActions) {
|
|
949
982
|
const { featureName, screenId, screenType, where, section, screens, allWriteHandlerQns, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens, } = args;
|
|
950
983
|
for (const action of rowActions) {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
}
|
|
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);
|
|
965
997
|
}
|
|
966
998
|
// section.rowClick (legacy, navigate-only) and a rowActions entry
|
|
967
999
|
// marked rowClick:true both claim the row-body click — same
|
|
@@ -981,7 +1013,7 @@ function validateRelatedListDefaultSort(args, defaultSort) {
|
|
|
981
1013
|
const col = section.columns.find((c) => normalizeListColumn(c).field === sortField);
|
|
982
1014
|
if (col === undefined) {
|
|
983
1015
|
throw new Error(`[Feature ${featureName}] ${where} ` +
|
|
984
|
-
`defaultSort.field "${sortField}" is not a listed column.`);
|
|
1016
|
+
`defaultSort.field "${sortField}" is not a listed column. Use a listed column as defaultSort.field.`);
|
|
985
1017
|
}
|
|
986
1018
|
if (normalizeListColumn(col).sortable !== true) {
|
|
987
1019
|
throw new Error(`[Feature ${featureName}] ${where} ` +
|
|
@@ -1031,7 +1063,16 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1031
1063
|
if ((screen.type === "entityList" || screen.type === "projectionList") &&
|
|
1032
1064
|
screen.emptyState?.action !== undefined) {
|
|
1033
1065
|
validateActionHasIcon(feature.name, screenId, screen.type, "emptyState action", screen.emptyState.action);
|
|
1034
|
-
|
|
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);
|
|
1035
1076
|
}
|
|
1036
1077
|
// Output of buildAppSchema, which overwrites it — an authored value would be a silent no-op.
|
|
1037
1078
|
if ((screen.type === "entityEdit" ||
|
|
@@ -1062,7 +1103,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1062
1103
|
// Column-Felder können nicht gegen eine Entity gecheckt werden; sie
|
|
1063
1104
|
// werden zur Render-Zeit gegen die Projection-Rows aufgelöst).
|
|
1064
1105
|
if (!screen.query || typeof screen.query !== "string") {
|
|
1065
|
-
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.`);
|
|
1066
1107
|
}
|
|
1067
1108
|
if (screen.columns.length === 0) {
|
|
1068
1109
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has an empty columns list — ` +
|
|
@@ -1084,7 +1125,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1084
1125
|
screen.filter.op === "in" &&
|
|
1085
1126
|
!Array.isArray(screen.filter.value)) {
|
|
1086
1127
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) filter.op "in" requires ` +
|
|
1087
|
-
`filter.value to be a readonly array.`);
|
|
1128
|
+
`filter.value to be a readonly array. Pass an array as filter.value.`);
|
|
1088
1129
|
}
|
|
1089
1130
|
if (screen.facets !== undefined) {
|
|
1090
1131
|
validateListFacets(`[Feature ${feature.name}] Screen "${screenId}" (projectionList)`, screen.facets, screen.columns, feature.name, featureMap);
|
|
@@ -1122,7 +1163,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1122
1163
|
// Columns. Kein Entity-Check möglich — Felder werden render-seitig
|
|
1123
1164
|
// gegen die Query-Response aufgelöst, nicht gegen eine Entity.
|
|
1124
1165
|
if (!screen.query || typeof screen.query !== "string") {
|
|
1125
|
-
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.`);
|
|
1126
1167
|
}
|
|
1127
1168
|
if (screen.singleton === true && screen.idParam !== undefined) {
|
|
1128
1169
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
|
|
@@ -1175,7 +1216,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1175
1216
|
if (navigate.tab !== undefined &&
|
|
1176
1217
|
!screen.layout.sections.some((section) => section.id === navigate.tab)) {
|
|
1177
1218
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) metric "${field}" ` +
|
|
1178
|
-
`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.`);
|
|
1179
1220
|
}
|
|
1180
1221
|
}
|
|
1181
1222
|
else {
|
|
@@ -1204,14 +1245,14 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1204
1245
|
if (Array.isArray(screen.header?.subtitle)) {
|
|
1205
1246
|
if (screen.header.subtitleHref !== undefined) {
|
|
1206
1247
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets header.subtitleHref ` +
|
|
1207
|
-
`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.`);
|
|
1208
1249
|
}
|
|
1209
1250
|
const seenPartFields = new Set();
|
|
1210
1251
|
for (const part of screen.header.subtitle) {
|
|
1211
1252
|
const partField = typeof part === "string" ? part : part.field;
|
|
1212
1253
|
if (seenPartFields.has(partField)) {
|
|
1213
1254
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) header.subtitle ` +
|
|
1214
|
-
`lists field "${partField}" twice.`);
|
|
1255
|
+
`lists field "${partField}" twice. List each field once.`);
|
|
1215
1256
|
}
|
|
1216
1257
|
seenPartFields.add(partField);
|
|
1217
1258
|
const navigate = typeof part === "string" ? undefined : part.navigate;
|
|
@@ -1243,7 +1284,16 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1243
1284
|
}
|
|
1244
1285
|
if (section.kind === "relatedList" && section.emptyState?.action !== undefined) {
|
|
1245
1286
|
validateActionHasIcon(feature.name, screenId, "projectionDetail", `section "${sectionLabel}" emptyState action`, section.emptyState.action);
|
|
1246
|
-
|
|
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);
|
|
1247
1297
|
}
|
|
1248
1298
|
if (isExtensionEditSection(section)) {
|
|
1249
1299
|
// projectionDetail is read-only (no composed form submit) — an
|
|
@@ -1317,7 +1367,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1317
1367
|
if (section.fieldDefs[fieldName] === undefined) {
|
|
1318
1368
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
1319
1369
|
`(writeForm) field "${fieldName}" has no entry in fieldDefs — every rendered field needs ` +
|
|
1320
|
-
`a type declared there.`);
|
|
1370
|
+
`a type declared there. Add the field to fieldDefs.`);
|
|
1321
1371
|
}
|
|
1322
1372
|
}
|
|
1323
1373
|
if (screen.layout.mode === "wizard") {
|
|
@@ -1410,11 +1460,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1410
1460
|
if (section.kind === "relatedList") {
|
|
1411
1461
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) relatedList section ` +
|
|
1412
1462
|
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
1413
|
-
`primitive (fw#2166).`);
|
|
1463
|
+
`primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
|
|
1414
1464
|
}
|
|
1415
1465
|
if (isWriteFormEditSection(section)) {
|
|
1416
1466
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) writeForm section ` +
|
|
1417
|
-
`"${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.`);
|
|
1418
1468
|
}
|
|
1419
1469
|
validateFieldsXorGroups(`[Feature ${feature.name}] Screen "${screenId}" (configEdit)`, section);
|
|
1420
1470
|
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
@@ -1431,15 +1481,15 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1431
1481
|
for (const fname of fieldNames) {
|
|
1432
1482
|
const qualified = screen.configKeys[fname];
|
|
1433
1483
|
if (qualified === undefined) {
|
|
1434
|
-
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}"
|
|
1435
|
-
`
|
|
1436
|
-
`
|
|
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.`);
|
|
1437
1487
|
}
|
|
1438
1488
|
const configKeyDef = configKeyDefsByQn.get(qualified);
|
|
1439
1489
|
if (configKeyDef === undefined) {
|
|
1440
1490
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" → ` +
|
|
1441
|
-
`Config
|
|
1442
|
-
`
|
|
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)"}`);
|
|
1443
1493
|
}
|
|
1444
1494
|
if (isWriteOnlyField(screen.fields[fname]) && !isEncryptedAtRest(configKeyDef)) {
|
|
1445
1495
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" declares writeOnly ` +
|
|
@@ -1463,7 +1513,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1463
1513
|
for (const revealField of screen.reveal.fields) {
|
|
1464
1514
|
if (typeof revealField.field !== "string" || revealField.field.trim() === "") {
|
|
1465
1515
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) has a reveal.fields entry ` +
|
|
1466
|
-
`with an empty or non-string "field".`);
|
|
1516
|
+
`with an empty or non-string "field". Set field to a non-empty string.`);
|
|
1467
1517
|
}
|
|
1468
1518
|
if (revealFieldNames.has(revealField.field)) {
|
|
1469
1519
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) reveal.fields has a ` +
|
|
@@ -1472,7 +1522,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1472
1522
|
revealFieldNames.add(revealField.field);
|
|
1473
1523
|
if (typeof revealField.label !== "string" || revealField.label.trim() === "") {
|
|
1474
1524
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) reveal.fields entry ` +
|
|
1475
|
-
`"${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.`);
|
|
1476
1526
|
}
|
|
1477
1527
|
}
|
|
1478
1528
|
validateSecretMintConfirm(feature, screenId, screen, allWriteHandlerQns);
|
|
@@ -1607,7 +1657,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1607
1657
|
}
|
|
1608
1658
|
if (screen.filter.op === "in" && !Array.isArray(screen.filter.value)) {
|
|
1609
1659
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter.op "in" requires ` +
|
|
1610
|
-
`filter.value to be a readonly array.`);
|
|
1660
|
+
`filter.value to be a readonly array. Pass an array as filter.value.`);
|
|
1611
1661
|
}
|
|
1612
1662
|
}
|
|
1613
1663
|
// Tier 2.7e-1: rowActions pinnen — navigate-target existiert (selbes
|
|
@@ -1678,7 +1728,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1678
1728
|
const candidateQn = qualifyEntityName(feature.name, "screen", action.screen);
|
|
1679
1729
|
if (!allScreenQns.has(candidateQn) && !navTargetShortIds.has(action.screen)) {
|
|
1680
1730
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) toolbarAction "${action.id}" ` +
|
|
1681
|
-
`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.`);
|
|
1682
1732
|
}
|
|
1683
1733
|
if (action.tab !== undefined) {
|
|
1684
1734
|
resolveRowActionNavigateTarget(feature.name, screenId, "entityList", "toolbarAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
|
|
@@ -1746,11 +1796,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1746
1796
|
if (section.kind === "relatedList") {
|
|
1747
1797
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) relatedList section ` +
|
|
1748
1798
|
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
1749
|
-
`primitive (fw#2166).`);
|
|
1799
|
+
`primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
|
|
1750
1800
|
}
|
|
1751
1801
|
if (isWriteFormEditSection(section)) {
|
|
1752
1802
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) writeForm section ` +
|
|
1753
|
-
`"${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.`);
|
|
1754
1804
|
}
|
|
1755
1805
|
validateFieldsXorGroups(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit)`, section);
|
|
1756
1806
|
for (const fieldSpec of sectionFieldSpecs(section)) {
|
|
@@ -1818,7 +1868,7 @@ function validateDashboardScreen(featureName, screenId, screen, featureMap) {
|
|
|
1818
1868
|
const panelIds = new Set();
|
|
1819
1869
|
const addPanelId = (id, context) => {
|
|
1820
1870
|
if (panelIds.has(id)) {
|
|
1821
|
-
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.`);
|
|
1822
1872
|
}
|
|
1823
1873
|
panelIds.add(id);
|
|
1824
1874
|
};
|
|
@@ -1847,9 +1897,9 @@ function validateDashboardScreen(featureName, screenId, screen, featureMap) {
|
|
|
1847
1897
|
function validateDashboardTimeRange(featureName, screenId, timeRange, filterId) {
|
|
1848
1898
|
const prefix = `[Feature ${featureName}] Screen "${screenId}" (dashboard) timeRange`;
|
|
1849
1899
|
if (timeRange.id.length === 0)
|
|
1850
|
-
throw new Error(`${prefix} has an empty id.`);
|
|
1900
|
+
throw new Error(`${prefix} has an empty id. Set a non-empty id.`);
|
|
1851
1901
|
if (timeRange.id === filterId) {
|
|
1852
|
-
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.`);
|
|
1853
1903
|
}
|
|
1854
1904
|
if (timeRange.options.length === 0) {
|
|
1855
1905
|
throw new Error(`${prefix}.options is empty — declare at least one option.`);
|
|
@@ -1857,15 +1907,15 @@ function validateDashboardTimeRange(featureName, screenId, timeRange, filterId)
|
|
|
1857
1907
|
const values = new Set();
|
|
1858
1908
|
for (const option of timeRange.options) {
|
|
1859
1909
|
if (option.value.length === 0 || option.label.length === 0) {
|
|
1860
|
-
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.`);
|
|
1861
1911
|
}
|
|
1862
1912
|
if (values.has(option.value)) {
|
|
1863
|
-
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.`);
|
|
1864
1914
|
}
|
|
1865
1915
|
values.add(option.value);
|
|
1866
1916
|
}
|
|
1867
1917
|
if (!values.has(timeRange.default)) {
|
|
1868
|
-
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.`);
|
|
1869
1919
|
}
|
|
1870
1920
|
}
|
|
1871
1921
|
const EMBEDDABLE_SCREEN_TYPES = new Set([
|
|
@@ -1901,7 +1951,7 @@ export function validateDashboardScreenPanel(featureName, screenId, panel, featu
|
|
|
1901
1951
|
}
|
|
1902
1952
|
function validateDashboardPanelVisibleWhen(context, visibleWhen) {
|
|
1903
1953
|
if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
|
|
1904
|
-
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.`);
|
|
1905
1955
|
}
|
|
1906
1956
|
}
|
|
1907
1957
|
function validateDashboardPanelSpan(featureName, screenId, panel) {
|
|
@@ -1912,10 +1962,10 @@ function validateDashboardPanelSpan(featureName, screenId, panel) {
|
|
|
1912
1962
|
function validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelId) {
|
|
1913
1963
|
const context = `[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}"`;
|
|
1914
1964
|
if (panel.stats.length === 0) {
|
|
1915
|
-
throw new Error(`${context} has an empty stats list.`);
|
|
1965
|
+
throw new Error(`${context} has an empty stats list. Add at least one stat.`);
|
|
1916
1966
|
}
|
|
1917
1967
|
if (panel.subtitle !== undefined && panel.label === undefined) {
|
|
1918
|
-
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.`);
|
|
1919
1969
|
}
|
|
1920
1970
|
validateDashboardPanelSpan(featureName, screenId, panel);
|
|
1921
1971
|
validateDashboardPanelVisibleWhen(context, panel.visibleWhen);
|
|
@@ -1925,22 +1975,27 @@ function validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelI
|
|
|
1925
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.`);
|
|
1926
1976
|
}
|
|
1927
1977
|
if (!stat.query || typeof stat.query !== "string") {
|
|
1928
|
-
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.`);
|
|
1929
1979
|
}
|
|
1930
1980
|
if (stat.valueField.length === 0) {
|
|
1931
|
-
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.`);
|
|
1932
1982
|
}
|
|
1933
1983
|
validateDashboardValueFormat(featureName, screenId, stat.id, stat.valueFormat);
|
|
1934
1984
|
}
|
|
1935
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
|
+
}
|
|
1936
1991
|
function validateDashboardChartDisplay(featureName, screenId, panel) {
|
|
1937
1992
|
validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
|
|
1938
1993
|
const where = `[Feature ${featureName}] Screen "${screenId}" (dashboard) chart-panel "${panel.id}"`;
|
|
1939
1994
|
if (panel.scrollable !== undefined && typeof panel.scrollable !== "boolean") {
|
|
1940
|
-
throw new Error(`${where} has a non-boolean scrollable.`);
|
|
1995
|
+
throw new Error(`${where} has a non-boolean scrollable. Use true or false.`);
|
|
1941
1996
|
}
|
|
1942
1997
|
if (panel.scrollable === true && panel.chart !== "stacked-area") {
|
|
1943
|
-
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".`);
|
|
1944
1999
|
}
|
|
1945
2000
|
validateStackedAreaOnlyProps(where, panel);
|
|
1946
2001
|
validateStackedAreaEnumProp(where, "dateFormat", panel.dateFormat, ["day", "month"]);
|
|
@@ -1951,8 +2006,9 @@ function validateDashboardChartDisplay(featureName, screenId, panel) {
|
|
|
1951
2006
|
validateStackedAreaEnumProp(where, "markerLegend", panel.markerLegend, ["list", "legend"]);
|
|
1952
2007
|
const nonBooleanProp = ["brush", "legendTotals", "markerLabels"].find((prop) => panel[prop] !== undefined && typeof panel[prop] !== "boolean");
|
|
1953
2008
|
if (nonBooleanProp !== undefined) {
|
|
1954
|
-
throw new Error(`${where} has a non-boolean ${nonBooleanProp}.`);
|
|
2009
|
+
throw new Error(`${where} has a non-boolean ${nonBooleanProp}. Use true or false.`);
|
|
1955
2010
|
}
|
|
2011
|
+
validateFromTodayWindowReach(where, panel);
|
|
1956
2012
|
for (const [key, color] of Object.entries(panel.seriesColors ?? {})) {
|
|
1957
2013
|
if (typeof color !== "string" || color.trim() === "") {
|
|
1958
2014
|
throw new Error(`${where} seriesColors["${key}"] must be a non-empty CSS color.`);
|
|
@@ -1987,7 +2043,7 @@ function validateStackedAreaOnlyProps(where, panel) {
|
|
|
1987
2043
|
"markerLabels",
|
|
1988
2044
|
].filter((prop) => panel[prop] !== undefined);
|
|
1989
2045
|
if (stackedAreaOnly.length > 0) {
|
|
1990
|
-
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".`);
|
|
1991
2047
|
}
|
|
1992
2048
|
}
|
|
1993
2049
|
const DASHBOARD_CHART_TONES = new Set([
|
|
@@ -1998,7 +2054,7 @@ const DASHBOARD_CHART_TONES = new Set([
|
|
|
1998
2054
|
]);
|
|
1999
2055
|
function validateDashboardChartMarkerKind(where, look) {
|
|
2000
2056
|
if (look.tone === undefined && look.color === undefined) {
|
|
2001
|
-
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.`);
|
|
2002
2058
|
}
|
|
2003
2059
|
if (look.tone !== undefined && !DASHBOARD_CHART_TONES.has(look.tone)) {
|
|
2004
2060
|
throw new Error(`${where}.tone "${String(look.tone)}" is not one of ${[...DASHBOARD_CHART_TONES].join(", ")}.`);
|
|
@@ -2017,10 +2073,10 @@ function validateDashboardChartRanges(where, ranges) {
|
|
|
2017
2073
|
const values = new Set();
|
|
2018
2074
|
for (const option of ranges.options) {
|
|
2019
2075
|
if (option.value.length === 0 || option.label.length === 0) {
|
|
2020
|
-
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.`);
|
|
2021
2077
|
}
|
|
2022
2078
|
if (values.has(option.value)) {
|
|
2023
|
-
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.`);
|
|
2024
2080
|
}
|
|
2025
2081
|
if (option.months !== undefined && (!Number.isInteger(option.months) || option.months <= 0)) {
|
|
2026
2082
|
throw new Error(`${where} option "${option.value}" months "${String(option.months)}" must be a positive integer.`);
|
|
@@ -2028,7 +2084,7 @@ function validateDashboardChartRanges(where, ranges) {
|
|
|
2028
2084
|
values.add(option.value);
|
|
2029
2085
|
}
|
|
2030
2086
|
if (!values.has(ranges.default)) {
|
|
2031
|
-
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.`);
|
|
2032
2088
|
}
|
|
2033
2089
|
}
|
|
2034
2090
|
const CURRENCY_CODE_RE = /^[A-Z]{3}$/;
|
|
@@ -2038,6 +2094,9 @@ function validateDashboardValueFormat(featureName, screenId, panelId, valueForma
|
|
|
2038
2094
|
if (valueFormat === undefined)
|
|
2039
2095
|
return;
|
|
2040
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
|
+
}
|
|
2041
2100
|
if (!CURRENCY_CODE_RE.test(valueFormat.currency)) {
|
|
2042
2101
|
throw new Error(`${where}.currency "${String(valueFormat.currency)}" must be a 3-letter uppercase ISO currency code.`);
|
|
2043
2102
|
}
|
|
@@ -2055,12 +2114,12 @@ export function validateDashboardCustomPanel(featureName, screenId, panel) {
|
|
|
2055
2114
|
}
|
|
2056
2115
|
function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
|
|
2057
2116
|
if (!panel.query || typeof panel.query !== "string") {
|
|
2058
|
-
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.`);
|
|
2059
2118
|
}
|
|
2060
2119
|
validateDashboardPanelSpan(featureName, screenId, panel);
|
|
2061
2120
|
if (panel.kind === "stat") {
|
|
2062
2121
|
if (panel.valueField.length === 0) {
|
|
2063
|
-
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.`);
|
|
2064
2123
|
}
|
|
2065
2124
|
validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
|
|
2066
2125
|
}
|
|
@@ -2069,7 +2128,7 @@ function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
|
|
|
2069
2128
|
validateDashboardChartDisplay(featureName, screenId, panel);
|
|
2070
2129
|
if (panel.kind === "list") {
|
|
2071
2130
|
if (panel.columns.length === 0) {
|
|
2072
|
-
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.`);
|
|
2073
2132
|
}
|
|
2074
2133
|
for (const col of panel.columns) {
|
|
2075
2134
|
const normalized = normalizeListColumn(col);
|
|
@@ -2082,10 +2141,10 @@ function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
|
|
|
2082
2141
|
}
|
|
2083
2142
|
function validateDashboardFilterDefinition(featureName, screenId, filter) {
|
|
2084
2143
|
if (filter.id.length === 0) {
|
|
2085
|
-
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.`);
|
|
2086
2145
|
}
|
|
2087
2146
|
if (filter.label.length === 0) {
|
|
2088
|
-
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.`);
|
|
2089
2148
|
}
|
|
2090
2149
|
const hasOptions = filter.options !== undefined;
|
|
2091
2150
|
const hasOptionsQuery = filter.optionsQuery !== undefined;
|
|
@@ -2098,7 +2157,7 @@ function validateDashboardFilterDefinition(featureName, screenId, filter) {
|
|
|
2098
2157
|
`declare at least one option or use optionsQuery instead.`);
|
|
2099
2158
|
}
|
|
2100
2159
|
if (hasOptionsQuery && filter.optionsQuery?.length === 0) {
|
|
2101
|
-
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.`);
|
|
2102
2161
|
}
|
|
2103
2162
|
}
|
|
2104
2163
|
// Form-check für ListColumn-Renderer in der PlatformComponent-Form
|
|
@@ -2119,7 +2178,7 @@ function rejectInertColumnSortable(featureName, screenId, screenType, column) {
|
|
|
2119
2178
|
? "set sortable: true on the entity field instead"
|
|
2120
2179
|
: "sorting is derived from the query's Zod schema; only sortable: false is honored";
|
|
2121
2180
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) column "${column.field}" sets sortable — ` +
|
|
2122
|
-
`it is ignored here; ${source}.`);
|
|
2181
|
+
`it is ignored here; ${source}. Remove sortable from this column.`);
|
|
2123
2182
|
}
|
|
2124
2183
|
export function validateColumnRendererForm(featureName, screenId, column) {
|
|
2125
2184
|
const renderer = column.renderer;
|
|
@@ -2190,7 +2249,7 @@ function validateListFacets(prefix, facets, columns, currentFeatureName, feature
|
|
|
2190
2249
|
const seenFacetFields = new Set();
|
|
2191
2250
|
for (const facet of facets) {
|
|
2192
2251
|
if (seenFacetFields.has(facet.field)) {
|
|
2193
|
-
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.`);
|
|
2194
2253
|
}
|
|
2195
2254
|
seenFacetFields.add(facet.field);
|
|
2196
2255
|
if (facet.type !== "reference" && !columnFieldNames.has(facet.field)) {
|