compony 0.11.8 → 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/.yardopts +36 -1
- data/CHANGELOG.md +43 -0
- data/CLAUDE.md +85 -0
- data/Gemfile.lock +1 -1
- data/README.md +13 -3
- data/VERSION +1 -1
- data/compony.gemspec +4 -4
- data/doc/ComponentGenerator.html +2 -2
- data/doc/Components.html +2 -2
- data/doc/ComponentsGenerator.html +2 -2
- data/doc/Compony/Component.html +55 -55
- 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 +110 -71
- data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +65 -29
- data/doc/Compony/ComponentMixins/Default/Standalone.html +2 -2
- data/doc/Compony/ComponentMixins/Default.html +2 -2
- data/doc/Compony/ComponentMixins/Resourceful.html +214 -75
- 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 +84 -30
- data/doc/Compony/Components/Edit.html +111 -39
- data/doc/Compony/Components/Form.html +552 -209
- data/doc/Compony/Components/Index.html +2 -2
- data/doc/Compony/Components/List.html +51 -35
- data/doc/Compony/Components/New.html +111 -39
- data/doc/Compony/Components/Show.html +2 -2
- data/doc/Compony/Components/WithForm.html +195 -48
- 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 +3 -3
- 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 +98 -2
- data/doc/file.CHANGELOG.html +765 -0
- data/doc/file.README.html +26 -5
- data/doc/file.basic_component.html +314 -0
- data/doc/file.cookbook.html +189 -0
- data/doc/file.destroy.html +105 -0
- data/doc/file.dsl_reference.html +672 -0
- data/doc/file.edit.html +109 -0
- data/doc/file.example.html +291 -0
- data/doc/file.example_advanced.html +257 -0
- data/doc/file.feasibility.html +115 -0
- data/doc/file.form.html +195 -0
- data/doc/file.generators.html +89 -0
- data/doc/file.glossary.html +217 -0
- data/doc/file.gotchas.html +222 -0
- data/doc/file.index.html +135 -0
- data/doc/file.inheritance.html +136 -0
- data/doc/file.installation.html +115 -0
- data/doc/file.integrations.html +218 -0
- data/doc/file.intents.html +265 -0
- data/doc/file.internal_datastructures.html +129 -0
- data/doc/file.list.html +253 -0
- data/doc/file.maintaining.html +127 -0
- data/doc/file.model_fields.html +137 -0
- data/doc/file.nesting.html +237 -0
- data/doc/file.new.html +109 -0
- data/doc/file.ownership.html +98 -0
- data/doc/file.patterns.html +669 -0
- data/doc/file.pre_built_components.html +99 -0
- data/doc/file.resourceful.html +181 -0
- data/doc/file.show.html +158 -0
- data/doc/file.standalone.html +233 -0
- data/doc/file.virtual_models.html +117 -0
- data/doc/file.with_form.html +157 -0
- data/doc/file_list.html +160 -0
- data/doc/guide/cookbook.md +41 -0
- data/doc/guide/dsl_reference.md +155 -0
- data/doc/guide/example_advanced.md +209 -0
- data/doc/guide/generators.md +1 -1
- data/doc/guide/glossary.md +42 -0
- data/doc/guide/gotchas.md +125 -0
- data/doc/guide/maintaining.md +64 -0
- data/doc/guide/patterns.md +681 -0
- data/doc/guide/pre_built_components/edit.md +1 -1
- data/doc/guide/pre_built_components/index.md +64 -1
- data/doc/guide/pre_built_components/list.md +111 -7
- data/doc/guide/pre_built_components/show.md +57 -2
- data/doc/guide/pre_built_components/with_form.md +56 -9
- data/doc/guide/pre_built_components.md +7 -2
- data/doc/guide/standalone.md +16 -1
- data/doc/index.html +26 -5
- data/doc/integrations.md +61 -0
- data/doc/llms.txt +62 -0
- data/doc/top-level-namespace.html +2 -2
- data/lib/compony/component.rb +8 -3
- data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +32 -15
- data/lib/compony/component_mixins/default/standalone/verb_dsl.rb +11 -3
- data/lib/compony/component_mixins/resourceful.rb +30 -16
- data/lib/compony/components/destroy.rb +21 -1
- data/lib/compony/components/edit.rb +25 -1
- data/lib/compony/components/form.rb +63 -21
- data/lib/compony/components/list.rb +9 -1
- data/lib/compony/components/new.rb +25 -1
- data/lib/compony/components/with_form.rb +20 -5
- data/lib/compony/intent.rb +1 -1
- data/lib/compony/model_mixin.rb +66 -3
- data/lib/compony.rb +4 -6
- metadata +44 -2
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
|
|
@@ -172,6 +172,7 @@ files:
|
|
|
172
172
|
- ".ruby-version"
|
|
173
173
|
- ".yardopts"
|
|
174
174
|
- CHANGELOG.md
|
|
175
|
+
- CLAUDE.md
|
|
175
176
|
- Gemfile
|
|
176
177
|
- Gemfile.lock
|
|
177
178
|
- LICENSE
|
|
@@ -250,20 +251,59 @@ files:
|
|
|
250
251
|
- doc/css/common.css
|
|
251
252
|
- doc/css/full_list.css
|
|
252
253
|
- doc/css/style.css
|
|
254
|
+
- doc/file.CHANGELOG.html
|
|
253
255
|
- doc/file.README.html
|
|
256
|
+
- doc/file.basic_component.html
|
|
257
|
+
- doc/file.cookbook.html
|
|
258
|
+
- doc/file.destroy.html
|
|
259
|
+
- doc/file.dsl_reference.html
|
|
260
|
+
- doc/file.edit.html
|
|
261
|
+
- doc/file.example.html
|
|
262
|
+
- doc/file.example_advanced.html
|
|
263
|
+
- doc/file.feasibility.html
|
|
264
|
+
- doc/file.form.html
|
|
265
|
+
- doc/file.generators.html
|
|
266
|
+
- doc/file.glossary.html
|
|
267
|
+
- doc/file.gotchas.html
|
|
268
|
+
- doc/file.index.html
|
|
269
|
+
- doc/file.inheritance.html
|
|
270
|
+
- doc/file.installation.html
|
|
271
|
+
- doc/file.integrations.html
|
|
272
|
+
- doc/file.intents.html
|
|
273
|
+
- doc/file.internal_datastructures.html
|
|
274
|
+
- doc/file.list.html
|
|
275
|
+
- doc/file.maintaining.html
|
|
276
|
+
- doc/file.model_fields.html
|
|
277
|
+
- doc/file.nesting.html
|
|
278
|
+
- doc/file.new.html
|
|
279
|
+
- doc/file.ownership.html
|
|
280
|
+
- doc/file.patterns.html
|
|
281
|
+
- doc/file.pre_built_components.html
|
|
282
|
+
- doc/file.resourceful.html
|
|
283
|
+
- doc/file.show.html
|
|
284
|
+
- doc/file.standalone.html
|
|
285
|
+
- doc/file.virtual_models.html
|
|
286
|
+
- doc/file.with_form.html
|
|
254
287
|
- doc/file_list.html
|
|
255
288
|
- doc/frames.html
|
|
256
289
|
- doc/guide/basic_component.md
|
|
290
|
+
- doc/guide/cookbook.md
|
|
291
|
+
- doc/guide/dsl_reference.md
|
|
257
292
|
- doc/guide/example.md
|
|
293
|
+
- doc/guide/example_advanced.md
|
|
258
294
|
- doc/guide/feasibility.md
|
|
259
295
|
- doc/guide/generators.md
|
|
296
|
+
- doc/guide/glossary.md
|
|
297
|
+
- doc/guide/gotchas.md
|
|
260
298
|
- doc/guide/inheritance.md
|
|
261
299
|
- doc/guide/installation.md
|
|
262
300
|
- doc/guide/intents.md
|
|
263
301
|
- doc/guide/internal_datastructures.md
|
|
302
|
+
- doc/guide/maintaining.md
|
|
264
303
|
- doc/guide/model_fields.md
|
|
265
304
|
- doc/guide/nesting.md
|
|
266
305
|
- doc/guide/ownership.md
|
|
306
|
+
- doc/guide/patterns.md
|
|
267
307
|
- doc/guide/pre_built_components.md
|
|
268
308
|
- doc/guide/pre_built_components/destroy.md
|
|
269
309
|
- doc/guide/pre_built_components/edit.md
|
|
@@ -282,9 +322,11 @@ files:
|
|
|
282
322
|
- doc/imgs/intro-example-new.png
|
|
283
323
|
- doc/imgs/intro-example-show.png
|
|
284
324
|
- doc/index.html
|
|
325
|
+
- doc/integrations.md
|
|
285
326
|
- doc/js/app.js
|
|
286
327
|
- doc/js/full_list.js
|
|
287
328
|
- doc/js/jquery.js
|
|
329
|
+
- doc/llms.txt
|
|
288
330
|
- doc/method_list.html
|
|
289
331
|
- doc/resourceful_lifecycle.graphml
|
|
290
332
|
- doc/resourceful_lifecycle.pdf
|
|
@@ -369,7 +411,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
369
411
|
- !ruby/object:Gem::Version
|
|
370
412
|
version: '0'
|
|
371
413
|
requirements: []
|
|
372
|
-
rubygems_version: 4.0.
|
|
414
|
+
rubygems_version: 4.0.13
|
|
373
415
|
specification_version: 4
|
|
374
416
|
summary: Compony is a Gem that allows you to write your Rails application in component-style
|
|
375
417
|
fashion. It combines a controller action and route along \ with its view into a
|