# Base page abstraction (internal)

Three-layer abstraction for entity list/detail pages with server-side paging,
search, filter, sort and export. Ported from `AuthenticationUILib`'s
`projects/verben-authentication-ui/src/lib/base/`, which in turn came from
White360FE's `src/app/shared/base-page/`.

**Internal to this library.** This folder is an ng-packagr entry point so
component folders can import it, but it is deliberately **not** re-exported from
`src/public-api.ts` — the consuming application ships its own copy of these
classes and re-exporting ours would clash. Do not add it to the root public API.

## Layout

```
core/      base-data-page.state.ts     item + query-param store
           base-data-page.service.ts   URL building + HTTP
           base-data-page.facade.ts    orchestration; the component's only dependency
handlers/  search-handler.service.ts   debounced search subject
           filter-handler.service.ts   FilterCondition[] / IDataFilter[] → SearchPropertyValue[]
           sort-handler.service.ts     SortCondition[] → URL sort + payload
models/    page-config.interface.ts    BasePageConfig<T>
           api-payload.ts              re-exports the payload enums/types
utils/     column-helpers.ts           accessorKey → property name / inferred type
```

## How this port differs from AuthenticationUILib

Read this before copying anything else across from the sibling libraries.

### 1. Sort travels in the URL, not the payload

Every endpoint in the workflow API ends `/{skip}/{limit}/{sortParam}/{sortOrder}`
— including the POST search (`SearchWorkflow/0/20/CreatedAt/Desc`). So the URL
seams take sort parameters, and `SortHandlerService.buildPrimarySort()` collapses
`SortCondition[]` to the single sort the URL can carry. `buildSortPayload()` still
emits the full set on the POST body so multi-column sort starts working the moment
the backend reads it.

Consequence: **a sort-only request stays a GET.** Only filters force the POST
search path, because the plain list GET already carries sort.

### 2. `post()` takes `baseUrl` fourth, not third

```ts
// this library
get<T>(url, baseUrl?, overrideToken?)
post<T>(url, body, overrideToken?, baseUrl?)   // <- baseUrl is FOURTH
delete<T>(url, baseUrl?, overrideToken?)
```

AuthenticationUILib's `post` takes `baseUrl` third. Passing it third here sends
the URL as an auth token, and since `HttpWebRequestService` resolves failures
into `ErrorResponse` rather than throwing, the request fails **silently**.

### 3. `delete` rejects; `get`/`post` do not

`delete` returns a raw observable that rejects on HTTP error, while `get`/`post`
resolve an `ErrorResponse`. `deleteItems()` normalizes this so the facade's error
contract stays uniform.

### 4. Identity keys on `Code` first

```ts
protected getItemKey(item: T): string { return item.Code || item.Id || item.id; }
```

`Code` leads because it is what every page in this library matches on and what
the backend assigns on persist. `Id` is frequently `''` on saved records, and the
lowercase `id` is a client-generated UUID that only exists on unsaved rows.

### 5. `Paged<T>`, not `PagedResult<T>`

Both exist in `lib/models`. The workflow API returns `Paged<T>`
(`Skip`/`PageSize`/`Total`/`Result`/`LastItem`).

### 6. The filter handler accepts two input shapes

`lib-data-filter` emits `FilterCondition[]` (columnId + operator), which is the
preferred shape. `verben-table-filter` emits `IDataFilter[]` (property name +
type, no operator) and is still supported so pages can migrate one at a time.

## One request cycle

1. Component calls a facade method (`loadData`, `applyFilters`, `onSearch`, …).
2. Facade reads current query params, filters, sorts and columns from **state**,
   and flips `setUpdating(true)` — wired to `UtilService.sendBI()`, which drives
   the host busy indicator.
3. Facade calls `service.getData(skip, limit, filters, sorts, searchTerm, columns)`.
   The service picks the request shape: filters → POST search; search term → GET
   WithParam; otherwise plain paged GET. Sort goes into the URL either way.
4. Service returns `Paged<T>` **or** `ErrorResponse` — it never throws.
5. Facade writes results into state or surfaces `utilService.showError()`, then
   `setUpdating(false)`.
6. Component renders from the facade's observables (`items$`, `isLoading$`,
   `visibleColumns$`, `filters$`, `sorts$`) and the `cardData` /
   `currentCardData` signals.

## Two bugs the base fixes structurally

- **Form not closing after search.** `loadData()` calls `clearCardSelection()`
  before replacing the list. `loadMore()` deliberately does not — it only
  appends, so an open record stays valid.
- **Form re-opening on its own.** The old `BaseDataViewComponent` inferred "this
  is the new record" from a falsy `Name` in a constructor `effect()`, so any
  refetch containing a blank-named record re-opened the form. Explicit
  `createItem()` / `openForm()` replace that inference.

## The DataState trap

`withDataState()` stamps `ObjectState.Changed` when the record has a `Code` and
`New` when it does not. **Do not trust the DataState on a loaded record** —
records returned from the backend report `'New'`, which would force an insert
(and thus a duplicate) on every edit.

## Wiring a page

Provide all four classes **per component** — never `providedIn: 'root'`, or page
instances share state:

```ts
providers: [FooFacade, FooState, FooService, SearchHandlerService]
```

## Seams to override

```ts
// Service — URL shape
protected listUrl(skip, limit, sortParam, sortOrder)
protected searchTermUrl(term, skip, limit, sortParam, sortOrder)
protected filterSearchUrl(skip, limit, sortParam, sortOrder)
protected saveUrl()
protected deleteUrl(itemIds)          // e.g. workflows: `DeleteWorkflows?data=${ids}`
protected defaultSortParam()          // 'CreatedAt'

// State — record identity
protected getItemKey(item: T): string
```

Override the URL, not `getData()` — request-shape selection stays in the base.
When an operation diverges semantically rather than just by URL, override the
standard method (`deleteItems()`) in the **concrete service**, so upper layers
keep calling the standard name.

For the migration procedure and the pitfall catalogue, see the shared
`verben-base-page` skill.
