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.
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, ChangeDetectorRef, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, Optional, Inject, EventEmitter, ElementRef, computed, afterNextRender, HostListener, ViewChildren, Output } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { ReactiveFormsModule, Validators, FormBuilder, FormGroup, FormArray, FormsModule } from '@angular/forms';
5
- import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, isValidUrl, isValidPhone, fileRefName, sliderBounds, ratingScale, normalizeHexColor, normalizeTags, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, getTabPath, fieldRefFor, toRefToken, refOf, parseFieldRef, setTabData, setValueByPath, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds, collectFieldScopes, applyAutoPatch, isUnsafePath, fieldsUnderTab, valuesEqual, parseCsv, deriveImportColumns, suggestMapping, applyMapping, toCsv, stripIndices, CORE_VERSION, coerceCell, buildTemplateSpec, collectLeafTargets, formatConfigProblems } from '@dynamic-entity/core';
5
+ import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, isValidUrl, isValidPhone, fileRefName, sliderBounds, ratingScale, normalizeHexColor, normalizeTags, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, fieldsUnderTab, getTabPath, fieldRefFor, flattenFieldValues, parseFieldRef, setTabData, setValueByPath, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds, toRefToken, collectFieldScopes, applyAutoPatch, refOf, isUnsafePath, valuesEqual, parseCsv, arrayBoundFor, deriveImportColumns, suggestMapping, cellText, applyMapping, decimalMarkFor, toCsv, stripIndices, CORE_VERSION, DEFAULT_ARRAY_ROWS, upgradeLegacyRefs, coerceCell, buildTemplateSpec, collectLeafTargets, formatConfigProblems } from '@dynamic-entity/core';
6
6
  export { formatDisplayValue, normalizeConfig, normalizeOption, resolveLabel, resolveOptionLabel, resolveOptionValue, valuesMatch } from '@dynamic-entity/core';
7
7
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
8
  import { Subject, isObservable, firstValueFrom } from 'rxjs';
@@ -920,8 +920,16 @@ const DEFAULT_UI_TEXT = {
920
920
  importGuessed: 'Guessed',
921
921
  importMappedCount: '{mapped} of {total} field(s) matched.',
922
922
  importRequiredUnmapped: '{count} required field(s) still need a column.',
923
+ importConstant: 'Fixed value: {value}',
924
+ importAddSlot: 'Add a {label} row',
925
+ importStaleHeading: 'Mapped to fields this form no longer has',
926
+ importStaleExplain: 'The import will refuse this mapping until these are removed.',
927
+ importRemoveEntry: 'Remove',
923
928
  // Import wizard — review and result
924
929
  importPreviewHeading: 'First {count} row(s), as they will be saved',
930
+ importDelimiterComma: 'Columns are separated by commas.',
931
+ importDelimiterSemicolon: 'Columns are separated by semicolons, and numbers use a decimal comma.',
932
+ importDelimiterTab: 'Columns are separated by tabs.',
925
933
  importRowRejected: 'This row will not be imported: {reason}',
926
934
  importRunImport: 'Import {count} row(s)',
927
935
  importImporting: 'Importing…',
@@ -7825,6 +7833,12 @@ class RulesEvaluationService {
7825
7833
  * The predicate is `isFieldVisible`, the same one the render filter uses, so what is on
7826
7834
  * screen and what counts toward validity cannot drift apart.
7827
7835
  *
7836
+ * A tab that is not rendered — hidden by a rule, or `visibility: false` with no rule showing
7837
+ * it — hides everything it owns, sub-tabs and container children included. Only fields were
7838
+ * checked here, so a rule hiding a tab with a required field on it left the form invalid
7839
+ * with the field out of sight and out of the error summary: Save did nothing, silently.
7840
+ * Import already relaxed a hidden tab's fields, so the form refused rows import accepted.
7841
+ *
7828
7842
  * Entries must arrive parent-first, which is what `collectFieldScopes` yields: a container's
7829
7843
  * children are skipped once the container itself is hidden, because Angular's `disable()`
7830
7844
  * cascades to descendants and a child's `enable()` walks back up recalculating ancestors.
@@ -7832,6 +7846,7 @@ class RulesEvaluationService {
7832
7846
  */
7833
7847
  syncHiddenFieldState(entries, ctx) {
7834
7848
  const hiddenContainers = [];
7849
+ const onHiddenTab = this.fieldsOnHiddenTabs(ctx.result, ctx.config);
7835
7850
  for (const entry of entries) {
7836
7851
  const field = entry.field;
7837
7852
  if (!field?.id)
@@ -7839,7 +7854,7 @@ class RulesEvaluationService {
7839
7854
  const address = ctx.addressOf(field);
7840
7855
  if (hiddenContainers.some(prefix => address.startsWith(`${prefix}.`)))
7841
7856
  continue;
7842
- const hidden = !this.isFieldVisible(ctx.result, field, ctx.namesOf(field), ctx.values);
7857
+ const hidden = onHiddenTab.has(field) || !this.isFieldVisible(ctx.result, field, ctx.namesOf(field), ctx.values);
7843
7858
  if (hidden && (field.type === 'group' || field.type === 'array'))
7844
7859
  hiddenContainers.push(address);
7845
7860
  const ctrl = ctx.controlFor(entry);
@@ -7854,6 +7869,30 @@ class RulesEvaluationService {
7854
7869
  }
7855
7870
  }
7856
7871
  }
7872
+ /**
7873
+ * Every field owned by a tab that does not render, under `isTabVisible`'s precedence.
7874
+ *
7875
+ * Stops descending at the first hidden tab: `fieldsUnderTab` already covers its sub-tabs,
7876
+ * and a sub-tab of a hidden tab is not rendered whatever its own visibility says.
7877
+ */
7878
+ fieldsOnHiddenTabs(result, config) {
7879
+ const out = new Set();
7880
+ const visit = (tabs) => {
7881
+ for (const tab of Array.isArray(tabs) ? tabs : []) {
7882
+ if (!tab || typeof tab !== 'object' || !tab.id)
7883
+ continue;
7884
+ if (this.isTabVisible(result, tab)) {
7885
+ visit(tab.children);
7886
+ }
7887
+ else {
7888
+ for (const entry of fieldsUnderTab(config, tab.id))
7889
+ out.add(entry.field);
7890
+ }
7891
+ }
7892
+ };
7893
+ visit(config?.tabs);
7894
+ return out;
7895
+ }
7857
7896
  /**
7858
7897
  * The hidden and shown names as sets, memoised against the result they came from.
7859
7898
  *
@@ -8063,18 +8102,11 @@ class FormStructureService {
8063
8102
  * unknown>` and needs no knowledge of refs at all — the extra keys simply resolve.
8064
8103
  */
8065
8104
  flattenValues(form, entries) {
8066
- const out = {};
8067
- for (const entry of entries) {
8068
- const field = entry.field;
8069
- if (!field?.id)
8070
- continue;
8105
+ // The naming rule is core's, shared with the importer; only where values come from is ours.
8106
+ return flattenFieldValues(entries, entry => {
8071
8107
  const ctrl = this.controlAt(form, entry);
8072
- if (!ctrl)
8073
- continue;
8074
- out[field.id] = ctrl.value;
8075
- out[toRefToken(refOf(field, entry.scope))] = ctrl.value;
8076
- }
8077
- return out;
8108
+ return ctrl ? { value: ctrl.value } : undefined;
8109
+ });
8078
8110
  }
8079
8111
  /**
8080
8112
  * Find a control by reference: a bracketed path, or a bare field id.
@@ -9235,6 +9267,7 @@ class DynamicFormComponent {
9235
9267
  namesOf: field => this.namesOf(field),
9236
9268
  addressOf: field => this.addressOf(field),
9237
9269
  controlFor: entry => this.controlForEntry(entry),
9270
+ config: this.config,
9238
9271
  });
9239
9272
  }
9240
9273
  /** Every field in the config, including `group`/`array` children. */
@@ -10086,7 +10119,10 @@ const defaultSheetParser = async (file) => {
10086
10119
  throw new Error(`Cannot read "${file.name}". The built-in parser handles CSV only — register a ` +
10087
10120
  'sheetParser with provideNgxDynamicEntity({ sheetParser }) to accept other formats.');
10088
10121
  }
10089
- return parseCsv(await file.text());
10122
+ // A `.tsv` says what it is. Anything else is read the way `parseCsv` reads text — the
10123
+ // separator its header line uses most — which is also how the server reads an upload.
10124
+ const tabs = /\.tsv$/i.test(file?.name ?? '');
10125
+ return parseCsv(await file.text(), tabs ? { delimiter: '\t' } : {});
10090
10126
  };
10091
10127
 
10092
10128
  /**
@@ -10134,12 +10170,18 @@ class LocalImportTransport {
10134
10170
  }
10135
10171
  async preview(file, context) {
10136
10172
  const sheet = await this.read(file);
10137
- const { columns } = deriveImportColumns(context.config, { lang: context.lang });
10173
+ // Sized from the headers exactly as the server sizes them, so both offer the same slots.
10174
+ const arrayBound = arrayBoundFor(sheet.headers, context.config, null, context.lang);
10175
+ const { columns } = deriveImportColumns(context.config, { lang: context.lang, maxArrayRows: arrayBound });
10138
10176
  return {
10139
10177
  headers: sheet.headers,
10140
- sample: sheet.rows.slice(0, SAMPLE_ROWS),
10178
+ // Rendered the way the server renders it, so a typed date shows — and previews — as the
10179
+ // calendar date the commit will store, not as a timestamp.
10180
+ sample: sheet.rows.slice(0, SAMPLE_ROWS).map(row => row.map(cellText)),
10141
10181
  rowCount: sheet.rows.length,
10142
10182
  suggestion: suggestMapping(sheet.headers, columns, context.config?.entity ?? ''),
10183
+ arrayBound,
10184
+ ...(sheet.delimiter ? { delimiter: sheet.delimiter } : {}),
10143
10185
  };
10144
10186
  }
10145
10187
  async commit(file, plan, context) {
@@ -10148,6 +10190,8 @@ class LocalImportTransport {
10148
10190
  lang: context.lang,
10149
10191
  rules: context.rules,
10150
10192
  lookups: context.lookups,
10193
+ // A `;` file means `1,5` is one and a half — the same call the server makes.
10194
+ decimal: decimalMarkFor(sheet.delimiter),
10151
10195
  });
10152
10196
  }
10153
10197
  async template(spec, format) {
@@ -10309,6 +10353,9 @@ class HttpImportTransport {
10309
10353
  sample: preview.sample ?? [],
10310
10354
  suggestion: preview.suggestion,
10311
10355
  rowCount: preview.rowCount ?? 0,
10356
+ // A server older than 2.3 does not say, and always sized for the default.
10357
+ arrayBound: preview.arrayBound ?? DEFAULT_ARRAY_ROWS,
10358
+ ...(preview.delimiter ? { delimiter: preview.delimiter } : {}),
10312
10359
  };
10313
10360
  }
10314
10361
  async commit(file, plan, context) {
@@ -10468,6 +10515,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
10468
10515
  type: Input
10469
10516
  }] } });
10470
10517
 
10518
+ /** The select's value for "keep the plan's fixed value", which is not a column. */
10519
+ const CONSTANT = -1;
10471
10520
  /**
10472
10521
  * Match the sheet's columns to the config's fields.
10473
10522
  *
@@ -10497,18 +10546,70 @@ class ImportMapperComponent {
10497
10546
  * attachment column was never going to arrive.
10498
10547
  */
10499
10548
  unsupported = signal(0, ...(ngDevMode ? [{ debugName: "unsupported" }] : /* istanbul ignore next */ []));
10549
+ /** A plan entry whose field this config no longer has. Kept, and emitted, until removed. */
10550
+ stale = signal([], ...(ngDevMode ? [{ debugName: "stale" }] : /* istanbul ignore next */ []));
10551
+ /** Repeating fields the user can add a row to. */
10552
+ arrays = signal([], ...(ngDevMode ? [{ debugName: "arrays" }] : /* istanbul ignore next */ []));
10553
+ constant = CONSTANT;
10554
+ /** Rows added by hand per repeating field, on top of what the sheet and plan imply. */
10555
+ extraSlots = new Map();
10556
+ /** The incoming plan, read through the 2.2 alias. Every entry in it survives `toPlan`. */
10557
+ incoming = null;
10500
10558
  ngOnChanges() {
10501
- const { columns, unsupported } = deriveImportColumns(this.config, { lang: this.language });
10559
+ this.incoming = this.plan ? upgradeLegacyRefs(this.plan, this.config) : null;
10560
+ this.extraSlots.clear();
10561
+ this.rows.set([]);
10562
+ this.rebuild();
10563
+ }
10564
+ /**
10565
+ * Rows for every field, sized so nothing the plan or the sheet names is out of reach.
10566
+ *
10567
+ * Sizing from the default three meant a sheet with `Phone 4` could not be mapped at all,
10568
+ * and a stored plan reaching further lost those entries on the next change — `toPlan` only
10569
+ * ever emitted rows, and there were none for them.
10570
+ */
10571
+ rebuild() {
10572
+ const entries = (this.incoming?.entries ?? []).filter(Boolean);
10573
+ const base = arrayBoundFor(this.headers, this.config, this.incoming, this.language);
10574
+ const extra = Math.max(0, ...this.extraSlots.values());
10575
+ // Readonly and system fields are derived so a plan that maps one keeps it, but only
10576
+ // offered as a row when the plan does — nobody maps a column onto a field the form fills.
10577
+ const { columns, unsupported } = deriveImportColumns(this.config, {
10578
+ lang: this.language,
10579
+ maxArrayRows: base + extra,
10580
+ includeReadonly: true,
10581
+ includeSystemDefault: true,
10582
+ });
10502
10583
  this.unsupported.set(unsupported.length);
10503
- const byRef = new Map((this.plan?.entries ?? []).map(entry => [entry.ref, entry]));
10504
- this.rows.set(columns.map(column => {
10584
+ const byRef = new Map(entries.map(entry => [entry.ref, entry]));
10585
+ const known = new Set(columns.map(column => column.ref));
10586
+ const previous = new Map(this.rows().map(row => [row.column.ref, row]));
10587
+ const offered = columns.filter(column => (byRef.has(column.ref) || (!column.field.readonly && !column.field.systemDefault)) &&
10588
+ (column.arrayRef === undefined ||
10589
+ byRef.has(column.ref) ||
10590
+ column.arrayIndex < base + (this.extraSlots.get(column.arrayRef) ?? 0)));
10591
+ this.rows.set(offered.map(column => {
10592
+ // A choice already made survives a rebuild for an added slot.
10593
+ const kept = previous.get(column.ref);
10594
+ if (kept)
10595
+ return { ...kept, column };
10505
10596
  const entry = byRef.get(column.ref);
10506
10597
  return {
10507
10598
  column,
10508
- source: entry?.column ?? null,
10599
+ source: entry?.column ?? (entry?.constant !== undefined ? CONSTANT : null),
10509
10600
  guessed: entry?.confidence === 'guess',
10601
+ entry,
10602
+ dirty: false,
10510
10603
  };
10511
10604
  }));
10605
+ this.stale.set(entries.filter(entry => !known.has(entry.ref)));
10606
+ const arrays = new Map();
10607
+ for (const column of offered) {
10608
+ if (column.arrayRef !== undefined && !arrays.has(column.arrayRef)) {
10609
+ arrays.set(column.arrayRef, column.arrayLabel ?? column.arrayRef);
10610
+ }
10611
+ }
10612
+ this.arrays.set([...arrays].map(([arrayRef, label]) => ({ arrayRef, label })));
10512
10613
  }
10513
10614
  mappedCount() {
10514
10615
  return this.rows().filter(row => row.source !== null).length;
@@ -10516,24 +10617,75 @@ class ImportMapperComponent {
10516
10617
  missingRequired() {
10517
10618
  return this.rows().filter(row => row.column.required && row.source === null).length;
10518
10619
  }
10620
+ hasConstant(row) {
10621
+ return !!row.entry && row.entry.constant !== undefined && row.entry.column === undefined;
10622
+ }
10623
+ constantText(entry) {
10624
+ const value = entry.constant;
10625
+ return typeof value === 'string' ? value : JSON.stringify(value);
10626
+ }
10519
10627
  choose(row, source) {
10520
10628
  this.rows.update(rows => rows.map(candidate => candidate.column.ref === row.column.ref
10521
- ? // A hand-made choice is not a guess any more, so the badge goes.
10522
- { ...candidate, source, guessed: false }
10629
+ ? // A hand-made choice is not a guess any more, so the badge goes. Going back to
10630
+ // the plan's fixed value is going back to its entry, untouched.
10631
+ { ...candidate, source, guessed: false, dirty: source !== CONSTANT }
10523
10632
  : candidate));
10524
10633
  this.planChange.emit(this.toPlan());
10525
10634
  }
10526
- /** The current selections as the plan that will be applied. */
10635
+ /** One more row for one repeating field, for this session. */
10636
+ addSlot(arrayRef) {
10637
+ this.extraSlots.set(arrayRef, (this.extraSlots.get(arrayRef) ?? 0) + 1);
10638
+ this.rebuild();
10639
+ }
10640
+ removeStale(entry) {
10641
+ this.stale.update(stale => stale.filter(candidate => candidate !== entry));
10642
+ this.planChange.emit(this.toPlan());
10643
+ }
10644
+ /**
10645
+ * The current selections as the plan that will be applied.
10646
+ *
10647
+ * Every entry of the incoming plan survives in its own position — a fixed value, a ref the
10648
+ * config no longer has — unless the user changed or removed it, and an untouched entry is
10649
+ * emitted exactly as it arrived. New selections follow, in field order.
10650
+ */
10527
10651
  toPlan() {
10528
- const entries = this.rows()
10529
- .filter(row => row.source !== null)
10530
- .map(row => ({
10531
- ref: row.column.ref,
10532
- column: row.source,
10533
- header: this.headers[row.source],
10534
- confidence: row.guessed ? 'guess' : 'exact',
10535
- }));
10652
+ const rowByRef = new Map(this.rows().map(row => [row.column.ref, row]));
10653
+ const stale = new Set(this.stale());
10654
+ const emitted = new Set();
10655
+ const entries = [];
10656
+ const entryOf = (row) => {
10657
+ if (!row.dirty && row.entry)
10658
+ return row.entry;
10659
+ if (row.source === null || row.source === CONSTANT)
10660
+ return null;
10661
+ return {
10662
+ ref: row.column.ref,
10663
+ column: row.source,
10664
+ header: this.headers[row.source],
10665
+ confidence: row.guessed ? 'guess' : 'exact',
10666
+ };
10667
+ };
10668
+ for (const original of this.incoming?.entries ?? []) {
10669
+ const row = original ? rowByRef.get(original.ref) : undefined;
10670
+ if (row) {
10671
+ emitted.add(row.column.ref);
10672
+ const entry = entryOf(row);
10673
+ if (entry)
10674
+ entries.push(entry);
10675
+ }
10676
+ else if (stale.has(original)) {
10677
+ entries.push(original);
10678
+ }
10679
+ }
10680
+ for (const row of this.rows()) {
10681
+ if (emitted.has(row.column.ref))
10682
+ continue;
10683
+ const entry = entryOf(row);
10684
+ if (entry)
10685
+ entries.push(entry);
10686
+ }
10536
10687
  return {
10688
+ ...this.incoming,
10537
10689
  entity: this.config?.entity ?? '',
10538
10690
  configVersion: this.config?.version,
10539
10691
  sourceHeaders: [...this.headers],
@@ -10582,6 +10734,11 @@ class ImportMapperComponent {
10582
10734
  (ngModelChange)="choose(row, $event)"
10583
10735
  >
10584
10736
  <option [ngValue]="null">{{ ui.text('importUnmapped', language) }}</option>
10737
+ @if (hasConstant(row)) {
10738
+ <option [ngValue]="constant">
10739
+ {{ ui.text('importConstant', language, { value: constantText(row.entry!) }) }}
10740
+ </option>
10741
+ }
10585
10742
  @for (header of headers; track $index) {
10586
10743
  <option [ngValue]="$index">{{ header || '—' }}</option>
10587
10744
  }
@@ -10596,8 +10753,47 @@ class ImportMapperComponent {
10596
10753
  }
10597
10754
  </tbody>
10598
10755
  </table>
10756
+
10757
+ @if (arrays().length) {
10758
+ <p class="ngx-import-mapper__slots">
10759
+ @for (array of arrays(); track array.arrayRef) {
10760
+ <button
10761
+ type="button"
10762
+ class="ngx-import-mapper__add-slot"
10763
+ [attr.data-testid]="'import-add-slot-' + array.arrayRef"
10764
+ (click)="addSlot(array.arrayRef)"
10765
+ >
10766
+ {{ ui.text('importAddSlot', language, { label: array.label }) }}
10767
+ </button>
10768
+ }
10769
+ </p>
10770
+ }
10771
+
10772
+ @if (stale().length) {
10773
+ <div class="ngx-import-mapper__stale" data-testid="import-stale" role="alert">
10774
+ <p>
10775
+ <strong>{{ ui.text('importStaleHeading', language) }}</strong>
10776
+ {{ ui.text('importStaleExplain', language) }}
10777
+ </p>
10778
+ <ul>
10779
+ @for (entry of stale(); track entry.ref) {
10780
+ <li [attr.data-testid]="'import-stale-' + entry.ref">
10781
+ <code>{{ entry.ref }}</code>
10782
+ <button
10783
+ type="button"
10784
+ class="ngx-import-mapper__remove"
10785
+ [attr.data-testid]="'import-remove-' + entry.ref"
10786
+ (click)="removeStale(entry)"
10787
+ >
10788
+ {{ ui.text('importRemoveEntry', language) }}
10789
+ </button>
10790
+ </li>
10791
+ }
10792
+ </ul>
10793
+ </div>
10794
+ }
10599
10795
  </section>
10600
- `, isInline: true, styles: [".ngx-import-mapper__summary,.ngx-import-mapper__warning,.ngx-import-mapper__note{margin:0 0 10px}.ngx-import-mapper__warning{color:var(--ngx-color-warning, #b45309);background:var(--ngx-color-warning-soft, #fffbeb);border:1px solid var(--ngx-color-border, #fef3c7);border-radius:var(--ngx-radius-sm, 6px);padding:8px 12px}.ngx-import-mapper__note{color:var(--ngx-color-muted, #6b7280)}.ngx-import-mapper__table{width:100%;border-collapse:collapse}.ngx-import-mapper__table th,.ngx-import-mapper__table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--ngx-color-border, #e5e7eb);vertical-align:middle}.ngx-import-mapper__table thead th{color:var(--ngx-color-muted, #6b7280);font-size:var(--ngx-font-size, 13px);text-transform:uppercase;letter-spacing:.04em}.ngx-import-mapper__required{color:var(--ngx-color-error, #b91c1c);margin-left:2px}.ngx-import-mapper__select{min-width:220px;max-width:100%;padding:6px 8px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit}.ngx-import-mapper__guess{margin-left:8px;padding:2px 8px;border-radius:999px;font-size:12px;color:var(--ngx-color-info, #1d4ed8);background:var(--ngx-color-info-soft, #eff6ff)}@media(max-width:640px){.ngx-import-mapper__select{min-width:0;width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10796
+ `, isInline: true, styles: [".ngx-import-mapper__summary,.ngx-import-mapper__warning,.ngx-import-mapper__note{margin:0 0 10px}.ngx-import-mapper__warning{color:var(--ngx-color-warning, #b45309);background:var(--ngx-color-warning-soft, #fffbeb);border:1px solid var(--ngx-color-border, #fef3c7);border-radius:var(--ngx-radius-sm, 6px);padding:8px 12px}.ngx-import-mapper__note{color:var(--ngx-color-muted, #6b7280)}.ngx-import-mapper__table{width:100%;border-collapse:collapse}.ngx-import-mapper__table th,.ngx-import-mapper__table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--ngx-color-border, #e5e7eb);vertical-align:middle}.ngx-import-mapper__table thead th{color:var(--ngx-color-muted, #6b7280);font-size:var(--ngx-font-size, 13px);text-transform:uppercase;letter-spacing:.04em}.ngx-import-mapper__required{color:var(--ngx-color-error, #b91c1c);margin-left:2px}.ngx-import-mapper__select{min-width:220px;max-width:100%;padding:6px 8px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit}.ngx-import-mapper__slots{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 0}.ngx-import-mapper__add-slot,.ngx-import-mapper__remove{padding:4px 10px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit;cursor:pointer}.ngx-import-mapper__remove{margin-left:8px}.ngx-import-mapper__stale{margin-top:12px;padding:8px 12px;color:var(--ngx-color-error, #b91c1c);background:var(--ngx-color-error-soft, #fef2f2);border-radius:var(--ngx-radius-sm, 6px)}.ngx-import-mapper__stale p,.ngx-import-mapper__stale ul{margin:0 0 6px}.ngx-import-mapper__guess{margin-left:8px;padding:2px 8px;border-radius:999px;font-size:12px;color:var(--ngx-color-info, #1d4ed8);background:var(--ngx-color-info-soft, #eff6ff)}@media(max-width:640px){.ngx-import-mapper__select{min-width:0;width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10601
10797
  }
10602
10798
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportMapperComponent, decorators: [{
10603
10799
  type: Component,
@@ -10642,6 +10838,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
10642
10838
  (ngModelChange)="choose(row, $event)"
10643
10839
  >
10644
10840
  <option [ngValue]="null">{{ ui.text('importUnmapped', language) }}</option>
10841
+ @if (hasConstant(row)) {
10842
+ <option [ngValue]="constant">
10843
+ {{ ui.text('importConstant', language, { value: constantText(row.entry!) }) }}
10844
+ </option>
10845
+ }
10645
10846
  @for (header of headers; track $index) {
10646
10847
  <option [ngValue]="$index">{{ header || '—' }}</option>
10647
10848
  }
@@ -10656,8 +10857,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
10656
10857
  }
10657
10858
  </tbody>
10658
10859
  </table>
10860
+
10861
+ @if (arrays().length) {
10862
+ <p class="ngx-import-mapper__slots">
10863
+ @for (array of arrays(); track array.arrayRef) {
10864
+ <button
10865
+ type="button"
10866
+ class="ngx-import-mapper__add-slot"
10867
+ [attr.data-testid]="'import-add-slot-' + array.arrayRef"
10868
+ (click)="addSlot(array.arrayRef)"
10869
+ >
10870
+ {{ ui.text('importAddSlot', language, { label: array.label }) }}
10871
+ </button>
10872
+ }
10873
+ </p>
10874
+ }
10875
+
10876
+ @if (stale().length) {
10877
+ <div class="ngx-import-mapper__stale" data-testid="import-stale" role="alert">
10878
+ <p>
10879
+ <strong>{{ ui.text('importStaleHeading', language) }}</strong>
10880
+ {{ ui.text('importStaleExplain', language) }}
10881
+ </p>
10882
+ <ul>
10883
+ @for (entry of stale(); track entry.ref) {
10884
+ <li [attr.data-testid]="'import-stale-' + entry.ref">
10885
+ <code>{{ entry.ref }}</code>
10886
+ <button
10887
+ type="button"
10888
+ class="ngx-import-mapper__remove"
10889
+ [attr.data-testid]="'import-remove-' + entry.ref"
10890
+ (click)="removeStale(entry)"
10891
+ >
10892
+ {{ ui.text('importRemoveEntry', language) }}
10893
+ </button>
10894
+ </li>
10895
+ }
10896
+ </ul>
10897
+ </div>
10898
+ }
10659
10899
  </section>
10660
- `, styles: [".ngx-import-mapper__summary,.ngx-import-mapper__warning,.ngx-import-mapper__note{margin:0 0 10px}.ngx-import-mapper__warning{color:var(--ngx-color-warning, #b45309);background:var(--ngx-color-warning-soft, #fffbeb);border:1px solid var(--ngx-color-border, #fef3c7);border-radius:var(--ngx-radius-sm, 6px);padding:8px 12px}.ngx-import-mapper__note{color:var(--ngx-color-muted, #6b7280)}.ngx-import-mapper__table{width:100%;border-collapse:collapse}.ngx-import-mapper__table th,.ngx-import-mapper__table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--ngx-color-border, #e5e7eb);vertical-align:middle}.ngx-import-mapper__table thead th{color:var(--ngx-color-muted, #6b7280);font-size:var(--ngx-font-size, 13px);text-transform:uppercase;letter-spacing:.04em}.ngx-import-mapper__required{color:var(--ngx-color-error, #b91c1c);margin-left:2px}.ngx-import-mapper__select{min-width:220px;max-width:100%;padding:6px 8px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit}.ngx-import-mapper__guess{margin-left:8px;padding:2px 8px;border-radius:999px;font-size:12px;color:var(--ngx-color-info, #1d4ed8);background:var(--ngx-color-info-soft, #eff6ff)}@media(max-width:640px){.ngx-import-mapper__select{min-width:0;width:100%}}\n"] }]
10900
+ `, styles: [".ngx-import-mapper__summary,.ngx-import-mapper__warning,.ngx-import-mapper__note{margin:0 0 10px}.ngx-import-mapper__warning{color:var(--ngx-color-warning, #b45309);background:var(--ngx-color-warning-soft, #fffbeb);border:1px solid var(--ngx-color-border, #fef3c7);border-radius:var(--ngx-radius-sm, 6px);padding:8px 12px}.ngx-import-mapper__note{color:var(--ngx-color-muted, #6b7280)}.ngx-import-mapper__table{width:100%;border-collapse:collapse}.ngx-import-mapper__table th,.ngx-import-mapper__table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--ngx-color-border, #e5e7eb);vertical-align:middle}.ngx-import-mapper__table thead th{color:var(--ngx-color-muted, #6b7280);font-size:var(--ngx-font-size, 13px);text-transform:uppercase;letter-spacing:.04em}.ngx-import-mapper__required{color:var(--ngx-color-error, #b91c1c);margin-left:2px}.ngx-import-mapper__select{min-width:220px;max-width:100%;padding:6px 8px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit}.ngx-import-mapper__slots{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 0}.ngx-import-mapper__add-slot,.ngx-import-mapper__remove{padding:4px 10px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit;cursor:pointer}.ngx-import-mapper__remove{margin-left:8px}.ngx-import-mapper__stale{margin-top:12px;padding:8px 12px;color:var(--ngx-color-error, #b91c1c);background:var(--ngx-color-error-soft, #fef2f2);border-radius:var(--ngx-radius-sm, 6px)}.ngx-import-mapper__stale p,.ngx-import-mapper__stale ul{margin:0 0 6px}.ngx-import-mapper__guess{margin-left:8px;padding:2px 8px;border-radius:999px;font-size:12px;color:var(--ngx-color-info, #1d4ed8);background:var(--ngx-color-info-soft, #eff6ff)}@media(max-width:640px){.ngx-import-mapper__select{min-width:0;width:100%}}\n"] }]
10661
10901
  }], propDecorators: { config: [{
10662
10902
  type: Input,
10663
10903
  args: [{ required: true }]
@@ -10702,13 +10942,23 @@ class ImportPreviewComponent {
10702
10942
  /** The same rules the commit will apply; without them a rule-driven rejection is invisible. */
10703
10943
  rules;
10704
10944
  language = 'en';
10945
+ /** The decimal mark number cells use — the one the commit will read them with. */
10946
+ decimal = '.';
10705
10947
  ui = inject(UiTextService);
10706
10948
  /** The columns this plan fills, in the plan's own order. */
10707
10949
  columns = signal([], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
10708
10950
  grid = signal([], ...(ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []));
10709
10951
  ngOnChanges() {
10710
- const byRef = new Map(deriveImportColumns(this.config, { lang: this.language }).columns.map(c => [c.ref, c]));
10711
- const entries = (this.plan?.entries ?? []).filter(entry => byRef.has(entry.ref));
10952
+ // Sized by the plan, so a mapped sixth phone is shown rather than silently left out, and
10953
+ // read through the 2.2 alias, so an old ref shows the field it now means.
10954
+ const plan = upgradeLegacyRefs(this.plan, this.config);
10955
+ const byRef = new Map(deriveImportColumns(this.config, {
10956
+ lang: this.language,
10957
+ maxArrayRows: arrayBoundFor([], this.config, plan),
10958
+ includeReadonly: true,
10959
+ includeSystemDefault: true,
10960
+ }).columns.map(c => [c.ref, c]));
10961
+ const entries = (plan?.entries ?? []).filter(entry => byRef.has(entry.ref));
10712
10962
  this.columns.set(entries.map(entry => byRef.get(entry.ref)));
10713
10963
  // The same engine the commit runs, over the sample only. Row 0 of the sample is numbered
10714
10964
  // 0 here rather than 2, because these row numbers index the preview, not the file.
@@ -10716,6 +10966,7 @@ class ImportPreviewComponent {
10716
10966
  lang: this.language,
10717
10967
  lookups: this.lookups,
10718
10968
  rules: this.rules,
10969
+ decimal: this.decimal,
10719
10970
  stamp: false,
10720
10971
  firstRowNumber: 0,
10721
10972
  });
@@ -10731,6 +10982,7 @@ class ImportPreviewComponent {
10731
10982
  const outcome = coerceCell(field, raw, {
10732
10983
  lang: this.language,
10733
10984
  lookups: this.lookups,
10985
+ decimal: this.decimal,
10734
10986
  });
10735
10987
  if ('error' in outcome)
10736
10988
  return { text: outcome.error, failed: true };
@@ -10742,7 +10994,7 @@ class ImportPreviewComponent {
10742
10994
  })));
10743
10995
  }
10744
10996
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10745
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportPreviewComponent, isStandalone: true, selector: "ngx-import-preview", inputs: { config: "config", plan: "plan", rows: "rows", lookups: "lookups", rules: "rules", language: "language" }, usesOnChanges: true, ngImport: i0, template: `
10997
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportPreviewComponent, isStandalone: true, selector: "ngx-import-preview", inputs: { config: "config", plan: "plan", rows: "rows", lookups: "lookups", rules: "rules", language: "language", decimal: "decimal" }, usesOnChanges: true, ngImport: i0, template: `
10746
10998
  <section class="ngx-import-preview" data-testid="import-preview">
10747
10999
  <h4 class="ngx-import-preview__heading">
10748
11000
  {{ ui.text('importPreviewHeading', language, { count: rows.length }) }}
@@ -10833,6 +11085,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
10833
11085
  type: Input
10834
11086
  }], language: [{
10835
11087
  type: Input
11088
+ }], decimal: [{
11089
+ type: Input
10836
11090
  }] } });
10837
11091
 
10838
11092
  /**
@@ -11049,6 +11303,8 @@ class EntityImportComponent {
11049
11303
  headers = signal([], ...(ngDevMode ? [{ debugName: "headers" }] : /* istanbul ignore next */ []));
11050
11304
  sample = signal([], ...(ngDevMode ? [{ debugName: "sample" }] : /* istanbul ignore next */ []));
11051
11305
  rowCount = signal(0, ...(ngDevMode ? [{ debugName: "rowCount" }] : /* istanbul ignore next */ []));
11306
+ /** The separator a delimited file turned out to use, so the user can see how it was read. */
11307
+ delimiter = signal(undefined, ...(ngDevMode ? [{ debugName: "delimiter" }] : /* istanbul ignore next */ []));
11052
11308
  fileName = signal('', ...(ngDevMode ? [{ debugName: "fileName" }] : /* istanbul ignore next */ []));
11053
11309
  plan = signal(null, ...(ngDevMode ? [{ debugName: "plan" }] : /* istanbul ignore next */ []));
11054
11310
  result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
@@ -11133,6 +11389,7 @@ class EntityImportComponent {
11133
11389
  this.headers.set(preview.headers);
11134
11390
  this.sample.set(preview.sample);
11135
11391
  this.rowCount.set(preview.rowCount);
11392
+ this.delimiter.set(preview.delimiter);
11136
11393
  this.plan.set(preview.suggestion);
11137
11394
  this.step.set('map');
11138
11395
  }
@@ -11216,12 +11473,30 @@ class EntityImportComponent {
11216
11473
  failedRowCount(result) {
11217
11474
  return result.failed ?? new Set(result.errors.map(error => error.row)).size;
11218
11475
  }
11476
+ /** The decimal mark the commit will read numbers with, so the preview reads them the same. */
11477
+ decimal() {
11478
+ return decimalMarkFor(this.delimiter());
11479
+ }
11480
+ /** How the file's columns were split, in words — or nothing for a workbook. */
11481
+ delimiterText() {
11482
+ switch (this.delimiter()) {
11483
+ case ',':
11484
+ return this.ui.text('importDelimiterComma', this.language);
11485
+ case ';':
11486
+ return this.ui.text('importDelimiterSemicolon', this.language);
11487
+ case '\t':
11488
+ return this.ui.text('importDelimiterTab', this.language);
11489
+ default:
11490
+ return null;
11491
+ }
11492
+ }
11219
11493
  restart() {
11220
11494
  this.file = null;
11221
11495
  this.fileName.set('');
11222
11496
  this.headers.set([]);
11223
11497
  this.sample.set([]);
11224
11498
  this.rowCount.set(0);
11499
+ this.delimiter.set(undefined);
11225
11500
  this.plan.set(null);
11226
11501
  this.result.set(null);
11227
11502
  this.problem.set(null);
@@ -11284,6 +11559,9 @@ class EntityImportComponent {
11284
11559
  }
11285
11560
 
11286
11561
  @case ('review') {
11562
+ @if (delimiterText(); as separator) {
11563
+ <p class="ngx-entity-import__delimiter" data-testid="import-delimiter">{{ separator }}</p>
11564
+ }
11287
11565
  @if (plan(); as current) {
11288
11566
  <ngx-import-preview
11289
11567
  [config]="config"
@@ -11292,6 +11570,7 @@ class EntityImportComponent {
11292
11570
  [lookups]="resolvedLookups()"
11293
11571
  [rules]="rules"
11294
11572
  [language]="language"
11573
+ [decimal]="decimal()"
11295
11574
  />
11296
11575
  }
11297
11576
  <button
@@ -11344,7 +11623,7 @@ class EntityImportComponent {
11344
11623
  }
11345
11624
  }
11346
11625
  </div>
11347
- `, isInline: true, styles: [".ngx-import{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);color:var(--ngx-color-text, #1f2937);font-size:var(--ngx-font-size, 14px)}.ngx-import__steps{display:flex;flex-wrap:wrap;gap:8px;list-style:none;margin:0;padding:0}.ngx-import__step{padding:4px 12px;border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);color:var(--ngx-color-muted, #6b7280);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-import__step--current{color:var(--ngx-color-accent, #4f46e5);border-color:var(--ngx-color-accent, #4f46e5);background:var(--ngx-color-accent-soft, #e0e7ff);font-weight:600}.ngx-import__problem{margin:0;padding:10px 12px;border:1px solid var(--ngx-color-border, #fecaca);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-error-soft, #fef2f2);color:var(--ngx-color-error, #b91c1c);overflow-wrap:anywhere}.ngx-import__file{display:flex;flex-direction:column;gap:6px;padding:14px;border:1px dashed var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff)}.ngx-import button{align-self:flex-start;padding:8px 16px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit;cursor:pointer}.ngx-import button:hover:not(:disabled){border-color:var(--ngx-color-accent, #4f46e5);background:var(--ngx-color-accent-soft, #e0e7ff)}.ngx-import button:disabled{opacity:.55;cursor:not-allowed}\n"], dependencies: [{ kind: "component", type: ImportTemplateComponent, selector: "ngx-import-template", inputs: ["config", "language"], outputs: ["download"] }, { kind: "component", type: ImportMapperComponent, selector: "ngx-import-mapper", inputs: ["config", "headers", "plan", "language"], outputs: ["planChange"] }, { kind: "component", type: ImportPreviewComponent, selector: "ngx-import-preview", inputs: ["config", "plan", "rows", "lookups", "rules", "language"] }, { kind: "component", type: ImportErrorsComponent, selector: "ngx-import-errors", inputs: ["errors", "language"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11626
+ `, isInline: true, styles: [".ngx-import{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);color:var(--ngx-color-text, #1f2937);font-size:var(--ngx-font-size, 14px)}.ngx-import__steps{display:flex;flex-wrap:wrap;gap:8px;list-style:none;margin:0;padding:0}.ngx-import__step{padding:4px 12px;border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);color:var(--ngx-color-muted, #6b7280);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-import__step--current{color:var(--ngx-color-accent, #4f46e5);border-color:var(--ngx-color-accent, #4f46e5);background:var(--ngx-color-accent-soft, #e0e7ff);font-weight:600}.ngx-import__problem{margin:0;padding:10px 12px;border:1px solid var(--ngx-color-border, #fecaca);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-error-soft, #fef2f2);color:var(--ngx-color-error, #b91c1c);overflow-wrap:anywhere}.ngx-import__file{display:flex;flex-direction:column;gap:6px;padding:14px;border:1px dashed var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff)}.ngx-import button{align-self:flex-start;padding:8px 16px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit;cursor:pointer}.ngx-import button:hover:not(:disabled){border-color:var(--ngx-color-accent, #4f46e5);background:var(--ngx-color-accent-soft, #e0e7ff)}.ngx-import button:disabled{opacity:.55;cursor:not-allowed}\n"], dependencies: [{ kind: "component", type: ImportTemplateComponent, selector: "ngx-import-template", inputs: ["config", "language"], outputs: ["download"] }, { kind: "component", type: ImportMapperComponent, selector: "ngx-import-mapper", inputs: ["config", "headers", "plan", "language"], outputs: ["planChange"] }, { kind: "component", type: ImportPreviewComponent, selector: "ngx-import-preview", inputs: ["config", "plan", "rows", "lookups", "rules", "language", "decimal"] }, { kind: "component", type: ImportErrorsComponent, selector: "ngx-import-errors", inputs: ["errors", "language"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11348
11627
  }
11349
11628
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityImportComponent, decorators: [{
11350
11629
  type: Component,
@@ -11409,6 +11688,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
11409
11688
  }
11410
11689
 
11411
11690
  @case ('review') {
11691
+ @if (delimiterText(); as separator) {
11692
+ <p class="ngx-entity-import__delimiter" data-testid="import-delimiter">{{ separator }}</p>
11693
+ }
11412
11694
  @if (plan(); as current) {
11413
11695
  <ngx-import-preview
11414
11696
  [config]="config"
@@ -11417,6 +11699,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
11417
11699
  [lookups]="resolvedLookups()"
11418
11700
  [rules]="rules"
11419
11701
  [language]="language"
11702
+ [decimal]="decimal()"
11420
11703
  />
11421
11704
  }
11422
11705
  <button