backstage 0.1.14 → 0.1.16

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: af33e498a31b4178f2565526576a747f3b40f79fe869d0c9f02e7b9147f250ed
4
- data.tar.gz: e4a561394fe10ec3ac30a8bcbc3492bd13285881c9e1e1cf2d16a2ef3302e259
3
+ metadata.gz: c93addd91e80f76d095d7fd56542ebea748d27be3043cad7affcee189143718e
4
+ data.tar.gz: a9df1684acf09fb155e449e612e1e92c117874b581d4161f4b9c4b3749f610b4
5
5
  SHA512:
6
- metadata.gz: faf0d674103b06bffd2625b114aec195037f8ab3c08ff6a5c8786abb8b8fd5c53938c6f394a934e10820459089af5c0fcafda82d86ddae9c584081918287ceef
7
- data.tar.gz: 1b2ee32beb0c16edfd8a7d2b08f86671ead990cc131272a60b9fd3a027fd8b9b1c5d37314ee7eccec221e6cd037fb9e6706d60dc8f97041dbfd1f11470e1a2ac
6
+ metadata.gz: 48c81f6f435f100053c573b886ad40f6df175ae871302bbc381c161ade4f99bbe2062505de73133702d8a7c05e679d721e31d0a95f76b6555d3d1923df0f328a
7
+ data.tar.gz: 6653ba7832ea040c05081c5f46e9b4f8504503ed7bf117a49c7d111e95d144c56edbf9262f7864eed7d12094c482574e31ad0505a33323ea6141601e9d13d4e3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.16] — 2026-06-02
11
+
12
+ ### Fixed
13
+
14
+ - Nested table now uses a `<template>` element instead of a static new row, so unsaved blank rows are never submitted unintentionally
15
+ - New-row template includes readonly fields as plain inputs so new records can be fully populated via the Add button
16
+ - Readonly nested fields are now permitted in Strong Parameters, so new records with readonly fields save correctly
17
+ - `belongs_to :assoc, readonly: true` now propagates `readonly:` to the generated FK field
18
+
19
+ ## [0.1.15] — 2026-05-27
20
+
21
+ ### Added
22
+
23
+ - `c.nested :assoc, fields: [...]` DSL renders existing `has_many` records as an inline editable table using `accepts_nested_attributes_for`
24
+ - Nested rows include a destroy button (×) and a `_destroy` hidden field; clicking hides the row and marks it for deletion on save
25
+ - A blank add row is always rendered at the bottom of the nested table for creating new records
26
+ - `image_url` fields on index and dashboard tables now render as `<img>` tags instead of raw URLs
27
+ - `belongs_to` fields now render correctly on dashboard tables (was missing)
28
+ - Shared `backstage/shared/_cell` partial extracted for consistent field rendering across index and dashboard views
29
+
10
30
  ## [0.1.14] — 2026-05-26
11
31
 
12
32
  ### Fixed
@@ -99,8 +99,7 @@ module Backstage
99
99
  elsif field.has_many?
100
100
  [{field.name => []}]
101
101
  elsif field.nested?
102
- writable = field.nested_fields - field.nested_readonly_fields
103
- [{"#{field.name}_attributes": [:id, *writable]}]
102
+ [{"#{field.name}_attributes": [:id, :_destroy, *field.nested_fields]}]
104
103
  else
105
104
  [field.name]
106
105
  end
@@ -13,13 +13,7 @@
13
13
  <% @records.each do |record| %>
14
14
  <tr id="<%= @resource_config.model_name_param %>_<%= record.id %>_row">
15
15
  <% @resource_config.index_fields.each do |field| %>
16
- <td>
17
- <% if field.enum? %>
18
- <%= record.public_send(field.name).to_s.humanize %>
19
- <% else %>
20
- <%= record.public_send(field.name) %>
21
- <% end %>
22
- </td>
16
+ <td><%= render "backstage/shared/cell", field: field, record: record %></td>
23
17
  <% end %>
24
18
  <td><%= link_to "Edit", edit_resource_path(resource: @resource_config.model_name_param, id: record.id) %></td>
25
19
  </tr>
@@ -1,30 +1,55 @@
1
1
  <% records = record.public_send(field.name) %>
2
- <% if records.any? %>
3
- <table>
4
- <thead>
2
+ <table>
3
+ <thead>
4
+ <tr>
5
+ <% field.nested_fields.each do |col| %>
6
+ <th><%= col.to_s.humanize %></th>
7
+ <% end %>
8
+ <th></th>
9
+ </tr>
10
+ </thead>
11
+ <tbody>
12
+ <%= f.fields_for field.name do |nf| %>
5
13
  <tr>
14
+ <%= nf.hidden_field :id %>
15
+ <%= nf.hidden_field :_destroy, value: "0", data: {nested_destroy_flag: true} %>
6
16
  <% field.nested_fields.each do |col| %>
7
- <th><%= col.to_s.humanize %></th>
17
+ <td>
18
+ <% if field.nested_readonly_fields.include?(col) %>
19
+ <%= nf.object.public_send(col) %>
20
+ <% else %>
21
+ <%= nf.text_field col %>
22
+ <% end %>
23
+ </td>
8
24
  <% end %>
25
+ <td>
26
+ <button type="button" data-nested-destroy aria-label="Remove">&times;</button>
27
+ </td>
9
28
  </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 %>
29
+ <% end %>
30
+ <%# Template is not part of the DOM — never submitted until the user clicks Add %>
31
+ <template data-nested-template data-next-index="<%= records.size %>">
32
+ <tr>
33
+ <% field.nested_fields.each do |col| %>
34
+ <td>
35
+ <input type="text"
36
+ name="<%= "#{f.object_name}[#{field.name}_attributes][__INDEX__][#{col}]" %>"
37
+ value="" />
38
+ </td>
39
+ <% end %>
40
+ <td>
41
+ <input type="hidden"
42
+ name="<%= "#{f.object_name}[#{field.name}_attributes][__INDEX__][_destroy]" %>"
43
+ value="0"
44
+ data-nested-destroy-flag="true" />
45
+ <button type="button" data-nested-destroy aria-label="Remove">&times;</button>
46
+ </td>
47
+ </tr>
48
+ </template>
49
+ <tr>
50
+ <td colspan="<%= field.nested_fields.size + 1 %>">
51
+ <button type="button" data-nested-add>+ Add</button>
52
+ </td>
53
+ </tr>
54
+ </tbody>
55
+ </table>
@@ -32,20 +32,7 @@
32
32
  <% @records.each do |record| %>
33
33
  <tr>
34
34
  <% @resource_config.index_fields.each do |field| %>
35
- <td>
36
- <% if field.enum? %>
37
- <%= record.public_send(field.name).to_s.humanize %>
38
- <% elsif field.belongs_to? %>
39
- <% related = record.public_send(field.association.name)
40
- col = field.association.display_column
41
- label = related&.public_send(col) %>
42
- <% if related %>
43
- <%= link_to label, edit_resource_path(resource: related.class.model_name.plural, id: related.id) %>
44
- <% end %>
45
- <% else %>
46
- <%= record.public_send(field.name) %>
47
- <% end %>
48
- </td>
35
+ <td><%= render "backstage/shared/cell", field: field, record: record %></td>
49
36
  <% end %>
50
37
  <td><%= link_to "Edit", edit_resource_path(resource: params[:resource], id: record.id) %></td>
51
38
  </tr>
@@ -0,0 +1,17 @@
1
+ <% if field.image_url? %>
2
+ <% url = record.public_send(field.name) %>
3
+ <% if url.present? %>
4
+ <%= image_tag url, style: "max-height:60px;" %>
5
+ <% end %>
6
+ <% elsif field.enum? %>
7
+ <%= record.public_send(field.name).to_s.humanize %>
8
+ <% elsif field.belongs_to? %>
9
+ <% related = record.public_send(field.association.name)
10
+ col = field.association.display_column
11
+ label = related&.public_send(col) %>
12
+ <% if related %>
13
+ <%= link_to label, edit_resource_path(resource: related.class.model_name.plural, id: related.id) %>
14
+ <% end %>
15
+ <% else %>
16
+ <%= record.public_send(field.name) %>
17
+ <% end %>
@@ -26,6 +26,24 @@
26
26
  })
27
27
  })
28
28
  })
29
+ document.addEventListener("click", function(e) {
30
+ var destroyBtn = e.target.closest("[data-nested-destroy]")
31
+ if (destroyBtn) {
32
+ var row = destroyBtn.closest("tr")
33
+ var flag = row.querySelector("[data-nested-destroy-flag]")
34
+ if (flag) flag.value = "1"
35
+ row.hidden = true
36
+ }
37
+ var addBtn = e.target.closest("[data-nested-add]")
38
+ if (addBtn) {
39
+ var table = addBtn.closest("table")
40
+ var tmpl = table.querySelector("[data-nested-template]")
41
+ var index = parseInt(tmpl.dataset.nextIndex, 10)
42
+ tmpl.dataset.nextIndex = index + 1
43
+ var html = tmpl.innerHTML.replace(/__INDEX__/g, index)
44
+ addBtn.closest("tr").insertAdjacentHTML("beforebegin", html)
45
+ }
46
+ })
29
47
  })
30
48
  </script>
31
49
  </head>
@@ -36,6 +36,10 @@ module Backstage
36
36
  type == :nested
37
37
  end
38
38
 
39
+ def image_url?
40
+ type == :image_url
41
+ end
42
+
39
43
  def nested_fields
40
44
  options[:nested_fields] || []
41
45
  end
@@ -63,7 +63,7 @@ module Backstage
63
63
  def belongs_to(name, **opts)
64
64
  assoc = AssociationConfig.new(name, :belongs_to, opts)
65
65
  @associations << assoc
66
- fk_field = Field.new(assoc.foreign_key, :belongs_to, association: assoc)
66
+ fk_field = Field.new(assoc.foreign_key, :belongs_to, association: assoc, readonly: opts[:readonly])
67
67
  index_field = Field.new(name.to_sym, :belongs_to, association: assoc)
68
68
  @edit_fields.reject! { |f| f.name == fk_field.name }
69
69
  @index_fields.reject! { |f| f.name == fk_field.name || f.name == index_field.name }
@@ -1,3 +1,3 @@
1
1
  module Backstage
2
- VERSION = "0.1.14"
2
+ VERSION = "0.1.16"
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.14
4
+ version: 0.1.16
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-26 00:00:00.000000000 Z
11
+ date: 2026-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -61,6 +61,7 @@ files:
61
61
  - app/views/backstage/resources/edit.html.erb
62
62
  - app/views/backstage/resources/index.html.erb
63
63
  - app/views/backstage/resources/new.html.erb
64
+ - app/views/backstage/shared/_cell.html.erb
64
65
  - app/views/backstage/shared/_pagination.html.erb
65
66
  - app/views/layouts/backstage/backstage.html.erb
66
67
  - config/routes.rb