proscenium 0.19.0.beta4-aarch64-linux → 0.19.0.beta6-aarch64-linux
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 +4 -4
- data/README.md +1 -1
- data/lib/proscenium/builder.rb +9 -13
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/importer.rb +13 -13
- data/lib/proscenium/middleware/base.rb +5 -2
- data/lib/proscenium/middleware/engines.rb +5 -9
- data/lib/proscenium/middleware/esbuild.rb +13 -8
- data/lib/proscenium/middleware.rb +2 -4
- data/lib/proscenium/railtie.rb +15 -5
- data/lib/proscenium/react_componentable.rb +1 -1
- data/lib/proscenium/resolver.rb +3 -8
- data/lib/proscenium/side_load.rb +1 -1
- data/lib/proscenium/ui/flash/index.css +1 -0
- data/lib/proscenium/ui/flash/index.js +73 -0
- data/lib/proscenium/ui/flash.rb +15 -0
- data/lib/proscenium/ui/form/field_methods.rb +88 -0
- data/lib/proscenium/ui/form/fields/base.rb +188 -0
- data/lib/proscenium/ui/form/fields/checkbox/index.jsx +48 -0
- data/lib/proscenium/ui/form/fields/checkbox/index.module.css +9 -0
- data/lib/proscenium/ui/form/fields/checkbox/previews/basic.jsx +8 -0
- data/lib/proscenium/ui/form/fields/checkbox.rb +32 -0
- data/lib/proscenium/ui/form/fields/date.module.css +27 -0
- data/lib/proscenium/ui/form/fields/datetime.rb +15 -0
- data/lib/proscenium/ui/form/fields/hidden.rb +9 -0
- data/lib/proscenium/ui/form/fields/input/index.jsx +71 -0
- data/lib/proscenium/ui/form/fields/input/index.module.css +13 -0
- data/lib/proscenium/ui/form/fields/input/previews/basic.jsx +8 -0
- data/lib/proscenium/ui/form/fields/input.rb +14 -0
- data/lib/proscenium/ui/form/fields/radio_group.rb +173 -0
- data/lib/proscenium/ui/form/fields/radio_input/index.jsx +44 -0
- data/lib/proscenium/ui/form/fields/radio_input/index.module.css +13 -0
- data/lib/proscenium/ui/form/fields/radio_input/previews/basic.jsx +8 -0
- data/lib/proscenium/ui/form/fields/radio_input.rb +17 -0
- data/lib/proscenium/ui/form/fields/rich_textarea.css +23 -0
- data/lib/proscenium/ui/form/fields/rich_textarea.js +6 -0
- data/lib/proscenium/ui/form/fields/rich_textarea.rb +18 -0
- data/lib/proscenium/ui/form/fields/select.jsx +47 -0
- data/lib/proscenium/ui/form/fields/select.module.css +46 -0
- data/lib/proscenium/ui/form/fields/select.rb +300 -0
- data/lib/proscenium/ui/form/fields/tel.css +297 -0
- data/lib/proscenium/ui/form/fields/tel.js +83 -0
- data/lib/proscenium/ui/form/fields/tel.rb +54 -0
- data/lib/proscenium/ui/form/fields/textarea/index.jsx +50 -0
- data/lib/proscenium/ui/form/fields/textarea/index.module.css +13 -0
- data/lib/proscenium/ui/form/fields/textarea/previews/basic.jsx +8 -0
- data/lib/proscenium/ui/form/fields/textarea.rb +18 -0
- data/lib/proscenium/ui/form/translation.rb +71 -0
- data/lib/proscenium/ui/form.css +52 -0
- data/lib/proscenium/ui/form.rb +213 -0
- data/lib/proscenium/ui/props.css +7 -0
- data/lib/proscenium/ui/react-manager/index.jsx +1 -1
- data/lib/proscenium/ui/test.js +1 -1
- data/lib/proscenium/ui/ujs/index.js +1 -1
- data/lib/proscenium/ui.rb +3 -0
- data/lib/proscenium/utils.rb +33 -0
- data/lib/proscenium/version.rb +1 -1
- data/lib/proscenium/view_component.rb +0 -2
- data/lib/proscenium.rb +12 -2
- metadata +61 -10
- 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,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
|