compony 0.5.9 → 0.6.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/CHANGELOG.md +5 -0
- data/Gemfile.lock +1 -1
- data/README.md +21 -0
- data/VERSION +1 -1
- data/compony.gemspec +3 -3
- data/config/locales/de.yml +13 -0
- data/config/locales/en.yml +13 -0
- data/config/locales/fr.yml +14 -1
- data/doc/ComponentGenerator.html +16 -4
- data/doc/Components.html +3 -3
- data/doc/ComponentsGenerator.html +3 -3
- data/doc/Compony/Component.html +4 -4
- data/doc/Compony/ComponentMixins/Default/Labelling.html +3 -3
- data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +3 -3
- data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +3 -3
- data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +3 -3
- data/doc/Compony/ComponentMixins/Default/Standalone.html +3 -3
- data/doc/Compony/ComponentMixins/Default.html +3 -3
- data/doc/Compony/ComponentMixins/Resourceful.html +4 -4
- data/doc/Compony/ComponentMixins.html +3 -3
- data/doc/Compony/Components/Button.html +3 -3
- data/doc/Compony/Components/Destroy.html +3 -3
- data/doc/Compony/Components/Edit.html +3 -3
- data/doc/Compony/Components/Form.html +3 -3
- data/doc/Compony/Components/Index.html +172 -0
- data/doc/Compony/Components/List.html +2731 -0
- data/doc/Compony/Components/New.html +3 -3
- data/doc/Compony/Components/Show.html +740 -0
- data/doc/Compony/Components/WithForm.html +3 -3
- data/doc/Compony/Components.html +5 -5
- data/doc/Compony/ControllerMixin.html +3 -3
- data/doc/Compony/Engine.html +3 -3
- data/doc/Compony/MethodAccessibleHash.html +3 -3
- data/doc/Compony/ModelFields/Anchormodel.html +136 -4
- data/doc/Compony/ModelFields/Association.html +4 -4
- data/doc/Compony/ModelFields/Attachment.html +4 -4
- data/doc/Compony/ModelFields/Base.html +143 -13
- data/doc/Compony/ModelFields/Boolean.html +136 -4
- data/doc/Compony/ModelFields/Color.html +4 -4
- data/doc/Compony/ModelFields/Currency.html +4 -4
- data/doc/Compony/ModelFields/Date.html +4 -4
- data/doc/Compony/ModelFields/Datetime.html +4 -4
- data/doc/Compony/ModelFields/Decimal.html +4 -4
- data/doc/Compony/ModelFields/Email.html +4 -4
- data/doc/Compony/ModelFields/Float.html +4 -4
- data/doc/Compony/ModelFields/Integer.html +83 -5
- data/doc/Compony/ModelFields/Percentage.html +4 -4
- data/doc/Compony/ModelFields/Phone.html +4 -4
- data/doc/Compony/ModelFields/RichText.html +4 -4
- data/doc/Compony/ModelFields/String.html +4 -4
- data/doc/Compony/ModelFields/Text.html +4 -4
- data/doc/Compony/ModelFields/Time.html +4 -4
- data/doc/Compony/ModelFields/Url.html +4 -4
- data/doc/Compony/ModelFields.html +3 -3
- data/doc/Compony/ModelMixin.html +26 -26
- data/doc/Compony/NaturalOrdering.html +3 -3
- data/doc/Compony/RequestContext.html +3 -3
- data/doc/Compony/Version.html +3 -3
- data/doc/Compony/ViewHelpers.html +3 -3
- data/doc/Compony.html +4 -4
- data/doc/ComponyController.html +3 -3
- data/doc/_index.html +25 -4
- data/doc/class_list.html +3 -6
- data/doc/css/full_list.css +3 -3
- data/doc/css/style.css +0 -6
- data/doc/file.README.html +37 -10
- data/doc/file_list.html +2 -5
- data/doc/frames.html +5 -10
- data/doc/index.html +37 -10
- data/doc/js/app.js +264 -294
- data/doc/js/full_list.js +4 -30
- data/doc/method_list.html +423 -114
- data/doc/top-level-namespace.html +3 -3
- data/lib/compony/components/index.rb +33 -0
- data/lib/compony/components/list.rb +418 -0
- data/lib/compony/components/show.rb +116 -0
- data/lib/compony/model_fields/anchormodel.rb +13 -0
- data/lib/compony/model_fields/base.rb +12 -0
- data/lib/compony/model_fields/boolean.rb +13 -0
- data/lib/compony/model_fields/integer.rb +3 -0
- data/lib/compony/model_mixin.rb +5 -0
- data/lib/compony.rb +3 -0
- data/lib/generators/component/component_generator.rb +6 -0
- data/lib/generators/component/templates/index.rb.erb +2 -0
- data/lib/generators/component/templates/list.rb.erb +7 -0
- data/lib/generators/component/templates/show.rb.erb +2 -0
- metadata +10 -1
@@ -6,7 +6,7 @@
|
|
6
6
|
<title>
|
7
7
|
Top Level Namespace
|
8
8
|
|
9
|
-
— Documentation by YARD 0.9.
|
9
|
+
— Documentation by YARD 0.9.34
|
10
10
|
|
11
11
|
</title>
|
12
12
|
|
@@ -102,9 +102,9 @@
|
|
102
102
|
</div>
|
103
103
|
|
104
104
|
<div id="footer">
|
105
|
-
Generated on Fri
|
105
|
+
Generated on Fri Sep 5 14:00:24 2025 by
|
106
106
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
107
|
-
0.9.
|
107
|
+
0.9.34 (ruby-3.3.5).
|
108
108
|
</div>
|
109
109
|
|
110
110
|
</div>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Compony
|
2
|
+
module Components
|
3
|
+
# @api description
|
4
|
+
# This component is used for the Rails index paradigm. Nests the `::List` component of the same family.
|
5
|
+
class Index < Compony::Component
|
6
|
+
include Compony::ComponentMixins::Resourceful
|
7
|
+
|
8
|
+
setup do
|
9
|
+
standalone path: family_name do
|
10
|
+
verb :get do
|
11
|
+
authorize { can? :index, data_class }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
label(:all) { data_class.model_name.human(count: 2) }
|
16
|
+
|
17
|
+
load_data do
|
18
|
+
@data = data_class.accessible_by(controller.current_ability)
|
19
|
+
end
|
20
|
+
|
21
|
+
action :new do
|
22
|
+
if Compony.comp_class_for(:new, data_class)
|
23
|
+
Compony.button(:new, data_class.model_name.plural)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
content do
|
28
|
+
concat resourceful_sub_comp(component.class.module_parent.const_get(:List)).render(controller)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,418 @@
|
|
1
|
+
module Compony
|
2
|
+
module Components
|
3
|
+
# @api description
|
4
|
+
# This component is used for the Rails list paradigm. It is meant to be nested inside the same family's `::Index` or an owner's `::Show` component.
|
5
|
+
class List < Compony::Component
|
6
|
+
include Compony::ComponentMixins::Resourceful
|
7
|
+
|
8
|
+
# The following parameters are meant for the case where this component is nested and therefore instanciated by a parent comp.
|
9
|
+
# If the component is to configure itself, use the DSL calls below instead.
|
10
|
+
# @param skip_pagination [Boolean] Disables pagination (caution: all records will be loaded)
|
11
|
+
# @param results_per_page [Integer] In case pagination is active, defines the amount of records to display per page.
|
12
|
+
# @param skip_filtering [Boolean] Disables filtering entirely (sorting is independent of this setting).
|
13
|
+
# @param skip_sorting [Boolean] Disables sorting entirely (both links and sorting input in filter).
|
14
|
+
# @param skip_sorting_in_filter [Boolean] Disables sorting in filter.
|
15
|
+
# @param skip_sorting_links [Boolean] Disables sorting links.
|
16
|
+
# @param skip_columns [Array] Column names to be skipped.
|
17
|
+
# @param skip_row_actions [Array] Row action names to be skipped.
|
18
|
+
# @param skip_filters [Array] Filter names to be skipped.
|
19
|
+
def initialize(*,
|
20
|
+
skip_pagination: false,
|
21
|
+
results_per_page: 20,
|
22
|
+
skip_filtering: false,
|
23
|
+
skip_sorting: false,
|
24
|
+
skip_sorting_in_filter: false,
|
25
|
+
skip_sorting_links: false,
|
26
|
+
skip_columns: [],
|
27
|
+
skip_row_actions: [],
|
28
|
+
skip_filters: [],
|
29
|
+
**)
|
30
|
+
@pagination = !skip_pagination
|
31
|
+
@results_per_page = results_per_page
|
32
|
+
@filtering = !skip_filtering
|
33
|
+
@sorting_in_filter = !skip_sorting && !skip_sorting_in_filter
|
34
|
+
@sorting_links = !skip_sorting && !skip_sorting_links
|
35
|
+
@columns = Compony::NaturalOrdering.new
|
36
|
+
@row_actions = Compony::NaturalOrdering.new
|
37
|
+
@skipped_columns = skip_columns.map(&:to_sym)
|
38
|
+
@skipped_row_actions = skip_row_actions.map(&:to_sym)
|
39
|
+
@filters = Compony::NaturalOrdering.new
|
40
|
+
@sorts = Compony::NaturalOrdering.new
|
41
|
+
@skipped_filters = skip_filters.map(&:to_sym)
|
42
|
+
@filter_label_class = 'list-filter-label'
|
43
|
+
@filter_input_class = 'list-filter-input'
|
44
|
+
@filter_select_class = 'list-filter-select'
|
45
|
+
@filter_item_wrapper_class = nil
|
46
|
+
super(*, **)
|
47
|
+
end
|
48
|
+
|
49
|
+
# DSL method
|
50
|
+
# Disables pagination (caution: all records will be loaded).
|
51
|
+
def skip_pagination!
|
52
|
+
@pagination = false
|
53
|
+
end
|
54
|
+
|
55
|
+
# DSL method
|
56
|
+
# In case pagination is active, defines the amount of records to display per page.
|
57
|
+
def results_per_page(new_results_per_page)
|
58
|
+
@results_per_page = new_results_per_page
|
59
|
+
end
|
60
|
+
|
61
|
+
# DSL method
|
62
|
+
# Disables filtering entirely (sorting is independent of this setting).
|
63
|
+
def skip_filtering!
|
64
|
+
@filtering = false
|
65
|
+
end
|
66
|
+
|
67
|
+
# DSL method
|
68
|
+
# Disables sorting entirely (both links and sorting input in filter).
|
69
|
+
def skip_sorting!
|
70
|
+
@sorting_in_filter = false
|
71
|
+
@sorting_links = false
|
72
|
+
end
|
73
|
+
|
74
|
+
# DSL method
|
75
|
+
# Disables sorting in filter.
|
76
|
+
def skip_sorting_in_filter!
|
77
|
+
@sorting_in_filter = false
|
78
|
+
end
|
79
|
+
|
80
|
+
# DSL method
|
81
|
+
# Disables sorting links.
|
82
|
+
def skip_sorting_links!
|
83
|
+
@sorting_links = false
|
84
|
+
end
|
85
|
+
|
86
|
+
# DSL method
|
87
|
+
# Sets the CSS class attribute for form label elements in filters.
|
88
|
+
# @param class_str [String] Space-separated list of CSS classes
|
89
|
+
def filter_label_class(class_str)
|
90
|
+
@filter_label_class = class_str
|
91
|
+
end
|
92
|
+
|
93
|
+
# DSL method
|
94
|
+
# Sets the CSS class attribute for string form inputs in filters.
|
95
|
+
# @param class_str [String] Space-separated list of CSS classes
|
96
|
+
def filter_input_class(class_str)
|
97
|
+
@filter_input_class = class_str
|
98
|
+
end
|
99
|
+
|
100
|
+
# DSL method
|
101
|
+
# Sets the CSS class attribute for form select inputs in filters.
|
102
|
+
# @param class_str [String] Space-separated list of CSS classes
|
103
|
+
def filter_select_class(class_str)
|
104
|
+
@filter_select_class = class_str
|
105
|
+
end
|
106
|
+
|
107
|
+
# DSL method
|
108
|
+
# Sets the CSS class attribute for the div that wraps input-related elements in filters (inputs, selects, labels).
|
109
|
+
# @param class_str [String] Space-separated list of CSS classes
|
110
|
+
def filter_item_wrapper_class(class_str)
|
111
|
+
@filter_item_wrapper_class = class_str
|
112
|
+
end
|
113
|
+
|
114
|
+
# DSL method
|
115
|
+
# Adds a new column to the list. If `name` corresponds to that of a field, everything is auto-inferred.
|
116
|
+
# Custom columns can be added by providing at least `label` and a block that will be given a record and instance-execed for every row.
|
117
|
+
# Please note that the column is only shown if the current user has permission to index the attribute.
|
118
|
+
# @param name [String] Name of the field that is supposed to be displayed. If custom name, make sure the user has the permission to index the attribute.
|
119
|
+
# @param label [String] Title of the column to be displayed in the table header.
|
120
|
+
# @param class [String] Space-separated list of CSS classes for each cell
|
121
|
+
# @param link_opts [Hash] Only used in the case of a model field, this is used to pass options to the field's `value_for`.
|
122
|
+
# @param block [Block] Custom block, given the record and instance-execed in the context of the cell for every row.
|
123
|
+
def column(name, label: nil, class: nil, link_opts: {}, **, &block)
|
124
|
+
name = name.to_sym
|
125
|
+
unless block_given?
|
126
|
+
# Assume field column
|
127
|
+
field = data_class.fields[name] || fail("Field #{name.inspect} was not found for data class #{data_class}")
|
128
|
+
block = proc do |record|
|
129
|
+
if controller.current_ability.permitted_attributes(:index, record).include?(field.name.to_sym)
|
130
|
+
next field.value_for(record, link_to_component: :show, controller:, link_opts:).to_s
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
@columns.natural_push(name, block, label: label || field.label, class:, **)
|
135
|
+
end
|
136
|
+
|
137
|
+
# DSL method
|
138
|
+
# Adds multiple columns at once, sharing the same kwargs.
|
139
|
+
def columns(*col_names, **)
|
140
|
+
col_names.each { |col_name| column(col_name, **) }
|
141
|
+
end
|
142
|
+
|
143
|
+
# DSL method
|
144
|
+
# Marks a single column as skipped. It will not be displayed, even if it is defined.
|
145
|
+
# @param name [Symbol,String] Name of the column to be skipped.
|
146
|
+
def skip_column(name)
|
147
|
+
@skipped_columns << name.to_sym
|
148
|
+
end
|
149
|
+
|
150
|
+
# DSL method
|
151
|
+
# Adds a row action. The very last col provides actions such as :show, :edit or :destroy. Use this method to add your own.
|
152
|
+
# In case the action exists as a component in the family of `data_class`, it is enough to pass the action's name, and the rest is auto-generated.
|
153
|
+
# In order to create a custom row action, pass a block that will be given the current record and instance-execed once per row, for every record.
|
154
|
+
# @param name [Symbol, String] The name of the action (e.g. :edit).
|
155
|
+
# @param button_opts [Hash] Only relevant in case of an auto-generated row action, this allows to configure the generated button.
|
156
|
+
# @param block [Block] To create a custom row action; block will be given the current record and instance-execed once per row, for every record.
|
157
|
+
def row_action(name, button_opts: {}, **, &block)
|
158
|
+
name = name.to_sym
|
159
|
+
unless block_given?
|
160
|
+
block = proc do |record|
|
161
|
+
next if Compony.comp_class_for(name, record).nil?
|
162
|
+
compony_button(name, record, **button_opts)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
@row_actions.natural_push(name, block, **)
|
166
|
+
end
|
167
|
+
|
168
|
+
# DSL method
|
169
|
+
# Marks a single row action as skipped. It will not be displayed, even if it is defined.
|
170
|
+
# @param name [Symbol,String] Name of the row action to be skipped.
|
171
|
+
def skip_row_action(name)
|
172
|
+
@skipped_row_actions << name.to_sym
|
173
|
+
end
|
174
|
+
|
175
|
+
# DSL method
|
176
|
+
# Adds a ransack filter. If `name` is the name of an existing model field, the filter is auto-generated.
|
177
|
+
# If `name` is a valid Ransack search string (e.g. `id_eq`), all you need to pass is `name` and `label`.
|
178
|
+
# To create a fully custom filter, pass `name` and `block`. The block will be given the Ransack search form and should return HTML.
|
179
|
+
# @param name [String] The name of the filter. Can either be the name of a field, a ransack search string or a custom name (see above).
|
180
|
+
# @param label [String] The text to use in the input's label.
|
181
|
+
# @param block [Block] Custom block that will be given the Ransack search form and should produce a label and a search input.
|
182
|
+
def filter(name, label: nil, **, &block)
|
183
|
+
name = name.to_sym
|
184
|
+
unless block_given?
|
185
|
+
field = data_class.fields[name]
|
186
|
+
block ||= proc do |f|
|
187
|
+
label ||= field.label if field
|
188
|
+
fail("You must provide a label to filter #{name.inspect}.") unless label
|
189
|
+
|
190
|
+
if field
|
191
|
+
filter_name = field.ransack_filter_name
|
192
|
+
filter_input_html = capture { field.ransack_filter_input(f, filter_input_class: @filter_input_class, filter_select_class: @filter_select_class) }
|
193
|
+
else
|
194
|
+
filter_name = name
|
195
|
+
filter_input_html = capture { f.search_field(filter_name, class: @filter_input_class) }
|
196
|
+
end
|
197
|
+
div tag.label(label, for: filter_name, class: @filter_label_class), class: @filter_item_wrapper_class
|
198
|
+
div filter_input_html, class: @filter_item_wrapper_class
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
@filters.natural_push(name, block, **)
|
203
|
+
end
|
204
|
+
|
205
|
+
# DSL method
|
206
|
+
# Adds multiple filters at once, sharing the same kwargs.
|
207
|
+
def filters(*filter_names, **)
|
208
|
+
filter_names.each { |filter_name| filter(filter_name, **) }
|
209
|
+
end
|
210
|
+
|
211
|
+
# DSL method
|
212
|
+
# Adds a sorting criterion that will be processed by ransack. `data_class` must be sortable by this criterion. See Ransack's sorting for constraints.
|
213
|
+
# For every call of this method, one sorting link and two entries (asc, desc) in the sorting-in-filter feature will be generated, if enabled.
|
214
|
+
# @param name [Symbol, String] Sorting criteria, e.g. `:id` or `:label`.
|
215
|
+
# @param label [String] Label of the sorting link / entries.
|
216
|
+
def sort(name, label: nil)
|
217
|
+
label ||= data_class.fields[name].label
|
218
|
+
@sorts.natural_push(name.to_sym, nil, label:)
|
219
|
+
end
|
220
|
+
|
221
|
+
# DSL method
|
222
|
+
# Adds multiple sorts at once, sharing the same kwargs.
|
223
|
+
def sorts(*names, **)
|
224
|
+
names.each { |name| sort(name, **) }
|
225
|
+
end
|
226
|
+
|
227
|
+
# This method must be called before the data is read for the first time. It makes the data fit for display. Only call it once.
|
228
|
+
def process_data!(controller)
|
229
|
+
fail('Data was already processed!') if @processed_data
|
230
|
+
# Filtering
|
231
|
+
if filtering_enabled?
|
232
|
+
@q = @data.ransack(controller.params[param_name(:q)], auth_object: controller.current_ability, search_key: param_name(:q))
|
233
|
+
filtered_data = @q.result.accessible_by(controller.current_ability)
|
234
|
+
else
|
235
|
+
filtered_data = @data
|
236
|
+
end
|
237
|
+
# Pagination
|
238
|
+
if pagination_enabled?
|
239
|
+
@page = controller.params[param_name('page')].presence&.to_i || 1
|
240
|
+
@pagination_offset = (@page - 1) * @results_per_page
|
241
|
+
@total_pages = (filtered_data.count.to_f / @results_per_page).ceil
|
242
|
+
if @pagination_offset < 0 || @pagination_offset >= filtered_data.count # out of bounds check
|
243
|
+
@page = 1
|
244
|
+
@pagination_offset = 0
|
245
|
+
end
|
246
|
+
@processed_data = filtered_data.offset(@pagination_offset).limit(@results_per_page)
|
247
|
+
else
|
248
|
+
@processed_data = filtered_data
|
249
|
+
end
|
250
|
+
# Apply skips to configs
|
251
|
+
# Exclude columns that are skipped or the user is not allowed to display
|
252
|
+
@columns.select! do |col, _|
|
253
|
+
@skipped_columns.exclude?(col[:name]) && controller.current_ability.permitted_attributes(:index, data_class).include?(col[:name])
|
254
|
+
end
|
255
|
+
# Exclude skipped filters
|
256
|
+
@filters.select! { |filter, _| @skipped_filters.exclude?(filter[:name]) }
|
257
|
+
end
|
258
|
+
|
259
|
+
setup do
|
260
|
+
label(:all) { I18n.t(family_name.humanize) }
|
261
|
+
|
262
|
+
load_data do
|
263
|
+
# Prepare raw data. This should not be used directly - processed_data should be used instead
|
264
|
+
@data = data_class.accessible_by(controller.current_ability)
|
265
|
+
end
|
266
|
+
|
267
|
+
# Default row actions (use override or skip_row_action to prevent)
|
268
|
+
row_action(:show)
|
269
|
+
row_action(:edit)
|
270
|
+
row_action(:destroy)
|
271
|
+
|
272
|
+
before_render do
|
273
|
+
process_data!(controller)
|
274
|
+
end
|
275
|
+
|
276
|
+
content do
|
277
|
+
content :filter if (sorting_in_filter_enabled? && @sorts.any?) || (filtering_enabled? && @filters.any?)
|
278
|
+
content :sorting_links if sorting_links_enabled? && @sorts.any?
|
279
|
+
content :data
|
280
|
+
content :pagination if pagination_enabled? && @total_pages > 1
|
281
|
+
end
|
282
|
+
|
283
|
+
content :filter, hidden: true do
|
284
|
+
div class: 'list-filter-container' do
|
285
|
+
form_html = search_form_for @q, url: url_for, as: param_name(:q) do |f|
|
286
|
+
# Sorting in filter
|
287
|
+
if sorting_in_filter_enabled? && @sorts.any?
|
288
|
+
div class: 'list-sorting-in-filter' do
|
289
|
+
div f.label(:s, I18n.t('compony.components.index.sorting'))
|
290
|
+
div f.select(:s, sorting_in_filter_select_opts, { include_blank: true, selected: params.dig(param_name(:q), :s) })
|
291
|
+
end
|
292
|
+
end
|
293
|
+
# Filters
|
294
|
+
if filtering_enabled? && @filters.any?
|
295
|
+
div class: 'list-filters' do
|
296
|
+
strong I18n.t('compony.components.index.filters')
|
297
|
+
@filters.each do |filter|
|
298
|
+
div do
|
299
|
+
instance_exec(f, &filter[:payload])
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
# Submit button
|
305
|
+
div class: 'list-filter-button-container' do
|
306
|
+
concat f.submit
|
307
|
+
end
|
308
|
+
end
|
309
|
+
concat form_html
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
content :sorting_links, hidden: true do
|
314
|
+
div do
|
315
|
+
strong I18n.t('compony.components.index.sorting')
|
316
|
+
@sorts.each do |sort|
|
317
|
+
span sort_link(@q, sort[:name], sort[:label])
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
content :data, hidden: true do
|
323
|
+
table class: 'list-data-table' do
|
324
|
+
thead do
|
325
|
+
tr do
|
326
|
+
@columns.each do |column|
|
327
|
+
th column[:label], class: 'list-data-label'
|
328
|
+
end
|
329
|
+
if @row_actions.any? { |row_action| @skipped_row_actions.exclude?(row_action[:name]) }
|
330
|
+
th I18n.t('compony.components.index.actions'), class: 'list-actions-label'
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
tbody do
|
335
|
+
@processed_data.each do |record|
|
336
|
+
tr do
|
337
|
+
@columns.each do |column|
|
338
|
+
td class: column[:class] do
|
339
|
+
instance_exec(record, &column[:payload])
|
340
|
+
end
|
341
|
+
end
|
342
|
+
rendered_row_actions = @row_actions.map do |row_action|
|
343
|
+
next if @skipped_row_actions.include?(row_action[:name])
|
344
|
+
next instance_exec(record, &row_action[:payload])
|
345
|
+
end.compact
|
346
|
+
if rendered_row_actions.any?
|
347
|
+
td do
|
348
|
+
rendered_row_actions.each do |row_action_html|
|
349
|
+
concat row_action_html if row_action_html
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
content :pagination, hidden: true do
|
360
|
+
current_params = request.GET.dup
|
361
|
+
div class: 'list-pagination-wrapper' do
|
362
|
+
unless @page == 1
|
363
|
+
span link_to(I18n.t('compony.components.index.pagination.first'), current_params.merge(param_name('page') =>1)),
|
364
|
+
class: 'list-pagination list-pagination-first'
|
365
|
+
span link_to(I18n.t('compony.components.index.pagination.previous'), current_params.merge(param_name('page') =>@page - 1)),
|
366
|
+
class: 'list-pagination list--pagination-previous'
|
367
|
+
end
|
368
|
+
span @page, class: 'list-pagination list--pagination-current'
|
369
|
+
unless @page == @total_pages
|
370
|
+
span link_to(I18n.t('compony.components.index.pagination.next'), current_params.merge(param_name('page') =>@page + 1),
|
371
|
+
class: 'list-pagination list--pagination-next')
|
372
|
+
span link_to(I18n.t('compony.components.index.pagination.last'), current_params.merge(param_name('page') =>@total_pages),
|
373
|
+
class: 'list-pagination list--pagination-last')
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
protected
|
380
|
+
|
381
|
+
# Returns whether filtering is possible and wanted in general (regardless of whether there are any filters defined)
|
382
|
+
def filtering_enabled?
|
383
|
+
@filtering && defined?(Ransack)
|
384
|
+
end
|
385
|
+
|
386
|
+
# Returns whether sorting is possible and wanted in general (regardless of whether there are any sorts defined)
|
387
|
+
def sorting_enabled?
|
388
|
+
(@sorting_in_filter || @sorting_links) && defined?(Ransack)
|
389
|
+
end
|
390
|
+
|
391
|
+
# Returns whether sorting in filter is possible and wanted in general (regardless of whether there are any sorts defined)
|
392
|
+
def sorting_in_filter_enabled?
|
393
|
+
sorting_enabled? && @sorting_in_filter
|
394
|
+
end
|
395
|
+
|
396
|
+
# Returns whether generating sorting links is possible and wanted in general (regardless of whether there are any sorts defined)
|
397
|
+
def sorting_links_enabled?
|
398
|
+
sorting_enabled? && @sorting_links
|
399
|
+
end
|
400
|
+
|
401
|
+
# Returns whether pagination is enabled (regardless of whether there is more than one page)
|
402
|
+
def pagination_enabled?
|
403
|
+
@pagination
|
404
|
+
end
|
405
|
+
|
406
|
+
# Returns the select options for sorting suitable for passing in a `f.select`. Used in sorting-in-filter feature. Useful for custom subclasses of List.
|
407
|
+
def sorting_in_filter_select_opts
|
408
|
+
@sorts.flat_map do |sort|
|
409
|
+
%w[asc desc].map do |order|
|
410
|
+
label = "#{sort[:label]} #{order == 'asc' ? '↑' : '↓'}"
|
411
|
+
value = "#{sort[:name]} #{order}"
|
412
|
+
[label, value]
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Compony
|
2
|
+
module Components
|
3
|
+
# @api description
|
4
|
+
# This component is used for the Rails show paradigm.
|
5
|
+
class Show < Compony::Component
|
6
|
+
include Compony::ComponentMixins::Resourceful
|
7
|
+
|
8
|
+
setup do
|
9
|
+
standalone path: "/#{family_name}/:id", constraints: { id: /\d*/ } do
|
10
|
+
verb :get do
|
11
|
+
authorize { can?(:show, @data) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
label(:long) { |data| data.label } # rubocop:disable Style/SymbolProc
|
16
|
+
label(:short) { |_| I18n.t('compony.components.show.label.short') }
|
17
|
+
icon { :eye }
|
18
|
+
|
19
|
+
action :back_to_owner do
|
20
|
+
next if data_class.owner_model_attr.blank?
|
21
|
+
Compony.button(:show, @data.send(data_class.owner_model_attr), icon: :'arrow-left', color: :secondary, label: I18n.t('compony.back'))
|
22
|
+
end
|
23
|
+
|
24
|
+
if Compony.comp_class_for(:edit, family_cst)
|
25
|
+
action :edit do
|
26
|
+
Compony.button(:edit, @data, label_opts: { format: :short })
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if Compony.comp_class_for(:destroy, family_cst)
|
31
|
+
action :destroy do
|
32
|
+
Compony.button(:destroy, @data, label_opts: { format: :short })
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
content :label do
|
37
|
+
h2 component.label
|
38
|
+
end
|
39
|
+
|
40
|
+
content do
|
41
|
+
content :data # Overwrite the main content block to wrap the data content block into e.g. a bootstrap card etc.
|
42
|
+
end
|
43
|
+
|
44
|
+
content :data, hidden: true do
|
45
|
+
all_field_columns(@data) if @columns.none? # Default to showing everything
|
46
|
+
|
47
|
+
table do
|
48
|
+
thead do
|
49
|
+
@columns.each do |column|
|
50
|
+
value = instance_exec(@data, &column[:payload])
|
51
|
+
next if value.nil?
|
52
|
+
tr do
|
53
|
+
td column[:label]
|
54
|
+
td value, class: column[:class]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param skip_columns [Array] Column names to be skipped in the case where this component is nested and therefore instanciated by a parent comp.
|
63
|
+
def initialize(*, skip_columns: [], **)
|
64
|
+
@columns = NaturalOrdering.new
|
65
|
+
@skipped_columns = skip_columns.map(&:to_sym)
|
66
|
+
super(*, **)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Adds a column. The term column is for consistency with the List component and columns are typically model fields / attributes.
|
70
|
+
# @param name [String,Symbol] The technical name of the attribute this column will be for.
|
71
|
+
# @param label [nil,String] The human displayed label for this attribte. If nil, will consider `name` to be a field name and load the field's label.
|
72
|
+
# @param class [nil,String] Extra CSS classes for the column's value.
|
73
|
+
# @param link_opts [Hash] Options to pass to the `link_to` helper. Only used in the case of a field column that will produce a link (e.g. accociation).
|
74
|
+
# @param link_to_component [Symbol] In the case a link is produced (e.g. association), defines the component the link points to. Detaults to `:show`.
|
75
|
+
# @param block [Block] Custom code to run in order to provide the displayed value. Will be given the current record.
|
76
|
+
def column(name, label: nil, class: nil, link_opts: {}, link_to_component: :show, **, &block)
|
77
|
+
name = name.to_sym
|
78
|
+
unless block_given?
|
79
|
+
# Assume field column
|
80
|
+
field = data_class.fields[name] || fail("Field #{name.inspect} was not found for data class #{data_class}")
|
81
|
+
block = proc do |record|
|
82
|
+
if controller.current_ability.permitted_attributes(:show, record).include?(field.name.to_sym)
|
83
|
+
next field.value_for(record, link_to_component:, controller:, link_opts:).to_s
|
84
|
+
else
|
85
|
+
Rails.logger.debug { "Skipping show col #{field.name.inspect} because the current user is not allowed to perform show on #{data}." }
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
@columns.natural_push(name, block, label: label || field.label, class:, **)
|
91
|
+
end
|
92
|
+
|
93
|
+
# DSL method
|
94
|
+
# Adds multiple columns that have identical kwargs, e.g. `class` (see `column`). Typically only used for bulk-adding model fields.
|
95
|
+
# @param col_names [String] Names of the fields in `@data` that are to be added as attributes.
|
96
|
+
def columns(*col_names, **)
|
97
|
+
col_names.each { |col_name| column(col_name, **) }
|
98
|
+
end
|
99
|
+
|
100
|
+
# DSL method
|
101
|
+
# Marks a column as skipped. Useful only when inheriting from a component that provides too many columns.
|
102
|
+
# When nesting components and a column of a child `Show` component is to be skipped, use the constructor's `skip_columns` argument instead.
|
103
|
+
# @param name [String] Name of the column to be skipped.
|
104
|
+
def skip_column(name)
|
105
|
+
@skipped_columns << name.to_sym
|
106
|
+
end
|
107
|
+
|
108
|
+
# DSL method
|
109
|
+
# Goes through the fields of the given data and adds a field column for every field found.
|
110
|
+
# @param data [ApplicationModel] Compony-enriched model that will be queried for fields.
|
111
|
+
def all_field_columns(data)
|
112
|
+
data.fields.each_key { |field_name| column(field_name) }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -20,6 +20,19 @@ module Compony
|
|
20
20
|
end
|
21
21
|
return form.input name || @name, as: :hidden, **input_opts
|
22
22
|
end
|
23
|
+
|
24
|
+
def ransack_filter_name
|
25
|
+
:"#{@name}_eq"
|
26
|
+
end
|
27
|
+
|
28
|
+
def ransack_filter_input(form, **input_opts)
|
29
|
+
form.select(
|
30
|
+
ransack_filter_name,
|
31
|
+
self.class.collect(@model_class.anchormodel_attributes[@name].anchormodel_class.all),
|
32
|
+
{ include_blank: true },
|
33
|
+
{ class: input_opts[:filter_select_class] }
|
34
|
+
)
|
35
|
+
end
|
23
36
|
end
|
24
37
|
end
|
25
38
|
end
|
@@ -52,6 +52,18 @@ module Compony
|
|
52
52
|
return form.input name || @name, as: :hidden, **input_opts
|
53
53
|
end
|
54
54
|
|
55
|
+
# Used in list component
|
56
|
+
# Given a column name, returns the most suitable ransack filter name
|
57
|
+
def ransack_filter_name
|
58
|
+
:"#{@name}_cont"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Used in list component
|
62
|
+
# Given a ransack search form, returns a suitable search input
|
63
|
+
def ransack_filter_input(form, **input_opts)
|
64
|
+
form.search_field(ransack_filter_name, class: input_opts[:filter_input_class])
|
65
|
+
end
|
66
|
+
|
55
67
|
protected
|
56
68
|
|
57
69
|
# If given a scalar, calls the block on the scalar. If given a list, calls the block on every member and joins the result with ",".
|
@@ -4,6 +4,19 @@ module Compony
|
|
4
4
|
def value_for(data, controller: nil, **_)
|
5
5
|
return transform_and_join(data.send(@name), controller:) { |el| el.nil? ? nil : I18n.t("compony.boolean.#{el}") }
|
6
6
|
end
|
7
|
+
|
8
|
+
def ransack_filter_name
|
9
|
+
:"#{@name}_eq"
|
10
|
+
end
|
11
|
+
|
12
|
+
def ransack_filter_input(form, **input_opts)
|
13
|
+
form.select(
|
14
|
+
ransack_filter_name,
|
15
|
+
[['', nil], [I18n.t('compony.boolean.true'), true], [I18n.t('compony.boolean.false'), false]],
|
16
|
+
{},
|
17
|
+
{ class: input_opts[:filter_select_class] }
|
18
|
+
)
|
19
|
+
end
|
7
20
|
end
|
8
21
|
end
|
9
22
|
end
|
data/lib/compony/model_mixin.rb
CHANGED
@@ -81,6 +81,11 @@ module Compony
|
|
81
81
|
end
|
82
82
|
self.autodetect_feasibilities_completed = true
|
83
83
|
end
|
84
|
+
|
85
|
+
# Provides Ransack defaults (auth_object must be a cancancan ability)
|
86
|
+
def ransackable_attributes(auth_object)
|
87
|
+
auth_object.permitted_attributes(:read, self).map(&:to_s)
|
88
|
+
end
|
84
89
|
end
|
85
90
|
|
86
91
|
# Retrieves feasibility for the given instance, returning a boolean indicating whether the action is feasibly.
|
data/lib/compony.rb
CHANGED
@@ -307,6 +307,9 @@ require 'compony/component_mixins/default/labelling'
|
|
307
307
|
require 'compony/component_mixins/resourceful'
|
308
308
|
require 'compony/component'
|
309
309
|
require 'compony/components/button'
|
310
|
+
require 'compony/components/index'
|
311
|
+
require 'compony/components/list'
|
312
|
+
require 'compony/components/show'
|
310
313
|
require 'compony/components/form'
|
311
314
|
require 'compony/components/with_form'
|
312
315
|
require 'compony/components/new'
|