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.
- 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
|