chobble-forms 0.3.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3 -0
  3. data/lib/chobble-forms.rb +6 -0
  4. data/lib/engine.rb +21 -0
  5. data/lib/helpers.rb +206 -0
  6. data/lib/version.rb +3 -0
  7. data/views/chobble_forms/_auto_submit_select.html.erb +55 -0
  8. data/views/chobble_forms/_autocomplete_field.html.erb +17 -0
  9. data/views/chobble_forms/_checkbox.html.erb +12 -0
  10. data/views/chobble_forms/_comment.html.erb +40 -0
  11. data/views/chobble_forms/_comment_checkbox.html.erb +18 -0
  12. data/views/chobble_forms/_date_field.html.erb +15 -0
  13. data/views/chobble_forms/_decimal_comment.html.erb +58 -0
  14. data/views/chobble_forms/_display_field.html.erb +11 -0
  15. data/views/chobble_forms/_errors.html.erb +26 -0
  16. data/views/chobble_forms/_field_with_link.html.erb +33 -0
  17. data/views/chobble_forms/_fields.html.erb +9 -0
  18. data/views/chobble_forms/_fieldset.html.erb +29 -0
  19. data/views/chobble_forms/_file_field.html.erb +29 -0
  20. data/views/chobble_forms/_form_context.html.erb +74 -0
  21. data/views/chobble_forms/_header.html.erb +3 -0
  22. data/views/chobble_forms/_integer_comment.html.erb +58 -0
  23. data/views/chobble_forms/_number.html.erb +29 -0
  24. data/views/chobble_forms/_number_pass_fail_comment.html.erb +73 -0
  25. data/views/chobble_forms/_number_pass_fail_na_comment.html.erb +83 -0
  26. data/views/chobble_forms/_pass_fail.html.erb +14 -0
  27. data/views/chobble_forms/_pass_fail_comment.html.erb +47 -0
  28. data/views/chobble_forms/_pass_fail_na_comment.html.erb +49 -0
  29. data/views/chobble_forms/_radio_comment.html.erb +47 -0
  30. data/views/chobble_forms/_radio_pass_fail.html.erb +43 -0
  31. data/views/chobble_forms/_search_field.html.erb +15 -0
  32. data/views/chobble_forms/_select.html.erb +41 -0
  33. data/views/chobble_forms/_submit_button.html.erb +1 -0
  34. data/views/chobble_forms/_text_area.html.erb +22 -0
  35. data/views/chobble_forms/_text_field.html.erb +20 -0
  36. data/views/chobble_forms/_yes_no_radio.html.erb +15 -0
  37. data/views/chobble_forms/_yes_no_radio_comment.html.erb +48 -0
  38. metadata +186 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a667bfdadedd91552428ae7b8de49675340618beabd0a6784941861805a4655e
4
+ data.tar.gz: 6461e968376cf809af015f83dc89fd8cde783a79819c68faca28d3a938a945bb
5
+ SHA512:
6
+ metadata.gz: ac860f4f3d10aa3ab790b19a49871d8802a099ea4868f626888d2bd7dd29de2901ec604bd4f4fe31e7e47b327ac870d7565007c6ea18c0f6ac9c097e5d6429a3
7
+ data.tar.gz: 7c32149a54105636daffb3b88dc74327293cd3c0bd60abc4396ce256cac2562fa732fc1a17e66426c52947deb7251c8205bb60f1b2b6bad47f95021ec305149b
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Chobble Forms
2
+
3
+ Semantic Rails forms with strict i18n enforcement.
@@ -0,0 +1,6 @@
1
+ require_relative "version"
2
+ require_relative "engine"
3
+ require_relative "helpers"
4
+
5
+ module ChobbleForms
6
+ end
data/lib/engine.rb ADDED
@@ -0,0 +1,21 @@
1
+ module ChobbleForms
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ChobbleForms
4
+
5
+ initializer "chobble_forms.add_view_paths" do |app|
6
+ ActiveSupport.on_load(:action_controller) do
7
+ prepend_view_path ChobbleForms::Engine.root.join("views")
8
+ end
9
+ end
10
+
11
+ config.to_prepare do
12
+ ApplicationController.helper(ChobbleForms::Helpers)
13
+ end
14
+
15
+ initializer "chobble_forms.view_helpers" do
16
+ ActiveSupport.on_load(:action_view) do
17
+ include ChobbleForms::Helpers
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/helpers.rb ADDED
@@ -0,0 +1,206 @@
1
+ require "action_view"
2
+
3
+ module ChobbleForms
4
+ module Helpers
5
+ include ActionView::Helpers::NumberHelper
6
+ def form_field_setup(field, local_assigns)
7
+ validate_local_assigns(local_assigns)
8
+ validate_form_context
9
+
10
+ field_translations = build_field_translations(field)
11
+ value, prefilled = get_field_value_and_prefilled_status(@_current_form,
12
+ field)
13
+
14
+ build_field_setup_result(field_translations, value, prefilled)
15
+ end
16
+
17
+ def get_field_value_and_prefilled_status(form_object, field)
18
+ return [nil, false] unless form_object&.object
19
+ model = form_object.object
20
+ resolved = resolve_field_value(model, field)
21
+ [resolved[:value], resolved[:prefilled]]
22
+ end
23
+
24
+ def comment_field_options(form, comment_field, base_field_label)
25
+ raise ArgumentError, "form_object required" unless form
26
+ model = form.object
27
+
28
+ comment_value, comment_prefilled =
29
+ get_field_value_and_prefilled_status(
30
+ form,
31
+ comment_field
32
+ )
33
+
34
+ has_comment = comment_value.present?
35
+
36
+ base_field = comment_field.to_s.chomp("_comment")
37
+
38
+ placeholder_text = t("shared.field_comment_placeholder", field: base_field_label)
39
+ textarea_id = "#{base_field}_comment_textarea_#{model.object_id}"
40
+ checkbox_id = "#{base_field}_has_comment_#{model.object_id}"
41
+ display_style = has_comment ? "block" : "none"
42
+
43
+ {
44
+ options: {
45
+ rows: 2,
46
+ placeholder: placeholder_text,
47
+ id: textarea_id,
48
+ style: "display: #{display_style};",
49
+ value: comment_value
50
+ },
51
+ prefilled: comment_prefilled,
52
+ has_comment: has_comment,
53
+ checkbox_id: checkbox_id
54
+ }
55
+ end
56
+
57
+ def radio_button_options(prefilled, checked_value, expected_value)
58
+ (prefilled && checked_value == expected_value) ? {checked: true} : {}
59
+ end
60
+
61
+ private
62
+
63
+ ALLOWED_LOCAL_ASSIGNS = %i[
64
+ accept
65
+ field
66
+ max
67
+ min
68
+ number_options
69
+ options
70
+ preview_size
71
+ required
72
+ rows
73
+ step
74
+ type
75
+ ]
76
+
77
+ def validate_local_assigns(local_assigns)
78
+ if local_assigns[:field].present? &&
79
+ local_assigns[:field].to_s.match?(/^[A-Z]/)
80
+ raise ArgumentError, "Field names must be snake_case symbols, not class names. Use :field, not Field."
81
+ end
82
+
83
+ locally_assigned_keys = (local_assigns || {}).keys
84
+ disallowed_keys = locally_assigned_keys - ALLOWED_LOCAL_ASSIGNS
85
+
86
+ if disallowed_keys.any?
87
+ raise ArgumentError, "local_assigns contains #{disallowed_keys.inspect}"
88
+ end
89
+ end
90
+
91
+ def validate_form_context
92
+ raise ArgumentError, "missing i18n_base" unless @_current_i18n_base
93
+ raise ArgumentError, "missing form_object" unless @_current_form
94
+ end
95
+
96
+ def build_field_translations(field)
97
+ fields_key = "#{@_current_i18n_base}.fields.#{field}"
98
+ field_label = t(fields_key, raise: true)
99
+
100
+ base_parts = @_current_i18n_base.split(".")
101
+ root = base_parts[0..-2]
102
+ hint_key = (root + ["hints", field]).join(".")
103
+ placeholder_key = (root + ["placeholders", field]).join(".")
104
+
105
+ {
106
+ field_label:,
107
+ field_hint: t(hint_key, default: nil),
108
+ field_placeholder: t(placeholder_key, default: nil)
109
+ }
110
+ end
111
+
112
+ def build_field_setup_result(field_translations, value, prefilled)
113
+ {
114
+ form_object: @_current_form,
115
+ i18n_base: @_current_i18n_base,
116
+ value:,
117
+ prefilled:
118
+ }.merge(field_translations)
119
+ end
120
+
121
+ def resolve_field_value(model, field)
122
+ field_str = field.to_s
123
+
124
+ # Never return values for password fields
125
+ if field_str.include?("password")
126
+ return {value: nil, prefilled: false}
127
+ end
128
+
129
+ # Check current model value
130
+ current_value = model.send(field) if model.respond_to?(field)
131
+
132
+ # Check if this field should be excluded from prefilling
133
+ if defined?(InspectionsController::NOT_COPIED_FIELDS) &&
134
+ InspectionsController::NOT_COPIED_FIELDS.include?(field_str)
135
+ return {value: current_value, prefilled: false}
136
+ end
137
+
138
+ # Extract previous value if available
139
+ previous_value = extract_previous_value(@previous_inspection, model, field)
140
+
141
+ # Return previous value if current is nil and previous exists
142
+ if current_value.nil? && !previous_value.nil?
143
+ return {
144
+ value: format_numeric_value(previous_value),
145
+ prefilled: true
146
+ }
147
+ end
148
+
149
+ if field_str.end_with?("_id") && field_str != "id"
150
+ resolve_association_value(model, field_str)
151
+ else
152
+ # Always return current value, even if nil
153
+ {value: current_value, prefilled: false}
154
+ end
155
+ end
156
+
157
+ def extract_previous_value(previous_inspection, current_model, field)
158
+ if !previous_inspection
159
+ nil
160
+ elsif current_model.class.name.include?("Assessment")
161
+ assessment_type = current_model.class.name.demodulize.underscore
162
+ previous_model = previous_inspection.send(assessment_type)
163
+ previous_model&.send(field)
164
+ else
165
+ previous_inspection.send(field)
166
+ end
167
+ end
168
+
169
+ def format_numeric_value(value)
170
+ if value.is_a?(String) &&
171
+ value.match?(/\A-?\d*\.?\d+\z/) &&
172
+ (float_value = Float(value, exception: false))
173
+ value = float_value
174
+ end
175
+
176
+ return value unless value.is_a?(Numeric)
177
+
178
+ number_with_precision(
179
+ value,
180
+ precision: 4,
181
+ strip_insignificant_zeros: true
182
+ )
183
+ end
184
+
185
+ def resolve_association_value(model, field_str)
186
+ base_name = field_str.chomp("_id")
187
+ association_name = base_name.to_sym
188
+
189
+ if model.respond_to?(association_name)
190
+ {value: model.send(association_name), prefilled: true}
191
+ elsif model.respond_to?(field_str)
192
+ value = model.send(field_str)
193
+ if value && model.class.reflect_on_association(association_name)
194
+ associated = model.class
195
+ .reflect_on_association(association_name)
196
+ .klass.find_by(id: value)
197
+ {value: associated, prefilled: true}
198
+ else
199
+ {value: value, prefilled: true}
200
+ end
201
+ else
202
+ {value: nil, prefilled: false}
203
+ end
204
+ end
205
+ end
206
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module ChobbleForms
2
+ VERSION = "0.3.0"
3
+ end
@@ -0,0 +1,55 @@
1
+ <%
2
+ # Auto-submit select field options
3
+ field = local_assigns[:field] or raise ArgumentError, "field is required for auto submit select"
4
+ options = local_assigns[:options] or raise ArgumentError, "options is required for auto submit select"
5
+
6
+ # Check if we're in a form context with form object or need standalone
7
+ if local_assigns[:form]
8
+ form_object = local_assigns[:form]
9
+ selected_value = params[field] || form_object.object.send(field) rescue nil
10
+ else
11
+ # Standalone usage - must provide url
12
+ url = local_assigns[:url] or raise ArgumentError, "url is required for standalone auto submit select"
13
+ selected_value = params[field]
14
+ end
15
+
16
+ # Optional parameters
17
+ label = local_assigns[:label]
18
+ include_blank = local_assigns[:include_blank]
19
+ blank_text = local_assigns[:blank_text] || "All"
20
+ turbo_disabled = local_assigns.has_key?(:turbo_disabled) ? local_assigns[:turbo_disabled] : true
21
+ preserve_params = local_assigns[:preserve_params] || []
22
+ %>
23
+
24
+ <% if local_assigns[:form] %>
25
+ <% if label %>
26
+ <%= form_object.label field, label %>
27
+ <% end %>
28
+
29
+ <%= form_object.select field,
30
+ options_for_select(options, selected_value),
31
+ include_blank ? { include_blank: blank_text } : {},
32
+ { onchange: "this.form.submit();" } %>
33
+
34
+ <% else %>
35
+ <!-- Standalone auto-submit select form -->
36
+ <%= form_with url: url, method: :get, data: (turbo_disabled ? { turbo: false } : {}) do |form| %>
37
+
38
+ <!-- Preserve other parameters -->
39
+ <% preserve_params.each do |param| %>
40
+ <% if params[param].present? %>
41
+ <%= form.hidden_field param, value: params[param] %>
42
+ <% end %>
43
+ <% end %>
44
+
45
+ <% if label %>
46
+ <%= form.label field, label %>
47
+ <% end %>
48
+
49
+ <%= form.select field,
50
+ options_for_select(options, selected_value),
51
+ include_blank ? { include_blank: blank_text } : {},
52
+ { onchange: "this.form.submit();" } %>
53
+
54
+ <% end %>
55
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <%
2
+ setup = form_field_setup(field, local_assigns)
3
+
4
+ field_options = {
5
+ required: local_assigns[:required] || false,
6
+ list: "#{field}_autocomplete_#{setup[:form_object].object.object_id}",
7
+ value: setup[:value]
8
+ }
9
+ %>
10
+
11
+ <%= setup[:form_object].label field, setup[:field_label] %>
12
+ <%= setup[:form_object].text_field field, field_options %>
13
+ <datalist id="<%= field_options[:list] %>">
14
+ <% options&.each do |option| %>
15
+ <option value="<%= option %>"></option>
16
+ <% end %>
17
+ </datalist>
@@ -0,0 +1,12 @@
1
+ <%
2
+ # Get i18n_base from section (already validated there)
3
+ setup = form_field_setup(field, local_assigns)
4
+ %>
5
+
6
+ <%= setup[:form_object].label field do %>
7
+ <%= setup[:form_object].check_box field %>
8
+ <%= setup[:field_label] %>
9
+ <% if setup[:field_hint].present? %>
10
+ <small><%= setup[:field_hint] %></small>
11
+ <% end %>
12
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <%
2
+ # Form partial for comment fields with toggle visibility
3
+ # Required parameters:
4
+ # field: Symbol representing the field name
5
+ # Optional parameters:
6
+ # maxlength: Maximum character length (default: 1000)
7
+ # placeholder: Placeholder text
8
+
9
+ maxlength ||= 1000
10
+ placeholder ||= nil
11
+
12
+ form = @_current_form
13
+ checkbox_id = "toggle_#{field}_#{form.object.object_id}"
14
+ textarea_id = "#{form.object_name}_#{field}"
15
+
16
+ # Check if there's existing content
17
+ has_content = form.object.send(field).present?
18
+ %>
19
+
20
+ <div class="comment-field-container">
21
+ <label for="<%= checkbox_id %>">
22
+ <input type="checkbox" id="<%= checkbox_id %>"
23
+ <%= 'checked' if has_content %>
24
+ data-comment-toggle="<%= textarea_id %>"
25
+ data-comment-container="<%= textarea_id %>_container"
26
+ >
27
+ <%= t("shared.comment") %>
28
+ </label>
29
+
30
+ <div id="<%= textarea_id %>_container"
31
+ style="display: <%= has_content ? 'block' : 'none' %>"
32
+ >
33
+ <%= form.text_field field,
34
+ maxlength: maxlength,
35
+ placeholder: placeholder,
36
+ id: textarea_id,
37
+ title: "Max #{maxlength} characters"
38
+ %>
39
+ </div>
40
+ </div>
@@ -0,0 +1,18 @@
1
+ <%
2
+ # Partial for comment checkbox
3
+ # Required parameters:
4
+ # comment_field: The comment field name (e.g., :width_comment)
5
+ # checkbox_id: Unique ID for the checkbox
6
+ # textarea_id: ID of the textarea to show/hide
7
+ # has_comment: Whether comment exists
8
+ %>
9
+
10
+ <label for="<%= checkbox_id %>" class="comment-checkbox">
11
+ <input type="checkbox"
12
+ id="<%= checkbox_id %>"
13
+ <%= 'checked' if has_comment %>
14
+ data-comment-toggle="<%= textarea_id %>"
15
+ data-comment-container="<%= textarea_id %>"
16
+ >
17
+ <%= t('shared.comment') %>
18
+ </label>
@@ -0,0 +1,15 @@
1
+ <%
2
+ setup = form_field_setup(field, local_assigns)
3
+ required ||= false
4
+ field_options = {
5
+ placeholder: setup[:field_placeholder],
6
+ required: required
7
+ }.compact
8
+ field_options[:value] = local_assigns[:value] if local_assigns[:value]
9
+ %>
10
+
11
+ <%= setup[:form_object].label field, setup[:field_label] %>
12
+ <%= setup[:form_object].send(:date_field, field, field_options) %>
13
+ <% if setup[:field_hint].present? %>
14
+ <small><%= setup[:field_hint] %></small>
15
+ <% end %>
@@ -0,0 +1,58 @@
1
+ <%
2
+ # Composite partial for decimal + comment fields with grid layout
3
+ # Required parameters:
4
+ # field: Base field name (e.g., 'platform_height')
5
+ # Optional parameters:
6
+ # min: Minimum value
7
+ # max: Maximum value
8
+ # required: Whether the decimal field is required
9
+ # placeholder: Placeholder text
10
+ #
11
+ # This will render:
12
+ # - Decimal field: field (e.g., 'platform_height')
13
+ # - Comment field: field + '_comment' (e.g., 'platform_height_comment')
14
+
15
+ decimal_field = field
16
+ comment_field = "#{field}_comment".to_sym
17
+
18
+ form = @_current_form
19
+ model = form.object
20
+
21
+ field_data = form_field_setup(field, local_assigns)
22
+
23
+ decimal_options = {
24
+ inputmode: "decimal",
25
+ pattern: "[0-9]*[.]?[0-9]*",
26
+ placeholder: field_data[:field_placeholder] || local_assigns[:placeholder],
27
+ required: local_assigns[:required] || false,
28
+ class: "number"
29
+ }
30
+
31
+ if field_data[:prefilled]
32
+ decimal_options[:value] = field_data[:value]
33
+ end
34
+
35
+ decimal_options[:data] = {}
36
+ decimal_options[:data][:min] = local_assigns[:min] if local_assigns[:min]
37
+ decimal_options[:data][:max] = local_assigns[:max] if local_assigns[:max]
38
+
39
+ comment_info = comment_field_options(
40
+ form,
41
+ comment_field,
42
+ field_data[:field_label]
43
+ )
44
+ %>
45
+
46
+ <div class="form-grid number-comment" id="<%= decimal_field %>">
47
+ <%= form.label decimal_field, field_data[:field_label], class: "label" %>
48
+ <%= form.text_field decimal_field, decimal_options %>
49
+
50
+ <%= render 'chobble_forms/comment_checkbox',
51
+ comment_field: comment_field,
52
+ checkbox_id: comment_info[:checkbox_id],
53
+ textarea_id: comment_info[:options][:id],
54
+ has_comment: comment_info[:has_comment],
55
+ prefilled: comment_info[:prefilled] %>
56
+
57
+ <%= form.text_area comment_field, comment_info[:options] %>
58
+ </div>
@@ -0,0 +1,11 @@
1
+ <%
2
+ # Get i18n_base from section (already validated there)
3
+ setup = form_field_setup(field, local_assigns)
4
+
5
+ # Get value from model object automatically
6
+ model_object = setup[:form_object].object
7
+ value = model_object.send(field) if model_object.respond_to?(field)
8
+ %>
9
+
10
+ <%= setup[:form_object].label field, setup[:field_label] %>
11
+ <p><%= value %></p>
@@ -0,0 +1,26 @@
1
+ <%
2
+ # Form errors component - displays validation errors for any model object
3
+ model_object = local_assigns[:model] || local_assigns[:object]
4
+ raise ArgumentError, "model object is required for form errors" if model_object.nil?
5
+
6
+ # Use inherited i18n_base from form context
7
+ i18n_base = @_current_i18n_base
8
+ raise ArgumentError, "i18n_base is required for form errors" if i18n_base.nil?
9
+
10
+ # Custom header text or use i18n lookup - no fallbacks
11
+ header_text = local_assigns[:header] ||
12
+ t("#{i18n_base}.errors.header",
13
+ count: model_object.errors.count,
14
+ raise: true)
15
+ %>
16
+
17
+ <% if model_object.errors.any? %>
18
+ <aside class="form-errors" role="alert">
19
+ <h3><%= header_text %></h3>
20
+ <ul>
21
+ <% model_object.errors.each do |error| %>
22
+ <li><%= error.full_message %></li>
23
+ <% end %>
24
+ </ul>
25
+ </aside>
26
+ <% end %>
@@ -0,0 +1,33 @@
1
+ <%
2
+ # Field with optional link to the right
3
+ # Required locals:
4
+ # - field_type: :text_field or :autocomplete_field
5
+ # - field: field name
6
+ # - required: boolean
7
+ # Optional locals:
8
+ # - link_url: URL for the link
9
+ # - link_text: Text for the link
10
+ # - options: options for autocomplete_field
11
+
12
+ field_type = local_assigns[:field_type] || :text_field
13
+ field = local_assigns[:field]
14
+ required = local_assigns[:required] || false
15
+ link_url = local_assigns[:link_url]
16
+ link_text = local_assigns[:link_text]
17
+ options = local_assigns[:options] || []
18
+
19
+ has_link = link_url.present? && link_text.present?
20
+ css_classes = has_link ? "field field-with-link" : "field"
21
+ %>
22
+
23
+ <div class="<%= css_classes %>">
24
+ <% if field_type == :autocomplete_field %>
25
+ <%= render 'chobble_forms/autocomplete_field', field: field, required: required, options: options %>
26
+ <% else %>
27
+ <%= render 'chobble_forms/text_field', field: field, required: required %>
28
+ <% end %>
29
+
30
+ <% if has_link %>
31
+ <%= link_to link_text, link_url %>
32
+ <% end %>
33
+ </div>
@@ -0,0 +1,9 @@
1
+ <% model.class.form_fields(user: current_user).each do |fieldset| %>
2
+ <%= render 'chobble_forms/fieldset', legend_key: fieldset[:legend_i18n_key] do %>
3
+ <% fieldset[:fields].each do |field_config| %>
4
+ <% render_options = { field: field_config[:field] } %>
5
+ <% render_options.merge!(field_config[:attributes]) if field_config[:attributes] %>
6
+ <%= render "chobble_forms/#{field_config[:partial]}", render_options %>
7
+ <% end %>
8
+ <% end %>
9
+ <% end %>
@@ -0,0 +1,29 @@
1
+ <%
2
+ # Set form and i18n context for child form controls
3
+ @_current_form = local_assigns[:form] || @_current_form
4
+ i18n_base = @_current_i18n_base || local_assigns[:i18n_base]
5
+ raise ArgumentError, "i18n_base is required for form fieldsets" if i18n_base.nil?
6
+ @_current_i18n_base = i18n_base
7
+
8
+ # Determine legend text
9
+ if local_assigns[:legend]
10
+ legend_text = local_assigns[:legend]
11
+ elsif local_assigns[:legend_key] && i18n_base
12
+ # Remove .fields suffix if present to get to sections level
13
+ sections_base = i18n_base.sub(/\.fields$/, '')
14
+ legend_text = t("#{sections_base}.sections.#{local_assigns[:legend_key]}")
15
+ # Fail loudly if translation is missing
16
+ if legend_text.start_with?("translation missing:")
17
+ raise "Missing i18n key: #{sections_base}.sections.#{local_assigns[:legend_key]}"
18
+ end
19
+ else
20
+ legend_text = local_assigns[:legend_key]&.to_s&.humanize || "Section"
21
+ end
22
+ %>
23
+
24
+ <fieldset>
25
+ <% if legend_text.present? %>
26
+ <legend><%= legend_text %></legend>
27
+ <% end %>
28
+ <%= yield %>
29
+ </fieldset>
@@ -0,0 +1,29 @@
1
+ <%
2
+ # Get i18n_base from section (already validated there)
3
+ setup = form_field_setup(field, local_assigns)
4
+
5
+ # Form field options
6
+ accept = local_assigns[:accept] || "image/*"
7
+ preview_size = local_assigns[:preview_size] || 200
8
+ model = setup[:form_object].object
9
+ current_file = model.send(field) if model.respond_to?(field)
10
+ %>
11
+
12
+ <%= setup[:form_object].label field, setup[:field_label] %>
13
+ <%= setup[:form_object].file_field field, accept: accept %>
14
+
15
+ <% if current_file&.attached? %>
16
+ <div class="file-preview" style="margin-top: 10px;">
17
+ <% if current_file.image? %>
18
+ <%= image_tag current_file.variant(resize_to_limit: [preview_size, preview_size]),
19
+ alt: "Current #{setup[:field_label].downcase}" %>
20
+ <p><small>Current <%= setup[:field_label].downcase %>: <strong><%= current_file.filename %></strong></small></p>
21
+ <% else %>
22
+ <p>Current <%= setup[:field_label].downcase %>: <strong><%= current_file.filename %></strong></p>
23
+ <% end %>
24
+ </div>
25
+ <% end %>
26
+
27
+ <% if setup[:field_hint].present? %>
28
+ <small class="form-text"><%= setup[:field_hint] %></small>
29
+ <% end %>