# Requirements: QSP-39315 — Overview Search Across All Visible Columns

## Status
✅ Refined

## Summary

Broaden the Overview page search field to match **all tree levels** (capability, team, component) instead of only capability. When a search term matches a child row, its parent rows remain visible to preserve the tree hierarchy, and non-matching siblings are pruned. The filter also applies to the grade badges and grade history chart. The URL param is renamed from `capability` to `search`, and the field label becomes a generic `Search`.

## Acceptance Criteria

| # | Criterion |
|---|-----------|
| AC-1 | Typing a **capability** name in the search field filters the table to show that capability and **all** its children. |
| AC-2 | Typing a **team** name filters the table to show matching teams plus their parent capability; non-matching sibling teams under the same capability are **hidden**. |
| AC-3 | Typing a **component** name filters the table to show matching components plus their parent team and capability; non-matching sibling components/teams are **hidden**. |
| AC-4 | Search matching is **case-insensitive substring** match. |
| AC-5 | The search field label is a generic `Search` (no longer "Search for capability"). |
| AC-6 | When the search field is cleared, the full unfiltered table is restored. |
| AC-7 | The **grade badges** (Average, Quality, Security) update to reflect only the filtered data. |
| AC-8 | The **grade history chart** updates to reflect only the filtered data. |
| AC-9 | When a child matches, the **tree is auto-expanded** to reveal the matching rows. |
| AC-10 | The URL search param is renamed from `?capability=` to `?search=`. |

## Key Decisions

| # | Question | Decision |
|---|----------|----------|
| D-1 | Should grade badges & grade history update when filtering? | **Yes** — all filters are adjusted, not just the table. |
| D-2 | What does "search across columns" mean given the single-column tree? | Search matches `identifier.name` at **all three tree levels** (capability, team, component). |
| D-3 | Auto-expand or manual expand when a child matches? | **Auto-expand** the tree to show matching rows. |
| D-4 | What happens to non-matching siblings? | Non-matching **teams** under a matching capability are **hidden**. Non-matching **components** under a matching team are **hidden**. The parent chain is always kept visible. |
| D-5 | FilterBar.Item label? | Generic `'Search'`. |
| D-6 | URL param name? | Rename from `capability` to `search`. |
| D-7 | Changes to OverviewAggregationView? | **No** — only the main Overview page is in scope. |

## Data Model

No model changes. The existing `DatatableEntriesModel` tree (capability → team → asset) and `AssetWithRelation` flat list are sufficient.

## UX / UI Specification

### FilterBar
- `FilterBar.Item` `name` prop: `'search'` (was `'capability'`)
- `FilterBar.Item` `label` prop: `'Search'` (was `'Search for capability'`)
- `TextInput` `placeholder` prop: `'Search'` (unchanged)

### Tree behaviour on search
- When a **parent** matches, all children remain visible (unchanged from today).
- When a **child** matches but the parent does not, only the matching child (and its ancestors) are shown — non-matching siblings are pruned.
- Matching subtrees are **auto-expanded** so the user sees the results immediately.

### Grade badges & chart
- `AverageGradeContainer` and `GradeHistoryContainer` both consume the filtered `AssetWithRelation[]` — this is already the case, but `filteredAssets` must be updated to filter by all tree levels, not just capability.

## API Contracts

No API changes.

## DQL Queries

No DQL changes.

## Implementation Plan

### 1. Update `overviewUtils.ts` — new `applyOverviewFilters` logic
- Replace the current top-level-only filter with a **recursive** tree filter:
  1. If an entry's `identifier.name` matches → keep it with **all** children (parent match).
  2. Else recursively filter `subEntry`. If any children survive → keep the entry (ancestor of a match) with only the surviving children.
  3. Else → prune the entry.

### 2. Update `overviewUtils.ts` — new `filterAssetsBySearch` helper
- Replace the current `filteredAssets` logic in `Overview.tsx` that only checks `asset.capability.name`.
- New function checks `asset.capability.name`, `asset.team.name`, and `asset.asset.name` (case-insensitive substring).
- This ensures grade badges and grade history reflect the search across all levels.

### 3. Update `Overview.tsx`
- Rename state variable from `capabilityFilter` to `searchFilter`.
- Rename URL param from `capability` to `search`.
- Update `FilterBar.Item` `name` to `'search'` and `label` to `'Search'`.
- Use new `filterAssetsBySearch` for `filteredAssets`.
- Pass only the `searchFilter` to the updated `applyOverviewFilters`.
- Add `defaultExpandedRows` computation: build expanded row indices from `tableData` where a search is active and children matched.

### 4. Unit tests — `overviewUtils.spec.ts`
Update existing tests and add new cases:
- Search matching **only** a capability → returns capability with all children.
- Search matching **only** a team → returns parent capability + matching team + all its children; non-matching teams hidden.
- Search matching **only** a component → returns capability + team + matching component; non-matching components hidden.
- Search matching **nothing** → returns empty array.
- Search `undefined` → returns all entries unchanged.
- Case-insensitive matching.
- Add tests for `filterAssetsBySearch`.

### 5. Component tests
- Add/update Playwright component tests to verify the tree auto-expands and non-matching siblings are pruned.

## Unit Test Plan

### `overviewUtils.spec.ts` — `applyOverviewFilters`

| Test case | Input | Expected |
|-----------|-------|----------|
| No filter (`undefined`) | Full tree | All entries returned |
| Capability match | Search = capability name | That capability + all children |
| Team match only | Search = team name, not in capability name | Capability kept, only matching team + its children |
| Component match only | Search = component name, not in capability/team name | Capability + team kept, only matching component |
| No match | Search = gibberish | Empty array |
| Case insensitive | Search = uppercase variant of team name | Matching entries returned |
| Partial match | Search = substring of component name | Matching entries returned |

### `overviewUtils.spec.ts` — `filterAssetsBySearch`

| Test case | Input | Expected |
|-----------|-------|----------|
| No filter (`undefined`) | Full list | All relations returned |
| Matches capability | Search = capability name | Relations with that capability |
| Matches team | Search = team name | Relations with that team |
| Matches component | Search = asset name | Relations with that asset |
| Case insensitive | Uppercase search | Matching relations |
| No match | Gibberish | Empty array |

## Edge Cases & Error Handling

| Edge case | Handling |
|-----------|---------|
| Empty search string (`""`) | Treat as no filter — show all |
| Search string with leading/trailing whitespace | Trim before matching |
| All rows filtered out | Table shows empty state (existing behaviour) |
| Data still loading | Filter is applied after load completes (existing behaviour) |

## Out of Scope

- Full-text search across non-visible data (grades, signal details).
- Persisting the search term in the URL (covered by QSP-39408).
- Changes to the `OverviewAggregationView` page.

## Refinement Log

### Step 1 — Initial code investigation

**Code investigation findings:**
- `src/app/pages/Overview.tsx`: The Overview page uses a `capabilityFilter` state variable initialized from `searchParams.get('capability')`. The filter input is a `TextInput` inside a `FilterBar.Item` with `name='capability'` and `label='Search for capability'`.
- **Two filtering layers exist:**
  1. **`filteredAssets` (line 41–48):** Filters raw `AssetWithRelation[]` by `asset.capability.name` only — used for grade badges and grade history computation.
  2. **`applyOverviewFilters` in `overviewUtils.ts` (line 119–123):** Filters the tree `DatatableEntriesModel[]` by `entry.identifier.name` at the top (capability) level only.
- `src/app/utils/overviewUtils.ts`: `applyOverviewFilters(entries, capability)` only does a top-level capability name filter — no recursion into teams or assets.
- `src/app/models/datatable-entries.model.ts`: The tree structure is `DatatableEntriesModel { identifier: IdentifierNode, subEntry: DatatableEntriesModel[], scores }`. Levels are capability → team → asset.
- `src/app/components/constants/overview-constants.ts`: Only two columns are defined: "Capability / Owner" (showing `identifier.name`) and "Average grade".
- The search param currently uses `?capability=...` in the URL.
- The context file says URL persistence is **out of scope** (covered by QSP-39408).

### Step 2 — Clarifying questions resolved

| # | Question | Answer |
|---|----------|--------|
| 1 | Should grade badges & grade history update? | Yes — adjust all filters. |
| 2 | Search across tree levels? | Yes — match any layer, parents remain visible. |
| 3 | Auto-expand or manual? | Auto-expanded. |
| 4 | Partial tree pruning? | Non-matching teams hidden, parent capability visible. |
| 5 | FilterBar.Item label? | Generic `'Search'`. |
| 6 | URL param name? | Rename to `search`. |
| 7 | OverviewAggregationView changes? | No — only the main Overview page. |
