proscenium 0.19.0.beta4-x86_64-linux → 0.19.0.beta6-x86_64-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,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 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
|
+
}
|