satis 1.0.66
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +92 -0
- data/Rakefile +23 -0
- data/app/assets/config/satis_manifest.js +1 -0
- data/app/assets/stylesheets/satis/application.css +15 -0
- data/app/components/satis/appearance_switcher/component.html.slim +6 -0
- data/app/components/satis/appearance_switcher/component.rb +11 -0
- data/app/components/satis/appearance_switcher/component.scss +34 -0
- data/app/components/satis/appearance_switcher/component_controller.js +62 -0
- data/app/components/satis/application_component.rb +50 -0
- data/app/components/satis/avatar/component.html.slim +7 -0
- data/app/components/satis/avatar/component.rb +52 -0
- data/app/components/satis/breadcrumbs/component.html.slim +8 -0
- data/app/components/satis/breadcrumbs/component.rb +23 -0
- data/app/components/satis/breadcrumbs/component.scss +19 -0
- data/app/components/satis/breadcrumbs/crumb.slim +8 -0
- data/app/components/satis/card/component.html.slim +54 -0
- data/app/components/satis/card/component.md +14 -0
- data/app/components/satis/card/component.rb +41 -0
- data/app/components/satis/card/component.scss +15 -0
- data/app/components/satis/date_time_picker/component.html.slim +48 -0
- data/app/components/satis/date_time_picker/component.md +11 -0
- data/app/components/satis/date_time_picker/component.rb +48 -0
- data/app/components/satis/date_time_picker/component.scss +5 -0
- data/app/components/satis/date_time_picker/component_controller.js +499 -0
- data/app/components/satis/dropdown/component.html.slim +36 -0
- data/app/components/satis/dropdown/component.md +48 -0
- data/app/components/satis/dropdown/component.rb +77 -0
- data/app/components/satis/dropdown/component.scss +10 -0
- data/app/components/satis/dropdown/component_controller.js +547 -0
- data/app/components/satis/flash_messages/component.html.slim +3 -0
- data/app/components/satis/flash_messages/component.rb +31 -0
- data/app/components/satis/flash_messages/component.scss +18 -0
- data/app/components/satis/flash_messages/message.html.slim +8 -0
- data/app/components/satis/info/component.html.slim +4 -0
- data/app/components/satis/info/component.rb +22 -0
- data/app/components/satis/info_item/component.html.slim +7 -0
- data/app/components/satis/info_item/component.rb +19 -0
- data/app/components/satis/input/component.html.slim +11 -0
- data/app/components/satis/input/component.rb +38 -0
- data/app/components/satis/input/component.scss +50 -0
- data/app/components/satis/input/element.html.slim +2 -0
- data/app/components/satis/map/component.html.slim +2 -0
- data/app/components/satis/map/component.rb +17 -0
- data/app/components/satis/map/component.scss +9 -0
- data/app/components/satis/map/component_controller.js +37 -0
- data/app/components/satis/menu/component.html.slim +13 -0
- data/app/components/satis/menu/component.md +1 -0
- data/app/components/satis/menu/component.rb +16 -0
- data/app/components/satis/menu/component_controller.js +62 -0
- data/app/components/satis/menu_item/component.html.slim +16 -0
- data/app/components/satis/menu_item/component.rb +14 -0
- data/app/components/satis/page/component.html.slim +45 -0
- data/app/components/satis/page/component.rb +15 -0
- data/app/components/satis/page/component_controller.js +86 -0
- data/app/components/satis/sidebar_menu/component.html.slim +3 -0
- data/app/components/satis/sidebar_menu/component.rb +17 -0
- data/app/components/satis/sidebar_menu/component.scss +0 -0
- data/app/components/satis/sidebar_menu/component_controller.js +9 -0
- data/app/components/satis/sidebar_menu/mobile/component.html.slim +3 -0
- data/app/components/satis/sidebar_menu/mobile/component.rb +10 -0
- data/app/components/satis/sidebar_menu_item/component.html.slim +15 -0
- data/app/components/satis/sidebar_menu_item/component.rb +20 -0
- data/app/components/satis/sidebar_menu_item/component.scss +27 -0
- data/app/components/satis/sidebar_menu_item/component_controller.js +62 -0
- data/app/components/satis/sidebar_menu_item/mobile/component.html.slim +17 -0
- data/app/components/satis/sidebar_menu_item/mobile/component.rb +10 -0
- data/app/components/satis/switch/component.html.slim +14 -0
- data/app/components/satis/switch/component.rb +24 -0
- data/app/components/satis/switch/component_controller.js +49 -0
- data/app/components/satis/tab/component.rb +35 -0
- data/app/components/satis/tabs/component.html.slim +23 -0
- data/app/components/satis/tabs/component.md +21 -0
- data/app/components/satis/tabs/component.rb +16 -0
- data/app/components/satis/tabs/component.scss +33 -0
- data/app/components/satis/tabs/component_controller.js +123 -0
- data/app/controllers/satis/application_controller.rb +4 -0
- data/app/helpers/satis/application_helper.rb +15 -0
- data/app/jobs/satis/application_job.rb +4 -0
- data/app/mailers/satis/application_mailer.rb +6 -0
- data/app/models/satis/application_record.rb +5 -0
- data/app/views/shared/_fields_for.html.slim +35 -0
- data/config/routes.rb +5 -0
- data/lib/satis/action_controller_helpers.rb +29 -0
- data/lib/satis/configuration.rb +61 -0
- data/lib/satis/engine.rb +27 -0
- data/lib/satis/forms/builder.rb +440 -0
- data/lib/satis/forms/concerns/buttons.rb +49 -0
- data/lib/satis/forms/concerns/file.rb +35 -0
- data/lib/satis/forms/concerns/options.rb +44 -0
- data/lib/satis/forms/concerns/required.rb +68 -0
- data/lib/satis/forms/concerns/select.rb +95 -0
- data/lib/satis/helpers/container.rb +83 -0
- data/lib/satis/menus/builder.rb +13 -0
- data/lib/satis/menus/item.rb +34 -0
- data/lib/satis/menus/menu.rb +23 -0
- data/lib/satis/version.rb +3 -0
- data/lib/satis.rb +36 -0
- data/lib/tasks/satis_tasks.rake +4 -0
- 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
|