ngx-dynamic-entity 2.4.1 → 3.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -258,6 +258,7 @@ rendering decision, not an access-control boundary.
258
258
  | `rules` | Pass the same rules the form uses, or a rule-hidden required field rejects rows the form would save. |
259
259
  | `lookups` | Option lists already resolved, merged over what `LOOKUP_REGISTRY` provides. |
260
260
  | `language` | Default `'en'`. |
261
+ | `dateOrder` | `'DMY'` or `'MDY'`: the order of a date written in digits, such as `05/09/2026`. Without it, a date that reads two ways is an error, never a guess. With a server transport the server's order is the one applied: state it on the router, and the wizard reads its review with what the server reports. If you state one here too and it differs, the wizard refuses to commit. |
261
262
  | `templateFormat` | `'csv'` (default) or `'xlsx'`. `xlsx` needs a server transport. |
262
263
 
263
264
  | Output | Payload |
@@ -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, fieldsUnderTab, getTabPath, fieldRefFor, flattenFieldValues, parseFieldRef, placeTabFields, 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';
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, placeTabFields, 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, planForServer, 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';
@@ -930,6 +930,8 @@ const DEFAULT_UI_TEXT = {
930
930
  importDelimiterComma: 'Columns are separated by commas.',
931
931
  importDelimiterSemicolon: 'Columns are separated by semicolons, and numbers use a decimal comma.',
932
932
  importDelimiterTab: 'Columns are separated by tabs.',
933
+ importDateOrderMismatch: 'This page reads dates as {stated} but the import reads them as {reported}. Nothing can be imported until both use the same date order.',
934
+ importDateOrderUnconfirmed: 'The import did not confirm the date order {stated}, so it is not applied. A date such as 05/09/2026 is refused, or read month first by a server older than 2.5.0.',
933
935
  importRowRejected: 'This row will not be imported: {reason}',
934
936
  importRunImport: 'Import {count} row(s)',
935
937
  importImporting: 'Importing…',
@@ -10086,6 +10088,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImpo
10086
10088
  * on a server because their files have fifty thousand rows. Collapsing them into one option
10087
10089
  * would make each answer imply the other.
10088
10090
  */
10091
+ /**
10092
+ * How the order the host states compares with the one the transport reports it reads with.
10093
+ *
10094
+ * - `mismatch`: both state one and they differ. Committing would store dates read one way
10095
+ * after a review that showed them the other, so nothing may be committed.
10096
+ * - `unconfirmed`: the host states one and the transport reports none. The transport reads the
10097
+ * file, so the host's order is not applied: worth a warning, not a refusal.
10098
+ * - `agreed`: anything else. Only the transport stating one is fine: it is the one that reads.
10099
+ */
10100
+ function compareDateOrders(stated, reported) {
10101
+ if (!stated || stated === reported)
10102
+ return 'agreed';
10103
+ return reported ? 'mismatch' : 'unconfirmed';
10104
+ }
10089
10105
 
10090
10106
  /**
10091
10107
  * sheet-parser.ts — the built-in CSV reader, and the reason it is the only one.
@@ -10162,6 +10178,10 @@ class LocalImportTransport {
10162
10178
  pending.catch(() => this.parsed.delete(file));
10163
10179
  return pending;
10164
10180
  }
10181
+ /**
10182
+ * The suggestion made here is always plan v2, and the return type says so. The contract's is
10183
+ * a plan in either shape, because a 2.x server sends the older one.
10184
+ */
10165
10185
  async preview(file, context) {
10166
10186
  const sheet = await this.read(file);
10167
10187
  // Sized from the headers exactly as the server sizes them, so both offer the same slots.
@@ -10176,6 +10196,8 @@ class LocalImportTransport {
10176
10196
  suggestion: suggestMapping(sheet.headers, columns, context.config?.entity ?? ''),
10177
10197
  arrayBound,
10178
10198
  ...(sheet.delimiter ? { delimiter: sheet.delimiter } : {}),
10199
+ // What `commit` below will read with, so the review step reads its sample the same way.
10200
+ ...(context.dateOrder ? { dateOrder: context.dateOrder } : {}),
10179
10201
  };
10180
10202
  }
10181
10203
  async commit(file, plan, context) {
@@ -10186,6 +10208,8 @@ class LocalImportTransport {
10186
10208
  lookups: context.lookups,
10187
10209
  // A `;` file means `1,5` is one and a half — the same call the server makes.
10188
10210
  decimal: decimalMarkFor(sheet.delimiter),
10211
+ // Stated by the host, never inferred: a date that reads two ways fails without it.
10212
+ dateOrder: context.dateOrder,
10189
10213
  });
10190
10214
  }
10191
10215
  async template(spec, format) {
@@ -10294,11 +10318,52 @@ function selectionOf(spec, context, fields) {
10294
10318
  * to resolve — and a consumer who wants two of them, pointed at two backends, gets that for
10295
10319
  * free.
10296
10320
  */
10321
+ /** What a Dynamic Entity server calls its adapter in the `adapter` stamp on every answer. */
10322
+ const ADAPTER_NAME = 'dynamic-entity';
10323
+ /** The first release whose server says which date order it reads with, and refuses to guess one. */
10324
+ const DATE_ORDER_SINCE = [2, 5];
10325
+ /** Whether an engine version is from before the server stopped reading dates month first. */
10326
+ function readsMonthFirst(version) {
10327
+ if (!version)
10328
+ return true;
10329
+ const [major, minor] = version.split('.').map(Number);
10330
+ return major < DATE_ORDER_SINCE[0] || (major === DATE_ORDER_SINCE[0] && minor < DATE_ORDER_SINCE[1]);
10331
+ }
10297
10332
  class HttpImportTransport {
10298
10333
  options;
10334
+ /**
10335
+ * The date order each previewed file's server reported, for `commit` to compare against.
10336
+ * Keyed on the `File`, so it lives exactly as long as the file does.
10337
+ */
10338
+ reported = new WeakMap();
10339
+ /**
10340
+ * The plan versions each previewed file's server said it reads. A file with no entry was
10341
+ * never previewed here, and one whose server said nothing is from before plan v2: either
10342
+ * way `commit` sends nothing that only a v2 reader would understand.
10343
+ */
10344
+ planVersions = new WeakMap();
10299
10345
  constructor(options) {
10300
10346
  this.options = options;
10301
10347
  }
10348
+ warn(message) {
10349
+ (this.options.warn ?? ((text) => console.warn(text)))(message);
10350
+ }
10351
+ /**
10352
+ * The server reads the file, so its date order is the one that is applied. When the host
10353
+ * stated one and the server confirms none, the host's order does nothing there: say so, and
10354
+ * say what the server will do instead, which depends on how old it is.
10355
+ */
10356
+ checkDateOrder(stated, reported, version) {
10357
+ if (compareDateOrders(stated, reported) !== 'unconfirmed')
10358
+ return;
10359
+ this.warn(`[ngx-dynamic-entity] This app states the date order ${stated}, but the import server did not confirm one, ` +
10360
+ (readsMonthFirst(version)
10361
+ ? `and runs @dynamic-entity/core ${version ?? '(unreported)'}, from before 2.5.0. A server that old reads a ` +
10362
+ 'date such as 05/09/2026 month first, whatever this app states. Upgrade the server and state ' +
10363
+ 'the same dateOrder on its router.'
10364
+ : 'so it will refuse a date such as 05/09/2026 that could be day first or month first. ' +
10365
+ 'State the same dateOrder on its router.'));
10366
+ }
10302
10367
  get base() {
10303
10368
  return trimSlash(this.options.baseUrl);
10304
10369
  }
@@ -10325,11 +10390,26 @@ class HttpImportTransport {
10325
10390
  // scroll past the warning that does matter.
10326
10391
  if (!version || sameLine(version, CORE_VERSION))
10327
10392
  return;
10328
- const warn = this.options.warn ?? ((message) => console.warn(message));
10329
- warn(`[ngx-dynamic-entity] The import server runs @dynamic-entity/core ${version}; this app ` +
10393
+ this.warn(`[ngx-dynamic-entity] The import server runs @dynamic-entity/core ${version}; this app ` +
10330
10394
  `was built against ${CORE_VERSION}. The two halves of an import are supposed to be the ` +
10331
10395
  'same engine — bring them into step before trusting a difference in what they produce.');
10332
10396
  }
10397
+ /**
10398
+ * Warn when the server runs another adapter, or this one at another major version.
10399
+ *
10400
+ * Since 3.0 a server says which adapter turned its rows into records. A server from before
10401
+ * that says nothing, and `checkEngine` has already compared the one version it does report.
10402
+ */
10403
+ checkAdapter(adapter) {
10404
+ if (!adapter)
10405
+ return;
10406
+ const major = (version) => String(version ?? '').split('.')[0];
10407
+ if (adapter.name === ADAPTER_NAME && major(adapter.version) === major(CORE_VERSION))
10408
+ return;
10409
+ this.warn(`[ngx-dynamic-entity] The import server reads rows with the adapter ${adapter.name} ${adapter.version}; ` +
10410
+ `this app expects ${ADAPTER_NAME} ${major(CORE_VERSION)}.x. What the server imports may differ from ` +
10411
+ 'what the review step showed. Bring the two into step.');
10412
+ }
10333
10413
  async preview(file, context) {
10334
10414
  const body = new FormData();
10335
10415
  body.append('file', file, file.name || 'upload');
@@ -10342,6 +10422,10 @@ class HttpImportTransport {
10342
10422
  throw await failureOf(response);
10343
10423
  const preview = (await response.json());
10344
10424
  this.checkEngine(preview.engineVersion);
10425
+ this.checkAdapter(preview.adapter);
10426
+ this.planVersions.set(file, Array.isArray(preview.planVersions) ? preview.planVersions : undefined);
10427
+ this.reported.set(file, preview.dateOrder);
10428
+ this.checkDateOrder(context?.dateOrder, preview.dateOrder, preview.engineVersion);
10345
10429
  return {
10346
10430
  headers: preview.headers ?? [],
10347
10431
  sample: preview.sample ?? [],
@@ -10350,14 +10434,32 @@ class HttpImportTransport {
10350
10434
  // A server older than 2.3 does not say, and always sized for the default.
10351
10435
  arrayBound: preview.arrayBound ?? DEFAULT_ARRAY_ROWS,
10352
10436
  ...(preview.delimiter ? { delimiter: preview.delimiter } : {}),
10437
+ ...(preview.dateOrder ? { dateOrder: preview.dateOrder } : {}),
10353
10438
  };
10354
10439
  }
10355
10440
  async commit(file, plan, context) {
10441
+ // Before a byte is sent. The review this user approved read dates one way and the server
10442
+ // would store them another, and a streaming import cannot be taken back.
10443
+ const reported = this.reported.get(file);
10444
+ if (compareDateOrders(context?.dateOrder, reported) === 'mismatch') {
10445
+ throw new Error(`This app reads dates as ${context.dateOrder} but the import server reads them as ${reported}. ` +
10446
+ 'Nothing was imported. Set the same dateOrder on the wizard and on the server\'s router.');
10447
+ }
10448
+ // Also before a byte is sent. A server from before plan v2 ignores what it does not know,
10449
+ // so a plan that groups rows would import one record per row there and report success.
10450
+ const sendable = planForServer(plan, this.planVersions.get(file));
10451
+ if (sendable.refused) {
10452
+ throw new Error(`This mapping uses ${sendable.refused.join(', ')}, which the import server does not read: it is from ` +
10453
+ 'before mapping plan v2 and would ignore it. Nothing was imported. Upgrade the server to ' +
10454
+ '@dynamic-entity/server 3.0 or later.');
10455
+ }
10356
10456
  const body = new FormData();
10357
10457
  // **The plan first, and the file second.** The server starts importing while the file is
10358
10458
  // still arriving — that is the whole reason it exists — so the plan has to have arrived
10359
10459
  // already. `FormData` preserves append order, so this ordering is the contract.
10360
- body.append('plan', JSON.stringify(plan));
10460
+ // As written for a server that reads plan v2, and in 2.x form (`entity`, `configVersion`)
10461
+ // for one that does not: the same mapping, in the words that server knows.
10462
+ body.append('plan', JSON.stringify(sendable.plan));
10361
10463
  body.append('file', file, file.name || 'upload');
10362
10464
  const response = await this.send(`${this.base}/${this.entityOf(context)}/import`, {
10363
10465
  method: 'POST',
@@ -10368,6 +10470,7 @@ class HttpImportTransport {
10368
10470
  throw await failureOf(response);
10369
10471
  const result = (await response.json());
10370
10472
  this.checkEngine(result.engineVersion);
10473
+ this.checkAdapter(result.adapter);
10371
10474
  return {
10372
10475
  // Empty, and not because nothing was imported. The server streamed the file precisely so
10373
10476
  // that fifty thousand records never had to exist at once; sending them back would undo
@@ -10381,6 +10484,8 @@ class HttpImportTransport {
10381
10484
  errorCount: result.errorCount ?? result.errors?.length ?? 0,
10382
10485
  truncated: result.truncated ?? false,
10383
10486
  planProblems: result.planProblems ?? [],
10487
+ // Only a 3.0 server sends them, and only a run with something to warn about has any.
10488
+ ...(result.warnings?.length ? { warnings: result.warnings } : {}),
10384
10489
  };
10385
10490
  }
10386
10491
  async template(spec, format, context, fields) {
@@ -10525,10 +10630,10 @@ const CONSTANT = -1;
10525
10630
  class ImportMapperComponent {
10526
10631
  config;
10527
10632
  headers = [];
10528
- /** The starting point, normally `suggestMapping`'s output. */
10633
+ /** The starting point, normally `suggestMapping`'s output. A stored 2.x plan is taken too. */
10529
10634
  plan = null;
10530
10635
  language = 'en';
10531
- /** Emitted whenever the user changes a selection. */
10636
+ /** Emitted whenever the user changes a selection. Always plan v2, whichever shape came in. */
10532
10637
  planChange = new EventEmitter();
10533
10638
  ui = inject(UiTextService);
10534
10639
  rows = signal([], ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
@@ -10641,6 +10746,10 @@ class ImportMapperComponent {
10641
10746
  * Every entry of the incoming plan survives in its own position — a fixed value, a ref the
10642
10747
  * config no longer has — unless the user changed or removed it, and an untouched entry is
10643
10748
  * emitted exactly as it arrived. New selections follow, in field order.
10749
+ *
10750
+ * What is emitted is plan v2, whichever shape came in: `target` and `schemaVersion`, where a
10751
+ * 2.x plan said `entity` and `configVersion`. Anything else the incoming plan carried, such
10752
+ * as its lists and its grouping, is kept.
10644
10753
  */
10645
10754
  toPlan() {
10646
10755
  const rowByRef = new Map(this.rows().map(row => [row.column.ref, row]));
@@ -10678,10 +10787,13 @@ class ImportMapperComponent {
10678
10787
  if (entry)
10679
10788
  entries.push(entry);
10680
10789
  }
10790
+ // The 2.x names go: a plan carrying both is one the reader has to warn about.
10791
+ const kept = Object.fromEntries(Object.entries(this.incoming ?? {}).filter(([key]) => key !== 'entity' && key !== 'configVersion'));
10681
10792
  return {
10682
- ...this.incoming,
10683
- entity: this.config?.entity ?? '',
10684
- configVersion: this.config?.version,
10793
+ ...kept,
10794
+ planVersion: 2,
10795
+ target: this.config?.entity ?? '',
10796
+ schemaVersion: this.config?.version,
10685
10797
  sourceHeaders: [...this.headers],
10686
10798
  entries,
10687
10799
  };
@@ -10938,6 +11050,8 @@ class ImportPreviewComponent {
10938
11050
  language = 'en';
10939
11051
  /** The decimal mark number cells use — the one the commit will read them with. */
10940
11052
  decimal = '.';
11053
+ /** The order of a date written in digits — the one the commit will read them in. */
11054
+ dateOrder;
10941
11055
  ui = inject(UiTextService);
10942
11056
  /** The columns this plan fills, in the plan's own order. */
10943
11057
  columns = signal([], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
@@ -10961,6 +11075,7 @@ class ImportPreviewComponent {
10961
11075
  lookups: this.lookups,
10962
11076
  rules: this.rules,
10963
11077
  decimal: this.decimal,
11078
+ dateOrder: this.dateOrder,
10964
11079
  stamp: false,
10965
11080
  firstRowNumber: 0,
10966
11081
  });
@@ -10977,6 +11092,8 @@ class ImportPreviewComponent {
10977
11092
  lang: this.language,
10978
11093
  lookups: this.lookups,
10979
11094
  decimal: this.decimal,
11095
+ // The column's own order when its plan entry states one, as the commit reads it.
11096
+ dateOrder: entry.dateOrder ?? this.dateOrder,
10980
11097
  });
10981
11098
  if ('error' in outcome)
10982
11099
  return { text: outcome.error, failed: true };
@@ -10988,7 +11105,7 @@ class ImportPreviewComponent {
10988
11105
  })));
10989
11106
  }
10990
11107
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ImportPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10991
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", 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: `
11108
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: ImportPreviewComponent, isStandalone: true, selector: "ngx-import-preview", inputs: { config: "config", plan: "plan", rows: "rows", lookups: "lookups", rules: "rules", language: "language", decimal: "decimal", dateOrder: "dateOrder" }, usesOnChanges: true, ngImport: i0, template: `
10992
11109
  <section class="ngx-import-preview" data-testid="import-preview">
10993
11110
  <h4 class="ngx-import-preview__heading">
10994
11111
  {{ ui.text('importPreviewHeading', language, { count: rows.length }) }}
@@ -11081,6 +11198,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImpo
11081
11198
  type: Input
11082
11199
  }], decimal: [{
11083
11200
  type: Input
11201
+ }], dateOrder: [{
11202
+ type: Input
11084
11203
  }] } });
11085
11204
 
11086
11205
  /**
@@ -11283,6 +11402,16 @@ class EntityImportComponent {
11283
11402
  */
11284
11403
  lookups;
11285
11404
  language = 'en';
11405
+ /**
11406
+ * The order of a date written in digits in the sheets this wizard imports: `DMY` for day
11407
+ * first, `MDY` for month first. Without it, a date that reads two ways (`05/09/2026`) is
11408
+ * shown, and imported, as an error rather than guessed.
11409
+ *
11410
+ * The transport reads the file, so the order it reports at preview is the one applied, and
11411
+ * the one the review step shows. If that differs from this, the wizard refuses to commit;
11412
+ * if the transport reports none, the wizard warns that this order is not being applied.
11413
+ */
11414
+ dateOrder;
11286
11415
  /** Format asked of the transport when a template is downloaded. */
11287
11416
  templateFormat = 'csv';
11288
11417
  /** The finished import, once it has run. */
@@ -11299,6 +11428,8 @@ class EntityImportComponent {
11299
11428
  rowCount = signal(0, ...(ngDevMode ? [{ debugName: "rowCount" }] : /* istanbul ignore next */ []));
11300
11429
  /** The separator a delimited file turned out to use, so the user can see how it was read. */
11301
11430
  delimiter = signal(undefined, ...(ngDevMode ? [{ debugName: "delimiter" }] : /* istanbul ignore next */ []));
11431
+ /** The date order the transport said it will read with at commit, from its preview. */
11432
+ reportedDateOrder = signal(undefined, ...(ngDevMode ? [{ debugName: "reportedDateOrder" }] : /* istanbul ignore next */ []));
11302
11433
  fileName = signal('', ...(ngDevMode ? [{ debugName: "fileName" }] : /* istanbul ignore next */ []));
11303
11434
  plan = signal(null, ...(ngDevMode ? [{ debugName: "plan" }] : /* istanbul ignore next */ []));
11304
11435
  result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
@@ -11317,6 +11448,7 @@ class EntityImportComponent {
11317
11448
  rules: this.rules,
11318
11449
  lookups: this.resolvedLookups(),
11319
11450
  lang: this.language,
11451
+ dateOrder: this.dateOrder,
11320
11452
  };
11321
11453
  }
11322
11454
  /**
@@ -11384,6 +11516,7 @@ class EntityImportComponent {
11384
11516
  this.sample.set(preview.sample);
11385
11517
  this.rowCount.set(preview.rowCount);
11386
11518
  this.delimiter.set(preview.delimiter);
11519
+ this.reportedDateOrder.set(preview.dateOrder);
11387
11520
  this.plan.set(preview.suggestion);
11388
11521
  this.step.set('map');
11389
11522
  }
@@ -11402,6 +11535,12 @@ class EntityImportComponent {
11402
11535
  this.problem.set(this.ui.text('importNeedFile', this.language));
11403
11536
  return;
11404
11537
  }
11538
+ // Never on a mismatch: the review showed dates read one way and the commit would store
11539
+ // them read another. The button is disabled too; this is for whatever calls it anyway.
11540
+ if (this.dateOrderCheck() === 'mismatch') {
11541
+ this.problem.set(this.ui.text('importDateOrderMismatch', this.language, this.dateOrders()));
11542
+ return;
11543
+ }
11405
11544
  this.busy.set(true);
11406
11545
  this.problem.set(null);
11407
11546
  try {
@@ -11467,6 +11606,14 @@ class EntityImportComponent {
11467
11606
  failedRowCount(result) {
11468
11607
  return result.failed ?? new Set(result.errors.map(error => error.row)).size;
11469
11608
  }
11609
+ /** How the order the host states compares with the one the transport reads with. */
11610
+ dateOrderCheck() {
11611
+ return compareDateOrders(this.dateOrder, this.reportedDateOrder());
11612
+ }
11613
+ /** The two orders, for a message that names them. Only asked for when they do not agree. */
11614
+ dateOrders() {
11615
+ return { stated: String(this.dateOrder), reported: String(this.reportedDateOrder()) };
11616
+ }
11470
11617
  /** The decimal mark the commit will read numbers with, so the preview reads them the same. */
11471
11618
  decimal() {
11472
11619
  return decimalMarkFor(this.delimiter());
@@ -11497,7 +11644,7 @@ class EntityImportComponent {
11497
11644
  this.step.set('upload');
11498
11645
  }
11499
11646
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: EntityImportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11500
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: EntityImportComponent, isStandalone: true, selector: "ngx-entity-import", inputs: { config: "config", rules: "rules", lookups: "lookups", language: "language", templateFormat: "templateFormat" }, outputs: { importComplete: "importComplete", templateReady: "templateReady" }, ngImport: i0, template: `
11647
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: EntityImportComponent, isStandalone: true, selector: "ngx-entity-import", inputs: { config: "config", rules: "rules", lookups: "lookups", language: "language", dateOrder: "dateOrder", templateFormat: "templateFormat" }, outputs: { importComplete: "importComplete", templateReady: "templateReady" }, ngImport: i0, template: `
11501
11648
  <div class="ngx-import" data-testid="entity-import">
11502
11649
  <ol class="ngx-import__steps" data-testid="import-steps">
11503
11650
  @for (name of steps; track name) {
@@ -11556,6 +11703,18 @@ class EntityImportComponent {
11556
11703
  @if (delimiterText(); as separator) {
11557
11704
  <p class="ngx-entity-import__delimiter" data-testid="import-delimiter">{{ separator }}</p>
11558
11705
  }
11706
+ @switch (dateOrderCheck()) {
11707
+ @case ('mismatch') {
11708
+ <p class="ngx-entity-import__problem" role="alert" data-testid="import-date-order-mismatch">
11709
+ {{ ui.text('importDateOrderMismatch', language, dateOrders()) }}
11710
+ </p>
11711
+ }
11712
+ @case ('unconfirmed') {
11713
+ <p class="ngx-entity-import__delimiter" data-testid="import-date-order-unconfirmed">
11714
+ {{ ui.text('importDateOrderUnconfirmed', language, dateOrders()) }}
11715
+ </p>
11716
+ }
11717
+ }
11559
11718
  @if (plan(); as current) {
11560
11719
  <ngx-import-preview
11561
11720
  [config]="config"
@@ -11565,12 +11724,13 @@ class EntityImportComponent {
11565
11724
  [rules]="rules"
11566
11725
  [language]="language"
11567
11726
  [decimal]="decimal()"
11727
+ [dateOrder]="reportedDateOrder()"
11568
11728
  />
11569
11729
  }
11570
11730
  <button
11571
11731
  type="button"
11572
11732
  data-testid="import-commit"
11573
- [disabled]="busy()"
11733
+ [disabled]="busy() || dateOrderCheck() === 'mismatch'"
11574
11734
  (click)="commit()"
11575
11735
  >
11576
11736
  {{
@@ -11617,7 +11777,7 @@ class EntityImportComponent {
11617
11777
  }
11618
11778
  }
11619
11779
  </div>
11620
- `, 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 });
11780
+ `, 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", "dateOrder"] }, { kind: "component", type: ImportErrorsComponent, selector: "ngx-import-errors", inputs: ["errors", "language"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11621
11781
  }
11622
11782
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: EntityImportComponent, decorators: [{
11623
11783
  type: Component,
@@ -11685,6 +11845,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImpo
11685
11845
  @if (delimiterText(); as separator) {
11686
11846
  <p class="ngx-entity-import__delimiter" data-testid="import-delimiter">{{ separator }}</p>
11687
11847
  }
11848
+ @switch (dateOrderCheck()) {
11849
+ @case ('mismatch') {
11850
+ <p class="ngx-entity-import__problem" role="alert" data-testid="import-date-order-mismatch">
11851
+ {{ ui.text('importDateOrderMismatch', language, dateOrders()) }}
11852
+ </p>
11853
+ }
11854
+ @case ('unconfirmed') {
11855
+ <p class="ngx-entity-import__delimiter" data-testid="import-date-order-unconfirmed">
11856
+ {{ ui.text('importDateOrderUnconfirmed', language, dateOrders()) }}
11857
+ </p>
11858
+ }
11859
+ }
11688
11860
  @if (plan(); as current) {
11689
11861
  <ngx-import-preview
11690
11862
  [config]="config"
@@ -11694,12 +11866,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImpo
11694
11866
  [rules]="rules"
11695
11867
  [language]="language"
11696
11868
  [decimal]="decimal()"
11869
+ [dateOrder]="reportedDateOrder()"
11697
11870
  />
11698
11871
  }
11699
11872
  <button
11700
11873
  type="button"
11701
11874
  data-testid="import-commit"
11702
- [disabled]="busy()"
11875
+ [disabled]="busy() || dateOrderCheck() === 'mismatch'"
11703
11876
  (click)="commit()"
11704
11877
  >
11705
11878
  {{
@@ -11756,6 +11929,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImpo
11756
11929
  type: Input
11757
11930
  }], language: [{
11758
11931
  type: Input
11932
+ }], dateOrder: [{
11933
+ type: Input
11759
11934
  }], templateFormat: [{
11760
11935
  type: Input
11761
11936
  }], importComplete: [{
@@ -11793,5 +11968,5 @@ function saveBlob(blob, filename) {
11793
11968
  * Generated bundle index. Do not edit.
11794
11969
  */
11795
11970
 
11796
- export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ColorFieldComponent, 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, PhoneFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RatingFieldComponent, RbacService, RulesEvaluationService, SHEET_PARSER, SYSTEM_DEFAULT_CAN_EDIT, SliderFieldComponent, TagsFieldComponent, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, UrlFieldComponent, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, defaultSheetParser, fieldDescribedBy, fieldDomId, nextFieldInstanceId, phoneValidator, provideBuiltInFieldTypes, provideFieldTypes, provideHttpImportTransport, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText, urlValidator };
11971
+ export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ColorFieldComponent, 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, PhoneFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RatingFieldComponent, RbacService, RulesEvaluationService, SHEET_PARSER, SYSTEM_DEFAULT_CAN_EDIT, SliderFieldComponent, TagsFieldComponent, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, UrlFieldComponent, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, compareDateOrders, defaultSheetParser, fieldDescribedBy, fieldDomId, nextFieldInstanceId, phoneValidator, provideBuiltInFieldTypes, provideFieldTypes, provideHttpImportTransport, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText, urlValidator };
11797
11972
  //# sourceMappingURL=ngx-dynamic-entity.mjs.map