ngx-dynamic-entity 1.13.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Type, EnvironmentProviders, WritableSignal, OnInit, OnChanges, OnDestroy, EventEmitter, SimpleChanges } from '@angular/core';
3
- import { ReferenceOption as ReferenceOption$1, LocalizedText, EntityReferenceLoader as EntityReferenceLoader$1, LookupListSource as LookupListSource$1, RecordMigration as RecordMigration$1, FieldValidators, NestedFieldConfig, ReferenceLoaderContext, EntityFormConfig, DropdownOption, LookupListValue, NestedTabConfig, FormRule, RuleEvaluationResult as RuleEvaluationResult$1 } from '@dynamic-entity/core';
2
+ import { InjectionToken, Type, EnvironmentProviders, WritableSignal, OnInit, OnChanges, OnDestroy, EventEmitter, SimpleChanges, Provider } from '@angular/core';
3
+ import { ReferenceOption, LocalizedText, EntityFormConfig, FormRule, ImportLookups, MappingPlan, ImportResult, TemplateSpec, SheetData, CommonModuleEntry, EntityReferenceLoader, LookupListSource, RecordMigration, FileUploadHandler, FieldValidators, NestedFieldConfig, ReferenceLoaderContext, DropdownOption, LookupListValue, NestedTabConfig, RuleEvaluationResult, FieldScopeEntry, EvaluateFormRulesOptions, ImportColumn, UnsupportedColumn, ImportRowError } from '@dynamic-entity/core';
4
4
  export { AutoPatchConfig, CommonModuleEntry, ComponentClass, DropdownOption, EntityFormConfig, EntityPermissions, EntityReferenceConfig, EntityReferenceLoader, FieldTableConfig, FieldValidators, FileRef, FormRule, LocalizedText, NestedFieldConfig, NestedTabConfig, PatchOnTrueMapping, RbacContext, ReferenceLoaderContext, ReferenceOption, RichFieldType, RuleAction, RuleCondition, RuleEvaluationResult, VersionedRecord, formatDisplayValue, normalizeConfig, normalizeOption, resolveLabel, resolveOptionLabel, resolveOptionValue, valuesMatch } from '@dynamic-entity/core';
5
- import { ValidatorFn, AsyncValidatorFn, ValidationErrors, FormGroup, FormArray, AbstractControl } from '@angular/forms';
5
+ import { ValidatorFn, AsyncValidatorFn, AbstractControl, ValidationErrors, FormGroup, FormArray } from '@angular/forms';
6
6
  import * as rxjs from 'rxjs';
7
+ import * as ngx_dynamic_entity from 'ngx-dynamic-entity';
7
8
 
8
9
  /**
9
10
  * Backing store for cached reference options.
@@ -15,8 +16,8 @@ import * as rxjs from 'rxjs';
15
16
  * that must not silently persist a tenant's data.
16
17
  */
17
18
  interface EntityRefCacheStore {
18
- get(key: string): ReferenceOption$1[] | undefined;
19
- set(key: string, value: ReferenceOption$1[]): void;
19
+ get(key: string): ReferenceOption[] | undefined;
20
+ set(key: string, value: ReferenceOption[]): void;
20
21
  delete(key: string): void;
21
22
  /** Every key currently held — used for prefix invalidation of one entity. */
22
23
  keys(): string[];
@@ -25,8 +26,8 @@ interface EntityRefCacheStore {
25
26
  /** Default in-memory store. Lives as long as the injector that created it. */
26
27
  declare class InMemoryEntityRefCacheStore implements EntityRefCacheStore {
27
28
  private readonly map;
28
- get(key: string): ReferenceOption$1[] | undefined;
29
- set(key: string, value: ReferenceOption$1[]): void;
29
+ get(key: string): ReferenceOption[] | undefined;
30
+ set(key: string, value: ReferenceOption[]): void;
30
31
  delete(key: string): void;
31
32
  keys(): string[];
32
33
  clear(): void;
@@ -95,6 +96,40 @@ declare const DEFAULT_UI_TEXT: {
95
96
  criticalFieldUnlockHint: string;
96
97
  dismissBanner: string;
97
98
  jumpToField: string;
99
+ importUpload: string;
100
+ importMap: string;
101
+ importReview: string;
102
+ importDone: string;
103
+ importChooseFile: string;
104
+ importFileChosen: string;
105
+ importReading: string;
106
+ importNeedFile: string;
107
+ importTemplateHeading: string;
108
+ importTemplateExplain: string;
109
+ importTemplateDownload: string;
110
+ importTemplateSelectAll: string;
111
+ importTemplateSelectNone: string;
112
+ importTemplateUnsupported: string;
113
+ importColumnInFile: string;
114
+ importTargetField: string;
115
+ importUnmapped: string;
116
+ importGuessed: string;
117
+ importMappedCount: string;
118
+ importRequiredUnmapped: string;
119
+ importPreviewHeading: string;
120
+ importRowRejected: string;
121
+ importRunImport: string;
122
+ importImporting: string;
123
+ importSucceeded: string;
124
+ importSkippedRows: string;
125
+ importFailedRows: string;
126
+ importErrorsTruncated: string;
127
+ importStartOver: string;
128
+ importErrorsHeading: string;
129
+ importErrorRow: string;
130
+ importErrorField: string;
131
+ importErrorReason: string;
132
+ importPlanProblem: string;
98
133
  entityLabel: string;
99
134
  criticalFieldChanged: string;
100
135
  recordModified: string;
@@ -162,6 +197,102 @@ declare class UiTextService {
162
197
  static ɵprov: i0.ɵɵInjectableDeclaration<UiTextService>;
163
198
  }
164
199
 
200
+ /**
201
+ * import-contracts.ts — the two seams the import wizard hangs on.
202
+ *
203
+ * Both follow the pattern the rest of this package already uses for anything the library
204
+ * cannot decide for a consumer — `FileUploadHandler`, `EntityReferenceLoader`,
205
+ * `LookupListLoader`: a function type here, a token to register it against, and a default
206
+ * that works without registering anything.
207
+ *
208
+ * **Why two rather than one.** Reading a file and deciding where the work happens are
209
+ * different questions with different answers. A consumer may want `.xlsx` support in a
210
+ * wizard that still runs entirely in the browser, or plain CSV that is nonetheless processed
211
+ * on a server because their files have fifty thousand rows. Collapsing them into one option
212
+ * would make each answer imply the other.
213
+ */
214
+
215
+ /**
216
+ * Reads an uploaded file into headers and positional rows.
217
+ *
218
+ * Positional (`string[][]`), not keyed by header, because a real sheet carries duplicate
219
+ * headers and blank ones — and because a mapping entry addresses a column by index for
220
+ * exactly that reason. A parser that returned objects keyed by header would silently lose
221
+ * the second of two columns both called "Notes".
222
+ *
223
+ * The library ships a CSV implementation and no more. Registering one that understands
224
+ * `.xlsx` is how a consumer adds it, and which spreadsheet library they use stays their
225
+ * choice — this package has no runtime dependency and gains none here.
226
+ *
227
+ * @example
228
+ * provideNgxDynamicEntity({
229
+ * sheetParser: async file => {
230
+ * const workbook = XLSX.read(await file.arrayBuffer());
231
+ * const rows = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], {
232
+ * header: 1, raw: false, defval: '',
233
+ * }) as string[][];
234
+ * return { headers: rows[0] ?? [], rows: rows.slice(1) };
235
+ * },
236
+ * })
237
+ */
238
+ type SheetParser = (file: File) => SheetData | Promise<SheetData>;
239
+ /** The formats a generated template can be written in. CSV needs no dependency. */
240
+ type TemplateFormat = 'csv' | 'xlsx';
241
+ /** What the wizard needs to know about the entity it is importing into. */
242
+ interface ImportContext {
243
+ config: EntityFormConfig;
244
+ /**
245
+ * Rules that apply to this entity.
246
+ *
247
+ * Without them an import checks field validators only, which is **not** what the form
248
+ * enforces: a rule can attach an error, and a rule can hide a field, which must relax
249
+ * `required`. Pass the same rules the form is rendered with or the two disagree.
250
+ */
251
+ rules?: readonly FormRule[];
252
+ /** Values for any `listName` field, already resolved — core cannot reach `LOOKUP_REGISTRY`. */
253
+ lookups?: ImportLookups;
254
+ lang?: string;
255
+ }
256
+ /** What a first look at an uploaded file yields. */
257
+ interface ImportPreview {
258
+ headers: string[];
259
+ /** The first few rows, for the mapping screen to show under each column. */
260
+ sample: string[][];
261
+ /** Where the suggester got to. Every inferred match is tagged `guess`. */
262
+ suggestion: MappingPlan;
263
+ /** Total data rows in the file, which may be far more than `sample` holds. */
264
+ rowCount: number;
265
+ }
266
+ /**
267
+ * Where the work happens.
268
+ *
269
+ * The default implementation does everything in the browser with `@dynamic-entity/core` and
270
+ * never touches the network, so the wizard is fully usable with no backend at all. Register
271
+ * an implementation that posts to a server and the same components drive it instead — the
272
+ * engine underneath is the same code either way, which is the point of it being pure.
273
+ *
274
+ * This is the real answer to "should parsing happen on the client or the server?": both,
275
+ * chosen at registration, without a second implementation of the mapping rules.
276
+ */
277
+ interface ImportTransport {
278
+ /** Headers, a sample, and a suggested mapping for a freshly chosen file. */
279
+ preview(file: File, context: ImportContext): Promise<ImportPreview>;
280
+ /** Apply a plan to the whole file. */
281
+ commit(file: File, plan: MappingPlan, context: ImportContext): Promise<ImportResult>;
282
+ /**
283
+ * Render a template for download.
284
+ *
285
+ * `fields` is the selection the user actually made — refs with row numbers stripped, one per
286
+ * choice — and `spec` is that selection already expanded against the config. A transport that
287
+ * renders locally wants the spec; one that asks a server wants the selection, because the
288
+ * server has the config and only the choice needs to cross the wire. Passing both means
289
+ * neither has to reconstruct the other: the HTTP transport used to recover the selection from
290
+ * the expanded spec, which worked only because `stripIndices` happens to be exactly the
291
+ * inverse of the expansion. Optional, so an existing implementation keeps compiling.
292
+ */
293
+ template(spec: TemplateSpec, format: TemplateFormat, context: ImportContext, fields?: readonly string[]): Promise<Blob>;
294
+ }
295
+
165
296
  /** Roles that see XXXXXXXXX for masked fields. Presentational only — enforce authz server-side. */
166
297
  declare const MASKED_ROLES: InjectionToken<string[]>;
167
298
  /**
@@ -279,6 +410,46 @@ declare const COMMON_MODULES_REGISTRY: InjectionToken<CommonModuleEntry[]>;
279
410
  * { provide: UPLOAD_HANDLER, useFactory: () => (file: File) => myUploadService.upload(file) }
280
411
  */
281
412
  declare const UPLOAD_HANDLER: InjectionToken<FileUploadHandler>;
413
+ /**
414
+ * Reads an uploaded spreadsheet into headers and positional rows, for the import wizard.
415
+ *
416
+ * Unregistered, the wizard reads CSV with the dependency-free parser in
417
+ * `@dynamic-entity/core`, so it works with nothing provided. Register one to accept `.xlsx`
418
+ * — which spreadsheet library that uses stays the consumer's choice, and this package keeps
419
+ * having no runtime dependency.
420
+ *
421
+ * Rows come back **positional** (`string[][]`) rather than keyed by header, because a real
422
+ * sheet has duplicate headers and blank ones; a header-keyed row silently loses the second
423
+ * of two columns both called "Notes".
424
+ *
425
+ * @example
426
+ * provideNgxDynamicEntity({
427
+ * sheetParser: async file => {
428
+ * const wb = XLSX.read(await file.arrayBuffer());
429
+ * const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], {
430
+ * header: 1, raw: false, defval: '',
431
+ * }) as string[][];
432
+ * return { headers: rows[0] ?? [], rows: rows.slice(1) };
433
+ * },
434
+ * })
435
+ */
436
+ declare const SHEET_PARSER: InjectionToken<SheetParser>;
437
+ /**
438
+ * Where an import's work happens.
439
+ *
440
+ * Unregistered, everything runs in the browser through `@dynamic-entity/core` and nothing
441
+ * touches the network — the wizard is fully usable with no backend. Register one that posts
442
+ * to a server and the same components drive that instead, over the same pure mapping engine,
443
+ * which is what keeps a client-side import and a server-side one from disagreeing about what
444
+ * they produced.
445
+ *
446
+ * The reason to register one is size: the default reads the whole file into memory, which is
447
+ * right for a sheet somebody assembled and wrong for a fifty-thousand-row export.
448
+ *
449
+ * @example
450
+ * { provide: IMPORT_TRANSPORT, useClass: HttpImportTransport }
451
+ */
452
+ declare const IMPORT_TRANSPORT: InjectionToken<ImportTransport>;
282
453
  /**
283
454
  * Backing store for the entity-reference options cache. Defaults to in-memory.
284
455
  * Provide your own to make the cache outlive a page refresh.
@@ -375,13 +546,13 @@ interface NgxDynamicEntityConfig {
375
546
  * Entity-ref option loaders keyed by entity key.
376
547
  * Each receives `{ parentValue, filters, lang }` and may return an array, Promise, or Observable.
377
548
  */
378
- entityRefs?: Record<string, EntityReferenceLoader$1>;
549
+ entityRefs?: Record<string, EntityReferenceLoader>;
379
550
  /**
380
551
  * Named master lists keyed by list name, for fields that set `listName`.
381
552
  * Each may be the values themselves (array/Promise/Observable) or a loader function — prefer
382
553
  * a loader for anything fetched, so an unused list is never loaded.
383
554
  */
384
- lookups?: Record<string, LookupListSource$1>;
555
+ lookups?: Record<string, LookupListSource>;
385
556
  /**
386
557
  * Override the messages shown under invalid fields, keyed by Angular error key. A value is
387
558
  * a string, or a function receiving `(language, error)` when it needs the error's detail.
@@ -414,7 +585,21 @@ interface NgxDynamicEntityConfig {
414
585
  * Ordered steps that upgrade a saved record when `EntityFormConfig.version` moves ahead
415
586
  * of the record's `_configVersion`. Applied to `initialData` before the form is patched.
416
587
  */
417
- migrations?: RecordMigration$1[];
588
+ migrations?: RecordMigration[];
589
+ /**
590
+ * Reads an uploaded spreadsheet for the import wizard, as `{ headers, rows }` with rows
591
+ * positional rather than keyed by header.
592
+ *
593
+ * Omit it and CSV is read by the dependency-free parser in `@dynamic-entity/core`; supply
594
+ * one to accept `.xlsx`, using whichever spreadsheet library you already have.
595
+ */
596
+ sheetParser?: SheetParser;
597
+ /**
598
+ * Where an import's work happens. Omit it and the whole import runs in the browser with no
599
+ * network at all; supply one that posts to a server for files too large to hold in memory.
600
+ * Either way the mapping, coercion and validation are the same pure functions.
601
+ */
602
+ importTransport?: ImportTransport;
418
603
  }
419
604
  /**
420
605
  * Provider function for ngx-dynamic-entity.
@@ -433,7 +618,7 @@ declare const provideNgxDynamicEntity: (config?: NgxDynamicEntityConfig) => Envi
433
618
  *
434
619
  * `FieldRegistryService` imports no components. Whatever you register here is what gets
435
620
  * bundled, so an app that only uses text/number/dropdown pays for three components and
436
- * their imports, not nineteen.
621
+ * their imports, not twenty-one.
437
622
  *
438
623
  * ```ts
439
624
  * // Everything (convenient; bundles all built-ins):
@@ -449,7 +634,7 @@ declare const provideNgxDynamicEntity: (config?: NgxDynamicEntityConfig) => Envi
449
634
  declare function builtInFieldTypes(): Record<string, Type<any>>;
450
635
  /** Register a specific set of field types. Only the components you name are bundled. */
451
636
  declare function provideFieldTypes(types: Record<string, Type<any>>): EnvironmentProviders;
452
- /** Register all 19 built-in field type keys. Convenience for apps that use most of them. */
637
+ /** Register all 21 built-in field type keys. Convenience for apps that use most of them. */
453
638
  declare function provideBuiltInFieldTypes(): EnvironmentProviders;
454
639
 
455
640
  /**
@@ -457,7 +642,7 @@ declare function provideBuiltInFieldTypes(): EnvironmentProviders;
457
642
  *
458
643
  * **Tree-shaking contract:** this service holds *no* static imports of field components.
459
644
  * Nothing is bundled until something registers it, which is what makes the built-ins
460
- * opt-in. Register them with `provideBuiltInFieldTypes()` (all 19 keys) or a narrower
645
+ * opt-in. Register them with `provideBuiltInFieldTypes()` (all 21 keys) or a narrower
461
646
  * subset via `provideFieldTypes({ ... })` — see `providers/provide-field-types.ts`.
462
647
  *
463
648
  * Resolution order: consumer token registry → runtime registrations → nothing.
@@ -521,6 +706,22 @@ declare class ValidatorRegistryService {
521
706
  * An unknown name is skipped rather than throwing, matching the synchronous behaviour.
522
707
  */
523
708
  resolveAsyncFromConfig(config?: FieldValidators | string[]): AsyncValidatorFn[];
709
+ /**
710
+ * Say when a named validator resolved to nothing.
711
+ *
712
+ * It used to be dropped in silence, which is the worst failure mode this library has: the
713
+ * schema says the field is checked, the field is not checked, the form saves, and nothing
714
+ * anywhere said so. For `customAsync` that is typically a uniqueness check, so the duplicate
715
+ * lands in the database.
716
+ *
717
+ * Dev only, once per name — this runs while controls are built, which happens on every
718
+ * config change. `validateConfig` with `knownValidators` is the build-time version of the
719
+ * same check, and it fails CI rather than a console.
720
+ */
721
+ private warnUnresolved;
722
+ /** Per service instance, which is per injector — so a test gets a clean slate. */
723
+ private readonly warned;
724
+ private warnOnce;
524
725
  static ɵfac: i0.ɵɵFactoryDeclaration<ValidatorRegistryService, never>;
525
726
  static ɵprov: i0.ɵɵInjectableDeclaration<ValidatorRegistryService>;
526
727
  }
@@ -539,7 +740,7 @@ declare class EntityRefRegistryService {
539
740
  * Get the option loader for an entity key.
540
741
  * @returns The loader, or null if none is registered.
541
742
  */
542
- resolve(entityKey: string): EntityReferenceLoader$1 | null;
743
+ resolve(entityKey: string): EntityReferenceLoader | null;
543
744
  has(entityKey: string): boolean;
544
745
  static ɵfac: i0.ɵɵFactoryDeclaration<EntityRefRegistryService, never>;
545
746
  static ɵprov: i0.ɵɵInjectableDeclaration<EntityRefRegistryService>;
@@ -548,7 +749,7 @@ declare class EntityRefRegistryService {
548
749
  interface EntityRefSelection {
549
750
  fieldId: string;
550
751
  /** The chosen option, or `null` when the selection was cleared. */
551
- option: ReferenceOption$1 | null;
752
+ option: ReferenceOption | null;
552
753
  }
553
754
  /**
554
755
  * EntityRefSelectionService — the channel an entity-ref field uses to publish *which record*
@@ -563,7 +764,7 @@ interface EntityRefSelection {
563
764
  declare class EntityRefSelectionService {
564
765
  private readonly subject;
565
766
  readonly selection$: rxjs.Observable<EntityRefSelection>;
566
- emit(fieldId: string, option: ReferenceOption$1 | null): void;
767
+ emit(fieldId: string, option: ReferenceOption | null): void;
567
768
  static ɵfac: i0.ɵɵFactoryDeclaration<EntityRefSelectionService, never>;
568
769
  static ɵprov: i0.ɵɵInjectableDeclaration<EntityRefSelectionService>;
569
770
  }
@@ -588,7 +789,7 @@ declare class CascadeDataService {
588
789
  * Load the options for a field, cascade-filtered against `parentValue`.
589
790
  * Returns `[]` when no loader is registered, or when a cascade child has no parent value yet.
590
791
  */
591
- load(field: NestedFieldConfig, ctx?: ReferenceLoaderContext): Promise<ReferenceOption$1[]>;
792
+ load(field: NestedFieldConfig, ctx?: ReferenceLoaderContext): Promise<ReferenceOption[]>;
592
793
  /**
593
794
  * Warm the cache for every entity a config's cascades depend on, so changing a parent
594
795
  * resolves from memory instead of a round-trip.
@@ -603,7 +804,7 @@ declare class CascadeDataService {
603
804
  * Child options for a parent selection, from cache only — no load is triggered.
604
805
  * Returns `[]` when the entity has not been preloaded.
605
806
  */
606
- getCachedChildOptions(field: NestedFieldConfig, parentValue: unknown, lang?: string): ReferenceOption$1[];
807
+ getCachedChildOptions(field: NestedFieldConfig, parentValue: unknown, lang?: string): ReferenceOption[];
607
808
  /** Forget the preload for one config. The option cache itself is untouched. */
608
809
  clearCache(formConfigId: string): void;
609
810
  /** Every entity-ref field in the config that participates in a cascade. */
@@ -637,9 +838,9 @@ declare class EntityReferenceService {
637
838
  * Options for an entity, from cache when present.
638
839
  * Concurrent callers with the same key share one load.
639
840
  */
640
- load(entityKey: string, ctx?: ReferenceLoaderContext): Promise<ReferenceOption$1[]>;
841
+ load(entityKey: string, ctx?: ReferenceLoaderContext): Promise<ReferenceOption[]>;
641
842
  /** Cached options without triggering a load. `undefined` when not cached. */
642
- peek(entityKey: string, ctx?: ReferenceLoaderContext): ReferenceOption$1[] | undefined;
843
+ peek(entityKey: string, ctx?: ReferenceLoaderContext): ReferenceOption[] | undefined;
643
844
  /**
644
845
  * Display text for a stored reference value, synchronously.
645
846
  * Returns `undefined` when the entity's options have never been loaded.
@@ -778,18 +979,88 @@ declare class RbacService {
778
979
  static ɵprov: i0.ɵɵInjectableDeclaration<RbacService>;
779
980
  }
780
981
 
982
+ /** What `syncHiddenFieldState` needs from the form it is operating on. */
983
+ interface RuleSyncContext {
984
+ result: RuleEvaluationResult;
985
+ /** The values `showWhen` is evaluated against — the flat map the rules engine reads. */
986
+ values: Record<string, unknown>;
987
+ /** Every name a field answers to: its bare id, and its bracketed path. */
988
+ namesOf(field: NestedFieldConfig): readonly string[];
989
+ /**
990
+ * The field's dotted address, unbracketed — `personal.address.city`.
991
+ *
992
+ * Separate from `namesOf` because it is compared as a *prefix*: a container hides its
993
+ * descendants, and `[personal.address]` is not a prefix of `[personal.address.city]` once
994
+ * the brackets are in the way.
995
+ */
996
+ addressOf(field: NestedFieldConfig): string;
997
+ /** The control a scope entry addresses, or `null` when it has no static path. */
998
+ controlFor(entry: FieldScopeEntry): AbstractControl | null;
999
+ }
781
1000
  /**
782
- * RulesEvaluationService — evaluates dynamic form rules against active reactive form state.
1001
+ * RulesEvaluationService — evaluates rules, and owns what a result *means*.
1002
+ *
1003
+ * It used to be two passthrough methods, with the interpretation spread across the component:
1004
+ * the render filter decided one way what a rule result did to a field, and the validity sync
1005
+ * decided again forty lines further down. They disagreed about what "every field" meant, and
1006
+ * the consequence was a required field inside a `group` that a rule had hidden holding
1007
+ * `form.invalid` true forever with nothing on screen to explain it.
1008
+ *
1009
+ * Precedence lives here now, in one method that both callers go through. Still stateless —
1010
+ * every method takes the result it is interpreting, so there is nothing to go stale against a
1011
+ * rebuilt form and nothing to reset between configs.
783
1012
  */
784
1013
  declare class RulesEvaluationService {
785
1014
  /**
786
1015
  * Evaluate rules against form values and optional original baseline values.
787
1016
  */
788
- evaluate(rules: FormRule[] | undefined | null, values: Record<string, unknown>, originalValues?: Record<string, unknown>): RuleEvaluationResult$1;
1017
+ evaluate(rules: FormRule[] | undefined | null, values: Record<string, unknown>, originalValues?: Record<string, unknown>, options?: EvaluateFormRulesOptions): RuleEvaluationResult;
789
1018
  /**
790
1019
  * Filter rules pertinent to a single tab.
791
1020
  */
792
1021
  filterForTab(rules: FormRule[] | undefined | null, tabId: string, config: EntityFormConfig): FormRule[];
1022
+ /**
1023
+ * Whether a field renders, under the precedence documented on `RuleEvaluationResult`.
1024
+ *
1025
+ * An explicit hide beats everything. A show beats static hiding — `visibility: false`, or a
1026
+ * `showWhen` that does not currently hold — because a rule that could only hide what the
1027
+ * config already hides would have nothing to say, and `{ visibility: true }` was a
1028
+ * documented no-op until it did. Otherwise the config's own visibility decides.
1029
+ */
1030
+ isFieldVisible(result: RuleEvaluationResult, field: NestedFieldConfig, names: readonly string[], values: Record<string, unknown>): boolean;
1031
+ /** The same precedence for a tab. */
1032
+ isTabVisible(result: RuleEvaluationResult, tab: NestedTabConfig): boolean;
1033
+ /**
1034
+ * Keep a hidden field's control out of form validity.
1035
+ *
1036
+ * A field hidden by a rule or a `showWhen` is filtered out of the render, but its
1037
+ * validators stay attached — so a hidden *required* field holds `form.invalid` true
1038
+ * forever, permanently disabling Save with nothing on screen to explain why.
1039
+ *
1040
+ * Disabling is the fix rather than stripping validators: Angular excludes disabled controls
1041
+ * from validity, and the field's own validators survive intact for when it comes back.
1042
+ * Values are untouched — a disabled control still carries its value, so rule evaluation and
1043
+ * the emitted record see exactly what they saw before. That also keeps this from feeding
1044
+ * back on itself: hiding a field cannot change the values the rules are evaluated over.
1045
+ *
1046
+ * The predicate is `isFieldVisible`, the same one the render filter uses, so what is on
1047
+ * screen and what counts toward validity cannot drift apart.
1048
+ *
1049
+ * Entries must arrive parent-first, which is what `collectFieldScopes` yields: a container's
1050
+ * children are skipped once the container itself is hidden, because Angular's `disable()`
1051
+ * cascades to descendants and a child's `enable()` walks back up recalculating ancestors.
1052
+ * Toggling a child inside a hidden group would fight its parent.
1053
+ */
1054
+ syncHiddenFieldState(entries: readonly FieldScopeEntry[], ctx: RuleSyncContext): void;
1055
+ /**
1056
+ * The hidden and shown names as sets, memoised against the result they came from.
1057
+ *
1058
+ * `isFieldVisible` runs once per field per change-detection pass; building two sets from
1059
+ * two arrays each time turns a rule result into allocation proportional to the form. A
1060
+ * `WeakMap` keyed by the result means it is built once per evaluation and collected with it.
1061
+ */
1062
+ private nameSets;
1063
+ private readonly nameSetCache;
793
1064
  static ɵfac: i0.ɵɵFactoryDeclaration<RulesEvaluationService, never>;
794
1065
  static ɵprov: i0.ɵɵInjectableDeclaration<RulesEvaluationService>;
795
1066
  }
@@ -961,6 +1232,8 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
961
1232
  private readonly hookRegistry;
962
1233
  private readonly rbacService;
963
1234
  private readonly rulesEvaluation;
1235
+ /** config ⇄ FormGroup: where a value lives. See `FormStructureService`. */
1236
+ private readonly structure;
964
1237
  /** Resolves what is wrong with a field, for the error summary — see `resolveForField`. */
965
1238
  private readonly messages;
966
1239
  private readonly entityRefSelection;
@@ -990,7 +1263,10 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
990
1263
  /** Previous values, for detecting `patchOnTrue` false→true transitions. */
991
1264
  private previousValues;
992
1265
  handleKeyboardEvent(event: KeyboardEvent): void;
993
- readonly ruleResult: i0.Signal<RuleEvaluationResult>;
1266
+ readonly ruleResult: i0.Signal<ngx_dynamic_entity.RuleEvaluationResult>;
1267
+ /** Warned-about rule problems, per component instance — see `warnedAmbiguousIds`. */
1268
+ private readonly warnedProblems;
1269
+ private warnOnce;
994
1270
  /**
995
1271
  * Critical fields whose value differs from the session baseline.
996
1272
  * These drive the deferred `VALUE_CHANGED` banner — a critical edit is announced once,
@@ -999,6 +1275,7 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
999
1275
  readonly changedCriticalFields: i0.Signal<NestedFieldConfig[]>;
1000
1276
  get tabs(): NestedTabConfig[];
1001
1277
  get visibleTabs(): NestedTabConfig[];
1278
+ private isTabVisible;
1002
1279
  get activeTabConfig(): NestedTabConfig | null;
1003
1280
  get visibleSubTabs(): NestedTabConfig[];
1004
1281
  get activeSubTabConfig(): NestedTabConfig | null;
@@ -1018,6 +1295,12 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1018
1295
  private readonly warnedSelectorModules;
1019
1296
  private warnSelectorModule;
1020
1297
  get fieldsForActiveTab(): NestedFieldConfig[];
1298
+ /**
1299
+ * Whether a field renders. Precedence lives in `RulesEvaluationService`, which is also
1300
+ * what `syncHiddenFieldState` asks — so what is on screen and what counts toward validity
1301
+ * cannot drift apart.
1302
+ */
1303
+ private isFieldVisible;
1021
1304
  /**
1022
1305
  * Entity-level permissions for the current roles.
1023
1306
  *
@@ -1159,6 +1442,14 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1159
1442
  * library never reaches for the global `document`.
1160
1443
  */
1161
1444
  jumpToField(fieldId: string): void;
1445
+ /**
1446
+ * Which tab (and sub-tab) holds a field.
1447
+ *
1448
+ * Descends into `group`/`array` children: this compared top-level fields only, so "jump to
1449
+ * the first invalid field" found nothing for a nested one and silently did nothing. Only
1450
+ * sub-tabs one level deep are named, because that is what the tab strip can select —
1451
+ * anything deeper resolves to the ancestor it renders under.
1452
+ */
1162
1453
  private locateField;
1163
1454
  /** A critical field stays read-only until the user deliberately unlocks it. */
1164
1455
  isFieldLocked(field: NestedFieldConfig): boolean;
@@ -1185,8 +1476,6 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1185
1476
  */
1186
1477
  changedCriticalFieldLabels(): string;
1187
1478
  private buildForm;
1188
- private buildFieldControl;
1189
- /** One row of an `array` field: a FormGroup when the field declares columns, else a bare control. */
1190
1479
  /**
1191
1480
  * The `FormArray` behind an `array` field, for callers that manage rows themselves —
1192
1481
  * the record editor's inline row drawer.
@@ -1223,9 +1512,8 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1223
1512
  * stay empty — with no error and no clue. That silent miss is the most expensive way to
1224
1513
  * lose an hour with this library, so name it in dev builds.
1225
1514
  *
1226
- * Only top-level keys that match a known field id are reported: anything else is assumed
1227
- * to be the consumer's own record metadata (ids, timestamps, `_configVersion`) and is not
1228
- * our business.
1515
+ * Which keys were missed is `FormStructureService`'s answer; warning once per key per form
1516
+ * is this component's, because it is the thing that lives as long as the form does.
1229
1517
  */
1230
1518
  private warnUnconsumedInitialData;
1231
1519
  /** Warn once per key per form instance — patchForm re-runs on every initialData change. */
@@ -1244,7 +1532,16 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1244
1532
  * consumers that never call the validator still get a signal in development.
1245
1533
  */
1246
1534
  private warnAmbiguousRuleReferences;
1247
- private static readonly warnedAmbiguousIds;
1535
+ /**
1536
+ * Ids already warned about, per component instance.
1537
+ *
1538
+ * This was `private static`, so it was module-global mutable state that nothing ever
1539
+ * cleared: under SSR every render of every form in the process accumulated into one set
1540
+ * that lived as long as the server did, and the first request to render a config
1541
+ * suppressed the warning for every request after it. Per instance, the warning is once per
1542
+ * form — which is what "warn once" was meant to mean.
1543
+ */
1544
+ private readonly warnedAmbiguousIds;
1248
1545
  /**
1249
1546
  * The value map rules and `showWhen` are evaluated against.
1250
1547
  *
@@ -1258,17 +1555,53 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1258
1555
  * unknown>` and needs no knowledge of refs at all — the extra keys simply resolve.
1259
1556
  */
1260
1557
  /**
1261
- * Whether a rule result names this field.
1558
+ * Every name a rule may address this field by.
1262
1559
  *
1263
1560
  * A rule target is either a bare field id — how every config so far addresses a field — or
1264
- * a bracketed path, which is the only way to name one of two fields that share an id.
1265
- * Both have to match, or a rule written either way would fail to hide what it targeted.
1561
+ * a bracketed path, which is the only way to name one of two fields that share an id. Both
1562
+ * have to match, or a rule written either way would fail to hide what it targeted.
1563
+ *
1564
+ * The path comes last, and `syncHiddenFieldState` relies on that: it uses the last entry
1565
+ * as the field's address when deciding whether a hidden container already covers it.
1566
+ */
1567
+ private namesOf;
1568
+ /**
1569
+ * The field's dotted address, unbracketed.
1570
+ *
1571
+ * The positional path rather than an authored `refererField`: this is compared as a prefix
1572
+ * to decide whether a hidden container already covers a field, and containment is a fact
1573
+ * about where the field *is*. A `refererField` is a binding override — it says where the
1574
+ * value goes, not which group renders it.
1266
1575
  */
1267
- private namesField;
1576
+ private addressOf;
1268
1577
  /** The field's declared path, or the one its position implies when it carries none. */
1269
1578
  private pathFor;
1579
+ /**
1580
+ * Every field in the config with the scope its value is stored under, cached per config.
1581
+ *
1582
+ * `collectFieldScopes` walks the whole tree. It was called once per field from `pathFor`,
1583
+ * which `namesField` calls, which the render filter calls for every field on the active
1584
+ * tab — a quadratic walk on every change-detection pass. Keyed by config identity, so a
1585
+ * new config invalidates it without an explicit lifecycle hook to forget.
1586
+ */
1587
+ private fieldScopes;
1588
+ private scopeEntryFor;
1589
+ private scopeCache?;
1590
+ /**
1591
+ * The control a scope entry addresses, named by path rather than by bare id.
1592
+ *
1593
+ * `getControl(field.id)` with no tab falls through to a first-match recursive search of the
1594
+ * whole form, so with `address` on two tabs, hiding one disabled the other. The control
1595
+ * tree mirrors the scope path exactly — `buildTabControls` nests by tab id and
1596
+ * `buildFieldControl` nests a `group` under its own id — so a `[path]` ref resolves through
1597
+ * `form.get()` on `getControl`'s first branch and names exactly one control.
1598
+ *
1599
+ * A field inside an `array` has no static path: its controls live in `FormArray` rows built
1600
+ * per row. `form.get()` returns null for those and they are left alone, which is correct —
1601
+ * a row's controls are created and destroyed with the row.
1602
+ */
1603
+ private controlForEntry;
1270
1604
  private flattenFormValues;
1271
- private getTabGroup;
1272
1605
  /**
1273
1606
  * `autoPatch` — copy mapped fields out of the record the user just selected in an
1274
1607
  * entity-ref field into the configured target tab's controls.
@@ -1279,37 +1612,44 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1279
1612
  * copy `from` → `to` within the current record.
1280
1613
  */
1281
1614
  private runPatchOnTrue;
1282
- /** Prefer a control declared on the configured target tab; fall back to a top-level control. */
1283
- private resolveTargetControl;
1284
1615
  /**
1285
- * The fields the render filter operates on — a tab's own fields, at every tab depth.
1286
- * Deliberately not `allFields()`: `group` and `array` children are shown or hidden with
1287
- * their parent, and toggling a child's state independently would fight the parent's.
1616
+ * Prefer a control declared on the configured target tab; fall back to a top-level control.
1617
+ *
1618
+ * "Declared on the tab" now means anywhere under it, sub-tabs and `group` children
1619
+ * included. It meant the tab's own top-level fields only, so an `autoPatch` mapping onto a
1620
+ * field inside a group resolved to `null` and copied nothing, silently.
1288
1621
  */
1289
- private tabFields;
1622
+ private resolveTargetControl;
1290
1623
  /**
1291
- * Keep a hidden field's control out of form validity.
1292
- *
1293
- * A field hidden by a rule or a `showWhen` condition is filtered out of the render, but
1294
- * its validators stay attached — so a hidden *required* field holds `form.invalid` true
1295
- * forever, permanently disabling Save with nothing on screen to explain why.
1296
- *
1297
- * Disabling is the fix rather than stripping validators: Angular excludes disabled
1298
- * controls from validity, and the field's own validators survive intact for when it
1299
- * comes back. Values are untouched — `flattenFormValues` and `extractRecord` both read
1300
- * `control.value`, which a disabled control still carries, so rule evaluation and the
1301
- * emitted record see exactly what they saw before. That also keeps this from feeding
1302
- * back on itself: hiding a field cannot change the values the rules are evaluated over.
1624
+ * Keep a hidden field's control out of form validity — see
1625
+ * `RulesEvaluationService.syncHiddenFieldState`, which owns the rule and the reasoning.
1303
1626
  *
1304
- * The visibility predicate is the same one `fieldsForActiveTab` filters with, so what is
1305
- * rendered and what counts toward validity cannot drift apart.
1627
+ * This supplies the three things it cannot know: which fields exist and in what order
1628
+ * (`collectFieldScopes`, parent-first), what each one is called, and which control each
1629
+ * one addresses.
1306
1630
  */
1307
1631
  private syncHiddenFieldState;
1308
1632
  /** Every field in the config, including `group`/`array` children. */
1309
1633
  private allFields;
1310
1634
  /** An empty `originalData` counts as "not supplied" — fall back to the captured baseline. */
1311
1635
  private baseline;
1636
+ /**
1637
+ * Whether a critical field still holds what it held when the session started.
1638
+ *
1639
+ * The same question `VALUE_CHANGED` asks, so it is answered by the same comparator rather
1640
+ * than by a third one. The local version compared objects through `JSON.stringify`, which
1641
+ * is key-order sensitive — two serialisers writing the same option in a different key
1642
+ * order made the field read as edited — and had its own idea of which values count as
1643
+ * empty. `valuesEqual` is order-independent and treats `null` and `undefined` as the same
1644
+ * absence; the empty-string case is kept here, because a cleared text input is not an edit.
1645
+ */
1312
1646
  private sameValue;
1647
+ /**
1648
+ * Find a control by a bracketed path or a bare field id.
1649
+ *
1650
+ * Public API, so it stays on the component and delegates — see `FormStructureService`,
1651
+ * which owns the resolution rule and is where the behaviour is documented.
1652
+ */
1313
1653
  getControl(fieldId: string, currentTabId?: string): AbstractControl | null;
1314
1654
  /**
1315
1655
  * Move focus into the newly shown panel.
@@ -1432,7 +1772,7 @@ declare class DynamicRecordFormComponent implements OnChanges {
1432
1772
  * building a row's FormGroup inside the tab view races the tab's own initialisation, so
1433
1773
  * the row form is kept detached and only pushed into the FormArray on save.
1434
1774
  */
1435
- readonly inlineRowField: i0.WritableSignal<any>;
1775
+ readonly inlineRowField: i0.WritableSignal<NestedFieldConfig>;
1436
1776
  readonly inlineRowIndex: i0.WritableSignal<number>;
1437
1777
  inlineRowForm: FormGroup | null;
1438
1778
  /** `array` fields on the tab currently shown, so the template can list their rows. */
@@ -1667,6 +2007,376 @@ declare class DynamicFieldComponent implements OnChanges, OnDestroy {
1667
2007
  static ɵcmp: i0.ɵɵComponentDeclaration<DynamicFieldComponent, "ngx-dynamic-field", never, { "field": { "alias": "field"; "required": false; }; "control": { "alias": "control"; "required": false; }; "config": { "alias": "config"; "required": false; }; "currentTabId": { "alias": "currentTabId"; "required": false; }; "language": { "alias": "language"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "userRoles": { "alias": "userRoles"; "required": false; }; }, {}, never, never, true, never>;
1668
2008
  }
1669
2009
 
2010
+ /**
2011
+ * sheet-parser.ts — the built-in CSV reader, and the reason it is the only one.
2012
+ *
2013
+ * CSV is what every spreadsheet tool exports and what a text editor can produce, and
2014
+ * `@dynamic-entity/core` parses it with no dependency. Shipping that as the default means the
2015
+ * import wizard works on a fresh `npm install` with nothing registered — `SHEET_PARSER` then
2016
+ * exists for the consumer who *also* wants `.xlsx`, rather than for the consumer who wants
2017
+ * the feature to work at all.
2018
+ *
2019
+ * The alternative was a hard dependency on a spreadsheet library in a package that currently
2020
+ * has none, for a file format many consumers will never see.
2021
+ */
2022
+
2023
+ /**
2024
+ * Read a text file as CSV.
2025
+ *
2026
+ * A binary workbook is refused by name rather than attempted: `.xlsx` is a zip, so reading it
2027
+ * as text yields a header row of mojibake and a mapping screen full of nonsense, which is a
2028
+ * much harder thing to diagnose than being told the format is not supported.
2029
+ */
2030
+ declare const defaultSheetParser: SheetParser;
2031
+
2032
+ declare class LocalImportTransport implements ImportTransport {
2033
+ private readonly registered;
2034
+ /**
2035
+ * One parse per file, not one per call.
2036
+ *
2037
+ * A wizard run reads the same file twice — once to show its headers, once to import it —
2038
+ * and this is the transport whose stated limit is that it holds the file in memory. Doing
2039
+ * the work twice doubled both the parse and the peak memory of the one implementation that
2040
+ * can least afford it.
2041
+ *
2042
+ * A `WeakMap` keyed on the `File` itself: the entry lives exactly as long as something
2043
+ * still holds the file, so choosing a different file drops the old sheet without anything
2044
+ * having to remember to. Keying on a name or a size would collide between two files a user
2045
+ * picked in sequence.
2046
+ */
2047
+ private readonly parsed;
2048
+ /** The registered parser, or the built-in CSV one. */
2049
+ private read;
2050
+ preview(file: File, context: ImportContext): Promise<ImportPreview>;
2051
+ commit(file: File, plan: MappingPlan, context: ImportContext): Promise<ImportResult>;
2052
+ template(spec: TemplateSpec, format: TemplateFormat): Promise<Blob>;
2053
+ static ɵfac: i0.ɵɵFactoryDeclaration<LocalImportTransport, never>;
2054
+ static ɵprov: i0.ɵɵInjectableDeclaration<LocalImportTransport>;
2055
+ }
2056
+
2057
+ /**
2058
+ * http-import-transport.ts — the same wizard, with the work happening on a server.
2059
+ *
2060
+ * The components do not change at all: they already talk to an `ImportTransport`, which was
2061
+ * the point of the seam. Registering this one moves the mapping, coercion and validation from
2062
+ * the tab to a server that runs the *same* `@dynamic-entity/core` functions — so the reason to
2063
+ * do it is size, not behaviour.
2064
+ *
2065
+ * **Built on `fetch`, not `HttpClient`.** This package has no `@angular/common/http` dependency
2066
+ * today, and four requests is not the reason to acquire one — a consumer who wants interceptors
2067
+ * passes their own `fetch`, which is a smaller seam than a dependency.
2068
+ */
2069
+
2070
+ interface HttpImportTransportOptions {
2071
+ /** Where the router is mounted, e.g. `/api/import`. A trailing slash is ignored. */
2072
+ baseUrl: string;
2073
+ /** Supply one to add interceptors, retries, or credentials. Defaults to the global `fetch`. */
2074
+ fetch?: typeof fetch;
2075
+ /** Headers on every request — an `Authorization`, a tenant id. Never a `Content-Type`. */
2076
+ headers?: Record<string, string> | (() => Record<string, string>);
2077
+ /**
2078
+ * Where a warning goes. Defaults to `console.warn`.
2079
+ *
2080
+ * There is exactly one warning, and it is the engine-version mismatch below — which is a
2081
+ * developer's problem, not a user's, so it does not belong in the wizard's error banner.
2082
+ */
2083
+ warn?: (message: string) => void;
2084
+ }
2085
+ /**
2086
+ * Not `@Injectable`, and registered by value rather than by class.
2087
+ *
2088
+ * Its constructor takes options rather than dependencies, so there is nothing for an injector
2089
+ * to resolve — and a consumer who wants two of them, pointed at two backends, gets that for
2090
+ * free.
2091
+ */
2092
+ declare class HttpImportTransport implements ImportTransport {
2093
+ private readonly options;
2094
+ constructor(options: HttpImportTransportOptions);
2095
+ private get base();
2096
+ private get send();
2097
+ private entityOf;
2098
+ /**
2099
+ * Warn when the server's engine is not the one this bundle was built against.
2100
+ *
2101
+ * The two halves are separately deployed and may differ. `MappingPlan.configVersion` catches
2102
+ * *config* drift and says nothing about *engine* drift, so "one engine, both sides" is only
2103
+ * true if something checks — and this is the something. A warning rather than a refusal: a
2104
+ * patch release is not a reason to stop somebody importing.
2105
+ */
2106
+ private checkEngine;
2107
+ preview(file: File, context: ImportContext): Promise<ImportPreview>;
2108
+ commit(file: File, plan: MappingPlan, context: ImportContext): Promise<ImportResult>;
2109
+ template(spec: TemplateSpec, format: TemplateFormat, context: ImportContext, fields?: readonly string[]): Promise<Blob>;
2110
+ }
2111
+ /**
2112
+ * Register the HTTP transport.
2113
+ *
2114
+ * ```ts
2115
+ * provideNgxDynamicEntity({}),
2116
+ * provideHttpImportTransport({ baseUrl: '/api/import' }),
2117
+ * ```
2118
+ *
2119
+ * A plain `Provider` rather than `EnvironmentProviders`, so it composes inside a component's
2120
+ * or a route's `providers` array as well as at the root — which is how a consumer points one
2121
+ * part of an app at a different backend.
2122
+ */
2123
+ declare function provideHttpImportTransport(options: HttpImportTransportOptions): Provider;
2124
+
2125
+ type Step = 'upload' | 'map' | 'review' | 'done';
2126
+ /**
2127
+ * The import wizard: choose a file, match its columns, look at what that produced, commit.
2128
+ *
2129
+ * It holds no import logic of its own. Everything it does goes through an `ImportTransport`,
2130
+ * which by default is the in-browser one built on `@dynamic-entity/core` — so the wizard
2131
+ * works with no backend, and a consumer who registers a server transport gets the same
2132
+ * screens over the same mapping rules rather than a second implementation of them.
2133
+ *
2134
+ * The review step exists because the expensive mistake here is not a file that fails to
2135
+ * import, it is one that imports *successfully into the wrong fields*. That is invisible in a
2136
+ * summary and obvious in five rendered rows.
2137
+ */
2138
+ declare class EntityImportComponent {
2139
+ config: EntityFormConfig;
2140
+ /**
2141
+ * The rules the form is rendered with.
2142
+ *
2143
+ * Pass them, or the import checks field validators only — which is not what the form
2144
+ * enforces, and a required field hidden by a rule will fail every row.
2145
+ */
2146
+ rules?: readonly FormRule[];
2147
+ /**
2148
+ * Named lists, overriding what the wizard already resolves from `LOOKUP_REGISTRY`.
2149
+ *
2150
+ * Not something a host has to supply: every `listName` a config mentions is loaded from the
2151
+ * registry the renderer is already holding. Pass this only to override one.
2152
+ */
2153
+ lookups?: ImportLookups;
2154
+ language: string;
2155
+ /** Format asked of the transport when a template is downloaded. */
2156
+ templateFormat: TemplateFormat;
2157
+ /** The finished import, once it has run. */
2158
+ readonly importComplete: EventEmitter<ImportResult>;
2159
+ /** A template the transport produced, for a host that would rather save it its own way. */
2160
+ readonly templateReady: EventEmitter<Blob>;
2161
+ protected readonly ui: UiTextService;
2162
+ protected readonly steps: Step[];
2163
+ protected readonly step: i0.WritableSignal<Step>;
2164
+ protected readonly busy: i0.WritableSignal<boolean>;
2165
+ protected readonly problem: i0.WritableSignal<string>;
2166
+ protected readonly headers: i0.WritableSignal<string[]>;
2167
+ protected readonly sample: i0.WritableSignal<string[][]>;
2168
+ protected readonly rowCount: i0.WritableSignal<number>;
2169
+ protected readonly fileName: i0.WritableSignal<string>;
2170
+ protected readonly plan: i0.WritableSignal<MappingPlan>;
2171
+ protected readonly result: i0.WritableSignal<ImportResult>;
2172
+ private readonly registered;
2173
+ private readonly local;
2174
+ private readonly lookupRegistry;
2175
+ private file;
2176
+ /** Named lists resolved from `LOOKUP_REGISTRY`, merged under the `lookups` input. */
2177
+ protected readonly resolvedLookups: i0.WritableSignal<ImportLookups>;
2178
+ private get transport();
2179
+ private context;
2180
+ /**
2181
+ * Load every named list this config's fields refer to.
2182
+ *
2183
+ * `@dynamic-entity/core` is framework-agnostic and cannot reach `LOOKUP_REGISTRY`, so
2184
+ * `coerceCell` takes the values as an argument. That is a reason for *core* to ask, not a
2185
+ * reason for the host to be asked — this package is holding the registry, and making the
2186
+ * consumer re-supply what the library already has is how a `listName` column ends up
2187
+ * storing the raw text `"Gold"` instead of the option object. Such a record renders
2188
+ * correctly and then matches no rule, which is the exact failure the option-shape contract
2189
+ * exists to prevent.
2190
+ *
2191
+ * The `lookups` input still wins where it names a list, so a host can override one without
2192
+ * having to provide all of them.
2193
+ */
2194
+ private loadLookups;
2195
+ protected stepLabel(step: Step): string;
2196
+ protected chooseFile(event: Event): Promise<void>;
2197
+ protected commit(): Promise<void>;
2198
+ protected downloadTemplate(fields: string[]): Promise<void>;
2199
+ /**
2200
+ * How many records were written.
2201
+ *
2202
+ * `records.length` for the in-browser transport, which returns every record it built, and
2203
+ * `imported` for a server one, which streamed the file precisely so that fifty thousand
2204
+ * records never had to exist at once and therefore has a number rather than a list. Reading
2205
+ * `records.length` alone would report a successful import of nothing.
2206
+ */
2207
+ protected importedCount(result: ImportResult): number;
2208
+ /**
2209
+ * How many distinct rows failed, not how many problems they had between them.
2210
+ *
2211
+ * `failed` when the transport reports it, and the distinct rows in `errors` only when it does
2212
+ * not. Counting the errors alone was wrong the moment a transport started capping them: a run
2213
+ * of a thousand rows where two hundred failed said **seven**, because seven was how many
2214
+ * distinct rows fitted inside the first twenty retained problems. This is the number a user
2215
+ * acts on — which rows do I go and fix — so a confident wrong answer is the worst of the
2216
+ * available ones. The in-browser transport returns every error, so counting them there is
2217
+ * exact and `failed` is absent.
2218
+ */
2219
+ protected failedRowCount(result: ImportResult): number;
2220
+ protected restart(): void;
2221
+ static ɵfac: i0.ɵɵFactoryDeclaration<EntityImportComponent, never>;
2222
+ static ɵcmp: i0.ɵɵComponentDeclaration<EntityImportComponent, "ngx-entity-import", never, { "config": { "alias": "config"; "required": true; }; "rules": { "alias": "rules"; "required": false; }; "lookups": { "alias": "lookups"; "required": false; }; "language": { "alias": "language"; "required": false; }; "templateFormat": { "alias": "templateFormat"; "required": false; }; }, { "importComplete": "importComplete"; "templateReady": "templateReady"; }, never, never, true, never>;
2223
+ }
2224
+
2225
+ /** One target field, and which of the sheet's columns feeds it. */
2226
+ interface MappingRow {
2227
+ column: ImportColumn;
2228
+ /** Index into the sheet's headers, or `null` for "not imported". */
2229
+ source: number | null;
2230
+ guessed: boolean;
2231
+ }
2232
+ /**
2233
+ * Match the sheet's columns to the config's fields.
2234
+ *
2235
+ * The table is driven by the **target fields**, not by the sheet's columns, which is the
2236
+ * choice that decides how the screen reads. A user is answering "where does Start Date come
2237
+ * from?", which they can do; column-first would ask "what is column G?", which they often
2238
+ * cannot, and would make a sheet with forty irrelevant columns forty questions long.
2239
+ *
2240
+ * A selection is a **column index**, never header text. Two columns headed "Notes" are two
2241
+ * different answers, and text cannot tell them apart.
2242
+ */
2243
+ declare class ImportMapperComponent implements OnChanges {
2244
+ config: EntityFormConfig;
2245
+ headers: readonly string[];
2246
+ /** The starting point, normally `suggestMapping`'s output. */
2247
+ plan: MappingPlan | null;
2248
+ language: string;
2249
+ /** Emitted whenever the user changes a selection. */
2250
+ readonly planChange: EventEmitter<MappingPlan>;
2251
+ protected readonly ui: UiTextService;
2252
+ protected readonly rows: i0.WritableSignal<MappingRow[]>;
2253
+ /**
2254
+ * How many fields a spreadsheet cannot carry at all.
2255
+ *
2256
+ * Shown here as well as in the template picker, because a user who already had a sheet
2257
+ * skipped that screen — and this is the only place they would otherwise find out that an
2258
+ * attachment column was never going to arrive.
2259
+ */
2260
+ protected readonly unsupported: i0.WritableSignal<number>;
2261
+ ngOnChanges(): void;
2262
+ protected mappedCount(): number;
2263
+ protected missingRequired(): number;
2264
+ protected choose(row: MappingRow, source: number | null): void;
2265
+ /** The current selections as the plan that will be applied. */
2266
+ private toPlan;
2267
+ static ɵfac: i0.ɵɵFactoryDeclaration<ImportMapperComponent, never>;
2268
+ static ɵcmp: i0.ɵɵComponentDeclaration<ImportMapperComponent, "ngx-import-mapper", never, { "config": { "alias": "config"; "required": true; }; "headers": { "alias": "headers"; "required": false; }; "plan": { "alias": "plan"; "required": false; }; "language": { "alias": "language"; "required": false; }; }, { "planChange": "planChange"; }, never, never, true, never>;
2269
+ }
2270
+
2271
+ /** One cell, already turned into the text the record will show. */
2272
+ interface PreviewCell {
2273
+ text: string;
2274
+ failed: boolean;
2275
+ }
2276
+ /** A row of cells, plus why the import will refuse it — if it will. */
2277
+ interface PreviewRow {
2278
+ cells: PreviewCell[];
2279
+ /** Empty when the row will be imported. */
2280
+ rejections: string[];
2281
+ }
2282
+ /**
2283
+ * The first few rows as the record will hold them, not as the sheet holds them.
2284
+ *
2285
+ * Every cell goes through the same `coerceCell` the import will use and then through
2286
+ * `formatDisplayValue`, which is what the renderer uses to show a stored value. So a
2287
+ * `dropdown` cell reading `Aktiv` shows as `Active` here if that is the option it resolved
2288
+ * to, and a cell that will fail shows its reason before anyone commits to anything.
2289
+ *
2290
+ * Showing the raw text instead would be easier and would preview the wrong thing: the point
2291
+ * of this screen is to catch a mapping that is subtly wrong, and a column of unchanged text
2292
+ * looks correct whatever field it was pointed at.
2293
+ *
2294
+ * **The rows are also run through the real import**, not just through coercion. `coerceCell`
2295
+ * alone catches a cell that will not parse and says nothing about a row that will be rejected
2296
+ * — a missing required field, a rule that forbids the combination — so a heading promising
2297
+ * "as they will be saved" was showing rows that would never be saved at all, looking exactly
2298
+ * like the ones that would. `applyMapping` over the sample answers that with the same engine
2299
+ * the commit uses, rather than a second opinion about what counts as valid.
2300
+ *
2301
+ * The whole grid is built once per input change rather than from template method calls.
2302
+ * Deriving the config's columns inside a `@for` means re-walking the entire schema for every
2303
+ * cell on every change-detection pass, which is a lot of work to produce five rows.
2304
+ */
2305
+ declare class ImportPreviewComponent implements OnChanges {
2306
+ config: EntityFormConfig;
2307
+ plan: MappingPlan;
2308
+ rows: readonly (readonly string[])[];
2309
+ lookups?: ImportLookups;
2310
+ /** The same rules the commit will apply; without them a rule-driven rejection is invisible. */
2311
+ rules?: readonly FormRule[];
2312
+ language: string;
2313
+ protected readonly ui: UiTextService;
2314
+ /** The columns this plan fills, in the plan's own order. */
2315
+ protected readonly columns: i0.WritableSignal<ImportColumn[]>;
2316
+ protected readonly grid: i0.WritableSignal<PreviewRow[]>;
2317
+ ngOnChanges(): void;
2318
+ static ɵfac: i0.ɵɵFactoryDeclaration<ImportPreviewComponent, never>;
2319
+ static ɵcmp: i0.ɵɵComponentDeclaration<ImportPreviewComponent, "ngx-import-preview", never, { "config": { "alias": "config"; "required": true; }; "plan": { "alias": "plan"; "required": true; }; "rows": { "alias": "rows"; "required": false; }; "lookups": { "alias": "lookups"; "required": false; }; "rules": { "alias": "rules"; "required": false; }; "language": { "alias": "language"; "required": false; }; }, {}, never, never, true, never>;
2320
+ }
2321
+
2322
+ /** One field offered for the template, with the guidance that belongs to it. */
2323
+ interface TemplateChoice {
2324
+ /** The ref with any row number removed, so a repeating field is offered once. */
2325
+ ref: string;
2326
+ header: string;
2327
+ note: string;
2328
+ selected: boolean;
2329
+ }
2330
+ /**
2331
+ * Pick the fields a template should carry, then download it.
2332
+ *
2333
+ * The guidance for each field is shown **here**, beside the checkbox, rather than written
2334
+ * into the file as a second header row. CSV has one header row and no concept of a second,
2335
+ * so a guidance row inside the file would come back as a record on re-import and fail row 2
2336
+ * of every import the user ever ran.
2337
+ *
2338
+ * A repeating field is offered once, not once per row number: "Contacts / Name" is a decision,
2339
+ * and "Contacts / Name 1", "Contacts / Name 2", "Contacts / Name 3" is the same decision
2340
+ * asked three times.
2341
+ */
2342
+ declare class ImportTemplateComponent implements OnChanges {
2343
+ config: EntityFormConfig;
2344
+ language: string;
2345
+ /** The refs to include, with row numbers stripped — the caller builds the spec. */
2346
+ readonly download: EventEmitter<string[]>;
2347
+ protected readonly ui: UiTextService;
2348
+ protected readonly choices: i0.WritableSignal<TemplateChoice[]>;
2349
+ protected readonly unsupported: i0.WritableSignal<UnsupportedColumn[]>;
2350
+ ngOnChanges(): void;
2351
+ protected selectedCount(): number;
2352
+ protected selectedRefs(): string[];
2353
+ protected toggle(ref: string): void;
2354
+ protected selectAll(selected: boolean): void;
2355
+ static ɵfac: i0.ɵɵFactoryDeclaration<ImportTemplateComponent, never>;
2356
+ static ɵcmp: i0.ɵɵComponentDeclaration<ImportTemplateComponent, "ngx-import-template", never, { "config": { "alias": "config"; "required": true; }; "language": { "alias": "language"; "required": false; }; }, { "download": "download"; }, never, never, true, never>;
2357
+ }
2358
+
2359
+ /**
2360
+ * The rows an import could not take, and why.
2361
+ *
2362
+ * Grouped by row rather than listed flat: a row with four problems is one row to go and fix,
2363
+ * and four separate lines invite the reader to think there are four. The row number is the
2364
+ * one the spreadsheet shows in its gutter — header counted as row 1 — because that is the
2365
+ * number they will scroll to.
2366
+ */
2367
+ declare class ImportErrorsComponent {
2368
+ errors: readonly ImportRowError[];
2369
+ language: string;
2370
+ protected readonly ui: UiTextService;
2371
+ /** Row number → its problems, in the order the rows appear in the file. */
2372
+ protected grouped(): {
2373
+ row: number;
2374
+ problems: ImportRowError[];
2375
+ }[];
2376
+ static ɵfac: i0.ɵɵFactoryDeclaration<ImportErrorsComponent, never>;
2377
+ static ɵcmp: i0.ɵɵComponentDeclaration<ImportErrorsComponent, "ngx-import-errors", never, { "errors": { "alias": "errors"; "required": false; }; "language": { "alias": "language"; "required": false; }; }, {}, never, never, true, never>;
2378
+ }
2379
+
1670
2380
  /**
1671
2381
  * What a field component must accept to be registered with `provideFieldTypes`.
1672
2382
  *
@@ -1745,7 +2455,7 @@ declare function fieldDomId(field: {
1745
2455
  * is read first. `null` rather than an empty string when there is nothing to describe — an
1746
2456
  * empty `aria-describedby` is a dangling reference, not an absent one.
1747
2457
  *
1748
- * Shared rather than written into each of the nineteen field components, because the rule is
2458
+ * Shared rather than written into each of the twenty-one field components, because the rule is
1749
2459
  * the same everywhere and getting it wrong is invisible until somebody uses a screen reader.
1750
2460
  */
1751
2461
  declare function fieldDescribedBy(domId: (suffix?: string) => string, parts: {
@@ -1876,7 +2586,14 @@ declare class MarkdownFieldComponent implements OnChanges {
1876
2586
  * A renderer is consumer code and may throw on input it does not like. Letting that
1877
2587
  * escape would take down the whole form over one malformed field, so it falls back to the
1878
2588
  * source text — which is what the field would have shown with no renderer at all.
2589
+ *
2590
+ * Memoised on the source, because this is called from the template: without it the
2591
+ * consumer's markdown parser ran on every change-detection pass, for every markdown field
2592
+ * on screen, over the whole document — which on a form holding a few long ones is a parse
2593
+ * per keystroke *elsewhere* in the form. The cache is keyed by the exact source string,
2594
+ * so a value that comes back to something already rendered is free too.
1879
2595
  */
2596
+ private cached?;
1880
2597
  protected rendered(): string | null;
1881
2598
  /**
1882
2599
  * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
@@ -2317,6 +3034,15 @@ declare class DropdownFieldComponent {
2317
3034
  isObjectVal(option: DropdownOption): boolean;
2318
3035
  get label(): string;
2319
3036
  get placeholder(): string;
3037
+ /**
3038
+ * The value this option stores — the option object itself.
3039
+ *
3040
+ * Kept as a method rather than binding `option` straight into the template, because it is
3041
+ * the one place that says *why* the two are the same thing. It used to delegate to
3042
+ * `getOptionStoredValue`, an exported identity function whose two branches returned their
3043
+ * argument unchanged; naming a transformation that does not happen is worse than naming
3044
+ * nothing, because a reader has to go and check.
3045
+ */
2320
3046
  getOptStoredVal(option: DropdownOption): unknown;
2321
3047
  getOptLabel(option: DropdownOption): string;
2322
3048
  /**
@@ -2359,6 +3085,15 @@ declare class RadioFieldComponent {
2359
3085
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
2360
3086
  readonly options: i0.WritableSignal<DropdownOption[]>;
2361
3087
  get label(): string;
3088
+ /**
3089
+ * The value this option stores — the option object itself.
3090
+ *
3091
+ * Kept as a method rather than binding `option` straight into the template, because it is
3092
+ * the one place that says *why* the two are the same thing. It used to delegate to
3093
+ * `getOptionStoredValue`, an exported identity function whose two branches returned their
3094
+ * argument unchanged; naming a transformation that does not happen is worse than naming
3095
+ * nothing, because a reader has to go and check.
3096
+ */
2362
3097
  getOptStoredVal(option: DropdownOption): unknown;
2363
3098
  /**
2364
3099
  * Stable DOM id for one radio, so the <label for> association works.
@@ -2419,6 +3154,15 @@ declare class MultiSelectFieldComponent {
2419
3154
  readonly compareFn: (o1: unknown, o2: unknown) => boolean;
2420
3155
  isObjectVal(option: DropdownOption): boolean;
2421
3156
  get label(): string;
3157
+ /**
3158
+ * The value this option stores — the option object itself.
3159
+ *
3160
+ * Kept as a method rather than binding `option` straight into the template, because it is
3161
+ * the one place that says *why* the two are the same thing. It used to delegate to
3162
+ * `getOptionStoredValue`, an exported identity function whose two branches returned their
3163
+ * argument unchanged; naming a transformation that does not happen is worse than naming
3164
+ * nothing, because a reader has to go and check.
3165
+ */
2422
3166
  getOptStoredVal(option: DropdownOption): unknown;
2423
3167
  getOptLabel(option: DropdownOption): string;
2424
3168
  /** Read-only display — synchronous, per §6.2. See `DropdownFieldComponent.getLabel`. */
@@ -2641,5 +3385,5 @@ declare class FileFieldComponent {
2641
3385
  static ɵcmp: i0.ɵɵComponentDeclaration<FileFieldComponent, "ngx-file-field", never, { "field": { "alias": "field"; "required": false; }; "control": { "alias": "control"; "required": false; }; "language": { "alias": "language"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "masked": { "alias": "masked"; "required": false; }; }, {}, never, never, true, never>;
2642
3386
  }
2643
3387
 
2644
- export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_UI_TEXT, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, fieldDescribedBy, fieldDomId, nextFieldInstanceId, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText };
2645
- export type { ChoiceOptionHost, ConfigSourceHandler, DynamicFieldComponentContract, EntityRefCacheStore, EntityRefSelection, HookFn, InvalidField, MessageOrderEntry, NgxDynamicEntityConfig, UiTextKey, UiTextOverrides, UiTextOverridesFor, UiTextParams, UiTextResolver, UiTextResolverFor, UiTextValue, ValidationMessage };
3388
+ export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_UI_TEXT, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityImportComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, HttpImportTransport, IMPORT_TRANSPORT, ImageFieldComponent, ImportErrorsComponent, ImportMapperComponent, ImportPreviewComponent, ImportTemplateComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LocalImportTransport, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SHEET_PARSER, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, defaultSheetParser, fieldDescribedBy, fieldDomId, nextFieldInstanceId, provideBuiltInFieldTypes, provideFieldTypes, provideHttpImportTransport, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText };
3389
+ export type { ChoiceOptionHost, ConfigSourceHandler, DynamicFieldComponentContract, EntityRefCacheStore, EntityRefSelection, HookFn, HttpImportTransportOptions, ImportContext, ImportPreview, ImportTransport, InvalidField, MessageOrderEntry, NgxDynamicEntityConfig, RuleSyncContext, SheetParser, TemplateFormat, UiTextKey, UiTextOverrides, UiTextOverridesFor, UiTextParams, UiTextResolver, UiTextResolverFor, UiTextValue, ValidationMessage };