@pega/angular-sdk-overrides 0.26.2 → 0.26.3

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.
Files changed (69) hide show
  1. package/lib/designSystemExtension/field-group/field-group.component.ts +11 -3
  2. package/lib/designSystemExtension/material-case-summary/material-case-summary.component.html +12 -3
  3. package/lib/field/auto-complete/auto-complete.component.html +44 -4
  4. package/lib/field/auto-complete/auto-complete.component.scss +92 -0
  5. package/lib/field/auto-complete/auto-complete.component.ts +341 -16
  6. package/lib/field/cancel-alert/cancel-alert.component.ts +38 -15
  7. package/lib/field/group/group.component.html +1 -1
  8. package/lib/field/group/group.component.ts +1 -3
  9. package/lib/field/semantic-link/semantic-link.component.html +1 -1
  10. package/lib/field/semantic-link/semantic-link.component.scss +4 -0
  11. package/lib/infra/Containers/flow-container/flow-container.component.html +1 -1
  12. package/lib/infra/Containers/modal-view-container/data-view-action-buttons/data-view-action-buttons.component.html +12 -0
  13. package/lib/infra/Containers/modal-view-container/data-view-action-buttons/data-view-action-buttons.component.spec.ts +130 -0
  14. package/lib/infra/Containers/modal-view-container/data-view-action-buttons/data-view-action-buttons.component.ts +77 -0
  15. package/lib/infra/Containers/modal-view-container/modal-view-container.component.html +35 -8
  16. package/lib/infra/Containers/modal-view-container/modal-view-container.component.spec.ts +24 -11
  17. package/lib/infra/Containers/modal-view-container/modal-view-container.component.ts +179 -184
  18. package/lib/infra/assignment/assignment.component.html +2 -1
  19. package/lib/infra/assignment/assignment.component.ts +30 -13
  20. package/lib/infra/defer-load/defer-load.component.ts +6 -3
  21. package/lib/infra/multi-step/multi-step.component.html +25 -33
  22. package/lib/infra/multi-step/multi-step.component.scss +48 -0
  23. package/lib/infra/multi-step/multi-step.component.ts +11 -0
  24. package/lib/infra/view/view.component.ts +17 -39
  25. package/lib/template/base/details-template-base.ts +35 -54
  26. package/lib/template/case-summary/case-summary.component.ts +15 -1
  27. package/lib/template/case-view/case-view.component.html +13 -1
  28. package/lib/template/case-view/case-view.component.spec.ts +119 -10
  29. package/lib/template/case-view/case-view.component.ts +52 -3
  30. package/lib/template/data-reference/data-reference.component.ts +27 -18
  31. package/lib/template/data-reference/search-form/search-form.component.html +1 -1
  32. package/lib/template/data-reference/search-form/search-form.component.ts +2 -0
  33. package/lib/template/details/details.component.html +23 -3
  34. package/lib/template/details/details.component.scss +9 -2
  35. package/lib/template/details/details.component.ts +3 -32
  36. package/lib/template/details-narrow-wide/details-narrow-wide.component.html +21 -2
  37. package/lib/template/details-narrow-wide/details-narrow-wide.component.scss +8 -1
  38. package/lib/template/details-narrow-wide/details-narrow-wide.component.ts +3 -37
  39. package/lib/template/details-one-column/details-one-column.component.html +23 -1
  40. package/lib/template/details-one-column/details-one-column.component.scss +14 -0
  41. package/lib/template/details-one-column/details-one-column.component.ts +3 -38
  42. package/lib/template/details-three-column/details-three-column.component.html +21 -2
  43. package/lib/template/details-three-column/details-three-column.component.scss +8 -1
  44. package/lib/template/details-three-column/details-three-column.component.ts +3 -46
  45. package/lib/template/details-two-column/details-two-column.component.html +21 -2
  46. package/lib/template/details-two-column/details-two-column.component.scss +8 -1
  47. package/lib/template/details-two-column/details-two-column.component.ts +3 -44
  48. package/lib/template/details-wide-narrow/details-wide-narrow.component.html +21 -2
  49. package/lib/template/details-wide-narrow/details-wide-narrow.component.scss +8 -1
  50. package/lib/template/details-wide-narrow/details-wide-narrow.component.ts +3 -41
  51. package/lib/template/field-value-list/field-value-list.component.scss +17 -0
  52. package/lib/template/list-view/list-view.component.ts +39 -0
  53. package/lib/template/self-service-case-view/self-service-case-view.component.html +13 -1
  54. package/lib/template/self-service-case-view/self-service-case-view.component.spec.ts +96 -11
  55. package/lib/template/self-service-case-view/self-service-case-view.component.ts +52 -3
  56. package/lib/template/simple-table-manual/helpers.ts +27 -6
  57. package/lib/template/simple-table-manual/simple-table-manual.component.html +1 -3
  58. package/lib/template/simple-table-manual/simple-table-manual.component.scss +4 -0
  59. package/lib/template/simple-table-manual/simple-table-manual.component.ts +51 -20
  60. package/lib/template/single-reference-readonly/single-reference-readonly.component.scss +0 -4
  61. package/package.json +1 -1
  62. package/lib/designSystemExtension/material-details/material-details.component.html +0 -40
  63. package/lib/designSystemExtension/material-details/material-details.component.scss +0 -116
  64. package/lib/designSystemExtension/material-details/material-details.component.spec.ts +0 -24
  65. package/lib/designSystemExtension/material-details/material-details.component.ts +0 -31
  66. package/lib/designSystemExtension/material-details-fields/material-details-fields.component.html +0 -50
  67. package/lib/designSystemExtension/material-details-fields/material-details-fields.component.scss +0 -34
  68. package/lib/designSystemExtension/material-details-fields/material-details-fields.component.spec.ts +0 -22
  69. package/lib/designSystemExtension/material-details-fields/material-details-fields.component.ts +0 -61
@@ -1,5 +1,5 @@
1
1
  import { CommonModule } from '@angular/common';
2
- import { Component, Input, TemplateRef } from '@angular/core';
2
+ import { Component, Input, OnChanges, SimpleChanges, TemplateRef } from '@angular/core';
3
3
  import { MatGridListModule } from '@angular/material/grid-list';
4
4
  import { MatIconModule } from '@angular/material/icon';
5
5
 
@@ -9,14 +9,22 @@ import { MatIconModule } from '@angular/material/icon';
9
9
  styleUrls: ['./field-group.component.scss'],
10
10
  imports: [CommonModule, MatGridListModule, MatIconModule]
11
11
  })
12
- export class FieldGroupComponent {
12
+ export class FieldGroupComponent implements OnChanges {
13
13
  @Input() name?: string;
14
- @Input() collapsible = false;
14
+ @Input() collapseOnLoad: 'none' | 'expanded' | 'collapsed' = 'none';
15
15
  @Input() instructions: string;
16
16
  @Input() childrenTemplate: TemplateRef<any>;
17
17
 
18
+ collapsible = false;
18
19
  collapsed = false;
19
20
 
21
+ ngOnChanges(changes: SimpleChanges): void {
22
+ if (changes['collapseOnLoad']) {
23
+ this.collapsible = this.collapseOnLoad !== 'none';
24
+ this.collapsed = this.collapseOnLoad === 'collapsed';
25
+ }
26
+ }
27
+
20
28
  headerClickHandler() {
21
29
  this.collapsed = !this.collapsed;
22
30
  }
@@ -18,6 +18,9 @@
18
18
  <a *ngSwitchCase="'email'" class="psdk-primary-color" href="mailto: {{ field.config.value }}">{{ field.config.value }}</a>
19
19
  <span *ngSwitchCase="'date'" class="psdk-csf-text-style">{{ field.config.value }}</span>
20
20
  <label *ngSwitchCase="'caseoperator'">operator</label>
21
+ <div *ngSwitchCase="'objectreference'" class="psdk-csf-object-reference-value">
22
+ <component-mapper name="ObjectReference" [props]="{ pConn$: field.kid, displayMode: 'DISPLAY_ONLY', hideLabel: true }"></component-mapper>
23
+ </div>
21
24
  <span *ngSwitchDefault class="psdk-csf-text-style">{{ field.config.value }}</span>
22
25
  </dd>
23
26
  </ng-template>
@@ -46,9 +49,15 @@
46
49
  {{ field.config?.displayLabel || field.config?.label }}
47
50
  </dt>
48
51
  <dd class="psdk-csf-secondary-field-data">
49
- <div [ngSwitch]="field.type">
50
- <label *ngSwitchCase="'UserReference'" class="psdk-secondary-value">{{ field.config.value.userName || '---' }}</label>
51
- <label *ngSwitchCase="'Checkbox'" class="psdk-secondary-value">{{ field.config.falseLabel || '---' }}</label>
52
+ <div [ngSwitch]="field.type.toLowerCase()">
53
+ <label *ngSwitchCase="'userreference'" class="psdk-secondary-value">{{ field.config.value.userName || '---' }}</label>
54
+ <label *ngSwitchCase="'checkbox'" class="psdk-secondary-value">{{ field.config.falseLabel || '---' }}</label>
55
+ <div *ngSwitchCase="'objectreference'" class="psdk-csf-object-reference-value">
56
+ <component-mapper
57
+ name="ObjectReference"
58
+ [props]="{ pConn$: field.kid, displayMode: 'DISPLAY_ONLY', hideLabel: true }"
59
+ ></component-mapper>
60
+ </div>
52
61
  <label *ngSwitchDefault class="psdk-secondary-value">{{ field.config.value || '---' }}</label>
53
62
  </div>
54
63
  </dd>
@@ -15,10 +15,50 @@
15
15
  [matAutocomplete]="auto"
16
16
  (input)="fieldOnChange($event)"
17
17
  />
18
- <mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption (optionSelected)="optionChanged($event)">
19
- <mat-option *ngFor="let opt of filteredOptions | async" [value]="opt.value">
20
- <span>{{ opt.value }}</span>
21
- </mat-option>
18
+ <mat-autocomplete #auto="matAutocomplete" [class]="'psdk-autocomplete-panel'" autoActiveFirstOption (optionSelected)="optionChanged($event)">
19
+ <ng-container *ngIf="hasGroupBy; else flatOptionsList">
20
+ <mat-optgroup *ngFor="let group of groupedFilteredOptions$ | async" [label]="group.label">
21
+ <mat-option *ngFor="let opt of group.options" [value]="opt.value">
22
+ <span class="psdk-autocomplete-option-primary">{{ opt.value }}</span>
23
+ <div class="psdk-autocomplete-option-secondary" *ngIf="opt.secondaryComponents?.length">
24
+ <ng-container *ngFor="let secondary of opt.secondaryComponents; let last = last">
25
+ <component-mapper
26
+ [name]="secondary.getPConnect().getComponentName()"
27
+ [props]="{ pConn$: secondary.getPConnect(), formGroup$ }"
28
+ ></component-mapper>
29
+ <span *ngIf="!last" class="psdk-autocomplete-option-separator" aria-hidden="true"> · </span>
30
+ </ng-container>
31
+ </div>
32
+ </mat-option>
33
+ </mat-optgroup>
34
+ </ng-container>
35
+ <ng-template #flatOptionsList>
36
+ <mat-option *ngFor="let opt of filteredOptions | async" [value]="opt.value">
37
+ <span class="psdk-autocomplete-option-primary">{{ opt.value }}</span>
38
+ <div class="psdk-autocomplete-option-secondary" *ngIf="opt.secondaryComponents?.length">
39
+ <ng-container *ngFor="let secondary of opt.secondaryComponents; let last = last">
40
+ <component-mapper
41
+ [name]="secondary.getPConnect().getComponentName()"
42
+ [props]="{ pConn$: secondary.getPConnect(), formGroup$ }"
43
+ ></component-mapper>
44
+ <span *ngIf="!last" class="psdk-autocomplete-option-separator" aria-hidden="true"> · </span>
45
+ </ng-container>
46
+ </div>
47
+ </mat-option>
48
+ </ng-template>
49
+ <div *ngIf="showCreateButton" class="psdk-autocomplete-create-new-wrapper">
50
+ <mat-divider></mat-divider>
51
+ <button
52
+ mat-button
53
+ type="button"
54
+ class="psdk-autocomplete-create-new"
55
+ (mousedown)="$event.preventDefault()"
56
+ (click)="createNewButtonHandler()"
57
+ >
58
+ <mat-icon>add</mat-icon>
59
+ {{ createNewLabel }}
60
+ </button>
61
+ </div>
22
62
  </mat-autocomplete>
23
63
  <mat-hint *ngIf="helperText" [appFieldWarning]="bFieldMessageVisible$">{{ helperText }}</mat-hint>
24
64
  <mat-error *ngIf="fieldControl.invalid">{{ getErrorMessage() }}</mat-error>
@@ -18,3 +18,95 @@
18
18
  ::ng-deep .mat-mdc-form-field-infix {
19
19
  width: auto;
20
20
  }
21
+
22
+ // Scoped to this component's own panel via mat-autocomplete's [class] input — does not affect
23
+ // other mat-option/mat-autocomplete instances in the app (research.md §3).
24
+ ::ng-deep .psdk-autocomplete-panel {
25
+ overflow-x: hidden;
26
+
27
+ .mat-mdc-option {
28
+ .mdc-list-item__primary-text {
29
+ width: 100%;
30
+ white-space: normal;
31
+ overflow: visible;
32
+ text-overflow: unset;
33
+ }
34
+
35
+ // Only grow/top-align options that actually render secondary content — leaves options
36
+ // without secondary text at Material's default single-line height/vertical centering.
37
+ &:has(.psdk-autocomplete-option-secondary) {
38
+ height: auto;
39
+ min-height: 48px;
40
+ align-items: flex-start;
41
+ padding-top: 8px;
42
+ padding-bottom: 8px;
43
+ }
44
+ }
45
+ }
46
+
47
+ .psdk-autocomplete-option-secondary {
48
+ display: flex;
49
+ flex-wrap: wrap;
50
+ align-items: baseline;
51
+ column-gap: 0.25rem;
52
+ width: 100%;
53
+ overflow-wrap: break-word;
54
+ word-break: break-word;
55
+ color: var(--mat-sys-on-surface-variant);
56
+
57
+ // Each secondary field renders through FieldValueList's own DISPLAY_ONLY template, which
58
+ // defaults to a two-column grid block (.psdk-container-labels-left) or an unset-display
59
+ // block (.psdk-container-nolabels). Override both to render inline here so multiple fields
60
+ // join on one line via the `·` separators, instead of stacking as separate block rows
61
+ // (FR-002/FR-004). FieldValueListComponent itself is not modified (research.md §6).
62
+ ::ng-deep .psdk-container-labels-left {
63
+ display: inline-flex;
64
+ column-gap: 0.25rem;
65
+ row-gap: 0;
66
+ padding-block: 0;
67
+ }
68
+
69
+ ::ng-deep .psdk-container-nolabels {
70
+ display: inline;
71
+ padding-block: 0;
72
+ }
73
+
74
+ ::ng-deep .psdk-grid-label::after {
75
+ content: ':';
76
+ }
77
+
78
+ ::ng-deep .psdk-val-labels-left {
79
+ white-space: normal;
80
+ }
81
+ }
82
+
83
+ ::ng-deep .psdk-autocomplete-create-new-wrapper {
84
+ position: sticky;
85
+ bottom: 0;
86
+ z-index: 1;
87
+ background-color: var(--mat-sys-surface-container, var(--mat-sys-surface, #fff));
88
+
89
+ .mat-divider {
90
+ margin: 0;
91
+ }
92
+
93
+ .psdk-autocomplete-create-new {
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: flex-start;
97
+ width: 100%;
98
+ height: 48px;
99
+ padding: 0 16px;
100
+ text-align: left;
101
+ text-transform: none;
102
+ color: var(--mat-sys-primary);
103
+
104
+ .mat-mdc-button-touch-target {
105
+ width: 100%;
106
+ }
107
+
108
+ .mat-icon {
109
+ margin-right: 8px;
110
+ }
111
+ }
112
+ }
@@ -1,10 +1,13 @@
1
- import { Component, EventEmitter, OnInit, Output, forwardRef, inject } from '@angular/core';
1
+ import { Component, EventEmitter, OnInit, Output, ViewChild, forwardRef, inject } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import { ReactiveFormsModule } from '@angular/forms';
4
4
  import { MatOptionModule } from '@angular/material/core';
5
- import { MatAutocompleteModule } from '@angular/material/autocomplete';
5
+ import { MatAutocompleteModule, MatAutocompleteTrigger } from '@angular/material/autocomplete';
6
6
  import { MatInputModule } from '@angular/material/input';
7
7
  import { MatFormFieldModule } from '@angular/material/form-field';
8
+ import { MatButtonModule } from '@angular/material/button';
9
+ import { MatIconModule } from '@angular/material/icon';
10
+ import { MatDividerModule } from '@angular/material/divider';
8
11
  import { Observable } from 'rxjs';
9
12
  import { map, startWith } from 'rxjs/operators';
10
13
 
@@ -15,10 +18,22 @@ import { DatapageService } from '@pega/angular-sdk-components';
15
18
  import { handleEvent } from '@pega/angular-sdk-components';
16
19
  import { PConnFieldProps } from '@pega/angular-sdk-components';
17
20
 
18
- interface IOption {
21
+ interface AutoCompleteOption {
19
22
  key: string;
20
23
  value: string;
24
+ // Present only when at least one secondary column resolves to a non-empty value
25
+ secondaryComponents?: any[];
26
+ secondarySearchText?: string;
27
+ // Present only when a group-by field is configured for this (datapage-sourced) field
28
+ group?: string;
21
29
  }
30
+
31
+ // Internal, render-time-only view-model — never part of the PConnect contract
32
+ interface AutoCompleteGroup {
33
+ label: string;
34
+ options: AutoCompleteOption[];
35
+ }
36
+
22
37
  interface AutoCompleteProps extends PConnFieldProps {
23
38
  // If any, enter additional props that only exist on AutoComplete here
24
39
  deferDatasource?: boolean;
@@ -29,6 +44,12 @@ interface AutoCompleteProps extends PConnFieldProps {
29
44
  parameters?: any;
30
45
  datasource: any;
31
46
  columns: any[];
47
+ allowCreatingRecords?: boolean;
48
+ onCreateNew?: () => void;
49
+ createNewLabel?: string;
50
+ createNewRecord?: () => Promise<unknown>;
51
+ contextClass?: string;
52
+ referenceType?: string;
32
53
  }
33
54
 
34
55
  @Component({
@@ -42,6 +63,9 @@ interface AutoCompleteProps extends PConnFieldProps {
42
63
  MatInputModule,
43
64
  MatAutocompleteModule,
44
65
  MatOptionModule,
66
+ MatButtonModule,
67
+ MatIconModule,
68
+ MatDividerModule,
45
69
  FieldWarningDirective,
46
70
  forwardRef(() => ComponentMapperComponent)
47
71
  ],
@@ -52,15 +76,29 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
52
76
 
53
77
  @Output() onRecordChange: EventEmitter<any> = new EventEmitter();
54
78
 
79
+ @ViewChild(MatAutocompleteTrigger) private autocompleteTrigger?: MatAutocompleteTrigger;
80
+
55
81
  configProps$: AutoCompleteProps;
56
82
 
57
- options$: any[];
83
+ options$: AutoCompleteOption[];
58
84
  listType: string;
59
- columns = [];
85
+ columns: any[] = [];
60
86
  parameters: {};
61
- filteredOptions: Observable<any[]>;
87
+ datasource: any;
88
+ filteredOptions: Observable<AutoCompleteOption[]>;
89
+ // Grouped view of filteredOptions, only rendered when hasGroupBy is true
90
+ groupedFilteredOptions$: Observable<AutoCompleteGroup[]>;
91
+ hasGroupBy = false;
62
92
  filterValue = '';
63
93
 
94
+ // "Create new" footer button state
95
+ showCreateButton = false;
96
+ createNewLabel = 'Create new';
97
+ contextClass?: string;
98
+ referenceType?: string;
99
+ private onCreateNewFn?: () => void;
100
+ private createNewRecordFn?: () => Promise<unknown>;
101
+
64
102
  // Override ngOnInit method
65
103
  override async ngOnInit(): Promise<void> {
66
104
  super.ngOnInit();
@@ -69,18 +107,36 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
69
107
  startWith(''),
70
108
  map(value => this._filter((value as string) || ''))
71
109
  );
110
+
111
+ this.groupedFilteredOptions$ = this.filteredOptions.pipe(map(options => this.buildGroups(options)));
72
112
  }
73
113
 
74
- setOptions(options: IOption[]) {
114
+ setOptions(options: AutoCompleteOption[]) {
75
115
  this.options$ = options;
76
116
  const index = this.options$?.findIndex(element => element.key === this.configProps$.value);
77
117
  this.value$ = index > -1 ? this.options$[index].value : this.configProps$.value;
78
118
  this.fieldControl.setValue(this.value$);
79
119
  }
80
120
 
81
- private _filter(value: string): string[] {
121
+ // Matches only primary text and secondary search text — group value is never used for search
122
+ private _filter(value: string): AutoCompleteOption[] {
82
123
  const filterVal = (value || this.filterValue).toLowerCase();
83
- return this.options$?.filter(option => option.value?.toLowerCase().includes(filterVal));
124
+ return this.options$?.filter(option => option.value?.toLowerCase().includes(filterVal) || option.secondarySearchText?.includes(filterVal));
125
+ }
126
+
127
+ // Buckets the already-sorted option list into contiguous groups by exact group value
128
+ buildGroups(options: AutoCompleteOption[]): AutoCompleteGroup[] {
129
+ const groups: AutoCompleteGroup[] = [];
130
+ options?.forEach(option => {
131
+ const label = option.group ?? '';
132
+ const lastGroup = groups[groups.length - 1];
133
+ if (lastGroup && lastGroup.label === label) {
134
+ lastGroup.options.push(option);
135
+ } else {
136
+ groups.push({ label, options: [option] });
137
+ }
138
+ });
139
+ return groups;
84
140
  }
85
141
 
86
142
  /**
@@ -94,18 +150,29 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
94
150
  this.updateComponentCommonProperties(this.configProps$);
95
151
 
96
152
  // Set component specific properties
97
- const { value, listType, parameters } = this.configProps$;
153
+ const { value, listType, parameters, allowCreatingRecords, onCreateNew, createNewLabel, createNewRecord, contextClass, referenceType } =
154
+ this.configProps$;
98
155
 
99
156
  this.listType = listType;
100
157
  this.parameters = parameters;
101
158
 
159
+ this.showCreateButton = allowCreatingRecords === true;
160
+ this.createNewLabel = createNewLabel || 'Create new';
161
+ this.contextClass = contextClass;
162
+ this.referenceType = referenceType;
163
+ this.onCreateNewFn = onCreateNew;
164
+ this.createNewRecordFn = createNewRecord;
165
+
102
166
  const context = this.pConn$.getContextName();
103
167
  const { columns, datasource } = this.generateColumnsAndDataSource();
168
+ this.datasource = datasource;
104
169
 
105
170
  if (columns) {
106
171
  this.columns = this.preProcessColumns(columns);
107
172
  }
108
173
 
174
+ this.hasGroupBy = this.columns?.some(col => col.groupBy === 'true') ?? false;
175
+
109
176
  if (this.listType === 'associated') {
110
177
  const optionsList = this.utils.getOptionList(this.configProps$, this.pConn$.getDataObject('')); // 1st arg empty string until typedef marked correctly
111
178
  this.setOptions(optionsList);
@@ -152,22 +219,157 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
152
219
  ];
153
220
  }
154
221
 
222
+ // Secondary text and grouping are both out of scope for associated/local list options
223
+ if (this.listType !== 'associated') {
224
+ const secondaryColumns = this.getSecondaryColumnsFromMetadata();
225
+ if (secondaryColumns.length > 0) {
226
+ columns = [...(columns || []), ...secondaryColumns];
227
+ }
228
+
229
+ const groupByColumns = this.getGroupByColumnsFromMetadata();
230
+ if (groupByColumns.length > 0) {
231
+ columns = [...(columns || []), ...groupByColumns];
232
+ }
233
+ }
234
+
155
235
  return { columns, datasource };
156
236
  }
157
237
 
238
+ // Reads unresolved groupsFields metadata to derive group-by column descriptor(s); not a
239
+ // display/search column, so grouping stays independent of primary/secondary text
240
+ getGroupByColumnsFromMetadata() {
241
+ const groupsFields = (this.pConn$.getRawMetadata()?.config as any)?.groupsFields;
242
+ if (!Array.isArray(groupsFields)) {
243
+ return [];
244
+ }
245
+ return this.mapMetadataColumns(groupsFields, { display: 'false', groupBy: 'true', useForSearch: false });
246
+ }
247
+
248
+ // Reads unresolved columnsFormatter metadata to derive secondary (contextual) display columns
249
+ getSecondaryColumnsFromMetadata() {
250
+ const columnsFormatter = (this.pConn$.getRawMetadata()?.config as any)?.columnsFormatter;
251
+ if (!Array.isArray(columnsFormatter)) {
252
+ return [];
253
+ }
254
+ return this.mapMetadataColumns(columnsFormatter, { display: 'true', secondary: 'true', useForSearch: true });
255
+ }
256
+
257
+ // Shared by getSecondaryColumnsFromMetadata/getGroupByColumnsFromMetadata: value must stay an
258
+ // unresolved property reference (e.g. "@P .propName") for use as a raw-row lookup key
259
+ mapMetadataColumns(rawColumns: any[], columnFlags: object): any[] {
260
+ return rawColumns
261
+ .map(item => {
262
+ const property = item?.config?.value;
263
+ if (typeof property !== 'string' || !property) {
264
+ return undefined;
265
+ }
266
+ let value = property;
267
+ if (property.startsWith('@P ')) {
268
+ value = property.substring(3);
269
+ } else if (property.startsWith('@USER ')) {
270
+ value = property.substring(6);
271
+ }
272
+ return { value, type: item?.type, label: item?.config?.label, ...columnFlags };
273
+ })
274
+ .filter(Boolean);
275
+ }
276
+
158
277
  fillOptions(results: any) {
159
- const optionsData: any[] = [];
278
+ const optionsData: AutoCompleteOption[] = [];
160
279
  const displayColumn = this.getDisplayFieldsMetaData(this.columns);
280
+ const secondaryColumns = this.columns?.filter(col => col.display === 'true' && col.secondary === 'true') || [];
281
+ const groupByColumn = this.columns?.find(col => col.groupBy === 'true');
282
+
161
283
  results?.forEach(element => {
162
- const obj = {
284
+ const obj: AutoCompleteOption = {
163
285
  key: element[displayColumn.key] || element.pyGUID,
164
286
  value: element[displayColumn.primary]?.toString()
165
287
  };
288
+
289
+ if (secondaryColumns.length > 0) {
290
+ const secondaryComponents = this.buildSecondaryComponents(element, secondaryColumns);
291
+ if (secondaryComponents.length > 0) {
292
+ obj.secondaryComponents = secondaryComponents;
293
+ }
294
+
295
+ const secondarySearchText = this.buildSecondarySearchText(element, secondaryColumns);
296
+ if (secondarySearchText) {
297
+ obj.secondarySearchText = secondarySearchText;
298
+ }
299
+ }
300
+
301
+ if (groupByColumn) {
302
+ obj.group = this.resolveGroupValue(element[groupByColumn.value as string]);
303
+ }
304
+
166
305
  optionsData.push(obj);
167
306
  });
307
+
308
+ if (groupByColumn) {
309
+ this.sortByGroup(optionsData);
310
+ }
311
+
168
312
  this.setOptions(optionsData);
169
313
  }
170
314
 
315
+ // Null/undefined/whitespace-only source values normalize to '' — the shared blank group
316
+ resolveGroupValue(rawValue: any): string {
317
+ if (rawValue === null || rawValue === undefined) {
318
+ return '';
319
+ }
320
+ const stringValue = rawValue.toString();
321
+ return stringValue.trim() ? stringValue : '';
322
+ }
323
+
324
+ // Ascending, case-sensitive, stable sort so same-group options keep their original relative order
325
+ sortByGroup(options: AutoCompleteOption[]): void {
326
+ options.sort((a, b) => {
327
+ const groupA = a.group ?? '';
328
+ const groupB = b.group ?? '';
329
+ if (groupA < groupB) {
330
+ return -1;
331
+ }
332
+ if (groupA > groupB) {
333
+ return 1;
334
+ }
335
+ return 0;
336
+ });
337
+ }
338
+
339
+ // Rendering only — one read-only PConnect component per configured secondary field, in
340
+ // configured order, regardless of whether its value is empty (FieldValueList's own
341
+ // empty-value fallback renders the placeholder, e.g. "Label: ---"). Mirrors ScalarListComponent's
342
+ // createComponent/DISPLAY_ONLY pattern.
343
+ buildSecondaryComponents(element: any, secondaryColumns): any[] {
344
+ return secondaryColumns.map(col =>
345
+ this.pConn$.createComponent(
346
+ {
347
+ type: col.type,
348
+ config: {
349
+ value: element[col.value as string],
350
+ displayMode: 'DISPLAY_ONLY',
351
+ readOnly: true,
352
+ label: col.label
353
+ }
354
+ },
355
+ '',
356
+ 0,
357
+ {}
358
+ )
359
+ ); // 2nd, 3rd, and 4th args empty string/object/null until typedef marked correctly as optional
360
+ }
361
+
362
+ // Search only — independent of buildSecondaryComponents; never derived from rendered output.
363
+ buildSecondarySearchText(element: any, secondaryColumns): string {
364
+ return secondaryColumns
365
+ .map(col => {
366
+ const rawValue = element[col.value as string];
367
+ return rawValue === null || rawValue === undefined ? '' : rawValue.toString().trim().toLowerCase();
368
+ })
369
+ .filter(Boolean)
370
+ .join(' ');
371
+ }
372
+
171
373
  flattenParameters(params = {}) {
172
374
  const flatParams = {};
173
375
  Object.keys(params).forEach(key => {
@@ -182,12 +384,12 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
182
384
  const displayColumns = columnList.filter(col => col.display === 'true');
183
385
  const metaDataObj: any = { key: '', primary: '', secondary: [] };
184
386
  const keyCol = columnList.filter(col => col.key === 'true');
185
- metaDataObj.key = keyCol.length > 0 ? keyCol[0].value : 'auto';
387
+ metaDataObj.key = keyCol.length > 0 ? (keyCol[0].value ?? 'auto') : 'auto';
186
388
  for (let index = 0; index < displayColumns.length; index += 1) {
187
389
  if (displayColumns[index].primary === 'true') {
188
- metaDataObj.primary = displayColumns[index].value;
189
- } else {
190
- metaDataObj.secondary.push(displayColumns[index].value);
390
+ metaDataObj.primary = displayColumns[index].value ?? '';
391
+ } else if (displayColumns[index].secondary === 'true') {
392
+ metaDataObj.secondary.push(displayColumns[index].value ?? '');
191
393
  }
192
394
  }
193
395
  return metaDataObj;
@@ -222,4 +424,127 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
222
424
  this.onRecordChange.emit(value);
223
425
  }
224
426
  }
427
+
428
+ // Re-fetches the options list
429
+ refreshOptionsList(): void {
430
+ if (!this.displayMode$ && this.listType !== 'associated') {
431
+ const context = this.pConn$.getContextName();
432
+ this.dataPageService
433
+ .getDataPageData(this.datasource, this.parameters, context)
434
+ .then((results: any) => this.fillOptions(results))
435
+ .catch(e => console.error(e));
436
+ }
437
+ }
438
+
439
+ // Sets values for all columns that have setProperty defined
440
+ setValuesToAdditionalFields(record: Record<string, unknown>): void {
441
+ const setPropertyList = this.columns.filter(col => col.setProperty).map(col => ({ source: col.value, target: col.setProperty, key: col.key }));
442
+
443
+ setPropertyList.forEach(prop => {
444
+ let valueToSet: string;
445
+ if (prop.key === 'true') {
446
+ valueToSet = record[prop.source]?.toString() || (record as any).pyGUID || '';
447
+ } else {
448
+ valueToSet = record[prop.source]?.toString() || '';
449
+ }
450
+
451
+ if (prop.target === 'Associated property') {
452
+ handleEvent(this.actionsApi, 'changeNblur', this.propName, valueToSet);
453
+ } else {
454
+ const target = typeof prop.target === 'string' ? prop.target : '';
455
+ const targetProp = target.startsWith('.') ? target : `.${target}`;
456
+ (this.actionsApi as any).updateFieldValue(targetProp, valueToSet, { associatedProperty: this.propName });
457
+ (this.actionsApi as any).triggerFieldChange(targetProp, valueToSet);
458
+ }
459
+ });
460
+ }
461
+
462
+ createNewButtonHandler(): void {
463
+ // Close the options panel so it doesn't remain open on top of the create-new modal
464
+ this.autocompleteTrigger?.closePanel();
465
+
466
+ if (this.onCreateNewFn) {
467
+ this.onCreateNewFn();
468
+ return;
469
+ }
470
+
471
+ if (!this.contextClass) {
472
+ return;
473
+ }
474
+
475
+ const context = this.pConn$.getContextName();
476
+ const normalizedReferenceType = typeof this.referenceType === 'string' ? this.referenceType.toLowerCase() : '';
477
+ const isDataReference = normalizedReferenceType === 'data';
478
+ const { CREATE_STAGE_DONE } = PCore.getConstants().PUB_SUB_EVENTS.CASE_EVENTS;
479
+ const DATA_OBJECT_CREATED = (PCore.getConstants().PUB_SUB_EVENTS as any).DATA_EVENTS?.DATA_OBJECT_CREATED;
480
+ const eventType = isDataReference && DATA_OBJECT_CREATED ? DATA_OBJECT_CREATED : CREATE_STAGE_DONE;
481
+ const contextClass = this.contextClass;
482
+
483
+ const createNewCallback = isDataReference
484
+ ? (data: { data?: { responseData?: Record<string, unknown> } }) => {
485
+ // Clear contexted cache before re-fetching
486
+ PCore.getDataApi().clearContextedCache(context);
487
+
488
+ const responseData = data?.data?.responseData;
489
+ if (responseData) {
490
+ this.setValuesToAdditionalFields(responseData);
491
+ const displayColumn = this.getDisplayFieldsMetaData(this.columns);
492
+ const newKey = responseData[displayColumn.key]?.toString() || (responseData as any).pyGUID;
493
+ if (this.onRecordChange && newKey) {
494
+ this.onRecordChange.emit({ id: newKey });
495
+ }
496
+ }
497
+
498
+ this.refreshOptionsList();
499
+ PCore.getPubSubUtils().unsubscribe(eventType, contextClass);
500
+ }
501
+ : (data: { caseId?: string; caseType?: string; ID?: string }) => {
502
+ // Clear contexted cache before re-fetching
503
+ PCore.getDataApi().clearContextedCache(context);
504
+
505
+ const newCaseId = data.caseId?.split(' ').pop();
506
+ if (data.caseType === contextClass) {
507
+ const selectKey = data.ID || newCaseId;
508
+
509
+ if (selectKey && this.listType !== 'associated' && this.datasource) {
510
+ this.dataPageService
511
+ .getDataPageData(this.datasource, this.parameters, context)
512
+ .then((results: any) => {
513
+ this.fillOptions(results);
514
+
515
+ const displayColumn = this.getDisplayFieldsMetaData(this.columns);
516
+ const newRecord = results?.find((el: any) => el.ID === data.ID || (el[displayColumn.key] || el.pyGUID) === selectKey);
517
+ if (newRecord) {
518
+ this.setValuesToAdditionalFields(newRecord);
519
+ } else {
520
+ handleEvent(this.actionsApi, 'changeNblur', this.propName, selectKey);
521
+ }
522
+ if (this.onRecordChange) {
523
+ this.onRecordChange.emit({ id: selectKey });
524
+ }
525
+ })
526
+ .catch(e => console.error(e));
527
+ }
528
+ PCore.getPubSubUtils().unsubscribe(eventType, contextClass);
529
+ }
530
+ };
531
+
532
+ // Build the create action if createNewRecord fn is not provided
533
+ const triggerCreate = this.createNewRecordFn
534
+ ? this.createNewRecordFn()
535
+ : isDataReference
536
+ ? this.pConn$.getActionsApi().showDataObjectCreateView(contextClass)
537
+ : this.pConn$.getActionsApi().createWork(contextClass, {
538
+ openCaseViewAfterCreate: false,
539
+ startingFields: {}
540
+ });
541
+
542
+ Promise.resolve(triggerCreate)
543
+ .then(() => {
544
+ PCore.getPubSubUtils().subscribe(eventType, createNewCallback, contextClass);
545
+ // Re-initialize the list
546
+ this.refreshOptionsList();
547
+ })
548
+ .catch(e => console.error(e));
549
+ }
225
550
  }