From 6dadd84b9ebf0f738b17544d3cbba7ef570b8c1b Mon Sep 17 00:00:00 2001 From: samhere06 Date: Mon, 21 Sep 2026 21:46:27 +0530 Subject: [PATCH 1/3] feat(field): add grouping support for data page sourced options in autocomplete --- .../auto-complete.component.html | 42 ++-- .../auto-complete/auto-complete.component.ts | 102 ++++++++-- .../plan.md | 98 ++++++++++ .../spec.md | 121 ++++++++++++ .../tasks.md | 181 ++++++++++++++++++ 5 files changed, 519 insertions(+), 25 deletions(-) create mode 100644 specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/plan.md create mode 100644 specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/spec.md create mode 100644 specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/tasks.md diff --git a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.html b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.html index a0cfb6e3..a72d8b4d 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.html +++ b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.html @@ -16,18 +16,36 @@ (input)="fieldOnChange($event)" /> - - {{ opt.value }} -
- - - - -
-
+ + + + {{ opt.value }} +
+ + + + +
+
+
+
+ + + {{ opt.value }} +
+ + + + +
+
+
{{ helperText }} {{ getErrorMessage() }} diff --git a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts index 813911c8..ad6cfadf 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts @@ -21,6 +21,14 @@ interface AutoCompleteOption { // Present only when at least one secondary column resolves to a non-empty value (research.md §4a/§4b) secondaryComponents?: any[]; secondarySearchText?: string; + // Present only when a group-by field is configured for this (datapage-sourced) field (data-model.md) + group?: string; +} + +// Internal, render-time-only view-model — never part of the PConnect contract (data-model.md) +interface AutoCompleteGroup { + label: string; + options: AutoCompleteOption[]; } interface AutoCompleteProps extends PConnFieldProps { @@ -63,6 +71,9 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { columns: any[] = []; parameters: {}; filteredOptions: Observable; + // Grouped view of filteredOptions, only rendered when hasGroupBy is true (research.md §4) + groupedFilteredOptions$: Observable; + hasGroupBy = false; filterValue = ''; // Override ngOnInit method @@ -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[]) { @@ -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 (FR-007) 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 (research.md §7) + 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. */ @@ -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); @@ -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 (FR-012/FR-013) 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 (FR-001/FR-007) + 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 (research.md §1) + mapMetadataColumns(rawColumns: any[], columnFlags: object): any[] { + return rawColumns .map(item => { const property = item?.config?.value; if (typeof property !== 'string' || !property) { @@ -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); } @@ -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 = { @@ -223,11 +266,44 @@ 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 (FR-011) + 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 (FR-005/FR-012) + 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 diff --git a/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/plan.md b/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/plan.md new file mode 100644 index 00000000..32542e72 --- /dev/null +++ b/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/plan.md @@ -0,0 +1,98 @@ +# Implementation Plan: AutoComplete Option Grouping + +**Branch**: `ENHANCEMENT-14802-grouping-support-autocomplete-component` | **Date**: 2026-09-21 | **Spec**: [spec.md](./spec.md) + +**Input**: Feature specification from `/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/spec.md` + +**Note**: This template is filled in by the `/speckit-plan` command; its definition describes the execution workflow. + +## Summary + +Add optional grouping to `AutoCompleteComponent`: when a group-by field is configured (via `pConn$.getRawMetadata().config.groupsFields`), datapage-sourced options are annotated with a `group` value, sorted ascending by the exact case-sensitive value (stable within a group), and rendered using Angular Material's native `` autocomplete grouping — which already provides non-selectable headers and keyboard-navigation skipping for free. When no group-by field is configured, or the list is associated/local-list sourced, behavior and markup are byte-for-byte unchanged from today. Search/filtering continues to match only primary/secondary text; grouping is a pure post-filter, order-preserving regrouping step. + +## Technical Context + +**Language/Version**: TypeScript (Angular ^21.x, per repo-wide `angular.json`/`package.json`) + +**Primary Dependencies**: Angular Material `MatAutocompleteModule`, `MatOptionModule` (already imported by `AutoCompleteComponent` — provides both `MatOption` and `MatOptgroup`, no new imports needed), RxJS (`map`, `startWith`, already used in `filteredOptions` pipeline) + +**Storage**: N/A — data comes from the existing datapage fetch (`DatapageService.getDataPageData()` via `PCore.getDataApiUtils().getData()`); no new storage or persistence introduced + +**Testing**: Karma + Jasmine unit tests (`auto-complete.component.spec.ts`), consistent with existing secondary-text test suite in the same file; Playwright E2E only as manual/quickstart validation (no case-flow/assignment behavior changes, so new E2E specs are not required by Constitution VI, but existing `Picklist.spec.js`/`DataReference.spec.js` AutoComplete E2E coverage must keep passing) + +**Target Platform**: Browser (Angular component library consumed by the Angular SDK test app and downstream consumer apps) + +**Project Type**: Library component (single Angular library project — `packages/angular-sdk-components`) + +**Performance Goals**: No new performance target beyond existing AutoComplete behavior; grouping adds one linear pass (sort) over already-fetched datapage results and one linear pass (bucket) over the already-filtered option list per keystroke — both O(n) over the existing option count, no new network calls + +**Constraints**: Must not change any existing `AutoCompleteComponent` public behavior, props, or DOM output when no `groupsFields` metadata is configured (Constitution III); must reuse Angular Material's built-in grouping/keyboard-navigation/accessibility behavior rather than reimplementing it (per user-provided design-system direction); must not extend scope to associated/local-list options (FR-013) + +**Scale/Scope**: Single component file set (`auto-complete.component.ts`/`.html`/`.scss`/`.spec.ts`); no bridge, container, or cross-component changes + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Assessment | +|---|---| +| I. Platform Boundary | PASS — group-by value is read from `pConn$.getRawMetadata()`, and option data continues to come from `DatapageService`/`PCore.getDataApiUtils()`. No direct REST calls, no custom state store introduced. | +| II. Component Contracts | PASS — `AutoCompleteProps`/`AutoCompleteOption` remain typed interfaces (extended, not loosened to `any`); value propagation via `handleEvent`/`optionChanged` is untouched; read-only/display mode still delegates to `FieldValueList` via `component-mapper` (unchanged); children (secondary components) still render through `component-mapper`. | +| III. Backward Compatibility | PASS (by design) — `groupsFields` is a new, optional metadata key; when absent, the existing flat render path, `AutoCompleteOption` shape, and search behavior are byte-for-byte unchanged (see research.md §4 for why two render paths are used instead of one unified path). | +| IV. Infrastructure Protection | N/A — no bridge or container component is touched. | +| V. Security | PASS — no secrets, tokens, or URLs involved; `config.groupsFields` entries are property-reference strings read the same way existing `columnsFormatter` metadata is already read (via the shared `mapMetadataColumns()` helper). | +| VI. Testing Standards | PARTIAL (accepted, see Complexity Tracking) — unit tests cover column processing, option `group` derivation, sorting, and the search/grouping interaction; group-header non-selectability and keyboard-navigation-skip (FR-006/FR-010) rely on Angular Material's native `mat-optgroup` behavior plus manual quickstart validation rather than a new automated test, and no new embedded-mode E2E coverage is added — both deferred for this iteration, since tests are not the current priority. | +| VII. Spec and Plan Separation | PASS — `spec.md` contains no framework/file names; this `plan.md` carries all technical decisions (Angular Material APIs, file names, data shapes). | +| VIII. Minimal Change and Code Health | PASS (planned) — changes are scoped to `auto-complete.component.{ts,html,scss,spec.ts}` only; `getSecondaryColumnsFromMetadata()`/`getGroupByColumnsFromMetadata()` share a single `mapMetadataColumns()` helper rather than duplicating the `@P `/`@USER ` prefix-stripping logic. | +| IX. UX Consistency | PASS — uses Angular Material's own `mat-optgroup` grouping primitive exclusively; no custom header markup or bespoke ARIA wiring. | + +See Complexity Tracking below for the one accepted, documented relaxation of Principle VI. + +**Post-Phase-1 re-check**: After completing research.md and data-model.md, all rows above still hold — the finalized design (single scalar `config.groupsFields` array, additive `AutoCompleteOption.group` field, two mutually-exclusive template render paths, native `mat-optgroup`) introduces no new dependency, no bridge/container change, and no deviation from the constitution beyond the accepted testing-scope relaxation. Gate remains PASS with that one documented exception. + +## Project Structure + +### Documentation (this feature) + +```text +specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/ +├── plan.md # This file (/speckit-plan command output) +├── research.md # Phase 0 output (/speckit-plan command) +├── data-model.md # Phase 1 output (/speckit-plan command) +├── quickstart.md # Phase 1 output (/speckit-plan command) +├── contracts/ # Phase 1 output (/speckit-plan command) +│ └── auto-complete-grouping-contract.md +└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan) +``` + +### Source Code (repository root) + +```text +packages/angular-sdk-components/src/lib/_components/field/auto-complete/ +├── auto-complete.component.ts # Column processing (groupBy column descriptor), option +│ # `group` derivation, sorting, grouped-view-model construction +├── auto-complete.component.html # New `hasGroupBy`-gated render path, +│ # alongside the untouched existing flat render path +├── auto-complete.component.scss # Only if mat-optgroup default styling needs the same +│ # panel-width/wrapping treatment already applied to mat-option +└── auto-complete.component.spec.ts # New unit tests for grouping (column processing, sort, + # grouped view-model, search interaction, backward + # compatibility, associated/local-list exclusion) +``` + +**Structure Decision**: This is a single-project Angular component library (no frontend/backend split, no mobile/API split). All changes are contained within the existing `auto-complete` component directory — no new files, directories, modules, or shared services are introduced; the feature reuses the existing `field.base.ts`, `event-util.ts`, and `DatapageService` infrastructure as-is. + +## Complexity Tracking + +> Tests are not a priority for this iteration; both rows below were resolved by choosing the option with the least implementation impact (documenting acceptance here) rather than adding new automated tests or E2E coverage. + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| No embedded-mode E2E validation added for grouping (Constitution VI requires E2E validation in both portal and embedded modes for form-behavior changes) | Grouping is opt-in/additive and low-risk; existing portal-mode E2E (`Picklist.spec.js`, `DataReference.spec.js`) plus manual quickstart validation (quickstart.md §2) already cover the ungrouped-regression risk | Adding a new embedded-mode Playwright spec is more implementation work than this iteration prioritizes; deferred rather than built now | +| No automated test for group-header non-selectability / keyboard-nav skip (FR-006/FR-010/SC-005) | This behavior is provided natively by Angular Material's `mat-optgroup`/`ActiveDescendantKeyManager` (verified by source inspection, research.md §4) — the risk of regression is low and library-owned, not custom code | Writing a dedicated automated test (unit or E2E) for framework-guaranteed behavior is extra implementation effort not prioritized this iteration; manual quickstart validation (quickstart.md §2, scenario 2) is accepted as interim coverage | + +## Implementation Notes (post-implementation deltas from this plan) + +- **Metadata API corrected**: The actual platform metadata is `pConn$.getRawMetadata()?.config.groupsFields` — an array shaped like `columnsFormatter` (`{ type, config: { value, label } }[]`) — not the single scalar `config.groupBy` string originally assumed. `getGroupByColumnsFromMetadata()` maps it the same way `getSecondaryColumnsFromMetadata()` maps `columnsFormatter`, producing descriptors marked `{ groupBy: 'true', display: 'false', useForSearch: false }`. +- **Shared mapping helper**: Both metadata readers now delegate to a single `mapMetadataColumns(rawColumns, columnFlags)` helper to avoid duplicating the `@P `/`@USER ` prefix-stripping logic. +- **Sorting is case-sensitive, not case-insensitive**: `sortByGroup()` compares the exact `group` string (no `.toLowerCase()`), so `"Sales"` and `"sales"` are both distinct groups **and** ordered separately — this refines FR-005, which originally called for case-insensitive ordering while keeping case-sensitive equality. diff --git a/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/spec.md b/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/spec.md new file mode 100644 index 00000000..280fbea4 --- /dev/null +++ b/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/spec.md @@ -0,0 +1,121 @@ +# Feature Specification: AutoComplete Option Grouping + +**Feature Branch**: `ENHANCEMENT-14802-grouping-support-autocomplete-component` + +**Created**: 2026-09-21 + +**Status**: Draft + +**Input**: User description: "Add grouping support to the AutoComplete component. Users should be able to view options grouped by a configured field when group-by configuration is provided. Requirements: Grouping should be optional. Existing AutoComplete behavior should remain unchanged when no group-by field is configured. When a group-by field is configured, options should be grouped by the value of that field. A group header should be displayed for each distinct group value. Options should appear under their corresponding group header. Options should be sorted by group value to ensure all items belonging to the same group are displayed together. Group headers should not be selectable. Search and filtering should continue to work when grouping is enabled. Grouping should work with existing primary and secondary text functionality." + +## Clarifications + +### Session 2026-09-21 + +- Q: How should options whose group-by field value is null or empty (blank) be grouped and labeled? → A: Group them together under a single header with no visible label (blank header); this group's position follows normal ascending sort order alongside every other group value. +- Q: Should grouping apply to all AutoComplete list types, or only datapage-sourced options? → A: Datapage-sourced options only, matching the existing restriction already used for secondary text; associated/local list options remain out of scope for this feature. +- Q: Must the group-by field also be one of the option's displayed primary/secondary columns, or can it be a separate field used only for grouping? → A: Independent designation — a field may be marked as the group-by field on its own, whether or not it is also shown as primary or secondary text. +- Q: When a user's search term matches a group's value but not any individual option's own text, should that group's options be shown anyway? → A: No — the group value itself is not searchable text; a group's options are shown only when the term matches an option's own primary or secondary text, exactly as search already works today. +- Q: For the group formed by null/empty group values (no visible header text), should it still expose an accessible name to screen readers? → A: No — it remains fully blank for assistive technology as well as visually; no accessible name is announced for that header. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Scan a large option list organized by category (Priority: P1) + +As a user typing into an AutoComplete field whose options naturally fall into categories (e.g., region, status, type), when I open the option list, I want options visually organized under a header for each category so I can quickly find the option I'm looking for instead of scanning one long flat list. + +**Why this priority**: This is the core value of the feature — without grouped display, there is nothing else to search within or navigate. It delivers standalone value even before considering search interaction (Story 2). + +**Independent Test**: Configure an AutoComplete field with a group-by field set, open the dropdown, and verify options are displayed under headers matching each distinct group value, with all options sharing a group value appearing together. + +**Acceptance Scenarios**: + +1. **Given** an AutoComplete field configured with a group-by field, **When** the user opens the dropdown, **Then** a non-selectable header is displayed for each distinct group value found among the options. +2. **Given** options whose group values are not contiguous in the underlying data, **When** the option list is rendered, **Then** all options sharing the same group value are sorted and displayed together under one header. +3. **Given** an AutoComplete field with no group-by field configured, **When** the user opens the dropdown, **Then** the option list renders as a flat list with no headers, identical to current behavior. + +--- + +### User Story 2 - Search within a grouped option list (Priority: P2) + +As a user, I want to type a search term into a grouped AutoComplete field and still see matching options organized under their group headers, so grouping doesn't get in the way of quickly finding an option by typing. + +**Why this priority**: This extends Story 1 by ensuring the existing, essential search/filter capability keeps working once grouping is layered on top; it depends on grouped display already being in place. + +**Independent Test**: With grouping configured, type a search term that matches options in more than one group and verify the filtered results remain organized under their respective group headers, with non-matching groups and options omitted. + +**Acceptance Scenarios**: + +1. **Given** a grouped option list, **When** the user types a search term that matches options in two different groups, **Then** both group headers are shown, each with only its matching option(s) beneath it. +2. **Given** a grouped option list, **When** the user types a search term that matches no options in a particular group, **Then** that group's header is not shown. +3. **Given** a grouped option list where options include secondary text, **When** the user searches by a term found only in secondary text, **Then** the matching option still appears under its correct group header (existing primary/secondary search behavior is preserved). + +--- + +### User Story 3 - Existing AutoComplete configurations keep working (Priority: P3) + +As a developer/consumer who already uses AutoComplete fields without any group-by configuration, I want my existing forms to keep behaving exactly as before after this feature ships, so I don't need to change anything to avoid regressions. + +**Why this priority**: This is a compatibility safeguard rather than new user-facing value, so it is prioritized after the two stories that deliver new capability, but it must hold true before release. + +**Independent Test**: Load an existing AutoComplete field configuration that has no group-by field defined, exercise typing, filtering, keyboard navigation, and selection, and verify behavior and appearance match the pre-feature experience exactly. + +**Acceptance Scenarios**: + +1. **Given** an existing AutoComplete configuration with no group-by field, **When** the user opens, filters, navigates by keyboard, and selects an option, **Then** all interactions behave exactly as before this feature was added. +2. **Given** an existing AutoComplete configuration, **When** the feature is deployed, **Then** no code or configuration changes are required by the consumer for the field to keep working. + +--- + +### Edge Cases + +- What happens when a group-by field is configured but every option resolves to the same group value? (A single group header is shown, with every option displayed beneath it.) +- What happens when an option's group-by value is null or an empty string? (Those options are grouped together under one header with no visible label and no accessible name; that group's position in the sort order is determined the same way as any other group value — i.e., it is not forced to a fixed position.) +- What happens when two group values differ only in letter case (e.g., "Sales" vs. "sales")? (They are treated as distinct group values and produce two separate headers, since grouping compares the field's exact value.) +- What happens when a group-by field is configured for an associated/local list AutoComplete? (Grouping is out of scope for associated/local list options in this feature; those options continue to render as an ungrouped flat list.) +- What happens when the number of distinct group values is large (e.g., dozens)? (All group headers are rendered; no artificial cap or pagination is introduced by this feature.) +- What happens when a search term matches options across many groups simultaneously? (Every matching group is shown, each containing only its own matching option(s).) +- What happens to the relative order of options that share the same group value? (Their original relative order is preserved within the group; only the group-level ordering is changed by sorting.) + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST allow an AutoComplete field's configuration to optionally designate one field as the group-by field for its options, independently of which field(s), if any, are designated as primary or secondary display text — a field MAY serve as the group-by field whether or not it is also displayed. +- **FR-002**: When no group-by field is configured, System MUST render the option list exactly as it does today, with no group headers and no change in behavior, appearance, search, or selection. +- **FR-003**: When a group-by field is configured, System MUST determine each datapage-sourced option's group value from that field and MUST display one non-selectable group header for each distinct group value present among the current options. +- **FR-004**: System MUST display each option beneath the group header matching its group value. +- **FR-005**: System MUST sort options by group value (ascending, using the exact case-sensitive string value, consistent with FR-012) before rendering, so that every option belonging to the same group is displayed contiguously; the relative order of options within the same group MUST be preserved from their original order. +- **FR-006**: Group headers MUST NOT be selectable, focusable as a selection target, or returned as a chosen value; only individual options remain selectable. +- **FR-007**: Search/filtering MUST continue to operate on options' existing searchable text (primary text and, when present, secondary text) while grouping is enabled; a group's value itself MUST NOT be treated as separate searchable text, so a term matching only a group's value (and no option's own primary/secondary text) does not cause that group's options to display. Only groups that contain at least one matching option after filtering MUST be shown, and headers for groups with no matches MUST be omitted. +- **FR-008**: Grouping MUST be compatible with options that display secondary text, showing each option's primary and secondary text beneath the correct group header unchanged. +- **FR-009**: System MUST continue to support existing AutoComplete configurations that do not define a group-by field, without requiring any consumer-side changes. +- **FR-010**: System MUST preserve existing keyboard navigation and accessibility behavior of the option list (e.g., arrow-key movement between selectable options, selection announcement) when grouping is enabled, such that keyboard navigation moves only between options and skips over group headers. +- **FR-011**: System MUST group all options whose group-by value is null, undefined, or an empty/whitespace-only string together under a single header with no visible label and no accessible name (the header is blank for both sighted and assistive-technology users), and MUST sort that group's position using the same ascending rule applied to every other group value (no special-cased placement). +- **FR-012**: System MUST treat group values as distinct based on their exact string value (case-sensitive), so values differing only in letter case produce separate group headers. +- **FR-013**: System MUST NOT extend grouping support to associated/local list options (list type "associated") in this feature; those options continue to render as an ungrouped flat list regardless of any group-by configuration. + +### Key Entities + +- **Option**: A selectable entry in the AutoComplete list. Gains an optional group value, derived from the configured group-by field, used only to determine which group header the option is displayed under and how options are sorted; the group value does not change how an option is selected or what value is committed on selection. Only options sourced from a datapage/prompt-list configuration carry a group value in this feature; associated/local list options are unaffected. +- **Group**: A collection of options that share an identical group value, presented as a single non-selectable header followed by its member options. Groups are ordered by their exact, case-sensitive group value (ascending); the group formed by null/empty/whitespace-only values has no visible header label and no accessible name, but otherwise participates in ordering and filtering like any other group. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 100% of AutoComplete fields configured with a group-by field display one header per distinct group value, with every option appearing under its correct header. +- **SC-002**: 100% of options sharing the same group value are displayed contiguously (no interleaving with another group), verified across data sets where group values are not pre-sorted. +- **SC-003**: 100% of existing AutoComplete field configurations without a group-by field continue to display, filter, and select options with no observable change in behavior after the feature ships. +- **SC-004**: Users can locate and select an option in a grouped list by typing a search term, with the correct option remaining reachable and selectable in the same number of interactions (typing plus one selection) as in an ungrouped list, in 100% of test cases. +- **SC-005**: 0 instances across testing of a group header being selectable, focusable as a chosen value, or returned as a field value. + +## Assumptions + +- A group-by field is designated using the same option/column configuration mechanism already used to define an option's primary and secondary display columns (e.g., datapage/prompt-list column configuration), consistent with how the existing "primary" and "secondary" column designations already work, but as its own independent designation — the group-by field does not need to also be marked primary or secondary. +- Grouping support applies to datapage/prompt-list-sourced options only; associated/local list options are out of scope for this feature and continue to render as an ungrouped flat list, consistent with the same restriction already applied to secondary text. +- Group value comparison and sorting are both case-sensitive (e.g. "Sales" and "sales" are distinct groups and are ordered separately, by their exact string value); this mirrors typical grouping behavior where visually similar values are still kept as separate, explicit groups unless the underlying data normalizes them. +- Null, undefined, and empty/whitespace-only group values are treated as a single shared group with no visible header label, and this group is not pinned to a fixed first/last position — it sorts naturally alongside other group values. +- Options within a group retain their original relative order (stable sort by group value only); no secondary sort key such as primary text is introduced by this feature. +- Accessibility and keyboard navigation behavior already implemented for the option list (e.g., arrow-key movement, selection announcement) is expected to extend to skip non-selectable group headers, without requiring a redesigned interaction model. +- Search/filter matching behavior (case-insensitive substring match against primary and secondary text) is unchanged by this feature; grouping only affects how already-filtered results are organized for display. The group value itself is never used as a match target — only each option's own primary/secondary text is searched. diff --git a/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/tasks.md b/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/tasks.md new file mode 100644 index 00000000..acf2dffc --- /dev/null +++ b/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/tasks.md @@ -0,0 +1,181 @@ +# Tasks: AutoComplete Option Grouping + +**Input**: Design documents from `/specs/ENHANCEMENT-14802-grouping-support-autocomplete-component/` + +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/, quickstart.md + +**Tests**: Included — Constitution Principle VI ("Unit tests MUST be added or updated for every behavior change") makes unit-test tasks mandatory for this change, not optional. + +**Organization**: Tasks are grouped by user story (from spec.md) to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (US1, US2, US3) +- Include exact file paths in descriptions + +## Path Conventions + +Single Angular library project. All paths are under: +`packages/angular-sdk-components/src/lib/_components/field/auto-complete/` + +- `auto-complete.component.ts` — component logic +- `auto-complete.component.html` — template +- `auto-complete.component.scss` — styles +- `auto-complete.component.spec.ts` — unit tests + +--- + +## Phase 1: Setup + +**Purpose**: Establish a verified pre-change baseline; no new tooling/dependencies are required (Angular Material's `MatOptionModule`, which exports `MatOptgroup`, is already imported by the component — research.md §4). + +- [X] T001 Run the existing suite (`npx ng test angular-sdk-components --include='**/auto-complete/**/*.spec.ts' --watch=false`) against `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.spec.ts` and record the passing baseline, per quickstart.md §1 — note: the isolated `--include` filter hits a pre-existing, unrelated webpack circular-init error; baseline was instead confirmed via the full-suite run (one pre-existing, environment-related AutoComplete failure unrelated to this feature; 118 pre-existing failures repo-wide) + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Shared data-shape and metadata-reading scaffolding that every user story depends on. These changes are additive/no-ops when no `groupsFields` metadata is configured, so they do not themselves alter existing behavior. + +**⚠️ CRITICAL**: No user story task can begin until this phase is complete + +- [X] T002 Extend the `AutoCompleteOption` interface with an optional `group?: string` field in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts` (data-model.md → Entity: `AutoCompleteOption`) +- [X] T003 Add the internal `AutoCompleteGroup` view-model interface (`{ label: string; options: AutoCompleteOption[] }`) in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts` (data-model.md → Entity: `AutoCompleteGroup`) +- [X] T004 Add a `getGroupByColumnsFromMetadata()` method that reads `pConn$.getRawMetadata()?.config?.groupsFields` (array, corrected during implementation from an initially-assumed single `groupBy` string), mapping each entry the same way `getSecondaryColumnsFromMetadata()` does (strip a leading `@P `/`@USER ` prefix), returning column descriptors marked `{ groupBy: 'true', display: 'false', useForSearch: false }` in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts` (research.md §1, contracts/auto-complete-grouping-contract.md) +- [X] T005 In `generateColumnsAndDataSource()`, when `listType !== 'associated'` and `getGroupByColumnsFromMetadata()` resolves any descriptors, append them to the working `columns` array in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts` (research.md §2, data-model.md → Entity: group-by column descriptor, FR-013) +- [X] T006 Add a `hasGroupBy` boolean component property, derived from whether a group-by column descriptor was produced in T005, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts` (contracts/auto-complete-grouping-contract.md → rendering contract) + +**Checkpoint**: Foundation ready — group-by metadata can be resolved and carried through column processing, with zero effect on existing option shape or rendering until Phase 3 wires it up. + +--- + +## Phase 3: User Story 1 - Scan a large option list organized by category (Priority: P1) 🎯 MVP + +**Goal**: When a group-by field is configured, options are grouped under one non-selectable header per distinct group value, sorted so same-group options are contiguous. + +**Independent Test**: Configure an AutoComplete field with a group-by field set, open the dropdown, and verify options are displayed under headers matching each distinct group value, with all options sharing a group value appearing together. + +### Implementation for User Story 1 + +- [X] T007 [US1] In `fillOptions()`, when a group-by column descriptor exists, resolve each result row's raw group value, normalize `null`/`undefined`/whitespace-only to `''`, and set it as `option.group` in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts` (data-model.md → `AutoCompleteOption.group`, FR-003/FR-011) +- [X] T008 [US1] Sort the built `optionsData` array (before calling `setOptions()`) ascending by `group` value using a case-sensitive comparator (updated during implementation from an initially case-insensitive comparator) that returns `0` for equal values, relying on native stable sort to preserve each option's original relative order within a group, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts` (research.md §6, FR-005/FR-012) +- [X] T009 [US1] Add a `groupedFilteredOptions$: Observable` derived from `filteredOptions` via `map()`, bucketing the already-sorted, already-filtered array into contiguous groups whenever the `group` value changes (case-sensitive equality), using `''` as the label for the blank group, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts` (research.md §4, data-model.md → `AutoCompleteGroup`) +- [X] T010 [US1] Add a second, `hasGroupBy`-gated `` render block inside `mat-autocomplete` that iterates `groupedFilteredOptions$ | async` (nesting the existing per-option markup, including secondary text), leaving the current flat `*ngFor="let opt of filteredOptions | async"` block completely untouched as the non-grouped path, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.html` (research.md §4, FR-002/FR-004/FR-006/FR-008) +- [X] T011 [P] [US1] Adjust `.psdk-autocomplete-panel` SCSS rules, if visual verification (quickstart.md §2) shows overflow or misalignment, so `mat-optgroup` labels and nested options inherit the same width/wrapping treatment already applied to `mat-option` in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.scss` — evaluated: the existing `.psdk-autocomplete-panel .mat-mdc-option` selector already applies to options nested inside `mat-optgroup` (same DOM class, same panel container), so no SCSS change was needed +- [X] T012 [US1] Add unit tests for group-by field resolution (with/without `@P `/`@USER `/leading-dot prefixes) and column-descriptor derivation guarded by list type, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.spec.ts` (quickstart.md §1) +- [X] T013 [US1] Add unit tests for `group` population and blank-value normalization in `fillOptions()`, and for ascending/case-sensitive/stable sorting (including that same-group options keep their original relative order), in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.spec.ts` (quickstart.md §1, FR-005/FR-011/FR-012) +- [X] T014 [US1] Add unit tests for `groupedFilteredOptions$` construction: distinct group values produce separate buckets, same-group options stay contiguous, and the blank group's label is `''`, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.spec.ts` (quickstart.md §1, FR-003/FR-004) + +**Checkpoint**: User Story 1 is fully functional and independently testable — grouped display works end-to-end for a configured group-by field. + +--- + +## Phase 4: User Story 2 - Search within a grouped option list (Priority: P2) + +**Goal**: Typing a search term still filters correctly when grouping is enabled, with only matching groups/options shown. + +**Independent Test**: With grouping configured, type a search term that matches options in more than one group and verify the filtered results remain organized under their respective group headers, with non-matching groups and options omitted. + +### Implementation for User Story 2 + +- [X] T015 [US2] Confirm `_filter()` is unchanged — it must continue to match only `option.value`/`option.secondarySearchText` and never read `option.group` — adding an explicit code comment noting this invariant in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts` (research.md §7, FR-007) +- [X] T016 [US2] Add unit tests confirming that after filtering, `groupedFilteredOptions$` includes only groups containing at least one matching option and produces no bucket for groups with zero matches, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.spec.ts` (quickstart.md §1, FR-007) +- [X] T017 [US2] Add a unit test confirming an option matching only via `secondarySearchText` still appears grouped under its correct header after filtering, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.spec.ts` (FR-008) +- [X] T018 [US2] Add a unit test confirming a search term matching only a group's value (not any option's own primary/secondary text) does NOT surface that group's options, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.spec.ts` (FR-007, Clarifications session 2026-09-21) + +**Checkpoint**: User Stories 1 AND 2 both work independently — search and grouping compose correctly. + +--- + +## Phase 5: User Story 3 - Existing AutoComplete configurations keep working (Priority: P3) + +**Goal**: Zero observable change for any AutoComplete field that does not configure a group-by field, and no grouping applied to associated/local-list options. + +**Independent Test**: Load an existing AutoComplete field configuration that has no group-by field defined, exercise typing, filtering, keyboard navigation, and selection, and verify behavior and appearance match the pre-feature experience exactly. + +### Implementation for User Story 3 + +- [X] T019 [US3] Add a regression unit test confirming that when no `config.groupsFields` is present, no option carries a `group` property, `hasGroupBy` is `false`, and `fillOptions()`/`_filter()` output matches the pre-feature `{key, value}` shape exactly, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.spec.ts` (FR-002/FR-009) +- [X] T020 [US3] Add a regression unit test confirming `listType === 'associated'` never derives a `group` value or sets `hasGroupBy`, even if `config.groupsFields` is present in raw metadata, in `packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.spec.ts` (FR-013) +- [X] T021 [US3] Run the full `auto-complete.component.spec.ts` suite (existing + new tests from T012–T020) and confirm zero regressions against the Phase 1 (T001) baseline — verified via full-suite run: 149/150 executed, 31 passing (16 new, all passing) vs. the same 118 pre-existing/unrelated failures as baseline +- [ ] T022 [US3] Manually validate quickstart.md §2 scenario 1 (ungrouped baseline) using the existing `projects/angular-test-app/tests/e2e/DigV2/FormFields/Picklist.spec.js` and `projects/angular-test-app/tests/e2e/DigV2/ComplexFields/DataReference.spec.js` E2E coverage to confirm no visual/behavioral change for consumers without a group-by field configured — **not run**: requires a live Pega Infinity server + running test app, unavailable in this environment + +**Checkpoint**: All user stories are independently functional; backward compatibility is confirmed by regression tests and existing E2E coverage. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Final validation across all stories + +- [ ] T023 [P] Manually run quickstart.md §2 scenarios 2–5 (grouped display, search-in-groups, blank group, case-sensitivity) against a live datapage configured with a group-by field — **not run**: requires a live Pega Infinity server + running test app, unavailable in this environment +- [X] T024 Run `npm run lint` and fix any issues introduced by the new code (Constitution VI) — `npx eslint` scoped to the auto-complete component reported zero errors/warnings +- [X] T025 Confirm test coverage for `auto-complete.component.ts` has not regressed below the level on `main` (Constitution VI) — coverage increased (53.65% statements / 61.53% functions post-change vs. a near-zero baseline measured via `git stash`), no regression + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — can start immediately +- **Foundational (Phase 2)**: Depends on Setup (T001) completion — BLOCKS all user stories +- **User Story 1 (Phase 3)**: Depends on Foundational (Phase 2) completion +- **User Story 2 (Phase 4)**: Depends on Foundational (Phase 2) **and** User Story 1 (Phase 3), since grouped search verification exercises `groupedFilteredOptions$` built in Phase 3 +- **User Story 3 (Phase 5)**: Depends on Foundational (Phase 2); can run in parallel with Phase 3/4 since its tests only assert the *absence* of grouping behavior, but is listed last to match its P3 priority and because T021/T022 validate the fully-assembled feature +- **Polish (Phase 6)**: Depends on all desired user stories being complete + +### Within Each User Story + +- Column/data-shape changes before sort/group-construction changes +- Sort/group-construction changes before template wiring +- Implementation before its corresponding unit tests +- Story complete before moving to the next priority + +### Parallel Opportunities + +- T011 (SCSS) can run in parallel with T012–T014 (spec.ts tests), since they touch different files +- T023 (manual E2E) can run in parallel with T024/T025 (lint/coverage), since they are independent checks +- Tasks within the same file (`auto-complete.component.ts` or `auto-complete.component.spec.ts`) are **not** marked `[P]` — they must be done sequentially to avoid edit conflicts + +--- + +## Parallel Example: User Story 1 + +```bash +# T011 (different file: .scss) can run alongside the spec.ts test tasks: +Task: "Adjust .psdk-autocomplete-panel SCSS rules in auto-complete.component.scss" +Task: "Add unit tests for group-by field resolution in auto-complete.component.spec.ts" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup (baseline) +2. Complete Phase 2: Foundational (metadata reading, data shape, column descriptor) +3. Complete Phase 3: User Story 1 (grouped display) +4. **STOP and VALIDATE**: Run quickstart.md §1 unit tests and §2 scenario 2 manually +5. Deploy/demo if ready — grouped display alone already delivers the core value + +### Incremental Delivery + +1. Setup + Foundational → group-by metadata can be read, no visible change yet +2. Add User Story 1 → grouped display works → validate independently +3. Add User Story 2 → search stays correct with grouping → validate independently +4. Add User Story 3 → regression tests lock in backward compatibility → validate independently +5. Polish → full manual quickstart pass, lint, coverage check + +--- + +## Notes + +- `[P]` tasks = different files, no dependencies +- `[US1]`/`[US2]`/`[US3]` labels map tasks to spec.md's user stories for traceability +- Tests are included per Constitution Principle VI, not because the spec explicitly requested them +- Commit after each task or logical group +- Stop at any checkpoint to validate a story independently before proceeding +- Avoid: vague tasks, same-file conflicts, cross-story dependencies that break independence From 6655381a9bc0cd6a4a4467e8195ea3c81554b2d5 Mon Sep 17 00:00:00 2001 From: samhere06 Date: Mon, 21 Sep 2026 21:55:57 +0530 Subject: [PATCH 2/3] chore(changelog): update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49a28831..cc726f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) ### **Bug fixes** * **Fixed the issue where views are not rendering in Details Template.** From d34e77859a98f507f8b86be3835e7cc3b4b3ffcc Mon Sep 17 00:00:00 2001 From: samhere06 Date: Tue, 22 Sep 2026 20:30:57 +0530 Subject: [PATCH 3/3] refactor(auto-complete): clean up code comments to remove spec/doc references --- .../auto-complete/auto-complete.component.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts index ad6cfadf..ed05bfc2 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/field/auto-complete/auto-complete.component.ts @@ -18,14 +18,14 @@ 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 (data-model.md) + // 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 (data-model.md) +// Internal, render-time-only view-model — never part of the PConnect contract interface AutoCompleteGroup { label: string; options: AutoCompleteOption[]; @@ -71,7 +71,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { columns: any[] = []; parameters: {}; filteredOptions: Observable; - // Grouped view of filteredOptions, only rendered when hasGroupBy is true (research.md §4) + // Grouped view of filteredOptions, only rendered when hasGroupBy is true groupedFilteredOptions$: Observable; hasGroupBy = false; filterValue = ''; @@ -95,13 +95,13 @@ 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 (FR-007) + // 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 (research.md §7) + // Buckets the already-sorted option list into contiguous groups by exact group value buildGroups(options: AutoCompleteOption[]): AutoCompleteGroup[] { const groups: AutoCompleteGroup[] = []; options?.forEach(option => { @@ -187,7 +187,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { ]; } - // Secondary text and grouping are both out of scope for associated/local list options (FR-012/FR-013) + // 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) { @@ -204,7 +204,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { } // Reads unresolved groupsFields metadata to derive group-by column descriptor(s); not a - // display/search column, so grouping stays independent of primary/secondary text (FR-001/FR-007) + // 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)) { @@ -223,7 +223,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { } // Shared by getSecondaryColumnsFromMetadata/getGroupByColumnsFromMetadata: value must stay an - // unresolved property reference (e.g. "@P .propName") for use as a raw-row lookup key (research.md §1) + // 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 => { @@ -280,7 +280,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { this.setOptions(optionsData); } - // Null/undefined/whitespace-only source values normalize to '' — the shared blank group (FR-011) + // Null/undefined/whitespace-only source values normalize to '' — the shared blank group resolveGroupValue(rawValue: any): string { if (rawValue === null || rawValue === undefined) { return ''; @@ -289,7 +289,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { return stringValue.trim() ? stringValue : ''; } - // Ascending, case-sensitive, stable sort so same-group options keep their original relative order (FR-005/FR-012) + // 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 ?? ''; @@ -307,7 +307,7 @@ export class AutoCompleteComponent extends FieldBase implements OnInit { // 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( @@ -327,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 => {