satis 1.0.66

Sign up to get free protection for your applications and to get access to all the features.
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