satis 1.0.66

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 (101) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +92 -0
  4. data/Rakefile +23 -0
  5. data/app/assets/config/satis_manifest.js +1 -0
  6. data/app/assets/stylesheets/satis/application.css +15 -0
  7. data/app/components/satis/appearance_switcher/component.html.slim +6 -0
  8. data/app/components/satis/appearance_switcher/component.rb +11 -0
  9. data/app/components/satis/appearance_switcher/component.scss +34 -0
  10. data/app/components/satis/appearance_switcher/component_controller.js +62 -0
  11. data/app/components/satis/application_component.rb +50 -0
  12. data/app/components/satis/avatar/component.html.slim +7 -0
  13. data/app/components/satis/avatar/component.rb +52 -0
  14. data/app/components/satis/breadcrumbs/component.html.slim +8 -0
  15. data/app/components/satis/breadcrumbs/component.rb +23 -0
  16. data/app/components/satis/breadcrumbs/component.scss +19 -0
  17. data/app/components/satis/breadcrumbs/crumb.slim +8 -0
  18. data/app/components/satis/card/component.html.slim +54 -0
  19. data/app/components/satis/card/component.md +14 -0
  20. data/app/components/satis/card/component.rb +41 -0
  21. data/app/components/satis/card/component.scss +15 -0
  22. data/app/components/satis/date_time_picker/component.html.slim +48 -0
  23. data/app/components/satis/date_time_picker/component.md +11 -0
  24. data/app/components/satis/date_time_picker/component.rb +48 -0
  25. data/app/components/satis/date_time_picker/component.scss +5 -0
  26. data/app/components/satis/date_time_picker/component_controller.js +499 -0
  27. data/app/components/satis/dropdown/component.html.slim +36 -0
  28. data/app/components/satis/dropdown/component.md +48 -0
  29. data/app/components/satis/dropdown/component.rb +77 -0
  30. data/app/components/satis/dropdown/component.scss +10 -0
  31. data/app/components/satis/dropdown/component_controller.js +547 -0
  32. data/app/components/satis/flash_messages/component.html.slim +3 -0
  33. data/app/components/satis/flash_messages/component.rb +31 -0
  34. data/app/components/satis/flash_messages/component.scss +18 -0
  35. data/app/components/satis/flash_messages/message.html.slim +8 -0
  36. data/app/components/satis/info/component.html.slim +4 -0
  37. data/app/components/satis/info/component.rb +22 -0
  38. data/app/components/satis/info_item/component.html.slim +7 -0
  39. data/app/components/satis/info_item/component.rb +19 -0
  40. data/app/components/satis/input/component.html.slim +11 -0
  41. data/app/components/satis/input/component.rb +38 -0
  42. data/app/components/satis/input/component.scss +50 -0
  43. data/app/components/satis/input/element.html.slim +2 -0
  44. data/app/components/satis/map/component.html.slim +2 -0
  45. data/app/components/satis/map/component.rb +17 -0
  46. data/app/components/satis/map/component.scss +9 -0
  47. data/app/components/satis/map/component_controller.js +37 -0
  48. data/app/components/satis/menu/component.html.slim +13 -0
  49. data/app/components/satis/menu/component.md +1 -0
  50. data/app/components/satis/menu/component.rb +16 -0
  51. data/app/components/satis/menu/component_controller.js +62 -0
  52. data/app/components/satis/menu_item/component.html.slim +16 -0
  53. data/app/components/satis/menu_item/component.rb +14 -0
  54. data/app/components/satis/page/component.html.slim +45 -0
  55. data/app/components/satis/page/component.rb +15 -0
  56. data/app/components/satis/page/component_controller.js +86 -0
  57. data/app/components/satis/sidebar_menu/component.html.slim +3 -0
  58. data/app/components/satis/sidebar_menu/component.rb +17 -0
  59. data/app/components/satis/sidebar_menu/component.scss +0 -0
  60. data/app/components/satis/sidebar_menu/component_controller.js +9 -0
  61. data/app/components/satis/sidebar_menu/mobile/component.html.slim +3 -0
  62. data/app/components/satis/sidebar_menu/mobile/component.rb +10 -0
  63. data/app/components/satis/sidebar_menu_item/component.html.slim +15 -0
  64. data/app/components/satis/sidebar_menu_item/component.rb +20 -0
  65. data/app/components/satis/sidebar_menu_item/component.scss +27 -0
  66. data/app/components/satis/sidebar_menu_item/component_controller.js +62 -0
  67. data/app/components/satis/sidebar_menu_item/mobile/component.html.slim +17 -0
  68. data/app/components/satis/sidebar_menu_item/mobile/component.rb +10 -0
  69. data/app/components/satis/switch/component.html.slim +14 -0
  70. data/app/components/satis/switch/component.rb +24 -0
  71. data/app/components/satis/switch/component_controller.js +49 -0
  72. data/app/components/satis/tab/component.rb +35 -0
  73. data/app/components/satis/tabs/component.html.slim +23 -0
  74. data/app/components/satis/tabs/component.md +21 -0
  75. data/app/components/satis/tabs/component.rb +16 -0
  76. data/app/components/satis/tabs/component.scss +33 -0
  77. data/app/components/satis/tabs/component_controller.js +123 -0
  78. data/app/controllers/satis/application_controller.rb +4 -0
  79. data/app/helpers/satis/application_helper.rb +15 -0
  80. data/app/jobs/satis/application_job.rb +4 -0
  81. data/app/mailers/satis/application_mailer.rb +6 -0
  82. data/app/models/satis/application_record.rb +5 -0
  83. data/app/views/shared/_fields_for.html.slim +35 -0
  84. data/config/routes.rb +5 -0
  85. data/lib/satis/action_controller_helpers.rb +29 -0
  86. data/lib/satis/configuration.rb +61 -0
  87. data/lib/satis/engine.rb +27 -0
  88. data/lib/satis/forms/builder.rb +440 -0
  89. data/lib/satis/forms/concerns/buttons.rb +49 -0
  90. data/lib/satis/forms/concerns/file.rb +35 -0
  91. data/lib/satis/forms/concerns/options.rb +44 -0
  92. data/lib/satis/forms/concerns/required.rb +68 -0
  93. data/lib/satis/forms/concerns/select.rb +95 -0
  94. data/lib/satis/helpers/container.rb +83 -0
  95. data/lib/satis/menus/builder.rb +13 -0
  96. data/lib/satis/menus/item.rb +34 -0
  97. data/lib/satis/menus/menu.rb +23 -0
  98. data/lib/satis/version.rb +3 -0
  99. data/lib/satis.rb +36 -0
  100. data/lib/tasks/satis_tasks.rake +4 -0
  101. metadata +213 -0
@@ -0,0 +1,440 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'satis/forms/concerns/buttons'
4
+ require 'satis/forms/concerns/file'
5
+ require 'satis/forms/concerns/required'
6
+ require 'satis/forms/concerns/select'
7
+ require 'satis/forms/concerns/options'
8
+
9
+ module Satis
10
+ module Forms
11
+ class Builder < ActionView::Helpers::FormBuilder
12
+ delegate :t, :tag, :safe_join, :render, to: :@template
13
+
14
+ attr_reader :template, :assocation
15
+
16
+ include Concerns::Options
17
+ include Concerns::Select
18
+ include Concerns::File
19
+ include Concerns::Required
20
+ include Concerns::Buttons
21
+
22
+ # Regular input
23
+ def input(method, options = {}, &block)
24
+ @form_options = options
25
+
26
+ options[:input_html] ||= {}
27
+ options[:input_html][:disabled] = options.delete(:disabled)
28
+
29
+ send(input_type_for(method, options), method, options, &block)
30
+ end
31
+
32
+ # A codemirror editor, backed by a text-area
33
+ def editor(method, options = {}, &block)
34
+ @form_options = options
35
+
36
+ editor_input(method, options, &block)
37
+ end
38
+
39
+ # Simple-form like association
40
+ def association(name, options, &block)
41
+ @form_options = options
42
+
43
+ @association = name
44
+ reflection = @object.class.reflections[name.to_s]
45
+
46
+ method = reflection.join_foreign_key
47
+
48
+ send(input_type_for(method, options), method, options, &block)
49
+ end
50
+
51
+ alias rails_fields_for fields_for
52
+
53
+ # Wrapper around fields_for, using Satis::Forms::Builder
54
+ # Example:
55
+ #
56
+ # form_for @user do |f|
57
+ # f.simple_fields_for :printers do |printers_form|
58
+ # # Here you have all satis' form methods available
59
+ # printers_form.input :name
60
+ # end
61
+ # end
62
+ def fields_for(*args, &block)
63
+ options = args.extract_options!
64
+ name = args.first
65
+ template_object = args.second
66
+
67
+ # FIXME: Yuk - is it possible to detect when this should not be allowed?
68
+ # Like checking for whether destroy is allowed on assocations?
69
+ allow_actions = options.key?(:allow_actions) ? options[:allow_actions] : true
70
+ show_actions = @object.respond_to?("#{name}_attributes=") && @object.send(name).respond_to?(:each) && template_object && allow_actions == true
71
+
72
+ html_options = options[:html] || {}
73
+
74
+ html_options[:data] ||= {}
75
+ html_options[:data] = flatten_hash(html_options[:data])
76
+ html_options[:data][:controller] =
77
+ ['satis-fields-for'].concat(options[:html]&.[](:data)&.[](:controller).to_s.split).join(' ')
78
+ html_options[:class] = ['fields_for'].concat(options[:html]&.[](:class).to_s.split).join(' ')
79
+
80
+ options[:builder] ||= if self.class < ActionView::Helpers::FormBuilder
81
+ self.class
82
+ else
83
+ Satis::Forms::Builder
84
+ end
85
+
86
+ # Only do the whole nested-form thing with a collection
87
+ if show_actions
88
+ view_options = {
89
+ form: self,
90
+ collection: name,
91
+ template_object: template_object,
92
+ options: options
93
+ }
94
+ tag.div(**html_options) do
95
+ render 'shared/fields_for', view_options, &block
96
+ end
97
+
98
+ # FIXME: You would want to do this:
99
+ # render(Satis::FieldsFor::Component.new(
100
+ # form: self, name: name, template_object: template_object, **options, &block
101
+ # ))
102
+ else
103
+ invalid_feedback = nil
104
+ if @object.errors.messages[name].present?
105
+ invalid_feedback = tag.div(@object.errors.messages[name].join(', '),
106
+ class: 'invalid-feedback')
107
+ end
108
+ safe_join [
109
+ invalid_feedback,
110
+ rails_fields_for(*args, options, &block)
111
+ ].compact
112
+ end
113
+ end
114
+
115
+ def rich_text(*args)
116
+ options = args.extract_options!
117
+ form_group(*args, options) do
118
+ safe_join [
119
+ (custom_label(*args, options[:label]) unless options[:label] == false),
120
+ rich_text_area(*args, options)
121
+ ]
122
+ end
123
+ end
124
+
125
+ # A switch backed by a hidden value
126
+ def switch(method, options = {}, &block)
127
+ @form_options = options
128
+
129
+ switch_input(method, options, &block)
130
+ end
131
+
132
+ # A hidden input
133
+ def hidden(method, options = {}, &block)
134
+ @form_options = options
135
+
136
+ hidden_input(method, options, &block)
137
+ end
138
+
139
+ # Non public
140
+
141
+ def input_type_for(method, options)
142
+ object_type = object_type_for_method(method)
143
+ input_type = case object_type
144
+ when :date then :date_time
145
+ when :datetime then :date_time
146
+ when :integer then :string
147
+ when :float then :string
148
+ else object_type
149
+ end
150
+ override_input_type = if options[:as]
151
+ options[:as]
152
+ elsif options[:collection]
153
+ :select
154
+ elsif options[:url]
155
+ :dropdown
156
+ end
157
+
158
+ "#{override_input_type || input_type}_input"
159
+ end
160
+
161
+ def form_group(method, options = {}, &block)
162
+ tag.div(class: "form-group form-group-#{method}", data: options[:data]) do
163
+ safe_join [
164
+ block.call,
165
+ hint_text(options[:hint]),
166
+ error_text(method)
167
+ ].compact
168
+ end
169
+ end
170
+
171
+ def hint_text(text)
172
+ return if text.nil?
173
+
174
+ tag.small text, class: 'form-text text-muted'
175
+ end
176
+
177
+ # FIXME: These don't work for relations or location_id, error is on location
178
+ # When using the association helper, we need to set a @assocation variable
179
+ # any other input should clear it
180
+ def error_text(method)
181
+ return if !has_error?(method) && !has_error?(method.to_s.gsub(/_id$/, ''))
182
+
183
+ all_errors = @object.errors[method].dup
184
+ all_errors += @object.errors[method.to_s.gsub(/_id$/, '')] if method.to_s.ends_with?('_id')
185
+
186
+ tag.div(all_errors.uniq.join('<br />').html_safe,
187
+ class: 'invalid-feedback')
188
+ end
189
+
190
+ def object_type_for_method(method)
191
+ result = if @object.respond_to?(:type_for_attribute) && @object.has_attribute?(method)
192
+ @object.type_for_attribute(method.to_s).try(:type)
193
+ elsif @object.respond_to?(:column_for_attribute) && @object.has_attribute?(method)
194
+ @object.column_for_attribute(method).try(:type)
195
+ end
196
+ result || :string
197
+ end
198
+
199
+ def has_error?(method)
200
+ return false unless @object.respond_to?(:errors)
201
+
202
+ @object.errors.key?(method)
203
+ end
204
+
205
+ def custom_label(method, title, options = {})
206
+ all_classes = "#{options[:class]} form-label".strip
207
+ label(method, title, class: all_classes, data: options[:data]) do |translation|
208
+ safe_join [
209
+ tag.span(title || translation, class: required?(method) ? 'required' : ''),
210
+ ' ',
211
+ required(method, options),
212
+ help(method, options)
213
+ ]
214
+ end
215
+ end
216
+
217
+ def required(method, _options = {})
218
+ return unless required?(method)
219
+
220
+ tag.i(class: 'fas fa-hexagon-exclamation')
221
+ end
222
+
223
+ def help(method, options = {})
224
+ text = if options[:help].present?
225
+ options[:help]
226
+ else
227
+ Satis.config.default_help_text(@template, @object, method,
228
+ @options[:help_scope] || options[:help_scope])
229
+ end
230
+
231
+ return if text.blank?
232
+
233
+ tag.i(class: 'fal fa-circle-info', 'data-controller': 'help', 'data-help-content-value': text)
234
+ end
235
+
236
+ def hidden_input(method, options = {})
237
+ hidden_field(method, options[:input_html] || {})
238
+ end
239
+
240
+ # Inputs and helpers
241
+ def string_input(method, options = {})
242
+ form_group(method, options) do
243
+ safe_join [
244
+ (custom_label(method, options[:label], options) unless options[:label] == false),
245
+ string_field(method,
246
+ merge_input_options({ as: options[:as], class: "form-control #{if has_error?(method)
247
+ 'is-invalid'
248
+ end}" }, options[:input_html]))
249
+ ]
250
+ end
251
+ end
252
+
253
+ def number_input(method, options = {})
254
+ form_group(method, options) do
255
+ safe_join [
256
+ (custom_label(method, options[:label], options) unless options[:label] == false),
257
+ number_field(method,
258
+ merge_input_options({ class: "form-control #{if has_error?(method)
259
+ 'is-invalid'
260
+ end}" }, options[:input_html]))
261
+ ]
262
+ end
263
+ end
264
+
265
+ def text_input(method, options = {})
266
+ form_group(method, options) do
267
+ safe_join [
268
+ (custom_label(method, options[:label], options) unless options[:label] == false),
269
+ text_area(method,
270
+ merge_input_options({ class: "form-control #{if has_error?(method)
271
+ 'is-invalid'
272
+ end}" }, options[:input_html]))
273
+ ]
274
+ end
275
+ end
276
+
277
+ def editor_input(method, options = {})
278
+ form_group(method, options) do
279
+ safe_join [
280
+ (custom_label(method, options[:label], options) unless options[:label] == false),
281
+ tag.div(text_area(method,
282
+ merge_input_options({
283
+ class: 'form-control',
284
+ data: {
285
+ controller: 'satis-editor',
286
+ 'satis-editor-target' => 'textarea',
287
+ 'satis-editor-read-only-value' => options.delete(:read_only) || false,
288
+ 'satis-editor-mode-value' => options.delete(:mode) || 'text/html',
289
+ 'satis-editor-height-value' => options.delete(:height) || '200px',
290
+ 'satis-editor-color-scheme-value' => options.delete(:color_scheme),
291
+ 'satis-editor-color-scheme-dark-value' => options.delete(:color_scheme_dark) || 'lucario'
292
+ }
293
+
294
+ }, options[:input_html])), class: "editor #{if has_error?(method)
295
+ 'is-invalid'
296
+ end}"),
297
+ hint_text(options[:hint] || '⌘-F/⌃-f: search; ⌥-g: goto line, ⌃-space: autocomplete')
298
+ ]
299
+ end
300
+ end
301
+
302
+ def boolean_input(method, options = {})
303
+ form_group(method, options) do
304
+ tag.div(class: 'custom-control custom-checkbox') do
305
+ safe_join [
306
+ check_box(method, merge_input_options({ class: 'custom-control-input' }, options[:input_html])),
307
+ label(method, options[:label], class: 'custom-control-label')
308
+ ]
309
+ end
310
+ end
311
+ end
312
+
313
+ # Switch
314
+ # Pass icon: false for no icon
315
+ def switch_input(method, options = {}, &block)
316
+ form_group(method, options) do
317
+ render(Satis::Switch::Component.new(form: self, attribute: method, title: options[:label], **options,
318
+ &block))
319
+ end
320
+ end
321
+
322
+ # wrapper_html: { data: { 'date-time-picker-time-picker': 'true', controller: 'date-time-picker', 'date-time-picker-start-date' => (@holiday.start_at || params[:holiday]&.[](:start_at) && Time.parse(params[:holiday][:start_at]) || Time.current)&.iso8601 } }
323
+ def date_time_input(method, options = {}, &block)
324
+ case object_type_for_method(method)
325
+ when :date
326
+ options[:time_picker] = options.key?(:time_picker) ? options[:time_picker] : false
327
+ when :datetime
328
+ options[:time_picker] = options.key?(:time_picker) ? options[:time_picker] : true
329
+ end
330
+ form_group(method, options) do
331
+ safe_join [
332
+ (custom_label(method, options[:label], options) unless options[:label] == false),
333
+ render(Satis::DateTimePicker::Component.new(form: self, attribute: method, title: options[:label], **options,
334
+ &block))
335
+ ]
336
+ end
337
+ end
338
+
339
+ def phone_input(method, options = {})
340
+ # options[:input_html] = {}
341
+
342
+ # options[:input_html] = { 'data-controller' => 'phone-number',
343
+ # 'data-phone-number-target': 'input',
344
+ # 'data-action': 'keyup->phone-number#change blur->phone-number#change' }
345
+
346
+ tag.div('data-controller' => 'phone-number') do
347
+ safe_join [
348
+ hidden_field(method,
349
+ merge_input_options({ class: 'form-control', 'data-phone-number-target': 'hiddenInput' },
350
+ options[:input_html])),
351
+ @template.text_field_tag('dummy', @object.try(method), class: "form-control #{if has_error?(method)
352
+ 'is-invalid'
353
+ end}", 'data-phone-number-target': 'input',
354
+ 'data-action': 'input->phone-number#change')
355
+ ]
356
+ end
357
+ end
358
+
359
+ def collection_of(input_type, method, options = {})
360
+ form_builder_method, custom_class, input_builder_method = case input_type
361
+ when :radio_buttons then [:collection_radio_buttons,
362
+ 'custom-radio', :radio_button]
363
+ when :check_boxes then [:collection_check_boxes,
364
+ 'custom-checkbox', :check_box]
365
+ else raise 'Invalid input_type for collection_of, valid input_types are ":radio_buttons", ":check_boxes"'
366
+ end
367
+ options[:value_method] ||= :last
368
+ options[:text_method] ||= options[:label_method] || :first
369
+ form_group(method, options) do
370
+ safe_join [
371
+ label(method, options[:label]),
372
+ tag.br,
373
+ (send(form_builder_method, method, options[:collection], options[:value_method],
374
+ options[:text_method]) do |b|
375
+ tag.div(class: "custom-control #{custom_class}") do
376
+ safe_join [
377
+ b.send(input_builder_method, options.fetch(:input_html, {}).merge(class: 'custom-control-input')),
378
+ b.label(class: 'custom-control-label')
379
+ ]
380
+ end
381
+ end)
382
+ ]
383
+ end
384
+ end
385
+
386
+ def radio_buttons_input(method, options = {})
387
+ collection_of(:radio_buttons, method, options)
388
+ end
389
+
390
+ def check_boxes_input(method, options = {})
391
+ collection_of(:check_boxes, method, options)
392
+ end
393
+
394
+ def string_field(method, options = {})
395
+ # if specifically set to string, no more magic.
396
+ return text_field(method, options) if options[:as] == :string
397
+
398
+ case options[:as] || object_type_for_method(method)
399
+ when :date then text_field(method, options)
400
+ when :datetime then text_field(method, options)
401
+ when :integer then number_field(method, options)
402
+ when :float then text_field(method, options)
403
+ else
404
+ case method.to_s
405
+ when /password/ then password_field(method, options)
406
+ # FIXME: Possibly use time_zone_select with dropdown?
407
+ when /time_zone/ then time_zone_select(method, options.delete(:priority_zones), options,
408
+ { class: 'custom-select form-control' })
409
+ # FIXME: Possibly use country_select with dropdown?
410
+ when /country/ then country_select(method, options, class: 'custom-select form-control')
411
+ when /email/ then email_field(method, options)
412
+ when /phone/ then phone_input(method, options)
413
+ when /url/ then url_field(method, options)
414
+ else
415
+ text_field(method, options)
416
+ end
417
+ end
418
+ end
419
+
420
+ def merge_input_options(options, user_options)
421
+ return options if user_options.nil?
422
+
423
+ # TODO: handle class merging here
424
+ options.merge(user_options)
425
+ end
426
+
427
+ def flatten_hash(hash)
428
+ hash.each_with_object({}) do |(k, v), h|
429
+ if v.is_a? Hash
430
+ flatten_hash(v).map do |h_k, h_v|
431
+ h["#{k}_#{h_k}".to_sym] = h_v
432
+ end
433
+ else
434
+ h[k] = v
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end
440
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Satis
6
+ module Forms
7
+ module Concerns
8
+ module Buttons
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ alias_method :button_button, :button
13
+ alias_method :submit_button, :submit
14
+
15
+ # A submit button
16
+ def submit(value = nil, options = {})
17
+ button_button(value,
18
+ options.reverse_merge(name: 'commit', type: :submit, class: 'button primary',
19
+ value: 'commit'))
20
+ end
21
+
22
+ # A regular button
23
+ def button(value = nil, options = {}, &block)
24
+ options = options.reverse_merge(class: 'button')
25
+ options[:name] ||= :commit
26
+ options[:type] ||= :submit
27
+ button_button(value, options, &block)
28
+ end
29
+
30
+ # A continue button
31
+ def continue(value = nil, options = {}, &block)
32
+ value ||= if !@object.persisted?
33
+ t('.create_continue', default: 'Create and continue editing')
34
+ else
35
+ t('.update_continue', default: 'Update and continue editing')
36
+ end
37
+ button_button(value, options.reverse_merge(name: 'commit', value: 'continue', class: 'button secondary'),
38
+ &block)
39
+ end
40
+
41
+ # A reset button
42
+ def reset(value = nil, options = {}, &block)
43
+ button_button(value, options.reverse_merge(type: :reset, class: 'button'), &block)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Satis
6
+ module Forms
7
+ module Concerns
8
+ module File
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ def file_input(method, options = {})
13
+ form_group(method, options.merge(data: {
14
+ controller: 'satis-file'
15
+ })) do
16
+ safe_join [
17
+ (custom_label(method, options[:label], options) unless options[:label] == false),
18
+ tag.div(class: 'overflow-hidden relative inline-block -mb-2') do
19
+ safe_join [
20
+ tag.button((options[:multiple] ? t('choose_files') : t('choose_file')), type: 'button',
21
+ class: 'bg-white py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500'),
22
+ file_field(method,
23
+ options.merge(class: 'w-full cursor-pointer absolute block opacity-0 inset-0',
24
+ data: { 'satis-file-target': 'input' }))
25
+ ]
26
+ end,
27
+ tag.div(class: 'mt-2 text-gray-300 text-xs', data: { 'satis-file-target': 'selection' })
28
+ ]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Satis
6
+ module Forms
7
+ module Concerns
8
+ module Options
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ def value_text_methods(collection)
13
+ # An array of models
14
+ if collection.is_a?(Array) && collection.first.class < ActiveRecord::Base
15
+ value_method ||= :id
16
+ text_method ||= :name
17
+ # An array of arrays, whereby the inner array is size 2: [[text,value],[text,value]]
18
+ elsif collection.is_a?(Array) && collection.first.is_a?(Array) && collection.first.size == 2
19
+ value_method ||= :second
20
+ text_method ||= :first
21
+ # An array:["textvalue","textvalue"]
22
+ elsif collection.is_a?(Array) && !collection.first.is_a?(Array)
23
+ value_method ||= :to_s
24
+ text_method ||= :to_s
25
+ # An activerecord relation
26
+ elsif collection.class < ActiveRecord::Relation
27
+ value_method ||= :id
28
+ text_method ||= :name
29
+ # An activerecord relation
30
+ elsif collection.respond_to?(:each) && collection.first.respond_to?(:id) && collection.first.respond_to?(:name)
31
+ value_method ||= :id
32
+ text_method ||= :name
33
+ # Whatever else
34
+ else
35
+ value_method ||= :second
36
+ text_method ||= :first
37
+ end
38
+ [value_method, text_method]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Satis
6
+ module Forms
7
+ module Concerns
8
+ module Required
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ def required?(method)
13
+ result = if !options[:required].nil?
14
+ options[:required]
15
+ elsif @object.respond_to?("#{method}_required?".to_sym)
16
+ @object.send("#{method}_required?".to_sym)
17
+ elsif has_validators?(method)
18
+ required_by_validators?(method)
19
+ end
20
+
21
+ result = required?(method.to_s.sub(/_id$/, '')) if !result && method.match(/_id$/)
22
+ result
23
+ end
24
+
25
+ def has_validators?(method)
26
+ @has_validators ||= method && @object.class.respond_to?(:validators_on)
27
+ end
28
+
29
+ def required_by_validators?(method)
30
+ (attribute_validators(method) + reflection_validators(method)).any? do |v|
31
+ v.kind == :presence && valid_validator?(v)
32
+ end
33
+ end
34
+
35
+ def attribute_validators(method)
36
+ @object.class.validators_on(method)
37
+ end
38
+
39
+ def reflection_validators(_method)
40
+ []
41
+ # reflection ? object.class.validators_on(reflection.name) : []
42
+ end
43
+
44
+ def valid_validator?(validator)
45
+ !conditional_validators?(validator) && action_validator_match?(validator)
46
+ end
47
+
48
+ def conditional_validators?(validator)
49
+ validator.options.include?(:if) || validator.options.include?(:unless)
50
+ end
51
+
52
+ def action_validator_match?(validator)
53
+ return true unless validator.options.include?(:on)
54
+
55
+ case validator.options[:on]
56
+ when :save
57
+ true
58
+ when :create
59
+ !object.persisted?
60
+ when :update
61
+ object.persisted?
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end