@mintplayer/ng-spark 22.27.0 → 22.28.1
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/fesm2022/mintplayer-ng-spark-grid.mjs +5 -9
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +5 -2
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-create.mjs +13 -5
- package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +2 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +38 -13
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +28 -9
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +2 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +0 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +3 -2
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mintplayer-ng-spark-grid.d.ts +3 -14
- package/types/mintplayer-ng-spark-models.d.ts +13 -13
- package/types/mintplayer-ng-spark-moderation.d.ts +8 -4
- package/types/mintplayer-ng-spark-pipes.d.ts +3 -0
- package/types/mintplayer-ng-spark-po-create.d.ts +4 -0
- package/types/mintplayer-ng-spark-po-detail.d.ts +4 -0
- package/types/mintplayer-ng-spark-po-edit.d.ts +16 -0
- package/types/mintplayer-ng-spark-po-form.d.ts +1 -0
- package/types/mintplayer-ng-spark-services.d.ts +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-models.mjs","names":[],"sources":["../../models/src/translated-string.ts","../../models/src/showed-on.ts","../../models/src/entity-type.ts","../../models/src/datetime-local.ts","../../models/src/as-detail-conversions.ts","../../models/src/query-result.ts","../../models/src/lookup-reference.ts","../../models/src/comparable-value.ts","../../models/src/query-actions.ts","../../models/src/selection-rule.ts","../../models/src/selection-mode.ts","../../models/src/sub-query.ts","../../models/src/refresh-overlay.ts","../../models/src/rule-evaluation.ts"],"sourcesContent":["import { signal, type WritableSignal } from '@angular/core';\n\nexport type TranslatedString = Record<string, string>;\n\n/** Global reactive language state — shared across library boundaries via globalThis */\nexport const currentLanguage: WritableSignal<string> =\n ((globalThis as any).__sparkCurrentLanguage ??= signal('en'));\n\nexport function resolveTranslation(ts: TranslatedString | undefined, lang?: string): string {\n if (!ts) return '';\n const language = lang ?? currentLanguage();\n return ts[language] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n}\n","/**\n * Flags enum controlling on which pages an attribute should be displayed.\n * Values can be combined: ShowedOn.Query | ShowedOn.PersistentObject\n */\nexport enum ShowedOn {\n Query = 1,\n PersistentObject = 2,\n}\n\n/**\n * Helper function to check if a ShowedOn value includes a specific flag.\n */\nexport function hasShowedOnFlag(value: ShowedOn | string | undefined, flag: ShowedOn): boolean {\n if (value === undefined) return true; // Default: show on all pages\n\n // Handle string values from JSON (e.g., \"Query, PersistentObject\")\n if (typeof value === 'string') {\n const parts = value.split(',').map(s => s.trim());\n const flagName = ShowedOn[flag];\n return parts.includes(flagName);\n }\n\n // Handle numeric flag values\n return (value & flag) === flag;\n}\n","import { ShowedOn } from './showed-on';\nimport { TranslatedString } from './translated-string';\nimport { ValidationRule } from './validation-rule';\nimport { SparkSubQueryEntry } from './sub-query';\n\n/**\n * Controls how a Reference attribute is picked in the PO-edit form.\n * Serialized as a string by the server (mirrors the .NET EReferenceDisplayType).\n */\nexport enum EReferenceDisplayType {\n /** Renders as a `<bs-select>` listing every referenced item. */\n Dropdown = 'Dropdown',\n /** Renders a readonly textbox + \"…\" button that opens a searchable modal grid picker. */\n Modal = 'Modal',\n}\n\n/**\n * When changing an attribute asks the server to reshape the object (mirrors the .NET\n * `ERefreshTrigger`, serialized PascalCase like {@link EReferenceDisplayType}).\n *\n * - `None` — no refresh; the same as absent.\n * - `Auto` — free text refreshes on blur; discrete editors (lookup, reference, boolean, date,\n * datetime, enum, color) refresh immediately.\n * - `ValueChanged` — refreshes on every change; free text is debounced (300 ms) and flushed by\n * blur or save.\n * - `Blur` — refreshes on blur. A discrete editor never blurs meaningfully, so there it acts as\n * `ValueChanged` (and `--spark-verify-model` warns).\n */\nexport type RefreshTrigger = 'None' | 'Auto' | 'ValueChanged' | 'Blur';\n\nexport interface EntityAttributeDefinition {\n id: string;\n name: string;\n label?: TranslatedString;\n /** Help text rendered as an [i] tooltip beside the label. Absent when the model declares none. */\n description?: TranslatedString;\n dataType: string;\n isRequired: boolean;\n isVisible: boolean;\n isReadOnly: boolean;\n order: number;\n query?: string;\n /** For reference attributes, specifies the target entity type's CLR type name */\n referenceType?: string;\n /** For AsDetail attributes, specifies the nested entity type's CLR type name */\n asDetailType?: string;\n /** When true, the attribute represents an array/collection of AsDetail objects */\n isArray?: boolean;\n /** For array AsDetail attributes: \"modal\" (default) or \"inline\" */\n editMode?: 'inline' | 'modal';\n /**\n * For Reference attributes: 'Modal' renders the \"…\" + modal query-grid picker;\n * 'Dropdown'/absent (default) renders a `<bs-select>`. Hand-set in the model JSON.\n */\n referenceDisplayType?: EReferenceDisplayType;\n /** For array AsDetail attributes: when true, rows can be drag-reordered (order = array position) */\n isSortable?: boolean;\n /**\n * When set (and not `'None'`), changing this attribute's value posts the in-progress object to\n * `/spark/po/{objectTypeId}/refresh` and applies the reshaped result as an overlay; the value\n * decides when (see {@link RefreshTrigger}). Absent means no refresh.\n * Schema-only by design — it never travels on a PersistentObjectAttribute, so a client\n * cannot claim a trigger the model did not declare.\n */\n triggersRefresh?: RefreshTrigger;\n /** For LookupReference attributes, specifies the lookup reference type name */\n lookupReferenceType?: string;\n /**\n * Controls on which pages the attribute should be displayed.\n * Query = shown in list views, PersistentObject = shown in detail/edit views.\n * Can be a numeric flag value or a string like \"Query, PersistentObject\".\n */\n showedOn?: ShowedOn | string;\n rules: ValidationRule[];\n /** References an AttributeGroup.id to assign this attribute to a group */\n group?: string;\n /** Number of grid columns this attribute spans within a tab's column layout */\n columnSpan?: number;\n /** Renderer component name for custom display in detail/list views */\n renderer?: string;\n /** Options passed to the renderer component */\n rendererOptions?: Record<string, any>;\n}\n\nexport interface AttributeTab {\n id: string;\n name: string;\n label?: TranslatedString;\n order: number;\n /** Number of columns for the grid layout within this tab */\n columnCount?: number;\n}\n\nexport interface AttributeGroup {\n id: string;\n name: string;\n label?: TranslatedString;\n /** References an AttributeTab.id to assign this group to a tab */\n tab?: string;\n order: number;\n}\n\nexport interface EntityType {\n id: string;\n name: string;\n /** The type's display name, resolved by the server (#467, D6; formerly `description`). */\n label?: TranslatedString;\n /**\n * The backing CLR type, or absent for a JSON-only virtual type (#325).\n *\n * Optional on purpose: the server sends null for a virtual type, and this was declared\n * non-nullable, so any unguarded `.endsWith(...)` on it threw a TypeError naming an innocent\n * query. Types are data from the wire, not a promise about it.\n */\n clrType?: string;\n\n /**\n * Whether this caller may OPEN one of these, as opposed to list them.\n *\n * The catalogue is gated on Query, so its presence here means \"you may list this type\" and says\n * nothing about opening one. A reference must not render as a link when this is false: it used\n * to, and the click refused on arrival.\n */\n canRead?: boolean;\n alias?: string;\n /**\n * Breadcrumb template: literal text plus `{AttributeName}` placeholders. A scalar placeholder\n * renders its value; a reference placeholder renders the referenced entity's breadcrumb.\n * The server resolves this — clients only read the resulting strings. Example: \"{Street}, {City}\".\n */\n breadcrumb?: string;\n /**\n * When false, the breadcrumb needs the collection document (a placeholder field is not on the\n * projection). null/absent means renderable from the projection. Informational on the client.\n */\n breadcrumbProjectionSatisfiable?: boolean;\n tabs?: AttributeTab[];\n groups?: AttributeGroup[];\n attributes: EntityAttributeDefinition[];\n /**\n * The sub-queries displayed as related query tables on the detail page: a bare alias or id, or\n * `{ query, selectionMode?, parentReference? }` (#460, D17). Read them with `subQueriesOf(type)`.\n */\n queries?: SparkSubQueryEntry[];\n /**\n * The definitions of this type's AsDetail row types, sent alongside it so a detail table can draw\n * its columns.\n *\n * Needed because a row type usually has no rights of its own — a row is edited through its parent\n * — and the entity-type catalogue is gated on `Query`. Resolving a row type from the catalogue\n * therefore failed silently, and the table rendered with **no columns at all**.\n *\n * Per-caller and gated on the *parent's* right, which is the same gate that already ships each\n * row's attribute schema inside `attr.objects`. Pruned server-side: no `queryType`, `indexName`,\n * `queries` or `alias`.\n */\n detailTypes?: EntityType[];\n /**\n * Whether adding or removing a row of this type asks the server first.\n *\n * Absent or false — the default — keeps the purely client-side behaviour: New pushes a blank row,\n * Delete splices it out, and the parent's save is the first the server hears of either. On, the\n * grid calls `newObject` before showing a row and `deleteRow` before removing one, so\n * `OnNewAsync` can default it and the delete hook can refuse.\n *\n * ⚠️ Read it off the **row type**, never the parent: the type that owns the hooks owns the\n * decision. For a row type resolved out of `detailTypes` the flag survives the server-side\n * pruning, so both sources agree.\n *\n * ⚠️ It governs the round trip and nothing else. It is not a permission, and it is outside the\n * model hash, so nothing that gates a write may be conditional on it — the save path enforces the\n * row type's New/Edit/Delete rights on every embedded collection whether this is on or off.\n */\n serverSideRowLifecycle?: boolean;\n}\n","/**\n * Conversions between the wire format for a timestamp and what an `<input type=\"datetime-local\">`\n * or `<input type=\"date\">` will accept.\n *\n * A `DateTimeOffset` in Spark means an **instant**. The wire carries a full ISO-8601 string with an\n * offset (`2026-12-31T23:59:00-08:00`); an HTML date/time control speaks only bare local wall clock\n * (`2026-12-31T23:59`). Neither end can consume the other's format, so both directions need\n * converting, and the browser is deliberately the side that does it: its timezone rules are kept\n * fresh by the operating system, while a container's are frozen at image build time.\n *\n * Assigning a wire value straight to a `datetime-local` input does not throw and does not warn -- the\n * control simply renders **blank**, and saving the untouched form then writes that blank back.\n */\n\nfunction pad(n: number): string {\n return String(n).padStart(2, '0');\n}\n\n/** Formats a UTC offset in minutes as `+HH:mm` / `-HH:mm`. */\nfunction formatOffset(offsetMinutes: number): string {\n const sign = offsetMinutes >= 0 ? '+' : '-';\n const abs = Math.abs(offsetMinutes);\n return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;\n}\n\n/**\n * Parses a wire timestamp. Returns `null` for anything unusable, matching the contract of\n * `parsedDate` / `queryCellValue` -- a bad value renders as empty rather than as `Invalid Date`.\n */\nexport function parseWireDate(value: unknown): Date | null {\n if (value === null || value === undefined || value === '') return null;\n if (value instanceof Date) return isNaN(value.getTime()) ? null : value;\n if (typeof value !== 'string' && typeof value !== 'number') return null;\n\n const parsed = new Date(value);\n return isNaN(parsed.getTime()) ? null : parsed;\n}\n\n/**\n * Wire value -> the `yyyy-MM-ddTHH:mm` a `datetime-local` input needs, in the **viewer's** zone.\n *\n * The conversion is date-sensitive, not a fixed shift: a value is rendered using the zone's offset\n * *on that value's own date*, so a timestamp in July and one in January convert differently in a\n * zone that observes DST.\n */\nexport function toDateTimeLocalInput(value: unknown): string {\n const d = parseWireDate(value);\n if (!d) return '';\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;\n}\n\n/** Wire value -> the `yyyy-MM-dd` a `date` input needs, in the viewer's zone. */\nexport function toDateInput(value: unknown): string {\n const d = parseWireDate(value);\n if (!d) return '';\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n}\n\n/**\n * `yyyy-MM-ddTHH:mm` from the control -> a complete ISO-8601 string carrying the viewer's offset\n * **for the entered date**.\n *\n * Building the `Date` from its parts rather than parsing the string is what makes this correct: it\n * lets the browser apply its own timezone rules for that date, including DST. Using the *current*\n * offset instead would be wrong for any value outside the present season -- entering a January date\n * during summer would record an offset an hour off, and the stored instant would be an hour wrong.\n *\n * Across a DST discontinuity the browser resolves the wall clock itself, taking the offset in force\n * before a fall-back and after a spring-forward. That is specified ECMAScript behaviour, and the\n * server's conversion helper deliberately reproduces the same choice so the two cannot drift.\n */\nexport function fromDateTimeLocalInput(local: unknown): string | null {\n if (typeof local !== 'string' || local === '') return null;\n\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2}))?/.exec(local);\n if (!match) return null;\n\n const [, y, mo, d, h, mi, s] = match;\n const date = new Date(+y, +mo - 1, +d, +h, +mi, s ? +s : 0);\n if (isNaN(date.getTime())) return null;\n\n // getTimezoneOffset() is minutes to ADD to local to reach UTC, so it is the negation of the offset\n // an ISO string carries.\n return `${y}-${mo}-${d}T${h}:${mi}:${pad(s ? +s : 0)}${formatOffset(-date.getTimezoneOffset())}`;\n}\n\n/** `yyyy-MM-dd` from a `date` control -> an ISO string at local midnight with the viewer's offset. */\nexport function fromDateInput(local: unknown): string | null {\n if (typeof local !== 'string' || local === '') return null;\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(local);\n if (!match) return null;\n return fromDateTimeLocalInput(`${local}T00:00`);\n}\n\n/**\n * Compares two wire timestamps **by instant**, which is what a `DateTimeOffset` means in Spark.\n *\n * Round-tripping a value through a date control legitimately rewrites its text -- a document holding\n * `2026-12-31T23:59:00-08:00` comes back as `2027-01-01T08:59:00+01:00` from a viewer in Brussels --\n * so a string comparison would mark every untouched date as edited and send a needless write on every\n * save. Both of those name the same moment.\n *\n * Note this is the *opposite* of the trap on the server side, where `DateTimeOffset.Equals` compares\n * the instant and silently hides a destroyed offset. Here comparing by instant is the correct thing:\n * we are asking \"did the user change this value\", not \"is the offset intact\".\n */\nexport function wireDatesEqual(a: unknown, b: unknown): boolean {\n const da = parseWireDate(a);\n const db = parseWireDate(b);\n if (da === null || db === null) return da === db;\n return da.getTime() === db.getTime();\n}\n\n/** True for the attribute data types that are edited through a native date/time control. */\nexport function isDateDataType(dataType: string | null | undefined): dataType is 'date' | 'datetime' {\n return dataType === 'date' || dataType === 'datetime';\n}\n\n/** Wire -> control, picking the right shape for the data type. */\nexport function toDateInputValue(dataType: string | null | undefined, value: unknown): string {\n return dataType === 'date' ? toDateInput(value) : toDateTimeLocalInput(value);\n}\n\n/** Control -> wire, picking the right shape for the data type. */\nexport function fromDateInputValue(dataType: string | null | undefined, value: unknown): string | null {\n return dataType === 'date' ? fromDateInput(value) : fromDateTimeLocalInput(value);\n}\n","import { fromDateInputValue, isDateDataType, toDateInputValue, wireDatesEqual } from './datetime-local';\nimport { EntityAttributeDefinition } from './entity-type';\nimport { EntityType } from './entity-type';\nimport { PersistentObject } from './persistent-object';\nimport { PersistentObjectAttribute } from './persistent-object-attribute';\n\n/**\n * Resolves an `EntityType` by its CLR type name (e.g. `\"HR.Entities.Address\"`).\n * Callers typically close over a list they already hold. `getEntityTypes()` is NOT cached --\n * it issues a request per call -- so resolve against a list you fetched once rather than\n * calling it inside the resolver.\n */\nexport type EntityTypeResolver = (clrTypeName: string) => EntityType | undefined;\n\n/**\n * Flattens a nested `PersistentObject` into the plain `Record<string, any>` shape the\n * form state uses throughout ng-spark. Primitive / reference attributes contribute their\n * `value`; nested AsDetail attributes recurse — single becomes an inner dict, array\n * becomes an array of inner dicts. Returns `{}` for `null` / `undefined` input.\n *\n * This is the ONE place that reads the server's new AsDetail wire shape and collapses it\n * back to the flat dict the form components already handle.\n */\nexport function nestedPoToDict(po: PersistentObject | null | undefined): Record<string, any> {\n if (!po) return {};\n const dict: Record<string, any> = {};\n for (const attr of po.attributes ?? []) {\n dict[attr.name] = attributeValueForForm(attr);\n }\n // The object's own server-resolved breadcrumb, kept under a reserved key so the form can label\n // it without re-deriving a template it may be structurally unable to resolve. Safe to carry\n // through save: `dictToNestedPo` walks the entity type's attributes, never the dict's keys, so\n // a reserved key is never sent to the server. Attached only when it resolved, which keeps the\n // common case byte-for-byte identical to the plain flat dict.\n if (typeof po.breadcrumb === 'string' && po.breadcrumb !== '') {\n dict[AS_DETAIL_SELF_BREADCRUMB_KEY] = po.breadcrumb;\n }\n // The row's own key, same reserved-key mechanism and for a sharper reason: without it the key\n // never returns, and a save cannot tell an edited row from a deleted one plus a new one. See\n // AS_DETAIL_ROW_KEY.\n if (typeof po.id === 'string' && po.id !== '') {\n dict[AS_DETAIL_ROW_KEY] = po.id;\n }\n // The wire values of any date cells, kept so the save side can tell an untouched cell from an\n // edited one. Without it, flattening to a wall clock and converting back rewrites the stored\n // offset to the viewer's on every save, even for rows nobody touched.\n const originalDates = originalDatesOf(po);\n if (originalDates) dict[AS_DETAIL_ORIGINAL_DATES_KEY] = originalDates;\n if (po.metadata) dict[AS_DETAIL_METADATA_KEY] = po.metadata;\n return dict;\n}\n\nfunction originalDatesOf(po: PersistentObject): Record<string, any> | undefined {\n let originals: Record<string, any> | undefined;\n for (const attr of po.attributes ?? []) {\n if (isDateDataType(attr.dataType)) (originals ??= {})[attr.name] = attr.value;\n }\n return originals;\n}\n\nfunction attributeValueForForm(attr: PersistentObjectAttribute): any {\n if (attr.dataType === 'AsDetail') {\n if (attr.isArray) return (attr.objects ?? []).map(po => nestedPoToDict(po));\n return attr.object ? nestedPoToDict(attr.object) : null;\n }\n // An embedded row's date cells are edited through the same native controls as a root object's,\n // so they need the same wire -> wall-clock conversion. This is the FORM path only;\n // `nestedPoToDisplayRow` deliberately keeps the wire value, because the display path formats\n // through `parsedDate` rather than feeding an input.\n if (isDateDataType(attr.dataType)) return toDateInputValue(attr.dataType, attr.value);\n return attr.value;\n}\n\n/**\n * Reserved key under which a flattened nested object keeps the breadcrumb the SERVER resolved for\n * that object itself (as opposed to {@link AS_DETAIL_BREADCRUMBS_KEY}, which keys the breadcrumbs\n * of its reference attributes by attribute name).\n *\n * This exists because a breadcrumb template can name a property the model does not carry. HR's\n * `Address` declares `[Breadcrumb, IgnoreProperty] string Crumb`, and `Address.json` renders it as\n * `\"{Crumb}\"` — the server resolves that by reflecting over the CLR property, which no client can\n * do, because `[IgnoreProperty]` is exactly the instruction to keep it out of the model. Flattening\n * used to discard the resolved string, leaving the form to substitute `{Crumb}` against a dict that\n * can never contain it.\n */\nexport const AS_DETAIL_SELF_BREADCRUMB_KEY = '__sparkBreadcrumb';\n\n/**\n * Reserved key under which a flattened nested object keeps its own row key — the value of whatever\n * property the server registered as that type's `[ValueKey]`.\n *\n * A value object's key is not a model attribute (nothing declares `Id` in its model file), so it\n * travels as the nested `PersistentObject`'s `id` and has to be carried across the flat-dict form\n * state by hand. Before this existed, flattening dropped it and `dictToNestedPo` rebuilt `id` from\n * `dict['Id']`, which is never present — so **every** embedded row reached the server with an empty\n * id, and the fresh instance the mapper builds kept the guid its own field initializer had just\n * minted.\n *\n * That is not cosmetic. Stored keys and incoming keys were then both plausible and never equal, so\n * a save could not match rows: an edit was indistinguishable from a delete plus a create. Every\n * per-row rule — preserving read-only fields, and the `New`/`Edit`/`Delete` rights of the row type —\n * rests on this key surviving the round trip.\n *\n * Safe to carry, for the same reason the breadcrumb is: `dictToNestedPo` walks the entity type's\n * attributes and never the dict's keys, so a reserved key is never sent back as an attribute.\n */\nexport const AS_DETAIL_ROW_KEY = '__sparkRowKey';\n\n/**\n * The breadcrumb the server resolved for a flattened object, or null when it resolved to nothing.\n *\n * `EntityMapper` never emits an empty breadcrumb: when the template renders blank it substitutes\n * the CLR type name, so an unset `Address` arrives as the literal string `\"Address\"`\n * (EntityMapper.cs:209-211). That is a placeholder, not data, and rendering it would be worse than\n * rendering nothing — it reads as a real value. `typeName` lets a caller filter it back out.\n */\nexport function selfBreadcrumb(row: Record<string, any> | null | undefined, typeName?: string): string | null {\n return resolvedBreadcrumb(row?.[AS_DETAIL_SELF_BREADCRUMB_KEY], typeName);\n}\n\n/**\n * The same filter as {@link selfBreadcrumb}, for a caller holding the server's breadcrumb string\n * directly rather than a flattened row dict — `attr.object.breadcrumb` on a detail page, or a query\n * cell's `breadcrumb`.\n *\n * Split out so there is exactly one answer to \"is this the type-name placeholder?\". The first\n * implementation lived inside `selfBreadcrumb` and so covered only the two pipes that flatten a row\n * first; the two that read the string straight off the wire kept printing `BuildFeedback` and\n * `GateSettings` at users (#384). A second copy of the comparison is how that happens again.\n */\nexport function resolvedBreadcrumb(value: unknown, typeName?: string): string | null {\n if (typeof value !== 'string' || value.trim() === '') return null;\n\n // Callers hold the type name in two shapes — `EntityType.name` is the short model name, while an\n // attribute's `asDetailType` is the full CLR name — and the server's placeholder is always the\n // short one. Compare on the last dotted segment so either shape filters it.\n if (typeName) {\n const shortName = typeName.slice(typeName.lastIndexOf('.') + 1);\n if (value === shortName) return null;\n }\n return value;\n}\n\n/**\n * Reserved key under which {@link nestedPoToDisplayRow} stashes the server-resolved breadcrumb of\n * each reference attribute (keyed by attribute name). Lets an AsDetail reference cell render the\n * label the server already resolved by id — page-independent — instead of guessing from a single\n * reference-query options page. Prefixed to avoid colliding with a real attribute name.\n */\nexport const AS_DETAIL_BREADCRUMBS_KEY = '__sparkBreadcrumbs';\n\n/**\n * Reserved key under which a flattened row keeps the **wire** values of its date cells, keyed by\n * attribute name.\n *\n * A date cell is edited through a native control that speaks only a bare local wall clock, so\n * flattening converts the stored instant into the viewer's zone and saving converts it back. That\n * round trip preserves the instant but necessarily rewrites the **offset** to the viewer's. For a\n * cell the user actually edited that is correct — the offset is not business data. For one nobody\n * touched it is pure loss: merely opening a row and saving would relabel a Seattle timestamp as a\n * Brussels one.\n *\n * Keeping the original lets {@link dictToNestedPo} send back exactly what it was given whenever the\n * instant is unchanged. Safe to carry for the same reason as the other reserved keys: the rebuild\n * walks the entity type's attributes, never the dict's keys.\n */\nexport const AS_DETAIL_ORIGINAL_DATES_KEY = '__sparkOriginalDates';\n\n/**\n * Whether a flattened-row key is one this module reserved rather than a model attribute.\n *\n * `nestedPoToDict` and `nestedPoToDisplayRow` stash the row key and the resolved breadcrumbs\n * alongside the real values, which is safe on the way back — `dictToNestedPo` walks the entity\n * type's attributes and never the dict's keys. It is *not* safe for anything that enumerates the\n * dict to build display text: a caller joining `Object.values(dict)` prints the row's guid and its\n * own breadcrumb as though they were fields.\n */\nexport function isReservedAsDetailKey(key: string): boolean {\n return key === AS_DETAIL_ROW_KEY\n || key === AS_DETAIL_SELF_BREADCRUMB_KEY\n || key === AS_DETAIL_BREADCRUMBS_KEY\n || key === AS_DETAIL_ORIGINAL_DATES_KEY\n || key === AS_DETAIL_METADATA_KEY;\n}\n\n/**\n * Reserved key under which a flattened row keeps the {@link PersistentObject.metadata} the server\n * attached to it on load (an add-on's read-side facts — contributions' `{ contribution: { own } }`).\n * Never sent back: `dictToNestedPo` walks the entity type's attributes, never the dict's keys.\n */\nexport const AS_DETAIL_METADATA_KEY = '__sparkMetadata';\n\n/** The metadata the server attached to a flattened row under `key`, or undefined. */\nexport function rowMetadata<T = any>(row: Record<string, any> | null | undefined, key: string): T | undefined {\n const metadata = row?.[AS_DETAIL_METADATA_KEY];\n return metadata && typeof metadata === 'object' ? (metadata as Record<string, any>)[key] as T : undefined;\n}\n\n/**\n * Like {@link nestedPoToDict}, but for the read-only detail display path. In addition to each\n * attribute's value it preserves the server-resolved per-reference `breadcrumb` under\n * {@link AS_DETAIL_BREADCRUMBS_KEY}, so an AsDetail reference cell can render the label by id\n * regardless of whether the referenced document fits on the reference query's first options page.\n * The form/edit path keeps using {@link nestedPoToDict}, which never carries breadcrumbs.\n */\nexport function nestedPoToDisplayRow(po: PersistentObject | null | undefined): Record<string, any> {\n if (!po) return {};\n const dict: Record<string, any> = {};\n let breadcrumbs: Record<string, string> | undefined;\n for (const attr of po.attributes ?? []) {\n dict[attr.name] = displayValueForAttribute(attr);\n if (attr.dataType === 'Reference' && !attr.isArray && typeof attr.breadcrumb === 'string' && attr.breadcrumb !== '') {\n (breadcrumbs ??= {})[attr.name] = attr.breadcrumb;\n }\n }\n // Only attach the side channel when something resolved — keeps reference-free rows (the common\n // case) byte-for-byte identical to the plain flat dict.\n if (breadcrumbs) dict[AS_DETAIL_BREADCRUMBS_KEY] = breadcrumbs;\n // Same self-breadcrumb the form path carries: a nested-AsDetail COLUMN in a detail table has an\n // inner dict as its value, which used to stringify to \"[object Object]\".\n if (typeof po.breadcrumb === 'string' && po.breadcrumb !== '') {\n dict[AS_DETAIL_SELF_BREADCRUMB_KEY] = po.breadcrumb;\n }\n // And the row key, for the same reason as the form path — a display row can be handed back for\n // saving, and a row that loses its key on that trip is indistinguishable from a new one.\n if (typeof po.id === 'string' && po.id !== '') {\n dict[AS_DETAIL_ROW_KEY] = po.id;\n }\n if (po.metadata) dict[AS_DETAIL_METADATA_KEY] = po.metadata;\n return dict;\n}\n\nfunction displayValueForAttribute(attr: PersistentObjectAttribute): any {\n if (attr.dataType === 'AsDetail') {\n if (attr.isArray) return (attr.objects ?? []).map(po => nestedPoToDisplayRow(po));\n return attr.object ? nestedPoToDisplayRow(attr.object) : null;\n }\n return attr.value;\n}\n\n/**\n * Builds a nested `PersistentObject` from a flat dict against the schema in\n * <paramref name=\"entityType\"/>. Used when the form is about to save — AsDetail attributes\n * are no longer sent as flat dicts in `attribute.value`; the server now requires\n * `attribute.object` / `attribute.objects` with fully scaffolded nested POs.\n *\n * `resolve` walks through AsDetail types registered elsewhere (usually the full\n * `getEntityTypes()` list, keyed by CLR type name). Nested AsDetail inside AsDetail is\n * handled recursively.\n */\nexport function dictToNestedPo(\n dict: Record<string, any> | null | undefined,\n entityType: EntityType,\n resolve: EntityTypeResolver,\n): PersistentObject {\n const originalDates = (dict?.[AS_DETAIL_ORIGINAL_DATES_KEY] ?? {}) as Record<string, any>;\n const attributes: PersistentObjectAttribute[] = (entityType.attributes ?? [])\n .map(attrDef => buildAttribute(attrDef, dict?.[attrDef.name], resolve, originalDates[attrDef.name]));\n\n return {\n // The reserved key first: it is the only one that is always present for a stored row, because\n // a value object's key is not a model attribute. `Id`/`id` remain as fallbacks for a root\n // object, whose id genuinely is part of the dict.\n id: (dict?.[AS_DETAIL_ROW_KEY] as string) ?? (dict?.['Id'] as string) ?? (dict?.['id'] as string) ?? '',\n name: entityType.name,\n objectTypeId: entityType.id,\n attributes,\n };\n}\n\nfunction buildAttribute(\n attrDef: EntityAttributeDefinition,\n raw: any,\n resolve: EntityTypeResolver,\n originalDate?: any,\n): PersistentObjectAttribute {\n const attr: PersistentObjectAttribute = {\n id: attrDef.id,\n name: attrDef.name,\n label: attrDef.label,\n dataType: attrDef.dataType,\n isArray: attrDef.isArray,\n isRequired: attrDef.isRequired,\n isVisible: attrDef.isVisible,\n isReadOnly: attrDef.isReadOnly,\n order: attrDef.order,\n rules: attrDef.rules ?? [],\n isValueChanged: true,\n };\n\n if (attrDef.dataType === 'AsDetail') {\n // Server expects attr.value null for AsDetail; the nested PO carries the data.\n attr.value = null;\n attr.asDetailType = attrDef.asDetailType;\n\n const nestedType = attrDef.asDetailType ? resolve(attrDef.asDetailType) : undefined;\n if (!nestedType) {\n attr.object = null;\n attr.objects = attrDef.isArray ? [] : null;\n return attr;\n }\n\n if (attrDef.isArray) {\n const items: any[] = Array.isArray(raw) ? raw : [];\n attr.objects = items.map(item => dictToNestedPo((item as Record<string, any>) ?? {}, nestedType, resolve));\n } else {\n attr.object = raw ? dictToNestedPo(raw as Record<string, any>, nestedType, resolve) : null;\n }\n return attr;\n }\n\n if (isDateDataType(attrDef.dataType)) {\n // The mirror of the conversion in `attributeValueForForm`: back from the control's bare wall\n // clock to a complete ISO-8601 instant carrying the viewer's offset for that date. When the\n // instant is unchanged, send back exactly what was loaded instead, so an untouched row keeps\n // its stored offset. See AS_DETAIL_ORIGINAL_DATES_KEY.\n const converted = fromDateInputValue(attrDef.dataType, raw);\n attr.value = wireDatesEqual(converted, originalDate) ? originalDate : converted;\n return attr;\n }\n\n attr.value = raw;\n return attr;\n}\n","import { TranslatedString } from './translated-string';\nimport { PersistentObject } from './persistent-object';\nimport { AS_DETAIL_BREADCRUMBS_KEY } from './as-detail-conversions';\n\n/**\n * What a query returns: the column metadata once, then one lightweight row per result.\n *\n * Rows used to be full `PersistentObject`s, so every row repeated the complete attribute\n * metadata the client already held from `/spark/types` and never read off the row. More\n * importantly it conflated two things that are not the same: a **row is a projection**, a\n * **persistent object is a document**. Nothing here is entitled to act on a row — every\n * mutating path re-loads by id and re-applies security server-side.\n */\nexport interface QueryResult {\n columns: QueryColumn[];\n items: QueryResultItem[];\n /** Rows matching the query after filtering, before paging. */\n totalItems: number;\n skip: number;\n take: number;\n typeHints?: Record<string, string>;\n /**\n * Actions withheld for THIS query, by name (custom, and `New`). Absent or empty means every\n * action the caller has the right to is offered.\n *\n * The mirror of `PersistentObject.disabledActions`, set server-side by the entity's\n * `OnDisableActionsAsync` hook with a query target (#460, D13). The server asks the same hook when\n * an action is submitted from this query and refuses a disabled one with 403.\n */\n disabledActions?: string[];\n}\n\n/**\n * The subset of column metadata a cell needs in order to render.\n *\n * Both {@link QueryColumn} (a query result's column) and `EntityAttributeDefinition` (a detail\n * page's attribute) satisfy this, which is what lets one cell component serve the query grid and\n * the AsDetail sub-table without either side knowing about the other's shape.\n */\nexport interface SparkCellColumn {\n name: string;\n label?: TranslatedString;\n /** Help text rendered as an [i] tooltip in the column header. Absent when the model declares none. */\n description?: TranslatedString;\n dataType: string;\n isArray?: boolean;\n renderer?: string;\n rendererOptions?: Record<string, any>;\n referenceType?: string;\n lookupReferenceType?: string;\n asDetailType?: string;\n}\n\n/** One column of a query result, sent once rather than repeated on every row. */\nexport interface QueryColumn extends SparkCellColumn {\n order: number;\n /**\n * Whether the grid offers a sort affordance for this column. Resolved server-side from the query's\n * override then the attribute, so it arrives already decided.\n *\n * This replaced an `isSortable` that carried the server's AsDetail *drag-reorder* flag — a\n * different concept that was `false` for every scalar column while the grid hard-coded every\n * header sortable anyway. Neither side read it. The attribute-level `isSortable` on `EntityType`\n * is the drag-reorder flag and stays.\n *\n * Absent means sortable, matching `isVisible`: a server predating the field still draws arrows.\n */\n canSort?: boolean;\n /** Whether the grid draws a filter cell for this column. Absent means filterable. */\n canFilter?: boolean;\n /**\n * Whether this column's distinct values may be listed. When `false` and `canFilter` is true the\n * panel offers a free-text filter instead of a value list — listing a column's values is a much\n * stronger disclosure than filtering by one already known.\n */\n canListDistincts?: boolean;\n /**\n * Whether the grid draws this column. `false` means the row carries the value but no column is\n * rendered — for a renderer that needs a sibling value (a lock glyph beside a name) without\n * spending a column on it. `showedOn` decides what ships; this decides what is drawn.\n */\n isVisible?: boolean;\n /** The query backing a Reference column, by name — the client's option source. */\n query?: string;\n}\n\n/**\n * One row: an id, a display string, and a value per column.\n *\n * Deliberately too weak to act on — no attribute metadata, no `can` block, because neither can be\n * trusted from a projection. It does carry its document's `etag` (#467, D14), a version rather than\n * a judgement, so a delete from the list says which version it removes.\n */\nexport interface QueryResultItem {\n /** Never null and unique within a result; the server refuses anything else. */\n id: string;\n /** What to show when the row is named rather than tabulated (a reference picker's value). */\n breadcrumb?: string;\n /**\n * The document's change vector when the list loaded (#467, D14). A delete sends it back; a row\n * changed since is a 409. Absent for a row with no document behind it.\n */\n etag?: string;\n values: QueryResultItemValue[];\n /** Presentation hints for the whole row. Keys arrive lower-cased. */\n typeHints?: Record<string, string>;\n}\n\n/** One cell. */\nexport interface QueryResultItemValue {\n /** The {@link QueryColumn.name} this value belongs to. */\n key: string;\n /** Typed JSON, not a string — the server converts on the way out. */\n value?: any;\n /** For a reference cell: the target document's id, so a link needs no second lookup. */\n objectId?: string;\n /** For a reference cell: the target's display string. */\n breadcrumb?: string;\n /** Resolved display labels per referenced id, for a multi-reference cell. */\n breadcrumbs?: Record<string, string | null>;\n typeHints?: Record<string, string>;\n}\n\n/**\n * Every shape a renderer's `item` can arrive in.\n *\n * Three, and they are genuinely different objects rather than variations of one: a query grid hands\n * a {@link QueryResultItem}, an AsDetail sub-table hands the flat record `nestedPoToDisplayRow`\n * built, and a detail or form host hands the `PersistentObject` itself. A renderer reused across a\n * grid and an AsDetail table therefore sees two of them, which is why {@link valueFor} reads all\n * three rather than making every app write the branch.\n */\nexport type SparkRow = QueryResultItem | PersistentObject | Record<string, unknown>;\n\n/**\n * The value a row carries for a column, whatever shape the row is in — or `undefined` when it\n * carries none.\n *\n * ```ts\n * const isPrivate = valueFor(this.item(), 'IsPrivate')?.value === true;\n * ```\n *\n * Always returns the **cell** rather than the bare value, because a reference cell is worth as much\n * for its `objectId` as for its text, and a helper that discarded it would have to be replaced the\n * first time a renderer wanted to link somewhere. `?.value` is the cost, once per call site.\n *\n * ⚠️ A value only reaches a grid row if its attribute is on the **query surface**. To read a sibling\n * the grid does not draw, mark it `\"showedOn\": \"Query\", \"isVisible\": false` — shipped, not drawn. An\n * attribute marked `\"showedOn\": \"PersistentObject\"` is absent from a row by design, and this returns\n * `undefined` for it; that is the first thing to check when a sibling read comes back empty.\n */\nexport function valueFor(item: SparkRow | null | undefined, columnName: string): QueryResultItemValue | undefined {\n if (!item) return undefined;\n\n if (isQueryRow(item)) {\n return item.values.find(v => v.key === columnName);\n }\n\n if (isPersistentObject(item)) {\n const attribute = item.attributes.find(a => a.name === columnName);\n if (!attribute) return undefined;\n return {\n key: attribute.name,\n value: attribute.value,\n // A single reference's value IS the target's id, which is what the row shape carries\n // separately. Deriving it here keeps a renderer from having to know that.\n objectId: attribute.dataType === 'Reference' && !attribute.isArray && attribute.value != null\n ? String(attribute.value)\n : undefined,\n breadcrumb: attribute.breadcrumb,\n breadcrumbs: attribute.breadcrumbs,\n };\n }\n\n const record = item as Record<string, unknown>;\n if (!(columnName in record)) return undefined;\n\n const value = record[columnName];\n // The flat row's side channel is populated only for single references, so its presence for this\n // key is what identifies one — the record itself carries no dataType to ask.\n const breadcrumb = (record[AS_DETAIL_BREADCRUMBS_KEY] as Record<string, string> | undefined)?.[columnName];\n\n return {\n key: columnName,\n value,\n objectId: breadcrumb !== undefined && value != null ? String(value) : undefined,\n breadcrumb,\n };\n}\n\n/**\n * Whether this row is a query result item.\n *\n * Discriminated on the *elements*, not just on `Array.isArray(values)`: a flat AsDetail record may\n * legitimately have a column named `values` holding an array, and mistaking one for the other would\n * silently read the wrong thing.\n */\nexport function isQueryRow(row: SparkRow | null | undefined): row is QueryResultItem {\n const values = (row as QueryResultItem | undefined)?.values;\n return Array.isArray(values) && (values.length === 0 || typeof values[0]?.key === 'string');\n}\n\n/** Whether this row is a full persistent object. Discriminated on elements, as above. */\nexport function isPersistentObject(row: SparkRow | null | undefined): row is PersistentObject {\n const attributes = (row as PersistentObject | undefined)?.attributes;\n return Array.isArray(attributes) && (attributes.length === 0 || typeof attributes[0]?.name === 'string');\n}\n\n/**\n * One column's value filter (#431): the values a caller picked, or the values they excluded.\n *\n * **Values, never labels.** Each entry is the raw stored value — the same `value` the distinct\n * endpoint returned, never the `label` beside it. A reference column's values are document ids while\n * its labels are resolved breadcrumb text, and two rows can legitimately render the same text, so\n * matching on labels would be both wrong and ambiguous.\n *\n * `includes` and `excludes` are the two halves of the inverse toggle rather than a flag: the panel\n * moves values between them. `null` is a real, selectable value meaning \"no value\"; an empty or\n * absent array is the absent filter.\n */\nexport interface QueryColumnFilter {\n name: string;\n includes?: unknown[];\n excludes?: unknown[];\n}\n\n/**\n * One selectable entry in a column's filter panel.\n *\n * Structurally identical to the datatable's own `DistinctValue`, deliberately — it is what lets the\n * HTTP response be handed straight to `[distincts]` with no mapper in between.\n */\nexport interface DistinctValue {\n /** The raw stored value. `null` is a real entry meaning \"no value\", not an absent one. */\n value: unknown;\n /** What the user reads. Never matched on. */\n label: string;\n}\n\n/**\n * A column's distinct values.\n *\n * `remaining` is always empty from this server: the datatable snapshots the list when a selection is\n * first made and derives both buckets itself by diffing. The server answers \"what matches now\".\n *\n * `hasMore` reports that the cap truncated the list, and the panel re-queries only when it is set or\n * the search term widens — so a dishonest `false` strands a user typing past the cap.\n */\nexport interface DistinctValuesResult {\n matching: DistinctValue[];\n remaining: DistinctValue[];\n hasMore: boolean;\n}\n","import { TranslatedString } from './translated-string';\n\nexport enum ELookupDisplayType {\n Dropdown = 0,\n Modal = 1\n}\n\nexport interface LookupReferenceListItem {\n name: string;\n isTransient: boolean;\n valueCount: number;\n displayType: ELookupDisplayType;\n}\n\nexport interface LookupReference {\n name: string;\n isTransient: boolean;\n displayType: ELookupDisplayType;\n values: LookupReferenceValue[];\n}\n\nexport interface LookupReferenceValue {\n key: string;\n values: TranslatedString;\n isActive: boolean;\n extra?: Record<string, unknown>;\n}\n","import { isReservedAsDetailKey } from './as-detail-conversions';\nimport { PersistentObject } from './persistent-object';\nimport { PersistentObjectAttribute } from './persistent-object-attribute';\n\n/**\n * The one answer to \"do these two reads hold the same content?\", shared by History's revision diff\n * and po-edit's three-way conflict merge.\n *\n * Two reads of identical content are NOT byte-identical: their envelopes carry per-read fields\n * (etags, breadcrumbs, row keys, the wire values a flattened row keeps for its date cells) that\n * differ between reads, and comparing them reports every AsDetail attribute as changed. Both\n * functions below reduce a value to its content first. They come in two shapes because the two\n * callers hold two shapes: History compares wire attributes, po-edit compares the flat form dict.\n */\n\n/**\n * A comparable form of a wire attribute's content.\n *\n * Nested objects (AsDetail rows, references rendered as objects) are reduced to their attributes'\n * values: their envelopes carry per-read fields (etags, breadcrumbs, row keys) that differ between two\n * reads of identical content and would report every AsDetail attribute as changed.\n */\nexport function comparableValue(attribute: PersistentObjectAttribute | undefined): string {\n if (!attribute) return '\\u0000absent';\n return JSON.stringify({\n v: attribute.value ?? null,\n o: attribute.object ? nestedValues(attribute.object) : null,\n os: attribute.objects ? attribute.objects.map(nestedValues) : null,\n });\n}\n\nfunction nestedValues(po: PersistentObject): Record<string, string> {\n const result: Record<string, string> = {};\n for (const a of po.attributes ?? []) result[a.name] = comparableValue(a);\n return result;\n}\n\n/**\n * A comparable form of a value in the flat form dict ({@link nestedPoToDict}'s shape).\n *\n * The reserved keys a flattened row carries (its row key, its breadcrumb, the wire values of its\n * dates) are dropped, and object keys are sorted, so two flattenings of the same content compare\n * equal whatever order their attributes arrived in. `undefined` and `null` are the same absence.\n */\nexport function comparableFormValue(value: unknown): string {\n return JSON.stringify(normalizeFormValue(value));\n}\n\n/** Whether two form-dict values hold the same content (see {@link comparableFormValue}). */\nexport function formValuesEqual(a: unknown, b: unknown): boolean {\n return comparableFormValue(a) === comparableFormValue(b);\n}\n\nfunction normalizeFormValue(value: unknown): unknown {\n if (value === undefined || value === null) return null;\n if (Array.isArray(value)) return value.map(normalizeFormValue);\n if (typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value as object).sort()) {\n if (isReservedAsDetailKey(key)) continue;\n result[key] = normalizeFormValue((value as Record<string, unknown>)[key]);\n }\n return result;\n }\n return value;\n}\n","import { CustomActionDefinition } from './custom-action';\nimport { resolveTranslation } from './translated-string';\n\n/**\n * The prompt to show before running `action` on `count` rows, with `{count}` substituted, or null\n * when the action asks for no confirmation (#467, D7).\n */\nexport function confirmationText(action: CustomActionDefinition | undefined | null, count: number, lang?: string): string | null {\n const text = resolveTranslation(action?.confirmation, lang);\n return text ? text.replace(/\\{count\\}/g, String(count)) : null;\n}\n\n/** The built-in action named `name` (`New`, `Edit`, `Delete`) in `actions`, when the server listed it. */\nexport function builtInAction(actions: CustomActionDefinition[], name: 'New' | 'Edit' | 'Delete'): CustomActionDefinition | undefined {\n return actions.find(a => !!a.isDefault && a.name.toLowerCase() === name.toLowerCase());\n}\n\n/** Whether `showedOn` offers the action on a query: `\"query\"` or `\"both\"`, in any casing. */\nexport function isShowedOnQuery(showedOn: string | undefined | null): boolean {\n const value = (showedOn ?? '').trim().toLowerCase();\n return value === 'query' || value === 'both';\n}\n\n/** Whether `showedOn` offers the action on a detail page: `\"detail\"` or `\"both\"`, in any casing. */\nexport function isShowedOnDetail(showedOn: string | undefined | null): boolean {\n const value = (showedOn ?? '').trim().toLowerCase();\n return value === 'detail' || value === 'both';\n}\n\n/**\n * The custom actions a query should offer, from the entity type's full set.\n *\n * `showedOn` must include the query side. The accepted values are `\"detail\"`, `\"query\"`\n * and `\"both\"` — as the server model and the custom-actions guide have always\n * documented — compared case-insensitively (#460 M15: `\"Both\"` used to render nowhere).\n *\n * The built-in New, Edit and Delete (`isDefault`, #460 D18, #467 D7) are left out: they are not executed as\n * custom actions, and the grid offers them itself (see {@link defaultQueryActions}).\n *\n * ⚠️ This narrows what is DISPLAYED. It is NOT an authorization boundary: the grant\n * is, and it is enforced independently in `ExecuteCustomAction` regardless of which\n * query the caller clicked from — a caller can always POST directly.\n */\nexport function filterQueryActions(\n actions: CustomActionDefinition[],\n): CustomActionDefinition[] {\n return actions.filter(a => !a.isDefault && isShowedOnQuery(a.showedOn));\n}\n\n/** The custom actions a detail page should offer — the detail-side counterpart of {@link filterQueryActions}. */\nexport function filterDetailActions(\n actions: CustomActionDefinition[],\n): CustomActionDefinition[] {\n return actions.filter(a => !a.isDefault && isShowedOnDetail(a.showedOn));\n}\n\n/**\n * The built-in New, Edit and Delete the server listed for this caller (#460 D18, #467 D7), when their `showedOn`\n * includes the query side. Present only when the caller holds `New/T`, `Edit/T` or `Delete/T`.\n */\nexport function defaultQueryActions(\n actions: CustomActionDefinition[],\n): CustomActionDefinition[] {\n return actions.filter(a => !!a.isDefault && isShowedOnQuery(a.showedOn));\n}\n","/**\n * Parses a custom action's `selectionRule` — a cardinality expression over the number\n * of selected rows — into a predicate.\n *\n * A port of the server's `SelectionRuleParser`, and the two MUST agree: they are tested\n * against one shared fixture (`selection-rule.fixture.json`) for exactly this reason.\n * Vidyano, where this grammar comes from, has the same algorithm in C# and JavaScript and\n * the two have already drifted — one throws on a non-numeric operand where the other\n * silently permits everything.\n *\n * Grammar: `X` is the count placeholder, whitespace is insignificant, terms split on `X`\n * are AND-combined (`1<X<5` is a range), operators are `<= >= < > != =` matched in that\n * order so `>=` is never read as `>`, and a number-first term is mirrored (`0<X` is `>0`).\n *\n * Client-side this only drives whether a button is disabled. The server enforces the same\n * rule independently — and neither is an authorization boundary: the action's grant is.\n */\nexport function parseSelectionRule(rule?: string | null): (count: number) => boolean {\n if (!rule || !rule.trim()) return () => true;\n try {\n return compile(rule);\n } catch {\n // Unlike the server, which refuses to start on a malformed rule, the client cannot\n // usefully fail: the rule arrived over the wire from a server that already validated\n // it. Disabling the button is the safe direction — it never permits an action the\n // server would refuse.\n return () => false;\n }\n}\n\nconst OPERATORS = ['<=', '>=', '<', '>', '!=', '='] as const;\n\nfunction compile(rule: string): (count: number) => boolean {\n const normalized = rule.replace(/ /g, '').toUpperCase();\n const terms = normalized.split('X').filter(t => t.length > 0);\n if (terms.length === 0) throw new Error(`Selection rule '${rule}' has no condition.`);\n\n const numberFirst = !normalized.startsWith('X') && normalized.includes('X');\n const predicates = terms.map((term, i) => compileTerm(term, rule, numberFirst && i === 0));\n\n return count => predicates.every(p => p(count));\n}\n\nfunction compileTerm(term: string, rule: string, mirrored: boolean): (count: number) => boolean {\n const op = OPERATORS.find(o => (mirrored ? term.endsWith(o) : term.startsWith(o)));\n if (!op) throw new Error(`Selection rule '${rule}' has no recognised operator in '${term}'.`);\n\n const numberPart = mirrored ? term.slice(0, term.length - op.length) : term.slice(op.length);\n // Number(' ') is 0 and Number('1.5') is 1.5 — neither is a valid operand here.\n if (!/^-?\\d+$/.test(numberPart)) {\n throw new Error(`Selection rule '${rule}' has a non-numeric operand in '${term}'.`);\n }\n const value = Number(numberPart);\n const effective = mirrored ? mirror(op) : op;\n\n switch (effective) {\n case '<=': return count => count <= value;\n case '>=': return count => count >= value;\n case '<': return count => count < value;\n case '>': return count => count > value;\n case '!=': return count => count !== value;\n case '=': return count => count === value;\n default: throw new Error(`Selection rule '${rule}' has an unsupported operator '${effective}'.`);\n }\n}\n\nfunction mirror(op: string): string {\n switch (op) {\n case '<': return '>';\n case '>': return '<';\n case '<=': return '>=';\n case '>=': return '<=';\n default: return op;\n }\n}\n","import { CustomActionDefinition } from './custom-action';\nimport { parseSelectionRule } from './selection-rule';\n\n/**\n * Whether a grid lets the user tick rows — what `<bs-datatable>` renders. There is no single\n * selection (#467, D10): a selectable list is a checkbox list, and actions whose rule does not\n * match the count are disabled rather than the selection trimmed.\n */\nexport type SparkSelectionMode = 'none' | 'multiple';\n\n/**\n * What a query definition (or a sub-query entry) declares (#460 D17): a fixed mode, or `'auto'` —\n * the default, and what an absent field means — to derive it from the actions offered.\n */\nexport type SparkSelectionModeSetting = 'auto' | SparkSelectionMode;\n\n/** The largest selection the server accepts in one request (`SparkDefaultActions.MaxSelectedItems`). */\nexport const SPARK_MAX_SELECTED_ITEMS = 200;\n\n/**\n * Whether an action needs ticked rows (#467, R1(4)): it has a rule, and the rule accepts at least\n * one row. `=0` accepts none, so an action that runs only without a selection never makes a list\n * selectable; an action without a rule acts on the query, not on rows.\n */\nexport function needsSelection(action: CustomActionDefinition): boolean {\n if (!action.selectionRule?.trim()) return false;\n const rule = parseSelectionRule(action.selectionRule);\n for (let count = 1; count <= SPARK_MAX_SELECTED_ITEMS; count++)\n if (rule(count)) return true;\n return false;\n}\n\n/**\n * The selection mode a grid renders (#467, R1).\n *\n * A declared `'none'` or `'multiple'` wins outright. `'auto'` (or nothing) derives it from\n * `actions`, which must already be the **effective** list for this result — listed for the caller\n * (the right), shown on queries, not withheld by `disabledActions` and not hidden by the recycle\n * bin. Built-in Edit and Delete count like any other action. `'multiple'` when at least one of them\n * {@link needsSelection}, else `'none'`, so a read-only user sees a list with no checkbox column.\n */\nexport function selectionModeFor(\n actions: CustomActionDefinition[],\n declared?: SparkSelectionModeSetting | null,\n): SparkSelectionMode {\n if (declared && declared !== 'auto') return declared;\n return actions.some(needsSelection) ? 'multiple' : 'none';\n}\n","import { SparkSelectionModeSetting } from './selection-mode';\n\n/**\n * One sub-query of a type, with the overrides the parent's type states for it (#460, D17).\n */\nexport interface SparkSubQuery {\n /** The query's alias or id. */\n query: string;\n /** Overrides the query's own `selectionMode` on this parent's detail page. */\n selectionMode?: SparkSelectionModeSetting;\n /** The row attribute pointing at the parent, when the row type has several (server-side). */\n parentReference?: string;\n}\n\n/**\n * An entry of `EntityType.queries` as it arrives on the wire: a bare alias — the shape every model\n * had before #460 M15, and still what the server sends for an entry without overrides — or the\n * object form.\n */\nexport type SparkSubQueryEntry = string | SparkSubQuery;\n\n/** One entry in its object form, whichever shape it arrived in. */\nexport function normalizeSubQuery(entry: SparkSubQueryEntry): SparkSubQuery {\n return typeof entry === 'string' ? { query: entry } : entry;\n}\n\n/** A type's sub-queries in object form; empty when it declares none. */\nexport function subQueriesOf(type: { queries?: SparkSubQueryEntry[] } | null | undefined): SparkSubQuery[] {\n return (type?.queries ?? []).map(normalizeSubQuery).filter(entry => !!entry.query);\n}\n","import { EntityAttributeDefinition } from './entity-type';\nimport { PersistentObject } from './persistent-object';\nimport { ValidationRule } from './validation-rule';\n\n/**\n * One selectable value, as replaced by a refresh hook. Mirrors the server's\n * `PersistentObjectAttributeOption`.\n */\nexport interface RefreshedOption {\n key: string;\n label?: Record<string, string>;\n}\n\n/**\n * What a refresh changed about one attribute's *presentation*.\n *\n * Kept separate from `EntityType` on purpose. The form's option loading hangs off a single effect\n * keyed on `entityType` identity, and `SparkService` caches nothing — so applying a refresh by\n * setting a new `EntityType` re-issues every reference query, every lookup fetch, a full\n * `getEntityTypes()` and a `getPermissions()` per array-AsDetail attribute, on every refresh.\n * Mutating the existing object instead is inert, because the rendering computed would not re-run.\n * An overlay is the only shape that is both reactive and free.\n */\nexport interface AttributeOverlay {\n isRequired?: boolean;\n isReadOnly?: boolean;\n isVisible?: boolean;\n rules?: ValidationRule[];\n query?: string;\n /** `undefined` means the hook did not touch the options; an empty array means there are none. */\n options?: RefreshedOption[];\n}\n\nexport type RefreshOverlay = Record<string, AttributeOverlay>;\n\n/** Applies an overlay to one attribute definition, returning a new object when anything changed. */\nexport function applyOverlay(\n attr: EntityAttributeDefinition,\n overlay: AttributeOverlay | undefined,\n): EntityAttributeDefinition {\n if (!overlay) return attr;\n\n return {\n ...attr,\n isRequired: overlay.isRequired ?? attr.isRequired,\n isReadOnly: overlay.isReadOnly ?? attr.isReadOnly,\n isVisible: overlay.isVisible ?? attr.isVisible,\n rules: overlay.rules ?? attr.rules,\n query: overlay.query ?? attr.query,\n };\n}\n\n/**\n * Reads a refresh response into an overlay.\n *\n * Everything here is presentation the server owns outright, so it is taken verbatim — there is no\n * merging to do on this half, only on values.\n */\nexport function overlayFromResponse(response: PersistentObject): RefreshOverlay {\n const overlay: RefreshOverlay = {};\n\n for (const attr of response.attributes ?? []) {\n overlay[attr.name] = {\n isRequired: attr.isRequired,\n isReadOnly: attr.isReadOnly,\n isVisible: attr.isVisible,\n rules: attr.rules ?? [],\n query: attr.query,\n options: (attr as { options?: RefreshedOption[] | null }).options ?? undefined,\n };\n }\n\n return overlay;\n}\n\n/**\n * Merges a refresh response's values into the live form.\n *\n * The rule, and the reason for it: a refresh is not instant, and the user keeps typing during it —\n * the form is deliberately never frozen. So for each attribute we ask whether the *server* changed\n * it, by comparing the response against the values that were **sent**, not against what is on\n * screen now.\n *\n * - server value equals what we sent → the hook did not touch it, so whatever is in the form now\n * wins, including anything typed while the request was in flight;\n * - server value differs → the hook deliberately changed it, and it wins over a concurrent edit.\n *\n * Comparing against the displayed value instead is the classic \"refresh eats my typing\" bug;\n * refusing to overwrite anything the user touched is the equally wrong opposite, where a dependent\n * field the hook computed never appears.\n *\n * @param sent values as they were POSTed, captured before the request left\n * @param current values as they are now, which may have moved on\n * @param response the reshaped object\n */\nexport function mergeRefreshValues(\n sent: Record<string, any>,\n current: Record<string, any>,\n response: PersistentObject,\n): Record<string, any> {\n const merged = { ...current };\n\n for (const attr of response.attributes ?? []) {\n const serverValue = attr.value ?? null;\n const sentValue = sent[attr.name] ?? null;\n\n if (!valuesEqual(serverValue, sentValue)) {\n merged[attr.name] = attr.value;\n }\n }\n\n return merged;\n}\n\n/**\n * Structural for arrays, `===` otherwise.\n *\n * Multi-reference attributes hold `string[]`, and a fresh array of the same ids is a different\n * object — so reference equality would report every one of them as \"changed by the server\" on\n * every refresh, and clobber in-flight edits to them.\n */\nfunction valuesEqual(a: any, b: any): boolean {\n if (a === b) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((item, i) => valuesEqual(item, b[i]));\n }\n // Values arrive off the wire as JSON scalars; anything else compares by JSON shape rather than\n // by identity, which is what a caller means by \"did this change\".\n if (a !== null && b !== null && typeof a === 'object' && typeof b === 'object') {\n return JSON.stringify(a) === JSON.stringify(b);\n }\n return false;\n}\n","import { ValidationRule } from './validation-rule';\nimport { TranslatedString } from './translated-string';\n\n/** One rule failure, in the shape the form already renders per field. */\nexport interface RuleFailure {\n attributeName: string;\n ruleType: string;\n message: string;\n}\n\nexport interface EvaluableAttribute {\n name: string;\n label?: TranslatedString;\n isRequired?: boolean;\n rules?: ValidationRule[];\n}\n\nconst EMAIL = /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/i;\nconst URL = /^https?:\\/\\/[^\\s]+$/i;\n\nfunction isEmpty(value: any): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === 'string') return value.trim() === '';\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * Evaluates an attribute's rules against a value, mirroring the server's `ValidationService`.\n *\n * ⚠️ **Parity with the server is the point, and disagreement is worse than silence.** A client that\n * rejects something the server would accept blocks legitimate work with no recourse; one that\n * accepts something the server rejects merely defers the error to the round-trip, which is where it\n * used to live anyway. So the rule set here is deliberately limited to the types the server\n * implements, and an unrecognised rule type is ignored rather than guessed at.\n *\n * This exists because a refresh hook that imposes a rule needs it to bite before Save — previously\n * `rules` was carried on the wire and never evaluated in the browser at all.\n */\nexport function evaluateRules(attr: EvaluableAttribute, value: any): RuleFailure[] {\n const failures: RuleFailure[] = [];\n const label = attr.label?.['en'] ?? attr.name;\n\n if (attr.isRequired && isEmpty(value)) {\n return [{ attributeName: attr.name, ruleType: 'required', message: `${label} is required` }];\n }\n\n // A rule other than \"required\" says nothing about an absent value — that is what required is for.\n if (isEmpty(value)) return failures;\n\n const text = value?.toString() ?? '';\n\n for (const rule of attr.rules ?? []) {\n const failure = evaluateRule(attr.name, label, rule, value, text);\n if (failure) failures.push(failure);\n }\n\n return failures;\n}\n\nfunction evaluateRule(\n name: string,\n label: string,\n rule: ValidationRule,\n value: any,\n text: string,\n): RuleFailure | null {\n const message = rule.message?.['en'];\n\n switch (rule.type?.toLowerCase()) {\n case 'maxlength': {\n const max = toInt(rule.value);\n if (max === null || text.length <= max) return null;\n return { attributeName: name, ruleType: 'maxLength', message: message ?? `${label} must be at most ${max} characters` };\n }\n case 'minlength': {\n const min = toInt(rule.value);\n if (min === null || text.length >= min) return null;\n return { attributeName: name, ruleType: 'minLength', message: message ?? `${label} must be at least ${min} characters` };\n }\n case 'range': {\n const numeric = toNumber(value);\n if (numeric === null) return null;\n if (rule.min !== undefined && rule.min !== null && numeric < rule.min) {\n return { attributeName: name, ruleType: 'range', message: message ?? `${label} must be at least ${rule.min}` };\n }\n if (rule.max !== undefined && rule.max !== null && numeric > rule.max) {\n return { attributeName: name, ruleType: 'range', message: message ?? `${label} must be at most ${rule.max}` };\n }\n return null;\n }\n case 'regex': {\n const pattern = rule.value?.toString();\n if (!pattern) return null;\n let re: RegExp;\n try {\n re = new RegExp(pattern);\n } catch {\n // An unparseable pattern is a server-side authoring bug. Staying silent leaves the server to\n // report it, which is strictly better than blocking the user over a rule nobody can evaluate.\n return null;\n }\n return re.test(text) ? null : { attributeName: name, ruleType: 'regex', message: message ?? `${label} is not in the expected format` };\n }\n case 'email':\n return EMAIL.test(text) ? null : { attributeName: name, ruleType: 'email', message: message ?? `${label} must be a valid email address` };\n case 'url':\n return URL.test(text) ? null : { attributeName: name, ruleType: 'url', message: message ?? `${label} must be a valid URL` };\n default:\n return null;\n }\n}\n\n/**\n * Mirrors the server's `TryGetIntValue`: an integer number, or a string `int.TryParse` accepts.\n * Anything else means \"no rule\". `Number(value)` is NOT that — it turns null and '' into 0 (so every\n * non-empty value failed \"must be at most 0 characters\") and truncated 2.5, which the server ignores.\n */\nfunction toInt(value: any): number | null {\n if (typeof value === 'number') return Number.isInteger(value) ? value : null;\n if (typeof value === 'string' && /^\\s*[+-]?\\d+\\s*$/.test(value)) return parseInt(value, 10);\n return null;\n}\n\nfunction toNumber(value: any): number | null {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-models.mjs","names":[],"sources":["../../models/src/translated-string.ts","../../models/src/showed-on.ts","../../models/src/entity-type.ts","../../models/src/datetime-local.ts","../../models/src/as-detail-conversions.ts","../../models/src/query-result.ts","../../models/src/lookup-reference.ts","../../models/src/comparable-value.ts","../../models/src/query-actions.ts","../../models/src/selection-rule.ts","../../models/src/selection-mode.ts","../../models/src/sub-query.ts","../../models/src/refresh-overlay.ts","../../models/src/rule-evaluation.ts"],"sourcesContent":["import { signal, type WritableSignal } from '@angular/core';\n\nexport type TranslatedString = Record<string, string>;\n\n/** Global reactive language state — shared across library boundaries via globalThis */\nexport const currentLanguage: WritableSignal<string> =\n ((globalThis as any).__sparkCurrentLanguage ??= signal('en'));\n\nexport function resolveTranslation(ts: TranslatedString | undefined, lang?: string): string {\n if (!ts) return '';\n const language = lang ?? currentLanguage();\n return ts[language] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n}\n","/**\n * Flags enum controlling on which pages an attribute should be displayed.\n * Values can be combined: ShowedOn.Query | ShowedOn.PersistentObject\n */\nexport enum ShowedOn {\n /**\n * Drawn on no page (#264; Vidyano's `Never`). The attribute still ships on the object, and an action can show\n * it for one object through the runtime `showedOn` it sets on load, new or refresh.\n */\n None = 0,\n Query = 1,\n PersistentObject = 2,\n}\n\n/**\n * Helper function to check if a ShowedOn value includes a specific flag.\n */\nexport function hasShowedOnFlag(value: ShowedOn | string | undefined, flag: ShowedOn): boolean {\n if (value === undefined) return true; // Default: show on all pages\n\n // Handle string values from JSON (e.g., \"Query, PersistentObject\")\n if (typeof value === 'string') {\n const parts = value.split(',').map(s => s.trim());\n const flagName = ShowedOn[flag];\n return parts.includes(flagName);\n }\n\n // Handle numeric flag values\n return (value & flag) === flag;\n}\n","import { ShowedOn } from './showed-on';\nimport { TranslatedString } from './translated-string';\nimport { ValidationRule } from './validation-rule';\nimport { SparkSubQueryEntry } from './sub-query';\n\n/**\n * Controls how a Reference attribute is picked in the PO-edit form.\n * Serialized as a string by the server (mirrors the .NET EReferenceDisplayType).\n */\nexport enum EReferenceDisplayType {\n /** Renders as a `<bs-select>` listing every referenced item. */\n Dropdown = 'Dropdown',\n /** Renders a readonly textbox + \"…\" button that opens a searchable modal grid picker. */\n Modal = 'Modal',\n}\n\n/**\n * When changing an attribute asks the server to reshape the object (mirrors the .NET\n * `ERefreshTrigger`, serialized PascalCase like {@link EReferenceDisplayType}).\n *\n * - `None` — no refresh; the same as absent.\n * - `Auto` — free text refreshes on blur; discrete editors (lookup, reference, boolean, date,\n * datetime, enum, color) refresh immediately.\n * - `ValueChanged` — refreshes on every change; free text is debounced (300 ms) and flushed by\n * blur or save.\n * - `Blur` — refreshes on blur. A discrete editor never blurs meaningfully, so there it acts as\n * `ValueChanged` (and `--spark-verify-model` warns).\n */\nexport type RefreshTrigger = 'None' | 'Auto' | 'ValueChanged' | 'Blur';\n\nexport interface EntityAttributeDefinition {\n id: string;\n name: string;\n label?: TranslatedString;\n /** Help text rendered as an [i] tooltip beside the label. Absent when the model declares none. */\n description?: TranslatedString;\n dataType: string;\n isRequired: boolean;\n isReadOnly: boolean;\n order: number;\n query?: string;\n /** For reference attributes, specifies the target entity type's CLR type name */\n referenceType?: string;\n /** For AsDetail attributes, specifies the nested entity type's CLR type name */\n asDetailType?: string;\n /** When true, the attribute represents an array/collection of AsDetail objects */\n isArray?: boolean;\n /** For array AsDetail attributes: \"modal\" (default) or \"inline\" */\n editMode?: 'inline' | 'modal';\n /**\n * For Reference attributes: 'Modal' renders the \"…\" + modal query-grid picker;\n * 'Dropdown'/absent (default) renders a `<bs-select>`. Hand-set in the model JSON.\n */\n referenceDisplayType?: EReferenceDisplayType;\n /** For array AsDetail attributes: when true, rows can be drag-reordered (order = array position) */\n isSortable?: boolean;\n /**\n * When set (and not `'None'`), changing this attribute's value posts the in-progress object to\n * `/spark/po/{objectTypeId}/refresh` and applies the reshaped result as an overlay; the value\n * decides when (see {@link RefreshTrigger}). Absent means no refresh.\n * Schema-only by design — it never travels on a PersistentObjectAttribute, so a client\n * cannot claim a trigger the model did not declare.\n */\n triggersRefresh?: RefreshTrigger;\n /** For LookupReference attributes, specifies the lookup reference type name */\n lookupReferenceType?: string;\n /**\n * Controls on which pages the attribute should be displayed.\n * Query = shown in list views, PersistentObject = shown in detail/edit views.\n * Can be a numeric flag value or a string like \"Query, PersistentObject\".\n */\n showedOn?: ShowedOn | string;\n rules: ValidationRule[];\n /** References an AttributeGroup.id to assign this attribute to a group */\n group?: string;\n /** Number of grid columns this attribute spans within a tab's column layout */\n columnSpan?: number;\n /** Renderer component name for custom display in detail/list views */\n renderer?: string;\n /** Options passed to the renderer component */\n rendererOptions?: Record<string, any>;\n}\n\nexport interface AttributeTab {\n id: string;\n name: string;\n label?: TranslatedString;\n order: number;\n /** Number of columns for the grid layout within this tab */\n columnCount?: number;\n}\n\nexport interface AttributeGroup {\n id: string;\n name: string;\n label?: TranslatedString;\n /** References an AttributeTab.id to assign this group to a tab */\n tab?: string;\n order: number;\n}\n\nexport interface EntityType {\n id: string;\n name: string;\n /** The type's display name, resolved by the server (#467, D6; formerly `description`). */\n label?: TranslatedString;\n /**\n * The backing CLR type, or absent for a JSON-only virtual type (#325).\n *\n * Optional on purpose: the server sends null for a virtual type, and this was declared\n * non-nullable, so any unguarded `.endsWith(...)` on it threw a TypeError naming an innocent\n * query. Types are data from the wire, not a promise about it.\n */\n clrType?: string;\n\n /**\n * Whether this caller may OPEN one of these, as opposed to list them.\n *\n * The catalogue is gated on Query, so its presence here means \"you may list this type\" and says\n * nothing about opening one. A reference must not render as a link when this is false: it used\n * to, and the click refused on arrival.\n */\n canRead?: boolean;\n alias?: string;\n /**\n * Breadcrumb template: literal text plus `{AttributeName}` placeholders. A scalar placeholder\n * renders its value; a reference placeholder renders the referenced entity's breadcrumb.\n * The server resolves this — clients only read the resulting strings. Example: \"{Street}, {City}\".\n */\n breadcrumb?: string;\n /**\n * When false, the breadcrumb needs the collection document (a placeholder field is not on the\n * projection). null/absent means renderable from the projection. Informational on the client.\n */\n breadcrumbProjectionSatisfiable?: boolean;\n tabs?: AttributeTab[];\n groups?: AttributeGroup[];\n attributes: EntityAttributeDefinition[];\n /**\n * The sub-queries displayed as related query tables on the detail page: a bare alias or id, or\n * `{ query, selectionMode?, parentReference? }` (#460, D17). Read them with `subQueriesOf(type)`.\n */\n queries?: SparkSubQueryEntry[];\n /**\n * The definitions of this type's AsDetail row types, sent alongside it so a detail table can draw\n * its columns.\n *\n * Needed because a row type usually has no rights of its own — a row is edited through its parent\n * — and the entity-type catalogue is gated on `Query`. Resolving a row type from the catalogue\n * therefore failed silently, and the table rendered with **no columns at all**.\n *\n * Per-caller and gated on the *parent's* right, which is the same gate that already ships each\n * row's attribute schema inside `attr.objects`. Pruned server-side: no `queryType`, `indexName`,\n * `queries` or `alias`.\n */\n detailTypes?: EntityType[];\n /**\n * Whether adding or removing a row of this type asks the server first.\n *\n * Absent or false — the default — keeps the purely client-side behaviour: New pushes a blank row,\n * Delete splices it out, and the parent's save is the first the server hears of either. On, the\n * grid calls `newObject` before showing a row and `deleteRow` before removing one, so\n * `OnNewAsync` can default it and the delete hook can refuse.\n *\n * ⚠️ Read it off the **row type**, never the parent: the type that owns the hooks owns the\n * decision. For a row type resolved out of `detailTypes` the flag survives the server-side\n * pruning, so both sources agree.\n *\n * ⚠️ It governs the round trip and nothing else. It is not a permission, and it is outside the\n * model hash, so nothing that gates a write may be conditional on it — the save path enforces the\n * row type's New/Edit/Delete rights on every embedded collection whether this is on or off.\n */\n serverSideRowLifecycle?: boolean;\n}\n","/**\n * Conversions between the wire format for a timestamp and what an `<input type=\"datetime-local\">`\n * or `<input type=\"date\">` will accept.\n *\n * A `DateTimeOffset` in Spark means an **instant**. The wire carries a full ISO-8601 string with an\n * offset (`2026-12-31T23:59:00-08:00`); an HTML date/time control speaks only bare local wall clock\n * (`2026-12-31T23:59`). Neither end can consume the other's format, so both directions need\n * converting, and the browser is deliberately the side that does it: its timezone rules are kept\n * fresh by the operating system, while a container's are frozen at image build time.\n *\n * Assigning a wire value straight to a `datetime-local` input does not throw and does not warn -- the\n * control simply renders **blank**, and saving the untouched form then writes that blank back.\n */\n\nfunction pad(n: number): string {\n return String(n).padStart(2, '0');\n}\n\n/** Formats a UTC offset in minutes as `+HH:mm` / `-HH:mm`. */\nfunction formatOffset(offsetMinutes: number): string {\n const sign = offsetMinutes >= 0 ? '+' : '-';\n const abs = Math.abs(offsetMinutes);\n return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;\n}\n\n/**\n * Parses a wire timestamp. Returns `null` for anything unusable, matching the contract of\n * `parsedDate` / `queryCellValue` -- a bad value renders as empty rather than as `Invalid Date`.\n */\nexport function parseWireDate(value: unknown): Date | null {\n if (value === null || value === undefined || value === '') return null;\n if (value instanceof Date) return isNaN(value.getTime()) ? null : value;\n if (typeof value !== 'string' && typeof value !== 'number') return null;\n\n const parsed = new Date(value);\n return isNaN(parsed.getTime()) ? null : parsed;\n}\n\n/**\n * Wire value -> the `yyyy-MM-ddTHH:mm` a `datetime-local` input needs, in the **viewer's** zone.\n *\n * The conversion is date-sensitive, not a fixed shift: a value is rendered using the zone's offset\n * *on that value's own date*, so a timestamp in July and one in January convert differently in a\n * zone that observes DST.\n */\nexport function toDateTimeLocalInput(value: unknown): string {\n const d = parseWireDate(value);\n if (!d) return '';\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;\n}\n\n/** Wire value -> the `yyyy-MM-dd` a `date` input needs, in the viewer's zone. */\nexport function toDateInput(value: unknown): string {\n const d = parseWireDate(value);\n if (!d) return '';\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n}\n\n/**\n * `yyyy-MM-ddTHH:mm` from the control -> a complete ISO-8601 string carrying the viewer's offset\n * **for the entered date**.\n *\n * Building the `Date` from its parts rather than parsing the string is what makes this correct: it\n * lets the browser apply its own timezone rules for that date, including DST. Using the *current*\n * offset instead would be wrong for any value outside the present season -- entering a January date\n * during summer would record an offset an hour off, and the stored instant would be an hour wrong.\n *\n * Across a DST discontinuity the browser resolves the wall clock itself, taking the offset in force\n * before a fall-back and after a spring-forward. That is specified ECMAScript behaviour, and the\n * server's conversion helper deliberately reproduces the same choice so the two cannot drift.\n */\nexport function fromDateTimeLocalInput(local: unknown): string | null {\n if (typeof local !== 'string' || local === '') return null;\n\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2}))?/.exec(local);\n if (!match) return null;\n\n const [, y, mo, d, h, mi, s] = match;\n const date = new Date(+y, +mo - 1, +d, +h, +mi, s ? +s : 0);\n if (isNaN(date.getTime())) return null;\n\n // getTimezoneOffset() is minutes to ADD to local to reach UTC, so it is the negation of the offset\n // an ISO string carries.\n return `${y}-${mo}-${d}T${h}:${mi}:${pad(s ? +s : 0)}${formatOffset(-date.getTimezoneOffset())}`;\n}\n\n/** `yyyy-MM-dd` from a `date` control -> an ISO string at local midnight with the viewer's offset. */\nexport function fromDateInput(local: unknown): string | null {\n if (typeof local !== 'string' || local === '') return null;\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(local);\n if (!match) return null;\n return fromDateTimeLocalInput(`${local}T00:00`);\n}\n\n/**\n * Compares two wire timestamps **by instant**, which is what a `DateTimeOffset` means in Spark.\n *\n * Round-tripping a value through a date control legitimately rewrites its text -- a document holding\n * `2026-12-31T23:59:00-08:00` comes back as `2027-01-01T08:59:00+01:00` from a viewer in Brussels --\n * so a string comparison would mark every untouched date as edited and send a needless write on every\n * save. Both of those name the same moment.\n *\n * Note this is the *opposite* of the trap on the server side, where `DateTimeOffset.Equals` compares\n * the instant and silently hides a destroyed offset. Here comparing by instant is the correct thing:\n * we are asking \"did the user change this value\", not \"is the offset intact\".\n */\nexport function wireDatesEqual(a: unknown, b: unknown): boolean {\n const da = parseWireDate(a);\n const db = parseWireDate(b);\n if (da === null || db === null) return da === db;\n return da.getTime() === db.getTime();\n}\n\n/** True for the attribute data types that are edited through a native date/time control. */\nexport function isDateDataType(dataType: string | null | undefined): dataType is 'date' | 'datetime' {\n return dataType === 'date' || dataType === 'datetime';\n}\n\n/** Wire -> control, picking the right shape for the data type. */\nexport function toDateInputValue(dataType: string | null | undefined, value: unknown): string {\n return dataType === 'date' ? toDateInput(value) : toDateTimeLocalInput(value);\n}\n\n/** Control -> wire, picking the right shape for the data type. */\nexport function fromDateInputValue(dataType: string | null | undefined, value: unknown): string | null {\n return dataType === 'date' ? fromDateInput(value) : fromDateTimeLocalInput(value);\n}\n","import { fromDateInputValue, isDateDataType, toDateInputValue, wireDatesEqual } from './datetime-local';\nimport { EntityAttributeDefinition } from './entity-type';\nimport { EntityType } from './entity-type';\nimport { PersistentObject } from './persistent-object';\nimport { PersistentObjectAttribute } from './persistent-object-attribute';\n\n/**\n * Resolves an `EntityType` by its CLR type name (e.g. `\"HR.Entities.Address\"`).\n * Callers typically close over a list they already hold. `getEntityTypes()` is NOT cached --\n * it issues a request per call -- so resolve against a list you fetched once rather than\n * calling it inside the resolver.\n */\nexport type EntityTypeResolver = (clrTypeName: string) => EntityType | undefined;\n\n/**\n * Flattens a nested `PersistentObject` into the plain `Record<string, any>` shape the\n * form state uses throughout ng-spark. Primitive / reference attributes contribute their\n * `value`; nested AsDetail attributes recurse — single becomes an inner dict, array\n * becomes an array of inner dicts. Returns `{}` for `null` / `undefined` input.\n *\n * This is the ONE place that reads the server's new AsDetail wire shape and collapses it\n * back to the flat dict the form components already handle.\n */\nexport function nestedPoToDict(po: PersistentObject | null | undefined): Record<string, any> {\n if (!po) return {};\n const dict: Record<string, any> = {};\n for (const attr of po.attributes ?? []) {\n dict[attr.name] = attributeValueForForm(attr);\n }\n // The object's own server-resolved breadcrumb, kept under a reserved key so the form can label\n // it without re-deriving a template it may be structurally unable to resolve. Safe to carry\n // through save: `dictToNestedPo` walks the entity type's attributes, never the dict's keys, so\n // a reserved key is never sent to the server. Attached only when it resolved, which keeps the\n // common case byte-for-byte identical to the plain flat dict.\n if (typeof po.breadcrumb === 'string' && po.breadcrumb !== '') {\n dict[AS_DETAIL_SELF_BREADCRUMB_KEY] = po.breadcrumb;\n }\n // The row's own key, same reserved-key mechanism and for a sharper reason: without it the key\n // never returns, and a save cannot tell an edited row from a deleted one plus a new one. See\n // AS_DETAIL_ROW_KEY.\n if (typeof po.id === 'string' && po.id !== '') {\n dict[AS_DETAIL_ROW_KEY] = po.id;\n }\n // The wire values of any date cells, kept so the save side can tell an untouched cell from an\n // edited one. Without it, flattening to a wall clock and converting back rewrites the stored\n // offset to the viewer's on every save, even for rows nobody touched.\n const originalDates = originalDatesOf(po);\n if (originalDates) dict[AS_DETAIL_ORIGINAL_DATES_KEY] = originalDates;\n if (po.metadata) dict[AS_DETAIL_METADATA_KEY] = po.metadata;\n return dict;\n}\n\nfunction originalDatesOf(po: PersistentObject): Record<string, any> | undefined {\n let originals: Record<string, any> | undefined;\n for (const attr of po.attributes ?? []) {\n if (isDateDataType(attr.dataType)) (originals ??= {})[attr.name] = attr.value;\n }\n return originals;\n}\n\nfunction attributeValueForForm(attr: PersistentObjectAttribute): any {\n if (attr.dataType === 'AsDetail') {\n if (attr.isArray) return (attr.objects ?? []).map(po => nestedPoToDict(po));\n return attr.object ? nestedPoToDict(attr.object) : null;\n }\n // An embedded row's date cells are edited through the same native controls as a root object's,\n // so they need the same wire -> wall-clock conversion. This is the FORM path only;\n // `nestedPoToDisplayRow` deliberately keeps the wire value, because the display path formats\n // through `parsedDate` rather than feeding an input.\n if (isDateDataType(attr.dataType)) return toDateInputValue(attr.dataType, attr.value);\n return attr.value;\n}\n\n/**\n * Reserved key under which a flattened nested object keeps the breadcrumb the SERVER resolved for\n * that object itself (as opposed to {@link AS_DETAIL_BREADCRUMBS_KEY}, which keys the breadcrumbs\n * of its reference attributes by attribute name).\n *\n * This exists because a breadcrumb template can name a property the model does not carry. HR's\n * `Address` declares `[Breadcrumb, IgnoreProperty] string Crumb`, and `Address.json` renders it as\n * `\"{Crumb}\"` — the server resolves that by reflecting over the CLR property, which no client can\n * do, because `[IgnoreProperty]` is exactly the instruction to keep it out of the model. Flattening\n * used to discard the resolved string, leaving the form to substitute `{Crumb}` against a dict that\n * can never contain it.\n */\nexport const AS_DETAIL_SELF_BREADCRUMB_KEY = '__sparkBreadcrumb';\n\n/**\n * Reserved key under which a flattened nested object keeps its own row key — the value of whatever\n * property the server registered as that type's `[ValueKey]`.\n *\n * A value object's key is not a model attribute (nothing declares `Id` in its model file), so it\n * travels as the nested `PersistentObject`'s `id` and has to be carried across the flat-dict form\n * state by hand. Before this existed, flattening dropped it and `dictToNestedPo` rebuilt `id` from\n * `dict['Id']`, which is never present — so **every** embedded row reached the server with an empty\n * id, and the fresh instance the mapper builds kept the guid its own field initializer had just\n * minted.\n *\n * That is not cosmetic. Stored keys and incoming keys were then both plausible and never equal, so\n * a save could not match rows: an edit was indistinguishable from a delete plus a create. Every\n * per-row rule — preserving read-only fields, and the `New`/`Edit`/`Delete` rights of the row type —\n * rests on this key surviving the round trip.\n *\n * Safe to carry, for the same reason the breadcrumb is: `dictToNestedPo` walks the entity type's\n * attributes and never the dict's keys, so a reserved key is never sent back as an attribute.\n */\nexport const AS_DETAIL_ROW_KEY = '__sparkRowKey';\n\n/**\n * The breadcrumb the server resolved for a flattened object, or null when it resolved to nothing.\n *\n * `EntityMapper` never emits an empty breadcrumb: when the template renders blank it substitutes\n * the CLR type name, so an unset `Address` arrives as the literal string `\"Address\"`\n * (EntityMapper.cs:209-211). That is a placeholder, not data, and rendering it would be worse than\n * rendering nothing — it reads as a real value. `typeName` lets a caller filter it back out.\n */\nexport function selfBreadcrumb(row: Record<string, any> | null | undefined, typeName?: string): string | null {\n return resolvedBreadcrumb(row?.[AS_DETAIL_SELF_BREADCRUMB_KEY], typeName);\n}\n\n/**\n * The same filter as {@link selfBreadcrumb}, for a caller holding the server's breadcrumb string\n * directly rather than a flattened row dict — `attr.object.breadcrumb` on a detail page, or a query\n * cell's `breadcrumb`.\n *\n * Split out so there is exactly one answer to \"is this the type-name placeholder?\". The first\n * implementation lived inside `selfBreadcrumb` and so covered only the two pipes that flatten a row\n * first; the two that read the string straight off the wire kept printing `BuildFeedback` and\n * `GateSettings` at users (#384). A second copy of the comparison is how that happens again.\n */\nexport function resolvedBreadcrumb(value: unknown, typeName?: string): string | null {\n if (typeof value !== 'string' || value.trim() === '') return null;\n\n // Callers hold the type name in two shapes — `EntityType.name` is the short model name, while an\n // attribute's `asDetailType` is the full CLR name — and the server's placeholder is always the\n // short one. Compare on the last dotted segment so either shape filters it.\n if (typeName) {\n const shortName = typeName.slice(typeName.lastIndexOf('.') + 1);\n if (value === shortName) return null;\n }\n return value;\n}\n\n/**\n * Reserved key under which {@link nestedPoToDisplayRow} stashes the server-resolved breadcrumb of\n * each reference attribute (keyed by attribute name). Lets an AsDetail reference cell render the\n * label the server already resolved by id — page-independent — instead of guessing from a single\n * reference-query options page. Prefixed to avoid colliding with a real attribute name.\n */\nexport const AS_DETAIL_BREADCRUMBS_KEY = '__sparkBreadcrumbs';\n\n/**\n * Reserved key under which a flattened row keeps the **wire** values of its date cells, keyed by\n * attribute name.\n *\n * A date cell is edited through a native control that speaks only a bare local wall clock, so\n * flattening converts the stored instant into the viewer's zone and saving converts it back. That\n * round trip preserves the instant but necessarily rewrites the **offset** to the viewer's. For a\n * cell the user actually edited that is correct — the offset is not business data. For one nobody\n * touched it is pure loss: merely opening a row and saving would relabel a Seattle timestamp as a\n * Brussels one.\n *\n * Keeping the original lets {@link dictToNestedPo} send back exactly what it was given whenever the\n * instant is unchanged. Safe to carry for the same reason as the other reserved keys: the rebuild\n * walks the entity type's attributes, never the dict's keys.\n */\nexport const AS_DETAIL_ORIGINAL_DATES_KEY = '__sparkOriginalDates';\n\n/**\n * Whether a flattened-row key is one this module reserved rather than a model attribute.\n *\n * `nestedPoToDict` and `nestedPoToDisplayRow` stash the row key and the resolved breadcrumbs\n * alongside the real values, which is safe on the way back — `dictToNestedPo` walks the entity\n * type's attributes and never the dict's keys. It is *not* safe for anything that enumerates the\n * dict to build display text: a caller joining `Object.values(dict)` prints the row's guid and its\n * own breadcrumb as though they were fields.\n */\nexport function isReservedAsDetailKey(key: string): boolean {\n return key === AS_DETAIL_ROW_KEY\n || key === AS_DETAIL_SELF_BREADCRUMB_KEY\n || key === AS_DETAIL_BREADCRUMBS_KEY\n || key === AS_DETAIL_ORIGINAL_DATES_KEY\n || key === AS_DETAIL_METADATA_KEY;\n}\n\n/**\n * Reserved key under which a flattened row keeps the {@link PersistentObject.metadata} the server\n * attached to it on load (an add-on's read-side facts — contributions' `{ contribution: { own } }`).\n * Never sent back: `dictToNestedPo` walks the entity type's attributes, never the dict's keys.\n */\nexport const AS_DETAIL_METADATA_KEY = '__sparkMetadata';\n\n/** The metadata the server attached to a flattened row under `key`, or undefined. */\nexport function rowMetadata<T = any>(row: Record<string, any> | null | undefined, key: string): T | undefined {\n const metadata = row?.[AS_DETAIL_METADATA_KEY];\n return metadata && typeof metadata === 'object' ? (metadata as Record<string, any>)[key] as T : undefined;\n}\n\n/**\n * Like {@link nestedPoToDict}, but for the read-only detail display path. In addition to each\n * attribute's value it preserves the server-resolved per-reference `breadcrumb` under\n * {@link AS_DETAIL_BREADCRUMBS_KEY}, so an AsDetail reference cell can render the label by id\n * regardless of whether the referenced document fits on the reference query's first options page.\n * The form/edit path keeps using {@link nestedPoToDict}, which never carries breadcrumbs.\n */\nexport function nestedPoToDisplayRow(po: PersistentObject | null | undefined): Record<string, any> {\n if (!po) return {};\n const dict: Record<string, any> = {};\n let breadcrumbs: Record<string, string> | undefined;\n for (const attr of po.attributes ?? []) {\n dict[attr.name] = displayValueForAttribute(attr);\n if (attr.dataType === 'Reference' && !attr.isArray && typeof attr.breadcrumb === 'string' && attr.breadcrumb !== '') {\n (breadcrumbs ??= {})[attr.name] = attr.breadcrumb;\n }\n }\n // Only attach the side channel when something resolved — keeps reference-free rows (the common\n // case) byte-for-byte identical to the plain flat dict.\n if (breadcrumbs) dict[AS_DETAIL_BREADCRUMBS_KEY] = breadcrumbs;\n // Same self-breadcrumb the form path carries: a nested-AsDetail COLUMN in a detail table has an\n // inner dict as its value, which used to stringify to \"[object Object]\".\n if (typeof po.breadcrumb === 'string' && po.breadcrumb !== '') {\n dict[AS_DETAIL_SELF_BREADCRUMB_KEY] = po.breadcrumb;\n }\n // And the row key, for the same reason as the form path — a display row can be handed back for\n // saving, and a row that loses its key on that trip is indistinguishable from a new one.\n if (typeof po.id === 'string' && po.id !== '') {\n dict[AS_DETAIL_ROW_KEY] = po.id;\n }\n if (po.metadata) dict[AS_DETAIL_METADATA_KEY] = po.metadata;\n return dict;\n}\n\nfunction displayValueForAttribute(attr: PersistentObjectAttribute): any {\n if (attr.dataType === 'AsDetail') {\n if (attr.isArray) return (attr.objects ?? []).map(po => nestedPoToDisplayRow(po));\n return attr.object ? nestedPoToDisplayRow(attr.object) : null;\n }\n return attr.value;\n}\n\n/**\n * Builds a nested `PersistentObject` from a flat dict against the schema in\n * <paramref name=\"entityType\"/>. Used when the form is about to save — AsDetail attributes\n * are no longer sent as flat dicts in `attribute.value`; the server now requires\n * `attribute.object` / `attribute.objects` with fully scaffolded nested POs.\n *\n * `resolve` walks through AsDetail types registered elsewhere (usually the full\n * `getEntityTypes()` list, keyed by CLR type name). Nested AsDetail inside AsDetail is\n * handled recursively.\n */\nexport function dictToNestedPo(\n dict: Record<string, any> | null | undefined,\n entityType: EntityType,\n resolve: EntityTypeResolver,\n): PersistentObject {\n const originalDates = (dict?.[AS_DETAIL_ORIGINAL_DATES_KEY] ?? {}) as Record<string, any>;\n const attributes: PersistentObjectAttribute[] = (entityType.attributes ?? [])\n .map(attrDef => buildAttribute(attrDef, dict?.[attrDef.name], resolve, originalDates[attrDef.name]));\n\n return {\n // The reserved key first: it is the only one that is always present for a stored row, because\n // a value object's key is not a model attribute. `Id`/`id` remain as fallbacks for a root\n // object, whose id genuinely is part of the dict.\n id: (dict?.[AS_DETAIL_ROW_KEY] as string) ?? (dict?.['Id'] as string) ?? (dict?.['id'] as string) ?? '',\n name: entityType.name,\n objectTypeId: entityType.id,\n attributes,\n };\n}\n\nfunction buildAttribute(\n attrDef: EntityAttributeDefinition,\n raw: any,\n resolve: EntityTypeResolver,\n originalDate?: any,\n): PersistentObjectAttribute {\n const attr: PersistentObjectAttribute = {\n id: attrDef.id,\n name: attrDef.name,\n label: attrDef.label,\n dataType: attrDef.dataType,\n isArray: attrDef.isArray,\n isRequired: attrDef.isRequired,\n isReadOnly: attrDef.isReadOnly,\n order: attrDef.order,\n rules: attrDef.rules ?? [],\n isValueChanged: true,\n };\n\n if (attrDef.dataType === 'AsDetail') {\n // Server expects attr.value null for AsDetail; the nested PO carries the data.\n attr.value = null;\n attr.asDetailType = attrDef.asDetailType;\n\n const nestedType = attrDef.asDetailType ? resolve(attrDef.asDetailType) : undefined;\n if (!nestedType) {\n attr.object = null;\n attr.objects = attrDef.isArray ? [] : null;\n return attr;\n }\n\n if (attrDef.isArray) {\n const items: any[] = Array.isArray(raw) ? raw : [];\n attr.objects = items.map(item => dictToNestedPo((item as Record<string, any>) ?? {}, nestedType, resolve));\n } else {\n attr.object = raw ? dictToNestedPo(raw as Record<string, any>, nestedType, resolve) : null;\n }\n return attr;\n }\n\n if (isDateDataType(attrDef.dataType)) {\n // The mirror of the conversion in `attributeValueForForm`: back from the control's bare wall\n // clock to a complete ISO-8601 instant carrying the viewer's offset for that date. When the\n // instant is unchanged, send back exactly what was loaded instead, so an untouched row keeps\n // its stored offset. See AS_DETAIL_ORIGINAL_DATES_KEY.\n const converted = fromDateInputValue(attrDef.dataType, raw);\n attr.value = wireDatesEqual(converted, originalDate) ? originalDate : converted;\n return attr;\n }\n\n attr.value = raw;\n return attr;\n}\n","import { TranslatedString } from './translated-string';\nimport { PersistentObject } from './persistent-object';\nimport { AS_DETAIL_BREADCRUMBS_KEY } from './as-detail-conversions';\n\n/**\n * What a query returns: the column metadata once, then one lightweight row per result.\n *\n * Rows used to be full `PersistentObject`s, so every row repeated the complete attribute\n * metadata the client already held from `/spark/types` and never read off the row. More\n * importantly it conflated two things that are not the same: a **row is a projection**, a\n * **persistent object is a document**. Nothing here is entitled to act on a row — every\n * mutating path re-loads by id and re-applies security server-side.\n */\nexport interface QueryResult {\n columns: QueryColumn[];\n items: QueryResultItem[];\n /** Rows matching the query after filtering, before paging. */\n totalItems: number;\n skip: number;\n take: number;\n typeHints?: Record<string, string>;\n /**\n * Actions withheld for THIS query, by name (custom, and `New`). Absent or empty means every\n * action the caller has the right to is offered.\n *\n * The mirror of `PersistentObject.disabledActions`, set server-side by the entity's\n * `OnDisableActionsAsync` hook with a query target (#460, D13). The server asks the same hook when\n * an action is submitted from this query and refuses a disabled one with 403.\n */\n disabledActions?: string[];\n}\n\n/**\n * The subset of column metadata a cell needs in order to render.\n *\n * Both {@link QueryColumn} (a query result's column) and `EntityAttributeDefinition` (a detail\n * page's attribute) satisfy this, which is what lets one cell component serve the query grid and\n * the AsDetail sub-table without either side knowing about the other's shape.\n */\nexport interface SparkCellColumn {\n name: string;\n label?: TranslatedString;\n /** Help text rendered as an [i] tooltip in the column header. Absent when the model declares none. */\n description?: TranslatedString;\n dataType: string;\n isArray?: boolean;\n renderer?: string;\n rendererOptions?: Record<string, any>;\n referenceType?: string;\n lookupReferenceType?: string;\n asDetailType?: string;\n}\n\n/** One column of a query result, sent once rather than repeated on every row. */\nexport interface QueryColumn extends SparkCellColumn {\n order: number;\n /**\n * Whether the grid offers a sort affordance for this column. Resolved server-side from the query's\n * override then the attribute, so it arrives already decided.\n *\n * This replaced an `isSortable` that carried the server's AsDetail *drag-reorder* flag — a\n * different concept that was `false` for every scalar column while the grid hard-coded every\n * header sortable anyway. Neither side read it. The attribute-level `isSortable` on `EntityType`\n * is the drag-reorder flag and stays.\n *\n * Absent means sortable: a server predating the field still draws arrows.\n */\n canSort?: boolean;\n /** Whether the grid draws a filter cell for this column. Absent means filterable. */\n canFilter?: boolean;\n /**\n * Whether this column's distinct values may be listed. When `false` and `canFilter` is true the\n * panel offers a free-text filter instead of a value list — listing a column's values is a much\n * stronger disclosure than filtering by one already known.\n */\n canListDistincts?: boolean;\n /** The query backing a Reference column, by name — the client's option source. */\n query?: string;\n}\n\n/**\n * One row: an id, a display string, and a value per column.\n *\n * Deliberately too weak to act on — no attribute metadata, no `can` block, because neither can be\n * trusted from a projection. It does carry its document's `etag` (#467, D14), a version rather than\n * a judgement, so a delete from the list says which version it removes.\n */\nexport interface QueryResultItem {\n /** Never null and unique within a result; the server refuses anything else. */\n id: string;\n /** What to show when the row is named rather than tabulated (a reference picker's value). */\n breadcrumb?: string;\n /**\n * The document's change vector when the list loaded (#467, D14). A delete sends it back; a row\n * changed since is a 409. Absent for a row with no document behind it.\n */\n etag?: string;\n values: QueryResultItemValue[];\n /** Presentation hints for the whole row. Keys arrive lower-cased. */\n typeHints?: Record<string, string>;\n}\n\n/** One cell. */\nexport interface QueryResultItemValue {\n /** The {@link QueryColumn.name} this value belongs to. */\n key: string;\n /** Typed JSON, not a string — the server converts on the way out. */\n value?: any;\n /** For a reference cell: the target document's id, so a link needs no second lookup. */\n objectId?: string;\n /** For a reference cell: the target's display string. */\n breadcrumb?: string;\n /** Resolved display labels per referenced id, for a multi-reference cell. */\n breadcrumbs?: Record<string, string | null>;\n typeHints?: Record<string, string>;\n}\n\n/**\n * Every shape a renderer's `item` can arrive in.\n *\n * Three, and they are genuinely different objects rather than variations of one: a query grid hands\n * a {@link QueryResultItem}, an AsDetail sub-table hands the flat record `nestedPoToDisplayRow`\n * built, and a detail or form host hands the `PersistentObject` itself. A renderer reused across a\n * grid and an AsDetail table therefore sees two of them, which is why {@link valueFor} reads all\n * three rather than making every app write the branch.\n */\nexport type SparkRow = QueryResultItem | PersistentObject | Record<string, unknown>;\n\n/**\n * The value a row carries for a column, whatever shape the row is in — or `undefined` when it\n * carries none.\n *\n * ```ts\n * const isPrivate = valueFor(this.item(), 'IsPrivate')?.value === true;\n * ```\n *\n * Always returns the **cell** rather than the bare value, because a reference cell is worth as much\n * for its `objectId` as for its text, and a helper that discarded it would have to be replaced the\n * first time a renderer wanted to link somewhere. `?.value` is the cost, once per call site.\n *\n * ⚠️ A value only reaches a grid row if its attribute is on the **query surface**, which also makes it a\n * column: there is no shipped-but-not-drawn value (#264). An\n * attribute marked `\"showedOn\": \"PersistentObject\"` is absent from a row by design, and this returns\n * `undefined` for it; that is the first thing to check when a sibling read comes back empty.\n */\nexport function valueFor(item: SparkRow | null | undefined, columnName: string): QueryResultItemValue | undefined {\n if (!item) return undefined;\n\n if (isQueryRow(item)) {\n return item.values.find(v => v.key === columnName);\n }\n\n if (isPersistentObject(item)) {\n const attribute = item.attributes.find(a => a.name === columnName);\n if (!attribute) return undefined;\n return {\n key: attribute.name,\n value: attribute.value,\n // A single reference's value IS the target's id, which is what the row shape carries\n // separately. Deriving it here keeps a renderer from having to know that.\n objectId: attribute.dataType === 'Reference' && !attribute.isArray && attribute.value != null\n ? String(attribute.value)\n : undefined,\n breadcrumb: attribute.breadcrumb,\n breadcrumbs: attribute.breadcrumbs,\n };\n }\n\n const record = item as Record<string, unknown>;\n if (!(columnName in record)) return undefined;\n\n const value = record[columnName];\n // The flat row's side channel is populated only for single references, so its presence for this\n // key is what identifies one — the record itself carries no dataType to ask.\n const breadcrumb = (record[AS_DETAIL_BREADCRUMBS_KEY] as Record<string, string> | undefined)?.[columnName];\n\n return {\n key: columnName,\n value,\n objectId: breadcrumb !== undefined && value != null ? String(value) : undefined,\n breadcrumb,\n };\n}\n\n/**\n * Whether this row is a query result item.\n *\n * Discriminated on the *elements*, not just on `Array.isArray(values)`: a flat AsDetail record may\n * legitimately have a column named `values` holding an array, and mistaking one for the other would\n * silently read the wrong thing.\n */\nexport function isQueryRow(row: SparkRow | null | undefined): row is QueryResultItem {\n const values = (row as QueryResultItem | undefined)?.values;\n return Array.isArray(values) && (values.length === 0 || typeof values[0]?.key === 'string');\n}\n\n/** Whether this row is a full persistent object. Discriminated on elements, as above. */\nexport function isPersistentObject(row: SparkRow | null | undefined): row is PersistentObject {\n const attributes = (row as PersistentObject | undefined)?.attributes;\n return Array.isArray(attributes) && (attributes.length === 0 || typeof attributes[0]?.name === 'string');\n}\n\n/**\n * One column's value filter (#431): the values a caller picked, or the values they excluded.\n *\n * **Values, never labels.** Each entry is the raw stored value — the same `value` the distinct\n * endpoint returned, never the `label` beside it. A reference column's values are document ids while\n * its labels are resolved breadcrumb text, and two rows can legitimately render the same text, so\n * matching on labels would be both wrong and ambiguous.\n *\n * `includes` and `excludes` are the two halves of the inverse toggle rather than a flag: the panel\n * moves values between them. `null` is a real, selectable value meaning \"no value\"; an empty or\n * absent array is the absent filter.\n */\nexport interface QueryColumnFilter {\n name: string;\n includes?: unknown[];\n excludes?: unknown[];\n}\n\n/**\n * One selectable entry in a column's filter panel.\n *\n * Structurally identical to the datatable's own `DistinctValue`, deliberately — it is what lets the\n * HTTP response be handed straight to `[distincts]` with no mapper in between.\n */\nexport interface DistinctValue {\n /** The raw stored value. `null` is a real entry meaning \"no value\", not an absent one. */\n value: unknown;\n /** What the user reads. Never matched on. */\n label: string;\n}\n\n/**\n * A column's distinct values.\n *\n * `remaining` is always empty from this server: the datatable snapshots the list when a selection is\n * first made and derives both buckets itself by diffing. The server answers \"what matches now\".\n *\n * `hasMore` reports that the cap truncated the list, and the panel re-queries only when it is set or\n * the search term widens — so a dishonest `false` strands a user typing past the cap.\n */\nexport interface DistinctValuesResult {\n matching: DistinctValue[];\n remaining: DistinctValue[];\n hasMore: boolean;\n}\n","import { TranslatedString } from './translated-string';\n\nexport enum ELookupDisplayType {\n Dropdown = 0,\n Modal = 1\n}\n\nexport interface LookupReferenceListItem {\n name: string;\n isTransient: boolean;\n valueCount: number;\n displayType: ELookupDisplayType;\n}\n\nexport interface LookupReference {\n name: string;\n isTransient: boolean;\n displayType: ELookupDisplayType;\n values: LookupReferenceValue[];\n}\n\nexport interface LookupReferenceValue {\n key: string;\n values: TranslatedString;\n isActive: boolean;\n extra?: Record<string, unknown>;\n}\n","import { isReservedAsDetailKey } from './as-detail-conversions';\nimport { PersistentObject } from './persistent-object';\nimport { PersistentObjectAttribute } from './persistent-object-attribute';\n\n/**\n * The one answer to \"do these two reads hold the same content?\", shared by History's revision diff\n * and po-edit's three-way conflict merge.\n *\n * Two reads of identical content are NOT byte-identical: their envelopes carry per-read fields\n * (etags, breadcrumbs, row keys, the wire values a flattened row keeps for its date cells) that\n * differ between reads, and comparing them reports every AsDetail attribute as changed. Both\n * functions below reduce a value to its content first. They come in two shapes because the two\n * callers hold two shapes: History compares wire attributes, po-edit compares the flat form dict.\n */\n\n/**\n * A comparable form of a wire attribute's content.\n *\n * Nested objects (AsDetail rows, references rendered as objects) are reduced to their attributes'\n * values: their envelopes carry per-read fields (etags, breadcrumbs, row keys) that differ between two\n * reads of identical content and would report every AsDetail attribute as changed.\n */\nexport function comparableValue(attribute: PersistentObjectAttribute | undefined): string {\n if (!attribute) return '\\u0000absent';\n return JSON.stringify({\n v: attribute.value ?? null,\n o: attribute.object ? nestedValues(attribute.object) : null,\n os: attribute.objects ? attribute.objects.map(nestedValues) : null,\n });\n}\n\nfunction nestedValues(po: PersistentObject): Record<string, string> {\n const result: Record<string, string> = {};\n for (const a of po.attributes ?? []) result[a.name] = comparableValue(a);\n return result;\n}\n\n/**\n * A comparable form of a value in the flat form dict ({@link nestedPoToDict}'s shape).\n *\n * The reserved keys a flattened row carries (its row key, its breadcrumb, the wire values of its\n * dates) are dropped, and object keys are sorted, so two flattenings of the same content compare\n * equal whatever order their attributes arrived in. `undefined` and `null` are the same absence.\n */\nexport function comparableFormValue(value: unknown): string {\n return JSON.stringify(normalizeFormValue(value));\n}\n\n/** Whether two form-dict values hold the same content (see {@link comparableFormValue}). */\nexport function formValuesEqual(a: unknown, b: unknown): boolean {\n return comparableFormValue(a) === comparableFormValue(b);\n}\n\nfunction normalizeFormValue(value: unknown): unknown {\n if (value === undefined || value === null) return null;\n if (Array.isArray(value)) return value.map(normalizeFormValue);\n if (typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value as object).sort()) {\n if (isReservedAsDetailKey(key)) continue;\n result[key] = normalizeFormValue((value as Record<string, unknown>)[key]);\n }\n return result;\n }\n return value;\n}\n","import { CustomActionDefinition } from './custom-action';\nimport { resolveTranslation } from './translated-string';\n\n/**\n * The prompt to show before running `action` on `count` rows, with `{count}` substituted, or null\n * when the action asks for no confirmation (#467, D7).\n */\nexport function confirmationText(action: CustomActionDefinition | undefined | null, count: number, lang?: string): string | null {\n const text = resolveTranslation(action?.confirmation, lang);\n return text ? text.replace(/\\{count\\}/g, String(count)) : null;\n}\n\n/** The built-in action named `name` (`New`, `Edit`, `Delete`) in `actions`, when the server listed it. */\nexport function builtInAction(actions: CustomActionDefinition[], name: 'New' | 'Edit' | 'Delete'): CustomActionDefinition | undefined {\n return actions.find(a => !!a.isDefault && a.name.toLowerCase() === name.toLowerCase());\n}\n\n/** Whether `showedOn` offers the action on a query: `\"query\"` or `\"both\"`, in any casing. */\nexport function isShowedOnQuery(showedOn: string | undefined | null): boolean {\n const value = (showedOn ?? '').trim().toLowerCase();\n return value === 'query' || value === 'both';\n}\n\n/** Whether `showedOn` offers the action on a detail page: `\"detail\"` or `\"both\"`, in any casing. */\nexport function isShowedOnDetail(showedOn: string | undefined | null): boolean {\n const value = (showedOn ?? '').trim().toLowerCase();\n return value === 'detail' || value === 'both';\n}\n\n/**\n * The custom actions a query should offer, from the entity type's full set.\n *\n * `showedOn` must include the query side. The accepted values are `\"detail\"`, `\"query\"`\n * and `\"both\"` — as the server model and the custom-actions guide have always\n * documented — compared case-insensitively (#460 M15: `\"Both\"` used to render nowhere).\n *\n * The built-in New, Edit and Delete (`isDefault`, #460 D18, #467 D7) are left out: they are not executed as\n * custom actions, and the grid offers them itself (see {@link defaultQueryActions}).\n *\n * ⚠️ This narrows what is DISPLAYED. It is NOT an authorization boundary: the grant\n * is, and it is enforced independently in `ExecuteCustomAction` regardless of which\n * query the caller clicked from — a caller can always POST directly.\n */\nexport function filterQueryActions(\n actions: CustomActionDefinition[],\n): CustomActionDefinition[] {\n return actions.filter(a => !a.isDefault && isShowedOnQuery(a.showedOn));\n}\n\n/** The custom actions a detail page should offer — the detail-side counterpart of {@link filterQueryActions}. */\nexport function filterDetailActions(\n actions: CustomActionDefinition[],\n): CustomActionDefinition[] {\n return actions.filter(a => !a.isDefault && isShowedOnDetail(a.showedOn));\n}\n\n/**\n * The built-in New, Edit and Delete the server listed for this caller (#460 D18, #467 D7), when their `showedOn`\n * includes the query side. Present only when the caller holds `New/T`, `Edit/T` or `Delete/T`.\n */\nexport function defaultQueryActions(\n actions: CustomActionDefinition[],\n): CustomActionDefinition[] {\n return actions.filter(a => !!a.isDefault && isShowedOnQuery(a.showedOn));\n}\n","/**\n * Parses a custom action's `selectionRule` — a cardinality expression over the number\n * of selected rows — into a predicate.\n *\n * A port of the server's `SelectionRuleParser`, and the two MUST agree: they are tested\n * against one shared fixture (`selection-rule.fixture.json`) for exactly this reason.\n * Vidyano, where this grammar comes from, has the same algorithm in C# and JavaScript and\n * the two have already drifted — one throws on a non-numeric operand where the other\n * silently permits everything.\n *\n * Grammar: `X` is the count placeholder, whitespace is insignificant, terms split on `X`\n * are AND-combined (`1<X<5` is a range), operators are `<= >= < > != =` matched in that\n * order so `>=` is never read as `>`, and a number-first term is mirrored (`0<X` is `>0`).\n *\n * Client-side this only drives whether a button is disabled. The server enforces the same\n * rule independently — and neither is an authorization boundary: the action's grant is.\n */\nexport function parseSelectionRule(rule?: string | null): (count: number) => boolean {\n if (!rule || !rule.trim()) return () => true;\n try {\n return compile(rule);\n } catch {\n // Unlike the server, which refuses to start on a malformed rule, the client cannot\n // usefully fail: the rule arrived over the wire from a server that already validated\n // it. Disabling the button is the safe direction — it never permits an action the\n // server would refuse.\n return () => false;\n }\n}\n\nconst OPERATORS = ['<=', '>=', '<', '>', '!=', '='] as const;\n\nfunction compile(rule: string): (count: number) => boolean {\n const normalized = rule.replace(/ /g, '').toUpperCase();\n const terms = normalized.split('X').filter(t => t.length > 0);\n if (terms.length === 0) throw new Error(`Selection rule '${rule}' has no condition.`);\n\n const numberFirst = !normalized.startsWith('X') && normalized.includes('X');\n const predicates = terms.map((term, i) => compileTerm(term, rule, numberFirst && i === 0));\n\n return count => predicates.every(p => p(count));\n}\n\nfunction compileTerm(term: string, rule: string, mirrored: boolean): (count: number) => boolean {\n const op = OPERATORS.find(o => (mirrored ? term.endsWith(o) : term.startsWith(o)));\n if (!op) throw new Error(`Selection rule '${rule}' has no recognised operator in '${term}'.`);\n\n const numberPart = mirrored ? term.slice(0, term.length - op.length) : term.slice(op.length);\n // Number(' ') is 0 and Number('1.5') is 1.5 — neither is a valid operand here.\n if (!/^-?\\d+$/.test(numberPart)) {\n throw new Error(`Selection rule '${rule}' has a non-numeric operand in '${term}'.`);\n }\n const value = Number(numberPart);\n const effective = mirrored ? mirror(op) : op;\n\n switch (effective) {\n case '<=': return count => count <= value;\n case '>=': return count => count >= value;\n case '<': return count => count < value;\n case '>': return count => count > value;\n case '!=': return count => count !== value;\n case '=': return count => count === value;\n default: throw new Error(`Selection rule '${rule}' has an unsupported operator '${effective}'.`);\n }\n}\n\nfunction mirror(op: string): string {\n switch (op) {\n case '<': return '>';\n case '>': return '<';\n case '<=': return '>=';\n case '>=': return '<=';\n default: return op;\n }\n}\n","import { CustomActionDefinition } from './custom-action';\nimport { parseSelectionRule } from './selection-rule';\n\n/**\n * Whether a grid lets the user tick rows — what `<bs-datatable>` renders. There is no single\n * selection (#467, D10): a selectable list is a checkbox list, and actions whose rule does not\n * match the count are disabled rather than the selection trimmed.\n */\nexport type SparkSelectionMode = 'none' | 'multiple';\n\n/**\n * What a query definition (or a sub-query entry) declares (#460 D17): a fixed mode, or `'auto'` —\n * the default, and what an absent field means — to derive it from the actions offered.\n */\nexport type SparkSelectionModeSetting = 'auto' | SparkSelectionMode;\n\n/** The largest selection the server accepts in one request (`SparkDefaultActions.MaxSelectedItems`). */\nexport const SPARK_MAX_SELECTED_ITEMS = 200;\n\n/**\n * Whether an action needs ticked rows (#467, R1(4)): it has a rule, and the rule accepts at least\n * one row. `=0` accepts none, so an action that runs only without a selection never makes a list\n * selectable; an action without a rule acts on the query, not on rows.\n */\nexport function needsSelection(action: CustomActionDefinition): boolean {\n if (!action.selectionRule?.trim()) return false;\n const rule = parseSelectionRule(action.selectionRule);\n for (let count = 1; count <= SPARK_MAX_SELECTED_ITEMS; count++)\n if (rule(count)) return true;\n return false;\n}\n\n/**\n * The selection mode a grid renders (#467, R1).\n *\n * A declared `'none'` or `'multiple'` wins outright. `'auto'` (or nothing) derives it from\n * `actions`, which must already be the **effective** list for this result — listed for the caller\n * (the right), shown on queries, not withheld by `disabledActions` and not hidden by the recycle\n * bin. Built-in Edit and Delete count like any other action. `'multiple'` when at least one of them\n * {@link needsSelection}, else `'none'`, so a read-only user sees a list with no checkbox column.\n */\nexport function selectionModeFor(\n actions: CustomActionDefinition[],\n declared?: SparkSelectionModeSetting | null,\n): SparkSelectionMode {\n if (declared && declared !== 'auto') return declared;\n return actions.some(needsSelection) ? 'multiple' : 'none';\n}\n","import { SparkSelectionModeSetting } from './selection-mode';\n\n/**\n * One sub-query of a type, with the overrides the parent's type states for it (#460, D17).\n */\nexport interface SparkSubQuery {\n /** The query's alias or id. */\n query: string;\n /** Overrides the query's own `selectionMode` on this parent's detail page. */\n selectionMode?: SparkSelectionModeSetting;\n /** The row attribute pointing at the parent, when the row type has several (server-side). */\n parentReference?: string;\n}\n\n/**\n * An entry of `EntityType.queries` as it arrives on the wire: a bare alias — the shape every model\n * had before #460 M15, and still what the server sends for an entry without overrides — or the\n * object form.\n */\nexport type SparkSubQueryEntry = string | SparkSubQuery;\n\n/** One entry in its object form, whichever shape it arrived in. */\nexport function normalizeSubQuery(entry: SparkSubQueryEntry): SparkSubQuery {\n return typeof entry === 'string' ? { query: entry } : entry;\n}\n\n/** A type's sub-queries in object form; empty when it declares none. */\nexport function subQueriesOf(type: { queries?: SparkSubQueryEntry[] } | null | undefined): SparkSubQuery[] {\n return (type?.queries ?? []).map(normalizeSubQuery).filter(entry => !!entry.query);\n}\n","import { EntityAttributeDefinition } from './entity-type';\nimport { PersistentObject } from './persistent-object';\nimport { ShowedOn } from './showed-on';\nimport { ValidationRule } from './validation-rule';\n\n/**\n * One selectable value, as replaced by a refresh hook. Mirrors the server's\n * `PersistentObjectAttributeOption`.\n */\nexport interface RefreshedOption {\n key: string;\n label?: Record<string, string>;\n}\n\n/**\n * What a refresh changed about one attribute's *presentation*.\n *\n * Kept separate from `EntityType` on purpose. The form's option loading hangs off a single effect\n * keyed on `entityType` identity, and `SparkService` caches nothing — so applying a refresh by\n * setting a new `EntityType` re-issues every reference query, every lookup fetch, a full\n * `getEntityTypes()` and a `getPermissions()` per array-AsDetail attribute, on every refresh.\n * Mutating the existing object instead is inert, because the rendering computed would not re-run.\n * An overlay is the only shape that is both reactive and free.\n */\nexport interface AttributeOverlay {\n isRequired?: boolean;\n isReadOnly?: boolean;\n /** Where the attribute is drawn for this object; an action sets it on load, new or refresh (#264). */\n showedOn?: ShowedOn | string;\n rules?: ValidationRule[];\n query?: string;\n /** `undefined` means the hook did not touch the options; an empty array means there are none. */\n options?: RefreshedOption[];\n}\n\nexport type RefreshOverlay = Record<string, AttributeOverlay>;\n\n/** Applies an overlay to one attribute definition, returning a new object when anything changed. */\nexport function applyOverlay(\n attr: EntityAttributeDefinition,\n overlay: AttributeOverlay | undefined,\n): EntityAttributeDefinition {\n if (!overlay) return attr;\n\n return {\n ...attr,\n isRequired: overlay.isRequired ?? attr.isRequired,\n isReadOnly: overlay.isReadOnly ?? attr.isReadOnly,\n showedOn: overlay.showedOn ?? attr.showedOn,\n rules: overlay.rules ?? attr.rules,\n query: overlay.query ?? attr.query,\n };\n}\n\n/**\n * Reads a refresh response into an overlay — or a loaded or new object, whose attributes carry the same\n * server-owned presentation: what `OnLoadAsync`/`OnNewAsync` set (`showedOn`, `isRequired`, `isReadOnly`) applies\n * from the first render, not only after the first refresh (#264, G5/G7).\n *\n * Everything here is presentation the server owns outright, so it is taken verbatim — there is no\n * merging to do on this half, only on values.\n */\nexport function overlayFromResponse(response: PersistentObject): RefreshOverlay {\n const overlay: RefreshOverlay = {};\n\n for (const attr of response.attributes ?? []) {\n overlay[attr.name] = {\n isRequired: attr.isRequired,\n isReadOnly: attr.isReadOnly,\n showedOn: attr.showedOn,\n rules: attr.rules ?? [],\n query: attr.query,\n options: (attr as { options?: RefreshedOption[] | null }).options ?? undefined,\n };\n }\n\n return overlay;\n}\n\n/**\n * Merges a refresh response's values into the live form.\n *\n * The rule, and the reason for it: a refresh is not instant, and the user keeps typing during it —\n * the form is deliberately never frozen. So for each attribute we ask whether the *server* changed\n * it, by comparing the response against the values that were **sent**, not against what is on\n * screen now.\n *\n * - server value equals what we sent → the hook did not touch it, so whatever is in the form now\n * wins, including anything typed while the request was in flight;\n * - server value differs → the hook deliberately changed it, and it wins over a concurrent edit.\n *\n * Comparing against the displayed value instead is the classic \"refresh eats my typing\" bug;\n * refusing to overwrite anything the user touched is the equally wrong opposite, where a dependent\n * field the hook computed never appears.\n *\n * @param sent values as they were POSTed, captured before the request left\n * @param current values as they are now, which may have moved on\n * @param response the reshaped object\n */\nexport function mergeRefreshValues(\n sent: Record<string, any>,\n current: Record<string, any>,\n response: PersistentObject,\n): Record<string, any> {\n const merged = { ...current };\n\n for (const attr of response.attributes ?? []) {\n const serverValue = attr.value ?? null;\n const sentValue = sent[attr.name] ?? null;\n\n if (!valuesEqual(serverValue, sentValue)) {\n merged[attr.name] = attr.value;\n }\n }\n\n return merged;\n}\n\n/**\n * Structural for arrays, `===` otherwise.\n *\n * Multi-reference attributes hold `string[]`, and a fresh array of the same ids is a different\n * object — so reference equality would report every one of them as \"changed by the server\" on\n * every refresh, and clobber in-flight edits to them.\n */\nfunction valuesEqual(a: any, b: any): boolean {\n if (a === b) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((item, i) => valuesEqual(item, b[i]));\n }\n // Values arrive off the wire as JSON scalars; anything else compares by JSON shape rather than\n // by identity, which is what a caller means by \"did this change\".\n if (a !== null && b !== null && typeof a === 'object' && typeof b === 'object') {\n return JSON.stringify(a) === JSON.stringify(b);\n }\n return false;\n}\n","import { ValidationRule } from './validation-rule';\nimport { TranslatedString } from './translated-string';\n\n/** One rule failure, in the shape the form already renders per field. */\nexport interface RuleFailure {\n attributeName: string;\n ruleType: string;\n message: string;\n}\n\nexport interface EvaluableAttribute {\n name: string;\n label?: TranslatedString;\n isRequired?: boolean;\n rules?: ValidationRule[];\n}\n\nconst EMAIL = /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/i;\nconst URL = /^https?:\\/\\/[^\\s]+$/i;\n\nfunction isEmpty(value: any): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === 'string') return value.trim() === '';\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * Evaluates an attribute's rules against a value, mirroring the server's `ValidationService`.\n *\n * ⚠️ **Parity with the server is the point, and disagreement is worse than silence.** A client that\n * rejects something the server would accept blocks legitimate work with no recourse; one that\n * accepts something the server rejects merely defers the error to the round-trip, which is where it\n * used to live anyway. So the rule set here is deliberately limited to the types the server\n * implements, and an unrecognised rule type is ignored rather than guessed at.\n *\n * This exists because a refresh hook that imposes a rule needs it to bite before Save — previously\n * `rules` was carried on the wire and never evaluated in the browser at all.\n */\nexport function evaluateRules(attr: EvaluableAttribute, value: any): RuleFailure[] {\n const failures: RuleFailure[] = [];\n const label = attr.label?.['en'] ?? attr.name;\n\n if (attr.isRequired && isEmpty(value)) {\n return [{ attributeName: attr.name, ruleType: 'required', message: `${label} is required` }];\n }\n\n // A rule other than \"required\" says nothing about an absent value — that is what required is for.\n if (isEmpty(value)) return failures;\n\n const text = value?.toString() ?? '';\n\n for (const rule of attr.rules ?? []) {\n const failure = evaluateRule(attr.name, label, rule, value, text);\n if (failure) failures.push(failure);\n }\n\n return failures;\n}\n\nfunction evaluateRule(\n name: string,\n label: string,\n rule: ValidationRule,\n value: any,\n text: string,\n): RuleFailure | null {\n const message = rule.message?.['en'];\n\n switch (rule.type?.toLowerCase()) {\n case 'maxlength': {\n const max = toInt(rule.value);\n if (max === null || text.length <= max) return null;\n return { attributeName: name, ruleType: 'maxLength', message: message ?? `${label} must be at most ${max} characters` };\n }\n case 'minlength': {\n const min = toInt(rule.value);\n if (min === null || text.length >= min) return null;\n return { attributeName: name, ruleType: 'minLength', message: message ?? `${label} must be at least ${min} characters` };\n }\n case 'range': {\n const numeric = toNumber(value);\n if (numeric === null) return null;\n if (rule.min !== undefined && rule.min !== null && numeric < rule.min) {\n return { attributeName: name, ruleType: 'range', message: message ?? `${label} must be at least ${rule.min}` };\n }\n if (rule.max !== undefined && rule.max !== null && numeric > rule.max) {\n return { attributeName: name, ruleType: 'range', message: message ?? `${label} must be at most ${rule.max}` };\n }\n return null;\n }\n case 'regex': {\n const pattern = rule.value?.toString();\n if (!pattern) return null;\n let re: RegExp;\n try {\n re = new RegExp(pattern);\n } catch {\n // An unparseable pattern is a server-side authoring bug. Staying silent leaves the server to\n // report it, which is strictly better than blocking the user over a rule nobody can evaluate.\n return null;\n }\n return re.test(text) ? null : { attributeName: name, ruleType: 'regex', message: message ?? `${label} is not in the expected format` };\n }\n case 'email':\n return EMAIL.test(text) ? null : { attributeName: name, ruleType: 'email', message: message ?? `${label} must be a valid email address` };\n case 'url':\n return URL.test(text) ? null : { attributeName: name, ruleType: 'url', message: message ?? `${label} must be a valid URL` };\n default:\n return null;\n }\n}\n\n/**\n * Mirrors the server's `TryGetIntValue`: an integer number, or a string `int.TryParse` accepts.\n * Anything else means \"no rule\". `Number(value)` is NOT that — it turns null and '' into 0 (so every\n * non-empty value failed \"must be at most 0 characters\") and truncated 2.5, which the server ignores.\n */\nfunction toInt(value: any): number | null {\n if (typeof value === 'number') return Number.isInteger(value) ? value : null;\n if (typeof value === 'string' && /^\\s*[+-]?\\d+\\s*$/.test(value)) return parseInt(value, 10);\n return null;\n}\n\nfunction toNumber(value: any): number | null {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n}\n"],"mappings":"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"}
|
|
@@ -1091,7 +1091,7 @@ var SparkReviewQueueLinkComponent = class SparkReviewQueueLinkComponent {
|
|
|
1091
1091
|
constructor() {
|
|
1092
1092
|
effect(() => {
|
|
1093
1093
|
this.moderation.ownReputationChanged();
|
|
1094
|
-
this.moderation.ownReputation().then((r) => this.canReview.set(r
|
|
1094
|
+
this.moderation.ownReputation().then((r) => this.canReview.set(r?.canReview === true), () => this.canReview.set(false));
|
|
1095
1095
|
});
|
|
1096
1096
|
}
|
|
1097
1097
|
static ɵfac = i0.ɵɵngDeclareFactory({
|