uchi 0.1.8 → 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/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.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/ui/index/records_table/records_table.html.erb +1 -1
- data/app/controllers/uchi/repository_controller.rb +2 -2
- 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 +1 -1
- data/app/views/uchi/repository/show.html.erb +4 -4
- data/lib/uchi/field/configuration.rb +42 -5
- data/lib/uchi/repository.rb +19 -7
- data/lib/uchi/sort_order.rb +9 -4
- data/lib/uchi/version.rb +1 -1
- metadata +6 -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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) %>
|
|
@@ -129,11 +129,11 @@ module Uchi
|
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
def permitted_params_for_edit
|
|
132
|
-
@repository.fields_for_edit.map(&:permitted_param)
|
|
132
|
+
@repository.fields_for_edit(record: @record).map(&:permitted_param)
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def permitted_params_for_new
|
|
136
|
-
@repository.fields_for_new.map(&:permitted_param)
|
|
136
|
+
@repository.fields_for_new(record: @record || @repository.build).map(&:permitted_param)
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def record_params_for_edit
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<%= javascript_include_tag "uchi/application", "data-turbo-track": "reload" %>
|
|
1
|
+
<%= javascript_include_tag "uchi/application", "data-turbo-track": "reload", nonce: true %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
|
|
3
|
-
<html
|
|
3
|
+
<html lang="en">
|
|
4
4
|
<head>
|
|
5
5
|
<title>
|
|
6
6
|
<%= content_for?(:page_title) ? yield(:page_title) : "Uchi" %>
|
|
@@ -15,12 +15,10 @@
|
|
|
15
15
|
<%= render "layouts/uchi/stylesheets" %>
|
|
16
16
|
</head>
|
|
17
17
|
|
|
18
|
-
<body class="antialiased bg-neutral-secondary-medium h-
|
|
19
|
-
|
|
20
|
-
<%= render(partial: "uchi/navigation/main") %>
|
|
18
|
+
<body class="antialiased bg-neutral-secondary-medium p-2 h-screen overflow-hidden md:flex md:p-0">
|
|
19
|
+
<%= render(partial: "uchi/navigation/main") %>
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
</div>
|
|
21
|
+
<main class="py-6 overflow-x-hidden grow md:px-6"><%= yield %></main>
|
|
24
22
|
|
|
25
23
|
<%= render "layouts/uchi/flash_messages" %>
|
|
26
24
|
</body>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<ul class="p-4
|
|
1
|
+
<ul class="p-4 space-y-4 text-sm font-medium text-body">
|
|
2
2
|
<% @records.each do |record| %>
|
|
3
3
|
<%
|
|
4
4
|
checkbox_id = dom_id(record, [source_repository.model, @field_name, 'checkbox'].join("-"))
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
<input
|
|
13
13
|
type="checkbox"
|
|
14
14
|
id="<%= checkbox_id %>"
|
|
15
|
-
class="w-4 h-4 border border-default-strong rounded-xs bg-neutral-secondary-strong focus:ring-2 focus:ring-brand-soft
|
|
15
|
+
class="w-4 h-4 border cursor-pointer border-default-strong rounded-xs bg-neutral-secondary-strong focus:ring-2 focus:ring-brand-soft"
|
|
16
16
|
data-action="change->has-many#handleCheckboxChange"
|
|
17
17
|
data-has-many-target="checkbox"
|
|
18
|
-
|
|
18
|
+
<% if @current_values.include?(record) %>
|
|
19
|
+
checked
|
|
20
|
+
<% end %>
|
|
19
21
|
>
|
|
20
|
-
<label for="<%= checkbox_id %>" class="
|
|
22
|
+
<label for="<%= checkbox_id %>" class="text-sm font-medium cursor-pointer ms-2 text-heading">
|
|
21
23
|
<%= record_title(record) %>
|
|
22
24
|
</label>
|
|
23
25
|
</div>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<%= render(Uchi::Flowbite::Card.new) do %>
|
|
18
18
|
<%= form_with model: @record, url: @repository.routes.path_for(:update, id: @record.id) do |form| %>
|
|
19
19
|
<div class="space-y-6">
|
|
20
|
-
<% @repository.fields_for_edit.each do |field| %>
|
|
20
|
+
<% @repository.fields_for_edit(record: @record).each do |field| %>
|
|
21
21
|
<div>
|
|
22
22
|
<%= render(
|
|
23
23
|
field.edit_component(
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<%= render(Uchi::Flowbite::Card.new) do %>
|
|
16
16
|
<%= form_with model: @record, url: @repository.routes.path_for(:create) do |form| %>
|
|
17
17
|
<div class="space-y-6">
|
|
18
|
-
<% @repository.fields_for_new.each do |field| %>
|
|
18
|
+
<% @repository.fields_for_new(record: @record).each do |field| %>
|
|
19
19
|
<div>
|
|
20
20
|
<%= render(
|
|
21
21
|
field.new_component(
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
]) %>
|
|
40
40
|
<% end %>
|
|
41
41
|
|
|
42
|
+
<% visible_fields = @repository.fields_for_show(record: @record) %>
|
|
43
|
+
|
|
42
44
|
<%= render(Uchi::Ui::Show::AttributeFields.new(
|
|
43
|
-
attribute_fields:
|
|
45
|
+
attribute_fields: visible_fields.select { |field| field.group_as(:show) == :attributes },
|
|
44
46
|
record: @record,
|
|
45
47
|
repository: @repository
|
|
46
48
|
)) %>
|
|
47
49
|
|
|
48
|
-
<%
|
|
49
|
-
|
|
50
|
-
<% association_fields.each do |field| %>
|
|
50
|
+
<% visible_fields.select { |field| field.group_as(:show) == :associations }.each do |field| %>
|
|
51
51
|
<%= render(field.show_component(record: @record, repository: @repository)) %>
|
|
52
52
|
<% end %>
|
|
@@ -6,13 +6,15 @@ module Uchi
|
|
|
6
6
|
class Unset; end
|
|
7
7
|
|
|
8
8
|
DEFAULT_READER = ->(record, field_name) { record&.public_send(field_name) }
|
|
9
|
+
DEFAULT_VISIBLE = ->(_record) { true }
|
|
9
10
|
|
|
10
11
|
def initialize(*args)
|
|
11
12
|
super
|
|
12
13
|
@on = default_on
|
|
13
14
|
@reader = DEFAULT_READER
|
|
14
15
|
@searchable = default_searchable?
|
|
15
|
-
@
|
|
16
|
+
@visible = DEFAULT_VISIBLE
|
|
17
|
+
@sortable = default_sortable
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
# Sets or gets which actions this field should appear on.
|
|
@@ -89,6 +91,32 @@ module Uchi
|
|
|
89
91
|
!!@searchable
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
# Sets or gets a conditional proc that determines whether this field should
|
|
95
|
+
# be visible for a given record.
|
|
96
|
+
#
|
|
97
|
+
# When called with a proc argument, sets the visibility condition and returns
|
|
98
|
+
# self for chaining. When called without arguments, returns the current proc.
|
|
99
|
+
#
|
|
100
|
+
# @param visible_proc [Proc] A callable that receives the record and returns
|
|
101
|
+
# a boolean indicating whether the field should be visible.
|
|
102
|
+
# Raises ArgumentError for non-callables.
|
|
103
|
+
# @return [self, Proc] Returns self for method chaining when setting,
|
|
104
|
+
# or the current proc when getting
|
|
105
|
+
#
|
|
106
|
+
# @example Setting
|
|
107
|
+
# Field::String.new(:id).visible(lambda { |record| record.id.even? })
|
|
108
|
+
#
|
|
109
|
+
# @example Getting
|
|
110
|
+
# field.visible # => #<Proc...>
|
|
111
|
+
def visible(visible_proc = Configuration::Unset)
|
|
112
|
+
return @visible if visible_proc == Configuration::Unset
|
|
113
|
+
|
|
114
|
+
raise ArgumentError, "visible must be callable" unless visible_proc.respond_to?(:call)
|
|
115
|
+
|
|
116
|
+
@visible = visible_proc
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
92
120
|
# Sets or gets whether and how this field is sortable.
|
|
93
121
|
#
|
|
94
122
|
# When called with an argument, sets sortable and returns self for chaining.
|
|
@@ -111,7 +139,7 @@ module Uchi
|
|
|
111
139
|
# @example Getting
|
|
112
140
|
# field.sortable # => true
|
|
113
141
|
def sortable(value = Configuration::Unset)
|
|
114
|
-
return @sortable if value == Configuration::Unset
|
|
142
|
+
return @sortable.nil? ? default_sortable : @sortable if value == Configuration::Unset
|
|
115
143
|
|
|
116
144
|
@sortable = value
|
|
117
145
|
self
|
|
@@ -119,9 +147,18 @@ module Uchi
|
|
|
119
147
|
|
|
120
148
|
# Returns true if the field is sortable
|
|
121
149
|
def sortable?
|
|
122
|
-
|
|
150
|
+
!!sortable
|
|
151
|
+
end
|
|
123
152
|
|
|
124
|
-
|
|
153
|
+
# Returns whether this field should be visible for the given record.
|
|
154
|
+
#
|
|
155
|
+
# Calls the visible proc with the record. Defaults to DEFAULT_VISIBLE,
|
|
156
|
+
# which always returns true.
|
|
157
|
+
#
|
|
158
|
+
# @param record [Object] The record to check visibility for
|
|
159
|
+
# @return [Boolean] Whether the field should be visible
|
|
160
|
+
def visible_for?(record)
|
|
161
|
+
!!@visible.call(record)
|
|
125
162
|
end
|
|
126
163
|
|
|
127
164
|
protected
|
|
@@ -134,7 +171,7 @@ module Uchi
|
|
|
134
171
|
false
|
|
135
172
|
end
|
|
136
173
|
|
|
137
|
-
def default_sortable
|
|
174
|
+
def default_sortable
|
|
138
175
|
true
|
|
139
176
|
end
|
|
140
177
|
end
|