turbo_boost-commands 0.2.2 → 0.3.0
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.md +96 -29
- data/app/assets/builds/@turbo-boost/commands.js +1 -1
- data/app/assets/builds/@turbo-boost/commands.js.map +4 -4
- data/app/assets/builds/@turbo-boost/commands.metafile.json +1 -1
- data/app/controllers/concerns/turbo_boost/commands/controller.rb +1 -1
- data/app/javascript/elements.js +0 -1
- data/app/javascript/events.js +6 -3
- data/app/javascript/headers.js +2 -2
- data/app/javascript/index.js +20 -11
- data/app/javascript/invoker.js +2 -10
- data/app/javascript/lifecycle.js +3 -6
- data/app/javascript/logger.js +29 -2
- data/app/javascript/renderer.js +11 -5
- data/app/javascript/schema.js +2 -1
- data/app/javascript/state/index.js +47 -34
- data/app/javascript/state/observable.js +1 -1
- data/app/javascript/state/page.js +33 -0
- data/app/javascript/state/storage.js +11 -0
- data/app/javascript/turbo.js +0 -10
- data/app/javascript/version.js +1 -1
- data/lib/turbo_boost/commands/attribute_set.rb +8 -0
- data/lib/turbo_boost/commands/command.rb +8 -3
- data/lib/turbo_boost/commands/command_callbacks.rb +23 -6
- data/lib/turbo_boost/commands/command_validator.rb +44 -0
- data/lib/turbo_boost/commands/controller_pack.rb +10 -10
- data/lib/turbo_boost/commands/engine.rb +14 -10
- data/lib/turbo_boost/commands/errors.rb +15 -8
- data/lib/turbo_boost/commands/{middleware.rb → middlewares/entry_middleware.rb} +30 -21
- data/lib/turbo_boost/commands/middlewares/exit_middleware.rb +63 -0
- data/lib/turbo_boost/commands/patches/action_view_helpers_tag_helper_tag_builder_patch.rb +10 -2
- data/lib/turbo_boost/commands/responder.rb +28 -0
- data/lib/turbo_boost/commands/runner.rb +150 -186
- data/lib/turbo_boost/commands/sanitizer.rb +1 -1
- data/lib/turbo_boost/commands/state.rb +97 -47
- data/lib/turbo_boost/commands/state_store.rb +72 -0
- data/lib/turbo_boost/commands/token_validator.rb +51 -0
- data/lib/turbo_boost/commands/version.rb +1 -1
- metadata +29 -8
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class TurboBoost::Commands::
|
3
|
+
class TurboBoost::Commands::EntryMiddleware
|
4
4
|
PATH = "/turbo-boost-command-invocation"
|
5
5
|
|
6
6
|
def initialize(app)
|
@@ -35,6 +35,11 @@ class TurboBoost::Commands::Middleware
|
|
35
35
|
false
|
36
36
|
end
|
37
37
|
|
38
|
+
def convert_to_get_request?(driver)
|
39
|
+
return true if driver == "frame" || driver == "window"
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
38
43
|
# Modifies the given POST request so Rails sees it as GET.
|
39
44
|
#
|
40
45
|
# The posted JSON body content holds the TurboBoost Command meta data.
|
@@ -42,19 +47,21 @@ class TurboBoost::Commands::Middleware
|
|
42
47
|
#
|
43
48
|
# @example POST payload for: /turbo-boost-command-invocation
|
44
49
|
# {
|
45
|
-
# "
|
46
|
-
# "
|
47
|
-
# "
|
48
|
-
# "
|
49
|
-
# "
|
50
|
-
# "
|
51
|
-
# "
|
52
|
-
#
|
50
|
+
# "csrfToken" => "..." # Rails' CSRF token
|
51
|
+
# "id" => "turbo-command-f824ded1-a86e-4a36-9442-ea2165a64569", # Unique ID for the command invocation
|
52
|
+
# "name" => "ExampleCommand#perform", # Name of command being invoked
|
53
|
+
# "elementId" => nil, # Triggering element's DOM id
|
54
|
+
# "elementAttributes" => {...}, # Triggering element's attributes
|
55
|
+
# "startedAt" => 1708213193567, # Time the command was invoked
|
56
|
+
# "elementCache" => {...}, # Cache of ALL tracked element attributes (optimistic changes)
|
57
|
+
# "state" => { # State ... TODO: HOPSOFT
|
58
|
+
# "page" => {...}, # - transient page state (element attributes, etc.)
|
59
|
+
# "signed" => "", # - signed state used for the last server render (untampered)
|
60
|
+
# "unsigned" => {...} # - state with optimistic changes from the client
|
53
61
|
# },
|
54
|
-
# "
|
55
|
-
# "
|
56
|
-
# "
|
57
|
-
# "src" => "/basic_command.turbo_stream" # the URL to present to Rails (turbo-frame src, window location, etc.)
|
62
|
+
# "driver" => "frame", # Driver used to invoke the command
|
63
|
+
# "frameId" => "...", # TurboFrame id (if applicable)
|
64
|
+
# "src" => "..." # URL to present to Rails (turbo-frame src, window location, etc.)
|
58
65
|
# }
|
59
66
|
#
|
60
67
|
# @param request [Rack::Request] the request to modify
|
@@ -64,20 +71,22 @@ class TurboBoost::Commands::Middleware
|
|
64
71
|
|
65
72
|
request.env.tap do |env|
|
66
73
|
# Store the command params in the environment
|
67
|
-
env["
|
68
|
-
|
69
|
-
# Change the method from POST to GET
|
70
|
-
env["REQUEST_METHOD"] = "GET"
|
74
|
+
env["turbo_boost_command_params"] = params
|
71
75
|
|
72
76
|
# Update the URI, PATH_INFO, and QUERY_STRING
|
73
77
|
env["REQUEST_URI"] = uri.to_s if env.key?("REQUEST_URI")
|
74
78
|
env["PATH_INFO"] = uri.path
|
75
79
|
env["QUERY_STRING"] = uri.query.to_s
|
76
80
|
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
+
# Change the method from POST to GET
|
82
|
+
if convert_to_get_request?(params["driver"])
|
83
|
+
env["REQUEST_METHOD"] = "GET"
|
84
|
+
|
85
|
+
# Clear the body and related headers so the appears and behaves like a GET
|
86
|
+
env["rack.input"] = StringIO.new
|
87
|
+
env["CONTENT_LENGTH"] = "0"
|
88
|
+
env.delete("CONTENT_TYPE")
|
89
|
+
end
|
81
90
|
end
|
82
91
|
rescue => error
|
83
92
|
puts "#{self.class.name} failed to modify the request! #{error.message}"
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TurboBoost::Commands::ExitMiddleware
|
4
|
+
BODY_PATTERN = /<\/\s*body/io
|
5
|
+
TURBO_FRAME_PATTERN = /<\/\s*turbo-frame/io
|
6
|
+
TURBO_STREAM_PATTERN = /<\/\s*turbo-stream/io
|
7
|
+
TAIL_PATTERN = /\z/io
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
@params = env["turbo_boost_command_params"]
|
15
|
+
response = @app.call(env)
|
16
|
+
return modify!(env, response) if modify?(env)
|
17
|
+
response
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def modify?(env)
|
23
|
+
env["turbo_boost_command_responder"].is_a? TurboBoost::Commands::Responder
|
24
|
+
end
|
25
|
+
|
26
|
+
def modify!(env, response)
|
27
|
+
responder = env["turbo_boost_command_responder"]
|
28
|
+
status, headers, body = response
|
29
|
+
new_body = body_to_a(body).join
|
30
|
+
|
31
|
+
case response_type(new_body)
|
32
|
+
when :body
|
33
|
+
match = new_body.match(BODY_PATTERN).to_s
|
34
|
+
new_body.sub! BODY_PATTERN, [responder.body, match].join
|
35
|
+
when :frame
|
36
|
+
match = new_body.match(TURBO_FRAME_PATTERN).to_s
|
37
|
+
new_body.sub! TURBO_FRAME_PATTERN, [responder.body, match].join
|
38
|
+
else
|
39
|
+
new_body << responder.body
|
40
|
+
end
|
41
|
+
|
42
|
+
[status, headers.merge(responder.headers), [new_body]]
|
43
|
+
rescue => error
|
44
|
+
Rails.logger.error "TurboBoost::Commands::Runner failed to append to the response! #{error.message}"
|
45
|
+
[status, headers, body]
|
46
|
+
end
|
47
|
+
|
48
|
+
def body_to_a(body)
|
49
|
+
return [] if body.nil?
|
50
|
+
return body if body.is_a?(Array)
|
51
|
+
return [body] if body.is_a?(String)
|
52
|
+
return body.to_ary if body.respond_to?(:to_ary)
|
53
|
+
return body.each.to_a if body.respond_to?(:each)
|
54
|
+
[body.to_s]
|
55
|
+
end
|
56
|
+
|
57
|
+
def response_type(body)
|
58
|
+
return :body if body.match?(BODY_PATTERN)
|
59
|
+
return :frame if body.match?(TURBO_FRAME_PATTERN)
|
60
|
+
return :stream if body.match?(TURBO_STREAM_PATTERN)
|
61
|
+
:unknown
|
62
|
+
end
|
63
|
+
end
|
@@ -4,7 +4,15 @@ require_relative "../attribute_hydration"
|
|
4
4
|
|
5
5
|
module TurboBoost::Commands::Patches::ActionViewHelpersTagHelperTagBuilderPatch
|
6
6
|
def tag_options(options, ...)
|
7
|
-
|
8
|
-
|
7
|
+
options = turbo_boost&.state&.tag_options(options) || options
|
8
|
+
options = TurboBoost::Commands::AttributeHydration.dehydrate(options)
|
9
|
+
super(options, ...)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def turbo_boost
|
15
|
+
return nil unless @view_context.respond_to?(:turbo_boost)
|
16
|
+
@view_context.turbo_boost
|
9
17
|
end
|
10
18
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TurboBoost::Commands::Responder
|
4
|
+
attr_accessor :headers
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@headers = HashWithIndifferentAccess.new
|
8
|
+
@body = Set.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def body
|
12
|
+
@body.join.html_safe
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_header(key, value)
|
16
|
+
headers[key.to_s.downcase] = value.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_content(content)
|
20
|
+
@body << sanitizer.sanitize(content) + "\n"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def sanitizer
|
26
|
+
TurboBoost::Commands::Sanitizer.instance
|
27
|
+
end
|
28
|
+
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
3
|
+
require_relative "responder"
|
4
4
|
require_relative "state"
|
5
|
+
require_relative "command_validator"
|
6
|
+
require_relative "token_validator"
|
5
7
|
|
6
8
|
class TurboBoost::Commands::Runner
|
7
9
|
RESPONSE_HEADER = "TurboBoost-Command"
|
@@ -12,56 +14,24 @@ class TurboBoost::Commands::Runner
|
|
12
14
|
"text/vnd.turbo-stream.html" => true
|
13
15
|
}.freeze
|
14
16
|
|
15
|
-
attr_reader :controller
|
17
|
+
attr_reader :controller, :error, :responder
|
16
18
|
|
17
19
|
def initialize(controller)
|
18
20
|
@controller = controller
|
21
|
+
@responder = TurboBoost::Commands::Responder.new
|
19
22
|
end
|
20
23
|
|
21
|
-
def
|
22
|
-
@
|
23
|
-
sgid = command_params[:signed_state]
|
24
|
-
value = TurboBoost::Commands::State.from_sgid_param(sgid) if sgid
|
25
|
-
value || TurboBoost::Commands::State.new
|
26
|
-
end
|
24
|
+
def state
|
25
|
+
@state ||= TurboBoost::Commands::State.new(command_params.fetch(:state, {}))
|
27
26
|
end
|
28
27
|
|
29
28
|
def command_requested?
|
30
|
-
controller.request.env.key?("
|
31
|
-
end
|
32
|
-
|
33
|
-
def command_valid?
|
34
|
-
return false unless command_requested?
|
35
|
-
|
36
|
-
# validate class
|
37
|
-
unless command_instance.is_a?(TurboBoost::Commands::Command)
|
38
|
-
raise TurboBoost::Commands::InvalidClassError,
|
39
|
-
"`#{command_class_name}` is not a subclass of `TurboBoost::Commands::Command`!"
|
40
|
-
end
|
41
|
-
|
42
|
-
# validate method
|
43
|
-
ancestors = command_class.ancestors[0..command_class.ancestors.index(TurboBoost::Commands::Command) - 1]
|
44
|
-
unless ancestors.any? { |a| a.public_instance_methods(false).any? command_method_name.to_sym }
|
45
|
-
raise TurboBoost::Commands::InvalidMethodError,
|
46
|
-
"`#{command_class_name}` does not define the public method `#{command_method_name}`!"
|
47
|
-
end
|
48
|
-
|
49
|
-
# validate csrf token
|
50
|
-
unless valid_command_token?
|
51
|
-
raise TurboBoost::Commands::InvalidTokenError,
|
52
|
-
"Token mismatch! The token: #{client_command_token}` does not match the expected value of `#{server_command_token}`."
|
53
|
-
end
|
54
|
-
|
55
|
-
true
|
29
|
+
controller.request.env.key?("turbo_boost_command_params") || controller.params.key?("turbo_boost_command")
|
56
30
|
end
|
57
31
|
|
58
32
|
def command_params
|
59
|
-
return ActionController::Parameters.new unless command_requested?
|
60
|
-
@command_params ||=
|
61
|
-
payload = parsed_command_params.transform_keys(&:underscore)
|
62
|
-
payload["element_attributes"]&.deep_transform_keys!(&:underscore)
|
63
|
-
ActionController::Parameters.new(payload).permit!
|
64
|
-
end
|
33
|
+
return ActionController::Parameters.new.permit! unless command_requested?
|
34
|
+
@command_params ||= ActionController::Parameters.new(parsed_command_params).permit!
|
65
35
|
end
|
66
36
|
|
67
37
|
def command_name
|
@@ -87,17 +57,28 @@ class TurboBoost::Commands::Runner
|
|
87
57
|
end
|
88
58
|
|
89
59
|
def command_instance
|
90
|
-
@command_instance ||= command_class&.new(controller,
|
60
|
+
@command_instance ||= command_class&.new(controller, state, command_params).tap do |instance|
|
91
61
|
instance&.add_observer self, :handle_command_event
|
92
62
|
end
|
93
63
|
end
|
94
64
|
|
65
|
+
def command_valid?
|
66
|
+
return false unless command_requested?
|
67
|
+
|
68
|
+
validator = TurboBoost::Commands::CommandValidator.new(command_instance, command_method_name)
|
69
|
+
raise_on_invalid_command? ? validator.validate! : validator.valid?
|
70
|
+
|
71
|
+
validator = TurboBoost::Commands::TokenValidator.new(command_instance, command_method_name)
|
72
|
+
raise_on_invalid_command? ? validator.validate! : validator.valid?
|
73
|
+
end
|
74
|
+
|
95
75
|
def command_aborted?
|
96
76
|
!!command_instance&.aborted?
|
97
77
|
end
|
98
78
|
|
99
79
|
def command_errored?
|
100
|
-
|
80
|
+
return false if command_aborted?
|
81
|
+
command_instance&.errored? || error.present?
|
101
82
|
end
|
102
83
|
|
103
84
|
def command_performing?
|
@@ -105,7 +86,7 @@ class TurboBoost::Commands::Runner
|
|
105
86
|
end
|
106
87
|
|
107
88
|
def command_performed?
|
108
|
-
|
89
|
+
command_instance&.performed? || command_errored?
|
109
90
|
end
|
110
91
|
|
111
92
|
def command_succeeded?
|
@@ -132,48 +113,51 @@ class TurboBoost::Commands::Runner
|
|
132
113
|
return if command_performing?
|
133
114
|
return if command_performed?
|
134
115
|
|
135
|
-
command_instance.resolve_state command_params[:changed_state]
|
136
116
|
command_instance.perform_with_callbacks command_method_name
|
117
|
+
rescue => error
|
118
|
+
prevent_controller_action error: error if command_requested?
|
137
119
|
end
|
138
120
|
|
139
|
-
|
140
|
-
|
141
|
-
|
121
|
+
# Always runs after the controller action has been performed
|
122
|
+
def flush
|
123
|
+
return unless command_requested? # not a command request
|
142
124
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
append_success_to_response
|
147
|
-
when TurboBoost::Commands::AbortError
|
148
|
-
render_response status: error.http_status_code, status_header: error.message
|
149
|
-
append_streams_to_response_body
|
150
|
-
when TurboBoost::Commands::PerformError
|
151
|
-
render_response status: error.http_status_code, status_header: error.message
|
152
|
-
append_error_to_response error
|
153
|
-
else
|
154
|
-
render_response status: :internal_server_error, status_header: error.message
|
155
|
-
append_error_to_response error
|
156
|
-
end
|
125
|
+
# global response components
|
126
|
+
add_header
|
127
|
+
add_state
|
157
128
|
|
158
|
-
|
159
|
-
|
129
|
+
# mutually exclusive responses
|
130
|
+
respond_with_abort if command_aborted?
|
131
|
+
respond_with_error if command_errored?
|
132
|
+
respond_with_success if command_succeeded?
|
160
133
|
|
161
|
-
|
162
|
-
|
163
|
-
|
134
|
+
# store the responder for use in → TurboBoost::Commands::ExitMiddleware
|
135
|
+
controller.request.env["turbo_boost_command_responder"] = responder
|
136
|
+
rescue => error
|
137
|
+
Rails.logger.error "TurboBoost::Commands::Runner failed to update the response! #{error.message}"
|
138
|
+
end
|
164
139
|
|
140
|
+
def prevent_controller_action(error: nil)
|
165
141
|
return if controller_action_prevented?
|
142
|
+
@controller_action_prevented = true
|
143
|
+
@error = error
|
166
144
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
145
|
+
status = case error
|
146
|
+
when nil then response_status
|
147
|
+
when TurboBoost::Commands::AbortError, TurboBoost::Commands::PerformError then error.http_status_code
|
148
|
+
else :internal_server_error
|
149
|
+
end
|
150
|
+
|
151
|
+
flush
|
152
|
+
render_response status: status
|
172
153
|
end
|
173
154
|
|
174
|
-
|
175
|
-
|
176
|
-
|
155
|
+
# Renders the response body instead of the controller action.
|
156
|
+
# Invoked by: `prevent_controller_action`
|
157
|
+
# NOTE: Halts the Rails controller callback chain
|
158
|
+
def render_response(html: "", status: nil)
|
159
|
+
return if controller.performed?
|
160
|
+
controller.render html: html, layout: false, status: status || response_status
|
177
161
|
end
|
178
162
|
|
179
163
|
def turbo_stream
|
@@ -199,53 +183,46 @@ class TurboBoost::Commands::Runner
|
|
199
183
|
|
200
184
|
def parsed_command_params
|
201
185
|
@parsed_command_params ||= begin
|
202
|
-
params = controller.request.env["
|
203
|
-
params ||= JSON.parse(controller.params["turbo_boost_command"])
|
204
|
-
params
|
186
|
+
params = controller.request.env["turbo_boost_command_params"]
|
187
|
+
params ||= controller.request.env["turbo_boost_command_params"] = JSON.parse(controller.params["turbo_boost_command"])
|
188
|
+
params.deep_transform_keys!(&:underscore)
|
189
|
+
params
|
205
190
|
end
|
206
191
|
end
|
207
192
|
|
208
|
-
def
|
209
|
-
TurboBoost::Commands
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
@new_command_token ||= SecureRandom.alphanumeric(13)
|
214
|
-
end
|
215
|
-
|
216
|
-
def client_command_token
|
217
|
-
command_params.dig(:client_state, :command_token)
|
193
|
+
def alert_on_abort?
|
194
|
+
return false unless TurboBoost::Commands.config.alert_on_abort
|
195
|
+
return true if TurboBoost::Commands.config.alert_on_abort == true
|
196
|
+
return true if TurboBoost::Commands.config.alert_on_abort.to_s == Rails.env.to_s
|
197
|
+
false
|
218
198
|
end
|
219
199
|
|
220
|
-
def
|
221
|
-
|
200
|
+
def alert_on_error?
|
201
|
+
return false unless TurboBoost::Commands.config.alert_on_error
|
202
|
+
return true if TurboBoost::Commands.config.alert_on_error == true
|
203
|
+
return true if TurboBoost::Commands.config.alert_on_error.to_s == Rails.env.to_s
|
204
|
+
false
|
222
205
|
end
|
223
206
|
|
224
|
-
def
|
225
|
-
return
|
226
|
-
return
|
227
|
-
return
|
228
|
-
server_command_token == message_verifier.verify(client_command_token, purpose: controller.request.session&.id)
|
229
|
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
207
|
+
def raise_on_invalid_command?
|
208
|
+
return false unless TurboBoost::Commands.config.raise_on_invalid_command
|
209
|
+
return true if TurboBoost::Commands.config.raise_on_invalid_command == true
|
210
|
+
return true if TurboBoost::Commands.config.raise_on_invalid_command.to_s == Rails.env.to_s
|
230
211
|
false
|
231
212
|
end
|
232
213
|
|
233
|
-
def
|
214
|
+
def redirect?
|
234
215
|
return false if controller.request.get?
|
235
216
|
controller.request.accepts.include? Mime::Type.lookup_by_extension(:turbo_stream)
|
236
217
|
end
|
237
218
|
|
238
|
-
def
|
239
|
-
|
240
|
-
:ok
|
219
|
+
def supported_media_type?
|
220
|
+
SUPPORTED_MEDIA_TYPES[controller.request.format.to_s]
|
241
221
|
end
|
242
222
|
|
243
|
-
def
|
244
|
-
|
245
|
-
|
246
|
-
return :frame if body.match?(/<\/\s*turbo-frame/io)
|
247
|
-
return :stream if body.match?(/<\/\s*turbo-stream/io)
|
248
|
-
:unknown
|
223
|
+
def response_status
|
224
|
+
return :multiple_choices if redirect?
|
225
|
+
:ok
|
249
226
|
end
|
250
227
|
|
251
228
|
# Indicates if a TurboStream template exists for the current action.
|
@@ -255,14 +232,15 @@ class TurboBoost::Commands::Runner
|
|
255
232
|
controller.lookup_context.exists? controller.action_name, controller.lookup_context.prefixes, formats: [:turbo_boost, :turbo_stream]
|
256
233
|
end
|
257
234
|
|
258
|
-
|
235
|
+
# Commands support the following redering strategies on the client.
|
236
|
+
# 1. Replace: The entire page (head, body) is replaced with the new content via morph
|
237
|
+
# 2. Append: The new content is appended to the body
|
238
|
+
def client_render_strategy
|
259
239
|
# Use the replace strategy if the follow things are true:
|
260
240
|
#
|
261
241
|
# 1. The command was triggered by the WINDOW driver
|
262
242
|
# 2. After the command finishes, normal Rails mechanics resume (i.e. prevent_controller_action was not called)
|
263
243
|
# 3. There is NO TurboStream template for the current action (i.e. example.turbo_boost.erb, example.turbo_frame.erb)
|
264
|
-
#
|
265
|
-
# TODO: Revisit the "Replace" strategy after morph ships with Turbo 8
|
266
244
|
if command_params[:driver] == "window" && controller_action_allowed?
|
267
245
|
return "Replace" unless turbo_stream_template_exists?
|
268
246
|
end
|
@@ -270,112 +248,98 @@ class TurboBoost::Commands::Runner
|
|
270
248
|
"Append"
|
271
249
|
end
|
272
250
|
|
273
|
-
def
|
274
|
-
|
275
|
-
|
251
|
+
def respond_with_abort
|
252
|
+
Rails.logger.debug error.message
|
253
|
+
add_abort_event
|
254
|
+
add_error_alert if alert_on_abort?
|
276
255
|
end
|
277
256
|
|
278
|
-
def
|
257
|
+
def respond_with_error
|
279
258
|
Rails.logger.error error.message
|
280
|
-
|
281
|
-
|
259
|
+
add_error_event
|
260
|
+
add_error_alert if alert_on_error?
|
282
261
|
end
|
283
262
|
|
284
|
-
def
|
285
|
-
|
263
|
+
def respond_with_success
|
264
|
+
add_success_event
|
265
|
+
add_turbo_streams
|
286
266
|
end
|
287
267
|
|
288
|
-
def
|
289
|
-
|
290
|
-
|
291
|
-
client_state = command_state.to_json
|
292
|
-
|
293
|
-
# use the unmasked token for the signed (server) state
|
294
|
-
command_state[:command_token] = new_command_token
|
295
|
-
signed_state = command_state.to_sgid_param
|
268
|
+
def add_turbo_streams
|
269
|
+
command_instance.turbo_streams.each { |stream| add_content stream }
|
270
|
+
end
|
296
271
|
|
297
|
-
|
272
|
+
def add_state
|
273
|
+
add_content turbo_stream.invoke("TurboBoost.State.initialize", args: [state.to_json], camelize: false)
|
298
274
|
rescue => error
|
299
|
-
|
275
|
+
message = "TurboBoost::Commands::Runner failed to append the Command state to the response! #{error.message}"
|
276
|
+
Rails.logger.error message
|
300
277
|
end
|
301
278
|
|
302
|
-
def
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
279
|
+
def add_event(name, detail = {})
|
280
|
+
options = {
|
281
|
+
args: [name, {
|
282
|
+
bubbles: true,
|
283
|
+
cancelable: false,
|
284
|
+
detail: command_params.to_unsafe_hash.except(:state)
|
285
|
+
.merge(detail).deep_transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
|
286
|
+
}]
|
287
|
+
}
|
288
|
+
|
289
|
+
options[:selector] = "##{command_instance.element.id}" if command_instance&.element&.id
|
290
|
+
add_content turbo_stream.invoke(:dispatch_event, **options)
|
291
|
+
end
|
292
|
+
|
293
|
+
def add_success_event
|
294
|
+
return unless command_succeeded?
|
295
|
+
add_event "turbo-boost:command:success"
|
310
296
|
end
|
311
297
|
|
312
|
-
def
|
313
|
-
return unless
|
298
|
+
def add_abort_event
|
299
|
+
return unless error && command_aborted?
|
300
|
+
add_event "turbo-boost:command:abort", message: error.message
|
301
|
+
end
|
302
|
+
|
303
|
+
def add_error_event
|
304
|
+
return unless error
|
305
|
+
add_event "turbo-boost:command:server-error", message: error.message
|
306
|
+
end
|
307
|
+
|
308
|
+
def add_error_alert
|
309
|
+
return unless error
|
310
|
+
|
314
311
|
message = <<~MSG
|
315
|
-
#{message}
|
312
|
+
#{error.message}
|
313
|
+
|
314
|
+
---
|
316
315
|
|
317
|
-
See the HTTP header: `TurboBoost-Command
|
316
|
+
See the HTTP header: `TurboBoost-Command`
|
318
317
|
|
319
318
|
Also check the JavaScript console if `TurboBoost.Commands.logger.level` has been set.
|
320
319
|
|
321
320
|
Finally, check server logs for additional info.
|
322
321
|
MSG
|
323
|
-
append_to_response_body turbo_stream.invoke(:alert, args: [message])
|
324
|
-
end
|
325
|
-
|
326
|
-
def append_error_event_to_response_body(message)
|
327
|
-
args = ["turbo-boost:command:server-error", {bubbles: true, cancelable: false, detail: parsed_command_params.merge(error: message)}]
|
328
|
-
event = if command_instance&.element.try(:id).present?
|
329
|
-
turbo_stream.invoke :dispatch_event, args: args, selector: "##{command_instance.element.id}"
|
330
|
-
else
|
331
|
-
turbo_stream.invoke :dispatch_event, args: args
|
332
|
-
end
|
333
|
-
append_to_response_body event
|
334
|
-
end
|
335
322
|
|
336
|
-
|
337
|
-
@appended_content ||= {}
|
323
|
+
add_content turbo_stream.invoke(:alert, args: [message.strip], delay: 100)
|
338
324
|
end
|
339
325
|
|
340
|
-
def
|
341
|
-
return unless
|
342
|
-
|
343
|
-
return if sanitized_content.blank?
|
344
|
-
|
345
|
-
return if appended_content[sanitized_content]
|
346
|
-
appended_content[sanitized_content] = true
|
347
|
-
|
348
|
-
html = case response_type
|
349
|
-
when :body
|
350
|
-
match = controller.response.body.match(/<\/\s*body/io).to_s
|
351
|
-
controller.response.body.sub match, [sanitized_content, match].join
|
352
|
-
when :frame
|
353
|
-
match = controller.response.body.match(/<\/\s*turbo-frame/io).to_s
|
354
|
-
controller.response.body.sub match, [sanitized_content, match].join
|
355
|
-
else
|
356
|
-
[controller.response.body, sanitized_content].join
|
357
|
-
end
|
326
|
+
def add_header
|
327
|
+
return unless command_requested?
|
328
|
+
return unless supported_media_type?
|
358
329
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
330
|
+
command_status = "Abort" if command_aborted?
|
331
|
+
command_status = "Error" if command_errored?
|
332
|
+
command_status = "OK" if command_succeeded?
|
333
|
+
command_status ||= "Unknown"
|
363
334
|
|
364
|
-
|
365
|
-
|
366
|
-
return if controller.response.get_header key.to_s
|
367
|
-
controller.response.set_header key.to_s, value.to_s
|
335
|
+
values = [command_name, command_status, client_render_strategy]
|
336
|
+
responder.add_header RESPONSE_HEADER, values.join(", ")
|
368
337
|
end
|
369
338
|
|
370
|
-
def
|
339
|
+
def add_content(content)
|
371
340
|
return unless command_performed?
|
341
|
+
return unless supported_media_type?
|
372
342
|
|
373
|
-
|
374
|
-
status || "#{controller.response.status} #{TurboBoost::Commands::HTTP_STATUS_CODES[controller.response.status]}".delete(","),
|
375
|
-
rendering_strategy,
|
376
|
-
command_name
|
377
|
-
]
|
378
|
-
|
379
|
-
append_response_header RESPONSE_HEADER, values.join(", ")
|
343
|
+
responder.add_content content
|
380
344
|
end
|
381
345
|
end
|