backstage 0.1.12 → 0.1.14

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: e1dc01f215a31bcb5c84237cdfb95a7622ab7c8298e3b822eb476a9363e2f35c
4
- data.tar.gz: 2a83eb127cfe5da24e409bf9f1ef69019a4d11be122402b5ca712f66ddfd1b3d
3
+ metadata.gz: af33e498a31b4178f2565526576a747f3b40f79fe869d0c9f02e7b9147f250ed
4
+ data.tar.gz: e4a561394fe10ec3ac30a8bcbc3492bd13285881c9e1e1cf2d16a2ef3302e259
5
5
  SHA512:
6
- metadata.gz: 46f7f8d377fa8c1fa57f3113b224df3428cdc99d21282ea1812a3e4d6b5f617c1f051adcc303217df1da8f4c2edfa1c800fa32dc3ae91b4f77aca0f3deb90a82
7
- data.tar.gz: 3600c7c9e27c410a887fa6e63458eee543eb2d23271ee24adb99d1a8785272c7e3625498a86ae6ee54f0d68da3b6a6ed826c858a764394dcaea0b30536195d81
6
+ metadata.gz: faf0d674103b06bffd2625b114aec195037f8ab3c08ff6a5c8786abb8b8fd5c53938c6f394a934e10820459089af5c0fcafda82d86ddae9c584081918287ceef
7
+ data.tar.gz: 1b2ee32beb0c16edfd8a7d2b08f86671ead990cc131272a60b9fd3a027fd8b9b1c5d37314ee7eccec221e6cd037fb9e6706d60dc8f97041dbfd1f11470e1a2ac
data/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.14] — 2026-05-26
11
+
12
+ ### Fixed
13
+
14
+ - `ActionsController` no longer falls back to `Backstage::ResourcesController` when no custom controller exists — raises `NotImplementedError` instead of silently dispatching inherited CRUD actions
15
+ - Custom action dispatch now uses `method_defined?(name, false)` so only actions defined directly on the host subclass are callable; inherited methods (`index`, `create`, `update`, `destroy`, etc.) are blocked
16
+ - Gemspec `homepage` corrected to point to the right repository
17
+
18
+ ## [0.1.13] — 2026-05-26
19
+
20
+ ### Added
21
+
22
+ - `c.nested :assoc, fields: [...]` DSL for `accepts_nested_attributes_for` associations — renders existing nested records as editable rows
23
+
24
+ ### Fixed
25
+
26
+ - SQL injection: sort column now uses Arel table quoting instead of raw string interpolation
27
+ - XSS: `respond_with_success` now HTML-escapes the message argument before rendering
28
+ - `new.html.erb` now respects container fields (row/section) — previously rendered a spurious wrapper `<div>` and label around each
29
+ - `decimal` and `float` column types now map to `:decimal` instead of `:integer`; a new `_decimal` partial renders them with `step: "any"`
30
+ - `section` block now uses `ensure` to reset `@current_target`, preventing a stale target if the block raises
31
+ - `find_field` now searches inside container `sub_fields`, preventing duplicate fields when re-specifying a field already moved into a section
32
+ - `DashboardConfig` now raises `ArgumentError` at initialisation if `name` or `model` is missing from the YAML hash
33
+ - `params.permit!` in `index.html.erb` replaced with explicit param slice
34
+ - Edit page no longer renders an empty `class=""` attribute when no sidebar is present
35
+ - Removed unused `sidebar_links`, `custom_actions`, and `excluded_columns` attributes from `ResourceConfig`
36
+
10
37
  ## [0.1.12] — 2026-05-25
11
38
 
12
39
  ### Added
@@ -8,14 +8,19 @@ module Backstage
8
8
  end
9
9
 
10
10
  resource_name = params[:resource].classify.pluralize
11
- controller_class = "Backstage::#{resource_name}Controller".safe_constantize ||
12
- Backstage::ResourcesController
11
+ controller_class = "Backstage::#{resource_name}Controller".safe_constantize
13
12
  action_name = params[:action_name]
14
13
 
15
- unless controller_class.method_defined?(action_name)
14
+ if controller_class.nil? || controller_class == Backstage::ResourcesController
15
+ raise NotImplementedError,
16
+ "Backstage: no custom controller found for #{resource_name}. " \
17
+ "Define Backstage::#{resource_name}Controller."
18
+ end
19
+
20
+ unless controller_class.method_defined?(action_name.to_sym, false)
16
21
  raise NotImplementedError,
17
22
  "Backstage: no action '#{action_name}' on #{controller_class}. " \
18
- "Define it in a Backstage::#{resource_name}Controller subclass."
23
+ "Define it in Backstage::#{resource_name}Controller."
19
24
  end
20
25
 
21
26
  status, headers, body = controller_class.action(action_name).call(request.env)
@@ -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
- scope = scope.order("#{@sort} #{@dir}")
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">#{message}</p></template>
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 class="<%= sidebar&.links&.any? ? "edit-layout" : "" %>">
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(params.permit!.except(:status, :page)) %>
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(params.permit!.merge(field.name => value, page: nil)) %>
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
- <div>
6
- <%= f.label field.name %>
5
+ <% if field.container? %>
7
6
  <%= render partial: field.partial_path, locals: { f: f, field: field, record: @record } %>
8
- </div>
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 %>
@@ -6,8 +6,8 @@ module Backstage
6
6
  string: :string,
7
7
  text: :text,
8
8
  integer: :integer,
9
- decimal: :integer,
10
- float: :integer,
9
+ decimal: :decimal,
10
+ float: :decimal,
11
11
  boolean: :boolean,
12
12
  date: :date,
13
13
  datetime: :datetime
@@ -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
 
@@ -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
- (@index_fields + @edit_fields).uniq.find { |f| f.name == name }
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)
@@ -1,3 +1,3 @@
1
1
  module Backstage
2
- VERSION = "0.1.12"
2
+ VERSION = "0.1.14"
3
3
  end
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.12
4
+ version: 0.1.14
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-25 00:00:00.000000000 Z
11
+ date: 2026-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -26,7 +26,6 @@ dependencies:
26
26
  version: '8.0'
27
27
  description:
28
28
  email:
29
- - g.claude@bemused.org
30
29
  executables: []
31
30
  extensions: []
32
31
  extra_rdoc_files: []
@@ -47,10 +46,12 @@ files:
47
46
  - app/views/backstage/fields/_boolean.html.erb
48
47
  - app/views/backstage/fields/_date.html.erb
49
48
  - app/views/backstage/fields/_datetime.html.erb
49
+ - app/views/backstage/fields/_decimal.html.erb
50
50
  - app/views/backstage/fields/_enum.html.erb
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/_nested.html.erb
54
55
  - app/views/backstage/fields/_row.html.erb
55
56
  - app/views/backstage/fields/_section.html.erb
56
57
  - app/views/backstage/fields/_string.html.erb
@@ -78,7 +79,7 @@ files:
78
79
  - lib/generators/backstage/install/templates/SKILL.md
79
80
  - lib/generators/backstage/install/templates/backstage.rb
80
81
  - lib/generators/backstage/install/templates/backstage.yml
81
- homepage: https://github.com/gjtorikian/backstage
82
+ homepage: https://github.com/garethfr/backstage
82
83
  licenses:
83
84
  - MIT
84
85
  metadata: {}