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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/uchi/application.js +384 -317
  3. data/app/assets/stylesheets/uchi/application.css +8 -0
  4. data/app/assets/tailwind/uchi.css +5 -0
  5. data/app/components/uchi/field/base/show.html.erb +1 -0
  6. data/app/components/uchi/field/base.rb +24 -27
  7. data/app/components/uchi/field/belongs_to/edit.html.erb +1 -1
  8. data/app/components/uchi/field/belongs_to.rb +36 -2
  9. data/app/components/uchi/field/boolean/index.html.erb +9 -7
  10. data/app/components/uchi/field/boolean/show.html.erb +9 -7
  11. data/app/components/uchi/field/file/show.html.erb +1 -1
  12. data/app/components/uchi/field/file.rb +1 -1
  13. data/app/components/uchi/field/has_and_belongs_to_many.rb +19 -1
  14. data/app/components/uchi/field/has_many/edit.html.erb +1 -1
  15. data/app/components/uchi/field/has_many.rb +20 -0
  16. data/app/components/uchi/field/id/show.html.erb +1 -1
  17. data/app/components/uchi/field/image/index.html.erb +1 -1
  18. data/app/components/uchi/field/image/show.html.erb +2 -2
  19. data/app/components/uchi/field/image.rb +1 -1
  20. data/app/components/uchi/field/select/edit.html.erb +1 -0
  21. data/app/components/uchi/field/select/index.html.erb +1 -0
  22. data/app/components/uchi/field/select/show.html.erb +1 -0
  23. data/app/components/uchi/field/select.rb +144 -0
  24. data/app/components/uchi/field/text.rb +1 -1
  25. data/app/components/uchi/flowbite/button.rb +1 -1
  26. data/app/components/uchi/flowbite/input/field.rb +6 -0
  27. data/app/components/uchi/flowbite/input/label.rb +2 -0
  28. data/app/components/uchi/flowbite/input_field.rb +11 -1
  29. data/app/components/uchi/flowbite/styles.rb +34 -0
  30. data/app/components/uchi/ui/actions/dropdown/dropdown.html.erb +1 -1
  31. data/app/components/uchi/ui/index/records_table/records_table.html.erb +1 -1
  32. data/app/controllers/uchi/repository_controller.rb +24 -8
  33. data/app/views/layouts/uchi/_javascript.html.erb +1 -1
  34. data/app/views/layouts/uchi/application.html.erb +4 -6
  35. data/app/views/uchi/has_many/associated_records/index.html.erb +6 -4
  36. data/app/views/uchi/repository/edit.html.erb +1 -1
  37. data/app/views/uchi/repository/new.html.erb +2 -2
  38. data/app/views/uchi/repository/show.html.erb +4 -4
  39. data/lib/uchi/field/configuration.rb +43 -6
  40. data/lib/uchi/field.rb +11 -0
  41. data/lib/uchi/plugins.rb +21 -0
  42. data/lib/uchi/repository/routes.rb +9 -5
  43. data/lib/uchi/repository.rb +45 -6
  44. data/lib/uchi/routes.rb +27 -5
  45. data/lib/uchi/sort_order.rb +9 -4
  46. data/lib/uchi/version.rb +1 -1
  47. data/lib/uchi.rb +5 -0
  48. metadata +8 -5
  49. data/app/components/uchi/field/date/show.html.erb +0 -1
  50. data/app/components/uchi/field/number/show.html.erb +0 -1
  51. data/app/components/uchi/field/string/show.html.erb +0 -1
  52. 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
- # Uchi::Field::Base::Edit components render fields in the edit view.
7
- class Edit < ViewComponent::Base
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:, form:, repository:, label: nil, hint: nil)
9
+ def initialize(field:, record:, repository:)
11
10
  super()
12
11
 
13
12
  @field = field
14
- @form = form
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
- # Uchi::Field::Base::Show components render fields in the show view.
23
- class Show < ViewComponent::Base
24
- attr_reader :field, :record, :repository
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
- def initialize(field:, record:, repository:)
27
- super()
22
+ @value = field.value(record)
23
+ end
24
+ end
28
25
 
29
- @field = field
30
- @record = record
31
- @repository = repository
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 < ViewComponent::Base
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
- def initialize(field:, record:, repository:)
48
- super()
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.klass
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.klass
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
- <% if field.value(record) %>
2
- <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">
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
- </svg>
5
- <% else %>
6
- <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">
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
- </svg>
9
- <% end %>
9
+
10
+ <% end %>
11
+ </svg>
@@ -1,9 +1,11 @@
1
- <% if field.value(record) %>
2
- <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">
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
- </svg>
5
- <% else %>
6
- <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">
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
- </svg>
9
- <% end %>
9
+
10
+ <% end %>
11
+ </svg>
@@ -1,4 +1,4 @@
1
- <% attachment = field.value(record) %>
1
+ <% attachment = value %>
2
2
 
3
3
  <% if attachment.attached? %>
4
4
  <%= render Uchi::Flowbite::Link.new(href: Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)) do %>
@@ -29,7 +29,7 @@ module Uchi
29
29
  false
30
30
  end
31
31
 
32
- def default_sortable?
32
+ def default_sortable
33
33
  false
34
34
  end
35
35
  end
@@ -25,7 +25,7 @@ module Uchi
25
25
 
26
26
  class Show < Uchi::Field::Base::Show
27
27
  def associated_records
28
- records = field.value(record)
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,4 +1,4 @@
1
1
  <%= render(Uchi::Flowbite::Link.new(
2
2
  href: @repository.routes.path_for(:show, id: record.id),
3
- target: "_top").with_content(field.value(record))
3
+ target: "_top").with_content(value)
4
4
  ) %>
@@ -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 = field.value(record) %>
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 %>
@@ -30,7 +30,7 @@ module Uchi
30
30
  false
31
31
  end
32
32
 
33
- def default_sortable?
33
+ def default_sortable
34
34
  false
35
35
  end
36
36
  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
@@ -27,7 +27,7 @@ module Uchi
27
27
  protected
28
28
 
29
29
  def default_on
30
- [:edit, :show]
30
+ [:edit, :new, :show]
31
31
  end
32
32
 
33
33
  def default_searchable?
@@ -20,7 +20,7 @@ module Uchi::Flowbite
20
20
 
21
21
  class << self
22
22
  def classes(size: :default, state: :default, style: :default)
23
- style = styles.fetch(style) or raise "wut"
23
+ style = styles.fetch(style)
24
24
  classes = style.fetch(state)
25
25
  classes + sizes.fetch(size)
26
26
  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
 
@@ -36,6 +36,8 @@ module Uchi::Flowbite
36
36
  end
37
37
 
38
38
  def errors?
39
+ return false unless @object
40
+
39
41
  @object.errors.include?(@attribute.intern)
40
42
  end
41
43
 
@@ -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
- "#{@form.object_name}_#{@attribute}_hint"
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&.name %>
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) %>