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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd456a360f63f94c4c38e833251b2d4b22a20c7df76b2b78612d9baf55135e9c
4
- data.tar.gz: 783f255e36a42959f5565862cf2354d5a57cb1ec5e2d98fbf281477613446c43
3
+ metadata.gz: 4560cdb00f90fd031ddb92e6a472e8b72e375c929864a36f013206f59abfa21a
4
+ data.tar.gz: b9b93f938d7448c751e9a78b66ff9f23ea174f72e6feec1c67752309bf3f8acb
5
5
  SHA512:
6
- metadata.gz: e29f1a82da384c20cf1d2b99793fedd5079d129a3767cd0c18ba81a55b41b4cf7621c9d0528d992239dc75480607ec0702bb6c70f2562660bb5a063d4faa33d6
7
- data.tar.gz: 6b144d42bb808d4ef5cab32ed5119ec6a976046e4dd82b9d3fac221c2aacfd2015985f0428f1011493a2bd46289e75876b44bce62e1c69b2f22cb42f0c2e1ff7
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`) 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.
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
- <% actions_proc = if @resource_can_edit || @resource_can_destroy
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
- if resource_can?(:edit, record)
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(safe_join(items), class: "l-ui-popover__menu")
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].freeze
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|
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Resource
3
- VERSION = "0.5.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
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.5.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