backstage 0.1.12 → 0.1.13
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 +19 -0
- data/app/controllers/backstage/resources_controller.rb +7 -2
- data/app/views/backstage/fields/_decimal.html.erb +1 -0
- data/app/views/backstage/fields/_nested.html.erb +30 -0
- data/app/views/backstage/resources/edit.html.erb +1 -1
- data/app/views/backstage/resources/index.html.erb +3 -2
- data/app/views/backstage/resources/new.html.erb +7 -3
- data/lib/backstage/auto_discovery.rb +2 -2
- data/lib/backstage/dashboard_config.rb +2 -2
- data/lib/backstage/field.rb +12 -0
- data/lib/backstage/resource_config.rb +20 -7
- data/lib/backstage/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9de1bd6823d300ceef1f45b44ff97b635b6497ed1e58df6d814ec5ce45efc56e
|
|
4
|
+
data.tar.gz: d2863d1096ece91857ab403a4fa874f015f4065b5d9922553eaccc09d2245061
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f2799b494c57a749cc0c29595121fda2a90bf5c99704138bf70597fe14b1480c96c49984528b29f95fee0b1714ce0376e351be31057ea67b0fd6fb261675cb5d
|
|
7
|
+
data.tar.gz: d95bbf7d40f238529b320ed0194f8c4fd6ecfc7799630e1274145423d869f1e36aeeb581a9abeb8a17761bc85464ca4618f705ad1e776ddf91f3cdb9bb1dcbef
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.13] — 2026-05-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `c.nested :assoc, fields: [...]` DSL for `accepts_nested_attributes_for` associations — renders existing nested records as editable rows
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- SQL injection: sort column now uses Arel table quoting instead of raw string interpolation
|
|
19
|
+
- XSS: `respond_with_success` now HTML-escapes the message argument before rendering
|
|
20
|
+
- `new.html.erb` now respects container fields (row/section) — previously rendered a spurious wrapper `<div>` and label around each
|
|
21
|
+
- `decimal` and `float` column types now map to `:decimal` instead of `:integer`; a new `_decimal` partial renders them with `step: "any"`
|
|
22
|
+
- `section` block now uses `ensure` to reset `@current_target`, preventing a stale target if the block raises
|
|
23
|
+
- `find_field` now searches inside container `sub_fields`, preventing duplicate fields when re-specifying a field already moved into a section
|
|
24
|
+
- `DashboardConfig` now raises `ArgumentError` at initialisation if `name` or `model` is missing from the YAML hash
|
|
25
|
+
- `params.permit!` in `index.html.erb` replaced with explicit param slice
|
|
26
|
+
- Edit page no longer renders an empty `class=""` attribute when no sidebar is present
|
|
27
|
+
- Removed unused `sidebar_links`, `custom_actions`, and `excluded_columns` attributes from `ResourceConfig`
|
|
28
|
+
|
|
10
29
|
## [0.1.12] — 2026-05-25
|
|
11
30
|
|
|
12
31
|
### Added
|
|
@@ -22,7 +22,8 @@ module Backstage
|
|
|
22
22
|
if params[:sort].present? && valid_columns.include?(params[:sort])
|
|
23
23
|
@sort = params[:sort]
|
|
24
24
|
@dir = (params[:dir] == "desc") ? "desc" : "asc"
|
|
25
|
-
|
|
25
|
+
arel_col = @resource_config.model_class.arel_table[@sort]
|
|
26
|
+
scope = scope.order((@dir == "desc") ? arel_col.desc : arel_col.asc)
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
@total_pages = [(scope.count.to_f / per_page).ceil, 1].max
|
|
@@ -78,8 +79,9 @@ module Backstage
|
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
def respond_with_success(message)
|
|
82
|
+
safe_message = ERB::Util.html_escape(message)
|
|
81
83
|
render html: %(<turbo-stream action="prepend" target="flash">
|
|
82
|
-
<template><p class="notice">#{
|
|
84
|
+
<template><p class="notice">#{safe_message}</p></template>
|
|
83
85
|
</turbo-stream>).html_safe,
|
|
84
86
|
content_type: "text/vnd.turbo-stream.html"
|
|
85
87
|
end
|
|
@@ -96,6 +98,9 @@ module Backstage
|
|
|
96
98
|
permitted_field_names(field.sub_fields)
|
|
97
99
|
elsif field.has_many?
|
|
98
100
|
[{field.name => []}]
|
|
101
|
+
elsif field.nested?
|
|
102
|
+
writable = field.nested_fields - field.nested_readonly_fields
|
|
103
|
+
[{"#{field.name}_attributes": [:id, *writable]}]
|
|
99
104
|
else
|
|
100
105
|
[field.name]
|
|
101
106
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= f.number_field field.name, step: "any", readonly: field.readonly? %>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<% records = record.public_send(field.name) %>
|
|
2
|
+
<% if records.any? %>
|
|
3
|
+
<table>
|
|
4
|
+
<thead>
|
|
5
|
+
<tr>
|
|
6
|
+
<% field.nested_fields.each do |col| %>
|
|
7
|
+
<th><%= col.to_s.humanize %></th>
|
|
8
|
+
<% end %>
|
|
9
|
+
</tr>
|
|
10
|
+
</thead>
|
|
11
|
+
<tbody>
|
|
12
|
+
<%= f.fields_for field.name do |nf| %>
|
|
13
|
+
<%= nf.hidden_field :id %>
|
|
14
|
+
<tr>
|
|
15
|
+
<% field.nested_fields.each do |col| %>
|
|
16
|
+
<td>
|
|
17
|
+
<% if field.nested_readonly_fields.include?(col) %>
|
|
18
|
+
<%= nf.object.public_send(col) %>
|
|
19
|
+
<% else %>
|
|
20
|
+
<%= nf.text_field col %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</td>
|
|
23
|
+
<% end %>
|
|
24
|
+
</tr>
|
|
25
|
+
<% end %>
|
|
26
|
+
</tbody>
|
|
27
|
+
</table>
|
|
28
|
+
<% else %>
|
|
29
|
+
<em>None</em>
|
|
30
|
+
<% end %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<h1>Edit <%= @resource_config.model_class.model_name.human %></h1>
|
|
2
2
|
|
|
3
3
|
<% sidebar = @resource_config.sidebar_config %>
|
|
4
|
-
<div
|
|
4
|
+
<div<% if sidebar&.links&.any? %> class="edit-layout"<% end %>>
|
|
5
5
|
<div>
|
|
6
6
|
<%= form_with model: @record, url: resource_path(resource: params[:resource], id: @record.id), method: :patch do |f| %>
|
|
7
7
|
<% @resource_config.edit_fields.each do |field| %>
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
</form>
|
|
7
7
|
|
|
8
8
|
<% @resource_config.index_fields.select(&:enum?).each do |field| %>
|
|
9
|
+
<% safe_params = params.permit(:q, :sort, :dir, :page, field.name) %>
|
|
9
10
|
<nav class="enum-filters">
|
|
10
|
-
<%= link_to "All", url_for(
|
|
11
|
+
<%= link_to "All", url_for(safe_params.except(field.name.to_s, "page")) %>
|
|
11
12
|
<% field.enum_values.each do |label, value| %>
|
|
12
|
-
<%= link_to label, url_for(
|
|
13
|
+
<%= link_to label, url_for(safe_params.merge(field.name => value, page: nil)) %>
|
|
13
14
|
<% end %>
|
|
14
15
|
</nav>
|
|
15
16
|
<% end %>
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<%= form_with model: @record, url: resources_path(resource: params[:resource]), method: :post do |f| %>
|
|
4
4
|
<% @resource_config.edit_fields.each do |field| %>
|
|
5
|
-
|
|
6
|
-
<%= f.label field.name %>
|
|
5
|
+
<% if field.container? %>
|
|
7
6
|
<%= render partial: field.partial_path, locals: { f: f, field: field, record: @record } %>
|
|
8
|
-
|
|
7
|
+
<% else %>
|
|
8
|
+
<div>
|
|
9
|
+
<%= f.label field.name %>
|
|
10
|
+
<%= render partial: field.partial_path, locals: { f: f, field: field, record: @record } %>
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
9
13
|
<% end %>
|
|
10
14
|
<%= f.submit "Create" %>
|
|
11
15
|
<% end %>
|
|
@@ -3,8 +3,8 @@ module Backstage
|
|
|
3
3
|
attr_reader :name, :model_name, :scope
|
|
4
4
|
|
|
5
5
|
def initialize(hash)
|
|
6
|
-
@name = hash["name"]
|
|
7
|
-
@model_name = hash["model"]
|
|
6
|
+
@name = hash["name"] or raise ArgumentError, "Dashboard config missing 'name'"
|
|
7
|
+
@model_name = hash["model"] or raise ArgumentError, "Dashboard config missing 'model'"
|
|
8
8
|
@scope = hash["scope"] || {}
|
|
9
9
|
end
|
|
10
10
|
|
data/lib/backstage/field.rb
CHANGED
|
@@ -32,6 +32,18 @@ module Backstage
|
|
|
32
32
|
type == :has_many
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def nested?
|
|
36
|
+
type == :nested
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def nested_fields
|
|
40
|
+
options[:nested_fields] || []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def nested_readonly_fields
|
|
44
|
+
options[:nested_readonly_fields] || []
|
|
45
|
+
end
|
|
46
|
+
|
|
35
47
|
def row?
|
|
36
48
|
type == :row
|
|
37
49
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module Backstage
|
|
2
2
|
class ResourceConfig
|
|
3
|
-
attr_accessor :model_class, :index_fields, :edit_fields,
|
|
4
|
-
:associations, :sidebar_links, :custom_actions, :excluded_columns
|
|
3
|
+
attr_accessor :model_class, :index_fields, :edit_fields, :associations
|
|
5
4
|
attr_writer :display_column
|
|
6
5
|
|
|
7
6
|
def initialize(model_class)
|
|
@@ -9,9 +8,6 @@ module Backstage
|
|
|
9
8
|
@index_fields = []
|
|
10
9
|
@edit_fields = []
|
|
11
10
|
@associations = []
|
|
12
|
-
@sidebar_links = []
|
|
13
|
-
@custom_actions = []
|
|
14
|
-
@excluded_columns = []
|
|
15
11
|
end
|
|
16
12
|
|
|
17
13
|
def display_column(value = nil)
|
|
@@ -56,6 +52,14 @@ module Backstage
|
|
|
56
52
|
end
|
|
57
53
|
end
|
|
58
54
|
|
|
55
|
+
def nested(name, fields:, readonly_fields: [])
|
|
56
|
+
field_obj = Field.new(name.to_sym, :nested,
|
|
57
|
+
nested_fields: Array(fields).map(&:to_sym),
|
|
58
|
+
nested_readonly_fields: Array(readonly_fields).map(&:to_sym))
|
|
59
|
+
@edit_fields.reject! { |f| f.name == field_obj.name }
|
|
60
|
+
(@current_target || @edit_fields) << field_obj
|
|
61
|
+
end
|
|
62
|
+
|
|
59
63
|
def belongs_to(name, **opts)
|
|
60
64
|
assoc = AssociationConfig.new(name, :belongs_to, opts)
|
|
61
65
|
@associations << assoc
|
|
@@ -98,14 +102,23 @@ module Backstage
|
|
|
98
102
|
heading: heading, sub_fields: [], **opts)
|
|
99
103
|
@current_target = section_field.sub_fields
|
|
100
104
|
yield if block_given?
|
|
101
|
-
@current_target = nil
|
|
102
105
|
@edit_fields << section_field
|
|
106
|
+
ensure
|
|
107
|
+
@current_target = nil
|
|
103
108
|
end
|
|
104
109
|
|
|
105
110
|
private
|
|
106
111
|
|
|
107
112
|
def find_field(name)
|
|
108
|
-
|
|
113
|
+
all_fields = @index_fields + @edit_fields
|
|
114
|
+
all_fields.uniq.each do |f|
|
|
115
|
+
return f if f.name == name
|
|
116
|
+
if f.container?
|
|
117
|
+
found = f.sub_fields.find { |sf| sf.name == name }
|
|
118
|
+
return found if found
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
nil
|
|
109
122
|
end
|
|
110
123
|
|
|
111
124
|
def find_or_build_field(name)
|
data/lib/backstage/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gareth James
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -47,10 +47,12 @@ files:
|
|
|
47
47
|
- app/views/backstage/fields/_boolean.html.erb
|
|
48
48
|
- app/views/backstage/fields/_date.html.erb
|
|
49
49
|
- app/views/backstage/fields/_datetime.html.erb
|
|
50
|
+
- app/views/backstage/fields/_decimal.html.erb
|
|
50
51
|
- app/views/backstage/fields/_enum.html.erb
|
|
51
52
|
- app/views/backstage/fields/_has_many.html.erb
|
|
52
53
|
- app/views/backstage/fields/_image_url.html.erb
|
|
53
54
|
- app/views/backstage/fields/_integer.html.erb
|
|
55
|
+
- app/views/backstage/fields/_nested.html.erb
|
|
54
56
|
- app/views/backstage/fields/_row.html.erb
|
|
55
57
|
- app/views/backstage/fields/_section.html.erb
|
|
56
58
|
- app/views/backstage/fields/_string.html.erb
|