proscenium 0.19.0.beta4-aarch64-linux → 0.19.0.beta5-aarch64-linux
Sign up to get free protection for your applications and to get access to all the features.
- 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 +0 -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 +6 -3
- 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,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Proscenium::UI
|
6
|
+
module Form::Fields
|
7
|
+
#
|
8
|
+
# Abstract class to provide basic rendering of an <input>. All field classes inherit this.
|
9
|
+
#
|
10
|
+
class Base < Component
|
11
|
+
attr_reader :attribute, :model, :form, :attributes
|
12
|
+
|
13
|
+
register_element :pui_field
|
14
|
+
|
15
|
+
# In most cases we want to use the main form component stylesheet. Override this method if
|
16
|
+
# you want to use a different stylesheet.
|
17
|
+
# def self.css_module_path
|
18
|
+
# source_path.join('../index.module.css')
|
19
|
+
# end
|
20
|
+
|
21
|
+
# @param attribute [Array]
|
22
|
+
# @param model [*]
|
23
|
+
# @param form [Proscenium::UI::Form]
|
24
|
+
# @param type [Symbol] input type, eg. 'text', 'select'
|
25
|
+
# @param error [ActiveModel::Error, String] error message for the attribute.
|
26
|
+
# @param attributes [Hash] HTML attributes to pass to the input.
|
27
|
+
def initialize(attribute, model, form, type: nil, error: nil, **attributes) # rubocop:disable Lint/MissingSuper,Metrics/ParameterLists
|
28
|
+
if attribute.count > 2
|
29
|
+
raise ArgumentError, 'attribute cannot be nested more than 2 levels deep'
|
30
|
+
end
|
31
|
+
|
32
|
+
@attribute = attribute
|
33
|
+
@model = model
|
34
|
+
@form = form
|
35
|
+
@field_type = type
|
36
|
+
@error = error
|
37
|
+
@attributes = attributes
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# @return [String] The error message for the attribute.
|
43
|
+
def error_message
|
44
|
+
@error_message ||= case @error
|
45
|
+
when ActiveModel::Error
|
46
|
+
@error.message
|
47
|
+
when String
|
48
|
+
@error
|
49
|
+
else
|
50
|
+
if model.errors.include?(attribute.join('.'))
|
51
|
+
model.errors.where(attribute.join('.')).first&.message
|
52
|
+
elsif model.errors.include?(attribute.first)
|
53
|
+
model.errors.where(attribute.first).first&.message
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def error?
|
59
|
+
error_message.present?
|
60
|
+
end
|
61
|
+
|
62
|
+
# The main wrapper for the field. This is where the label, input, and error message are
|
63
|
+
# rendered.
|
64
|
+
#
|
65
|
+
# @param tag_name: [Symbol] HTML tag name to use for the wrapper.
|
66
|
+
# @param ** [Hash] Additional HTML attributes to pass to the wrapper.
|
67
|
+
# @param [Proc] The block to render the field.
|
68
|
+
def field(tag_name = :pui_field, **rest, &)
|
69
|
+
classes = []
|
70
|
+
classes << rest.delete(:class) if rest.key?(:class)
|
71
|
+
classes << attributes.delete(:class) if attributes.key?(:class)
|
72
|
+
|
73
|
+
send(tag_name, class: classes, data: { field_error: error? }, **rest, &)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Builds the template for the label, along with any error message for the attribute.
|
77
|
+
#
|
78
|
+
# By default, the translated attribute name will be used as the content for the label. You can
|
79
|
+
# overide this by providing the `:label` keyword argument in `@arguments`. Passing false as
|
80
|
+
# the value to `:label` will omit the label.
|
81
|
+
#
|
82
|
+
# If a block is given, it will be yielded with the label content, and after the label and
|
83
|
+
# error message.
|
84
|
+
def label(**kwargs, &block)
|
85
|
+
content = attributes.delete(:label)
|
86
|
+
|
87
|
+
super(**kwargs) do
|
88
|
+
captured = capture do
|
89
|
+
div do
|
90
|
+
span { content || translate_label } if content != false
|
91
|
+
error? && span(part: :error) { error_message }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if !block
|
96
|
+
yield_content_with_no_args { captured }
|
97
|
+
elsif block.arity == 1
|
98
|
+
yield captured
|
99
|
+
else
|
100
|
+
yield_content_with_no_args { captured }
|
101
|
+
yield
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def hint(content = nil)
|
107
|
+
content ||= attributes.delete(:hint)
|
108
|
+
|
109
|
+
return if content == false
|
110
|
+
|
111
|
+
content ||= translate(:hints)
|
112
|
+
content.present? && div(part: :hint) { unsafe_raw content }
|
113
|
+
end
|
114
|
+
|
115
|
+
def field_type
|
116
|
+
@field_type ||= self.class.name.demodulize.underscore
|
117
|
+
end
|
118
|
+
|
119
|
+
def field_name(*names, multiple: false)
|
120
|
+
names.prepend attribute.last
|
121
|
+
|
122
|
+
if nested?
|
123
|
+
if nested_attributes_association?
|
124
|
+
names.prepend "#{attribute.first}_attributes"
|
125
|
+
else
|
126
|
+
names.prepend attribute.first
|
127
|
+
end
|
128
|
+
elsif names.count == 1 && names.first.is_a?(String)
|
129
|
+
return names.first
|
130
|
+
end
|
131
|
+
|
132
|
+
form.field_name(*names, multiple:)
|
133
|
+
end
|
134
|
+
|
135
|
+
def field_id(*)
|
136
|
+
@field_uid ||= SecureRandom.alphanumeric(10)
|
137
|
+
form.field_id(*attribute, @field_uid, *)
|
138
|
+
end
|
139
|
+
|
140
|
+
def translate(namespace, postfix: nil, default: '')
|
141
|
+
form.translate namespace, attribute, postfix:, default:
|
142
|
+
end
|
143
|
+
|
144
|
+
def translate_label(default: nil)
|
145
|
+
form.translate_label attribute, default:
|
146
|
+
end
|
147
|
+
|
148
|
+
def build_attributes(**attrs)
|
149
|
+
attributes.merge(attrs).tap do |x|
|
150
|
+
x[:value] ||= value.to_s
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def value
|
155
|
+
attr = attribute.last
|
156
|
+
if actual_model.respond_to?(attr)
|
157
|
+
actual_model.public_send(attribute.last)
|
158
|
+
else
|
159
|
+
''
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# @return [Boolean] true if the attribute is nested, otherwise false.
|
164
|
+
def nested?
|
165
|
+
attribute.count > 1
|
166
|
+
end
|
167
|
+
|
168
|
+
def nested_attributes_association?
|
169
|
+
parent_model.respond_to?(:"#{attribute.first}_attributes=")
|
170
|
+
end
|
171
|
+
|
172
|
+
# @return the nested model if nested, otherwise nil.
|
173
|
+
def nested_model
|
174
|
+
@nested_model ||= nested? ? model.public_send(attribute.first) : nil
|
175
|
+
end
|
176
|
+
|
177
|
+
def actual_model
|
178
|
+
@actual_model ||= nested_model || model
|
179
|
+
end
|
180
|
+
|
181
|
+
alias parent_model model
|
182
|
+
|
183
|
+
def virtual_path
|
184
|
+
@virtual_path ||= Proscenium::Resolver.resolve self.class.source_path.sub_ext('.jsx').to_s
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import clsx from 'clsx'
|
2
|
+
import PropTypes from 'prop-types'
|
3
|
+
|
4
|
+
import dsx from '/hue/lib/hue/utils/dsx'
|
5
|
+
import { useFormError } from '../../hooks'
|
6
|
+
|
7
|
+
import styles from './index.module.css'
|
8
|
+
|
9
|
+
const Component = ({ label, hint, className, errorAttrName, ...props }) => {
|
10
|
+
const [error, hasError] = useFormError(errorAttrName || props.name)
|
11
|
+
|
12
|
+
return (
|
13
|
+
<div className={clsx(styles.fieldWrapper, className)} {...dsx({ fieldError: hasError })}>
|
14
|
+
<label>
|
15
|
+
<input type="hidden" value="0" name={props.name} />
|
16
|
+
<input type="checkbox" value="1" {...props} />
|
17
|
+
|
18
|
+
<span>
|
19
|
+
{label ? <span>{label}</span> : null}
|
20
|
+
{hasError ? <span>{error}</span> : null}
|
21
|
+
</span>
|
22
|
+
</label>
|
23
|
+
|
24
|
+
{hint ? <div className={styles.hint}>{hint}</div> : null}
|
25
|
+
</div>
|
26
|
+
)
|
27
|
+
}
|
28
|
+
|
29
|
+
Component.displayName = 'Hue.Form.Fields.Checkbox'
|
30
|
+
Component.propTypes = {
|
31
|
+
name: PropTypes.string.isRequired,
|
32
|
+
|
33
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]),
|
34
|
+
|
35
|
+
// Custom class name. This will be appended to the default class.
|
36
|
+
className: PropTypes.string,
|
37
|
+
|
38
|
+
// The name of the attribute to use for the error message. Default: 'props.name'.
|
39
|
+
errorAttrName: PropTypes.string,
|
40
|
+
|
41
|
+
id: PropTypes.string,
|
42
|
+
hint: PropTypes.string,
|
43
|
+
disabled: PropTypes.bool
|
44
|
+
|
45
|
+
// All remaining non-descript props will be forwarded to the <input> element.
|
46
|
+
}
|
47
|
+
|
48
|
+
export default Component
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium::UI::Form::Fields
|
4
|
+
# Renders a checkbox field similar to how Rails handles it.
|
5
|
+
#
|
6
|
+
# A predicate attribute name can be given:
|
7
|
+
#
|
8
|
+
# checkbox_field :active?
|
9
|
+
#
|
10
|
+
class Checkbox < Base
|
11
|
+
register_element :pui_checkbox
|
12
|
+
|
13
|
+
def view_template
|
14
|
+
checked = ActiveModel::Type::Boolean.new.cast(value.nil? ? false : value)
|
15
|
+
|
16
|
+
checked_value = attributes.delete(:checked_value) || '1'
|
17
|
+
unchecked_value = attributes.delete(:unchecked_value) || '0'
|
18
|
+
|
19
|
+
# TODO: use component
|
20
|
+
# render Proscenium::UI::Fields::Checkbox::Component.new field_name, checked:
|
21
|
+
|
22
|
+
field :pui_checkbox do
|
23
|
+
label do |content|
|
24
|
+
input(name: field_name, type: :hidden, value: unchecked_value, **attributes)
|
25
|
+
input(name: field_name, type: :checkbox, value: checked_value, checked:, **attributes)
|
26
|
+
yield_content_with_no_args { content }
|
27
|
+
end
|
28
|
+
hint
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
@layer hue-component {
|
2
|
+
.field_wrapper {
|
3
|
+
@mixin fieldWrapper from url('/hue/lib/hue/mixins/form.mixin.css');
|
4
|
+
}
|
5
|
+
|
6
|
+
.inputs {
|
7
|
+
display: flex;
|
8
|
+
|
9
|
+
& div {
|
10
|
+
padding-right: 10px;
|
11
|
+
}
|
12
|
+
|
13
|
+
& label > span {
|
14
|
+
color: var(--gray6);
|
15
|
+
font-size: var(--12px);
|
16
|
+
margin: 0 0 0.2em 0.2em;
|
17
|
+
}
|
18
|
+
|
19
|
+
> div {
|
20
|
+
width: 7em;
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
.hint {
|
25
|
+
@mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
|
26
|
+
}
|
27
|
+
}
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import clsx from 'clsx'
|
2
|
+
import PropTypes from 'prop-types'
|
3
|
+
|
4
|
+
import dsx from '/hue/lib/hue/utils/dsx'
|
5
|
+
import { useFormError } from '../../hooks'
|
6
|
+
|
7
|
+
import styles from './index.module.css'
|
8
|
+
|
9
|
+
const Input = ({
|
10
|
+
label,
|
11
|
+
hint,
|
12
|
+
className,
|
13
|
+
inputClassName,
|
14
|
+
errorAttrName,
|
15
|
+
inputRef,
|
16
|
+
children,
|
17
|
+
...props
|
18
|
+
}) => {
|
19
|
+
const [error, hasError] = useFormError(errorAttrName || props.name)
|
20
|
+
|
21
|
+
return (
|
22
|
+
<div className={clsx(styles.fieldWrapper, className)} {...dsx({ fieldError: hasError })}>
|
23
|
+
<label>
|
24
|
+
<span>
|
25
|
+
{label ? <span>{label}</span> : null}
|
26
|
+
{hasError ? <span>{error}</span> : null}
|
27
|
+
</span>
|
28
|
+
|
29
|
+
{children || <input className={inputClassName || styles.input} {...props} ref={inputRef} />}
|
30
|
+
</label>
|
31
|
+
|
32
|
+
{hint ? <div className={styles.hint}>{hint}</div> : null}
|
33
|
+
</div>
|
34
|
+
)
|
35
|
+
}
|
36
|
+
Input.displayName = 'Hue.Form.Fields.Input'
|
37
|
+
Input.propTypes = {
|
38
|
+
name: PropTypes.string.isRequired,
|
39
|
+
|
40
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]),
|
41
|
+
|
42
|
+
// Input `type` attribute. Default: 'text'.
|
43
|
+
type: PropTypes.string,
|
44
|
+
|
45
|
+
// Custom class name. This will be appended to the default class.
|
46
|
+
className: PropTypes.string,
|
47
|
+
|
48
|
+
// Custom class name for the actual input element. This will replace the default class.
|
49
|
+
inputClassName: PropTypes.string,
|
50
|
+
|
51
|
+
// The name of the attribute to use for the error message. Default: 'props.name'.
|
52
|
+
errorAttrName: PropTypes.string,
|
53
|
+
|
54
|
+
children: PropTypes.node,
|
55
|
+
|
56
|
+
inputRef: PropTypes.oneOfType([
|
57
|
+
PropTypes.func,
|
58
|
+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
|
59
|
+
]),
|
60
|
+
|
61
|
+
id: PropTypes.string,
|
62
|
+
hint: PropTypes.string,
|
63
|
+
disabled: PropTypes.bool
|
64
|
+
|
65
|
+
// All remaining non-descript props will be forwarded to the <input> element.
|
66
|
+
}
|
67
|
+
Input.defaultProps = {
|
68
|
+
type: 'text'
|
69
|
+
}
|
70
|
+
|
71
|
+
export default Input
|
@@ -0,0 +1,13 @@
|
|
1
|
+
@layer hue-component {
|
2
|
+
.fieldWrapper {
|
3
|
+
@mixin fieldWrapper from url('/hue/lib/hue/mixins/form.mixin.css');
|
4
|
+
}
|
5
|
+
|
6
|
+
.input {
|
7
|
+
@mixin input from url('/hue/lib/hue/mixins/input.mixin.css');
|
8
|
+
}
|
9
|
+
|
10
|
+
.hint {
|
11
|
+
@mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
|
12
|
+
}
|
13
|
+
}
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium::UI::Form::Fields
|
4
|
+
# Render a group of <radio> inputs for the given model attribute. It supports ActiveRecord
|
5
|
+
# associations and enums.
|
6
|
+
#
|
7
|
+
# ## Supported options
|
8
|
+
#
|
9
|
+
# - options [Array] a list of options where each will render a radio input. If this is given, the
|
10
|
+
# automatic detection of options will be disabled. A flat array of strings will be used for
|
11
|
+
# both the label and value. While an Array of nested two-level arrays will be used.
|
12
|
+
class RadioGroup < Base
|
13
|
+
register_element :pui_radio_group
|
14
|
+
|
15
|
+
def before_template
|
16
|
+
@options_from_attributes = attributes.delete(:options)
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def view_template
|
22
|
+
field :pui_radio_group do
|
23
|
+
label
|
24
|
+
|
25
|
+
div part: :radio_group_inputs do
|
26
|
+
options.each do |opt|
|
27
|
+
form.radio_input(*attribute, name: field_name, value: opt[:value], label: opt[:label],
|
28
|
+
checked: opt[:checked], **attributes)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
hint
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def field_name(*names, multiple: false)
|
39
|
+
names.prepend association_attribute? ? association_attribute : attribute.last
|
40
|
+
|
41
|
+
if nested?
|
42
|
+
if nested_attributes_association?
|
43
|
+
names.prepend "#{attribute.first}_attributes"
|
44
|
+
else
|
45
|
+
names.prepend attribute.first
|
46
|
+
end
|
47
|
+
elsif names.count == 1 && names.first.is_a?(String)
|
48
|
+
return names.first
|
49
|
+
end
|
50
|
+
|
51
|
+
form.field_name(*names, multiple:)
|
52
|
+
end
|
53
|
+
|
54
|
+
def options
|
55
|
+
if @options_from_attributes
|
56
|
+
@options_from_attributes.map do |x|
|
57
|
+
if x.is_a?(Array)
|
58
|
+
{ value: x.first, label: x.last, checked: checked?(x.first) }
|
59
|
+
else
|
60
|
+
{ value: x, label: x, checked: checked?(x) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
elsif enum_attribute?
|
64
|
+
fetch_enum_collection.map do |x|
|
65
|
+
{
|
66
|
+
value: x,
|
67
|
+
label: model_class.human_attribute_name("#{attribute.last}.#{x}"),
|
68
|
+
checked: checked?(x)
|
69
|
+
}
|
70
|
+
end
|
71
|
+
elsif association_attribute?
|
72
|
+
fetch_association_collection.map do |x|
|
73
|
+
{
|
74
|
+
value: x.id,
|
75
|
+
label: x.to_s,
|
76
|
+
checked: checked?(x)
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def value
|
83
|
+
if actual_model.respond_to?(model_attribute)
|
84
|
+
actual_model.public_send(model_attribute)
|
85
|
+
else
|
86
|
+
''
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Is the given `option` the current value (checked)?
|
91
|
+
def checked?(option)
|
92
|
+
if !option.is_a?(String) && !option.is_a?(Integer) && association_attribute?
|
93
|
+
reflection = association_reflection
|
94
|
+
key = if reflection.respond_to?(:options) && reflection.options[:primary_key]
|
95
|
+
reflection.options[:primary_key]
|
96
|
+
else
|
97
|
+
option.class.primary_key.to_s
|
98
|
+
end
|
99
|
+
option.attributes[key] == value
|
100
|
+
else
|
101
|
+
option.to_s == value.to_s
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def model_attribute
|
106
|
+
@model_attribute ||= association_attribute? ? association_attribute : attribute.last
|
107
|
+
end
|
108
|
+
|
109
|
+
def enum_attribute?
|
110
|
+
model_class.defined_enums.key?(attribute.last.to_s)
|
111
|
+
end
|
112
|
+
|
113
|
+
def association_attribute?
|
114
|
+
association_reflection.present?
|
115
|
+
end
|
116
|
+
|
117
|
+
def association_attribute
|
118
|
+
@association_attribute ||= begin
|
119
|
+
reflection = association_reflection
|
120
|
+
|
121
|
+
case reflection.macro
|
122
|
+
when :belongs_to
|
123
|
+
(reflection.respond_to?(:options) && reflection.options[:foreign_key]&.to_sym) ||
|
124
|
+
:"#{reflection.name}_id"
|
125
|
+
else
|
126
|
+
# Force the association to be preloaded for performance.
|
127
|
+
if actual_model.respond_to?(attribute.last)
|
128
|
+
target = actual_model.send(attribute.last)
|
129
|
+
target.to_a if target.respond_to?(:to_a)
|
130
|
+
end
|
131
|
+
|
132
|
+
:"#{reflection.name.to_s.singularize}_ids"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def association_reflection
|
138
|
+
@association_reflection ||= model_class.try :reflect_on_association, attribute.last
|
139
|
+
end
|
140
|
+
|
141
|
+
def fetch_association_collection
|
142
|
+
relation = association_reflection.klass.all
|
143
|
+
|
144
|
+
# association_reflection.macro == :has_many
|
145
|
+
|
146
|
+
if association_reflection.respond_to?(:scope) && association_reflection.scope
|
147
|
+
relation = if association_reflection.scope.parameters.any?
|
148
|
+
association_reflection.klass.instance_exec(actual_model,
|
149
|
+
&association_reflection.scope)
|
150
|
+
else
|
151
|
+
association_reflection.klass.instance_exec(&association_reflection.scope)
|
152
|
+
end
|
153
|
+
else
|
154
|
+
order = association_reflection.options[:order]
|
155
|
+
conditions = association_reflection.options[:conditions]
|
156
|
+
conditions = actual_model.instance_exec(&conditions) if conditions.respond_to?(:call)
|
157
|
+
|
158
|
+
relation = relation.where(conditions) if relation.respond_to?(:where) && conditions.present?
|
159
|
+
relation = relation.order(order) if relation.respond_to?(:order)
|
160
|
+
end
|
161
|
+
|
162
|
+
relation
|
163
|
+
end
|
164
|
+
|
165
|
+
def fetch_enum_collection
|
166
|
+
actual_model.defined_enums[attribute.last.to_s].keys
|
167
|
+
end
|
168
|
+
|
169
|
+
def model_class
|
170
|
+
@model_class ||= actual_model.class
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import clsx from 'clsx'
|
2
|
+
import PropTypes from 'prop-types'
|
3
|
+
|
4
|
+
import dsx from '/hue/lib/hue/utils/dsx'
|
5
|
+
import { useFormError } from '../../hooks'
|
6
|
+
|
7
|
+
import styles from './index.module.css'
|
8
|
+
|
9
|
+
const Component = ({ label, hint, className, errorAttrName, ...props }) => {
|
10
|
+
const [error, hasError] = useFormError(errorAttrName || props.name)
|
11
|
+
|
12
|
+
return (
|
13
|
+
<div className={clsx(styles.fieldWrapper, className)} {...dsx({ fieldError: hasError })}>
|
14
|
+
<label>
|
15
|
+
<input type="radio" {...props} />
|
16
|
+
|
17
|
+
<span>{label}</span>
|
18
|
+
</label>
|
19
|
+
|
20
|
+
{hasError ? <div className={styles.error}>{error}</div> : null}
|
21
|
+
{hint ? <div className={styles.hint}>{hint}</div> : null}
|
22
|
+
</div>
|
23
|
+
)
|
24
|
+
}
|
25
|
+
|
26
|
+
Component.displayName = 'Hue.Form.Fields.RadioInput'
|
27
|
+
Component.propTypes = {
|
28
|
+
name: PropTypes.string.isRequired,
|
29
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]).isRequired,
|
30
|
+
|
31
|
+
// Custom class name. This will be appended to the default class.
|
32
|
+
className: PropTypes.string,
|
33
|
+
|
34
|
+
// The name of the attribute to use for the error message. Default: 'props.name'.
|
35
|
+
errorAttrName: PropTypes.string,
|
36
|
+
|
37
|
+
id: PropTypes.string,
|
38
|
+
hint: PropTypes.string,
|
39
|
+
disabled: PropTypes.bool
|
40
|
+
|
41
|
+
// All remaining non-descript props will be forwarded to the <input> element.
|
42
|
+
}
|
43
|
+
|
44
|
+
export default Component
|
@@ -0,0 +1,13 @@
|
|
1
|
+
@layer hue-component {
|
2
|
+
.fieldWrapper {
|
3
|
+
@mixin radio from url('/hue/lib/hue/mixins/radio.mixin.css');
|
4
|
+
}
|
5
|
+
|
6
|
+
.hint {
|
7
|
+
@mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
|
8
|
+
}
|
9
|
+
|
10
|
+
.error {
|
11
|
+
@mixin fieldError from url('/hue/lib/hue/mixins/field.mixin.css');
|
12
|
+
}
|
13
|
+
}
|