ngx-dynamic-entity 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-dynamic-entity",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Angular standalone UI library for rendering dynamic entity forms.",
5
5
  "//typings": "Local workspace resolution only: ng-packagr 21 writes types/ngx-dynamic-entity.d.ts into dist/package.json. Do NOT add a 'main' field here — ng-packagr does not generate one, so it would be copied verbatim and resolve wrongly in the published package.",
6
6
  "typings": "types/ngx-dynamic-entity.d.ts",
@@ -8,7 +8,7 @@
8
8
  "@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
9
9
  "@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
10
10
  "@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
11
- "@dynamic-entity/core": "^2.2.0",
11
+ "@dynamic-entity/core": "^2.3.0",
12
12
  "rxjs": "^7.0.0"
13
13
  },
14
14
  "keywords": [
@@ -31,6 +31,9 @@
31
31
  "author": "Nizamudeen",
32
32
  "module": "fesm2022/ngx-dynamic-entity.mjs",
33
33
  "exports": {
34
+ "./styles.css": {
35
+ "default": "./styles.css"
36
+ },
34
37
  "./package.json": {
35
38
  "default": "./package.json"
36
39
  },
@@ -43,4 +46,4 @@
43
46
  "dependencies": {
44
47
  "tslib": "^2.3.0"
45
48
  }
46
- }
49
+ }
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
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, SliderBounds } from '@dynamic-entity/core';
3
+ import { ReferenceOption, LocalizedText, EntityFormConfig, FormRule, ImportLookups, MappingPlan, CsvDelimiter, ImportResult, TemplateSpec, SheetGrid, CommonModuleEntry, EntityReferenceLoader, LookupListSource, RecordMigration, FileUploadHandler, FieldValidators, NestedFieldConfig, ReferenceLoaderContext, DropdownOption, LookupListValue, NestedTabConfig, RuleEvaluationResult, FieldScopeEntry, EvaluateFormRulesOptions, ImportColumn, MappingEntry, UnsupportedColumn, ImportRowError, SliderBounds } 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
5
  import { ValidatorFn, AsyncValidatorFn, AbstractControl, ValidationErrors, FormGroup, FormArray } from '@angular/forms';
6
6
  import * as rxjs from 'rxjs';
@@ -120,7 +120,15 @@ declare const DEFAULT_UI_TEXT: {
120
120
  importGuessed: string;
121
121
  importMappedCount: string;
122
122
  importRequiredUnmapped: string;
123
+ importConstant: string;
124
+ importAddSlot: string;
125
+ importStaleHeading: string;
126
+ importStaleExplain: string;
127
+ importRemoveEntry: string;
123
128
  importPreviewHeading: string;
129
+ importDelimiterComma: string;
130
+ importDelimiterSemicolon: string;
131
+ importDelimiterTab: string;
124
132
  importRowRejected: string;
125
133
  importRunImport: string;
126
134
  importImporting: string;
@@ -219,27 +227,35 @@ declare class UiTextService {
219
227
  /**
220
228
  * Reads an uploaded file into headers and positional rows.
221
229
  *
222
- * Positional (`string[][]`), not keyed by header, because a real sheet carries duplicate
223
- * headers and blank ones — and because a mapping entry addresses a column by index for
224
- * exactly that reason. A parser that returned objects keyed by header would silently lose
225
- * the second of two columns both called "Notes".
230
+ * Positional, not keyed by header, because a real sheet carries duplicate headers and blank
231
+ * ones — and because a mapping entry addresses a column by index for exactly that reason. A
232
+ * parser that returned objects keyed by header would silently lose the second of two columns
233
+ * both called "Notes".
226
234
  *
227
- * The library ships a CSV implementation and no more. Registering one that understands
235
+ * Cells may keep their types (`SheetGrid`): a date cell handed over as a `Date` at UTC midnight
236
+ * is imported as that calendar date, exactly as the server reads the same workbook, where its
237
+ * text is whatever a library's formatter printed. A parser returning text (`SheetData`, as 2.2's
238
+ * did) still fits.
239
+ *
240
+ * The library ships a delimited-text reader and no more. Registering one that understands
228
241
  * `.xlsx` is how a consumer adds it, and which spreadsheet library they use stays their
229
242
  * choice — this package has no runtime dependency and gains none here.
230
243
  *
231
244
  * @example
245
+ * // SheetJS 0.20.3+ from https://cdn.sheetjs.com — the npm `xlsx` package stopped at 0.18.5,
246
+ * // which has known vulnerabilities. `UTC: true` matters: without it a date cell arrives at
247
+ * // *local* midnight, which is the previous day in UTC for anyone east of Greenwich.
232
248
  * provideNgxDynamicEntity({
233
249
  * sheetParser: async file => {
234
- * const workbook = XLSX.read(await file.arrayBuffer());
250
+ * const workbook = XLSX.read(await file.arrayBuffer(), { cellDates: true });
235
251
  * const rows = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], {
236
- * header: 1, raw: false, defval: '',
237
- * }) as string[][];
238
- * return { headers: rows[0] ?? [], rows: rows.slice(1) };
252
+ * header: 1, raw: true, defval: '', UTC: true,
253
+ * }) as unknown[][];
254
+ * return { headers: (rows[0] ?? []).map(cellText), rows: rows.slice(1) };
239
255
  * },
240
256
  * })
241
257
  */
242
- type SheetParser = (file: File) => SheetData | Promise<SheetData>;
258
+ type SheetParser = (file: File) => SheetGrid | Promise<SheetGrid>;
243
259
  /** The formats a generated template can be written in. CSV needs no dependency. */
244
260
  type TemplateFormat = 'csv' | 'xlsx';
245
261
  /** What the wizard needs to know about the entity it is importing into. */
@@ -266,6 +282,13 @@ interface ImportPreview {
266
282
  suggestion: MappingPlan;
267
283
  /** Total data rows in the file, which may be far more than `sample` holds. */
268
284
  rowCount: number;
285
+ /**
286
+ * Rows of each repeating field the suggestion was sized for (`arrayBoundFor`). Optional so a
287
+ * transport written for 2.2 keeps compiling; the mapper sizes itself the same way regardless.
288
+ */
289
+ arrayBound?: number;
290
+ /** The field separator a delimited-text file used. Absent for a workbook. */
291
+ delimiter?: CsvDelimiter;
269
292
  }
270
293
  /**
271
294
  * Where the work happens.
@@ -1001,6 +1024,11 @@ interface RuleSyncContext {
1001
1024
  addressOf(field: NestedFieldConfig): string;
1002
1025
  /** The control a scope entry addresses, or `null` when it has no static path. */
1003
1026
  controlFor(entry: FieldScopeEntry): AbstractControl | null;
1027
+ /**
1028
+ * The config the entries came from. Needed to know which tab owns each field: a tab that is
1029
+ * not rendered hides every field it owns. Omitted, only field-level visibility applies.
1030
+ */
1031
+ config?: EntityFormConfig | null;
1004
1032
  }
1005
1033
  /**
1006
1034
  * RulesEvaluationService — evaluates rules, and owns what a result *means*.
@@ -1046,12 +1074,25 @@ declare class RulesEvaluationService {
1046
1074
  * The predicate is `isFieldVisible`, the same one the render filter uses, so what is on
1047
1075
  * screen and what counts toward validity cannot drift apart.
1048
1076
  *
1077
+ * A tab that is not rendered — hidden by a rule, or `visibility: false` with no rule showing
1078
+ * it — hides everything it owns, sub-tabs and container children included. Only fields were
1079
+ * checked here, so a rule hiding a tab with a required field on it left the form invalid
1080
+ * with the field out of sight and out of the error summary: Save did nothing, silently.
1081
+ * Import already relaxed a hidden tab's fields, so the form refused rows import accepted.
1082
+ *
1049
1083
  * Entries must arrive parent-first, which is what `collectFieldScopes` yields: a container's
1050
1084
  * children are skipped once the container itself is hidden, because Angular's `disable()`
1051
1085
  * cascades to descendants and a child's `enable()` walks back up recalculating ancestors.
1052
1086
  * Toggling a child inside a hidden group would fight its parent.
1053
1087
  */
1054
1088
  syncHiddenFieldState(entries: readonly FieldScopeEntry[], ctx: RuleSyncContext): void;
1089
+ /**
1090
+ * Every field owned by a tab that does not render, under `isTabVisible`'s precedence.
1091
+ *
1092
+ * Stops descending at the first hidden tab: `fieldsUnderTab` already covers its sub-tabs,
1093
+ * and a sub-tab of a hidden tab is not rendered whatever its own visibility says.
1094
+ */
1095
+ private fieldsOnHiddenTabs;
1055
1096
  /**
1056
1097
  * The hidden and shown names as sets, memoised against the result they came from.
1057
1098
  *
@@ -2135,6 +2176,8 @@ declare class EntityImportComponent {
2135
2176
  protected readonly headers: i0.WritableSignal<string[]>;
2136
2177
  protected readonly sample: i0.WritableSignal<string[][]>;
2137
2178
  protected readonly rowCount: i0.WritableSignal<number>;
2179
+ /** The separator a delimited file turned out to use, so the user can see how it was read. */
2180
+ protected readonly delimiter: i0.WritableSignal<CsvDelimiter>;
2138
2181
  protected readonly fileName: i0.WritableSignal<string>;
2139
2182
  protected readonly plan: i0.WritableSignal<MappingPlan>;
2140
2183
  protected readonly result: i0.WritableSignal<ImportResult>;
@@ -2186,6 +2229,10 @@ declare class EntityImportComponent {
2186
2229
  * exact and `failed` is absent.
2187
2230
  */
2188
2231
  protected failedRowCount(result: ImportResult): number;
2232
+ /** The decimal mark the commit will read numbers with, so the preview reads them the same. */
2233
+ protected decimal(): '.' | ',';
2234
+ /** How the file's columns were split, in words — or nothing for a workbook. */
2235
+ protected delimiterText(): string | null;
2189
2236
  protected restart(): void;
2190
2237
  static ɵfac: i0.ɵɵFactoryDeclaration<EntityImportComponent, never>;
2191
2238
  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>;
@@ -2194,9 +2241,18 @@ declare class EntityImportComponent {
2194
2241
  /** One target field, and which of the sheet's columns feeds it. */
2195
2242
  interface MappingRow {
2196
2243
  column: ImportColumn;
2197
- /** Index into the sheet's headers, or `null` for "not imported". */
2244
+ /** Index into the sheet's headers, `CONSTANT` for a fixed value, or `null` for "not imported". */
2198
2245
  source: number | null;
2199
2246
  guessed: boolean;
2247
+ /** The incoming plan's entry for this field, emitted untouched until the user changes it. */
2248
+ entry?: MappingEntry;
2249
+ /** The user changed this row, so `entry` no longer describes it. */
2250
+ dirty: boolean;
2251
+ }
2252
+ /** A repeating field, for its "Add a row" button. */
2253
+ interface ArraySlots {
2254
+ arrayRef: string;
2255
+ label: string;
2200
2256
  }
2201
2257
  /**
2202
2258
  * Match the sheet's columns to the config's fields.
@@ -2227,11 +2283,39 @@ declare class ImportMapperComponent implements OnChanges {
2227
2283
  * attachment column was never going to arrive.
2228
2284
  */
2229
2285
  protected readonly unsupported: i0.WritableSignal<number>;
2286
+ /** A plan entry whose field this config no longer has. Kept, and emitted, until removed. */
2287
+ protected readonly stale: i0.WritableSignal<MappingEntry[]>;
2288
+ /** Repeating fields the user can add a row to. */
2289
+ protected readonly arrays: i0.WritableSignal<ArraySlots[]>;
2290
+ protected readonly constant = -1;
2291
+ /** Rows added by hand per repeating field, on top of what the sheet and plan imply. */
2292
+ private readonly extraSlots;
2293
+ /** The incoming plan, read through the 2.2 alias. Every entry in it survives `toPlan`. */
2294
+ private incoming;
2230
2295
  ngOnChanges(): void;
2296
+ /**
2297
+ * Rows for every field, sized so nothing the plan or the sheet names is out of reach.
2298
+ *
2299
+ * Sizing from the default three meant a sheet with `Phone 4` could not be mapped at all,
2300
+ * and a stored plan reaching further lost those entries on the next change — `toPlan` only
2301
+ * ever emitted rows, and there were none for them.
2302
+ */
2303
+ private rebuild;
2231
2304
  protected mappedCount(): number;
2232
2305
  protected missingRequired(): number;
2306
+ protected hasConstant(row: MappingRow): boolean;
2307
+ protected constantText(entry: MappingEntry): string;
2233
2308
  protected choose(row: MappingRow, source: number | null): void;
2234
- /** The current selections as the plan that will be applied. */
2309
+ /** One more row for one repeating field, for this session. */
2310
+ protected addSlot(arrayRef: string): void;
2311
+ protected removeStale(entry: MappingEntry): void;
2312
+ /**
2313
+ * The current selections as the plan that will be applied.
2314
+ *
2315
+ * Every entry of the incoming plan survives in its own position — a fixed value, a ref the
2316
+ * config no longer has — unless the user changed or removed it, and an untouched entry is
2317
+ * emitted exactly as it arrived. New selections follow, in field order.
2318
+ */
2235
2319
  private toPlan;
2236
2320
  static ɵfac: i0.ɵɵFactoryDeclaration<ImportMapperComponent, never>;
2237
2321
  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>;
@@ -2279,13 +2363,15 @@ declare class ImportPreviewComponent implements OnChanges {
2279
2363
  /** The same rules the commit will apply; without them a rule-driven rejection is invisible. */
2280
2364
  rules?: readonly FormRule[];
2281
2365
  language: string;
2366
+ /** The decimal mark number cells use — the one the commit will read them with. */
2367
+ decimal: '.' | ',';
2282
2368
  protected readonly ui: UiTextService;
2283
2369
  /** The columns this plan fills, in the plan's own order. */
2284
2370
  protected readonly columns: i0.WritableSignal<ImportColumn[]>;
2285
2371
  protected readonly grid: i0.WritableSignal<PreviewRow[]>;
2286
2372
  ngOnChanges(): void;
2287
2373
  static ɵfac: i0.ɵɵFactoryDeclaration<ImportPreviewComponent, never>;
2288
- 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>;
2374
+ 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; }; "decimal": { "alias": "decimal"; "required": false; }; }, {}, never, never, true, never>;
2289
2375
  }
2290
2376
 
2291
2377
  /** One field offered for the template, with the guidance that belongs to it. */