compony 0.11.9 → 0.11.10
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/.ruby-version +1 -1
- data/CHANGELOG.md +15 -3
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/compony.gemspec +3 -3
- data/doc/ComponentGenerator.html +2 -2
- data/doc/Components.html +2 -2
- data/doc/ComponentsGenerator.html +2 -2
- data/doc/Compony/Component.html +2 -2
- data/doc/Compony/ComponentMixins/Default/Labelling.html +2 -2
- data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +2 -2
- data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +2 -2
- data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +2 -2
- data/doc/Compony/ComponentMixins/Default/Standalone.html +2 -2
- data/doc/Compony/ComponentMixins/Default.html +2 -2
- data/doc/Compony/ComponentMixins/Resourceful.html +2 -2
- data/doc/Compony/ComponentMixins.html +2 -2
- data/doc/Compony/Components/Buttons/CssButton.html +2 -2
- data/doc/Compony/Components/Buttons/Link.html +2 -2
- data/doc/Compony/Components/Buttons.html +2 -2
- data/doc/Compony/Components/Destroy.html +2 -2
- data/doc/Compony/Components/Edit.html +2 -2
- data/doc/Compony/Components/Form.html +2 -2
- data/doc/Compony/Components/Index.html +2 -2
- data/doc/Compony/Components/List.html +49 -33
- data/doc/Compony/Components/New.html +2 -2
- data/doc/Compony/Components/Show.html +2 -2
- data/doc/Compony/Components/WithForm.html +2 -2
- data/doc/Compony/Components.html +2 -2
- data/doc/Compony/ControllerMixin.html +2 -2
- data/doc/Compony/Engine.html +2 -2
- data/doc/Compony/Intent.html +2 -2
- data/doc/Compony/ManageIntentsDsl.html +2 -2
- data/doc/Compony/MethodAccessibleHash.html +2 -2
- data/doc/Compony/ModelFields/Anchormodel.html +2 -2
- data/doc/Compony/ModelFields/Association.html +2 -2
- data/doc/Compony/ModelFields/Attachment.html +2 -2
- data/doc/Compony/ModelFields/Base.html +2 -2
- data/doc/Compony/ModelFields/Boolean.html +2 -2
- data/doc/Compony/ModelFields/Color.html +2 -2
- data/doc/Compony/ModelFields/Currency.html +2 -2
- data/doc/Compony/ModelFields/Date.html +2 -2
- data/doc/Compony/ModelFields/Datetime.html +2 -2
- data/doc/Compony/ModelFields/Decimal.html +2 -2
- data/doc/Compony/ModelFields/Email.html +2 -2
- data/doc/Compony/ModelFields/Float.html +2 -2
- data/doc/Compony/ModelFields/Integer.html +2 -2
- data/doc/Compony/ModelFields/Percentage.html +2 -2
- data/doc/Compony/ModelFields/Phone.html +2 -2
- data/doc/Compony/ModelFields/RichText.html +2 -2
- data/doc/Compony/ModelFields/String.html +2 -2
- data/doc/Compony/ModelFields/Text.html +2 -2
- data/doc/Compony/ModelFields/Time.html +2 -2
- data/doc/Compony/ModelFields/Url.html +2 -2
- data/doc/Compony/ModelFields.html +2 -2
- data/doc/Compony/ModelMixin.html +36 -36
- data/doc/Compony/NaturalOrdering.html +2 -2
- data/doc/Compony/RequestContext.html +2 -2
- data/doc/Compony/Version.html +2 -2
- data/doc/Compony/ViewHelpers.html +2 -2
- data/doc/Compony/VirtualModel.html +2 -2
- data/doc/Compony.html +33 -37
- data/doc/ComponyController.html +2 -2
- data/doc/_index.html +2 -2
- data/doc/file.CHANGELOG.html +12 -5
- data/doc/file.README.html +2 -2
- data/doc/file.basic_component.html +2 -2
- data/doc/file.cookbook.html +2 -2
- data/doc/file.destroy.html +2 -2
- data/doc/file.dsl_reference.html +2 -2
- data/doc/file.edit.html +2 -2
- data/doc/file.example.html +2 -2
- data/doc/file.example_advanced.html +2 -2
- data/doc/file.feasibility.html +2 -2
- data/doc/file.form.html +2 -2
- data/doc/file.generators.html +2 -2
- data/doc/file.glossary.html +2 -2
- data/doc/file.gotchas.html +2 -2
- data/doc/file.index.html +2 -2
- data/doc/file.inheritance.html +2 -2
- data/doc/file.installation.html +2 -2
- data/doc/file.integrations.html +2 -2
- data/doc/file.intents.html +2 -2
- data/doc/file.internal_datastructures.html +2 -2
- data/doc/file.list.html +2 -2
- data/doc/file.maintaining.html +2 -2
- data/doc/file.model_fields.html +2 -2
- data/doc/file.nesting.html +2 -2
- data/doc/file.new.html +2 -2
- data/doc/file.ownership.html +2 -2
- data/doc/file.patterns.html +2 -2
- data/doc/file.pre_built_components.html +2 -2
- data/doc/file.resourceful.html +2 -2
- data/doc/file.show.html +2 -2
- data/doc/file.standalone.html +2 -2
- data/doc/file.virtual_models.html +2 -2
- data/doc/file.with_form.html +2 -2
- data/doc/index.html +2 -2
- data/doc/top-level-namespace.html +2 -2
- data/lib/compony/components/list.rb +8 -0
- data/lib/compony/model_mixin.rb +66 -3
- data/lib/compony.rb +4 -6
- metadata +2 -2
data/doc/file.show.html
CHANGED
|
@@ -148,9 +148,9 @@
|
|
|
148
148
|
</div></div>
|
|
149
149
|
|
|
150
150
|
<div id="footer">
|
|
151
|
-
Generated on
|
|
151
|
+
Generated on Thu Jun 4 20:37:16 2026 by
|
|
152
152
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
153
|
-
0.9.34 (ruby-3.
|
|
153
|
+
0.9.34 (ruby-3.4.9).
|
|
154
154
|
</div>
|
|
155
155
|
|
|
156
156
|
</div>
|
data/doc/file.standalone.html
CHANGED
|
@@ -223,9 +223,9 @@
|
|
|
223
223
|
</div></div>
|
|
224
224
|
|
|
225
225
|
<div id="footer">
|
|
226
|
-
Generated on
|
|
226
|
+
Generated on Thu Jun 4 20:37:15 2026 by
|
|
227
227
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
228
|
-
0.9.34 (ruby-3.
|
|
228
|
+
0.9.34 (ruby-3.4.9).
|
|
229
229
|
</div>
|
|
230
230
|
|
|
231
231
|
</div>
|
|
@@ -107,9 +107,9 @@
|
|
|
107
107
|
</div></div>
|
|
108
108
|
|
|
109
109
|
<div id="footer">
|
|
110
|
-
Generated on
|
|
110
|
+
Generated on Thu Jun 4 20:37:16 2026 by
|
|
111
111
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
112
|
-
0.9.34 (ruby-3.
|
|
112
|
+
0.9.34 (ruby-3.4.9).
|
|
113
113
|
</div>
|
|
114
114
|
|
|
115
115
|
</div>
|
data/doc/file.with_form.html
CHANGED
|
@@ -147,9 +147,9 @@
|
|
|
147
147
|
</div></div>
|
|
148
148
|
|
|
149
149
|
<div id="footer">
|
|
150
|
-
Generated on
|
|
150
|
+
Generated on Thu Jun 4 20:37:16 2026 by
|
|
151
151
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
152
|
-
0.9.34 (ruby-3.
|
|
152
|
+
0.9.34 (ruby-3.4.9).
|
|
153
153
|
</div>
|
|
154
154
|
|
|
155
155
|
</div>
|
data/doc/index.html
CHANGED
|
@@ -220,9 +220,9 @@
|
|
|
220
220
|
</div></div>
|
|
221
221
|
|
|
222
222
|
<div id="footer">
|
|
223
|
-
Generated on
|
|
223
|
+
Generated on Thu Jun 4 20:37:14 2026 by
|
|
224
224
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
225
|
-
0.9.34 (ruby-3.
|
|
225
|
+
0.9.34 (ruby-3.4.9).
|
|
226
226
|
</div>
|
|
227
227
|
|
|
228
228
|
</div>
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
</div>
|
|
103
103
|
|
|
104
104
|
<div id="footer">
|
|
105
|
-
Generated on
|
|
105
|
+
Generated on Thu Jun 4 20:37:17 2026 by
|
|
106
106
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
107
|
-
0.9.34 (ruby-3.
|
|
107
|
+
0.9.34 (ruby-3.4.9).
|
|
108
108
|
</div>
|
|
109
109
|
|
|
110
110
|
</div>
|
|
@@ -256,6 +256,14 @@ module Compony
|
|
|
256
256
|
@total_pages = 1
|
|
257
257
|
@pagination_offset = 0
|
|
258
258
|
end
|
|
259
|
+
# Batch-precompute feasibility for the records on this page, avoiding N+1 queries from per-row destroy buttons (see ModelMixin#precompute_feasibility).
|
|
260
|
+
if data_class.respond_to?(:precompute_feasibility) && !@skip_row_intents
|
|
261
|
+
page_records = @processed_data.to_a
|
|
262
|
+
if page_records.any?
|
|
263
|
+
action_names = row_intents(data: page_records.first).map(&:name).uniq
|
|
264
|
+
action_names.each { |action_name| data_class.precompute_feasibility(page_records, action_name) }
|
|
265
|
+
end
|
|
266
|
+
end
|
|
259
267
|
# Apply skips to configs
|
|
260
268
|
# Exclude columns that are skipped or the user is not allowed to display
|
|
261
269
|
@columns.select! do |col, _|
|
data/lib/compony/model_mixin.rb
CHANGED
|
@@ -52,12 +52,13 @@ module Compony
|
|
|
52
52
|
|
|
53
53
|
# DSL method, part of the Feasibility feature
|
|
54
54
|
# Block must return `false` if the action should be prevented.
|
|
55
|
-
|
|
55
|
+
# @param assoc [ActiveRecord::Reflection] Internal, set by {autodetect_feasibilities!}. Allows {precompute_feasibility} to batch the check.
|
|
56
|
+
def prevent(action_names, message, assoc: nil, &block)
|
|
56
57
|
action_names = [action_names] unless action_names.is_a? Enumerable
|
|
57
58
|
action_names.each do |action_name|
|
|
58
59
|
self.feasibility_preventions = feasibility_preventions.dup # Prevent cross-class contamination
|
|
59
60
|
feasibility_preventions[action_name.to_sym] ||= []
|
|
60
|
-
feasibility_preventions[action_name.to_sym] << MethodAccessibleHash.new(action_name:, message:, block:)
|
|
61
|
+
feasibility_preventions[action_name.to_sym] << MethodAccessibleHash.new(action_name:, message:, assoc:, block:)
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
@@ -71,7 +72,9 @@ module Compony
|
|
|
71
72
|
return if autodetect_feasibilities_completed
|
|
72
73
|
# Add a prevention that reflects the `has_many` `dependent' properties. Avoids that users can press buttons that will result in a failed destroy.
|
|
73
74
|
reflect_on_all_associations.select { |assoc| %i[restrict_with_exception restrict_with_error].include? assoc.options[:dependent] }.each do |assoc|
|
|
74
|
-
|
|
75
|
+
# The `assoc:` is stored so that `precompute_feasibility` can resolve dependent existence for a whole collection in a single query per
|
|
76
|
+
# association (see batchable_feasibility_assoc?). When called on a single record (no precompute), the block below is used instead.
|
|
77
|
+
prevent(:destroy, I18n.t('compony.feasibility.has_dependent_models', dependent_class: assoc.klass.model_name.human(count: 2)), assoc:) do
|
|
75
78
|
if assoc.is_a? ActiveRecord::Reflection::HasOneReflection
|
|
76
79
|
!public_send(assoc.name).nil?
|
|
77
80
|
else
|
|
@@ -82,6 +85,66 @@ module Compony
|
|
|
82
85
|
self.autodetect_feasibilities_completed = true
|
|
83
86
|
end
|
|
84
87
|
|
|
88
|
+
# Precomputes and caches feasibility for an entire collection of records, avoiding the N+1 queries that arise when `feasible?` is called per
|
|
89
|
+
# record (e.g. when rendering destroy buttons for every row of an index). For every autodetected dependent-association prevention that can be
|
|
90
|
+
# batched (see {batchable_feasibility_assoc?}), this issues a single existence query across all records instead of one query per record.
|
|
91
|
+
# Preventions that cannot be batched (custom `prevent` blocks, polymorphic/through/STI-ambiguous or argument-taking-scope associations) fall
|
|
92
|
+
# back to the per-record block. After this call, `feasible?` / `feasibility_messages` for the given action return cached results with no further
|
|
93
|
+
# queries for the batched preventions.
|
|
94
|
+
# @param records [Enumerable] the records to precompute feasibility for, typically the current index page
|
|
95
|
+
# @param action_name [Symbol,String] the action to precompute, e.g. :destroy
|
|
96
|
+
def precompute_feasibility(records, action_name)
|
|
97
|
+
action_name = action_name.to_sym
|
|
98
|
+
records = records.to_a
|
|
99
|
+
return if records.empty?
|
|
100
|
+
autodetect_feasibilities!
|
|
101
|
+
# Seed the per-record message cache so feasible? treats these as already computed.
|
|
102
|
+
records.each do |record|
|
|
103
|
+
messages = record.instance_variable_get(:@feasibility_messages) || record.instance_variable_set(:@feasibility_messages, {})
|
|
104
|
+
messages[action_name] = []
|
|
105
|
+
end
|
|
106
|
+
Array(feasibility_preventions[action_name]).each do |prevention|
|
|
107
|
+
assoc = prevention[:assoc]
|
|
108
|
+
if assoc && batchable_feasibility_assoc?(assoc)
|
|
109
|
+
apply_batched_feasibility_prevention(records, action_name, prevention, assoc)
|
|
110
|
+
else
|
|
111
|
+
# Fallback: run the prevention block once per record (custom blocks, polymorphic/through/scoped associations).
|
|
112
|
+
records.each do |record|
|
|
113
|
+
record.instance_variable_get(:@feasibility_messages)[action_name] << prevention.message if record.instance_exec(&prevention.block)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Whether a dependent association can be resolved for a whole collection in a single existence query. Conservative on purpose: anything that
|
|
120
|
+
# would make a flat `WHERE foreign_key IN (...)` query incorrect falls back to the per-record block.
|
|
121
|
+
def batchable_feasibility_assoc?(assoc)
|
|
122
|
+
return false unless %i[has_many has_one].include?(assoc.macro)
|
|
123
|
+
return false if assoc.through_reflection # has_*_through: join semantics, not a flat foreign key
|
|
124
|
+
return false if assoc.options[:as] # polymorphic inverse: needs a type column too
|
|
125
|
+
return false if assoc.polymorphic? # defensive; polymorphic on this side
|
|
126
|
+
return false if assoc.scope&.arity&.positive? # scope needs the owner instance, can't merge onto a bare relation
|
|
127
|
+
assoc.klass.present? && assoc.foreign_key.present?
|
|
128
|
+
rescue StandardError
|
|
129
|
+
# If anything about the reflection can't be resolved (e.g. STI / missing constant), prefer the safe per-record fallback.
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Runs one existence query for `assoc` across all `records` and appends the prevention message to every record that has at least one
|
|
134
|
+
# dependent row. See {precompute_feasibility}.
|
|
135
|
+
def apply_batched_feasibility_prevention(records, action_name, prevention, assoc)
|
|
136
|
+
ids = records.map(&:id).compact
|
|
137
|
+
return if ids.empty?
|
|
138
|
+
scope = assoc.klass.all
|
|
139
|
+
scope = scope.instance_exec(&assoc.scope) if assoc.scope # honor static `has_many ..., -> { where(...) }` conditions
|
|
140
|
+
# reorder(nil) drops any ORDER BY inherited from a default_scope or the association scope: PostgreSQL rejects
|
|
141
|
+
# `SELECT DISTINCT ... ORDER BY <col>` when the ordering column is not in the select list, and ordering is irrelevant here anyway.
|
|
142
|
+
triggered_ids = scope.where(assoc.foreign_key => ids).reorder(nil).distinct.pluck(assoc.foreign_key).to_set
|
|
143
|
+
records.each do |record|
|
|
144
|
+
record.instance_variable_get(:@feasibility_messages)[action_name] << prevention.message if triggered_ids.include?(record.id)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
85
148
|
# Provides Ransack defaults (auth_object must be a cancancan ability)
|
|
86
149
|
def ransackable_attributes(auth_object)
|
|
87
150
|
auth_object.permitted_attributes(:read, self).map(&:to_s)
|
data/lib/compony.rb
CHANGED
|
@@ -122,12 +122,10 @@ module Compony
|
|
|
122
122
|
##########=====-------
|
|
123
123
|
|
|
124
124
|
# Pure helper to create a Compony Intent. If given an intent, will return it unchanged. Otherwise, will give all params to the intent initializer.
|
|
125
|
-
def self.intent(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return Intent.new(intent_or_comp_args, ...)
|
|
130
|
-
end
|
|
125
|
+
def self.intent(*args, **)
|
|
126
|
+
first = args.first
|
|
127
|
+
return first if first.is_a?(Intent)
|
|
128
|
+
return Intent.new(*args, **)
|
|
131
129
|
end
|
|
132
130
|
|
|
133
131
|
# Generates a Rails path to a component. Examples: `Compony.path(:index, :users)`, `Compony.path(:show, User.first)`
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: compony
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.11.
|
|
4
|
+
version: 0.11.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Kalbermatter
|
|
@@ -411,7 +411,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
411
411
|
- !ruby/object:Gem::Version
|
|
412
412
|
version: '0'
|
|
413
413
|
requirements: []
|
|
414
|
-
rubygems_version: 4.0.
|
|
414
|
+
rubygems_version: 4.0.13
|
|
415
415
|
specification_version: 4
|
|
416
416
|
summary: Compony is a Gem that allows you to write your Rails application in component-style
|
|
417
417
|
fashion. It combines a controller action and route along \ with its view into a
|