uchi 0.1.7 → 0.2.0
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/app/assets/javascripts/uchi/application.js +384 -317
- data/app/assets/stylesheets/uchi/application.css +8 -0
- data/app/assets/tailwind/uchi.css +5 -0
- data/app/components/uchi/field/base/show.html.erb +1 -0
- data/app/components/uchi/field/base.rb +24 -27
- data/app/components/uchi/field/belongs_to/edit.html.erb +1 -1
- data/app/components/uchi/field/belongs_to.rb +36 -2
- data/app/components/uchi/field/boolean/index.html.erb +9 -7
- data/app/components/uchi/field/boolean/show.html.erb +9 -7
- data/app/components/uchi/field/file/show.html.erb +1 -1
- data/app/components/uchi/field/file.rb +1 -1
- data/app/components/uchi/field/has_and_belongs_to_many.rb +19 -1
- data/app/components/uchi/field/has_many/edit.html.erb +1 -1
- data/app/components/uchi/field/has_many.rb +20 -0
- data/app/components/uchi/field/id/show.html.erb +1 -1
- data/app/components/uchi/field/image/index.html.erb +1 -1
- data/app/components/uchi/field/image/show.html.erb +2 -2
- data/app/components/uchi/field/image.rb +1 -1
- data/app/components/uchi/field/select/edit.html.erb +1 -0
- data/app/components/uchi/field/select/index.html.erb +1 -0
- data/app/components/uchi/field/select/show.html.erb +1 -0
- data/app/components/uchi/field/select.rb +144 -0
- data/app/components/uchi/field/text.rb +1 -1
- data/app/components/uchi/flowbite/button.rb +1 -1
- data/app/components/uchi/flowbite/input/field.rb +6 -0
- data/app/components/uchi/flowbite/input/label.rb +2 -0
- data/app/components/uchi/flowbite/input_field.rb +11 -1
- data/app/components/uchi/flowbite/styles.rb +34 -0
- data/app/components/uchi/ui/actions/dropdown/dropdown.html.erb +1 -1
- data/app/components/uchi/ui/index/records_table/records_table.html.erb +1 -1
- data/app/controllers/uchi/repository_controller.rb +24 -8
- data/app/views/layouts/uchi/_javascript.html.erb +1 -1
- data/app/views/layouts/uchi/application.html.erb +4 -6
- data/app/views/uchi/has_many/associated_records/index.html.erb +6 -4
- data/app/views/uchi/repository/edit.html.erb +1 -1
- data/app/views/uchi/repository/new.html.erb +2 -2
- data/app/views/uchi/repository/show.html.erb +4 -4
- data/lib/uchi/field/configuration.rb +43 -6
- data/lib/uchi/field.rb +11 -0
- data/lib/uchi/plugins.rb +21 -0
- data/lib/uchi/repository/routes.rb +9 -5
- data/lib/uchi/repository.rb +45 -6
- data/lib/uchi/routes.rb +27 -5
- data/lib/uchi/sort_order.rb +9 -4
- data/lib/uchi/version.rb +1 -1
- data/lib/uchi.rb +5 -0
- metadata +8 -5
- data/app/components/uchi/field/date/show.html.erb +0 -1
- data/app/components/uchi/field/number/show.html.erb +0 -1
- data/app/components/uchi/field/string/show.html.erb +0 -1
- data/app/components/uchi/field/text/show.html.erb +0 -1
|
@@ -669,6 +669,9 @@
|
|
|
669
669
|
text-overflow: ellipsis;
|
|
670
670
|
white-space: nowrap;
|
|
671
671
|
}
|
|
672
|
+
.overflow-hidden {
|
|
673
|
+
overflow: hidden;
|
|
674
|
+
}
|
|
672
675
|
.overflow-x-auto {
|
|
673
676
|
overflow-x: auto;
|
|
674
677
|
}
|
|
@@ -1339,6 +1342,11 @@
|
|
|
1339
1342
|
}
|
|
1340
1343
|
}
|
|
1341
1344
|
}
|
|
1345
|
+
.md\:p-0 {
|
|
1346
|
+
@media (width >= 48rem) {
|
|
1347
|
+
padding: calc(var(--spacing) * 0);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1342
1350
|
.md\:px-0 {
|
|
1343
1351
|
@media (width >= 48rem) {
|
|
1344
1352
|
padding-inline: calc(var(--spacing) * 0);
|
|
@@ -19,3 +19,8 @@ running the following command:
|
|
|
19
19
|
|
|
20
20
|
/* Don't include classes used in docs into Uchis stylesheet */
|
|
21
21
|
@source not "docs";
|
|
22
|
+
|
|
23
|
+
/* Don't include classes from the the Tailwind-generated stylesheet into the
|
|
24
|
+
list of classes to preserve in the Tailwind-generated stylesheet. Doing so means
|
|
25
|
+
that classes would never be purged */
|
|
26
|
+
@source not "app/assets/stylesheets/uchi/application.css";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= value %>
|
|
@@ -3,39 +3,40 @@
|
|
|
3
3
|
module Uchi
|
|
4
4
|
class Field
|
|
5
5
|
class Base < Field
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
attr_reader :field, :form, :record, :repository, :label, :hint
|
|
6
|
+
class Component < ViewComponent::Base
|
|
7
|
+
attr_reader :field, :record, :repository
|
|
9
8
|
|
|
10
|
-
def initialize(field:,
|
|
9
|
+
def initialize(field:, record:, repository:)
|
|
11
10
|
super()
|
|
12
11
|
|
|
13
12
|
@field = field
|
|
14
|
-
@
|
|
15
|
-
@label = label
|
|
16
|
-
@hint = hint
|
|
17
|
-
@record = form.object
|
|
13
|
+
@record = record
|
|
18
14
|
@repository = repository
|
|
19
15
|
end
|
|
20
|
-
end
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
# Returns the raw value of the field as returned by the field's value
|
|
18
|
+
# method.
|
|
19
|
+
def value
|
|
20
|
+
return @value if instance_variable_defined?(:@value)
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
@value = field.value(record)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
# Uchi::Field::Base::Edit components render fields in the edit view.
|
|
27
|
+
class Edit < Component
|
|
28
|
+
attr_reader :form, :label, :hint
|
|
29
|
+
|
|
30
|
+
def initialize(field:, form:, repository:, label: nil, hint: nil)
|
|
31
|
+
super(field:, record: form.object, repository:)
|
|
32
|
+
@form = form
|
|
33
|
+
@label = label
|
|
34
|
+
@hint = hint
|
|
32
35
|
end
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# Uchi::Field::Base::Index components render fields in the index view.
|
|
36
|
-
class Index <
|
|
37
|
-
attr_reader :field, :record, :repository
|
|
38
|
-
|
|
39
|
+
class Index < Component
|
|
39
40
|
class << self
|
|
40
41
|
# Returns the CSS classes to apply to the td or th of the table where
|
|
41
42
|
# this field is rendered.
|
|
@@ -43,14 +44,10 @@ module Uchi
|
|
|
43
44
|
[]
|
|
44
45
|
end
|
|
45
46
|
end
|
|
47
|
+
end
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@field = field
|
|
51
|
-
@record = record
|
|
52
|
-
@repository = repository
|
|
53
|
-
end
|
|
49
|
+
# Uchi::Field::Base::Show components render fields in the show view.
|
|
50
|
+
class Show < Component
|
|
54
51
|
end
|
|
55
52
|
end
|
|
56
53
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
class="relative"
|
|
3
3
|
data-action="keydown.esc->belongs-to#closeDropdown"
|
|
4
4
|
data-controller="belongs-to"
|
|
5
|
-
data-belongs-to-backend-url-value="<%= helpers.belongs_to_associated_records_path(field: field.name, model: repository.model, record_id: record&.id) %>"
|
|
5
|
+
data-belongs-to-backend-url-value="<%= helpers.uchi.belongs_to_associated_records_path(field: field.name, model: repository.model, record_id: record&.id) %>"
|
|
6
6
|
>
|
|
7
7
|
<%= render(Uchi::Flowbite::Input::Label.new(attribute: field.name, form: form, options: {for: dom_id_for_toggle})) { label } %>
|
|
8
8
|
|
|
@@ -19,7 +19,14 @@ module Uchi
|
|
|
19
19
|
"No association named #{field.name.inspect} found on #{record.class}"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
model = reflection.
|
|
22
|
+
model = if reflection.polymorphic?
|
|
23
|
+
associated_record&.class
|
|
24
|
+
else
|
|
25
|
+
reflection.klass
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return nil if model.nil?
|
|
29
|
+
|
|
23
30
|
repository_class = Uchi::Repository.for_model(model)
|
|
24
31
|
repository_class.new
|
|
25
32
|
end
|
|
@@ -34,7 +41,14 @@ module Uchi
|
|
|
34
41
|
|
|
35
42
|
def associated_repository
|
|
36
43
|
@associated_repository ||= begin
|
|
37
|
-
model = reflection.
|
|
44
|
+
model = if reflection.polymorphic?
|
|
45
|
+
associated_record&.class
|
|
46
|
+
else
|
|
47
|
+
reflection.klass
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
return nil if model.nil?
|
|
51
|
+
|
|
38
52
|
repository_class = Uchi::Repository.for_model(model)
|
|
39
53
|
repository_class.new
|
|
40
54
|
end
|
|
@@ -125,11 +139,31 @@ module Uchi
|
|
|
125
139
|
:attributes
|
|
126
140
|
end
|
|
127
141
|
|
|
142
|
+
# Returns the actions this field should appear on.
|
|
143
|
+
#
|
|
144
|
+
# For polymorphic associations, excludes :edit and :new to prevent showing
|
|
145
|
+
# the field on forms where the type cannot be determined.
|
|
146
|
+
def on(*actions)
|
|
147
|
+
on = super
|
|
148
|
+
return on - [:edit, :new] if polymorphic? && actions.empty?
|
|
149
|
+
|
|
150
|
+
on
|
|
151
|
+
end
|
|
152
|
+
|
|
128
153
|
def param_key
|
|
129
154
|
# TODO: This is too naive. We need to match this to the actual foreign
|
|
130
155
|
# key of the model.
|
|
131
156
|
:"#{name}_id"
|
|
132
157
|
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def polymorphic?
|
|
162
|
+
return false unless repository
|
|
163
|
+
|
|
164
|
+
reflection = repository.model.reflect_on_association(name)
|
|
165
|
+
reflection&.polymorphic? || false
|
|
166
|
+
end
|
|
133
167
|
end
|
|
134
168
|
end
|
|
135
169
|
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
|
2
|
+
<% if field.value(record) %>
|
|
3
|
+
|
|
3
4
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.5 11.5 11 14l4-4m6 2a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
|
4
|
-
|
|
5
|
-
<% else %>
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
<% else %>
|
|
7
|
+
|
|
7
8
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.757 12h8.486M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
|
8
|
-
|
|
9
|
-
<% end %>
|
|
9
|
+
|
|
10
|
+
<% end %>
|
|
11
|
+
</svg>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
|
2
|
+
<% if value %>
|
|
3
|
+
|
|
3
4
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.5 11.5 11 14l4-4m6 2a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
|
4
|
-
|
|
5
|
-
<% else %>
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
<% else %>
|
|
7
|
+
|
|
7
8
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.757 12h8.486M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
|
8
|
-
|
|
9
|
-
<% end %>
|
|
9
|
+
|
|
10
|
+
<% end %>
|
|
11
|
+
</svg>
|
|
@@ -25,7 +25,7 @@ module Uchi
|
|
|
25
25
|
|
|
26
26
|
class Show < Uchi::Field::Base::Show
|
|
27
27
|
def associated_records
|
|
28
|
-
records =
|
|
28
|
+
records = value
|
|
29
29
|
|
|
30
30
|
associated_repository.find_all(scope: records)
|
|
31
31
|
end
|
|
@@ -126,6 +126,24 @@ module Uchi
|
|
|
126
126
|
def association
|
|
127
127
|
@association ||= repository.model.reflect_on_association(name)
|
|
128
128
|
end
|
|
129
|
+
|
|
130
|
+
def default_sortable
|
|
131
|
+
lambda { |query, direction|
|
|
132
|
+
reflection = query.klass.reflect_on_association(name)
|
|
133
|
+
return query unless reflection
|
|
134
|
+
|
|
135
|
+
associated_table = reflection.klass.table_name
|
|
136
|
+
associated_primary_key = reflection.klass.primary_key
|
|
137
|
+
count = Arel.sql("COUNT(#{associated_table}.#{associated_primary_key})")
|
|
138
|
+
primary_key = query.klass.arel_table[query.klass.primary_key]
|
|
139
|
+
|
|
140
|
+
SortOrder.new(count, direction).apply(
|
|
141
|
+
query
|
|
142
|
+
.left_outer_joins(name)
|
|
143
|
+
.group(primary_key)
|
|
144
|
+
).order(primary_key.asc)
|
|
145
|
+
}
|
|
146
|
+
end
|
|
129
147
|
end
|
|
130
148
|
end
|
|
131
149
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
class="relative"
|
|
3
3
|
data-action="keydown.esc->has-many#closeDropdown"
|
|
4
4
|
data-controller="has-many"
|
|
5
|
-
data-has-many-backend-url-value="<%= helpers.has_many_associated_records_path(field: field.name, model: repository.model, record_id: record&.id) %>"
|
|
5
|
+
data-has-many-backend-url-value="<%= helpers.uchi.has_many_associated_records_path(field: field.name, model: repository.model, record_id: record&.id) %>"
|
|
6
6
|
data-has-many-field-name-value="<%= field_name_for_input %>"
|
|
7
7
|
>
|
|
8
8
|
<%= render(Uchi::Flowbite::Input::Label.new(attribute: field.name, form: form, options: {for: dom_id_for_toggle})) { label } %>
|
|
@@ -150,6 +150,26 @@ module Uchi
|
|
|
150
150
|
def permitted_param
|
|
151
151
|
{param_key => []}
|
|
152
152
|
end
|
|
153
|
+
|
|
154
|
+
protected
|
|
155
|
+
|
|
156
|
+
def default_sortable
|
|
157
|
+
lambda { |query, direction|
|
|
158
|
+
reflection = query.klass.reflect_on_association(name)
|
|
159
|
+
return query unless reflection
|
|
160
|
+
|
|
161
|
+
associated_table = reflection.klass.table_name
|
|
162
|
+
associated_primary_key = reflection.klass.primary_key
|
|
163
|
+
count = Arel.sql("COUNT(#{associated_table}.#{associated_primary_key})")
|
|
164
|
+
primary_key = query.klass.arel_table[query.klass.primary_key]
|
|
165
|
+
|
|
166
|
+
SortOrder.new(count, direction).apply(
|
|
167
|
+
query
|
|
168
|
+
.left_outer_joins(name)
|
|
169
|
+
.group(primary_key)
|
|
170
|
+
).order(primary_key.asc)
|
|
171
|
+
}
|
|
172
|
+
end
|
|
153
173
|
end
|
|
154
174
|
end
|
|
155
175
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<% attachment = field.value(record) %>
|
|
2
2
|
|
|
3
3
|
<% if attachment.attached? %>
|
|
4
|
-
<%= image_tag attachment.variant(resize_to_limit: [100, 100]), class: "h-12 w-12 object-cover rounded" %>
|
|
4
|
+
<%= image_tag attachment.variant(resize_to_limit: [100, 100]), alt: repository.translate.field_label(field), class: "h-12 w-12 object-cover rounded" %>
|
|
5
5
|
<% else %>
|
|
6
6
|
<%= render(Uchi::Ui::NoValue.new) %>
|
|
7
7
|
<% end %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<% attachment =
|
|
1
|
+
<% attachment = value %>
|
|
2
2
|
|
|
3
3
|
<% if attachment.attached? %>
|
|
4
|
-
<%= image_tag attachment, class: "max-w-full h-auto rounded-lg shadow-md" %>
|
|
4
|
+
<%= image_tag attachment, alt: repository.translate.field_label(field),class: "max-w-full h-auto rounded-lg shadow-md" %>
|
|
5
5
|
<% else %>
|
|
6
6
|
<%= render(Uchi::Ui::NoValue.new) %>
|
|
7
7
|
<% end %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render(Uchi::Flowbite::InputField::Select.new(**options)) %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= field.label_for(field.value(record)) %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= field.label_for(value) %>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Field
|
|
5
|
+
class Select < Field
|
|
6
|
+
class Edit < Uchi::Field::Base::Edit
|
|
7
|
+
def collection
|
|
8
|
+
if field.grouped?
|
|
9
|
+
field.options.map { |group, group_options| [group, group_options.map { |value, label| [label, value] }] }
|
|
10
|
+
else
|
|
11
|
+
field.flat_options.map { |value, label| [label, value] }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def options
|
|
18
|
+
options = {
|
|
19
|
+
attribute: field.name,
|
|
20
|
+
collection: collection,
|
|
21
|
+
form: form,
|
|
22
|
+
label: {content: label}
|
|
23
|
+
}
|
|
24
|
+
options[:hint] = {content: hint} if hint.present?
|
|
25
|
+
options
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Index < Uchi::Field::Base::Index
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Show < Uchi::Field::Base::Show
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Resolves and memoizes the options as a flat value => label Hash, keyed
|
|
36
|
+
# by the string representation of each option's value so #label_for
|
|
37
|
+
# can look up a label in constant time. Memoized so that a Proc given
|
|
38
|
+
# to #options is only called once per field instance - see
|
|
39
|
+
# #resolved_options for details. The memo is cleared whenever #options
|
|
40
|
+
# is called with a new value.
|
|
41
|
+
#
|
|
42
|
+
# When the same value appears in more than one group, the first
|
|
43
|
+
# matching label wins - matching how a browser's `<select>` element
|
|
44
|
+
# only ever selects the first option with a matching value.
|
|
45
|
+
def flat_options
|
|
46
|
+
@flat_options ||= resolved_options.each_with_object({}) { |(key, value), flat|
|
|
47
|
+
if value.is_a?(Hash)
|
|
48
|
+
value.each { |group_key, group_label| flat[group_key.to_s] = group_label unless flat.key?(group_key.to_s) }
|
|
49
|
+
else
|
|
50
|
+
flat[key.to_s] = value unless flat.key?(key.to_s)
|
|
51
|
+
end
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns true if the configured options are grouped, ie. a non-empty
|
|
56
|
+
# Hash whose values are all themselves Hashes of options.
|
|
57
|
+
def grouped?
|
|
58
|
+
resolved_options.present? && resolved_options.values.all? { |value| value.is_a?(Hash) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def initialize(name)
|
|
62
|
+
super
|
|
63
|
+
@options = {}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns the label to display for the given value, or nil if the value
|
|
67
|
+
# doesn't match any of the configured options.
|
|
68
|
+
#
|
|
69
|
+
# Values are compared using their string representation, matching how
|
|
70
|
+
# a `<select>` element compares its options' values against the
|
|
71
|
+
# persisted attribute value.
|
|
72
|
+
def label_for(value)
|
|
73
|
+
return if value.nil?
|
|
74
|
+
|
|
75
|
+
flat_options[value.to_s]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Sets or gets the options to choose between.
|
|
79
|
+
#
|
|
80
|
+
# When called with an argument, sets the options and returns self for
|
|
81
|
+
# chaining. When called without arguments, returns the current options,
|
|
82
|
+
# resolved to a Hash.
|
|
83
|
+
#
|
|
84
|
+
# @param options [Hash, Array, Proc] A hash mapping stored values to
|
|
85
|
+
# their human-readable labels. An array can be given instead, in which
|
|
86
|
+
# case each item is used as both the stored value and its label. A
|
|
87
|
+
# proc can also be given; it's called with no arguments and should
|
|
88
|
+
# return a Hash or an Array as described above.
|
|
89
|
+
#
|
|
90
|
+
# To group options, pass a Hash whose values are themselves a Hash or
|
|
91
|
+
# Array of options, keyed by the group label.
|
|
92
|
+
# @return [self, Hash] Returns self for method chaining when setting,
|
|
93
|
+
# or the options hash when getting
|
|
94
|
+
#
|
|
95
|
+
# @example Setting with a Hash
|
|
96
|
+
# Field::Select.new(:size).options({s: "Small", m: "Medium", l: "Large"})
|
|
97
|
+
#
|
|
98
|
+
# @example Setting with an Array
|
|
99
|
+
# Field::Select.new(:size).options(["Small", "Medium", "Large"])
|
|
100
|
+
#
|
|
101
|
+
# @example Setting with a Proc
|
|
102
|
+
# Field::Select.new(:size).options(-> { Size.pluck(:key, :name).to_h })
|
|
103
|
+
#
|
|
104
|
+
# @example Setting grouped options
|
|
105
|
+
# Field::Select.new(:size).options({
|
|
106
|
+
# "Letters" => {s: "Small", m: "Medium", l: "Large"},
|
|
107
|
+
# "Numbers" => ["32", "34", "36"]
|
|
108
|
+
# })
|
|
109
|
+
#
|
|
110
|
+
# @example Getting
|
|
111
|
+
# field.options # => {s: "Small", m: "Medium", l: "Large"}
|
|
112
|
+
def options(options = Configuration::Unset)
|
|
113
|
+
return resolved_options if options == Configuration::Unset
|
|
114
|
+
|
|
115
|
+
@options = options
|
|
116
|
+
@resolved_options = nil
|
|
117
|
+
@flat_options = nil
|
|
118
|
+
self
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def normalize(options)
|
|
124
|
+
case options
|
|
125
|
+
when Array
|
|
126
|
+
options.to_h { |option| [option, option] }
|
|
127
|
+
when Hash
|
|
128
|
+
options.transform_values { |value| value.is_a?(Array) ? value.to_h { |option| [option, option] } : value }
|
|
129
|
+
else
|
|
130
|
+
raise ArgumentError, "Field::Select options must be a Hash, an Array, or a Proc returning one of those, got #{options.class}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Resolves and memoizes the configured options. Memoized so that a Proc
|
|
135
|
+
# given to #options is only called once per field instance, even though
|
|
136
|
+
# #options, #grouped?, and #label_for are all called multiple times
|
|
137
|
+
# while rendering a single page (e.g. once per row on an index page).
|
|
138
|
+
# The memo is cleared whenever #options is called with a new value.
|
|
139
|
+
def resolved_options
|
|
140
|
+
@resolved_options ||= normalize(@options.respond_to?(:call) ? @options.call : @options)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -93,7 +93,13 @@ module Uchi::Flowbite
|
|
|
93
93
|
!!@disabled
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Returns true if the object has errors. Returns false if there is no
|
|
97
|
+
# object.
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean] true if there are errors, false otherwise.
|
|
96
100
|
def errors?
|
|
101
|
+
return false unless @object
|
|
102
|
+
|
|
97
103
|
@object.errors.include?(@attribute.intern)
|
|
98
104
|
end
|
|
99
105
|
|
|
@@ -67,7 +67,11 @@ module Uchi::Flowbite
|
|
|
67
67
|
renders_one :label
|
|
68
68
|
|
|
69
69
|
# Returns the errors for attribute
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<String>] An array of error messages for the attribute.
|
|
70
72
|
def errors
|
|
73
|
+
return [] unless @object
|
|
74
|
+
|
|
71
75
|
@object.errors[@attribute] || []
|
|
72
76
|
end
|
|
73
77
|
|
|
@@ -185,7 +189,13 @@ module Uchi::Flowbite
|
|
|
185
189
|
end
|
|
186
190
|
|
|
187
191
|
def id_for_hint_element
|
|
188
|
-
|
|
192
|
+
[
|
|
193
|
+
@form.object_name,
|
|
194
|
+
@attribute,
|
|
195
|
+
"hint"
|
|
196
|
+
]
|
|
197
|
+
.compact_blank
|
|
198
|
+
.join("_")
|
|
189
199
|
end
|
|
190
200
|
|
|
191
201
|
# @return [Hash] The keyword arguments for the input component.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi::Flowbite
|
|
4
|
+
class Styles
|
|
5
|
+
class StyleNotFoundError < ::KeyError; end
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def from_hash(styles_hash)
|
|
9
|
+
styles = Styles.new
|
|
10
|
+
styles_hash.each do |style_name, states_hash|
|
|
11
|
+
styles.add_style(style_name, states_hash)
|
|
12
|
+
end
|
|
13
|
+
styles
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_style(style_name, states_hash)
|
|
18
|
+
@styles[style_name] = Uchi::Flowbite::Style.new(states_hash)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fetch(style_name)
|
|
22
|
+
return @styles[style_name] if @styles.key?(style_name)
|
|
23
|
+
|
|
24
|
+
raise \
|
|
25
|
+
StyleNotFoundError,
|
|
26
|
+
"Style not found: #{style_name}. Available styles: " \
|
|
27
|
+
"#{@styles.keys.sort.join(", ")}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@styles = {}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
>
|
|
43
43
|
<% actions.each do |action| %>
|
|
44
44
|
<li role="menuitem">
|
|
45
|
-
<%= form_with url: helpers.actions_executions_path, method: :post, class: "block" do %>
|
|
45
|
+
<%= form_with url: helpers.uchi.actions_executions_path, method: :post, class: "block" do %>
|
|
46
46
|
<%= hidden_field_tag :model, repository.model.name %>
|
|
47
47
|
<%= hidden_field_tag :action_name, action.class.name %>
|
|
48
48
|
<%= hidden_field_tag :id, record.id %>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
class="px-6 py-3 font-medium whitespace-nowrap <%= component_class.classes_for_table_cell.join(" ") %>"
|
|
16
16
|
>
|
|
17
17
|
<% if field.sortable? %>
|
|
18
|
-
<% if field.name == sort_order&.
|
|
18
|
+
<% if field.name == sort_order&.column %>
|
|
19
19
|
<% if sort_order&.ascending? %>
|
|
20
20
|
<%= link_to(repository.routes.path_for(:index, query: query, scope: scope, sort: {:by => field.name, :direction => :desc})) do %>
|
|
21
21
|
<%= repository.translate.field_label(field) %>
|