@yourself.create/ngx-form-designer 0.0.5 → 0.0.7

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.
@@ -5420,12 +5420,12 @@ class JsonFormRendererComponent {
5420
5420
  // Notify all widgets to show errors if any
5421
5421
  this.engine?.submit();
5422
5422
  const preUploadFieldValueMap = this.uploadOnSubmit
5423
- ? await this.buildFieldValueMap(this.engine?.getValues() ?? {})
5423
+ ? await this.buildFieldValueMap(this.engine?.getValues() ?? {}, { normalizeFileFieldsForSubmit: true })
5424
5424
  : undefined;
5425
5425
  const uploadedFiles = this.uploadOnSubmit ? await this.uploadPendingFiles() : {};
5426
5426
  this.uploadedFilesChange.emit(uploadedFiles);
5427
5427
  const values = this.engine?.getValues() ?? {};
5428
- const fieldValueMap = await this.buildFieldValueMap(values);
5428
+ const fieldValueMap = await this.buildFieldValueMap(values, { normalizeFileFieldsForSubmit: true });
5429
5429
  const submitValues = preUploadFieldValueMap
5430
5430
  ? this.mergeFileMetadata(fieldValueMap, preUploadFieldValueMap)
5431
5431
  : fieldValueMap;
@@ -5658,13 +5658,13 @@ class JsonFormRendererComponent {
5658
5658
  isObjectRecord(value) {
5659
5659
  return !!value && typeof value === 'object' && !Array.isArray(value);
5660
5660
  }
5661
- async buildFieldValueMap(values) {
5661
+ async buildFieldValueMap(values, options = {}) {
5662
5662
  const schema = this.engine?.getSchema();
5663
5663
  if (!schema)
5664
5664
  return {};
5665
- return this.buildFieldValueMapForSchema(schema, values);
5665
+ return this.buildFieldValueMapForSchema(schema, values, options);
5666
5666
  }
5667
- async buildFieldValueMapForSchema(schema, valuesScope) {
5667
+ async buildFieldValueMapForSchema(schema, valuesScope, options = {}) {
5668
5668
  const mapped = {};
5669
5669
  for (const field of schema.fields) {
5670
5670
  if (!field?.id || !field?.name)
@@ -5679,7 +5679,7 @@ class JsonFormRendererComponent {
5679
5679
  rows.push({});
5680
5680
  continue;
5681
5681
  }
5682
- rows.push(await this.buildFieldValueMapForSchema(itemSchema, row));
5682
+ rows.push(await this.buildFieldValueMapForSchema(itemSchema, row, options));
5683
5683
  }
5684
5684
  }
5685
5685
  mapped[field.id] = {
@@ -5691,7 +5691,10 @@ class JsonFormRendererComponent {
5691
5691
  if (field.type === 'file') {
5692
5692
  const fileValue = {
5693
5693
  fieldName: field.name,
5694
- fieldValue: rawValue,
5694
+ fieldValue: options.normalizeFileFieldsForSubmit
5695
+ ? this.normalizeFileFieldSubmitValue(rawValue)
5696
+ : rawValue,
5697
+ ...(options.normalizeFileFieldsForSubmit ? { fieldType: field.type } : {}),
5695
5698
  ...(await this.buildFileFieldMetadata(rawValue))
5696
5699
  };
5697
5700
  mapped[field.id] = fileValue;
@@ -5779,6 +5782,18 @@ class JsonFormRendererComponent {
5779
5782
  return undefined;
5780
5783
  return values.length === 1 ? values[0] : values;
5781
5784
  }
5785
+ normalizeFileFieldSubmitValue(value) {
5786
+ if (this.isFileList(value)) {
5787
+ return Array.from(value);
5788
+ }
5789
+ if (Array.isArray(value)) {
5790
+ return [...value];
5791
+ }
5792
+ if (value === null || value === undefined) {
5793
+ return [];
5794
+ }
5795
+ return [value];
5796
+ }
5782
5797
  getUploadedFileRefs(value) {
5783
5798
  if (this.isUploadedFileRef(value))
5784
5799
  return [value];
@@ -15425,6 +15440,8 @@ class DataPanelComponent {
15425
15440
  selectionFieldId;
15426
15441
  selectionMatchPath;
15427
15442
  childRowsPath;
15443
+ formatNumericOptionLabels = false;
15444
+ optionLabelPrefixPath;
15428
15445
  rootPathOptions = [];
15429
15446
  rowPathOptions = [];
15430
15447
  // Value/Image Config
@@ -15665,6 +15682,8 @@ class DataPanelComponent {
15665
15682
  this.selectionFieldId = undefined;
15666
15683
  this.selectionMatchPath = undefined;
15667
15684
  this.childRowsPath = undefined;
15685
+ this.formatNumericOptionLabels = false;
15686
+ this.optionLabelPrefixPath = undefined;
15668
15687
  this.rootPathOptions = [];
15669
15688
  this.rowPathOptions = [];
15670
15689
  }
@@ -15698,13 +15717,15 @@ class DataPanelComponent {
15698
15717
  this.staticOptions = (d.staticOptions || []).map(option => ({ ...option }));
15699
15718
  this.staticValue = d.staticValue !== undefined ? d.staticValue : this.readScalarTargetValue();
15700
15719
  this.selectedSourceId = d.datasourceId;
15701
- this.labelKey = d.labelPath ?? d.labelKey;
15702
- this.valueKey = d.valuePath ?? d.valueKey;
15720
+ this.labelKey = d.labelKey;
15721
+ this.valueKey = d.valueKey;
15703
15722
  this.rowsPath = d.rowsPath;
15704
15723
  this.rowSelectionMode = d.rowSelectionMode ?? 'first';
15705
15724
  this.selectionFieldId = d.selectionFieldId;
15706
15725
  this.selectionMatchPath = d.selectionMatchPath;
15707
15726
  this.childRowsPath = d.childRowsPath;
15727
+ this.formatNumericOptionLabels = d.formatNumericOptionLabels === true;
15728
+ this.optionLabelPrefixPath = d.optionLabelPrefixPath;
15708
15729
  // Search
15709
15730
  this.searchEnabled = !!d.searchEnabled;
15710
15731
  this.optionsLimit = d.optionsLimit;
@@ -15756,8 +15777,12 @@ class DataPanelComponent {
15756
15777
  labelKey: this.sourceType === 'source' ? this.labelKey : undefined,
15757
15778
  valueKey: this.sourceType === 'source' ? this.valueKey : undefined,
15758
15779
  rowsPath: this.sourceType === 'source' ? this.normalizedRowsPath() : undefined,
15759
- labelPath: this.sourceType === 'source' && this.shouldPersistStructuredLabelPath() ? this.labelKey : undefined,
15760
- valuePath: this.sourceType === 'source' && this.shouldPersistStructuredValuePath() ? this.valueKey : undefined,
15780
+ formatNumericOptionLabels: this.shouldPersistOptionLabelFormatting()
15781
+ ? this.formatNumericOptionLabels
15782
+ : undefined,
15783
+ optionLabelPrefixPath: this.shouldPersistOptionLabelFormatting()
15784
+ ? this.optionLabelPrefixPath
15785
+ : undefined,
15761
15786
  rowSelectionMode: this.sourceType === 'source' && this.bindingShape === 'scalar' && this.rowSelectionMode === 'selected'
15762
15787
  ? 'selected'
15763
15788
  : this.sourceType === 'source' && this.bindingShape === 'list' && this.rowSelectionMode === 'selected'
@@ -15866,6 +15891,9 @@ class DataPanelComponent {
15866
15891
  showOptionMappingControls() {
15867
15892
  return this.sourceType === 'source' && this.widgetType !== 'table' && this.usesOptionMapping();
15868
15893
  }
15894
+ showOptionLabelFormattingControls() {
15895
+ return this.widgetType === 'select' && this.usesOptionMapping();
15896
+ }
15869
15897
  showStaticOptionsEditor() {
15870
15898
  return this.sourceType === 'static' && this.widgetType !== 'table' && this.usesOptionMapping();
15871
15899
  }
@@ -15916,17 +15944,8 @@ class DataPanelComponent {
15916
15944
  const sample = this.extractPreviewRows(this.previewRows, this.effectiveRowsPath())[0];
15917
15945
  return sample ? collectArrayPaths(sample) : [];
15918
15946
  }
15919
- shouldPersistStructuredLabelPath() {
15920
- if (!this.usesOptionMapping() || !this.labelKey)
15921
- return false;
15922
- return !!this.normalizedRowsPath() || !!this.childRowsPath || hasPathSyntax$1(this.labelKey);
15923
- }
15924
- shouldPersistStructuredValuePath() {
15925
- if (!this.valueKey)
15926
- return false;
15927
- if (!this.usesOptionMapping())
15928
- return true;
15929
- return !!this.normalizedRowsPath() || !!this.childRowsPath || hasPathSyntax$1(this.valueKey);
15947
+ shouldPersistOptionLabelFormatting() {
15948
+ return this.widgetType === 'select' && this.usesOptionMapping();
15930
15949
  }
15931
15950
  usesOptionMapping() {
15932
15951
  return this.bindingShape === 'list' || this.widgetType === 'search';
@@ -16247,6 +16266,38 @@ class DataPanelComponent {
16247
16266
  </div>
16248
16267
  </div>
16249
16268
 
16269
+ <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showOptionLabelFormattingControls()">
16270
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Amount Display</div>
16271
+ <div class="mt-1 text-[11px] text-gray-500">Format the visible dropdown label without changing the stored option value.</div>
16272
+
16273
+ <label class="mt-3 flex items-center gap-2 text-sm text-gray-700">
16274
+ <input
16275
+ type="checkbox"
16276
+ [checked]="formatNumericOptionLabels"
16277
+ (change)="formatNumericOptionLabels = $any($event.target).checked; emitChange()"
16278
+ class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
16279
+ <span>Show thousand separators for numeric labels</span>
16280
+ </label>
16281
+
16282
+ <div class="mt-3 flex flex-col gap-1">
16283
+ <label class="text-xs font-medium text-gray-500">Prefix Key</label>
16284
+ <input
16285
+ [attr.list]="'prefix-cols-' + config.id"
16286
+ [(ngModel)]="optionLabelPrefixPath"
16287
+ (ngModelChange)="emitChange()"
16288
+ [placeholder]="effectiveRowsPath() ? 'e.g. currency or meta.currency' : 'e.g. currency'"
16289
+ class="h-8 w-full rounded-md border border-gray-300 px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
16290
+ <datalist [id]="'prefix-cols-' + config.id">
16291
+ <option *ngFor="let col of sourceColumns" [value]="col.name"></option>
16292
+ <option *ngFor="let path of availableRowPaths()" [value]="path"></option>
16293
+ <option *ngFor="let path of availableRootPaths()" [value]="path"></option>
16294
+ </datalist>
16295
+ <p class="text-[10px] text-gray-400">
16296
+ Reads a display-only prefix from the datasource or event payload. The select still stores only the mapped value.
16297
+ </p>
16298
+ </div>
16299
+ </div>
16300
+
16250
16301
  <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showScalarMappingControls()">
16251
16302
  <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Value Mapping</div>
16252
16303
  <div class="mt-1 text-[11px] text-gray-500">Choose the value path and how this field picks a row from the datasource.</div>
@@ -16834,6 +16885,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
16834
16885
  </div>
16835
16886
  </div>
16836
16887
 
16888
+ <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showOptionLabelFormattingControls()">
16889
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Amount Display</div>
16890
+ <div class="mt-1 text-[11px] text-gray-500">Format the visible dropdown label without changing the stored option value.</div>
16891
+
16892
+ <label class="mt-3 flex items-center gap-2 text-sm text-gray-700">
16893
+ <input
16894
+ type="checkbox"
16895
+ [checked]="formatNumericOptionLabels"
16896
+ (change)="formatNumericOptionLabels = $any($event.target).checked; emitChange()"
16897
+ class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
16898
+ <span>Show thousand separators for numeric labels</span>
16899
+ </label>
16900
+
16901
+ <div class="mt-3 flex flex-col gap-1">
16902
+ <label class="text-xs font-medium text-gray-500">Prefix Key</label>
16903
+ <input
16904
+ [attr.list]="'prefix-cols-' + config.id"
16905
+ [(ngModel)]="optionLabelPrefixPath"
16906
+ (ngModelChange)="emitChange()"
16907
+ [placeholder]="effectiveRowsPath() ? 'e.g. currency or meta.currency' : 'e.g. currency'"
16908
+ class="h-8 w-full rounded-md border border-gray-300 px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
16909
+ <datalist [id]="'prefix-cols-' + config.id">
16910
+ <option *ngFor="let col of sourceColumns" [value]="col.name"></option>
16911
+ <option *ngFor="let path of availableRowPaths()" [value]="path"></option>
16912
+ <option *ngFor="let path of availableRootPaths()" [value]="path"></option>
16913
+ </datalist>
16914
+ <p class="text-[10px] text-gray-400">
16915
+ Reads a display-only prefix from the datasource or event payload. The select still stores only the mapped value.
16916
+ </p>
16917
+ </div>
16918
+ </div>
16919
+
16837
16920
  <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showScalarMappingControls()">
16838
16921
  <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Value Mapping</div>
16839
16922
  <div class="mt-1 text-[11px] text-gray-500">Choose the value path and how this field picks a row from the datasource.</div>
@@ -31748,18 +31831,15 @@ class DefaultDataProvider extends DataProvider {
31748
31831
  return runtimeOptions;
31749
31832
  }
31750
31833
  const cfg = getEffectiveDataConfig(field);
31751
- // 1. Resolve Rows
31752
- let rows = await this.getRawRows(cfg, field, engine);
31753
- rows = this.resolveCollectionRows(rows, cfg, engine);
31754
- // 2. Apply Filters
31755
- rows = this.applyRowFilters(rows, cfg.filters, engine);
31834
+ const rowContexts = await this.getOptionRowContexts(cfg, field, engine);
31835
+ const filteredContexts = this.applyOptionRowFilters(rowContexts, cfg.filters, engine);
31756
31836
  // 3. Map to Options
31757
31837
  if (cfg.type === 'static') {
31758
- return rows.map(row => this.mapRowToOption(row, 'label', 'value'));
31838
+ return filteredContexts.map(context => this.mapContextToOption(context, 'label', 'value', cfg));
31759
31839
  }
31760
- const labelKey = cfg.labelPath || cfg.labelKey || 'label';
31761
- const valueKey = cfg.valuePath || cfg.valueKey || 'value';
31762
- return rows.map(row => this.mapRowToOption(row, labelKey, valueKey));
31840
+ const labelKey = cfg.labelKey || 'label';
31841
+ const valueKey = cfg.valueKey || 'value';
31842
+ return filteredContexts.map(context => this.mapContextToOption(context, labelKey, valueKey, cfg));
31763
31843
  }
31764
31844
  async queryOptions(field, query, engine) {
31765
31845
  const runtimeOptions = await this.getRuntimeOptions(field, engine);
@@ -31866,19 +31946,24 @@ class DefaultDataProvider extends DataProvider {
31866
31946
  }
31867
31947
  async getValue(field, engine) {
31868
31948
  const cfg = getEffectiveDataConfig(field);
31949
+ const currentValue = this.getCurrentFieldValue(field, engine);
31869
31950
  if (cfg.type === 'static') {
31870
31951
  if (cfg.staticValue !== undefined)
31871
31952
  return cfg.staticValue;
31872
- return field.defaultValue;
31953
+ return currentValue !== undefined ? currentValue : field.defaultValue;
31873
31954
  }
31874
31955
  let rows = await this.getRawRows(cfg, field, engine);
31875
31956
  rows = this.resolveCollectionRows(rows, cfg, engine);
31876
31957
  rows = this.applyRowFilters(rows, cfg.filters, engine);
31877
31958
  if (!rows || rows.length === 0) {
31878
- return field.defaultValue;
31959
+ return currentValue !== undefined ? currentValue : field.defaultValue;
31879
31960
  }
31880
- const row = this.selectScalarRow(rows, cfg, engine) ?? rows[0];
31881
- const resolvedPath = cfg.valuePath || cfg.valueKey;
31961
+ const selectedRow = this.selectScalarRow(rows, cfg, engine);
31962
+ if (cfg.rowSelectionMode === 'selected' && !selectedRow) {
31963
+ return currentValue !== undefined ? currentValue : field.defaultValue;
31964
+ }
31965
+ const row = selectedRow ?? rows[0];
31966
+ const resolvedPath = cfg.valueKey;
31882
31967
  const resolvedValue = resolvePathValue(row, resolvedPath);
31883
31968
  if (resolvedPath && resolvedValue !== undefined) {
31884
31969
  return resolvedValue;
@@ -31922,12 +32007,30 @@ class DefaultDataProvider extends DataProvider {
31922
32007
  return [];
31923
32008
  }
31924
32009
  }
32010
+ getCurrentFieldValue(field, engine) {
32011
+ return field.name && engine ? engine.getValue(field.name) : undefined;
32012
+ }
31925
32013
  async getGlobalRows(cfg) {
31926
32014
  if (!cfg.datasourceId)
31927
32015
  return [];
31928
32016
  const result = await this.client.query(cfg.datasourceId);
31929
32017
  return result.rows;
31930
32018
  }
32019
+ async getOptionRowContexts(cfg, field, engine) {
32020
+ const rows = await this.getRawRows(cfg, field, engine);
32021
+ return this.resolveCollectionRowContexts(rows, cfg, engine);
32022
+ }
32023
+ resolveCollectionRowContexts(rows, cfg, engine) {
32024
+ const parentContexts = this.extractRowContexts(rows, cfg.rowsPath);
32025
+ if (cfg.rowSelectionMode === 'selected' && cfg.childRowsPath) {
32026
+ const selectedParentContext = this.selectOptionContext(parentContexts, cfg, engine);
32027
+ if (!selectedParentContext) {
32028
+ return [];
32029
+ }
32030
+ return this.extractRowContexts([selectedParentContext.row], cfg.childRowsPath, selectedParentContext);
32031
+ }
32032
+ return parentContexts;
32033
+ }
31931
32034
  resolveCollectionRows(rows, cfg, engine) {
31932
32035
  const parentRows = this.extractRows(rows, cfg.rowsPath);
31933
32036
  if (cfg.rowSelectionMode === 'selected' && cfg.childRowsPath) {
@@ -31942,6 +32045,38 @@ class DefaultDataProvider extends DataProvider {
31942
32045
  }
31943
32046
  return parentRows;
31944
32047
  }
32048
+ extractRowContexts(rows, path, sourceContext) {
32049
+ const normalizedPath = path?.trim();
32050
+ if (!normalizedPath) {
32051
+ return rows.map(row => ({
32052
+ row,
32053
+ parentRow: sourceContext?.row,
32054
+ sourceRow: sourceContext?.sourceRow ?? row
32055
+ }));
32056
+ }
32057
+ const flattened = [];
32058
+ for (const row of rows) {
32059
+ const resolved = resolvePathValue(row, normalizedPath);
32060
+ if (Array.isArray(resolved)) {
32061
+ for (const entry of resolved) {
32062
+ flattened.push({
32063
+ row: this.toRowRecord(entry),
32064
+ parentRow: sourceContext?.row,
32065
+ sourceRow: sourceContext?.sourceRow ?? row
32066
+ });
32067
+ }
32068
+ continue;
32069
+ }
32070
+ if (resolved !== undefined && resolved !== null) {
32071
+ flattened.push({
32072
+ row: this.toRowRecord(resolved),
32073
+ parentRow: sourceContext?.row,
32074
+ sourceRow: sourceContext?.sourceRow ?? row
32075
+ });
32076
+ }
32077
+ }
32078
+ return flattened;
32079
+ }
31945
32080
  extractRows(rows, path) {
31946
32081
  const normalizedPath = path?.trim();
31947
32082
  if (!normalizedPath) {
@@ -31979,36 +32114,57 @@ class DefaultDataProvider extends DataProvider {
31979
32114
  }
31980
32115
  return rows.find(row => valuesMatch(resolvePathValue(row, cfg.selectionMatchPath), selectorValue));
31981
32116
  }
32117
+ selectOptionContext(contexts, cfg, engine) {
32118
+ if (contexts.length === 0)
32119
+ return undefined;
32120
+ if (cfg.rowSelectionMode !== 'selected' || !cfg.selectionFieldId || !cfg.selectionMatchPath || !engine) {
32121
+ return contexts[0];
32122
+ }
32123
+ const schema = engine.getSchema();
32124
+ const selectorField = schema.fields.find(candidate => candidate.id === cfg.selectionFieldId);
32125
+ if (!selectorField) {
32126
+ return undefined;
32127
+ }
32128
+ const selectorValue = engine.getValue(selectorField.name);
32129
+ if (selectorValue === undefined || selectorValue === null || selectorValue === '') {
32130
+ return undefined;
32131
+ }
32132
+ return contexts.find(context => valuesMatch(resolvePathValue(context.row, cfg.selectionMatchPath), selectorValue));
32133
+ }
31982
32134
  applyRowFilters(rows, filters, engine) {
31983
32135
  if (!filters || filters.length === 0)
31984
32136
  return rows;
31985
- return rows.filter(row => {
31986
- return filters.every(filter => {
31987
- const expected = this.resolveFilterValue(filter, engine);
31988
- if (expected === undefined)
31989
- return true; // Ignore if dependency missing
31990
- const actual = resolvePathValue(row, filter.column);
31991
- const val = expected;
31992
- switch (filter.op) {
31993
- case 'eq': return actual === val;
31994
- case 'neq': return actual !== val;
31995
- case 'in': return Array.isArray(val) ? val.includes(actual) : actual === val;
31996
- // String Ops
31997
- case 'contains':
31998
- case 'like':
31999
- return String(actual ?? '').toLowerCase().includes(String(val ?? '').toLowerCase());
32000
- case 'startsWith':
32001
- return String(actual ?? '').toLowerCase().startsWith(String(val ?? '').toLowerCase());
32002
- case 'endsWith':
32003
- return String(actual ?? '').toLowerCase().endsWith(String(val ?? '').toLowerCase());
32004
- // Comparison Ops
32005
- case 'gt': return (actual ?? 0) > (val ?? 0);
32006
- case 'gte': return (actual ?? 0) >= (val ?? 0);
32007
- case 'lt': return (actual ?? 0) < (val ?? 0);
32008
- case 'lte': return (actual ?? 0) <= (val ?? 0);
32009
- default: return true;
32010
- }
32011
- });
32137
+ return rows.filter(row => this.matchesRowFilters(row, filters, engine));
32138
+ }
32139
+ applyOptionRowFilters(contexts, filters, engine) {
32140
+ if (!filters || filters.length === 0)
32141
+ return contexts;
32142
+ return contexts.filter(context => this.matchesRowFilters(context.row, filters, engine));
32143
+ }
32144
+ matchesRowFilters(row, filters, engine) {
32145
+ return filters.every(filter => {
32146
+ const expected = this.resolveFilterValue(filter, engine);
32147
+ if (expected === undefined)
32148
+ return true;
32149
+ const actual = resolvePathValue(row, filter.column);
32150
+ const val = expected;
32151
+ switch (filter.op) {
32152
+ case 'eq': return actual === val;
32153
+ case 'neq': return actual !== val;
32154
+ case 'in': return Array.isArray(val) ? val.includes(actual) : actual === val;
32155
+ case 'contains':
32156
+ case 'like':
32157
+ return String(actual ?? '').toLowerCase().includes(String(val ?? '').toLowerCase());
32158
+ case 'startsWith':
32159
+ return String(actual ?? '').toLowerCase().startsWith(String(val ?? '').toLowerCase());
32160
+ case 'endsWith':
32161
+ return String(actual ?? '').toLowerCase().endsWith(String(val ?? '').toLowerCase());
32162
+ case 'gt': return (actual ?? 0) > (val ?? 0);
32163
+ case 'gte': return (actual ?? 0) >= (val ?? 0);
32164
+ case 'lt': return (actual ?? 0) < (val ?? 0);
32165
+ case 'lte': return (actual ?? 0) <= (val ?? 0);
32166
+ default: return true;
32167
+ }
32012
32168
  });
32013
32169
  }
32014
32170
  resolveFilters(cfg, engine) {
@@ -32066,6 +32222,56 @@ class DefaultDataProvider extends DataProvider {
32066
32222
  value: this.toOptionValue(resolvePathValue(row, valueKey))
32067
32223
  };
32068
32224
  }
32225
+ mapContextToOption(context, labelKey, valueKey, cfg) {
32226
+ const rawLabel = String(resolvePathValue(context.row, labelKey) ?? '');
32227
+ const prefix = this.resolveOptionLabelPrefix(context, cfg);
32228
+ const label = this.formatOptionLabel(rawLabel, cfg);
32229
+ return {
32230
+ label: prefix ? `${prefix} ${label}` : label,
32231
+ value: this.toOptionValue(resolvePathValue(context.row, valueKey))
32232
+ };
32233
+ }
32234
+ resolveOptionLabelPrefix(context, cfg) {
32235
+ const prefixPath = cfg.optionLabelPrefixPath?.trim();
32236
+ if (!prefixPath) {
32237
+ return '';
32238
+ }
32239
+ for (const candidate of [context.row, context.parentRow, context.sourceRow]) {
32240
+ if (!candidate)
32241
+ continue;
32242
+ const resolved = resolvePathValue(candidate, prefixPath);
32243
+ if (resolved === undefined || resolved === null)
32244
+ continue;
32245
+ const value = String(resolved).trim();
32246
+ if (value.length > 0) {
32247
+ return value;
32248
+ }
32249
+ }
32250
+ return '';
32251
+ }
32252
+ formatOptionLabel(label, cfg) {
32253
+ if (!cfg.formatNumericOptionLabels) {
32254
+ return label;
32255
+ }
32256
+ const trimmed = label.trim();
32257
+ if (!trimmed) {
32258
+ return label;
32259
+ }
32260
+ const normalized = trimmed.replace(/,/g, '');
32261
+ if (!/^-?\d+(\.\d+)?$/.test(normalized)) {
32262
+ return label;
32263
+ }
32264
+ const numericValue = Number(normalized);
32265
+ if (!Number.isFinite(numericValue)) {
32266
+ return label;
32267
+ }
32268
+ const fractionPart = normalized.split('.')[1];
32269
+ return new Intl.NumberFormat(undefined, {
32270
+ useGrouping: true,
32271
+ minimumFractionDigits: fractionPart?.length ?? 0,
32272
+ maximumFractionDigits: fractionPart?.length ?? 0
32273
+ }).format(numericValue);
32274
+ }
32069
32275
  async getRuntimeOptions(field, engine) {
32070
32276
  if (!engine)
32071
32277
  return undefined;
@@ -32080,7 +32286,7 @@ class DefaultDataProvider extends DataProvider {
32080
32286
  return cfg.type === 'source' || cfg.type === 'global' || cfg.type === 'api';
32081
32287
  }
32082
32288
  shouldUseLocalResolution(cfg) {
32083
- if (cfg.rowsPath || cfg.labelPath || cfg.valuePath || cfg.rowSelectionMode || cfg.selectionFieldId || cfg.selectionMatchPath || cfg.childRowsPath) {
32289
+ if (cfg.rowsPath || cfg.optionLabelPrefixPath || cfg.rowSelectionMode || cfg.selectionFieldId || cfg.selectionMatchPath || cfg.childRowsPath) {
32084
32290
  return true;
32085
32291
  }
32086
32292
  if (hasPathSyntax(cfg.labelKey) || hasPathSyntax(cfg.valueKey)) {
@@ -32471,7 +32677,6 @@ class TextFieldWidgetComponent {
32471
32677
  dataConfig.type ?? '',
32472
32678
  sourceKey,
32473
32679
  String(dataConfig.valueKey ?? ''),
32474
- String(dataConfig.valuePath ?? ''),
32475
32680
  String(dataConfig.rowsPath ?? ''),
32476
32681
  String(dataConfig.rowSelectionMode ?? ''),
32477
32682
  String(dataConfig.selectionFieldId ?? ''),
@@ -33266,12 +33471,14 @@ class SelectWidgetComponent {
33266
33471
  loading = false;
33267
33472
  loadError = null;
33268
33473
  options = [];
33474
+ rawOptions = [];
33269
33475
  searchTerms$ = new Subject();
33270
33476
  requestId = 0;
33271
33477
  currentSearchTerm = '';
33272
33478
  runtimeManagedField = false;
33273
33479
  runtimeOptionsLoaded = false;
33274
33480
  dependencyValueSnapshot = new Map();
33481
+ displayDependencyValueSnapshot = new Map();
33275
33482
  cachedStyleSource;
33276
33483
  cachedWrapperStyles = { width: '100%' };
33277
33484
  cachedControlCssVars = this.toCssVarMap({});
@@ -33313,7 +33520,7 @@ class SelectWidgetComponent {
33313
33520
  return;
33314
33521
  this.runtimeOptionsLoaded = false;
33315
33522
  this.currentSearchTerm = '';
33316
- this.options = [];
33523
+ this.setOptionsFromRaw([]);
33317
33524
  void this.loadOptions(this.currentSearchTerm);
33318
33525
  });
33319
33526
  }
@@ -33335,7 +33542,7 @@ class SelectWidgetComponent {
33335
33542
  this.runtimeManagedField = this.runtimeFieldDataAccessRegistry.hasFieldAccess(this.config, this.engine);
33336
33543
  if (!this.runtimeManagedField) {
33337
33544
  if (this.areApiCallsSuppressed() && this.hasSelectedValue()) {
33338
- this.options = this.withSelectedValueFallbackOptions([]);
33545
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions([]));
33339
33546
  }
33340
33547
  else {
33341
33548
  void this.loadOptions(this.currentSearchTerm);
@@ -33344,7 +33551,7 @@ class SelectWidgetComponent {
33344
33551
  }
33345
33552
  else if (this.hasSelectedValue()) {
33346
33553
  if (this.areApiCallsSuppressed()) {
33347
- this.options = this.withSelectedValueFallbackOptions([]);
33554
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions([]));
33348
33555
  this.runtimeOptionsLoaded = true;
33349
33556
  }
33350
33557
  else {
@@ -33353,32 +33560,38 @@ class SelectWidgetComponent {
33353
33560
  });
33354
33561
  }
33355
33562
  }
33356
- const dependencyIds = this.getDependencyFieldIds();
33357
33563
  if (this.engine) {
33358
33564
  const initialValues = this.engine.getValues();
33359
- this.seedDependencySnapshotFromValues(dependencyIds, initialValues);
33565
+ this.seedValueSnapshotFromValues(this.dependencyValueSnapshot, this.getDependencyFieldIds(), initialValues);
33566
+ this.seedValueSnapshotFromValues(this.displayDependencyValueSnapshot, this.getDisplayDependencyFieldIds(), initialValues);
33360
33567
  this.engine.valueChanges$
33361
33568
  .pipe(takeUntilDestroyed(this.destroyRef))
33362
33569
  .subscribe(values => {
33363
33570
  this.syncEnabledState();
33364
33571
  if (this.areApiCallsSuppressed()) {
33365
- this.options = this.withSelectedValueFallbackOptions(this.options);
33572
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(this.rawOptions));
33366
33573
  this.cdr.markForCheck();
33367
33574
  }
33368
- if (dependencyIds.length === 0)
33369
- return;
33370
33575
  const schema = this.engine.getSchema();
33371
- const depNames = schema.fields
33372
- .filter(f => dependencyIds.includes(f.id))
33373
- .map(f => f.name);
33374
- if (!this.haveDependencyValuesChanged(depNames, values))
33576
+ const dependencyIds = this.getDependencyFieldIds();
33577
+ const displayDependencyIds = this.getDisplayDependencyFieldIds();
33578
+ const dependencyNames = this.resolveFieldNames(schema.fields, dependencyIds);
33579
+ const displayDependencyNames = this.resolveFieldNames(schema.fields, displayDependencyIds);
33580
+ if (displayDependencyNames.length > 0 && this.haveFieldValuesChanged(this.displayDependencyValueSnapshot, displayDependencyNames, values)) {
33581
+ this.applyDisplayFormatting();
33582
+ this.syncStoredFieldLabel(this.control.value);
33583
+ this.cdr.markForCheck();
33584
+ }
33585
+ if (dependencyNames.length === 0)
33586
+ return;
33587
+ if (!this.haveFieldValuesChanged(this.dependencyValueSnapshot, dependencyNames, values))
33375
33588
  return;
33376
33589
  this.runtimeFieldDataAccessRegistry.invalidateFieldAndDescendants(this.engine, this.config.id);
33377
33590
  if (this.hasSelectedValue()) {
33378
33591
  this.control.setValue(null);
33379
33592
  }
33380
33593
  this.currentSearchTerm = '';
33381
- this.options = [];
33594
+ this.setOptionsFromRaw([]);
33382
33595
  this.runtimeOptionsLoaded = false;
33383
33596
  if (!this.runtimeManagedField) {
33384
33597
  void this.loadOptions(this.currentSearchTerm);
@@ -33521,6 +33734,10 @@ class SelectWidgetComponent {
33521
33734
  return [];
33522
33735
  return cfg.dependsOn.map(d => d.fieldId).filter((id) => !!id);
33523
33736
  }
33737
+ getDisplayDependencyFieldIds() {
33738
+ const prefixFieldId = this.config.dataConfig?.optionLabelPrefixFieldId;
33739
+ return prefixFieldId ? [prefixFieldId] : [];
33740
+ }
33524
33741
  toCssVarMap(controlStyles) {
33525
33742
  const vars = {
33526
33743
  '--fd-select-border-color': OUTLINED_FIELD_IDLE_BORDER_COLOR,
@@ -33589,7 +33806,7 @@ class SelectWidgetComponent {
33589
33806
  opts = await this.dataProvider.getOptions(this.config, this.engine);
33590
33807
  }
33591
33808
  if (this.requestId === reqId) {
33592
- this.options = this.withSelectedValueFallbackOptions(opts);
33809
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(opts));
33593
33810
  this.syncStoredFieldLabel(this.control.value);
33594
33811
  this.loading = false;
33595
33812
  this.loadError = null;
@@ -33601,7 +33818,7 @@ class SelectWidgetComponent {
33601
33818
  this.loading = false;
33602
33819
  this.loadError = 'Failed to load options.';
33603
33820
  if (!isSearch) {
33604
- this.options = this.withSelectedValueFallbackOptions(this.config.staticOptions || []);
33821
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(this.config.staticOptions || []));
33605
33822
  }
33606
33823
  this.cdr.markForCheck();
33607
33824
  }
@@ -33622,7 +33839,7 @@ class SelectWidgetComponent {
33622
33839
  if (this.runtimeOptionsLoaded && this.options.length > 0)
33623
33840
  return;
33624
33841
  if (this.areApiCallsSuppressed()) {
33625
- this.options = this.withSelectedValueFallbackOptions(this.options);
33842
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(this.rawOptions));
33626
33843
  this.runtimeOptionsLoaded = true;
33627
33844
  return;
33628
33845
  }
@@ -33631,25 +33848,29 @@ class SelectWidgetComponent {
33631
33848
  this.runtimeOptionsLoaded = true;
33632
33849
  }
33633
33850
  }
33634
- seedDependencySnapshotFromValues(dependencyIds, values) {
33635
- if (!this.engine || dependencyIds.length === 0)
33851
+ seedValueSnapshotFromValues(snapshot, fieldIds, values) {
33852
+ if (!this.engine || fieldIds.length === 0)
33636
33853
  return;
33637
- const schema = this.engine.getSchema();
33638
- const depNames = schema.fields
33639
- .filter(field => dependencyIds.includes(field.id))
33640
- .map(field => field.name);
33641
- for (const depName of depNames) {
33642
- this.dependencyValueSnapshot.set(depName, values[depName]);
33854
+ for (const fieldName of this.resolveFieldNames(this.engine.getSchema().fields, fieldIds)) {
33855
+ snapshot.set(fieldName, values[fieldName]);
33643
33856
  }
33644
33857
  }
33645
- haveDependencyValuesChanged(dependencyNames, values) {
33858
+ resolveFieldNames(fields, fieldIds) {
33859
+ if (fieldIds.length === 0) {
33860
+ return [];
33861
+ }
33862
+ return fields
33863
+ .filter(field => fieldIds.includes(field.id))
33864
+ .map(field => field.name);
33865
+ }
33866
+ haveFieldValuesChanged(snapshot, fieldNames, values) {
33646
33867
  let changed = false;
33647
- for (const depName of dependencyNames) {
33648
- const previousValue = this.dependencyValueSnapshot.get(depName);
33649
- const nextValue = values[depName];
33868
+ for (const fieldName of fieldNames) {
33869
+ const previousValue = snapshot.get(fieldName);
33870
+ const nextValue = values[fieldName];
33650
33871
  if (!Object.is(previousValue, nextValue)) {
33651
33872
  changed = true;
33652
- this.dependencyValueSnapshot.set(depName, nextValue);
33873
+ snapshot.set(fieldName, nextValue);
33653
33874
  }
33654
33875
  }
33655
33876
  return changed;
@@ -33661,12 +33882,19 @@ class SelectWidgetComponent {
33661
33882
  this.syncEnabledState();
33662
33883
  this.runtimeManagedField = this.runtimeFieldDataAccessRegistry.hasFieldAccess(this.config, this.engine);
33663
33884
  this.runtimeOptionsLoaded = false;
33664
- this.options = [];
33885
+ this.dependencyValueSnapshot.clear();
33886
+ this.displayDependencyValueSnapshot.clear();
33887
+ this.setOptionsFromRaw([]);
33665
33888
  this.loadError = null;
33666
33889
  this.currentSearchTerm = '';
33890
+ if (this.engine) {
33891
+ const currentValues = this.engine.getValues();
33892
+ this.seedValueSnapshotFromValues(this.dependencyValueSnapshot, this.getDependencyFieldIds(), currentValues);
33893
+ this.seedValueSnapshotFromValues(this.displayDependencyValueSnapshot, this.getDisplayDependencyFieldIds(), currentValues);
33894
+ }
33667
33895
  if (!this.runtimeManagedField) {
33668
33896
  if (this.areApiCallsSuppressed() && this.hasSelectedValue()) {
33669
- this.options = this.withSelectedValueFallbackOptions([]);
33897
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions([]));
33670
33898
  return;
33671
33899
  }
33672
33900
  void this.loadOptions(this.currentSearchTerm);
@@ -33702,6 +33930,8 @@ class SelectWidgetComponent {
33702
33930
  String(dataConfig.datasourceId ?? ''),
33703
33931
  String(dataConfig.labelKey ?? ''),
33704
33932
  String(dataConfig.valueKey ?? ''),
33933
+ String(dataConfig.formatNumericOptionLabels ?? ''),
33934
+ String(dataConfig.optionLabelPrefixFieldId ?? ''),
33705
33935
  String(dataConfig.searchEnabled ?? ''),
33706
33936
  String(dataConfig.optionsLimit ?? ''),
33707
33937
  dependencySignature,
@@ -33746,6 +33976,62 @@ class SelectWidgetComponent {
33746
33976
  this.cachedInputAttributes = { 'aria-label': accessibleLabel };
33747
33977
  }
33748
33978
  }
33979
+ setOptionsFromRaw(options) {
33980
+ this.rawOptions = [...options];
33981
+ this.applyDisplayFormatting();
33982
+ }
33983
+ applyDisplayFormatting() {
33984
+ this.options = this.rawOptions.map(option => ({
33985
+ ...option,
33986
+ label: this.formatOptionLabel(option.label)
33987
+ }));
33988
+ }
33989
+ formatOptionLabel(label) {
33990
+ const formattedLabel = this.config.dataConfig?.formatNumericOptionLabels
33991
+ ? this.formatNumericLabel(label)
33992
+ : label;
33993
+ const prefix = this.resolveOptionLabelPrefix();
33994
+ return prefix ? `${prefix} ${formattedLabel}` : formattedLabel;
33995
+ }
33996
+ resolveOptionLabelPrefix() {
33997
+ const prefixFieldId = this.config.dataConfig?.optionLabelPrefixFieldId;
33998
+ if (!prefixFieldId
33999
+ || !this.engine
34000
+ || typeof this.engine.getValue !== 'function') {
34001
+ return '';
34002
+ }
34003
+ const prefixField = this.engine.getSchema().fields.find(field => field.id === prefixFieldId);
34004
+ if (!prefixField) {
34005
+ return '';
34006
+ }
34007
+ const value = this.engine.getValue(prefixField.name);
34008
+ if (value === undefined || value === null) {
34009
+ return '';
34010
+ }
34011
+ const trimmed = String(value).trim();
34012
+ return trimmed;
34013
+ }
34014
+ formatNumericLabel(label) {
34015
+ const trimmed = label.trim();
34016
+ if (!trimmed) {
34017
+ return label;
34018
+ }
34019
+ const normalized = trimmed.replace(/,/g, '');
34020
+ if (!/^-?\d+(\.\d+)?$/.test(normalized)) {
34021
+ return label;
34022
+ }
34023
+ const numericValue = Number(normalized);
34024
+ if (!Number.isFinite(numericValue)) {
34025
+ return label;
34026
+ }
34027
+ const fractionPart = normalized.split('.')[1];
34028
+ const formatter = new Intl.NumberFormat(undefined, {
34029
+ useGrouping: true,
34030
+ minimumFractionDigits: fractionPart?.length ?? 0,
34031
+ maximumFractionDigits: fractionPart?.length ?? 0
34032
+ });
34033
+ return formatter.format(numericValue);
34034
+ }
33749
34035
  toSafeNonNegativeInt(value, fallback) {
33750
34036
  if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
33751
34037
  return Math.floor(value);
@@ -33779,7 +34065,11 @@ class SelectWidgetComponent {
33779
34065
  if (!this.hasMeaningfulValue(rawValue)) {
33780
34066
  return [];
33781
34067
  }
33782
- return buildFallbackOptions(rawValue, this.getStoredFieldLabel());
34068
+ const storedLabel = this.getStoredFieldLabel();
34069
+ if (this.shouldUseStoredFallbackLabel(storedLabel)) {
34070
+ return buildFallbackOptions(rawValue, storedLabel);
34071
+ }
34072
+ return buildFallbackOptions(rawValue);
33783
34073
  }
33784
34074
  hasOptionValue(options, value) {
33785
34075
  return options.some(option => Object.is(option.value, value) || String(option.value) === String(value));
@@ -33818,6 +34108,18 @@ class SelectWidgetComponent {
33818
34108
  }
33819
34109
  return this.engine.getFieldLabel(this.config.name);
33820
34110
  }
34111
+ shouldUseStoredFallbackLabel(label) {
34112
+ if (label === undefined) {
34113
+ return false;
34114
+ }
34115
+ if (this.config.dataConfig?.optionLabelPrefixPath) {
34116
+ return true;
34117
+ }
34118
+ if (this.config.dataConfig?.optionLabelPrefixFieldId) {
34119
+ return this.resolveOptionLabelPrefix().length === 0;
34120
+ }
34121
+ return this.config.dataConfig?.formatNumericOptionLabels !== true;
34122
+ }
33821
34123
  setStoredFieldLabel(label) {
33822
34124
  if (!this.engine || typeof this.engine.setFieldLabel !== 'function') {
33823
34125
  return;