base_editing_bootstrap 1.8.1 → 1.10.0

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: 40690c34bdbdecebbcf083394fefe5ea626b958a085ce695fe4a039b17c8cfa9
4
- data.tar.gz: a087acc3bda536ea0451af2e3818cc2c07c20a3a6a5e549bb74e96d7abcd6dd5
3
+ metadata.gz: 489e2ffcb61b916547f555b4b80a90e9d542c0738afb83dd104418e29865a542
4
+ data.tar.gz: 3f7cba0e07451401983eeef463ff0a18ad97f2aee1a5c16a836980d4e77e4ca3
5
5
  SHA512:
6
- metadata.gz: f85bc8f2ed0aa273b2e59258ace609e711dbcb088bf9420a5eaf5a48aa7a48c0907f7d6eb75ac9614cfcf0f536f1124a7bd9ee9541c68c3f4d29908e739a280d
7
- data.tar.gz: 5565e435135fd2bae6a1a6e1907a6d150cb6be69d5b82c52d91095a3637efa811322519ae6f696578760b961339cd986b036f508a06d08532311f88d53209508
6
+ metadata.gz: 2e62893f671fe1ef62e0c9fe52cef3c104fdab7646bb1a03b774ae9d9c5650f288e7329b5e4c5e29fda3f935c0e3706a1af8ecb90b220a16fa1f6f5e8759e726
7
+ data.tar.gz: 52055575f2459020e2963d8af95da7c904c3301cf8c4127685a1529f062ab884d9fa77fe59b22b3ff20234e23b5ee0d5db84ae9cfc472cda58ea7748cf63ec9c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
  All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
3
3
 
4
4
  - - -
5
+ ## 1.10.0 - 2025-09-12
6
+ #### Features
7
+ - Automatic Nested attributes - (dad38de) - Marino Bonetti
8
+
9
+ - - -
10
+
11
+ ## 1.9.0 - 2025-07-30
12
+ #### Bug Fixes
13
+ - Correct add is-invalid class on relation fields - (21e4a92) - Marino Bonetti
14
+ - Correct path generator for namespaced resources - (a4f72c8) - Marino Bonetti
15
+ - Migliore formattazione generatore - (1ceda4d) - Marino Bonetti
16
+ - Creazione dei nested attributi necessari con belongs to - (564f61b) - Marino Bonetti
17
+ - Correct Shared Specs - (1994128) - Marino Bonetti
18
+ - Authentication Model configurable (#19) - (cef424f) - Marino Bonetti
19
+ - Errore Traduzione di test - (95ff702) - Marino Bonetti
20
+ #### Documentation
21
+ - Add info on test matrix - (b097a01) - Marino Bonetti
22
+ #### Features
23
+ - Configurable test controller requests - (327163e) - Marino Bonetti
24
+ - In caso di molti attributi struttura multilinea - (21dfe09) - Marino Bonetti
25
+ - Add customizable title New and Edit - (d484e23) - Marino Bonetti
26
+ - Check model inheritance for partial search (#18) - (14f081f) - Marino Bonetti
27
+
28
+ - - -
29
+
5
30
  ## 1.8.1 - 2025-03-24
6
31
  #### Bug Fixes
7
32
  - Correct generator install multiple gems - (73948f5) - Marino Bonetti
data/README.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # BaseEditingBootstrap
2
+
2
3
  [![Gem Version](https://badge.fury.io/rb/base_editing_bootstrap.svg)](https://badge.fury.io/rb/base_editing_bootstrap)
3
4
 
4
- WIP
5
+ ### Active tested on:
6
+
7
+ * rails: 7.x,8.x
8
+ * ruby: 3.x
5
9
 
6
10
  ## Installation
11
+
7
12
  Add this line to your application's Gemfile:
8
13
 
9
14
  ```ruby
@@ -11,29 +16,46 @@ gem "base_editing_bootstrap"
11
16
  ```
12
17
 
13
18
  And then execute:
19
+
14
20
  ```bash
15
21
  $ bundle
16
22
  ```
17
23
 
18
24
  Or install it yourself as:
25
+
19
26
  ```bash
20
27
  $ gem install base_editing_bootstrap
21
28
  ```
22
29
 
23
30
  Then run installer:
31
+
24
32
  ```bash
25
33
  $ bundle exec rails g base_editing_bootstrap:install
26
34
  ```
27
35
 
28
36
  **Si presume quindi che ActiveStorage sia correttamente installato, completo del javascript per il direct upload**
29
37
 
38
+ ### Note for NestedAttributes
39
+
40
+ Seguire le istruzioni per installare anche NestedAttributeForm Controller per stimulus:
41
+
42
+ ```shell
43
+ bin/importmap pin @stimulus-components/rails-nested-form
44
+ ```
45
+
46
+ e seguire installazione https://www.stimulus-components.com/docs/stimulus-rails-nested-form
47
+
30
48
  ### Generators
49
+
31
50
  Then Install dependency (if you run base_editing_bootstrap:install you are good to go):
51
+
32
52
  ```bash
33
53
 
34
54
  bundle exec rails g pundit:install
35
55
  ```
36
- Aggiungere ad ApplicationController
56
+
57
+ Aggiungere ad ApplicationController
58
+
37
59
  ```ruby
38
60
  include Pundit::Authorization
39
61
  ```
@@ -46,40 +68,43 @@ documentazione e avrete la vostra versione di boostrap installata.
46
68
  Installare `gem "factory_bot_rails"`
47
69
 
48
70
  ### Initializers
71
+
49
72
  E' possibile configurare BaseEditingBootstrap con alcune impostazioni:
73
+
50
74
  ```ruby
51
75
  BaseEditingBootstrap.configure do |config|
52
- ##
53
- # Controller da cui derivare poi il BaseEditingController da cui derivano
54
- # tutti i controller sottostanti
55
- # @default "ApplicationController"
56
- # config.inherited_controller = 'ApplicationController'
57
-
58
- ##
59
- # Configurazione per alterare lo standard di azione post aggiornamento record
60
- # il default è andare nella pagina di editing del record
61
- # possibili valori :edit , :index
62
- # config_accessor :after_success_update_redirect, default: :edit
63
-
64
- ##
65
- # Configurazione per alterare lo standard di azione post creazione record
66
- # il default è andare nella pagina di editing del record
67
- # possibili valori :edit , :index
68
- # config_accessor :after_success_create_redirect, default: :edit
69
-
70
- end
76
+ ##
77
+ # Controller da cui derivare poi il BaseEditingController da cui derivano
78
+ # tutti i controller sottostanti
79
+ # @default "ApplicationController"
80
+ # config.inherited_controller = 'ApplicationController'
81
+
82
+ ##
83
+ # Configurazione per alterare lo standard di azione post aggiornamento record
84
+ # il default è andare nella pagina di editing del record
85
+ # possibili valori :edit , :index
86
+ # config_accessor :after_success_update_redirect, default: :edit
87
+
88
+ ##
89
+ # Configurazione per alterare lo standard di azione post creazione record
90
+ # il default è andare nella pagina di editing del record
91
+ # possibili valori :edit , :index
92
+ # config_accessor :after_success_create_redirect, default: :edit
93
+
94
+ end
71
95
 
72
96
  ```
73
97
 
74
98
  ## Usage
99
+
75
100
  Utilizzo per modello base, in questo esempio prendiamo come modello Post come esempio del dummy.
76
101
 
77
- - Creare il Modello ed includere
102
+ - Creare il Modello ed includere
78
103
  ```ruby
79
104
  include BaseEditingBootstrap::BaseModel
80
105
  ```
81
106
  - La factory nelle spec deve contenere il trait `with_invalid_attributes` per definire la situazione di dati per record
82
- non valido. ES:
107
+ non valido. ES:
83
108
  ```ruby
84
109
  trait :with_invalid_attributes do
85
110
  name {nil} # name dovrebbe essere obbligatorio nel modello
@@ -124,13 +149,13 @@ Utilizzo per modello base, in questo esempio prendiamo come modello Post come es
124
149
  ```shell
125
150
  rails g base_editing_bootstrap:field_override ModelName field1 field2:type
126
151
  ```
127
- - è possibile customizzare
128
- - un text help per ogni campo andando ad aggiungere nelle traduzioni la relativa
152
+ - è possibile customizzare
153
+ - un text help per ogni campo andando ad aggiungere nelle traduzioni la relativa
129
154
  traduzione nella posizione: `it.activerecord.attributes.MODEL.FIELD/help_text` oppure `help_text_html` in caso di
130
155
  contenuto con html
131
- - un blocco per l'unità di misura accanto al campo aggiungendo alle traduzioni:
156
+ - un blocco per l'unità di misura accanto al campo aggiungendo alle traduzioni:
132
157
  `it.activerecord.attributes.MODEL.FIELD/unit`
133
-
158
+
134
159
  - [OPTIONAL] la medesima cosa è possibile fare con il rendering dei campi
135
160
  delle celle della tabella
136
161
  ```shell
@@ -146,48 +171,51 @@ Utilizzo per modello base, in questo esempio prendiamo come modello Post come es
146
171
  **Cell Field**:
147
172
  - created_at => timestamps.html.erb
148
173
  - updated_at => timestamps.html.erb
149
- - Enum => _enum.html.erb
174
+ - Enum => _enum.html.erb
150
175
  Per gli enum, le traduzioni dei labels di ogni valore provengono da i18n
151
176
  attraverso l'helper: `Utilities::EnumHelper#enum_translation` con variant `:cell_field`
152
177
  il quale sfrutta human_attribute_name del modello con 'attributo.enum_value',
153
178
  quindi ad esempio per un modello `Post` con enum `categoria` e un enum `importante`, la ricerca nelle traduzioni
154
179
  saranno così composte:
155
180
  - it.activerecord.attributes.post/categoria.importante_cell_field
156
- - it.activerecord.attributes.categoria.importante_cell_field
181
+ - it.activerecord.attributes.categoria.importante_cell_field
157
182
  - it.attributes.importante_cell_field
158
183
  - it.activerecord.attributes.post/categoria.importante
159
184
  - it.activerecord.attributes.categoria.importante
160
185
  - it.attributes.importante => nil
161
- - default => base.html.erb
162
-
186
+ - default => base.html.erb
187
+
163
188
  **Form Field**
164
- - Integer => _integer.html.erb
165
- - Float => _decimal.html.erb
166
- - Decimal => _decimal.html.erb
167
- - DateTime => _datetime.html.erb
168
- - Date => _date.html.erb
169
- - Boolean => _boolean.html.erb
170
- - Enum => _enum.html.erb
189
+ - Integer => _integer.html.erb
190
+ - Float => _decimal.html.erb
191
+ - Decimal => _decimal.html.erb
192
+ - DateTime => _datetime.html.erb
193
+ - Date => _date.html.erb
194
+ - Boolean => _boolean.html.erb
195
+ - Enum => _enum.html.erb
171
196
  Per gli enum, le traduzioni dei labels di ogni valore provengono da i18n
172
197
  attraverso l'helper: `Utilities::EnumHelper#enum_translation`" con variante `:form_field`
173
- il quale sfrutta human_attribute_name del modello con 'attributo.enum_value',
198
+ il quale sfrutta human_attribute_name del modello con 'attributo.enum_value',
174
199
  quindi ad esempio per un modello `Post` con enum `categoria` e un enum `importante`, la ricerca nelle traduzioni
175
- saranno così composte:
200
+ saranno così composte:
176
201
  - it.activerecord.attributes.post/categoria.importante_form_field
177
202
  - it.activerecord.attributes.categoria.importante_form_field
178
203
  - it.attributes.importante_form_field
179
204
  - it.activerecord.attributes.post/categoria.importante
180
205
  - it.activerecord.attributes.categoria.importante
181
206
  - it.attributes.importante => nil
182
- - belongs_to => _belongs_to_select.html.erb
207
+ - belongs_to => _belongs_to_select.html.erb
183
208
  Come si può leggere dal partial, il modello che viene utilizzato come base dati per la collection deve
184
209
  avere come metodo `option_label` che deve ritornare la label da utilizzare nelle options.
185
210
  Di default questo metodo utilizza il semplice #to_s
186
211
  Ha anche un metodo per il valore da utilizzare come chiave, di default viene dedotto dalla reflection
187
212
  come anche il nome della classe da utilizzare come sorgente dei dati della collection
188
- - Default/String => _base.html.erb
189
-
190
- In futuro si prevede di aggiungere automatismi per renderizzare senza
213
+ - accept_nested_field => _accept_nested_field.html.erb
214
+ Questo partial renderizza una tabella per i campi associati al modello.
215
+ Più informazioni nelle note per il [nested attributes](#nested-attributes)
216
+ - Default/String => _base.html.erb
217
+
218
+ In futuro si prevede di aggiungere automatismi per renderizzare senza
191
219
  l'intervento dell'utente dei campi.
192
220
  - [OPTIONAL] Search Form:
193
221
  Per poter aggiungere una form di ricerca basta aggiungere alla policy
@@ -203,22 +231,40 @@ Utilizzo per modello base, in questo esempio prendiamo come modello Post come es
203
231
  #...
204
232
  ```
205
233
 
234
+ ### Nested Attributes
235
+
236
+ Il funzionamento si basa completamente sul sistema di NestedAttributes
237
+ di [Rails](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html)
238
+ Note:
239
+
240
+ - Nelle policy bisogna definire come campo da editare il nome della relazione/nested_attribute
241
+ - Nella policy bisogna inoltre definire per i permitted_attributes anche il
242
+ `XXXXX_attributes => [Array attributi da editare] + [:id,:_destroy]`
243
+ - Il permitted _destroy vale solamente nel caso in cui si definisca nei nested_attributes che debba essere cancellabile.
244
+ - I campi visualizzati del modello sono presi dalla relativa policy.
245
+
246
+ Fai riferimento all'implementazione di esempio del dummy `Company->addresses`
247
+
206
248
  ### Translations
249
+
207
250
  Traduzioni disponibili:
208
- Per i bottoni della index, è possibile eseguire l'override del testo presente nel bottone.
251
+ Per i bottoni della index, è possibile eseguire l'override del testo presente nel bottone.
209
252
  Leggere la documentazione nel file `app/helpers/base_editing_helper.rb#translate_with_controller_scoped`
210
253
 
211
-
212
254
  ## Testing helpers
213
255
 
214
256
  ### Requirements(installed with generators)
257
+
215
258
  ```ruby
216
259
  group :test do
217
260
  gem 'rails-controller-testing'
218
261
  end
219
262
  ```
220
- ### Usage
263
+
264
+ ### Usage
265
+
221
266
  Controllers:
267
+
222
268
  ```ruby
223
269
  require 'rails_helper'
224
270
  RSpec.describe "ServiceControllers", type: :request do
@@ -227,7 +273,9 @@ RSpec.describe "ServiceControllers", type: :request do
227
273
  end
228
274
  end
229
275
  ```
276
+
230
277
  Model:
278
+
231
279
  ```ruby
232
280
  require 'rails_helper'
233
281
  RSpec.describe Service, type: :model do
@@ -236,7 +284,9 @@ RSpec.describe Service, type: :model do
236
284
  ransack_permitted_associations: []
237
285
  end
238
286
  ```
287
+
239
288
  Policy
289
+
240
290
  ```ruby
241
291
  require 'rails_helper'
242
292
  ##
@@ -262,10 +312,11 @@ end
262
312
  ```
263
313
 
264
314
  ## Message translations
315
+
265
316
  I messaggi di generati per il flash provengono dal metodo BaseEditingBootstrap::ActionTranslation.human_action_message
266
317
  e seguono una logica simile ad human_attribute_name.
267
318
  Sono già presenti i messaggi di default, a cui viene passato il nome del modello,
268
- ma è possibile fare override del messaggio con la classe:
319
+ ma è possibile fare override del messaggio con la classe:
269
320
 
270
321
  ```yaml
271
322
  LANG:
@@ -279,22 +330,24 @@ LANG:
279
330
  created: "customized %{model} created"
280
331
  unsuccessful:
281
332
  messages:
282
- created:
333
+ created:
283
334
  updated:
284
335
  ```
285
336
 
337
+ ## Contributing
286
338
 
339
+ 1. Setup env with:
287
340
 
288
- ## Contributing
289
- 1. Setup env with:
290
341
  ```shell
291
342
  docker compose run app spec/dummy/bin/setup
292
343
  ```
293
344
 
294
- 2. Start environment with:
345
+ 2. Start environment with:
346
+
295
347
  ```shell
296
348
  docker compose up
297
349
  ```
298
350
 
299
351
  ## License
352
+
300
353
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -24,14 +24,20 @@ module Utilities
24
24
  type = :enum
25
25
  generic_field = "enum"
26
26
  elsif form.object.class.respond_to?(:reflect_on_association) &&
27
- form.object.class.reflect_on_association(field.to_s).is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
28
- !form.object.class.reflect_on_association(field.to_s).polymorphic? # non deve essere polymorphic
27
+ form.object.class.reflect_on_association(field.to_s).is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
28
+ !form.object.class.reflect_on_association(field.to_s).polymorphic? # non deve essere polymorphic
29
29
  # Abbiamo una relazione belongs_to da gestire
30
30
  reflection = form.object.class.reflect_on_association(field.to_s)
31
31
  type = :belongs_to
32
32
  generic_field = "belongs_to_select"
33
33
  locals[:relation_class] = reflection.klass
34
34
  locals[:foreign_key] = reflection.foreign_key
35
+ elsif form.object.class.respond_to?(:nested_attributes_options) &&
36
+ form.object.class.nested_attributes_options.key?(field.to_sym)
37
+ type= :nested_attributes
38
+ generic_field = "accept_nested_field"
39
+ reflection = form.object.class.reflect_on_association(field.to_s)
40
+ locals[:new_object] = reflection.klass.new(reflection.foreign_key => form.object)
35
41
  else
36
42
  if form.object.class.respond_to?(:type_for_attribute)
37
43
  type = form.object.class.type_for_attribute(field).type
@@ -1,13 +1,22 @@
1
1
  module Utilities::PageHelper
2
2
  include Utilities::IconHelper
3
+
4
+ ##
5
+ # Traduzione del titolo EDIT con possibilità di modificare intestazione rispetto a modello
6
+ # - Il default è quello di Utilizzare la chiave .edit
7
+ # - Viene cercato la traduzione con la chiave titles.CHIAVE_I18N_MODELLO.edit
3
8
  # @param [BaseModel] base_class
4
9
  def title_mod_g(base_class)
5
- "#{t("edit")} #{base_class.model_name.human}"
10
+ "#{t("titles.#{base_class.model_name.i18n_key}.edit", default: :edit)} #{base_class.model_name.human}"
6
11
  end
7
12
 
13
+ ##
14
+ # Traduzione del titolo NUOVO con possibilità di modificare intestazione rispetto a modello
15
+ # - Il default è quello di Utilizzare la chiave .new
16
+ # - Viene cercato la traduzione con la chiave titles.CHIAVE_I18N_MODELLO.new
8
17
  # @param [BaseModel] base_class
9
18
  def title_new_g(base_class)
10
- "#{t("new")} #{base_class.model_name.human}"
19
+ "#{t("titles.#{base_class.model_name.i18n_key}.new", default: :new)} #{base_class.model_name.human}"
11
20
  end
12
21
 
13
22
  # Quando e se servirà verrà testato:
@@ -15,20 +15,34 @@ module Utilities::TemplateHelper
15
15
  def find_template_with_fallbacks(obj, field, base_path, generic_field)
16
16
  # nei casi in cui passiamo la classe e non l'oggetto, dobbiamo utilizzare un metodo interno a rails per
17
17
  # avere la partial_path
18
+
19
+ obj_base_paths = []
20
+ # Primo livello in cui troviamo la partial path rispetto ad istanza o classe
18
21
  partial_path = (obj.respond_to? :to_partial_path) ? obj.to_partial_path : obj._to_partial_path
19
- obj_base_path = "#{partial_path}/#{base_path}"
22
+ obj_base_paths << "#{partial_path}/#{base_path}"
23
+
24
+ # Cerchiamo anche tutti i livelli di inheritance del modello
25
+ start_class = ((obj.respond_to? :to_partial_path) ? obj.class : obj).superclass
26
+
27
+ while start_class < ApplicationRecord
28
+ partial_path = start_class._to_partial_path
29
+ obj_base_paths << "#{partial_path}/#{base_path}"
30
+ start_class = start_class.superclass
31
+ end
20
32
 
21
33
  [
22
34
  # Precedenza modello e campo specifico
23
- [field, [obj_base_path]],
35
+ ["Campo SPECIFICO + inheritance tra modelli", field, obj_base_paths],
24
36
  # cerco tramite nome modello semplice, con namespace della risorsa (cell_field,header_field,form_field) e nome del campo specifico
25
- ["#{obj.model_name.element}/#{base_path}/#{field}", lookup_context.prefixes],
37
+ ["Campo specifico con nome modello + inheritance controllers", "#{obj.model_name.element}/#{base_path}/#{field}", lookup_context.prefixes],
38
+ # cerco struttura senza il livello del nome del modello
39
+ ["Campo specifico senza nome modello + inheritance controllers", "#{base_path}/#{field}", lookup_context.prefixes],
26
40
  # Ricerca tramite campo generico e prefissi di contesto che contiene anche controller e namespace di controller
27
- ["#{base_path}/#{generic_field}", lookup_context.prefixes],
28
- [generic_field, [obj_base_path]],
29
- ["base_editing/#{base_path}/#{generic_field}", []],
30
- ].each do |partial, prefixes|
31
- Rails.logger.debug { "[BASE EDITING BOOTSTRAP] Cerco partial:`#{partial}` in #{prefixes.inspect}" }
41
+ ["Campo GENERICO + inheritance controllers", "#{base_path}/#{generic_field}", lookup_context.prefixes],
42
+ ["Campo GENERICO + inheritance tra modelli", generic_field, obj_base_paths],
43
+ ["Default BaseEditingController", "base_editing/#{base_path}/#{generic_field}", []],
44
+ ].each do |desc,partial, prefixes|
45
+ Rails.logger.debug { "[BASE EDITING BOOTSTRAP] #{desc} - partial:`#{partial}` in #{prefixes.inspect}" }
32
46
  if lookup_context.exists?(partial, prefixes, true)
33
47
  return lookup_context.find(partial, prefixes, true)
34
48
  end
@@ -0,0 +1,16 @@
1
+ <tr class="nested-form-wrapper" data-new-record="<%= form.object.new_record? %>" id="<%= dom_id(form.object) %>">
2
+
3
+ <% policy(form.object).editable_attributes.each do |field| %>
4
+ <td>
5
+ <%= form_print_field(form, field) %>
6
+ <%= error_messages_for(form.object, field) %>
7
+ </td>
8
+
9
+ <% end %>
10
+
11
+ <td class="d-flex justify-content-end">
12
+ <button type="button" class="btn btn-outline-danger" data-action="nested-form#remove"><%= icon("trash") %></button>
13
+ <%= form.hidden_field :_destroy %>
14
+ </td>
15
+
16
+ </tr>
@@ -0,0 +1,48 @@
1
+ <%# locals: (form:, field:,new_object:) -%>
2
+ <%= content_tag :table, class: "table", data: {controller: 'nested-form'} do %>
3
+
4
+ <thead>
5
+ <tr>
6
+ <% policy(new_object).editable_attributes.each do |field| %>
7
+ <th>
8
+ <%= new_object.class.human_attribute_name(field) %>
9
+ </th>
10
+ <% end %>
11
+ <th class="text-end">
12
+ <button type="button" data-action="nested-form#add" class="btn btn-outline-success"><%= icon("plus-lg") %></button>
13
+ </th>
14
+ </tr>
15
+ </thead>
16
+
17
+ <template data-nested-form-target="template">
18
+ <%= form.fields_for field, new_object, child_index: 'NEW_RECORD' do |form_for_sections| %>
19
+ <%= render "nested_row_form", form: form_for_sections %>
20
+ <% end %>
21
+ </template>
22
+
23
+ <tbody>
24
+
25
+ <%= form.fields_for field do |form_for_sections| %>
26
+ <%= render "nested_row_form", form: form_for_sections %>
27
+ <% end %>
28
+
29
+ <!-- Inserted elements will be injected before that target. -->
30
+ <tr data-nested-form-target="target"></tr>
31
+
32
+ </tbody>
33
+
34
+
35
+ <% end %>
36
+
37
+ <% if form.object.errors.key?(field) %>
38
+
39
+ <div class="row">
40
+ <div class="col-12">
41
+ <div class="alert alert-danger">
42
+ <%= form.object.errors[field].join(", ") %>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <% end %>
48
+
@@ -78,8 +78,4 @@ it:
78
78
  i_cont: "contiene"
79
79
  base_editing:
80
80
  form_base_errors:
81
- title: Presenti errori generici
82
- post:
83
- base_editing:
84
- form_base_errors:
85
- title: Presenti errori per il modello POST
81
+ title: Presenti errori generici
@@ -1 +1 @@
1
- 1.8.1
1
+ 1.10.0
@@ -25,7 +25,7 @@ module BaseEditingBootstrap
25
25
  if auth_object
26
26
  Pundit.policy(auth_object, self.new).permitted_attributes_for_ransack.map(&:to_s)
27
27
  else
28
- Pundit.policy(User.new, self.new).permitted_attributes_for_ransack.map(&:to_s)
28
+ Pundit.policy(BaseEditingBootstrap.authentication_model.new, self.new).permitted_attributes_for_ransack.map(&:to_s)
29
29
  end
30
30
  end
31
31
 
@@ -33,7 +33,7 @@ module BaseEditingBootstrap
33
33
  if auth_object
34
34
  Pundit.policy(auth_object, self.new).permitted_associations_for_ransack.map(&:to_s)
35
35
  else
36
- Pundit.policy(User.new, self.new).permitted_associations_for_ransack.map(&:to_s)
36
+ Pundit.policy(BaseEditingBootstrap.authentication_model.new, self.new).permitted_associations_for_ransack.map(&:to_s)
37
37
  end
38
38
  end
39
39
 
@@ -41,7 +41,7 @@ module BaseEditingBootstrap
41
41
  if auth_object
42
42
  Pundit.policy(auth_object, self.new).permitted_scopes_for_ransack.map(&:to_s)
43
43
  else
44
- Pundit.policy(User.new, self.new).permitted_scopes_for_ransack.map(&:to_s)
44
+ Pundit.policy(BaseEditingBootstrap.authentication_model.new, self.new).permitted_scopes_for_ransack.map(&:to_s)
45
45
  end
46
46
  end
47
47
  end
@@ -26,7 +26,13 @@ module BaseEditingBootstrap::Forms
26
26
  #
27
27
  def form_style_class_for(method, options = {}, base_classes: ["form-control"])
28
28
  classes = base_classes
29
- classes << "is-invalid" if object.errors && object.errors.include?(method)
29
+ if object.errors
30
+ classes << "is-invalid" if object.errors.include?(method)
31
+ # caso in cui il metodo è una relazione
32
+ if method.to_s.match(/(.*)_id\z/)
33
+ classes << "is-invalid" if object.errors.include?(Regexp.last_match(1))
34
+ end
35
+ end
30
36
  classes << options[:class].split(" ") if options[:class]
31
37
  classes.flatten.compact.uniq.join(" ")
32
38
  end
@@ -47,7 +53,7 @@ module BaseEditingBootstrap::Forms
47
53
 
48
54
  def select(method, choices = nil, options = {}, html_options = {}, &block)
49
55
  html_options.merge!(class: form_style_class_for(method, html_options, base_classes: ["form-control", "form-select"]))
50
- super(method, choices, options,html_options , &block)
56
+ super(method, choices, options, html_options, &block)
51
57
  end
52
58
 
53
59
  def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module BaseEditingBootstrap::GeneratorsHelpers
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+
10
+ private
11
+ def class_to_view_path(class_name)
12
+ base_path = ["app/views"]
13
+
14
+ base_path << class_name.deconstantize.then { |c| c.underscore.downcase }
15
+ singular_name = class_name.demodulize.underscore.downcase.singularize
16
+ base_path << singular_name.pluralize
17
+ base_path << singular_name
18
+ base_path.compact_blank!
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
@@ -36,8 +36,20 @@ module BaseEditingBootstrap
36
36
  # possibili valori :edit , :index
37
37
  config_accessor :after_success_create_redirect, default: :edit
38
38
 
39
+ ##
40
+ # Classe che rappresenta l'utente, solitamente User
41
+ config_accessor :authentication_model_class, default: "User"
42
+
43
+ def self.authentication_model
44
+ self.authentication_model_class.constantize
45
+ end
46
+
47
+ ##
48
+ # Factory per la creazione del modello che rappresenta l'auteticazione
49
+ config_accessor :authentication_model_factory, default: :user
50
+
39
51
  def self.deprecator
40
- @deprecator ||= ActiveSupport::Deprecation.new("1.0", "BaseEditingBootstrap")
52
+ @deprecator ||= ActiveSupport::Deprecation.new("2.0", "BaseEditingBootstrap")
41
53
  end
42
54
 
43
55
  end
@@ -3,6 +3,7 @@
3
3
  module BaseEditingBootstrap
4
4
  module Generators
5
5
  class CellOverrideGenerator < ::Rails::Generators::Base
6
+ include BaseEditingBootstrap::GeneratorsHelpers
6
7
  source_root File.expand_path("../../../../app/views/base_editing", __dir__)
7
8
  argument :name, type: :string, banner: "Post", required: true
8
9
  argument :attributes, type: :array, default: [], banner: "field field:type"
@@ -20,15 +21,15 @@ module BaseEditingBootstrap
20
21
  if attributes.empty?
21
22
  say "Need one field"
22
23
  else
23
- singular_name = name.underscore.downcase.singularize
24
- plural_name = singular_name.pluralize
24
+ base_path = class_to_view_path(name)
25
+
25
26
  attributes.each do |a|
26
27
  attr_name, type = a.split(":")
27
28
 
28
29
  type = :base if type.nil?
29
30
  type = type.to_sym
30
31
  raise "Type #{type} not found in #{TYPES}" unless TYPES.include?(type)
31
- copy_file "cell_field/_#{type}.html.erb", File.join("app/views", plural_name, singular_name, 'cell_field', "_#{attr_name}.html.erb")
32
+ copy_file "cell_field/_#{type}.html.erb", File.join(*base_path, 'cell_field', "_#{attr_name}.html.erb")
32
33
  end
33
34
  end
34
35
 
@@ -3,6 +3,7 @@
3
3
  module BaseEditingBootstrap
4
4
  module Generators
5
5
  class FieldOverrideGenerator < ::Rails::Generators::Base
6
+ include BaseEditingBootstrap::GeneratorsHelpers
6
7
  source_root File.expand_path("../../../../app/views/base_editing", __dir__)
7
8
  argument :name, type: :string, banner: "Post", required: true
8
9
  argument :attributes, type: :array, default: [], banner: "field field:type"
@@ -20,15 +21,16 @@ module BaseEditingBootstrap
20
21
  if attributes.empty?
21
22
  say "Need one field"
22
23
  else
23
- singular_name = name.underscore.downcase.singularize
24
- plural_name = singular_name.pluralize
24
+
25
+ base_path = class_to_view_path(name)
26
+
25
27
  attributes.each do |a|
26
28
  attr_name, type = a.split(":")
27
29
 
28
30
  type = :base if type.nil?
29
31
  type = type.to_sym
30
32
  raise "Type #{type} not found in #{TYPES}" unless TYPES.include?(type)
31
- copy_file "form_field/_#{type}.html.erb", File.join("app/views", plural_name, singular_name, 'form_field', "_#{attr_name}.html.erb")
33
+ copy_file "form_field/_#{type}.html.erb", File.join(*base_path,"form_field", "_#{attr_name}.html.erb")
32
34
  end
33
35
  end
34
36
 
@@ -3,6 +3,8 @@
3
3
  module BaseEditingBootstrap
4
4
  module Generators
5
5
  class HeaderOverrideGenerator < ::Rails::Generators::Base
6
+ include BaseEditingBootstrap::GeneratorsHelpers
7
+
6
8
  source_root File.expand_path("../../../../app/views/base_editing", __dir__)
7
9
  argument :name, type: :string, banner: "Post", required: true
8
10
  argument :attributes, type: :array, default: [], banner: "field field:type"
@@ -20,15 +22,15 @@ module BaseEditingBootstrap
20
22
  if attributes.empty?
21
23
  say "Need one field"
22
24
  else
23
- singular_name = name.underscore.downcase.singularize
24
- plural_name = singular_name.pluralize
25
+ base_path = class_to_view_path(name)
26
+
25
27
  attributes.each do |a|
26
28
  attr_name, type = a.split(":")
27
29
 
28
30
  type = :base if type.nil?
29
31
  type = type.to_sym
30
32
  raise "Type #{type} not found in #{TYPES}" unless TYPES.include?(type)
31
- copy_file "header_field/_#{type}.html.erb", File.join("app/views", plural_name, singular_name, 'header_field', "_#{attr_name}.html.erb")
33
+ copy_file "header_field/_#{type}.html.erb", File.join(*base_path, 'header_field', "_#{attr_name}.html.erb")
32
34
  end
33
35
  end
34
36
 
@@ -30,6 +30,16 @@ module BaseEditingBootstrap
30
30
  gem 'rails-controller-testing', group: :test, comment: "Required if used with controllers spec"
31
31
  end
32
32
  end
33
+
34
+ def install_nested_attributes_dependencies
35
+ # attualmente penso sia più sensato semplicemente scrivere a video i passaggi necessari, dato che
36
+ # potrebbe essere già presente importmap, nested_attribute_controller e le varie configurazioni
37
+
38
+ say "Install dependencies for nested attributes:"
39
+ say " bin/importmap pin @stimulus-components/rails-nested-form"
40
+ say "Attiva quindi come spiegato qua: https://www.stimulus-components.com/docs/stimulus-rails-nested-form il controller"
41
+
42
+ end
33
43
  end
34
44
  end
35
45
  end
@@ -19,4 +19,12 @@ BaseEditingBootstrap.configure do |config|
19
19
  # possibili valori :edit , :index
20
20
  # config.after_success_create_redirect = :edit
21
21
 
22
+ ##
23
+ # Classe che rappresenta l'utente, solitamente User
24
+ # config.authentication_model_class= "User"
25
+
26
+ ##
27
+ # Factory per la creazione del modello che rappresenta l'auteticazione
28
+ # config.authentication_model_factory= :user
29
+
22
30
  end
@@ -1,17 +1,33 @@
1
1
  class <%= class_name %>Policy < BaseModelPolicy
2
2
 
3
- def editable_attributes = %i[<%= attributes_names.join(" ") %>]
4
- def permitted_attributes = %i[<%= attributes_names.join(" ") %>]
5
- def search_result_fields = %i[<%= attributes_names.join(" ") %>]
3
+ <%- if attributes_names.size > 3 -%>
4
+ <%- ["permitted_attributes","editable_attributes","search_result_fields"].each do |meth| -%>
5
+ def <%= meth %>
6
+ [
7
+ <%- attributes_names.each do |m| -%>
8
+ :<%= m %>,
9
+ <%- end -%>
10
+ ]
11
+ end
12
+
13
+ <%- end -%>
14
+ <%- else -%>
15
+ <%- ["permitted_attributes ","editable_attributes","search_result_fields"].each do |meth| -%>
16
+ def <%= meth %> = %i[<%= attributes_names.join(" ") %>]
17
+ <%- end -%>
18
+ <%- end -%>
6
19
  <%- if @search_attrs.any? -%>
20
+
7
21
  def search_fields
8
22
  %i[<%= @search_attrs.join(" ") %>]
9
23
  end
10
24
  <%- end -%>
11
25
  <%- if @permitted_attributes.any? -%>
26
+
12
27
  # TODO check if correct with search_fields
13
28
  def permitted_attributes_for_ransack
14
29
  %i[<%= @permitted_attributes.join(" ") %>]
15
30
  end
16
31
  <%- end -%>
32
+
17
33
  end
@@ -30,6 +30,8 @@ end
30
30
  # :url_for_create
31
31
  # :url_for_succ_delete
32
32
  # :url_for_fail_delete
33
+ # :url_for_succ_create
34
+ # :url_for_succ_update
33
35
  # :url_for_edit -> Rispetto agli altri questo risulta essere pià complicato in quanto
34
36
  # deve ritornare una proc a cui passiamo il valore dell'istanza persistente
35
37
  # che nei casi del after create non abbiamo a priori.
@@ -57,6 +59,27 @@ RSpec.shared_examples "base editing controller" do |factory: nil, only: [], exce
57
59
  let(:url_for_create) { url_for(model.new) }
58
60
  let(:url_for_succ_delete) { url_for(model) }
59
61
  let(:url_for_fail_delete) { url_for_succ_delete }
62
+
63
+ let(:url_for_succ_create) {
64
+ case BaseEditingBootstrap.after_success_create_redirect
65
+ when :index
66
+ url_for_index
67
+ else
68
+ # edit
69
+ url_for_edit.call(assigns[:object])
70
+ end
71
+ }
72
+
73
+ let(:url_for_succ_update) {
74
+ case BaseEditingBootstrap.after_success_update_redirect
75
+ when :index
76
+ url_for_index
77
+ else
78
+ # edit
79
+ url_for_edit.call(assigns[:object])
80
+ end
81
+ }
82
+
60
83
  let(:url_for_edit) { ->(p = persisted_instance) {
61
84
  url_for([p, action: :edit])
62
85
  } }
@@ -128,13 +151,7 @@ RSpec.shared_examples "base editing controller" do |factory: nil, only: [], exce
128
151
  put url_for_update, params: {param_key => valid_attributes}
129
152
  expect(assigns[:object]).to be_an_instance_of(model)
130
153
  expect(response).to have_http_status(303)
131
- case BaseEditingBootstrap.after_success_update_redirect
132
- when :index
133
- expect(response).to redirect_to(url_for_index)
134
- else
135
- # edit
136
- expect(response).to redirect_to(url_for_edit.call(assigns[:object]))
137
- end
154
+ expect(response).to redirect_to(url_for_succ_update)
138
155
  expect(flash.to_hash).to include("notice" => be_present)
139
156
  end
140
157
 
@@ -153,13 +170,7 @@ RSpec.shared_examples "base editing controller" do |factory: nil, only: [], exce
153
170
  post url_for_create, params: {param_key => valid_attributes}
154
171
  expect(assigns[:object]).to be_an_instance_of(model)
155
172
  expect(response).to have_http_status(303)
156
- case BaseEditingBootstrap.after_success_create_redirect
157
- when :index
158
- expect(response).to redirect_to(url_for_index)
159
- else
160
- # edit
161
- expect(response).to redirect_to(url_for_edit.call(assigns[:object]))
162
- end
173
+ expect(response).to redirect_to(url_for_succ_create)
163
174
  expect(flash.to_hash).to include("notice" => be_present)
164
175
  end
165
176
 
@@ -27,7 +27,7 @@ RSpec.shared_examples "a base model" do |ransack_permitted_attributes: [],
27
27
 
28
28
  ##
29
29
  # Oggetto solitamente di classe User che identifichi l'utente a cui eseguire il check dei permessi
30
- let(:auth_object) { :auth_object }
30
+ let(:auth_object) { BaseEditingBootstrap.authentication_model }
31
31
  let(:new_user_ransack_permitted_attributes) { ransack_permitted_attributes }
32
32
  let(:new_user_ransack_permitted_associations) { ransack_permitted_associations }
33
33
  let(:new_user_ransack_permitted_scopes) { ransack_permitted_scopes }
@@ -52,7 +52,7 @@ RSpec.shared_examples "a base model" do |ransack_permitted_attributes: [],
52
52
 
53
53
  let(:inner_auth_object) { nil }
54
54
  it "new user" do
55
- expect(Pundit).to receive(:policy).with(an_instance_of(User),
55
+ expect(Pundit).to receive(:policy).with(an_instance_of(BaseEditingBootstrap.authentication_model),
56
56
  an_instance_of(described_class)).and_call_original
57
57
 
58
58
  is_expected.to match_array(new_user_result)
@@ -13,6 +13,7 @@ module FactoryBot::Syntax::Methods
13
13
  klass = FactoryBot::Internal.factory_by_name(args.first).build_class
14
14
 
15
15
  klass.reflect_on_all_associations(:belongs_to).each do |r|
16
+ next if r.options.fetch(:optional, false)
16
17
  association = FactoryBot.create(r.class_name.underscore)
17
18
  attributes[:"#{r.name}_id"] = association.id
18
19
  attributes[:"#{r.name}_type"] = association.class.name if r.options[:polymorphic]
@@ -14,7 +14,7 @@ RSpec::Matchers.define :permit_editable_attributes do |*expected_attributes|
14
14
  end
15
15
 
16
16
  RSpec.shared_examples "a standard base model policy" do |factory, check_default_responses: false|
17
- let(:user) { create(:user) }
17
+ let(:user) { create(BaseEditingBootstrap.authentication_model_factory) }
18
18
  let(:instance) { described_class.new(user, build(factory)) }
19
19
 
20
20
  describe "response to all necessary methods" do
@@ -68,10 +68,10 @@ RSpec.shared_examples "a standard base model policy" do |factory, check_default_
68
68
  end
69
69
  end.flatten(1).compact.uniq
70
70
 
71
- elenco_campi_ordinabili_in_relazione.each do |relation,field|
71
+ elenco_campi_ordinabili_in_relazione.each do |relation, field|
72
72
  reflection = klass.reflect_on_association(relation.to_s)
73
73
  policy = Pundit.policy(instance.user, reflection.class_name.constantize.new)
74
- expect(policy.permitted_attributes_for_ransack.collect(&:to_sym).include?(field.to_sym)).to be_truthy, lambda{
74
+ expect(policy.permitted_attributes_for_ransack.collect(&:to_sym).include?(field.to_sym)).to be_truthy, lambda {
75
75
  "Mi aspetto che `#{policy.class.name}#permitted_attributes_for_ransack` includa `#{field}` per permettere l'ordinamento del campo tramite relazione"
76
76
  }
77
77
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: base_editing_bootstrap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marino Bonetti
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-24 00:00:00.000000000 Z
11
+ date: 2025-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -350,6 +350,7 @@ files:
350
350
  - app/views/base_editing/_index_main_buttons.html.erb
351
351
  - app/views/base_editing/_index_title_header.html.erb
352
352
  - app/views/base_editing/_navbar.html.erb
353
+ - app/views/base_editing/_nested_row_form.html.erb
353
354
  - app/views/base_editing/_new_page_title_header.html.erb
354
355
  - app/views/base_editing/_search.html.erb
355
356
  - app/views/base_editing/_search_field.erb
@@ -363,6 +364,7 @@ files:
363
364
  - app/views/base_editing/cell_field/_enum.html.erb
364
365
  - app/views/base_editing/cell_field/_timestamps.html.erb
365
366
  - app/views/base_editing/edit.html.erb
367
+ - app/views/base_editing/form_field/_accept_nested_field.html.erb
366
368
  - app/views/base_editing/form_field/_base.html.erb
367
369
  - app/views/base_editing/form_field/_belongs_to_select.html.erb
368
370
  - app/views/base_editing/form_field/_boolean.html.erb
@@ -394,6 +396,7 @@ files:
394
396
  - lib/base_editing_bootstrap/base_model.rb
395
397
  - lib/base_editing_bootstrap/engine.rb
396
398
  - lib/base_editing_bootstrap/forms/base.rb
399
+ - lib/base_editing_bootstrap/generators_helpers.rb
397
400
  - lib/base_editing_bootstrap/is_validated.rb
398
401
  - lib/base_editing_bootstrap/resource_finder.rb
399
402
  - lib/base_editing_bootstrap/searches/base.rb