@cosmicdrift/kumiko-framework 0.373.0 → 0.374.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,7 @@ import { ZodObject } from "zod";
27
27
  import { collectUrlPrefillFieldsByScreenQn } from "./boot-validator/url-prefill-fields.js";
28
28
  import { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, } from "./build-config-feature-schema.js";
29
29
  import { isEncryptedAtRest } from "./config-helpers.js";
30
+ import { isListQueryWithoutSearchableFields } from "./entity-list-searchability.js";
30
31
  import { QnTypes, qualifyEntityName } from "./qualified-name.js";
31
32
  export function buildAppSchema(registry, options = {}) {
32
33
  const features = [];
@@ -341,11 +342,14 @@ function projectConfigEditScreen(screen, encryptedConfigKeyQns) {
341
342
  return fields === undefined ? screen : { ...screen, fields };
342
343
  }
343
344
  function projectProjectionListScreen(screen, registry) {
344
- const schema = registry.getQueryHandler(screen.query)?.schema;
345
- const capabilities = deriveProjectionListCapabilities(schema);
345
+ const handler = registry.getQueryHandler(screen.query);
346
+ const capabilities = deriveProjectionListCapabilities(handler?.schema);
347
+ // A search box over an entity without searchable fields would never match.
348
+ const searchable = capabilities.searchable &&
349
+ !isListQueryWithoutSearchableFields(screen.query, handler, (name) => registry.getEntity(name));
346
350
  return {
347
351
  ...screen,
348
- searchable: screen.searchable ?? capabilities.searchable,
352
+ searchable: screen.searchable ?? searchable,
349
353
  sortable: capabilities.sortable,
350
354
  paginated: capabilities.paginated,
351
355
  };
@@ -0,0 +1,5 @@
1
+ import type { EntityDefinition, QueryHandlerDef } from "./types/index.ts";
2
+ export declare function resolveEntityBehindListQuery(queryQn: string, handler: QueryHandlerDef | undefined, lookupEntity: (entityName: string) => EntityDefinition | undefined): EntityDefinition | undefined;
3
+ /** True when the list query is an entity-convention one over an entity with no
4
+ * searchable field — a search box would never match anything. */
5
+ export declare function isListQueryWithoutSearchableFields(queryQn: string, handler: QueryHandlerDef | undefined, lookupEntity: (entityName: string) => EntityDefinition | undefined): boolean;
@@ -0,0 +1,35 @@
1
+ import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
2
+ import { parseQn } from "./qualified-name.js";
3
+ // Mirrors buildSearchableSortableCaches (registry-validate.ts): a `search`
4
+ // payload only matches text fields, embedded sub-fields and references that
5
+ // opted in with `searchable: true`.
6
+ function entityHasSearchableField(entity) {
7
+ return Object.values(entity.fields).some((field) => {
8
+ if (field.type === "embedded") {
9
+ return Object.values(field.schema).some((subField) => subField.searchable === true);
10
+ }
11
+ return (field.type === "text" || field.type === "reference") && field.searchable === true;
12
+ });
13
+ }
14
+ // Entity-convention list queries (ENTITY_CONVENTION_QUERY_BRAND) carry no
15
+ // entity on the handler; the entity name is recovered from the query QN's
16
+ // `<entityName>:list` naming convention. Hand-written handlers resolve to
17
+ // undefined — "capability unknown, no throw".
18
+ export function resolveEntityBehindListQuery(queryQn, handler, lookupEntity) {
19
+ if (handler === undefined || handler[ENTITY_CONVENTION_QUERY_BRAND] !== true)
20
+ return undefined;
21
+ let entityName;
22
+ try {
23
+ entityName = parseQn(queryQn).name.split(":")[0] ?? "";
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ return entityName === "" ? undefined : lookupEntity(entityName);
29
+ }
30
+ /** True when the list query is an entity-convention one over an entity with no
31
+ * searchable field — a search box would never match anything. */
32
+ export function isListQueryWithoutSearchableFields(queryQn, handler, lookupEntity) {
33
+ const entity = resolveEntityBehindListQuery(queryQn, handler, lookupEntity);
34
+ return entity !== undefined && !entityHasSearchableField(entity);
35
+ }
@@ -31,9 +31,12 @@ function qualifySelectorPanel(featureName, panel) {
31
31
  throw new Error(`[Feature ${featureName}] extensionSelector screen panel "${panel.id}" must not set a label — ` +
32
32
  `the settings dashboard has no i18n namespace for it; the embedded screen carries its own title.`);
33
33
  }
34
- if (panel.screen === "" || isValidQn(panel.screen))
34
+ const qualify = (ref) => ref === "" || isValidQn(ref) ? ref : qualifyEntityName(featureName, "screen", ref);
35
+ const screen = qualify(panel.screen);
36
+ const fallback = panel.fallback === undefined ? undefined : qualify(panel.fallback);
37
+ if (screen === panel.screen && fallback === panel.fallback)
35
38
  return panel;
36
- return { ...panel, screen: qualifyEntityName(featureName, "screen", panel.screen) };
39
+ return { ...panel, screen, ...(fallback !== undefined && { fallback }) };
37
40
  }
38
41
  throw new Error(`[Feature ${featureName}] extensionSelector panel ${JSON.stringify(panel)} has an unsupported kind — ` +
39
42
  `only "custom" and "screen" panels are allowed.`);
@@ -51,6 +51,7 @@ export { i18nKey } from "./i18n-key.ts";
51
51
  export { buildSessionRoles, findForbiddenMembershipRole, isForbiddenMembershipRole, stripForbiddenMembershipRoles, } from "./membership-roles.ts";
52
52
  export type { OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule } from "./ownership.ts";
53
53
  export { buildOwnershipClause, combineClauses, from, normalizeAccessEntry, userCanCreateFieldRow, userCanReadFieldRow, userCanWriteFieldRow, } from "./ownership.ts";
54
+ export { type PagedListRequest, type PagedListRowValue, pagedListSchema, pageRows, } from "./paged-list.ts";
54
55
  export type { ParsedRefTarget } from "./parse-ref-target.ts";
55
56
  export { parseRefTarget, parseRefTargetEntityName } from "./parse-ref-target.ts";
56
57
  export { buildPipelineSteps, stepsPipeline } from "./pipeline.ts";
@@ -83,7 +84,7 @@ export { assertNotificationVariables, templateVariable, templateVariablesToZod,
83
84
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, requireTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.ts";
84
85
  export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, type TransferAdjacency, type TransferEdge, type TransferEdgeLink, } from "./transfer-adjacency.ts";
85
86
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.ts";
86
- export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, BootOptionName, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityObjectDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookDefinition, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MaxPerTenantOptions, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StackBootRequirement, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.ts";
87
+ export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, BootOptionName, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityObjectDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookDefinition, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MaxPerTenantOptions, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StackBootRequirement, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.ts";
87
88
  export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.ts";
88
89
  export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.ts";
89
90
  export { runValidation } from "./validation.ts";
@@ -51,6 +51,7 @@ export { i18nKey } from "./i18n-key.js";
51
51
  // surface.
52
52
  export { buildSessionRoles, findForbiddenMembershipRole, isForbiddenMembershipRole, stripForbiddenMembershipRoles, } from "./membership-roles.js";
53
53
  export { buildOwnershipClause, combineClauses, from, normalizeAccessEntry, userCanCreateFieldRow, userCanReadFieldRow, userCanWriteFieldRow, } from "./ownership.js";
54
+ export { pagedListSchema, pageRows, } from "./paged-list.js";
54
55
  export { parseRefTarget, parseRefTargetEntityName } from "./parse-ref-target.js";
55
56
  export { buildPipelineSteps, stepsPipeline } from "./pipeline.js";
56
57
  export { projectAppSchemaForRoles } from "./project-app-schema-for-roles.js";
@@ -0,0 +1,29 @@
1
+ import type { CursorResult } from "@cosmicdrift/kumiko-types/cursor-types";
2
+ import * as z from "zod";
3
+ export type PagedListRowValue = string | number | boolean | null;
4
+ export declare function pagedListSchema<const TSortField extends string>(sortableFields: readonly [TSortField, ...TSortField[]]): z.ZodObject<{
5
+ limit: z.ZodDefault<z.ZodNumber>;
6
+ offset: z.ZodOptional<z.ZodNumber>;
7
+ search: z.ZodOptional<z.ZodString>;
8
+ sort: z.ZodOptional<z.ZodEnum<{ [k in TSortField]: k; } extends infer T ? { [k in keyof T]: T[k]; } : never>>;
9
+ sortDirection: z.ZodOptional<z.ZodEnum<{
10
+ asc: "asc";
11
+ desc: "desc";
12
+ }>>;
13
+ totalCount: z.ZodOptional<z.ZodBoolean>;
14
+ cursor: z.ZodOptional<z.ZodNever>;
15
+ }, z.core.$strip>;
16
+ export type PagedListRequest<TSortField extends string> = {
17
+ readonly limit: number;
18
+ readonly offset?: number | undefined;
19
+ readonly search?: string | undefined;
20
+ readonly sort?: TSortField | undefined;
21
+ readonly sortDirection?: "asc" | "desc" | undefined;
22
+ readonly totalCount?: boolean | undefined;
23
+ };
24
+ export declare function pageRows<TRow extends Readonly<Record<TSortField, PagedListRowValue>> & {
25
+ readonly id: string;
26
+ }, TSortField extends string>(rows: readonly TRow[], request: PagedListRequest<TSortField>, options: {
27
+ readonly searchFields: readonly TSortField[];
28
+ readonly defaultSort: TSortField;
29
+ }): CursorResult<TRow>;
@@ -0,0 +1,56 @@
1
+ // Search, sort and offset paging for projectionList queries whose rows are
2
+ // assembled in memory (merged from several tables, no single SQL query to
3
+ // page). The projectionList pager sends `offset` + `totalCount`, never a
4
+ // cursor, so `nextCursor` is always null here.
5
+ import * as z from "zod";
6
+ import { MAX_LIST_LIMIT } from "./entity-handlers.js";
7
+ export function pagedListSchema(sortableFields) {
8
+ return z.object({
9
+ limit: z.number().int().min(1).max(MAX_LIST_LIMIT).default(50),
10
+ offset: z.number().int().nonnegative().optional(),
11
+ search: z.string().optional(),
12
+ sort: z.enum(sortableFields).optional(),
13
+ sortDirection: z.enum(["asc", "desc"]).optional(),
14
+ totalCount: z.boolean().optional(),
15
+ // Declared so a caller still paging by cursor gets a 400 instead of page one.
16
+ cursor: z.never({ error: "cursor paging is not supported here; page with offset" }).optional(),
17
+ });
18
+ }
19
+ function isMissing(value) {
20
+ return value === null || value === undefined;
21
+ }
22
+ // Missing values sort last in both directions; numbers and booleans compare
23
+ // by value, everything else as a locale-aware string.
24
+ function compareRowValues(a, b, directionMultiplier) {
25
+ if (isMissing(a) && isMissing(b))
26
+ return 0;
27
+ if (isMissing(a))
28
+ return 1;
29
+ if (isMissing(b))
30
+ return -1;
31
+ if (typeof a === "number" && typeof b === "number")
32
+ return (a - b) * directionMultiplier;
33
+ if (typeof a === "boolean" && typeof b === "boolean") {
34
+ return (Number(a) - Number(b)) * directionMultiplier;
35
+ }
36
+ return String(a).localeCompare(String(b)) * directionMultiplier;
37
+ }
38
+ export function pageRows(rows, request, options) {
39
+ const needle = request.search?.trim().toLowerCase() ?? "";
40
+ const matching = needle === ""
41
+ ? rows
42
+ : rows.filter((row) => options.searchFields.some((field) => String(row[field] ?? "")
43
+ .toLowerCase()
44
+ .includes(needle)));
45
+ const sortField = request.sort ?? options.defaultSort;
46
+ const directionMultiplier = request.sortDirection === "desc" ? -1 : 1;
47
+ // The source rows come unordered, so ties fall back to the id; otherwise a
48
+ // tied row could show up on two pages or on none.
49
+ const sorted = [...matching].sort((a, b) => compareRowValues(a[sortField], b[sortField], directionMultiplier) || a.id.localeCompare(b.id));
50
+ const offset = request.offset ?? 0;
51
+ return {
52
+ rows: sorted.slice(offset, offset + request.limit),
53
+ nextCursor: null,
54
+ ...(request.totalCount === true && { total: matching.length }),
55
+ };
56
+ }
@@ -39,8 +39,8 @@
39
39
  // resolves app-wide via a global short-id map, mirroring
40
40
  // resolveRowActionNavigateTarget; kind:"drawer" resolves same-feature
41
41
  // only, mirroring validateDrawerTargetAction)
42
- // - projectionDetail header actions + metrics[].navigate
43
- // - entityEdit/actionForm/secretMint redirect + actionForm cancelTarget
42
+ // - projectionDetail and dashboard header actions + projectionDetail metrics[].navigate
43
+ // - entityEdit/actionForm/secretMint redirect + actionForm successLinks and cancelTarget
44
44
  // (resolveScreenTargetQn: same-feature short id or cross-feature QN)
45
45
  // - dashboard DashboardScreenPanel.screen (resolveScreenTargetQn)
46
46
  // - every EditSectionSpec kind's own `actions` (fields/extension/writeForm/
@@ -418,8 +418,27 @@ function projectHeader(header, indices, keptScreenQns) {
418
418
  return changed ? { ...header, subtitle: nextParts } : header;
419
419
  }
420
420
  function projectDashboardPanels(panels, feature, keptScreenQns) {
421
- const kept = panels.filter((p) => p.kind !== "screen" || isScreenTargetKept(feature.featureName, p.screen, keptScreenQns));
422
- return kept.length === panels.length ? panels : kept;
421
+ let changed = false;
422
+ const kept = [];
423
+ for (const panel of panels) {
424
+ if (panel.kind !== "screen") {
425
+ kept.push(panel);
426
+ }
427
+ else if (!isScreenTargetKept(feature.featureName, panel.screen, keptScreenQns)) {
428
+ changed = true;
429
+ }
430
+ else if (panel.fallback !== undefined &&
431
+ !isScreenTargetKept(feature.featureName, panel.fallback, keptScreenQns)) {
432
+ // The role cannot reach the stand-in, so the tile just hides.
433
+ const { fallback: _unreachable, ...withoutFallback } = panel;
434
+ kept.push(withoutFallback);
435
+ changed = true;
436
+ }
437
+ else {
438
+ kept.push(panel);
439
+ }
440
+ }
441
+ return changed ? kept : panels;
423
442
  }
424
443
  // --- per-screen-type projection ---
425
444
  function projectListScreenId(listScreenId, indices, keptScreenQns) {
@@ -512,12 +531,15 @@ function projectEntityEditScreen(screen, feature, indices, keptScreenQns) {
512
531
  };
513
532
  }
514
533
  function projectActionFormScreen(screen, feature, indices, keptScreenQns) {
515
- const { redirect, cancelTarget, layout, listScreenId, ...rest } = screen;
534
+ const { redirect, successLinks, cancelTarget, layout, listScreenId, ...rest } = screen;
516
535
  const nextRedirect = projectRedirect(feature.featureName, redirect, keptScreenQns);
536
+ const keptSuccessLinks = successLinks?.filter((link) => isRedirectTargetKept(feature.featureName, link, keptScreenQns));
537
+ const nextSuccessLinks = keptSuccessLinks?.length === 0 ? undefined : keptSuccessLinks;
517
538
  const nextCancelTarget = projectCancelTarget(feature.featureName, cancelTarget, keptScreenQns);
518
539
  const nextLayout = projectEditLayout(layout, feature, indices, keptScreenQns);
519
540
  const nextListScreenId = projectListScreenId(listScreenId, indices, keptScreenQns);
520
541
  const unchanged = nextRedirect === redirect &&
542
+ nextSuccessLinks?.length === successLinks?.length &&
521
543
  nextCancelTarget === cancelTarget &&
522
544
  nextLayout === layout &&
523
545
  nextListScreenId === listScreenId;
@@ -527,6 +549,7 @@ function projectActionFormScreen(screen, feature, indices, keptScreenQns) {
527
549
  ...rest,
528
550
  layout: nextLayout,
529
551
  ...(nextRedirect !== undefined && { redirect: nextRedirect }),
552
+ ...(nextSuccessLinks !== undefined && { successLinks: nextSuccessLinks }),
530
553
  ...(nextCancelTarget !== undefined && { cancelTarget: nextCancelTarget }),
531
554
  ...(nextListScreenId !== undefined && { listScreenId: nextListScreenId }),
532
555
  };
@@ -565,8 +588,16 @@ function projectScreen(screen, feature, indices, keptScreenQns) {
565
588
  case "projectionDetail":
566
589
  return projectProjectionDetailScreen(screen, feature, indices, keptScreenQns);
567
590
  case "dashboard": {
591
+ const { actions, ...rest } = screen;
568
592
  const nextPanels = projectDashboardPanels(screen.panels, feature, keptScreenQns);
569
- return nextPanels === screen.panels ? screen : { ...screen, panels: nextPanels };
593
+ const nextActions = projectRowActionArray(actions, feature, indices, keptScreenQns);
594
+ if (nextPanels === screen.panels && nextActions === actions)
595
+ return screen;
596
+ return {
597
+ ...rest,
598
+ panels: nextPanels,
599
+ ...(nextActions !== undefined && { actions: nextActions }),
600
+ };
570
601
  }
571
602
  case "entityEdit":
572
603
  return projectEntityEditScreen(screen, feature, indices, keptScreenQns);
@@ -17,7 +17,7 @@ export type { ContentCollectionDefinition, NavDefinition, NavIconKey, } from "@c
17
17
  export type { FromRule, FromRuleKind, OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule, SqlFragment, WhereRule, WhereRuleContext, } from "@cosmicdrift/kumiko-types/ownership";
18
18
  export type { EntityProjectionExtension, MspErrorMode, MspErrorPolicy, MultiStreamApplyFn, MultiStreamProjectionDefinition, ProjectionDefinition, ProjectionRowIdOf, ProjectionTable, SingleStreamApplyFn, } from "@cosmicdrift/kumiko-types/projection";
19
19
  export type { BelongsToRelation, EntityRelations, HasManyRelation, ManyToManyRelation, RelationDefinition, } from "@cosmicdrift/kumiko-types/relations";
20
- export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelGate, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListEmptyStateSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
20
+ export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelGate, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardValueFormat, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListEmptyStateSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
21
21
  export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
22
22
  export type { TargetRef } from "@cosmicdrift/kumiko-types/target-ref";
23
23
  export type { Subscribe, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, } from "@cosmicdrift/kumiko-types/tree-node";
@@ -45,6 +45,19 @@ function pushKey(out, value, treatAsKey = false) {
45
45
  function editFieldName(f) {
46
46
  return typeof f === "string" ? f : f.field;
47
47
  }
48
+ // Placeholder, affix texts and the fill button label of a field spec; a `{ field }`
49
+ // affix reads another form field and is no key.
50
+ function pushEditFieldTextKeys(out, spec, treatAsKey = false) {
51
+ // skip: shorthand field name carries no texts
52
+ if (typeof spec === "string")
53
+ return;
54
+ pushKey(out, spec.placeholder, treatAsKey);
55
+ for (const affix of [spec.prefix, spec.suffix]) {
56
+ if (typeof affix === "string")
57
+ pushKey(out, affix, treatAsKey);
58
+ }
59
+ pushKey(out, spec.fillAction?.label, treatAsKey);
60
+ }
48
61
  // Group titles are translated exactly like the section title (computeEditViewModel).
49
62
  function pushSectionTitles(out, section, treatAsKey = false) {
50
63
  pushKey(out, section.title, treatAsKey);
@@ -67,15 +80,20 @@ function pushActionFormShapedKeys(out, featureName, step) {
67
80
  pushSectionTitles(out, section);
68
81
  for (const f of sectionFieldSpecs(section)) {
69
82
  out.add(fieldLabelKey(featureName, ACTION_FORM_ENTITY, editFieldName(f)));
83
+ pushEditFieldTextKeys(out, f);
70
84
  }
71
85
  }
72
86
  }
73
87
  function pushRowActionKeys(out, action) {
74
88
  pushKey(out, action.label);
75
- if (action.kind === "writeHandler" || action.kind === undefined) {
89
+ if (action.kind === "writeHandler" ||
90
+ action.kind === "clientAction" ||
91
+ action.kind === undefined) {
76
92
  pushKey(out, action.confirm);
77
93
  pushKey(out, action.confirmLabel);
78
94
  }
95
+ if (action.kind === "clientAction")
96
+ pushKey(out, action.successMessage);
79
97
  if (action.kind === "drawer") {
80
98
  pushKey(out, action.title);
81
99
  pushKey(out, action.subtitle);
@@ -90,6 +108,8 @@ function pushDashboardScreenKeys(out, dashboard) {
90
108
  pushKey(out, option.label);
91
109
  pushKey(out, dashboard.scope?.badge);
92
110
  pushKey(out, dashboard.scope?.notice);
111
+ for (const action of dashboard.actions ?? [])
112
+ pushRowActionKeys(out, action);
93
113
  }
94
114
  function pushDashboardPanelKeys(out, panel) {
95
115
  // skip: custom-Panel übersetzt sich selbst, kein Key hier
@@ -98,8 +118,12 @@ function pushDashboardPanelKeys(out, panel) {
98
118
  pushKey(out, panel.label);
99
119
  if (panel.kind === "chart" || panel.kind === "stat-group")
100
120
  pushKey(out, panel.subtitle);
101
- if (panel.kind === "chart")
121
+ if (panel.kind === "text")
122
+ pushDashboardTextBlockKeys(out, panel);
123
+ if (panel.kind === "chart") {
124
+ pushKey(out, panel.unit);
102
125
  pushDashboardChartRangeKeys(out, panel);
126
+ }
103
127
  if (panel.kind === "chart" ||
104
128
  panel.kind === "list" ||
105
129
  panel.kind === "feed" ||
@@ -107,6 +131,9 @@ function pushDashboardPanelKeys(out, panel) {
107
131
  pushKey(out, panel.emptyLabel);
108
132
  pushKey(out, panel.emptyHint);
109
133
  }
134
+ pushDashboardChildLabelKeys(out, panel);
135
+ }
136
+ function pushDashboardChildLabelKeys(out, panel) {
110
137
  if (panel.kind === "stat-group") {
111
138
  for (const stat of panel.stats)
112
139
  pushKey(out, stat.label);
@@ -119,6 +146,20 @@ function pushDashboardPanelKeys(out, panel) {
119
146
  }
120
147
  }
121
148
  }
149
+ function pushDashboardTextBlockKeys(out, panel) {
150
+ for (const block of panel.blocks) {
151
+ // skip: code is shown verbatim, not translated
152
+ if (block.kind === "code")
153
+ continue;
154
+ if (block.kind === "list") {
155
+ for (const item of block.items)
156
+ pushKey(out, item);
157
+ }
158
+ else {
159
+ pushKey(out, block.text);
160
+ }
161
+ }
162
+ }
122
163
  function pushDashboardChartRangeKeys(out, panel) {
123
164
  for (const option of panel.ranges?.options ?? [])
124
165
  pushKey(out, option.label);
@@ -243,6 +284,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
243
284
  continue; // rejected at boot, unreachable here
244
285
  pushSectionTitles(out, section);
245
286
  for (const f of sectionFieldSpecs(section)) {
287
+ pushEditFieldTextKeys(out, f);
246
288
  const fieldName = editFieldName(f);
247
289
  const override = edit.fieldLabels?.[fieldName];
248
290
  if (override !== undefined)
@@ -260,6 +302,8 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
260
302
  pushKey(out, form.summary?.subtitle);
261
303
  for (const action of form.footerActions ?? [])
262
304
  pushKey(out, action.label);
305
+ for (const link of form.successLinks ?? [])
306
+ pushKey(out, link.label);
263
307
  for (const fieldName of Object.keys(form.fields)) {
264
308
  const override = form.fieldLabels?.[fieldName];
265
309
  if (override !== undefined)
@@ -276,6 +320,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
276
320
  continue; // rejected at boot, unreachable here
277
321
  pushSectionTitles(out, section);
278
322
  for (const f of sectionFieldSpecs(section)) {
323
+ pushEditFieldTextKeys(out, f);
279
324
  const fieldName = editFieldName(f);
280
325
  const override = form.fieldLabels?.[fieldName];
281
326
  if (override !== undefined)
@@ -328,6 +373,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
328
373
  pushSectionTitles(out, section, treatDotFormAsKey);
329
374
  pushKey(out, section.description, treatDotFormAsKey);
330
375
  for (const f of sectionFieldSpecs(section)) {
376
+ pushEditFieldTextKeys(out, f, treatDotFormAsKey);
331
377
  const fieldName = editFieldName(f);
332
378
  const override = config.fieldLabels?.[fieldName];
333
379
  if (override !== undefined)
@@ -366,6 +412,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
366
412
  pushKey(out, section.submitLabel);
367
413
  // kumiko-lint-ignore section-fields-raw writeForm sections carry no groups (EditWriteFormSection)
368
414
  for (const f of section.fields) {
415
+ pushEditFieldTextKeys(out, f);
369
416
  const fieldName = editFieldName(f);
370
417
  out.add(fieldLabelKey(featureName, WRITE_FORM_SECTION_ENTITY, fieldName));
371
418
  }
@@ -373,6 +420,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
373
420
  }
374
421
  pushSectionTitles(out, section);
375
422
  for (const f of sectionFieldSpecs(section)) {
423
+ pushEditFieldTextKeys(out, f);
376
424
  const fieldName = editFieldName(f);
377
425
  const override = detail.fieldLabels?.[fieldName];
378
426
  if (override !== undefined)
@@ -9,7 +9,7 @@ export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefin
9
9
  export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.ts";
10
10
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.ts";
11
11
  export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.ts";
12
- export type { ActionFormFooterAction, ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardChartTone, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelGate, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, EntityWriteExcludedFields, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListEmptyStateSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.ts";
12
+ export type { ActionFormFooterAction, ActionFormLoad, ActionFormRedirect, ActionFormScreenDefinition, ActionFormSuccessLink, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardChartTone, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelGate, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, EntityWriteExcludedFields, FieldAffix, FieldCondition, FieldFillAction, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListEmptyStateSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionClientAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.ts";
13
13
  export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.ts";
14
14
  export type { TargetRef } from "../engine/types/target-ref.ts";
15
15
  export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.373.0",
3
+ "version": "0.374.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -199,8 +199,8 @@
199
199
  "./package.json": "./package.json"
200
200
  },
201
201
  "dependencies": {
202
- "@cosmicdrift/kumiko-http": "0.373.0",
203
- "@cosmicdrift/kumiko-types": "0.373.0",
202
+ "@cosmicdrift/kumiko-http": "0.374.0",
203
+ "@cosmicdrift/kumiko-types": "0.374.0",
204
204
  "bullmq": "^5.76.7",
205
205
  "bun-types": "^1.3.13",
206
206
  "hono": "^4.13.1",
@@ -216,8 +216,8 @@
216
216
  "zod": "^4.4.3"
217
217
  },
218
218
  "devDependencies": {
219
- "@cosmicdrift/kumiko-dispatcher-live": "0.373.0",
220
- "@cosmicdrift/kumiko-testing": "0.373.0",
219
+ "@cosmicdrift/kumiko-dispatcher-live": "0.374.0",
220
+ "@cosmicdrift/kumiko-testing": "0.374.0",
221
221
  "bun-types": "^1.3.13",
222
222
  "pino-pretty": "^13.1.3"
223
223
  },
package/src/changes.json CHANGED
@@ -1,4 +1,65 @@
1
1
  [
2
+ {
3
+ "version": "0.374.0",
4
+ "type": "improvement",
5
+ "title": "actionForm successLinks and resetOnSuccess",
6
+ "detail": "successLinks keeps the actionForm open after a successful submit and lists links resolved like redirect; boot rejects it together with redirect, with an unknown target or with a record target without idFrom. resetOnSuccess clears typed values after success. Role projection drops links to screens the role cannot reach."
7
+ },
8
+ {
9
+ "version": "0.374.0",
10
+ "type": "improvement",
11
+ "title": "clientAction row actions",
12
+ "detail": "RowAction gains kind \"clientAction\" with a client-registered handler name; boot rejects a name that is not <feature>:<kebab>, and the surface keys include its confirm, confirmLabel and successMessage."
13
+ },
14
+ {
15
+ "version": "0.374.0",
16
+ "type": "improvement",
17
+ "title": "Boot validation for screen-panel fallback and text panels",
18
+ "detail": "DashboardScreenPanel.fallback needs visibleWhen and a registered, embeddable target without its own visibleWhen. DashboardTextPanel (kind \"text\", exported with DashboardTextBlock) rejects empty blocks and empty lists; its text keys are required translation keys. Extension-selector panels qualify a short fallback ref, and role projection drops a fallback the role cannot reach."
19
+ },
20
+ {
21
+ "version": "0.374.0",
22
+ "type": "improvement",
23
+ "title": "Dashboard record binding, header actions, refresh and relative time",
24
+ "detail": "Dashboards take record, actions and refresh; stat panels and text params gain a relativeTime format; the surface keys include kumiko.dashboard.record.missing."
25
+ },
26
+ {
27
+ "version": "0.374.0",
28
+ "type": "breaking",
29
+ "title": "DashboardChartPanel.valueFormat only takes the currency format",
30
+ "detail": "DashboardValueFormat is now a union of DashboardCurrencyFormat and DashboardRelativeTimeFormat. Stat panels take both, chart panels only the currency one, and boot rejects relativeTime on a chart.",
31
+ "migration": "A chart panel that declares valueFormat keeps working with { kind: \"currency\", currency }. Code that types a chart's valueFormat as DashboardValueFormat should use DashboardCurrencyFormat instead; relativeTime on a chart was never meaningful and now fails boot."
32
+ },
33
+ {
34
+ "version": "0.374.0",
35
+ "type": "improvement",
36
+ "title": "Derived list searchability, actionForm load and drawer rowClick",
37
+ "detail": "projectionList searchable is derived false for entity-convention lists without searchable fields (searchable true is rejected at boot), actionForm.load registers a query checked at boot, and a drawer row action may set rowClick, counted with navigate rowClick and rejected on projectionDetail header actions."
38
+ },
39
+ {
40
+ "version": "0.374.0",
41
+ "type": "improvement",
42
+ "title": "Boot checks for field affixes and fillAction",
43
+ "detail": "Boot fails when a fillAction query is not a registered query handler, or when a { field } affix or a fillAction params source is not a field of the same form."
44
+ },
45
+ {
46
+ "version": "0.374.0",
47
+ "type": "improvement",
48
+ "title": "pagedListSchema and pageRows for in-memory list queries",
49
+ "detail": "pagedListSchema(sortableFields) and pageRows(rows, request, { searchFields, defaultSort }) cover search, sort (missing values last, ties by id), offset paging and an optional total for projectionList queries over merged rows. PagedListRequest and PagedListRowValue are exported; nextCursor is always null."
50
+ },
51
+ {
52
+ "version": "0.374.0",
53
+ "type": "improvement",
54
+ "title": "DashboardProgressTone type and unlimited label key",
55
+ "detail": "New exported type DashboardProgressTone for progress-list row tones. The default translations gain kumiko.dashboard.progress.unlimited."
56
+ },
57
+ {
58
+ "version": "0.374.0",
59
+ "type": "improvement",
60
+ "title": "Dashboard chart panels take a y-axis unit",
61
+ "detail": "DashboardChartPanel.unit (i18n key) is shown once on the y axis of a timeseries chart, with or without valueFormat. The boot validator rejects unit on other chart kinds; an untranslated unit key fails the required-surface key check."
62
+ },
2
63
  {
3
64
  "version": "0.373.0",
4
65
  "type": "improvement",