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.
- checksums.yaml +4 -4
- data/README copy.md +1 -1
- data/README.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/app/views/{application → plutonium}/_resource_header.html copy.erb +1 -1
- data/app/views/{application → plutonium}/_resource_header.html.erb +1 -1
- data/app/views/{application → plutonium}/_resource_sidebar.html.erb +2 -0
- data/app/views/resource/_resource_details.html.erb +1 -36
- data/app/views/resource/_resource_form.html.erb +1 -5
- data/app/views/resource/_resource_table.html.erb +315 -85
- data/app/views/resource/edit.html.erb +1 -5
- data/app/views/resource/index.html.erb +1 -5
- data/app/views/resource/new.html.erb +1 -5
- data/app/views/resource/show.html.erb +1 -5
- data/config/initializers/pagy.rb +1 -0
- data/config/initializers/rabl.rb +27 -20
- data/gemfiles/rails_7.gemfile.lock +5 -1
- data/lib/generators/pu/core/assets/assets_generator.rb +2 -2
- data/lib/generators/pu/core/install/install_generator.rb +0 -3
- data/lib/generators/pu/core/install/templates/app/controllers/plutonium_controller.rb.tt +2 -0
- data/lib/generators/pu/core/install/templates/app/controllers/resource_controller.rb.tt +21 -1
- data/lib/generators/pu/core/install/templates/app/definitions/resource_definition.rb.tt +2 -0
- data/lib/generators/pu/core/install/templates/app/models/resource_record.rb.tt +0 -2
- data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +5 -2
- data/lib/generators/pu/eject/shell/shell_generator.rb +2 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +19 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/logger.rb +1 -1
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +5 -3
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +26 -2
- data/lib/generators/pu/pkg/{feature/feature_generator.rb → package/package_generator.rb} +4 -4
- data/lib/generators/pu/pkg/{feature → package}/templates/app/controllers/resource_controller.rb.tt +0 -2
- data/lib/generators/pu/pkg/package/templates/app/definitions/resource_definition.rb.tt +4 -0
- data/lib/generators/pu/pkg/package/templates/app/query_objects/resource_query_object.rb.tt +4 -0
- data/lib/generators/pu/pkg/{app/app_generator.rb → portal/portal_generator.rb} +10 -8
- data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/concerns/controller.rb.tt +3 -7
- data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/dashboard_controller.rb.tt +1 -1
- data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +5 -0
- data/lib/generators/pu/pkg/{app/templates/app/controllers/controller.rb.tt → portal/templates/app/controllers/resource_controller.rb.tt} +1 -1
- data/lib/generators/pu/pkg/portal/templates/app/definitions/resource_definition.rb.tt +4 -0
- data/lib/generators/pu/pkg/{app → portal}/templates/app/views/package/dashboard/index.html.erb +2 -1
- data/lib/generators/pu/res/conn/conn_generator.rb +78 -3
- data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +1 -1
- data/lib/generators/pu/res/conn/templates/app/definitions/resource_definition.rb.tt +3 -0
- data/lib/generators/pu/res/conn/templates/app/policies/resource_policy.rb.tt +29 -1
- data/lib/generators/pu/res/conn/templates/app/presenters/resource_presenter.rb.tt +1 -1
- data/lib/generators/pu/res/conn/templates/app/query_objects/resource_query_object.rb.tt +1 -1
- data/lib/generators/pu/res/model/model_generator.rb +0 -7
- data/lib/generators/pu/res/model/templates/model.rb.tt +4 -1
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +22 -4
- data/lib/generators/pu/res/scaffold/templates/controller.rb.tt +0 -1
- data/lib/generators/pu/res/scaffold/templates/definition.rb.tt +4 -0
- data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +2 -2
- data/lib/generators/pu/rodauth/templates/app/controllers/rodauth_controller.rb.tt +1 -1
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +270 -0
- data/lib/plutonium/action/README.md +0 -0
- data/lib/plutonium/action/base.rb +103 -0
- data/lib/plutonium/action/interactive.rb +117 -0
- data/lib/plutonium/action/route_options.rb +65 -0
- data/lib/plutonium/action/simple.rb +8 -0
- data/lib/plutonium/auth.rb +1 -1
- data/lib/plutonium/configuration.rb +130 -0
- data/lib/plutonium/core/actions/collection.rb +1 -1
- data/lib/plutonium/core/associations/renderers/factory.rb +3 -1
- data/lib/plutonium/core/autodiscovery/association_renderer_discoverer.rb +1 -1
- data/lib/plutonium/core/autodiscovery/input_discoverer.rb +1 -1
- data/lib/plutonium/core/autodiscovery/renderer_discoverer.rb +1 -1
- data/lib/plutonium/core/controller.rb +110 -0
- data/lib/plutonium/core/controllers/authorizable.rb +12 -35
- data/lib/plutonium/core/controllers/bootable.rb +38 -7
- data/lib/plutonium/core/controllers/entity_scoping.rb +6 -2
- data/lib/plutonium/core/fields/renderers/association_renderer.rb +1 -1
- data/lib/plutonium/core/ui/collection.rb +1 -1
- data/lib/plutonium/core/ui/detail.rb +1 -1
- data/lib/plutonium/core/ui/form.rb +1 -1
- data/lib/plutonium/definition/actions.rb +50 -0
- data/lib/plutonium/definition/base.rb +92 -0
- data/lib/plutonium/definition/config_attr.rb +30 -0
- data/lib/plutonium/definition/defineable_props.rb +96 -0
- data/lib/plutonium/definition/search.rb +21 -0
- data/lib/plutonium/engine/validator.rb +30 -0
- data/lib/plutonium/engine.rb +25 -0
- data/lib/plutonium/helpers/assets_helper.rb +73 -20
- data/lib/plutonium/helpers/form_helper.rb +1 -3
- data/lib/plutonium/interaction/README.md +369 -0
- data/lib/plutonium/interaction/base.rb +75 -0
- data/lib/plutonium/interaction/concerns/presentable.rb +61 -0
- data/lib/plutonium/interaction/concerns/workflow_dsl.rb +82 -0
- data/lib/plutonium/interaction/outcome.rb +129 -0
- data/lib/plutonium/interaction/response/base.rb +63 -0
- data/lib/plutonium/interaction/response/null.rb +33 -0
- data/lib/plutonium/interaction/response/redirect.rb +30 -0
- data/lib/plutonium/interaction/response/render.rb +28 -0
- data/lib/plutonium/lib/bit_flags.rb +70 -9
- data/lib/plutonium/lib/overlayed_hash.rb +86 -0
- data/lib/plutonium/lib/smart_cache.rb +171 -0
- data/lib/plutonium/models/has_cents.rb +170 -0
- data/lib/plutonium/{pkg/base.rb → package/engine.rb} +10 -2
- data/lib/plutonium/{application → portal}/controller.rb +3 -11
- data/lib/plutonium/{application → portal}/dynamic_controllers.rb +4 -4
- data/lib/plutonium/portal/engine.rb +15 -0
- data/lib/plutonium/railtie.rb +35 -15
- data/lib/plutonium/reloader.rb +71 -29
- data/lib/plutonium/resource/controller.rb +51 -34
- data/lib/plutonium/resource/controllers/authorizable.rb +128 -0
- data/lib/plutonium/{core → resource}/controllers/crud_actions.rb +23 -22
- data/lib/plutonium/resource/controllers/defineable.rb +26 -0
- data/lib/plutonium/{core → resource}/controllers/interactive_actions.rb +12 -12
- data/lib/plutonium/resource/controllers/presentable.rb +41 -0
- data/lib/plutonium/resource/controllers/queryable.rb +44 -0
- data/lib/plutonium/resource/definition.rb +6 -0
- data/lib/plutonium/resource/policy.rb +25 -13
- data/lib/plutonium/resource/query_object.rb +50 -51
- data/lib/plutonium/resource/record.rb +6 -89
- data/lib/plutonium/resource/register.rb +82 -0
- data/lib/plutonium/routing/mapper_extensions.rb +1 -1
- data/lib/plutonium/routing/resource_registration.rb +1 -1
- data/lib/plutonium/routing/route_set_extensions.rb +6 -18
- data/lib/plutonium/ui/action_button.rb +125 -0
- data/lib/plutonium/ui/breadcrumbs.rb +163 -0
- data/lib/plutonium/ui/component/base.rb +13 -0
- data/lib/plutonium/ui/component/behaviour.rb +38 -0
- data/lib/plutonium/ui/component/kit.rb +31 -0
- data/lib/plutonium/ui/component/methods.rb +54 -0
- data/lib/plutonium/ui/display/base.rb +25 -0
- data/lib/plutonium/ui/display/component/association.rb +26 -0
- data/lib/plutonium/ui/display/resource.rb +77 -0
- data/lib/plutonium/ui/display/theme.rb +27 -0
- data/lib/plutonium/ui/dyna_frame/content.rb +20 -0
- data/lib/plutonium/ui/empty_card.rb +20 -0
- data/lib/plutonium/ui/form/base.rb +37 -0
- data/lib/plutonium/ui/form/resource.rb +75 -0
- data/lib/plutonium/ui/form/theme.rb +42 -0
- data/lib/plutonium/ui/page/base.rb +112 -0
- data/lib/plutonium/ui/page/edit.rb +23 -0
- data/lib/plutonium/ui/page/index.rb +27 -0
- data/lib/plutonium/ui/page/new.rb +23 -0
- data/lib/plutonium/ui/page/show.rb +27 -0
- data/lib/plutonium/ui/page_header.rb +49 -0
- data/lib/plutonium/ui/table/base.rb +13 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +70 -0
- data/lib/plutonium/ui/table/components/pagy_page_info.rb +70 -0
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +105 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +136 -0
- data/lib/plutonium/ui/table/components/search_bar.rb +158 -0
- data/lib/plutonium/ui/table/display_theme.rb +21 -0
- data/lib/plutonium/ui/table/resource.rb +98 -0
- data/lib/plutonium/ui/table/theme.rb +35 -0
- data/lib/plutonium/ui.rb +9 -0
- data/lib/plutonium/version.rb +5 -1
- data/lib/plutonium.rb +53 -26
- data/package-lock.json +19 -22
- data/package.json +4 -4
- data/sig/.keep +0 -0
- data/src/css/plutonium.css +15 -0
- data/tailwind.options.js +11 -3
- metadata +220 -81
- data/lib/generators/pu/core/install/templates/app/presenters/resource_presenter.rb.tt +0 -2
- data/lib/generators/pu/core/install/templates/app/query_objects/resource_query_object.rb.tt +0 -2
- data/lib/generators/pu/pkg/feature/templates/app/query_objects/resource_query_object.rb.tt +0 -4
- data/lib/plutonium/concerns/resource_validatable.rb +0 -34
- data/lib/plutonium/config.rb +0 -9
- data/lib/plutonium/core/controllers/base.rb +0 -101
- data/lib/plutonium/core/controllers/presentable.rb +0 -65
- data/lib/plutonium/core/controllers/queryable.rb +0 -28
- data/lib/plutonium/pkg/app.rb +0 -35
- data/lib/plutonium/pkg/concerns/resource_validatable.rb +0 -36
- data/lib/plutonium/pkg/feature.rb +0 -18
- data/lib/plutonium/policy/initializer.rb +0 -22
- data/lib/plutonium/policy/scope.rb +0 -19
- data/lib/plutonium/pundit/context.rb +0 -18
- data/lib/plutonium/pundit/policy_finder.rb +0 -25
- data/lib/plutonium/resource/policy_context.rb +0 -5
- data/lib/plutonium/resource_register.rb +0 -83
- data/lib/plutonium/smart_cache.rb +0 -151
- data/sig/plutonium.rbs +0 -12
- /data/app/views/{application → plutonium}/_flash.html.erb +0 -0
- /data/app/views/{application → plutonium}/_flash_alerts.html.erb +0 -0
- /data/app/views/{application → plutonium}/_flash_toasts.html.erb +0 -0
- /data/lib/generators/pu/pkg/{app/templates/app/views/package → package/templates}/.keep +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/app/interactions/resource_interaction.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/app/models/resource_record.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/app/policies/resource_policy.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/app/presenters/resource_presenter.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/lib/engine.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{app → portal}/templates/app/policies/resource_policy.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{app → portal}/templates/app/presenters/resource_presenter.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{app → portal}/templates/app/query_objects/resource_query_object.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature/templates → portal/templates/app/views/package}/.keep +0 -0
- /data/lib/generators/pu/pkg/{app → portal}/templates/config/routes.rb.tt +0 -0
- /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 =
|
|
6
|
-
|
|
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 =
|
|
16
|
-
|
|
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
|
|
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
|
|
35
|
+
image_tag(resource_logo_asset, class: classname)
|
|
30
36
|
end
|
|
31
37
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
-->
|