layered-resource-rails 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/layered-resource-rails/SKILL.md +12 -1
- data/CHANGELOG.md +14 -0
- data/README.md +31 -2
- data/app/controllers/layered/resource/controller.rb +6 -0
- data/app/views/layered/resource/resources/_row_actions.html.erb +19 -0
- data/app/views/layered/resource/resources/index.html.erb +13 -16
- data/lib/generators/layered/resource/views/views_generator.rb +2 -1
- data/lib/layered/resource/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4560cdb00f90fd031ddb92e6a472e8b72e375c929864a36f013206f59abfa21a
|
|
4
|
+
data.tar.gz: b9b93f938d7448c751e9a78b66ff9f23ea174f72e6feec1c67752309bf3f8acb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e49ac83a4e70bcb16a51eee140652eb9331c856bd5664bad52955474cbb523ee2d78faf7146ef80aa66b78f2ae6e312ac9d733a099deb34dea4b0b2ccb6be76
|
|
7
|
+
data.tar.gz: 34c074eb474d6aa7aac0c0e9bc24f2feb334023ec6787fc11ea6d7a2c357147a72c258348244051fb60aeff0965b7be1cec0b4693994e52371b0ed96d06334f8
|
|
@@ -309,7 +309,7 @@ When enabled:
|
|
|
309
309
|
|
|
310
310
|
- `scope(controller)` defaults to the controller's `policy_scope(model)` helper, so apps that override `pundit_user` (e.g. `current_account`) get the same identity used by `authorize`.
|
|
311
311
|
- The controller calls `authorize(@record)` after loading a member record (show/edit/update/destroy and any custom member action).
|
|
312
|
-
- The `New`/`Edit`/`Delete` actions in the default views (inline buttons on show, a per-row actions popover menu on index, in a column pinned to the right edge via `l_ui_table`'s `floating_actions:`) hide for users whose policy denies the action - the views call a `resource_can?(action, record = nil)` helper that ANDs the route-exposure flag with `policy(record).<action>?`.
|
|
312
|
+
- The `New`/`Edit`/`Delete` actions in the default views (inline buttons on show, a per-row actions popover menu on index, in a column pinned to the right edge via `l_ui_table`'s `floating_actions:`) hide for users whose policy denies the action - the views call a `resource_can?(action, record = nil)` helper that ANDs the route-exposure flag with `policy(record).<action>?`. A resource's own `_row_actions` partial replaces the index menu's checks, so it must call `resource_can?` itself (see Row actions).
|
|
313
313
|
|
|
314
314
|
`owned_by` composes with `use_pundit`: Pundit's `Policy::Scope#resolve` wins for the read filter; `owned_by` still drives owner assignment on create. Stack both when needed.
|
|
315
315
|
|
|
@@ -344,6 +344,17 @@ end
|
|
|
344
344
|
|
|
345
345
|
Drop a partial at `app/views/layered/<resource>/_introduction.html.erb` to render content above the search area on that resource's index page. The partial is rendered when present and skipped otherwise — no DSL, no configuration. It uses the same per-resource view path as ejected templates and column overrides.
|
|
346
346
|
|
|
347
|
+
## Row actions
|
|
348
|
+
|
|
349
|
+
Each index row's popover menu holds whatever the `_row_actions` partial renders for that record. The default renders `Edit`/`Delete`, gated by `resource_can?(action, record)`. For a menu that varies per record or per user (a role-dependent "Members"/"Settings"/"Leave", say), drop `app/views/layered/<resource>/_row_actions.html.erb` - **don't eject the whole index for this**; the override keeps search, filters, sort and pagination.
|
|
350
|
+
|
|
351
|
+
- Locals: `record`. Full view context: `current_user`, host path helpers, `layered_member_path`, `resource_can?`.
|
|
352
|
+
- Render items only (the index wraps them in the popover): class `l-ui-popover__menu-item` (`--danger` for destructive), `<hr class="l-ui-popover__menu-divider">` between groups, `data: { turbo_frame: "_top" }` on each. Use `button_to ..., method: :delete` for non-GET actions.
|
|
353
|
+
- Empty output for a row means no menu on that row.
|
|
354
|
+
- The override replaces the default's gating - wrap any kept Edit/Delete in `resource_can?` so `only:` and Pundit still apply.
|
|
355
|
+
- The index renders every row's menu before the table and adds the actions column only when at least one row has one - so an index-only route gets a column only if its partial renders items, and an ejected copy of the default adds no empty column.
|
|
356
|
+
- The partial is keyed by the route's resource name, so every route for that name (e.g. `/team_posts` and `/browse/team_posts`) shares it.
|
|
357
|
+
|
|
347
358
|
## Ejection
|
|
348
359
|
|
|
349
360
|
Take over presentation or controller logic without giving up the rest:
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. This project follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-11-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- A resource can supply its own index row actions with a `_row_actions` partial at `app/views/layered/<resource>/_row_actions.html.erb`, rendered once per row with `record` as a local. It replaces the default `Edit`/`Delete` menu for that resource, keeping the index's search, filters, sorting and pagination, so menus that vary per record or per user no longer mean ejecting the whole index. A row whose partial renders nothing gets no menu, and the actions column appears only when at least one row on the page has one, so an index-only route can have row actions too. The default menu is now this partial too, and `rails g layered:resource:views` copies it
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The index leaves out the actions column when no row on the page has a menu, such as a page where the policy denies every row. It used to show an empty column whenever edit or destroy was routed
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- A partial the default views render by bare name, such as `_search` or `_filters`, is now picked up from `app/views/layered/<resource>/` when overridden there. The lookup context could be built before the resource was known, so its prefixes left out the per-resource directory and the gem's own partial always won
|
|
18
|
+
|
|
5
19
|
## [0.5.0] - 2026-11-11
|
|
6
20
|
|
|
7
21
|
### Fixed
|
data/README.md
CHANGED
|
@@ -133,7 +133,7 @@ That's it. You now have a full CRUD interface with search and pagination for `Po
|
|
|
133
133
|
|
|
134
134
|
The index table's primary column (`primary: true`, or the first column) links to each record's edit page - or, for read-only resources without an edit action, to its show page.
|
|
135
135
|
|
|
136
|
-
Row actions (`Edit`/`Delete
|
|
136
|
+
Row actions (`Edit`/`Delete`, or [your own](#row-actions)) live in a popover menu in the table's last column. That column is pinned to the right-hand edge of the table's scroll container, so the actions menu stays reachable when a wide table scrolls horizontally.
|
|
137
137
|
|
|
138
138
|
Each action also sets `@page_title` for use by the layout's `<title>` tag - `"Posts"` on index, `"New Post"` on new, the record's primary column value on show, and `"Edit <record label>"` on edit. Override after `super` in a custom action if you need something different.
|
|
139
139
|
|
|
@@ -291,7 +291,8 @@ When enabled:
|
|
|
291
291
|
`Edit`/`Delete` items in the index row's actions popover, and the
|
|
292
292
|
`Edit`/`Delete` buttons on the show page check
|
|
293
293
|
`policy(record).new?/update?/destroy?` so users only see actions they
|
|
294
|
-
can perform.
|
|
294
|
+
can perform. A resource's own [`_row_actions`](#row-actions) partial
|
|
295
|
+
replaces the row menu's checks, so it calls `resource_can?` itself.
|
|
295
296
|
|
|
296
297
|
> **`verify_authorized` / `verify_policy_scoped`**: if your
|
|
297
298
|
> `ApplicationController` runs Pundit's `after_action :verify_authorized`
|
|
@@ -684,6 +685,34 @@ To render an introduction above the search area on a resource's index page, drop
|
|
|
684
685
|
|
|
685
686
|
The partial sits inside the resource's view directory, so it follows the same per-resource override path as ejected views and column partials.
|
|
686
687
|
|
|
688
|
+
## Row actions
|
|
689
|
+
|
|
690
|
+
Each index row's actions menu holds whatever the `_row_actions` partial renders for that record. The gem's default renders `Edit` and `Delete`, each shown only when `resource_can?` allows it for the record. To give a resource its own menu, drop a partial at `app/views/layered/<resource>/_row_actions.html.erb`. It replaces the default for that resource only, and the index keeps its search, filters, sorting and pagination.
|
|
691
|
+
|
|
692
|
+
The partial renders once per row with `record` as a local, in the normal view context, so `current_user`, your app's path helpers, `layered_member_path` and `resource_can?` are all available. Render the menu items only; the index wraps them in the popover. A row whose partial renders nothing gets no menu.
|
|
693
|
+
|
|
694
|
+
```erb
|
|
695
|
+
<%# app/views/layered/accounts/_row_actions.html.erb %>
|
|
696
|
+
<% role = record.role_for(current_user) %>
|
|
697
|
+
<% if role.in?(%w[owner admin]) %>
|
|
698
|
+
<%= link_to "Members", account_members_path(record),
|
|
699
|
+
class: "l-ui-popover__menu-item", data: { turbo_frame: "_top" } %>
|
|
700
|
+
<% end %>
|
|
701
|
+
<% if role == "owner" %>
|
|
702
|
+
<%= link_to "Settings", edit_account_path(record),
|
|
703
|
+
class: "l-ui-popover__menu-item", data: { turbo_frame: "_top" } %>
|
|
704
|
+
<% end %>
|
|
705
|
+
<hr class="l-ui-popover__menu-divider">
|
|
706
|
+
<%= button_to "Leave", leave_account_path(record), method: :delete,
|
|
707
|
+
class: "l-ui-popover__menu-item l-ui-popover__menu-item--danger",
|
|
708
|
+
data: { turbo_confirm: "Leave this account?", turbo_frame: "_top" } %>
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
- **Item classes.** Give links and buttons `l-ui-popover__menu-item`, add `l-ui-popover__menu-item--danger` for destructive ones, and separate groups with `<hr class="l-ui-popover__menu-divider">`. Add `data: { turbo_frame: "_top" }` so the action leaves the index's Turbo frame.
|
|
712
|
+
- **Gating is yours.** Your partial replaces the default's `resource_can?` checks. Call `resource_can?(:edit, record)` / `resource_can?(:destroy, record)` around any built-in actions you keep, so they still respect the route's `only:` and your Pundit policy. Actions on your own routes are authorised by their own controllers, as usual.
|
|
713
|
+
- **When the column shows.** The actions column appears only when at least one row on the page has a menu. So an index-only route using the default gets none, while a resource whose own partial renders items gets the column on any route.
|
|
714
|
+
- **Ejected views.** `rails g layered:resource:views` copies the default `_row_actions.html.erb` along with the other templates; edit it in place.
|
|
715
|
+
|
|
687
716
|
## Record pickers
|
|
688
717
|
|
|
689
718
|
A field naming a `belongs_to`'s **foreign key** is a record picker, so it renders as a single-select [combobox](https://github.com/layered-ai-public/layered-ui-rails) - a type-ahead input whose selection becomes a removable token - over the associated records, rather than as the number the column happens to hold:
|
|
@@ -189,6 +189,12 @@ module Layered
|
|
|
189
189
|
@columns = @resource.columns
|
|
190
190
|
@layered_route_key = route_key
|
|
191
191
|
@layered_resource_name = @_route_entry[:resource_name].presence || route_key
|
|
192
|
+
# The lookup context may already exist (an earlier before_action can
|
|
193
|
+
# build it), holding prefixes from before the resource name was
|
|
194
|
+
# known. Partials rendered by bare name - `render "row_actions"` -
|
|
195
|
+
# search those prefixes, so refresh them or a per-resource override
|
|
196
|
+
# in app/views/layered/<name>/ is never found.
|
|
197
|
+
lookup_context.prefixes = _prefixes
|
|
192
198
|
@fields = @resource.resolved_fields
|
|
193
199
|
@crud_enabled = @fields.any?
|
|
194
200
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%# The items in an index row's actions menu, rendered once per record with
|
|
2
|
+
`record` as a local. The index wraps whatever this outputs in the row's
|
|
3
|
+
popover, and leaves the row without a menu when it outputs nothing.
|
|
4
|
+
Override per resource at app/views/layered/<resource>/_row_actions.html.erb. %>
|
|
5
|
+
<% can_edit = resource_can?(:edit, record) %>
|
|
6
|
+
<% if can_edit %>
|
|
7
|
+
<%= link_to "Edit", layered_member_path(record, action: :edit),
|
|
8
|
+
class: "l-ui-popover__menu-item",
|
|
9
|
+
data: { turbo_frame: "_top" } %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% if resource_can?(:destroy, record) %>
|
|
12
|
+
<% if can_edit %>
|
|
13
|
+
<hr class="l-ui-popover__menu-divider">
|
|
14
|
+
<% end %>
|
|
15
|
+
<%= button_to "Delete", layered_member_path(record),
|
|
16
|
+
method: :delete,
|
|
17
|
+
class: "l-ui-popover__menu-item l-ui-popover__menu-item--danger",
|
|
18
|
+
data: { turbo_confirm: "Are you sure?", turbo_frame: "_top" } %>
|
|
19
|
+
<% end %>
|
|
@@ -39,22 +39,19 @@
|
|
|
39
39
|
<%= render "filters", turbo_frame_name: turbo_frame_name %>
|
|
40
40
|
<% end %>
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
<%# Each row's menu holds whatever the _row_actions partial renders for it:
|
|
43
|
+
Edit and Delete by default, or a resource's own override. Rendering
|
|
44
|
+
them up front means the actions column only appears when at least one
|
|
45
|
+
row on the page has a menu - not on an index-only route, nor on a page
|
|
46
|
+
where the policy denies every row. %>
|
|
47
|
+
<% row_menus = @records.each_with_object({}.compare_by_identity) do |record, menus|
|
|
48
|
+
items = render("row_actions", record: record)
|
|
49
|
+
menus[record] = items if items.present?
|
|
50
|
+
end %>
|
|
51
|
+
<% actions_proc = if row_menus.any?
|
|
43
52
|
->(record) do
|
|
44
|
-
items = []
|
|
45
|
-
|
|
46
|
-
items << link_to("Edit", layered_member_path(record, action: :edit),
|
|
47
|
-
class: "l-ui-popover__menu-item",
|
|
48
|
-
data: { turbo_frame: "_top" })
|
|
49
|
-
end
|
|
50
|
-
if resource_can?(:destroy, record)
|
|
51
|
-
items << tag.hr(class: "l-ui-popover__menu-divider") if items.any?
|
|
52
|
-
items << button_to("Delete", layered_member_path(record),
|
|
53
|
-
method: :delete,
|
|
54
|
-
class: "l-ui-popover__menu-item l-ui-popover__menu-item--danger",
|
|
55
|
-
data: { turbo_confirm: "Are you sure?", turbo_frame: "_top" })
|
|
56
|
-
end
|
|
57
|
-
next if items.empty?
|
|
53
|
+
items = row_menus[record]
|
|
54
|
+
next unless items
|
|
58
55
|
|
|
59
56
|
l_ui_popover(align: :end) do |p|
|
|
60
57
|
p.trigger(class: "l-ui-button l-ui-button--outline l-ui-button--icon l-ui-button--small",
|
|
@@ -62,7 +59,7 @@
|
|
|
62
59
|
image_tag("layered_ui/icon_more.svg", alt: "",
|
|
63
60
|
class: "l-ui-icon l-ui-icon--sm", aria: { hidden: true })
|
|
64
61
|
end
|
|
65
|
-
tag.div(
|
|
62
|
+
tag.div(items, class: "l-ui-popover__menu")
|
|
66
63
|
end
|
|
67
64
|
end
|
|
68
65
|
end %>
|
|
@@ -16,7 +16,8 @@ module Layered
|
|
|
16
16
|
desc "Copy the gem's view templates into app/views/layered/<name>/ for full customisation."
|
|
17
17
|
|
|
18
18
|
VIEWS = %w[index.html.erb show.html.erb new.html.erb edit.html.erb
|
|
19
|
-
_search.html.erb _filters.html.erb _filter_control.html.erb
|
|
19
|
+
_search.html.erb _filters.html.erb _filter_control.html.erb
|
|
20
|
+
_row_actions.html.erb].freeze
|
|
20
21
|
|
|
21
22
|
def copy_views
|
|
22
23
|
VIEWS.each do |view|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: layered-resource-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- layered.ai
|
|
@@ -297,6 +297,7 @@ files:
|
|
|
297
297
|
- app/views/layered/resource/columns/_text.html.erb
|
|
298
298
|
- app/views/layered/resource/resources/_filter_control.html.erb
|
|
299
299
|
- app/views/layered/resource/resources/_filters.html.erb
|
|
300
|
+
- app/views/layered/resource/resources/_row_actions.html.erb
|
|
300
301
|
- app/views/layered/resource/resources/_search.html.erb
|
|
301
302
|
- app/views/layered/resource/resources/edit.html.erb
|
|
302
303
|
- app/views/layered/resource/resources/index.html.erb
|