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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +15 -3
  4. data/Gemfile.lock +1 -1
  5. data/VERSION +1 -1
  6. data/compony.gemspec +3 -3
  7. data/doc/ComponentGenerator.html +2 -2
  8. data/doc/Components.html +2 -2
  9. data/doc/ComponentsGenerator.html +2 -2
  10. data/doc/Compony/Component.html +2 -2
  11. data/doc/Compony/ComponentMixins/Default/Labelling.html +2 -2
  12. data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +2 -2
  13. data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +2 -2
  14. data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +2 -2
  15. data/doc/Compony/ComponentMixins/Default/Standalone.html +2 -2
  16. data/doc/Compony/ComponentMixins/Default.html +2 -2
  17. data/doc/Compony/ComponentMixins/Resourceful.html +2 -2
  18. data/doc/Compony/ComponentMixins.html +2 -2
  19. data/doc/Compony/Components/Buttons/CssButton.html +2 -2
  20. data/doc/Compony/Components/Buttons/Link.html +2 -2
  21. data/doc/Compony/Components/Buttons.html +2 -2
  22. data/doc/Compony/Components/Destroy.html +2 -2
  23. data/doc/Compony/Components/Edit.html +2 -2
  24. data/doc/Compony/Components/Form.html +2 -2
  25. data/doc/Compony/Components/Index.html +2 -2
  26. data/doc/Compony/Components/List.html +49 -33
  27. data/doc/Compony/Components/New.html +2 -2
  28. data/doc/Compony/Components/Show.html +2 -2
  29. data/doc/Compony/Components/WithForm.html +2 -2
  30. data/doc/Compony/Components.html +2 -2
  31. data/doc/Compony/ControllerMixin.html +2 -2
  32. data/doc/Compony/Engine.html +2 -2
  33. data/doc/Compony/Intent.html +2 -2
  34. data/doc/Compony/ManageIntentsDsl.html +2 -2
  35. data/doc/Compony/MethodAccessibleHash.html +2 -2
  36. data/doc/Compony/ModelFields/Anchormodel.html +2 -2
  37. data/doc/Compony/ModelFields/Association.html +2 -2
  38. data/doc/Compony/ModelFields/Attachment.html +2 -2
  39. data/doc/Compony/ModelFields/Base.html +2 -2
  40. data/doc/Compony/ModelFields/Boolean.html +2 -2
  41. data/doc/Compony/ModelFields/Color.html +2 -2
  42. data/doc/Compony/ModelFields/Currency.html +2 -2
  43. data/doc/Compony/ModelFields/Date.html +2 -2
  44. data/doc/Compony/ModelFields/Datetime.html +2 -2
  45. data/doc/Compony/ModelFields/Decimal.html +2 -2
  46. data/doc/Compony/ModelFields/Email.html +2 -2
  47. data/doc/Compony/ModelFields/Float.html +2 -2
  48. data/doc/Compony/ModelFields/Integer.html +2 -2
  49. data/doc/Compony/ModelFields/Percentage.html +2 -2
  50. data/doc/Compony/ModelFields/Phone.html +2 -2
  51. data/doc/Compony/ModelFields/RichText.html +2 -2
  52. data/doc/Compony/ModelFields/String.html +2 -2
  53. data/doc/Compony/ModelFields/Text.html +2 -2
  54. data/doc/Compony/ModelFields/Time.html +2 -2
  55. data/doc/Compony/ModelFields/Url.html +2 -2
  56. data/doc/Compony/ModelFields.html +2 -2
  57. data/doc/Compony/ModelMixin.html +36 -36
  58. data/doc/Compony/NaturalOrdering.html +2 -2
  59. data/doc/Compony/RequestContext.html +2 -2
  60. data/doc/Compony/Version.html +2 -2
  61. data/doc/Compony/ViewHelpers.html +2 -2
  62. data/doc/Compony/VirtualModel.html +2 -2
  63. data/doc/Compony.html +33 -37
  64. data/doc/ComponyController.html +2 -2
  65. data/doc/_index.html +2 -2
  66. data/doc/file.CHANGELOG.html +12 -5
  67. data/doc/file.README.html +2 -2
  68. data/doc/file.basic_component.html +2 -2
  69. data/doc/file.cookbook.html +2 -2
  70. data/doc/file.destroy.html +2 -2
  71. data/doc/file.dsl_reference.html +2 -2
  72. data/doc/file.edit.html +2 -2
  73. data/doc/file.example.html +2 -2
  74. data/doc/file.example_advanced.html +2 -2
  75. data/doc/file.feasibility.html +2 -2
  76. data/doc/file.form.html +2 -2
  77. data/doc/file.generators.html +2 -2
  78. data/doc/file.glossary.html +2 -2
  79. data/doc/file.gotchas.html +2 -2
  80. data/doc/file.index.html +2 -2
  81. data/doc/file.inheritance.html +2 -2
  82. data/doc/file.installation.html +2 -2
  83. data/doc/file.integrations.html +2 -2
  84. data/doc/file.intents.html +2 -2
  85. data/doc/file.internal_datastructures.html +2 -2
  86. data/doc/file.list.html +2 -2
  87. data/doc/file.maintaining.html +2 -2
  88. data/doc/file.model_fields.html +2 -2
  89. data/doc/file.nesting.html +2 -2
  90. data/doc/file.new.html +2 -2
  91. data/doc/file.ownership.html +2 -2
  92. data/doc/file.patterns.html +2 -2
  93. data/doc/file.pre_built_components.html +2 -2
  94. data/doc/file.resourceful.html +2 -2
  95. data/doc/file.show.html +2 -2
  96. data/doc/file.standalone.html +2 -2
  97. data/doc/file.virtual_models.html +2 -2
  98. data/doc/file.with_form.html +2 -2
  99. data/doc/index.html +2 -2
  100. data/doc/top-level-namespace.html +2 -2
  101. data/lib/compony/components/list.rb +8 -0
  102. data/lib/compony/model_mixin.rb +66 -3
  103. data/lib/compony.rb +4 -6
  104. 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 Mon May 18 13:55:33 2026 by
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.3.5).
153
+ 0.9.34 (ruby-3.4.9).
154
154
  </div>
155
155
 
156
156
  </div>
@@ -223,9 +223,9 @@
223
223
  </div></div>
224
224
 
225
225
  <div id="footer">
226
- Generated on Mon May 18 13:55:33 2026 by
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.3.5).
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 Mon May 18 13:55:33 2026 by
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.3.5).
112
+ 0.9.34 (ruby-3.4.9).
113
113
  </div>
114
114
 
115
115
  </div>
@@ -147,9 +147,9 @@
147
147
  </div></div>
148
148
 
149
149
  <div id="footer">
150
- Generated on Mon May 18 13:55:33 2026 by
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.3.5).
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 Mon May 18 13:55:32 2026 by
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.3.5).
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 Mon May 18 13:55:34 2026 by
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.3.5).
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, _|
@@ -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
- def prevent(action_names, message, &block)
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
- prevent(:destroy, I18n.t('compony.feasibility.has_dependent_models', dependent_class: assoc.klass.model_name.human(count: 2))) do
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(intent_or_comp_args, ...)
126
- if intent_or_comp_args.is_a?(Intent)
127
- return intent_or_comp_args
128
- else
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.9
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.11
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