plutonium 0.14.0 → 0.15.0.pre.rc1

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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/README copy.md +1 -1
  3. data/README.md +1 -1
  4. data/app/assets/plutonium.css +1 -1
  5. data/app/views/{application → plutonium}/_resource_header.html copy.erb +1 -1
  6. data/app/views/{application → plutonium}/_resource_header.html.erb +1 -1
  7. data/app/views/{application → plutonium}/_resource_sidebar.html.erb +2 -0
  8. data/app/views/resource/_resource_details.html.erb +1 -36
  9. data/app/views/resource/_resource_form.html.erb +1 -5
  10. data/app/views/resource/_resource_table.html.erb +315 -85
  11. data/app/views/resource/edit.html.erb +1 -5
  12. data/app/views/resource/index.html.erb +1 -5
  13. data/app/views/resource/new.html.erb +1 -5
  14. data/app/views/resource/show.html.erb +1 -5
  15. data/config/initializers/pagy.rb +1 -0
  16. data/config/initializers/rabl.rb +27 -20
  17. data/gemfiles/rails_7.gemfile.lock +5 -1
  18. data/lib/generators/pu/core/assets/assets_generator.rb +2 -2
  19. data/lib/generators/pu/core/install/install_generator.rb +0 -3
  20. data/lib/generators/pu/core/install/templates/app/controllers/plutonium_controller.rb.tt +2 -0
  21. data/lib/generators/pu/core/install/templates/app/controllers/resource_controller.rb.tt +21 -1
  22. data/lib/generators/pu/core/install/templates/app/definitions/resource_definition.rb.tt +2 -0
  23. data/lib/generators/pu/core/install/templates/app/models/resource_record.rb.tt +0 -2
  24. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +5 -2
  25. data/lib/generators/pu/eject/shell/shell_generator.rb +2 -2
  26. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +19 -0
  27. data/lib/generators/pu/lib/plutonium_generators/concerns/logger.rb +1 -1
  28. data/lib/generators/pu/lib/plutonium_generators/generator.rb +5 -3
  29. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +26 -2
  30. data/lib/generators/pu/pkg/{feature/feature_generator.rb → package/package_generator.rb} +4 -4
  31. data/lib/generators/pu/pkg/{feature → package}/templates/app/controllers/resource_controller.rb.tt +0 -2
  32. data/lib/generators/pu/pkg/package/templates/app/definitions/resource_definition.rb.tt +4 -0
  33. data/lib/generators/pu/pkg/package/templates/app/query_objects/resource_query_object.rb.tt +4 -0
  34. data/lib/generators/pu/pkg/{app/app_generator.rb → portal/portal_generator.rb} +10 -8
  35. data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/concerns/controller.rb.tt +3 -7
  36. data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/dashboard_controller.rb.tt +1 -1
  37. data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +5 -0
  38. data/lib/generators/pu/pkg/{app/templates/app/controllers/controller.rb.tt → portal/templates/app/controllers/resource_controller.rb.tt} +1 -1
  39. data/lib/generators/pu/pkg/portal/templates/app/definitions/resource_definition.rb.tt +4 -0
  40. data/lib/generators/pu/pkg/{app → portal}/templates/app/views/package/dashboard/index.html.erb +2 -1
  41. data/lib/generators/pu/res/conn/conn_generator.rb +78 -3
  42. data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +1 -1
  43. data/lib/generators/pu/res/conn/templates/app/definitions/resource_definition.rb.tt +3 -0
  44. data/lib/generators/pu/res/conn/templates/app/policies/resource_policy.rb.tt +29 -1
  45. data/lib/generators/pu/res/conn/templates/app/presenters/resource_presenter.rb.tt +1 -1
  46. data/lib/generators/pu/res/conn/templates/app/query_objects/resource_query_object.rb.tt +1 -1
  47. data/lib/generators/pu/res/model/model_generator.rb +0 -7
  48. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -1
  49. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +22 -4
  50. data/lib/generators/pu/res/scaffold/templates/controller.rb.tt +0 -1
  51. data/lib/generators/pu/res/scaffold/templates/definition.rb.tt +4 -0
  52. data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +2 -2
  53. data/lib/generators/pu/rodauth/templates/app/controllers/rodauth_controller.rb.tt +1 -1
  54. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +270 -0
  55. data/lib/plutonium/action/README.md +0 -0
  56. data/lib/plutonium/action/base.rb +103 -0
  57. data/lib/plutonium/action/interactive.rb +117 -0
  58. data/lib/plutonium/action/route_options.rb +65 -0
  59. data/lib/plutonium/action/simple.rb +8 -0
  60. data/lib/plutonium/auth.rb +1 -1
  61. data/lib/plutonium/configuration.rb +130 -0
  62. data/lib/plutonium/core/actions/collection.rb +1 -1
  63. data/lib/plutonium/core/associations/renderers/factory.rb +3 -1
  64. data/lib/plutonium/core/autodiscovery/association_renderer_discoverer.rb +1 -1
  65. data/lib/plutonium/core/autodiscovery/input_discoverer.rb +1 -1
  66. data/lib/plutonium/core/autodiscovery/renderer_discoverer.rb +1 -1
  67. data/lib/plutonium/core/controller.rb +110 -0
  68. data/lib/plutonium/core/controllers/authorizable.rb +12 -35
  69. data/lib/plutonium/core/controllers/bootable.rb +38 -7
  70. data/lib/plutonium/core/controllers/entity_scoping.rb +6 -2
  71. data/lib/plutonium/core/fields/renderers/association_renderer.rb +1 -1
  72. data/lib/plutonium/core/ui/collection.rb +1 -1
  73. data/lib/plutonium/core/ui/detail.rb +1 -1
  74. data/lib/plutonium/core/ui/form.rb +1 -1
  75. data/lib/plutonium/definition/actions.rb +50 -0
  76. data/lib/plutonium/definition/base.rb +92 -0
  77. data/lib/plutonium/definition/config_attr.rb +30 -0
  78. data/lib/plutonium/definition/defineable_props.rb +96 -0
  79. data/lib/plutonium/definition/search.rb +21 -0
  80. data/lib/plutonium/engine/validator.rb +30 -0
  81. data/lib/plutonium/engine.rb +25 -0
  82. data/lib/plutonium/helpers/assets_helper.rb +73 -20
  83. data/lib/plutonium/helpers/form_helper.rb +1 -3
  84. data/lib/plutonium/interaction/README.md +369 -0
  85. data/lib/plutonium/interaction/base.rb +75 -0
  86. data/lib/plutonium/interaction/concerns/presentable.rb +61 -0
  87. data/lib/plutonium/interaction/concerns/workflow_dsl.rb +82 -0
  88. data/lib/plutonium/interaction/outcome.rb +129 -0
  89. data/lib/plutonium/interaction/response/base.rb +63 -0
  90. data/lib/plutonium/interaction/response/null.rb +33 -0
  91. data/lib/plutonium/interaction/response/redirect.rb +30 -0
  92. data/lib/plutonium/interaction/response/render.rb +28 -0
  93. data/lib/plutonium/lib/bit_flags.rb +70 -9
  94. data/lib/plutonium/lib/overlayed_hash.rb +86 -0
  95. data/lib/plutonium/lib/smart_cache.rb +171 -0
  96. data/lib/plutonium/models/has_cents.rb +170 -0
  97. data/lib/plutonium/{pkg/base.rb → package/engine.rb} +10 -2
  98. data/lib/plutonium/{application → portal}/controller.rb +3 -11
  99. data/lib/plutonium/{application → portal}/dynamic_controllers.rb +4 -4
  100. data/lib/plutonium/portal/engine.rb +15 -0
  101. data/lib/plutonium/railtie.rb +35 -15
  102. data/lib/plutonium/reloader.rb +71 -29
  103. data/lib/plutonium/resource/controller.rb +51 -34
  104. data/lib/plutonium/resource/controllers/authorizable.rb +128 -0
  105. data/lib/plutonium/{core → resource}/controllers/crud_actions.rb +23 -22
  106. data/lib/plutonium/resource/controllers/defineable.rb +26 -0
  107. data/lib/plutonium/{core → resource}/controllers/interactive_actions.rb +12 -12
  108. data/lib/plutonium/resource/controllers/presentable.rb +41 -0
  109. data/lib/plutonium/resource/controllers/queryable.rb +44 -0
  110. data/lib/plutonium/resource/definition.rb +6 -0
  111. data/lib/plutonium/resource/policy.rb +25 -13
  112. data/lib/plutonium/resource/query_object.rb +50 -51
  113. data/lib/plutonium/resource/record.rb +6 -89
  114. data/lib/plutonium/resource/register.rb +82 -0
  115. data/lib/plutonium/routing/mapper_extensions.rb +1 -1
  116. data/lib/plutonium/routing/resource_registration.rb +1 -1
  117. data/lib/plutonium/routing/route_set_extensions.rb +6 -18
  118. data/lib/plutonium/ui/action_button.rb +125 -0
  119. data/lib/plutonium/ui/breadcrumbs.rb +163 -0
  120. data/lib/plutonium/ui/component/base.rb +13 -0
  121. data/lib/plutonium/ui/component/behaviour.rb +38 -0
  122. data/lib/plutonium/ui/component/kit.rb +31 -0
  123. data/lib/plutonium/ui/component/methods.rb +54 -0
  124. data/lib/plutonium/ui/display/base.rb +25 -0
  125. data/lib/plutonium/ui/display/component/association.rb +26 -0
  126. data/lib/plutonium/ui/display/resource.rb +77 -0
  127. data/lib/plutonium/ui/display/theme.rb +27 -0
  128. data/lib/plutonium/ui/dyna_frame/content.rb +20 -0
  129. data/lib/plutonium/ui/empty_card.rb +20 -0
  130. data/lib/plutonium/ui/form/base.rb +37 -0
  131. data/lib/plutonium/ui/form/resource.rb +75 -0
  132. data/lib/plutonium/ui/form/theme.rb +42 -0
  133. data/lib/plutonium/ui/page/base.rb +112 -0
  134. data/lib/plutonium/ui/page/edit.rb +23 -0
  135. data/lib/plutonium/ui/page/index.rb +27 -0
  136. data/lib/plutonium/ui/page/new.rb +23 -0
  137. data/lib/plutonium/ui/page/show.rb +27 -0
  138. data/lib/plutonium/ui/page_header.rb +49 -0
  139. data/lib/plutonium/ui/table/base.rb +13 -0
  140. data/lib/plutonium/ui/table/components/pagy_info.rb +70 -0
  141. data/lib/plutonium/ui/table/components/pagy_page_info.rb +70 -0
  142. data/lib/plutonium/ui/table/components/pagy_pagination.rb +105 -0
  143. data/lib/plutonium/ui/table/components/scopes_bar.rb +136 -0
  144. data/lib/plutonium/ui/table/components/search_bar.rb +158 -0
  145. data/lib/plutonium/ui/table/display_theme.rb +21 -0
  146. data/lib/plutonium/ui/table/resource.rb +98 -0
  147. data/lib/plutonium/ui/table/theme.rb +35 -0
  148. data/lib/plutonium/ui.rb +9 -0
  149. data/lib/plutonium/version.rb +5 -1
  150. data/lib/plutonium.rb +53 -26
  151. data/package-lock.json +19 -22
  152. data/package.json +4 -4
  153. data/sig/.keep +0 -0
  154. data/src/css/plutonium.css +15 -0
  155. data/tailwind.options.js +11 -3
  156. metadata +220 -81
  157. data/lib/generators/pu/core/install/templates/app/presenters/resource_presenter.rb.tt +0 -2
  158. data/lib/generators/pu/core/install/templates/app/query_objects/resource_query_object.rb.tt +0 -2
  159. data/lib/generators/pu/pkg/feature/templates/app/query_objects/resource_query_object.rb.tt +0 -4
  160. data/lib/plutonium/concerns/resource_validatable.rb +0 -34
  161. data/lib/plutonium/config.rb +0 -9
  162. data/lib/plutonium/core/controllers/base.rb +0 -101
  163. data/lib/plutonium/core/controllers/presentable.rb +0 -65
  164. data/lib/plutonium/core/controllers/queryable.rb +0 -28
  165. data/lib/plutonium/pkg/app.rb +0 -35
  166. data/lib/plutonium/pkg/concerns/resource_validatable.rb +0 -36
  167. data/lib/plutonium/pkg/feature.rb +0 -18
  168. data/lib/plutonium/policy/initializer.rb +0 -22
  169. data/lib/plutonium/policy/scope.rb +0 -19
  170. data/lib/plutonium/pundit/context.rb +0 -18
  171. data/lib/plutonium/pundit/policy_finder.rb +0 -25
  172. data/lib/plutonium/resource/policy_context.rb +0 -5
  173. data/lib/plutonium/resource_register.rb +0 -83
  174. data/lib/plutonium/smart_cache.rb +0 -151
  175. data/sig/plutonium.rbs +0 -12
  176. /data/app/views/{application → plutonium}/_flash.html.erb +0 -0
  177. /data/app/views/{application → plutonium}/_flash_alerts.html.erb +0 -0
  178. /data/app/views/{application → plutonium}/_flash_toasts.html.erb +0 -0
  179. /data/lib/generators/pu/pkg/{app/templates/app/views/package → package/templates}/.keep +0 -0
  180. /data/lib/generators/pu/pkg/{feature → package}/templates/app/interactions/resource_interaction.rb.tt +0 -0
  181. /data/lib/generators/pu/pkg/{feature → package}/templates/app/models/resource_record.rb.tt +0 -0
  182. /data/lib/generators/pu/pkg/{feature → package}/templates/app/policies/resource_policy.rb.tt +0 -0
  183. /data/lib/generators/pu/pkg/{feature → package}/templates/app/presenters/resource_presenter.rb.tt +0 -0
  184. /data/lib/generators/pu/pkg/{feature → package}/templates/lib/engine.rb.tt +0 -0
  185. /data/lib/generators/pu/pkg/{app → portal}/templates/app/policies/resource_policy.rb.tt +0 -0
  186. /data/lib/generators/pu/pkg/{app → portal}/templates/app/presenters/resource_presenter.rb.tt +0 -0
  187. /data/lib/generators/pu/pkg/{app → portal}/templates/app/query_objects/resource_query_object.rb.tt +0 -0
  188. /data/lib/generators/pu/pkg/{feature/templates → portal/templates/app/views/package}/.keep +0 -0
  189. /data/lib/generators/pu/pkg/{app → portal}/templates/config/routes.rb.tt +0 -0
  190. /data/lib/generators/pu/pkg/{app → portal}/templates/lib/engine.rb.tt +0 -0
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Definition
5
+ # Module for handling defineable properties in Plutonium definitions
6
+ #
7
+ # @example
8
+ # class MyDefinition
9
+ # include DefineableProperties
10
+ #
11
+ # defineable_property :field
12
+ # defineable_property :input
13
+ # defineable_property :filter
14
+ # defineable_property :scope
15
+ # defineable_property :sorter
16
+ # end
17
+ module DefineableProps
18
+ extend ActiveSupport::Concern
19
+
20
+ included do
21
+ class_attribute :_defineable_props_store, instance_accessor: false, instance_predicate: false, default: []
22
+ end
23
+
24
+ class_methods do
25
+ def defineable_props(*property_names)
26
+ property_names.each { |name| defineable_prop(name) }
27
+ end
28
+
29
+ # Defines a new property type for the class
30
+ #
31
+ # @param property_name [Symbol] The name of the property to define
32
+ # @return [void]
33
+ def defineable_prop(property_name)
34
+ property_plural = property_name.to_s.pluralize.to_sym
35
+ property_getter = :"defined_#{property_plural}"
36
+ self._defineable_props_store += [property_plural]
37
+
38
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
39
+ def self.#{property_name}(name, **options, &block)
40
+ #{property_getter}[name] = { options:, block:}.compact
41
+ end
42
+
43
+ def self.#{property_getter}
44
+ @#{property_getter} ||= {}
45
+ end
46
+
47
+ def #{property_name}(name, **options, &block)
48
+ instance_#{property_getter}[name] = { options:, block:}.compact
49
+ end
50
+
51
+ def #{property_getter}
52
+ @merged_#{property_getter} ||= begin
53
+ customize_#{property_plural}
54
+ merged = {}
55
+ self.class.#{property_getter}.each do |name, data|
56
+ merged[name] = {
57
+ options: data[:options].dup,
58
+ block: data[:block]
59
+ }.compact
60
+ end
61
+ instance_#{property_getter}.each do |name, data|
62
+ if merged.key?(name)
63
+ merged[name][:options].merge!(data[:options])
64
+ merged[name][:block] = data[:block] if data[:block]
65
+ else
66
+ merged[name] = data
67
+ end
68
+ # merged[name].compact!
69
+ end
70
+ merged
71
+ end
72
+ end
73
+
74
+ def customize_#{property_plural}
75
+ # Override in subclass to add or modify #{property_plural}
76
+ end
77
+
78
+ private
79
+
80
+ def instance_#{property_getter}
81
+ @instance_#{property_getter} ||= {}
82
+ end
83
+ RUBY
84
+ end
85
+
86
+ # Handles inheritance by duplicating class-level collections
87
+ def inherited(subclass)
88
+ super
89
+ _defineable_props_store.each do |property|
90
+ subclass.instance_variable_set(:"@defined_#{property}", instance_variable_get(:"@defined_#{property}")&.deep_dup || {})
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,21 @@
1
+ module Plutonium
2
+ module Definition
3
+ module Search
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :_search_definition, instance_accessor: false, instance_predicate: false
8
+ end
9
+
10
+ def search_definition
11
+ self.class._search_definition
12
+ end
13
+
14
+ class_methods do
15
+ def search(&block)
16
+ self._search_definition = block
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Engine
5
+ module Validator
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ # Validates that the current engine supports Plutonium features.
10
+ #
11
+ # @raise [ArgumentError] If the engine doesn't include Plutonium::Engine.
12
+ # @return [void]
13
+ def validate_engine!(engine)
14
+ return if supported_engine?(engine)
15
+
16
+ # TODO: make the error link to documentation on how to ensure that your engine is supported
17
+ raise ArgumentError, "#{engine} must include Plutonium::Engine to call register resources"
18
+ end
19
+
20
+ # Checks if the current engine supports Plutonium features.
21
+ #
22
+ # @return [Boolean] True if the engine includes Plutonium::Engine, false otherwise.
23
+ def supported_engine?(engine)
24
+ # TODO: fix constant being out of sync after reload during development
25
+ Plutonium.configuration.development? ? engine.respond_to?(:dom_id) : engine.include?(Plutonium::Engine)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Engine
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ attr_reader :scoped_entity_class, :scoped_entity_strategy, :scoped_entity_param_key
9
+
10
+ def scope_to_entity(entity_class, strategy: :path, param_key: nil)
11
+ @scoped_entity_class = entity_class
12
+ @scoped_entity_strategy = strategy
13
+ @scoped_entity_param_key = param_key || entity_class.model_name.singular_route_key.to_sym
14
+ end
15
+
16
+ def scoped_to_entity?
17
+ scoped_entity_class.present?
18
+ end
19
+
20
+ def dom_id
21
+ module_parent_name.underscore.dasherize
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,41 +1,94 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Plutonium
2
4
  module Helpers
5
+ # Helper module for managing asset-related functionality
3
6
  module AssetsHelper
7
+ # Generate a stylesheet tag for the resource
8
+ #
9
+ # @return [ActiveSupport::SafeBuffer] HTML stylesheet link tag
4
10
  def resource_stylesheet_tag
5
- url = if Plutonium.development?
6
- filename = JSON.parse(File.read(Plutonium.root.join("src", "build", "css.manifest")))["plutonium.css"]
7
- "/build/#{filename}"
8
- else
9
- resource_stylesheet_asset
10
- end
11
- stylesheet_link_tag url, "data-turbo-track": "reload"
11
+ url = resource_asset_url_for(:css, resource_stylesheet_asset)
12
+ stylesheet_link_tag(url, "data-turbo-track": "reload")
12
13
  end
13
14
 
15
+ # Generate a script tag for the resource
16
+ #
17
+ # @return [ActiveSupport::SafeBuffer] HTML script tag
14
18
  def resource_script_tag
15
- url = if Plutonium.development?
16
- filename = JSON.parse(File.read(Plutonium.root.join("src", "build", "js.manifest")))["plutonium.js"]
17
- "/build/#{filename}"
18
- else
19
- resource_script_asset
20
- end
21
- javascript_include_tag url, "data-turbo-track": "reload", type: "module"
19
+ url = resource_asset_url_for(:js, resource_script_asset)
20
+ javascript_include_tag(url, "data-turbo-track": "reload", type: "module")
22
21
  end
23
22
 
23
+ # Generate a favicon link tag
24
+ #
25
+ # @return [ActiveSupport::SafeBuffer] HTML favicon link tag
24
26
  def resource_favicon_tag
25
- favicon_link_tag resource_favicon_asset
27
+ favicon_link_tag(resource_favicon_asset)
26
28
  end
27
29
 
30
+ # Generate an image tag for the logo
31
+ #
32
+ # @param classname [String] CSS class name for the image tag
33
+ # @return [ActiveSupport::SafeBuffer] HTML image tag
28
34
  def resource_logo_tag(classname:)
29
- image_tag resource_logo_asset, class: classname
35
+ image_tag(resource_logo_asset, class: classname)
30
36
  end
31
37
 
32
- def resource_logo_asset = Rails.application.config.plutonium.assets.logo
38
+ # Get the logo asset path
39
+ #
40
+ # @return [String] path to the logo asset
41
+ def resource_logo_asset
42
+ Plutonium.configuration.assets.logo
43
+ end
33
44
 
34
- def resource_stylesheet_asset = Rails.application.config.plutonium.assets.stylesheet
45
+ # Get the stylesheet asset path
46
+ #
47
+ # @return [String] path to the stylesheet asset
48
+ def resource_stylesheet_asset
49
+ Plutonium.configuration.assets.stylesheet
50
+ end
35
51
 
36
- def resource_script_asset = Rails.application.config.plutonium.assets.script
52
+ # Get the script asset path
53
+ #
54
+ # @return [String] path to the script asset
55
+ def resource_script_asset
56
+ Plutonium.configuration.assets.script
57
+ end
37
58
 
38
- def resource_favicon_asset = Rails.application.config.plutonium.assets.favicon
59
+ # Get the favicon asset path
60
+ #
61
+ # @return [String] path to the favicon asset
62
+ def resource_favicon_asset
63
+ Plutonium.configuration.assets.favicon
64
+ end
65
+
66
+ private
67
+
68
+ # Generate the appropriate asset URL based on the environment
69
+ #
70
+ # @param type [Symbol] asset type (:css or :js)
71
+ # @param fallback [String] fallback asset path
72
+ # @return [String] asset URL
73
+ def resource_asset_url_for(type, fallback)
74
+ if Plutonium.configuration.development?
75
+ resource_development_asset_url(type)
76
+ else
77
+ fallback
78
+ end
79
+ end
80
+
81
+ # Generate the asset URL for development environment
82
+ #
83
+ # @param type [Symbol] asset type (:css or :js)
84
+ # @return [String] asset URL for development
85
+ def resource_development_asset_url(type)
86
+ manifest_file = (type == :css) ? "css.manifest" : "js.manifest"
87
+ asset_key = (type == :css) ? "plutonium.css" : "plutonium.js"
88
+
89
+ filename = JSON.parse(File.read(Plutonium.root.join("src", "build", manifest_file)))[asset_key]
90
+ "/build/#{filename}"
91
+ end
39
92
  end
40
93
  end
41
94
  end
@@ -1,5 +1,3 @@
1
- require "view_component/form"
2
-
3
1
  module Plutonium
4
2
  module Helpers
5
3
  module FormHelper
@@ -23,7 +21,7 @@ module Plutonium
23
21
 
24
22
  def token_tag(...)
25
23
  # needed to workaround https://github.com/tailwindlabs/tailwindcss/issues/3350
26
- super(...).sub(" />", " hidden />").html_safe
24
+ super.sub(" />", " hidden />").html_safe
27
25
  end
28
26
 
29
27
  private
@@ -0,0 +1,369 @@
1
+ # Interactions
2
+
3
+ ## Table of Contents
4
+
5
+ 1. [Introduction](#introduction)
6
+ 2. [Key Concepts](#key-concepts)
7
+ 3. [Core Components](#core-components)
8
+ 4. [Setup](#setup)
9
+ 5. [Usage](#usage)
10
+ 6. [Best Practices](#best-practices)
11
+ 7. [Testing](#testing)
12
+ 8. [Advanced Features](#advanced-features)
13
+ 9. [Examples](#examples)
14
+
15
+ ## Introduction
16
+
17
+ Interactions allows us to leverage an architectural approach that focuses on organizing code around business actions or user interactions.
18
+ It builds upon the traditional MVC pattern by introducing additional layers that encapsulate business logic and improve separation of concerns.
19
+
20
+ ### Key Benefits
21
+
22
+ - Clear separation of business logic from controllers
23
+ - Improved testability of business operations
24
+ - Consistent handling of success and failure cases
25
+ - Flexible and expressive way to chain operations
26
+ - Enhanced maintainability and readability of complex business processes
27
+ - Improved code organization and discoverability of business logic
28
+
29
+ ## Key Concepts
30
+
31
+ ### Interactions
32
+
33
+ Interactions are the core of this pattern. They represent specific use cases or business operations in your application. Each interaction is responsible for a single, well-defined task.
34
+ Interactions encapsulate the business logic, input validation, and outcome handling, providing a clean interface between the controller and the application's core functionality.
35
+
36
+ ### Outcomes
37
+
38
+ Outcomes encapsulate the result of an interaction, providing a consistent interface for handling both success and failure scenarios. Outcomes can have an associated response, which can be set explicitly using the `with_response` method. The value of the outcome is separate from its response, allowing for more flexible handling of interaction results.
39
+ <!-- ### Workflows
40
+
41
+ Workflows allow you to compose multiple interactions into a larger, more complex business process while maintaining separation of concerns. -->
42
+ ## Core Components
43
+
44
+ ### Plutonium::Interaction::Base
45
+
46
+ The foundation for all interactions. It integrates with ActiveModel for attribute definition and validations.
47
+
48
+ ```ruby
49
+ class MyInteraction < Plutonium::Interaction::Base
50
+ attribute :some_input, :string
51
+ validates :some_input, presence: true
52
+
53
+ private
54
+
55
+ def execute
56
+ # Implementation
57
+ end
58
+ end
59
+ ```
60
+
61
+ ### Plutonium::Interaction::Outcome
62
+
63
+ Encapsulates the result of an interaction. It has two subclasses:
64
+
65
+ - `Success`: Represents a successful operation
66
+ - `Failure`: Represents a failed operation
67
+
68
+ ### Plutonium::Interaction::Response
69
+
70
+ Represents controller operations that can be performed as a result of a successful interaction.
71
+ We ship with these out of the box:
72
+
73
+ - `Plutonium::Interaction::Response::Redirect`
74
+ - `Plutonium::Interaction::Response::Render`
75
+ - `Plutonium::Interaction::Response::Null`
76
+
77
+ ## Usage
78
+
79
+ ### Creating an Interaction
80
+
81
+ 1. Create a new file in `app/interactions/`, e.g., `app/interactions/users/create_user.rb`:
82
+
83
+ ```ruby
84
+ module Users
85
+ class CreateUser < Plutonium::Interaction::Base
86
+ attribute :first_name, :string
87
+ attribute :last_name, :string
88
+ attribute :email, :string
89
+
90
+ validates :first_name, :last_name, presence: true
91
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
92
+
93
+ private
94
+
95
+ def execute
96
+ user = User.new(attributes)
97
+ if user.save
98
+ success(user)
99
+ .with_response(Response::Redirect.new(user_path(user)))
100
+ .with_message("User was successfully created.")
101
+ else
102
+ failure(user.errors)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ ```
108
+
109
+ ### Using an Interaction in a Controller
110
+
111
+ ```ruby
112
+ class UsersController < ApplicationController
113
+ def create
114
+ process_outcome(Users::CreateUser.call(user_params))
115
+ end
116
+
117
+ private
118
+
119
+ def user_params
120
+ params.require(:user).permit(:first_name, :last_name, :email)
121
+ end
122
+ end
123
+ ```
124
+
125
+ ### Processing Outcomes
126
+
127
+ In your `ApplicationController`:
128
+
129
+ ```ruby
130
+ class ApplicationController < ActionController::Base
131
+ private
132
+
133
+ def process_outcome(outcome)
134
+ if outcome.success?
135
+ outcome.to_response.process(self) do |value|
136
+ # Default response.
137
+ # Executed if the interaction does not produce a specific Response.
138
+ render json: value
139
+ end
140
+ else
141
+ outcome.messages.each { |msg, type| flash.now[type] = msg }
142
+ render json: { errors: outcome.errors }, status: :unprocessable_entity
143
+ end
144
+ end
145
+ end
146
+ ```
147
+
148
+ ### Executing an Interaction
149
+
150
+ Interactions can be executed using the call class method:
151
+
152
+ ```ruby
153
+ outcome = MyInteraction.call(some_input: "value")
154
+
155
+ if outcome.success?
156
+ # Handle success case
157
+ else
158
+ # Handle failure case
159
+ end
160
+ ```
161
+
162
+ Or within another interaction:
163
+
164
+ ```ruby
165
+ def execute
166
+ MyInteraction.call(some_input: "value")
167
+ .and_then { |result| do_something_with(result) }
168
+ .with_response(Response::Redirect.new(some_path(final_result)))
169
+ .with_message("Operation completed successfully")
170
+ end
171
+ ```
172
+
173
+ ## Best Practices
174
+
175
+ 1. Keep interactions focused on a single responsibility
176
+ 2. Use meaningful names for interactions that describe the action being performed
177
+ 3. Leverage the `and_then` method for clean and expressive operation chaining
178
+ 4. Prefer small, composable interactions over large, monolithic ones
179
+ 5. Use `with_response` to explicitly set the desired response type
180
+ 6. Keep the interaction's core logic separate from response handling
181
+
182
+ ## Testing
183
+
184
+ Interactions are easy to test in isolation. Here's an example using RSpec:
185
+
186
+ ```ruby
187
+ RSpec.describe Users::CreateUser do
188
+ let(:valid_attributes) { { first_name: "John", last_name: "Doe", email: "john@example.com" } }
189
+
190
+ it "creates a user successfully" do
191
+ outcome = described_class.call(valid_attributes)
192
+
193
+ expect(outcome).to be_success
194
+ expect(outcome.value).to be_a(User)
195
+ expect(outcome.to_response).to be_a(Response::Redirect)
196
+ expect(User.last.email).to eq("john@example.com")
197
+ end
198
+
199
+ it "fails with invalid attributes" do
200
+ outcome = described_class.call(first_name: "", last_name: "Doe", email: "invalid")
201
+
202
+ expect(outcome).to be_failure
203
+ expect(outcome.errors).to include("First name can't be blank")
204
+ expect(outcome.errors).to include("Email is invalid")
205
+ end
206
+ end
207
+ ```
208
+
209
+ ## Advanced Features
210
+
211
+ <!--
212
+ ### Workflows
213
+
214
+ Workflows allow you to compose multiple interactions into a larger business process:
215
+
216
+ ```ruby
217
+ module Orders
218
+ class PlaceOrder < Plutonium::Interaction::Base
219
+ presents label: "Place Order",
220
+ icon: "shopping-cart",
221
+ description: "Process a new order"
222
+
223
+ attribute :user_id, :integer
224
+ attribute :product_ids, :string
225
+ attribute :payment_method, :string
226
+
227
+ validates :user_id, :product_ids, :payment_method, presence: true
228
+
229
+ workflow do
230
+ step :validate_products, ValidateProducts
231
+ step :check_inventory, CheckInventory
232
+ step :process_payment, ProcessPayment, if: ->(ctx) { ctx[:total_price] > 0 }
233
+ step :create_order, CreateOrder
234
+ step :send_confirmation, SendOrderConfirmation
235
+ end
236
+
237
+ private
238
+
239
+ def execute
240
+ execute_workflow(attributes.to_h)
241
+ .map { |ctx| Response::Redirect.new(order_path(ctx[:order])) }
242
+ .with_message("Order placed successfully.")
243
+ end
244
+ end
245
+ end
246
+ ```
247
+ -->
248
+
249
+ ### Presentable Concern
250
+
251
+ The `Presentable` concern allows you to add metadata to your interactions, which can be used for generating UI components or documentation:
252
+
253
+ ```ruby
254
+ class MyInteraction < Plutonium::Interaction::Base
255
+ include Plutonium::Interaction::Concerns::Presentable
256
+
257
+ presents label: "My Interaction",
258
+ icon: "star",
259
+ description: "Does something awesome"
260
+
261
+ # ... rest of the interaction
262
+ end
263
+ ```
264
+
265
+ ## Examples
266
+
267
+ ### Chaining Operations
268
+
269
+ ```ruby
270
+ module Orders
271
+ class PlaceOrder < Plutonium::Interaction::Base
272
+ attribute :user_id, :integer
273
+ attribute :product_ids, :string
274
+
275
+ private
276
+
277
+ def execute
278
+ success(attributes)
279
+ .and_then { |attrs| find_user(attrs[:user_id]) }
280
+ .and_then { |user| find_products(user, attributes[:product_ids]) }
281
+ .and_then { |user, products| create_order(user, products) }
282
+ .with_response(Response::Redirect.new(order_path(order)))
283
+ .with_message("Order placed successfully.")
284
+ end
285
+
286
+ def find_user(user_id)
287
+ user = User.find_by(id: user_id)
288
+ user ? success(user) : failure(["User not found"])
289
+ end
290
+
291
+ def find_products(user, product_ids)
292
+ products = Product.where(id: product_ids.split(','))
293
+ products.empty? ? failure(["No valid products found"]) : success([user, products])
294
+ end
295
+
296
+ def create_order(user, products)
297
+ order = Order.create(user: user, products: products)
298
+ order.persisted? ? success(order) : failure(order.errors.full_messages)
299
+ end
300
+ end
301
+ end
302
+ ```
303
+
304
+ This example demonstrates how to chain multiple operations, handle potential failures at each step, and return an appropriate outcome with a specific response type. Note how the `with_response` and `with_message` methods are used to set the response and add a message to the outcome.
305
+
306
+ By following these guidelines and examples, you can effectively implement and use the Interaction pattern in your Rails applications, leading to more maintainable and testable code.
307
+
308
+ <!--
309
+
310
+ This example demonstrates how to chain multiple operations, handle potential failures at each step, and return an appropriate outcome.
311
+
312
+ By following these guidelines and examples, you can effectively implement and use the Use Case Driven Design pattern in your Rails applications, leading to more maintainable and testable code.
313
+
314
+
315
+ ### Example interaction with workflow
316
+
317
+ ```ruby
318
+ module Orders
319
+ class PlaceOrder < Plutonium::Interaction::Base
320
+ presents label: "Place Order",
321
+ icon: "shopping-cart",
322
+ description: "Process a new order",
323
+ category: "Order Management"
324
+
325
+ attribute :user_id, :integer
326
+ attribute :product_ids, :string
327
+ attribute :payment_method, :string
328
+
329
+ validates :user_id, :product_ids, :payment_method, presence: true
330
+
331
+ workflow do
332
+ step :validate_products, use_case: ValidateProducts
333
+ step :check_inventory, use_case: CheckInventory
334
+ step :process_payment, use_case: ProcessPayment, if: ->(ctx) { ctx[:total_price] > 0 }
335
+ step :create_order, use_case: CreateOrder
336
+ step :send_confirmation, use_case: SendOrderConfirmation
337
+ end
338
+
339
+ private
340
+
341
+ def execute
342
+ execute_workflow(attributes.to_h)
343
+ .map { |ctx| Actions::RedirectAction.new(:order_path, id: ctx[:order].id) }
344
+ .with_message("Order placed successfully.")
345
+ end
346
+ end
347
+
348
+ class ValidateProducts < Plutonium::Interaction::Base
349
+ # Implementation...
350
+ end
351
+
352
+ class CheckInventory < Plutonium::Interaction::Base
353
+ # Implementation...
354
+ end
355
+
356
+ class ProcessPayment < Plutonium::Interaction::Base
357
+ # Implementation...
358
+ end
359
+
360
+ class CreateOrder < Plutonium::Interaction::Base
361
+ # Implementation...
362
+ end
363
+
364
+ class SendOrderConfirmation < Plutonium::Interaction::Base
365
+ # Implementation...
366
+ end
367
+ end
368
+ ```
369
+ -->