Skip to content

Commit 67bf334

Browse files
committed
Refactor user preferences
1 parent 19a669e commit 67bf334

40 files changed

Lines changed: 2773 additions & 287 deletions

assets/js/backpex.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import * as Hooks from './hooks'
22

33
export { Hooks }
4+
export { BackpexPreferences } from './hooks/_preferences'

assets/js/hooks/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export { default as BackpexStickyActions } from './_sticky_actions'
66
export { default as BackpexThemeSelector } from './_theme_selector'
77
export { default as BackpexTooltip } from './_tooltip'
88
export { default as BackpexCurrencyInput } from './_currency_input'
9+
10+
export { BackpexPreferences } from './_preferences'

demo/lib/demo_web/components/layouts/root.html.heex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html lang="en" class="[scrollbar-gutter:stable] h-full" data-theme={assigns[:theme] || "light"}>
2+
<html lang="en" class="[scrollbar-gutter:stable] h-full" data-theme={assigns[:current_theme] || "light"}>
33
<head>
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1" />

demo/lib/demo_web/live/post_live.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ defmodule DemoWeb.PostLive do
77
create_changeset: &Demo.Post.create_changeset/3
88
],
99
fluid?: true,
10-
save_and_continue_button?: true
10+
save_and_continue_button?: true,
11+
persist: [:order, :filters, :columns]
1112

1213
import Ecto.Query, warn: false
1314

demo/lib/demo_web/plugs/theme_plug.ex

Lines changed: 0 additions & 19 deletions
This file was deleted.

demo/lib/demo_web/router.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ defmodule DemoWeb.Router do
1111
plug :put_root_layout, {DemoWeb.Layouts, :root}
1212
plug :protect_from_forgery
1313
plug :put_secure_browser_headers
14-
plug DemoWeb.ThemePlug
1514
end
1615

1716
scope "/", DemoWeb do
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
defmodule DemoWeb.Live.PreferencesPersistenceTest do
2+
@moduledoc """
3+
End-to-end coverage for the `persist: [:order, :filters, :columns]` option on
4+
`Backpex.LiveResource`. Mounts `DemoWeb.PostLive` (configured with all three
5+
persistence kinds) and asserts that sort, filter, and column-toggle
6+
interactions emit a `push_event` with the expected preference key and value
7+
shape.
8+
9+
The wire event name comes from `Backpex.Preferences.LiveView.event_name/0`
10+
and the keys come from `Backpex.Preferences.Keys.{order,filters,columns}/1`,
11+
so the test reflects the same contract the emitter uses.
12+
"""
13+
14+
use DemoWeb.ConnCase, async: false
15+
16+
import Demo.EctoFactory
17+
import Phoenix.LiveViewTest
18+
19+
alias Backpex.Preferences.Keys, as: PrefKeys
20+
alias Backpex.Preferences.LiveView, as: PrefLiveView
21+
22+
@resource_mod DemoWeb.PostLive
23+
24+
# assert_push_event expands to assert_receive, which pattern-matches the
25+
# arguments. Bind the event name and key to module-level constants or local
26+
# variables before the macro call so the pattern is literal-shaped.
27+
@event_name PrefLiveView.event_name()
28+
29+
describe "persist: [:order]" do
30+
test "sort change via column-header click emits push_event with order key", %{conn: conn} do
31+
insert(:post, title: "Alpha", published: true)
32+
insert(:post, title: "Beta", published: true)
33+
34+
{:ok, view, _html} = live(conn, ~p"/admin/posts?filters[published][]=published")
35+
36+
# Click the Title column header — triggers a sort and routes through
37+
# maybe_persist_order/2 which fires the push_event.
38+
view
39+
|> element("a", "Title")
40+
|> render_click()
41+
42+
expected_key = PrefKeys.order(@resource_mod)
43+
44+
assert_push_event(view, @event_name, %{
45+
key: ^expected_key,
46+
value: %{"by" => "title", "direction" => "asc"}
47+
})
48+
end
49+
end
50+
51+
describe "persist: [:filters]" do
52+
test "filter change emits push_event with filters key", %{conn: conn} do
53+
insert(:post, title: "Published", published: true)
54+
insert(:post, title: "Draft", published: false)
55+
56+
# Mount with the default published-only filter applied.
57+
{:ok, view, _html} = live(conn, ~p"/admin/posts?filters[published][]=published")
58+
59+
# Toggle the filter to include not_published too — routes through
60+
# handle_params → maybe_persist_filters/2 → push_event.
61+
view
62+
|> form("form[phx-change='change-filter']",
63+
filters: %{published: ["published", "not_published"]}
64+
)
65+
|> render_change()
66+
67+
expected_key = PrefKeys.filters(@resource_mod)
68+
69+
# The LiveResource emits several filter-persistence events over the mount
70+
# + change cycle. We care that at least one of them reflects the new
71+
# two-value set and carries the filters key.
72+
assert_push_event(view, @event_name, %{
73+
key: ^expected_key,
74+
value: %{"published" => ["published", "not_published"]}
75+
})
76+
end
77+
end
78+
79+
describe "persist: [:columns]" do
80+
test "column toggle emits push_event with columns key", %{conn: conn} do
81+
insert(:post, title: "Alpha", published: true)
82+
83+
{:ok, view, _html} = live(conn, ~p"/admin/posts?filters[published][]=published")
84+
85+
# Toggle the "title" column off. maybe_push_columns/3 emits the
86+
# push_event with the full active-fields map.
87+
view
88+
|> element("input[phx-click='toggle_column'][phx-value-field='title']")
89+
|> render_click()
90+
91+
expected_key = PrefKeys.columns(@resource_mod)
92+
93+
assert_push_event(view, @event_name, %{key: ^expected_key, value: value})
94+
95+
# title was just toggled, so it must now be false; other fields remain true.
96+
assert is_map(value)
97+
assert value["title"] == false
98+
end
99+
end
100+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Backpex Claude Code Plugin — Design
2+
3+
## Summary
4+
5+
Ship a Claude Code plugin marketplace directly in the `naymspace/backpex` repo. The first plugin (`backpex`) contains a `create-filter` skill that gives Claude deep knowledge of Backpex filter types so it can generate correct filter modules and wire them into LiveResources.
6+
7+
## Decisions
8+
9+
- **Marketplace location**: In the Backpex repo itself (`.claude-plugin/marketplace.json`)
10+
- **Plugin name**: `backpex` — skills namespaced as `/backpex:create-filter`
11+
- **Skill approach**: Context-aware assistant with comprehensive reference material (not a rigid generator)
12+
- **Invocation**: Model-invocable (Claude auto-triggers when filter work is detected) + user-invocable via `/backpex:create-filter`
13+
14+
## Directory Structure
15+
16+
```
17+
backpex/
18+
├── .claude-plugin/
19+
│ ├── marketplace.json # marketplace catalog
20+
│ └── plugin.json # plugin manifest
21+
├── skills/
22+
│ └── create-filter/
23+
│ └── SKILL.md # filter creation skill
24+
├── lib/ # existing library code
25+
├── demo/ # existing demo app
26+
└── ...
27+
```
28+
29+
## Marketplace Config
30+
31+
`marketplace.json` lists one plugin sourced from the repo root (`"./"`). Named `naymspace-backpex` so users add it with `/plugin marketplace add naymspace/backpex`.
32+
33+
## Plugin Manifest
34+
35+
Minimal `plugin.json` with name, description, version, and author. Uses default directory locations (`skills/`).
36+
37+
## Skill: create-filter
38+
39+
SKILL.md provides:
40+
- Instructions for Claude to analyze the user's request and pick the right filter type
41+
- Complete reference for all 4 built-in filter types (Boolean, Select, MultiSelect, Range)
42+
- Required callbacks and signatures for each type
43+
- Conventions: module naming, file location, LiveResource `filters/0` declaration
44+
- Custom filter guidance for when built-ins don't fit
45+
46+
## Future Skills
47+
48+
Additional skills can be added as directories under `skills/`:
49+
- `create-field` — generate custom field type modules
50+
- `create-resource-action` — generate resource action modules
51+
- `create-live-resource` — scaffold a full LiveResource

0 commit comments

Comments
 (0)