proscenium 0.19.0.beta4-x86_64-darwin → 0.19.0.beta6-x86_64-darwin

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +5 -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 +15 -5
  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.slice(*value).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