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

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);
@@ -31877,8 +31957,13 @@ class DefaultDataProvider extends DataProvider {
31877
31957
  if (!rows || rows.length === 0) {
31878
31958
  return field.defaultValue;
31879
31959
  }
31880
- const row = this.selectScalarRow(rows, cfg, engine) ?? rows[0];
31881
- const resolvedPath = cfg.valuePath || cfg.valueKey;
31960
+ const selectedRow = this.selectScalarRow(rows, cfg, engine);
31961
+ if (cfg.rowSelectionMode === 'selected' && !selectedRow) {
31962
+ const currentValue = field.name && engine ? engine.getValue(field.name) : undefined;
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;
@@ -31928,6 +32013,21 @@ class DefaultDataProvider extends DataProvider {
31928
32013
  const result = await this.client.query(cfg.datasourceId);
31929
32014
  return result.rows;
31930
32015
  }
32016
+ async getOptionRowContexts(cfg, field, engine) {
32017
+ const rows = await this.getRawRows(cfg, field, engine);
32018
+ return this.resolveCollectionRowContexts(rows, cfg, engine);
32019
+ }
32020
+ resolveCollectionRowContexts(rows, cfg, engine) {
32021
+ const parentContexts = this.extractRowContexts(rows, cfg.rowsPath);
32022
+ if (cfg.rowSelectionMode === 'selected' && cfg.childRowsPath) {
32023
+ const selectedParentContext = this.selectOptionContext(parentContexts, cfg, engine);
32024
+ if (!selectedParentContext) {
32025
+ return [];
32026
+ }
32027
+ return this.extractRowContexts([selectedParentContext.row], cfg.childRowsPath, selectedParentContext);
32028
+ }
32029
+ return parentContexts;
32030
+ }
31931
32031
  resolveCollectionRows(rows, cfg, engine) {
31932
32032
  const parentRows = this.extractRows(rows, cfg.rowsPath);
31933
32033
  if (cfg.rowSelectionMode === 'selected' && cfg.childRowsPath) {
@@ -31942,6 +32042,38 @@ class DefaultDataProvider extends DataProvider {
31942
32042
  }
31943
32043
  return parentRows;
31944
32044
  }
32045
+ extractRowContexts(rows, path, sourceContext) {
32046
+ const normalizedPath = path?.trim();
32047
+ if (!normalizedPath) {
32048
+ return rows.map(row => ({
32049
+ row,
32050
+ parentRow: sourceContext?.row,
32051
+ sourceRow: sourceContext?.sourceRow ?? row
32052
+ }));
32053
+ }
32054
+ const flattened = [];
32055
+ for (const row of rows) {
32056
+ const resolved = resolvePathValue(row, normalizedPath);
32057
+ if (Array.isArray(resolved)) {
32058
+ for (const entry of resolved) {
32059
+ flattened.push({
32060
+ row: this.toRowRecord(entry),
32061
+ parentRow: sourceContext?.row,
32062
+ sourceRow: sourceContext?.sourceRow ?? row
32063
+ });
32064
+ }
32065
+ continue;
32066
+ }
32067
+ if (resolved !== undefined && resolved !== null) {
32068
+ flattened.push({
32069
+ row: this.toRowRecord(resolved),
32070
+ parentRow: sourceContext?.row,
32071
+ sourceRow: sourceContext?.sourceRow ?? row
32072
+ });
32073
+ }
32074
+ }
32075
+ return flattened;
32076
+ }
31945
32077
  extractRows(rows, path) {
31946
32078
  const normalizedPath = path?.trim();
31947
32079
  if (!normalizedPath) {
@@ -31979,36 +32111,57 @@ class DefaultDataProvider extends DataProvider {
31979
32111
  }
31980
32112
  return rows.find(row => valuesMatch(resolvePathValue(row, cfg.selectionMatchPath), selectorValue));
31981
32113
  }
32114
+ selectOptionContext(contexts, cfg, engine) {
32115
+ if (contexts.length === 0)
32116
+ return undefined;
32117
+ if (cfg.rowSelectionMode !== 'selected' || !cfg.selectionFieldId || !cfg.selectionMatchPath || !engine) {
32118
+ return contexts[0];
32119
+ }
32120
+ const schema = engine.getSchema();
32121
+ const selectorField = schema.fields.find(candidate => candidate.id === cfg.selectionFieldId);
32122
+ if (!selectorField) {
32123
+ return undefined;
32124
+ }
32125
+ const selectorValue = engine.getValue(selectorField.name);
32126
+ if (selectorValue === undefined || selectorValue === null || selectorValue === '') {
32127
+ return undefined;
32128
+ }
32129
+ return contexts.find(context => valuesMatch(resolvePathValue(context.row, cfg.selectionMatchPath), selectorValue));
32130
+ }
31982
32131
  applyRowFilters(rows, filters, engine) {
31983
32132
  if (!filters || filters.length === 0)
31984
32133
  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
- });
32134
+ return rows.filter(row => this.matchesRowFilters(row, filters, engine));
32135
+ }
32136
+ applyOptionRowFilters(contexts, filters, engine) {
32137
+ if (!filters || filters.length === 0)
32138
+ return contexts;
32139
+ return contexts.filter(context => this.matchesRowFilters(context.row, filters, engine));
32140
+ }
32141
+ matchesRowFilters(row, filters, engine) {
32142
+ return filters.every(filter => {
32143
+ const expected = this.resolveFilterValue(filter, engine);
32144
+ if (expected === undefined)
32145
+ return true;
32146
+ const actual = resolvePathValue(row, filter.column);
32147
+ const val = expected;
32148
+ switch (filter.op) {
32149
+ case 'eq': return actual === val;
32150
+ case 'neq': return actual !== val;
32151
+ case 'in': return Array.isArray(val) ? val.includes(actual) : actual === val;
32152
+ case 'contains':
32153
+ case 'like':
32154
+ return String(actual ?? '').toLowerCase().includes(String(val ?? '').toLowerCase());
32155
+ case 'startsWith':
32156
+ return String(actual ?? '').toLowerCase().startsWith(String(val ?? '').toLowerCase());
32157
+ case 'endsWith':
32158
+ return String(actual ?? '').toLowerCase().endsWith(String(val ?? '').toLowerCase());
32159
+ case 'gt': return (actual ?? 0) > (val ?? 0);
32160
+ case 'gte': return (actual ?? 0) >= (val ?? 0);
32161
+ case 'lt': return (actual ?? 0) < (val ?? 0);
32162
+ case 'lte': return (actual ?? 0) <= (val ?? 0);
32163
+ default: return true;
32164
+ }
32012
32165
  });
32013
32166
  }
32014
32167
  resolveFilters(cfg, engine) {
@@ -32066,6 +32219,56 @@ class DefaultDataProvider extends DataProvider {
32066
32219
  value: this.toOptionValue(resolvePathValue(row, valueKey))
32067
32220
  };
32068
32221
  }
32222
+ mapContextToOption(context, labelKey, valueKey, cfg) {
32223
+ const rawLabel = String(resolvePathValue(context.row, labelKey) ?? '');
32224
+ const prefix = this.resolveOptionLabelPrefix(context, cfg);
32225
+ const label = this.formatOptionLabel(rawLabel, cfg);
32226
+ return {
32227
+ label: prefix ? `${prefix} ${label}` : label,
32228
+ value: this.toOptionValue(resolvePathValue(context.row, valueKey))
32229
+ };
32230
+ }
32231
+ resolveOptionLabelPrefix(context, cfg) {
32232
+ const prefixPath = cfg.optionLabelPrefixPath?.trim();
32233
+ if (!prefixPath) {
32234
+ return '';
32235
+ }
32236
+ for (const candidate of [context.row, context.parentRow, context.sourceRow]) {
32237
+ if (!candidate)
32238
+ continue;
32239
+ const resolved = resolvePathValue(candidate, prefixPath);
32240
+ if (resolved === undefined || resolved === null)
32241
+ continue;
32242
+ const value = String(resolved).trim();
32243
+ if (value.length > 0) {
32244
+ return value;
32245
+ }
32246
+ }
32247
+ return '';
32248
+ }
32249
+ formatOptionLabel(label, cfg) {
32250
+ if (!cfg.formatNumericOptionLabels) {
32251
+ return label;
32252
+ }
32253
+ const trimmed = label.trim();
32254
+ if (!trimmed) {
32255
+ return label;
32256
+ }
32257
+ const normalized = trimmed.replace(/,/g, '');
32258
+ if (!/^-?\d+(\.\d+)?$/.test(normalized)) {
32259
+ return label;
32260
+ }
32261
+ const numericValue = Number(normalized);
32262
+ if (!Number.isFinite(numericValue)) {
32263
+ return label;
32264
+ }
32265
+ const fractionPart = normalized.split('.')[1];
32266
+ return new Intl.NumberFormat(undefined, {
32267
+ useGrouping: true,
32268
+ minimumFractionDigits: fractionPart?.length ?? 0,
32269
+ maximumFractionDigits: fractionPart?.length ?? 0
32270
+ }).format(numericValue);
32271
+ }
32069
32272
  async getRuntimeOptions(field, engine) {
32070
32273
  if (!engine)
32071
32274
  return undefined;
@@ -32080,7 +32283,7 @@ class DefaultDataProvider extends DataProvider {
32080
32283
  return cfg.type === 'source' || cfg.type === 'global' || cfg.type === 'api';
32081
32284
  }
32082
32285
  shouldUseLocalResolution(cfg) {
32083
- if (cfg.rowsPath || cfg.labelPath || cfg.valuePath || cfg.rowSelectionMode || cfg.selectionFieldId || cfg.selectionMatchPath || cfg.childRowsPath) {
32286
+ if (cfg.rowsPath || cfg.optionLabelPrefixPath || cfg.rowSelectionMode || cfg.selectionFieldId || cfg.selectionMatchPath || cfg.childRowsPath) {
32084
32287
  return true;
32085
32288
  }
32086
32289
  if (hasPathSyntax(cfg.labelKey) || hasPathSyntax(cfg.valueKey)) {
@@ -32471,7 +32674,6 @@ class TextFieldWidgetComponent {
32471
32674
  dataConfig.type ?? '',
32472
32675
  sourceKey,
32473
32676
  String(dataConfig.valueKey ?? ''),
32474
- String(dataConfig.valuePath ?? ''),
32475
32677
  String(dataConfig.rowsPath ?? ''),
32476
32678
  String(dataConfig.rowSelectionMode ?? ''),
32477
32679
  String(dataConfig.selectionFieldId ?? ''),
@@ -33266,12 +33468,14 @@ class SelectWidgetComponent {
33266
33468
  loading = false;
33267
33469
  loadError = null;
33268
33470
  options = [];
33471
+ rawOptions = [];
33269
33472
  searchTerms$ = new Subject();
33270
33473
  requestId = 0;
33271
33474
  currentSearchTerm = '';
33272
33475
  runtimeManagedField = false;
33273
33476
  runtimeOptionsLoaded = false;
33274
33477
  dependencyValueSnapshot = new Map();
33478
+ displayDependencyValueSnapshot = new Map();
33275
33479
  cachedStyleSource;
33276
33480
  cachedWrapperStyles = { width: '100%' };
33277
33481
  cachedControlCssVars = this.toCssVarMap({});
@@ -33313,7 +33517,7 @@ class SelectWidgetComponent {
33313
33517
  return;
33314
33518
  this.runtimeOptionsLoaded = false;
33315
33519
  this.currentSearchTerm = '';
33316
- this.options = [];
33520
+ this.setOptionsFromRaw([]);
33317
33521
  void this.loadOptions(this.currentSearchTerm);
33318
33522
  });
33319
33523
  }
@@ -33335,7 +33539,7 @@ class SelectWidgetComponent {
33335
33539
  this.runtimeManagedField = this.runtimeFieldDataAccessRegistry.hasFieldAccess(this.config, this.engine);
33336
33540
  if (!this.runtimeManagedField) {
33337
33541
  if (this.areApiCallsSuppressed() && this.hasSelectedValue()) {
33338
- this.options = this.withSelectedValueFallbackOptions([]);
33542
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions([]));
33339
33543
  }
33340
33544
  else {
33341
33545
  void this.loadOptions(this.currentSearchTerm);
@@ -33344,7 +33548,7 @@ class SelectWidgetComponent {
33344
33548
  }
33345
33549
  else if (this.hasSelectedValue()) {
33346
33550
  if (this.areApiCallsSuppressed()) {
33347
- this.options = this.withSelectedValueFallbackOptions([]);
33551
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions([]));
33348
33552
  this.runtimeOptionsLoaded = true;
33349
33553
  }
33350
33554
  else {
@@ -33353,32 +33557,38 @@ class SelectWidgetComponent {
33353
33557
  });
33354
33558
  }
33355
33559
  }
33356
- const dependencyIds = this.getDependencyFieldIds();
33357
33560
  if (this.engine) {
33358
33561
  const initialValues = this.engine.getValues();
33359
- this.seedDependencySnapshotFromValues(dependencyIds, initialValues);
33562
+ this.seedValueSnapshotFromValues(this.dependencyValueSnapshot, this.getDependencyFieldIds(), initialValues);
33563
+ this.seedValueSnapshotFromValues(this.displayDependencyValueSnapshot, this.getDisplayDependencyFieldIds(), initialValues);
33360
33564
  this.engine.valueChanges$
33361
33565
  .pipe(takeUntilDestroyed(this.destroyRef))
33362
33566
  .subscribe(values => {
33363
33567
  this.syncEnabledState();
33364
33568
  if (this.areApiCallsSuppressed()) {
33365
- this.options = this.withSelectedValueFallbackOptions(this.options);
33569
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(this.rawOptions));
33366
33570
  this.cdr.markForCheck();
33367
33571
  }
33368
- if (dependencyIds.length === 0)
33369
- return;
33370
33572
  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))
33573
+ const dependencyIds = this.getDependencyFieldIds();
33574
+ const displayDependencyIds = this.getDisplayDependencyFieldIds();
33575
+ const dependencyNames = this.resolveFieldNames(schema.fields, dependencyIds);
33576
+ const displayDependencyNames = this.resolveFieldNames(schema.fields, displayDependencyIds);
33577
+ if (displayDependencyNames.length > 0 && this.haveFieldValuesChanged(this.displayDependencyValueSnapshot, displayDependencyNames, values)) {
33578
+ this.applyDisplayFormatting();
33579
+ this.syncStoredFieldLabel(this.control.value);
33580
+ this.cdr.markForCheck();
33581
+ }
33582
+ if (dependencyNames.length === 0)
33583
+ return;
33584
+ if (!this.haveFieldValuesChanged(this.dependencyValueSnapshot, dependencyNames, values))
33375
33585
  return;
33376
33586
  this.runtimeFieldDataAccessRegistry.invalidateFieldAndDescendants(this.engine, this.config.id);
33377
33587
  if (this.hasSelectedValue()) {
33378
33588
  this.control.setValue(null);
33379
33589
  }
33380
33590
  this.currentSearchTerm = '';
33381
- this.options = [];
33591
+ this.setOptionsFromRaw([]);
33382
33592
  this.runtimeOptionsLoaded = false;
33383
33593
  if (!this.runtimeManagedField) {
33384
33594
  void this.loadOptions(this.currentSearchTerm);
@@ -33521,6 +33731,10 @@ class SelectWidgetComponent {
33521
33731
  return [];
33522
33732
  return cfg.dependsOn.map(d => d.fieldId).filter((id) => !!id);
33523
33733
  }
33734
+ getDisplayDependencyFieldIds() {
33735
+ const prefixFieldId = this.config.dataConfig?.optionLabelPrefixFieldId;
33736
+ return prefixFieldId ? [prefixFieldId] : [];
33737
+ }
33524
33738
  toCssVarMap(controlStyles) {
33525
33739
  const vars = {
33526
33740
  '--fd-select-border-color': OUTLINED_FIELD_IDLE_BORDER_COLOR,
@@ -33589,7 +33803,7 @@ class SelectWidgetComponent {
33589
33803
  opts = await this.dataProvider.getOptions(this.config, this.engine);
33590
33804
  }
33591
33805
  if (this.requestId === reqId) {
33592
- this.options = this.withSelectedValueFallbackOptions(opts);
33806
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(opts));
33593
33807
  this.syncStoredFieldLabel(this.control.value);
33594
33808
  this.loading = false;
33595
33809
  this.loadError = null;
@@ -33601,7 +33815,7 @@ class SelectWidgetComponent {
33601
33815
  this.loading = false;
33602
33816
  this.loadError = 'Failed to load options.';
33603
33817
  if (!isSearch) {
33604
- this.options = this.withSelectedValueFallbackOptions(this.config.staticOptions || []);
33818
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(this.config.staticOptions || []));
33605
33819
  }
33606
33820
  this.cdr.markForCheck();
33607
33821
  }
@@ -33622,7 +33836,7 @@ class SelectWidgetComponent {
33622
33836
  if (this.runtimeOptionsLoaded && this.options.length > 0)
33623
33837
  return;
33624
33838
  if (this.areApiCallsSuppressed()) {
33625
- this.options = this.withSelectedValueFallbackOptions(this.options);
33839
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(this.rawOptions));
33626
33840
  this.runtimeOptionsLoaded = true;
33627
33841
  return;
33628
33842
  }
@@ -33631,25 +33845,29 @@ class SelectWidgetComponent {
33631
33845
  this.runtimeOptionsLoaded = true;
33632
33846
  }
33633
33847
  }
33634
- seedDependencySnapshotFromValues(dependencyIds, values) {
33635
- if (!this.engine || dependencyIds.length === 0)
33848
+ seedValueSnapshotFromValues(snapshot, fieldIds, values) {
33849
+ if (!this.engine || fieldIds.length === 0)
33636
33850
  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]);
33851
+ for (const fieldName of this.resolveFieldNames(this.engine.getSchema().fields, fieldIds)) {
33852
+ snapshot.set(fieldName, values[fieldName]);
33643
33853
  }
33644
33854
  }
33645
- haveDependencyValuesChanged(dependencyNames, values) {
33855
+ resolveFieldNames(fields, fieldIds) {
33856
+ if (fieldIds.length === 0) {
33857
+ return [];
33858
+ }
33859
+ return fields
33860
+ .filter(field => fieldIds.includes(field.id))
33861
+ .map(field => field.name);
33862
+ }
33863
+ haveFieldValuesChanged(snapshot, fieldNames, values) {
33646
33864
  let changed = false;
33647
- for (const depName of dependencyNames) {
33648
- const previousValue = this.dependencyValueSnapshot.get(depName);
33649
- const nextValue = values[depName];
33865
+ for (const fieldName of fieldNames) {
33866
+ const previousValue = snapshot.get(fieldName);
33867
+ const nextValue = values[fieldName];
33650
33868
  if (!Object.is(previousValue, nextValue)) {
33651
33869
  changed = true;
33652
- this.dependencyValueSnapshot.set(depName, nextValue);
33870
+ snapshot.set(fieldName, nextValue);
33653
33871
  }
33654
33872
  }
33655
33873
  return changed;
@@ -33661,12 +33879,19 @@ class SelectWidgetComponent {
33661
33879
  this.syncEnabledState();
33662
33880
  this.runtimeManagedField = this.runtimeFieldDataAccessRegistry.hasFieldAccess(this.config, this.engine);
33663
33881
  this.runtimeOptionsLoaded = false;
33664
- this.options = [];
33882
+ this.dependencyValueSnapshot.clear();
33883
+ this.displayDependencyValueSnapshot.clear();
33884
+ this.setOptionsFromRaw([]);
33665
33885
  this.loadError = null;
33666
33886
  this.currentSearchTerm = '';
33887
+ if (this.engine) {
33888
+ const currentValues = this.engine.getValues();
33889
+ this.seedValueSnapshotFromValues(this.dependencyValueSnapshot, this.getDependencyFieldIds(), currentValues);
33890
+ this.seedValueSnapshotFromValues(this.displayDependencyValueSnapshot, this.getDisplayDependencyFieldIds(), currentValues);
33891
+ }
33667
33892
  if (!this.runtimeManagedField) {
33668
33893
  if (this.areApiCallsSuppressed() && this.hasSelectedValue()) {
33669
- this.options = this.withSelectedValueFallbackOptions([]);
33894
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions([]));
33670
33895
  return;
33671
33896
  }
33672
33897
  void this.loadOptions(this.currentSearchTerm);
@@ -33702,6 +33927,8 @@ class SelectWidgetComponent {
33702
33927
  String(dataConfig.datasourceId ?? ''),
33703
33928
  String(dataConfig.labelKey ?? ''),
33704
33929
  String(dataConfig.valueKey ?? ''),
33930
+ String(dataConfig.formatNumericOptionLabels ?? ''),
33931
+ String(dataConfig.optionLabelPrefixFieldId ?? ''),
33705
33932
  String(dataConfig.searchEnabled ?? ''),
33706
33933
  String(dataConfig.optionsLimit ?? ''),
33707
33934
  dependencySignature,
@@ -33746,6 +33973,62 @@ class SelectWidgetComponent {
33746
33973
  this.cachedInputAttributes = { 'aria-label': accessibleLabel };
33747
33974
  }
33748
33975
  }
33976
+ setOptionsFromRaw(options) {
33977
+ this.rawOptions = [...options];
33978
+ this.applyDisplayFormatting();
33979
+ }
33980
+ applyDisplayFormatting() {
33981
+ this.options = this.rawOptions.map(option => ({
33982
+ ...option,
33983
+ label: this.formatOptionLabel(option.label)
33984
+ }));
33985
+ }
33986
+ formatOptionLabel(label) {
33987
+ const formattedLabel = this.config.dataConfig?.formatNumericOptionLabels
33988
+ ? this.formatNumericLabel(label)
33989
+ : label;
33990
+ const prefix = this.resolveOptionLabelPrefix();
33991
+ return prefix ? `${prefix} ${formattedLabel}` : formattedLabel;
33992
+ }
33993
+ resolveOptionLabelPrefix() {
33994
+ const prefixFieldId = this.config.dataConfig?.optionLabelPrefixFieldId;
33995
+ if (!prefixFieldId
33996
+ || !this.engine
33997
+ || typeof this.engine.getValue !== 'function') {
33998
+ return '';
33999
+ }
34000
+ const prefixField = this.engine.getSchema().fields.find(field => field.id === prefixFieldId);
34001
+ if (!prefixField) {
34002
+ return '';
34003
+ }
34004
+ const value = this.engine.getValue(prefixField.name);
34005
+ if (value === undefined || value === null) {
34006
+ return '';
34007
+ }
34008
+ const trimmed = String(value).trim();
34009
+ return trimmed;
34010
+ }
34011
+ formatNumericLabel(label) {
34012
+ const trimmed = label.trim();
34013
+ if (!trimmed) {
34014
+ return label;
34015
+ }
34016
+ const normalized = trimmed.replace(/,/g, '');
34017
+ if (!/^-?\d+(\.\d+)?$/.test(normalized)) {
34018
+ return label;
34019
+ }
34020
+ const numericValue = Number(normalized);
34021
+ if (!Number.isFinite(numericValue)) {
34022
+ return label;
34023
+ }
34024
+ const fractionPart = normalized.split('.')[1];
34025
+ const formatter = new Intl.NumberFormat(undefined, {
34026
+ useGrouping: true,
34027
+ minimumFractionDigits: fractionPart?.length ?? 0,
34028
+ maximumFractionDigits: fractionPart?.length ?? 0
34029
+ });
34030
+ return formatter.format(numericValue);
34031
+ }
33749
34032
  toSafeNonNegativeInt(value, fallback) {
33750
34033
  if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
33751
34034
  return Math.floor(value);
@@ -33779,7 +34062,11 @@ class SelectWidgetComponent {
33779
34062
  if (!this.hasMeaningfulValue(rawValue)) {
33780
34063
  return [];
33781
34064
  }
33782
- return buildFallbackOptions(rawValue, this.getStoredFieldLabel());
34065
+ const storedLabel = this.getStoredFieldLabel();
34066
+ if (this.shouldUseStoredFallbackLabel(storedLabel)) {
34067
+ return buildFallbackOptions(rawValue, storedLabel);
34068
+ }
34069
+ return buildFallbackOptions(rawValue);
33783
34070
  }
33784
34071
  hasOptionValue(options, value) {
33785
34072
  return options.some(option => Object.is(option.value, value) || String(option.value) === String(value));
@@ -33818,6 +34105,18 @@ class SelectWidgetComponent {
33818
34105
  }
33819
34106
  return this.engine.getFieldLabel(this.config.name);
33820
34107
  }
34108
+ shouldUseStoredFallbackLabel(label) {
34109
+ if (label === undefined) {
34110
+ return false;
34111
+ }
34112
+ if (this.config.dataConfig?.optionLabelPrefixPath) {
34113
+ return true;
34114
+ }
34115
+ if (this.config.dataConfig?.optionLabelPrefixFieldId) {
34116
+ return this.resolveOptionLabelPrefix().length === 0;
34117
+ }
34118
+ return this.config.dataConfig?.formatNumericOptionLabels !== true;
34119
+ }
33821
34120
  setStoredFieldLabel(label) {
33822
34121
  if (!this.engine || typeof this.engine.setFieldLabel !== 'function') {
33823
34122
  return;