# `PhoenixKitProjects.Web.ListUi`
[🔗](https://github.com/BeamLabEU/phoenix_kit_projects/blob/v0.21.2/lib/phoenix_kit_projects/web/list_ui.ex#L1)

Shared plumbing for the admin list pages (Projects / Tasks /
Templates): column-visibility persistence, search-param coercion, the
client-search haystack builder, and the Columns dropdown component.

Each list page keeps its own column roster and settings key; these
helpers only own the mechanics so the three pages can't drift apart.

# `coerce_search`

```elixir
@spec coerce_search(map()) :: String.t()
```

Coerces the search event payload to a binary. A forged `search[x]=y`
body arrives as a map — the query side would shrug it off, but
rendering a map back into the input's `value` would crash the LV.

# `columns_control`

The Columns dropdown: a focus-based daisyUI dropdown (closes when
focus leaves) with one checkbox per optional column, each pushing
`toggle_column` with its column key. `options` is the page's
`{key, translated_label}` roster; `visible` its current visible set.

## Attributes

* `options` (`:list`) (required)
* `visible` (`:list`) (required)

# `delete_view`

```elixir
@spec delete_view(String.t(), String.t()) :: [map()]
```

Deletes a named view. Returns the new list.

# `read_views`

```elixir
@spec read_views(String.t()) :: [map()]
```

SHARED saved views for a list page (the columns-persistence precedent:
site-wide Settings, team presets rather than per-user — per-user views
can layer on when member surfaces land). Stored as a JSON list under
one settings key; malformed/legacy payloads read as no views.

# `read_visible_columns`

```elixir
@spec read_visible_columns(String.t(), [String.t()], [String.t()]) :: [String.t()]
```

Reads a page's visible-column set from settings. `nil` (never saved)
falls back to `defaults`; an empty string is a deliberate "all
optional columns hidden". Unknown names are dropped and order is
normalized to the `optional` roster.

# `save_view`

```elixir
@spec save_view(String.t(), String.t(), map()) :: [map()]
```

Upserts a named view (same name replaces; the cap drops the oldest).
`state` must be a JSON-safe map. Returns the new list.

# `search_haystack`

```elixir
@spec search_haystack(
  struct(),
  [String.t()]
) :: String.t()
```

Lowercased match target for the TableLocalSearch hook: the record's
primary `fields` plus every language's translated values for the same
fields — the same coverage as the server-side ilike search, so the
instant client filter and the authoritative server result agree.

`fields` are the translation-map keys (strings); each must also name
a schema field (e.g. `["name", "description"]`, `["title", "description"]`).

# `toggle_visible_column`

```elixir
@spec toggle_visible_column(String.t(), [String.t()], [String.t()], String.t()) :: [
  String.t()
]
```

Toggles one column in the visible set and persists the result
(comma-joined, one settings row per page). Returns the new set.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
