Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* Github: [PR-562](https://github.com/pegasystems/angular-sdk-components/pull/562)
* **DataReference as Autocomplete supports Secondary Text.**
* Github: [PR-569](https://github.com/pegasystems/angular-sdk-components/pull/569)
* **DataReference as Autocomplete supports grouping.**
* Github: [PR-578](https://github.com/pegasystems/angular-sdk-components/pull/578)
* **Added left and right alignment support for vertical multi-step assignment navigation.**
* Github: [PR-576](https://github.com/pegasystems/angular-sdk-components/pull/576)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,36 @@
(input)="fieldOnChange($event)"
/>
<mat-autocomplete #auto="matAutocomplete" [class]="'psdk-autocomplete-panel'" autoActiveFirstOption (optionSelected)="optionChanged($event)">
<mat-option *ngFor="let opt of filteredOptions | async" [value]="opt.value">
<span class="psdk-autocomplete-option-primary">{{ opt.value }}</span>
<div class="psdk-autocomplete-option-secondary" *ngIf="opt.secondaryComponents?.length">
<ng-container *ngFor="let secondary of opt.secondaryComponents; let last = last">
<component-mapper
[name]="secondary.getPConnect().getComponentName()"
[props]="{ pConn$: secondary.getPConnect(), formGroup$ }"
></component-mapper>
<span *ngIf="!last" class="psdk-autocomplete-option-separator" aria-hidden="true"> · </span>
</ng-container>
</div>
</mat-option>
<ng-container *ngIf="hasGroupBy; else flatOptionsList">
<mat-optgroup *ngFor="let group of groupedFilteredOptions$ | async" [label]="group.label">
<mat-option *ngFor="let opt of group.options" [value]="opt.value">
<span class="psdk-autocomplete-option-primary">{{ opt.value }}</span>
<div class="psdk-autocomplete-option-secondary" *ngIf="opt.secondaryComponents?.length">
<ng-container *ngFor="let secondary of opt.secondaryComponents; let last = last">
<component-mapper
[name]="secondary.getPConnect().getComponentName()"
[props]="{ pConn$: secondary.getPConnect(), formGroup$ }"
></component-mapper>
<span *ngIf="!last" class="psdk-autocomplete-option-separator" aria-hidden="true"> · </span>
</ng-container>
</div>
</mat-option>
</mat-optgroup>
</ng-container>
<ng-template #flatOptionsList>
<mat-option *ngFor="let opt of filteredOptions | async" [value]="opt.value">
<span class="psdk-autocomplete-option-primary">{{ opt.value }}</span>
<div class="psdk-autocomplete-option-secondary" *ngIf="opt.secondaryComponents?.length">
<ng-container *ngFor="let secondary of opt.secondaryComponents; let last = last">
<component-mapper
[name]="secondary.getPConnect().getComponentName()"
[props]="{ pConn$: secondary.getPConnect(), formGroup$ }"
></component-mapper>
<span *ngIf="!last" class="psdk-autocomplete-option-separator" aria-hidden="true"> · </span>
</ng-container>
</div>
</mat-option>
</ng-template>
</mat-autocomplete>
<mat-hint *ngIf="helperText" [appFieldWarning]="bFieldMessageVisible$">{{ helperText }}</mat-hint>
<mat-error *ngIf="fieldControl.invalid">{{ getErrorMessage() }}</mat-error>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,17 @@ import { PConnFieldProps } from '../../../_types/PConnProps.interface';
interface AutoCompleteOption {
key: string;
value: string;
// Present only when at least one secondary column resolves to a non-empty value (research.md §4a/§4b)
// Present only when at least one secondary column resolves to a non-empty value
secondaryComponents?: any[];
secondarySearchText?: string;
// Present only when a group-by field is configured for this (datapage-sourced) field
group?: string;
}

// Internal, render-time-only view-model — never part of the PConnect contract
interface AutoCompleteGroup {
label: string;
options: AutoCompleteOption[];
}

interface AutoCompleteProps extends PConnFieldProps {
Expand Down Expand Up @@ -63,6 +71,9 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
columns: any[] = [];
parameters: {};
filteredOptions: Observable<AutoCompleteOption[]>;
// Grouped view of filteredOptions, only rendered when hasGroupBy is true
groupedFilteredOptions$: Observable<AutoCompleteGroup[]>;
hasGroupBy = false;
filterValue = '';

// Override ngOnInit method
Expand All @@ -73,6 +84,8 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
startWith(''),
map(value => this._filter((value as string) || ''))
);

this.groupedFilteredOptions$ = this.filteredOptions.pipe(map(options => this.buildGroups(options)));
}

setOptions(options: AutoCompleteOption[]) {
Expand All @@ -82,11 +95,27 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
this.fieldControl.setValue(this.value$);
}

// Matches only primary text and secondary search text — group value is never used for search
private _filter(value: string): AutoCompleteOption[] {
const filterVal = (value || this.filterValue).toLowerCase();
return this.options$?.filter(option => option.value?.toLowerCase().includes(filterVal) || option.secondarySearchText?.includes(filterVal));
}

// Buckets the already-sorted option list into contiguous groups by exact group value
buildGroups(options: AutoCompleteOption[]): AutoCompleteGroup[] {
const groups: AutoCompleteGroup[] = [];
options?.forEach(option => {
const label = option.group ?? '';
const lastGroup = groups[groups.length - 1];
if (lastGroup && lastGroup.label === label) {
lastGroup.options.push(option);
} else {
groups.push({ label, options: [option] });
}
});
return groups;
}

/**
* Updates the component when there are changes in the state.
*/
Expand All @@ -110,6 +139,8 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
this.columns = this.preProcessColumns(columns);
}

this.hasGroupBy = this.columns?.some(col => col.groupBy === 'true') ?? false;

if (this.listType === 'associated') {
const optionsList = this.utils.getOptionList(this.configProps$, this.pConn$.getDataObject('')); // 1st arg empty string until typedef marked correctly
this.setOptions(optionsList);
Expand Down Expand Up @@ -156,27 +187,45 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
];
}

// Secondary text is out of scope for associated/local list options (FR-012)
// Secondary text and grouping are both out of scope for associated/local list options
if (this.listType !== 'associated') {
const secondaryColumns = this.getSecondaryColumnsFromMetadata();
if (secondaryColumns.length > 0) {
columns = [...(columns || []), ...secondaryColumns];
}

const groupByColumns = this.getGroupByColumnsFromMetadata();
if (groupByColumns.length > 0) {
columns = [...(columns || []), ...groupByColumns];
}
}

return { columns, datasource };
}

// Reads unresolved columnsFormatter metadata to derive secondary (contextual) display columns.
// Read from raw metadata, not resolved config, because config.value must stay an unresolved
// property reference (e.g. "@P .propName") for use as a column value (research.md §1).
// Reads unresolved groupsFields metadata to derive group-by column descriptor(s); not a
// display/search column, so grouping stays independent of primary/secondary text
getGroupByColumnsFromMetadata() {
const groupsFields = (this.pConn$.getRawMetadata()?.config as any)?.groupsFields;
if (!Array.isArray(groupsFields)) {
return [];
}
return this.mapMetadataColumns(groupsFields, { display: 'false', groupBy: 'true', useForSearch: false });
}

// Reads unresolved columnsFormatter metadata to derive secondary (contextual) display columns
getSecondaryColumnsFromMetadata() {
const columnsFormatter = (this.pConn$.getRawMetadata()?.config as any)?.columnsFormatter;
if (!Array.isArray(columnsFormatter)) {
return [];
}
return this.mapMetadataColumns(columnsFormatter, { display: 'true', secondary: 'true', useForSearch: true });
}

return columnsFormatter
// Shared by getSecondaryColumnsFromMetadata/getGroupByColumnsFromMetadata: value must stay an
// unresolved property reference (e.g. "@P .propName") for use as a raw-row lookup key
mapMetadataColumns(rawColumns: any[], columnFlags: object): any[] {
return rawColumns
.map(item => {
const property = item?.config?.value;
if (typeof property !== 'string' || !property) {
Expand All @@ -188,14 +237,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
} else if (property.startsWith('@USER ')) {
value = property.substring(6);
}
return {
display: 'true',
secondary: 'true',
useForSearch: true,
value,
type: item?.type,
label: item?.config?.label
};
return { value, type: item?.type, label: item?.config?.label, ...columnFlags };
})
.filter(Boolean);
}
Expand All @@ -204,6 +246,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
const optionsData: AutoCompleteOption[] = [];
const displayColumn = this.getDisplayFieldsMetaData(this.columns);
const secondaryColumns = this.columns?.filter(col => col.display === 'true' && col.secondary === 'true') || [];
const groupByColumn = this.columns?.find(col => col.groupBy === 'true');

results?.forEach(element => {
const obj: AutoCompleteOption = {
Expand All @@ -223,15 +266,48 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
}
}

if (groupByColumn) {
obj.group = this.resolveGroupValue(element[groupByColumn.value as string]);
}

optionsData.push(obj);
});

if (groupByColumn) {
this.sortByGroup(optionsData);
}

this.setOptions(optionsData);
}

// Null/undefined/whitespace-only source values normalize to '' — the shared blank group
resolveGroupValue(rawValue: any): string {
if (rawValue === null || rawValue === undefined) {
return '';
}
const stringValue = rawValue.toString();
return stringValue.trim() ? stringValue : '';
}

// Ascending, case-sensitive, stable sort so same-group options keep their original relative order
sortByGroup(options: AutoCompleteOption[]): void {
options.sort((a, b) => {
const groupA = a.group ?? '';
const groupB = b.group ?? '';
if (groupA < groupB) {
return -1;
}
if (groupA > groupB) {
return 1;
}
return 0;
});
}

// Rendering only — one read-only PConnect component per configured secondary field, in
// configured order, regardless of whether its value is empty (FieldValueList's own
// empty-value fallback renders the placeholder, e.g. "Label: ---"). Mirrors ScalarListComponent's
// createComponent/DISPLAY_ONLY pattern (research.md §4a).
// createComponent/DISPLAY_ONLY pattern.
buildSecondaryComponents(element: any, secondaryColumns): any[] {
return secondaryColumns.map(col =>
this.pConn$.createComponent(
Expand All @@ -251,7 +327,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit {
); // 2nd, 3rd, and 4th args empty string/object/null until typedef marked correctly as optional
}

// Search only — independent of buildSecondaryComponents; never derived from rendered output (research.md §4b).
// Search only — independent of buildSecondaryComponents; never derived from rendered output.
buildSecondarySearchText(element: any, secondaryColumns): string {
return secondaryColumns
.map(col => {
Expand Down
Loading
Loading