proscenium 0.19.0.beta3-x86_64-linux → 0.19.0.beta5-x86_64-linux

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/proscenium/builder.rb +9 -13
  4. data/lib/proscenium/ext/proscenium +0 -0
  5. data/lib/proscenium/importer.rb +13 -13
  6. data/lib/proscenium/middleware/base.rb +0 -2
  7. data/lib/proscenium/middleware/engines.rb +5 -9
  8. data/lib/proscenium/middleware/esbuild.rb +13 -8
  9. data/lib/proscenium/middleware.rb +2 -4
  10. data/lib/proscenium/railtie.rb +6 -3
  11. data/lib/proscenium/react_componentable.rb +1 -1
  12. data/lib/proscenium/resolver.rb +3 -8
  13. data/lib/proscenium/side_load.rb +1 -1
  14. data/lib/proscenium/ui/flash/index.css +1 -0
  15. data/lib/proscenium/ui/flash/index.js +73 -0
  16. data/lib/proscenium/ui/flash.rb +15 -0
  17. data/lib/proscenium/ui/form/field_methods.rb +88 -0
  18. data/lib/proscenium/ui/form/fields/base.rb +188 -0
  19. data/lib/proscenium/ui/form/fields/checkbox/index.jsx +48 -0
  20. data/lib/proscenium/ui/form/fields/checkbox/index.module.css +9 -0
  21. data/lib/proscenium/ui/form/fields/checkbox/previews/basic.jsx +8 -0
  22. data/lib/proscenium/ui/form/fields/checkbox.rb +32 -0
  23. data/lib/proscenium/ui/form/fields/date.module.css +27 -0
  24. data/lib/proscenium/ui/form/fields/datetime.rb +15 -0
  25. data/lib/proscenium/ui/form/fields/hidden.rb +9 -0
  26. data/lib/proscenium/ui/form/fields/input/index.jsx +71 -0
  27. data/lib/proscenium/ui/form/fields/input/index.module.css +13 -0
  28. data/lib/proscenium/ui/form/fields/input/previews/basic.jsx +8 -0
  29. data/lib/proscenium/ui/form/fields/input.rb +14 -0
  30. data/lib/proscenium/ui/form/fields/radio_group.rb +173 -0
  31. data/lib/proscenium/ui/form/fields/radio_input/index.jsx +44 -0
  32. data/lib/proscenium/ui/form/fields/radio_input/index.module.css +13 -0
  33. data/lib/proscenium/ui/form/fields/radio_input/previews/basic.jsx +8 -0
  34. data/lib/proscenium/ui/form/fields/radio_input.rb +17 -0
  35. data/lib/proscenium/ui/form/fields/rich_textarea.css +23 -0
  36. data/lib/proscenium/ui/form/fields/rich_textarea.js +6 -0
  37. data/lib/proscenium/ui/form/fields/rich_textarea.rb +18 -0
  38. data/lib/proscenium/ui/form/fields/select.jsx +47 -0
  39. data/lib/proscenium/ui/form/fields/select.module.css +46 -0
  40. data/lib/proscenium/ui/form/fields/select.rb +300 -0
  41. data/lib/proscenium/ui/form/fields/tel.css +297 -0
  42. data/lib/proscenium/ui/form/fields/tel.js +83 -0
  43. data/lib/proscenium/ui/form/fields/tel.rb +54 -0
  44. data/lib/proscenium/ui/form/fields/textarea/index.jsx +50 -0
  45. data/lib/proscenium/ui/form/fields/textarea/index.module.css +13 -0
  46. data/lib/proscenium/ui/form/fields/textarea/previews/basic.jsx +8 -0
  47. data/lib/proscenium/ui/form/fields/textarea.rb +18 -0
  48. data/lib/proscenium/ui/form/translation.rb +71 -0
  49. data/lib/proscenium/ui/form.css +52 -0
  50. data/lib/proscenium/ui/form.rb +213 -0
  51. data/lib/proscenium/ui/props.css +7 -0
  52. data/lib/proscenium/ui/react-manager/index.jsx +1 -1
  53. data/lib/proscenium/ui/test.js +1 -1
  54. data/lib/proscenium/ui/ujs/index.js +1 -1
  55. data/lib/proscenium/ui.rb +3 -0
  56. data/lib/proscenium/utils.rb +33 -0
  57. data/lib/proscenium/version.rb +1 -1
  58. data/lib/proscenium/view_component.rb +0 -2
  59. data/lib/proscenium.rb +12 -2
  60. metadata +61 -10
  61. data/lib/proscenium/middleware/runtime.rb +0 -18
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ class RadioInput < Base
5
+ def view_template
6
+ checked = attributes[:value].to_s == value.to_s
7
+
8
+ default = model.class.human_attribute_name("#{attribute.join('.')}.#{attributes[:value]}")
9
+ label_contents = attributes.delete(:label) || translate_label(default:)
10
+
11
+ label do |_|
12
+ input(name: field_name, type: :radio, checked:, **build_attributes)
13
+ span { label_contents }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ @import 'trix/dist/trix.css';
2
+
3
+ trix-editor {
4
+ @mixin textarea from url('/hue/lib/hue/mixins/textarea.mixin.css');
5
+ }
6
+
7
+ trix-toolbar {
8
+ .trix-button-group {
9
+ @mixin button-group from url('/hue/lib/hue/mixins/button_group.mixin.css');
10
+ @mixin button-group__nospace from url('/hue/lib/hue/mixins/button_group.mixin.css');
11
+ border-color: var(--gray5);
12
+ }
13
+
14
+ .trix-button {
15
+ @mixin button from url('/hue/lib/hue/mixins/button.mixin.css');
16
+ @mixin button__secondary from url('/hue/lib/hue/mixins/button.mixin.css');
17
+ box-shadow: none;
18
+ }
19
+
20
+ [data-trix-button-group='file-tools'] {
21
+ display: none;
22
+ }
23
+ }
@@ -0,0 +1,6 @@
1
+ import "trix"
2
+
3
+ document.addEventListener("trix-file-accept", function (event) {
4
+ // Prevent attachment drag and drop
5
+ event.preventDefault()
6
+ })
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ class RichTextarea < Base
5
+ register_element :trix_editor
6
+
7
+ def view_template
8
+ value = attributes.delete(:value)
9
+
10
+ field do
11
+ label
12
+ trix_editor input: field_id
13
+ hint
14
+ form.hidden_field(*attribute, id: field_id, value: value&.to_trix_html)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,47 @@
1
+ import { useCallback, useState } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import exact from 'prop-types-exact'
4
+
5
+ import SmartSelect, {
6
+ propTypes as smartSelectPropTypes
7
+ } from '/hue/app/components/lib/smart_select'
8
+
9
+ const Component = ({ inputName, ...props }) => {
10
+ const [selected, setSelected] = useState(() => {
11
+ return Array.isArray(props.initialSelectedItem)
12
+ ? props.initialSelectedItem
13
+ : [props.initialSelectedItem]
14
+ })
15
+
16
+ const onChange = useCallback(values => {
17
+ if (Array.isArray(values)) {
18
+ setSelected(values)
19
+ } else {
20
+ setSelected([values?.value])
21
+ }
22
+ }, [])
23
+
24
+ return (
25
+ <>
26
+ <SmartSelect {...props} onChange={onChange} />
27
+
28
+ {selected.length === 0 && <input type="hidden" name={inputName} value="" />}
29
+ {selected.map((item, i) => (
30
+ <input
31
+ key={item?.value || i}
32
+ type="hidden"
33
+ name={inputName}
34
+ value={item?.value || item || ''}
35
+ />
36
+ ))}
37
+ </>
38
+ )
39
+ }
40
+
41
+ Component.displayName = 'Hue.Form.Fields.Select'
42
+ Component.propTypes = exact({
43
+ inputName: PropTypes.string.isRequired,
44
+ ...smartSelectPropTypes
45
+ })
46
+
47
+ export default Component
@@ -0,0 +1,46 @@
1
+ @layer hue-component {
2
+ .field {
3
+ @mixin fieldWrapper from url('/hue/lib/hue/mixins/form.mixin.css');
4
+
5
+ &[data-field-error] > div > span > span {
6
+ color: var(--input-error-color);
7
+ display: var(--fieldError-display);
8
+
9
+ &:first-child {
10
+ font-weight: 500;
11
+
12
+ &:after {
13
+ content: '\00a0';
14
+ }
15
+ }
16
+ }
17
+ }
18
+
19
+ .hint {
20
+ @mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
21
+ }
22
+
23
+ .typeahead {
24
+ @mixin label from url('/hue/lib/hue/mixins/label.mixin.css');
25
+
26
+ > span:first-child {
27
+ margin: 0 0 0.2em 0.2em;
28
+ }
29
+
30
+ &:has(:not(> span)) {
31
+ margin: 0 0 0.2em 0.2em;
32
+ }
33
+ }
34
+
35
+ .typeahead_input {
36
+ &:empty {
37
+ @mixin select from url('/hue/lib/hue/mixins/select.mixin.css');
38
+ @mixin field__disabled from url('/hue/lib/hue/mixins/field.mixin.css');
39
+
40
+ &:after {
41
+ content: 'loading...';
42
+ color: var(--input-disabled-color);
43
+ }
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ #
5
+ # Render a <select> input for the given model attribute. It will attempt to automatically build an
6
+ # appropriate set of <option>'s, and supports ActiveRecord associations and enums. It will also
7
+ # detect if multiple options should be enabled or not.
8
+ #
9
+ # ## Supported options
10
+ #
11
+ # - options [Array] a list of options to use for the <select>. If this is given, the automatic
12
+ # detection of options will be disabled. A flat array of strings will be used for both the
13
+ # label and value. While an Array of nested two-level arrays will be used.
14
+ # - include_blank [Boolean, String] if true, will add an empty <option> to the <select>. If a
15
+ # String is given, it will be used as the label for the empty <option>.
16
+ # - label [String] the label to use for the field (default: humanized attribute name).
17
+ # - hint [String] the hint to use for the field (optional).
18
+ # - required [Boolean] if true, will add the `required` attribute to the <select> tag (default:
19
+ # false). Also supported as a bang attribute (e.g. `:required!`). See
20
+ # `Hue::Utils.merge_bang_attributes!`.
21
+ # - typeahead [Boolean] if true, will enable typeahead support by replacing with a SmartSelect
22
+ # React component (default: false). Also supported as a bang attribute (e.g. `:typeahead!`).
23
+ # See `Hue::Utils.merge_bang_attributes!`.
24
+ #
25
+ class Select < Base
26
+ def self.sideload(_options)
27
+ # Proscenium::Importer.import Hue::Phlex::ReactComponent.manager
28
+ Proscenium::Importer.sideload source_path, lazy: true
29
+ end
30
+
31
+ # Ensure both the form and select field are side loaded.
32
+ def self.css_module_path
33
+ source_path.sub_ext('.module.css')
34
+ end
35
+
36
+ def before_template
37
+ # Defined here to ensure they are deleted from `attributes` before `final_attributes` is
38
+ # called.
39
+ @options_from_attributes = attributes.delete(:options)
40
+ @include_blank = attributes.delete(:include_blank)
41
+
42
+ super
43
+ end
44
+
45
+ def view_template(&options_block)
46
+ field class: :@field do
47
+ if !options_block && typeahead?
48
+ @component_props = attributes.delete(:component_props) || {}
49
+
50
+ # SmartSelect - or most likely React - does not like being wrapped in a label.
51
+ div class: :@typeahead do
52
+ label
53
+ div(**final_attributes)
54
+ hint
55
+ end
56
+ else
57
+ label do
58
+ multiple? && input(name: field_name, type: :hidden, value: '')
59
+ select(name: field_name, **final_attributes) do
60
+ options_block ? yield_content(&options_block) : options_template
61
+ end
62
+ hint
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def options_template
69
+ options.each do |value, opts|
70
+ option(value:, selected: opts[:selected]) { opts[:label] }
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def final_attributes
77
+ attributes.tap do |attrs|
78
+ if typeahead?
79
+ attrs[:class] = :@typeahead_input
80
+ attrs[:data] ||= {}
81
+ attrs[:data][:proscenium_component_path] = virtual_path
82
+ attrs[:data][:proscenium_component_props] = {
83
+ **@component_props,
84
+ input_name: field_name,
85
+ multi: multiple?,
86
+ items: options.is_a?(String) ? options : options.values,
87
+ initial_selected_item: values_for_typeahead
88
+ }.deep_transform_keys { |k| k.to_s.camelize :lower }.to_json
89
+ else
90
+ attrs[:multiple] = multiple?
91
+ end
92
+ end
93
+ end
94
+
95
+ def field_name
96
+ names = '' if multiple?
97
+
98
+ return super(*names) unless association_attribute?
99
+
100
+ form.field_name association_attribute, *names
101
+ end
102
+
103
+ def enum_attribute?
104
+ model_class.defined_enums.key?(attribute.last.to_s)
105
+ end
106
+
107
+ def association_attribute?
108
+ association_reflection.present?
109
+ end
110
+
111
+ def values_for_typeahead
112
+ if !value ||
113
+ (value.is_a?(String) && !value.uuid?) ||
114
+ (options.is_a?(String) && !options.uuid?)
115
+ return value
116
+ end
117
+
118
+ options.filter { |k, _v| value.include?(k) }.values
119
+ end
120
+
121
+ def options
122
+ @options ||= begin
123
+ data = {}
124
+ data[''] = empty_option if empty_option?
125
+
126
+ if @options_from_attributes
127
+ if @options_from_attributes.is_a?(String)
128
+ data = @options_from_attributes
129
+ else
130
+ data.merge! build_options_from_attributes
131
+ end
132
+ elsif enum_attribute?
133
+ fetch_enum_collection.each do |opt|
134
+ data[opt] = {
135
+ value: opt,
136
+ label: model_class.human_attribute_name("#{attribute.last}.#{opt}"),
137
+ selected: selected?(opt)
138
+ }
139
+ end
140
+ elsif association_attribute?
141
+ fetch_association_collection.each do |opt|
142
+ data[opt.id] = {
143
+ value: opt.id,
144
+ label: opt.to_s,
145
+ selected: selected?(opt)
146
+ }
147
+ end
148
+ end
149
+
150
+ data
151
+ end
152
+ end
153
+
154
+ def build_options_from_attributes
155
+ @options_from_attributes.to_h do |opt|
156
+ label, value = option_text_and_value(opt)
157
+ [value, { label:, value:, selected: selected?(value) }]
158
+ end
159
+ end
160
+
161
+ def option_text_and_value(option)
162
+ # Options are [text, value] pairs or strings used for both.
163
+ if !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
164
+ option = option.reject { |e| e.is_a?(Hash) } if option.is_a?(Array)
165
+ [option.first, option.last]
166
+ else
167
+ [option, option]
168
+ end
169
+ end
170
+
171
+ # Should we show the empty <option>?
172
+ #
173
+ # @return [Boolean]
174
+ # true if include_blank option is given and is true or a string.
175
+ # false if include_blank is given and is false.
176
+ # true if not required, AND attribute has no default value.
177
+ # true if required, AND attribute has no value.
178
+ def empty_option
179
+ { label: @include_blank.is_a?(String) ? @include_blank : nil }
180
+ end
181
+
182
+ def empty_option?
183
+ if [true, false].include?(@include_blank)
184
+ return @include_blank
185
+ elsif @include_blank.is_a?(String)
186
+ return true
187
+ end
188
+
189
+ return false if typeahead? || multiple?
190
+
191
+ attributes[:required] == true ? !value? : !default_value?
192
+ end
193
+
194
+ def default_value?
195
+ default_value.present?
196
+ end
197
+
198
+ def default_value
199
+ model_class.new.attributes[model_attribute.to_s]
200
+ end
201
+
202
+ def multiple?
203
+ association_attribute? && association_reflection.macro == :has_many
204
+ end
205
+
206
+ def typeahead?
207
+ @typeahead ||= attributes.delete(:typeahead)
208
+ end
209
+
210
+ def value?
211
+ value.present?
212
+ end
213
+
214
+ def value
215
+ if association_attribute? && association_reflection.macro == :has_many &&
216
+ actual_model.respond_to?(attribute.last)
217
+ actual_model.send(model_attribute)
218
+ else
219
+ actual_model.attributes[model_attribute.to_s]
220
+ end
221
+ end
222
+
223
+ # Is the given `option` the current value (selected)?
224
+ def selected?(option)
225
+ if !option.is_a?(String) && !option.is_a?(Integer) && association_attribute?
226
+ if association_reflection.macro == :has_many && actual_model.respond_to?(attribute.last)
227
+ actual_model.send(attribute.last).include?(option)
228
+ else
229
+ reflection = association_reflection
230
+ key = if reflection.respond_to?(:options) && reflection.options[:primary_key]
231
+ reflection.options[:primary_key]
232
+ else
233
+ option.class.primary_key.to_s
234
+ end
235
+ option.attributes[key] == value
236
+ end
237
+ else
238
+ option == value
239
+ end
240
+ end
241
+
242
+ def model_attribute
243
+ association_attribute? ? association_attribute : attribute.last
244
+ end
245
+
246
+ def fetch_enum_collection
247
+ actual_model.defined_enums[attribute.last.to_s].keys
248
+ end
249
+
250
+ def association_reflection
251
+ @association_reflection ||= model_class.try :reflect_on_association, attribute.last
252
+ end
253
+
254
+ def fetch_association_collection
255
+ relation = association_reflection.klass.all
256
+
257
+ if association_reflection.respond_to?(:scope) && association_reflection.scope
258
+ relation = if association_reflection.scope.parameters.any?
259
+ association_reflection.klass.instance_exec(actual_model,
260
+ &association_reflection.scope)
261
+ else
262
+ association_reflection.klass.instance_exec(&association_reflection.scope)
263
+ end
264
+ else
265
+ order = association_reflection.options[:order]
266
+ conditions = association_reflection.options[:conditions]
267
+ conditions = actual_model.instance_exec(&conditions) if conditions.respond_to?(:call)
268
+
269
+ relation = relation.where(conditions) if relation.respond_to?(:where) && conditions.present?
270
+ relation = relation.order(order) if relation.respond_to?(:order)
271
+ end
272
+
273
+ relation
274
+ end
275
+
276
+ def association_attribute
277
+ @association_attribute ||= begin
278
+ reflection = association_reflection
279
+
280
+ case reflection.macro
281
+ when :belongs_to
282
+ (reflection.respond_to?(:options) && reflection.options[:foreign_key]) ||
283
+ :"#{reflection.name}_id"
284
+ else
285
+ # Force the association to be preloaded for performance.
286
+ if actual_model.respond_to?(attribute.last)
287
+ target = actual_model.send(attribute.last)
288
+ target.to_a if target.respond_to?(:to_a)
289
+ end
290
+
291
+ :"#{reflection.name.to_s.singularize}_ids"
292
+ end
293
+ end
294
+ end
295
+
296
+ def model_class
297
+ @model_class ||= actual_model.class
298
+ end
299
+ end
300
+ end