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 +4 -4
- data/CHANGELOG.md +15 -0
- data/app/assets/stylesheets/backstage/backstage.css +2 -1
- data/app/controllers/backstage/resources_controller.rb +14 -3
- data/app/views/backstage/fields/_row.html.erb +8 -0
- data/app/views/backstage/fields/_section.html.erb +13 -0
- data/app/views/backstage/resources/edit.html.erb +35 -14
- data/app/views/backstage/shared/_pagination.html.erb +8 -6
- data/app/views/layouts/backstage/backstage.html.erb +0 -12
- data/lib/backstage/field.rb +24 -0
- data/lib/backstage/resource_config.rb +23 -2
- data/lib/backstage/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1dc01f215a31bcb5c84237cdfb95a7622ab7c8298e3b822eb476a9363e2f35c
|
|
4
|
+
data.tar.gz: 2a83eb127cfe5da24e409bf9f1ef69019a4d11be122402b5ca712f66ddfd1b3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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,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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
<%=
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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 %> …<% end %>
|
|
8
|
+
<% (window_start..window_end).each do |p| %>
|
|
9
|
+
<%= link_to p, url_for(page: p), class: (p == @page ? "current" : nil) %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% if window_end < @total_pages - 1 %> …<% end %>
|
|
9
12
|
<% end %>
|
|
10
|
-
<% if window_end < @total_pages - 1 %> …<% end %>
|
|
11
13
|
<%= 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>
|
data/lib/backstage/field.rb
CHANGED
|
@@ -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)
|
data/lib/backstage/version.rb
CHANGED
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.
|
|
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
|