backstage 0.1.10 → 0.1.12

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: d8d5018436566611adae93216ace1303f8ae4ed22af25015099291bf4b042290
4
- data.tar.gz: 14af0f649cdb30c48cc3aaae389892f5e533894e1ab61339f342b55316be7367
3
+ metadata.gz: e1dc01f215a31bcb5c84237cdfb95a7622ab7c8298e3b822eb476a9363e2f35c
4
+ data.tar.gz: 2a83eb127cfe5da24e409bf9f1ef69019a4d11be122402b5ca712f66ddfd1b3d
5
5
  SHA512:
6
- metadata.gz: 71d2b10bbb890c1a372a23bfde83eb78aa7e8773485dcbbcc48db3d1b438e485a16ebea095f1f322a461939af9e8d57cf8bcd6271a6062fc197793489e9bbe04
7
- data.tar.gz: 7f767a0c986971fb1916194fc969df576d3fd2b315d39f1196fe92c65ccd4fbf90b356f267072a9bda02e59a2bbd17b1f3d16c2db6f122327ed92f934d3bd482
6
+ metadata.gz: 46f7f8d377fa8c1fa57f3113b224df3428cdc99d21282ea1812a3e4d6b5f617c1f051adcc303217df1da8f4c2edfa1c800fa32dc3ae91b4f77aca0f3deb90a82
7
+ data.tar.gz: 3600c7c9e27c410a887fa6e63458eee543eb2d23271ee24adb99d1a8785272c7e3625498a86ae6ee54f0d68da3b6a6ed826c858a764394dcaea0b30536195d81
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.12] — 2026-05-25
11
+
12
+ ### Added
13
+
14
+ - `c.row :field1, :field2` groups fields horizontally in a Pico CSS grid on the edit page
15
+ - `c.section "Label" [, collapsed: true]` wraps fields in a native `<details>`/`<summary>` collapsible block (no JavaScript required); rows and individual fields can be nested inside sections
16
+ - `c.field` called inside a `section` block moves an existing auto-discovered field into the section rather than leaving it at the top level
17
+
18
+ ## [0.1.11] — 2026-05-25
19
+
20
+ ### Fixed
21
+
22
+ - Pagination window no longer generates links below page 1 or above the last page when there are fewer than 6 total pages; window is clamped to the valid inner range and skipped entirely when `total_pages <= 2`
23
+ - Sidebar moved from the layout into the edit view, rendering to the right of the form in a two-column grid; links open in a new tab (`target="_blank"`) and blank URLs (e.g. from a proc returning `""`) are skipped silently
24
+
10
25
  ## [0.1.10] — 2026-05-25
11
26
 
12
27
  ### Fixed
@@ -3,7 +3,8 @@ body { display: grid; grid-template-columns: 200px 1fr; grid-template-rows: auto
3
3
  header { grid-column: 1 / -1; }
4
4
  nav { padding: 1rem; grid-row: 2; grid-column: 1; }
5
5
  main { padding: 1rem; grid-row: 2; grid-column: 2; }
6
- aside { grid-column: 2; padding: 1rem; border-top: 1px solid var(--pico-muted-border-color); }
6
+ .edit-layout { display: grid; grid-template-columns: 1fr 220px; gap: 2rem; align-items: start; }
7
+ .edit-layout aside { border-left: 1px solid var(--pico-muted-border-color); padding-left: 1rem; }
7
8
 
8
9
  /* Override Pico's horizontal nav list */
9
10
  nav ul { flex-direction: column; }
@@ -85,10 +85,21 @@ module Backstage
85
85
  end
86
86
 
87
87
  def record_params
88
- permitted = @resource_config.edit_fields.reject(&:readonly?).map do |field|
89
- field.has_many? ? {field.name => []} : field.name
88
+ params
89
+ .require(@resource_config.model_class.model_name.param_key)
90
+ .permit(permitted_field_names(@resource_config.edit_fields))
91
+ end
92
+
93
+ def permitted_field_names(fields)
94
+ fields.reject(&:readonly?).flat_map do |field|
95
+ if field.container?
96
+ permitted_field_names(field.sub_fields)
97
+ elsif field.has_many?
98
+ [{field.name => []}]
99
+ else
100
+ [field.name]
101
+ end
90
102
  end
91
- params.require(@resource_config.model_class.model_name.param_key).permit(permitted)
92
103
  end
93
104
  end
94
105
  end
@@ -0,0 +1,8 @@
1
+ <div class="grid">
2
+ <% field.sub_fields.each do |sub| %>
3
+ <div>
4
+ <%= f.label sub.name %>
5
+ <%= render partial: sub.partial_path, locals: { f: f, field: sub, record: @record } %>
6
+ </div>
7
+ <% end %>
8
+ </div>
@@ -0,0 +1,13 @@
1
+ <details <%= "open" unless field.collapsed? %>>
2
+ <summary><strong><%= field.heading %></strong></summary>
3
+ <% field.sub_fields.each do |sub| %>
4
+ <% if sub.container? %>
5
+ <%= render partial: sub.partial_path, locals: { f: f, field: sub, record: @record } %>
6
+ <% else %>
7
+ <div>
8
+ <%= f.label sub.name %>
9
+ <%= render partial: sub.partial_path, locals: { f: f, field: sub, record: @record } %>
10
+ </div>
11
+ <% end %>
12
+ <% end %>
13
+ </details>
@@ -1,18 +1,39 @@
1
1
  <h1>Edit <%= @resource_config.model_class.model_name.human %></h1>
2
2
 
3
- <%= form_with model: @record, url: resource_path(resource: params[:resource], id: @record.id), method: :patch do |f| %>
4
- <% @resource_config.edit_fields.each do |field| %>
5
- <div>
6
- <%= f.label field.name %>
7
- <%= render partial: field.partial_path, locals: { f: f, field: field, record: @record } %>
8
- </div>
9
- <% end %>
10
- <%= f.submit "Save" %>
11
- <% end %>
3
+ <% sidebar = @resource_config.sidebar_config %>
4
+ <div class="<%= sidebar&.links&.any? ? "edit-layout" : "" %>">
5
+ <div>
6
+ <%= form_with model: @record, url: resource_path(resource: params[:resource], id: @record.id), method: :patch do |f| %>
7
+ <% @resource_config.edit_fields.each do |field| %>
8
+ <% if field.container? %>
9
+ <%= render partial: field.partial_path, locals: { f: f, field: field, record: @record } %>
10
+ <% else %>
11
+ <div>
12
+ <%= f.label field.name %>
13
+ <%= render partial: field.partial_path, locals: { f: f, field: field, record: @record } %>
14
+ </div>
15
+ <% end %>
16
+ <% end %>
17
+ <%= f.submit "Save" %>
18
+ <% end %>
19
+
20
+ <%= link_to "Cancel", resources_path(resource: params[:resource]) %>
12
21
 
13
- <%= link_to "Cancel", resources_path(resource: params[:resource]) %>
22
+ <%= button_to "Delete",
23
+ resource_path(resource: params[:resource], id: @record.id),
24
+ method: :delete,
25
+ data: { confirm_message: "Delete this #{@resource_config.model_class.model_name.human}?" } %>
26
+ </div>
14
27
 
15
- <%= button_to "Delete",
16
- resource_path(resource: params[:resource], id: @record.id),
17
- method: :delete,
18
- data: { confirm_message: "Delete this #{@resource_config.model_class.model_name.human}?" } %>
28
+ <% if sidebar&.links&.any? %>
29
+ <aside>
30
+ <ul>
31
+ <% sidebar.links.each do |link| %>
32
+ <% url = link.url_or_proc.respond_to?(:call) ? link.url_or_proc.call(@record) : link.url_or_proc %>
33
+ <% next if url.blank? %>
34
+ <li><%= link_to link.label, url, target: "_blank" %></li>
35
+ <% end %>
36
+ </ul>
37
+ </aside>
38
+ <% end %>
39
+ </div>
@@ -1,13 +1,15 @@
1
1
  <% if @total_pages > 1 %>
2
- <% window_start = [[@page - 2, 2].max, @total_pages - 4].min
3
- window_end = [[@page + 2, @total_pages - 1].min, 5].max %>
4
2
  <nav>
5
3
  <%= link_to "1", url_for(page: 1), class: (@page == 1 ? "current" : nil) %>
6
- <% if window_start > 2 %> &hellip;<% end %>
7
- <% (window_start..window_end).each do |p| %>
8
- &nbsp;<%= link_to p, url_for(page: p), class: (p == @page ? "current" : nil) %>
4
+ <% if @total_pages > 2 %>
5
+ <% window_start = [[@page - 2, 2].max, @total_pages - 4].min.clamp(2, @total_pages - 1)
6
+ window_end = [[@page + 2, @total_pages - 1].min, 5].max.clamp(2, @total_pages - 1) %>
7
+ <% if window_start > 2 %> &hellip;<% end %>
8
+ <% (window_start..window_end).each do |p| %>
9
+ &nbsp;<%= link_to p, url_for(page: p), class: (p == @page ? "current" : nil) %>
10
+ <% end %>
11
+ <% if window_end < @total_pages - 1 %> &hellip;<% end %>
9
12
  <% end %>
10
- <% if window_end < @total_pages - 1 %> &hellip;<% end %>
11
13
  &nbsp;<%= link_to @total_pages, url_for(page: @total_pages), class: (@page == @total_pages ? "current" : nil) %>
12
14
  </nav>
13
15
  <% end %>
@@ -44,17 +44,5 @@
44
44
  <main>
45
45
  <%= yield %>
46
46
  </main>
47
- <% sidebar = defined?(@resource_config) && @resource_config&.sidebar_config
48
- record = defined?(@record) ? @record : nil %>
49
- <% if sidebar&.links&.any? && record&.persisted? %>
50
- <aside>
51
- <ul>
52
- <% sidebar.links.each do |link| %>
53
- <% url = link.url_or_proc.respond_to?(:call) ? link.url_or_proc.call(record) : link.url_or_proc %>
54
- <li><%= link_to link.label, url %></li>
55
- <% end %>
56
- </ul>
57
- </aside>
58
- <% end %>
59
47
  </body>
60
48
  </html>
@@ -32,6 +32,30 @@ module Backstage
32
32
  type == :has_many
33
33
  end
34
34
 
35
+ def row?
36
+ type == :row
37
+ end
38
+
39
+ def section?
40
+ type == :section
41
+ end
42
+
43
+ def container?
44
+ row? || section?
45
+ end
46
+
47
+ def sub_fields
48
+ options[:sub_fields] || []
49
+ end
50
+
51
+ def heading
52
+ options[:heading]
53
+ end
54
+
55
+ def collapsed?
56
+ options.fetch(:collapsed, false)
57
+ end
58
+
35
59
  def association
36
60
  options[:association]
37
61
  end
@@ -74,13 +74,34 @@ module Backstage
74
74
  if existing
75
75
  existing.options.merge!(opts)
76
76
  existing.instance_variable_set(:@type, type.to_sym) if type
77
+ if @current_target
78
+ @edit_fields.reject! { |f| f.name == sym }
79
+ @current_target << existing
80
+ end
77
81
  else
78
82
  new_field = Field.new(sym, type || :string, opts)
79
- @edit_fields << new_field
80
- @index_fields << new_field unless @index_fields_explicit
83
+ (@current_target || @edit_fields) << new_field
84
+ @index_fields << new_field unless @index_fields_explicit || @current_target
81
85
  end
82
86
  end
83
87
 
88
+ def row(*names)
89
+ syms = names.map(&:to_sym)
90
+ sub = syms.map { |n| find_field(n) || Field.new(n, :string) }
91
+ @edit_fields.reject! { |f| syms.include?(f.name) }
92
+ row_field = Field.new(:"row_#{names.first}", :row, sub_fields: sub)
93
+ (@current_target || @edit_fields) << row_field
94
+ end
95
+
96
+ def section(heading, **opts)
97
+ section_field = Field.new(:"section_#{heading.parameterize}", :section,
98
+ heading: heading, sub_fields: [], **opts)
99
+ @current_target = section_field.sub_fields
100
+ yield if block_given?
101
+ @current_target = nil
102
+ @edit_fields << section_field
103
+ end
104
+
84
105
  private
85
106
 
86
107
  def find_field(name)
@@ -1,3 +1,3 @@
1
1
  module Backstage
2
- VERSION = "0.1.10"
2
+ VERSION = "0.1.12"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backstage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gareth James
@@ -51,6 +51,8 @@ files:
51
51
  - app/views/backstage/fields/_has_many.html.erb
52
52
  - app/views/backstage/fields/_image_url.html.erb
53
53
  - app/views/backstage/fields/_integer.html.erb
54
+ - app/views/backstage/fields/_row.html.erb
55
+ - app/views/backstage/fields/_section.html.erb
54
56
  - app/views/backstage/fields/_string.html.erb
55
57
  - app/views/backstage/fields/_text.html.erb
56
58
  - app/views/backstage/fields/_thumbnails.html.erb