render_turbo_stream 2.1.2 → 3.0.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.
@@ -0,0 +1,233 @@
1
+ module RenderTurboStream
2
+ module ControllerHelpers
3
+
4
+ def turbo_stream_save(
5
+ save_action,
6
+ redirect_on_success_to: nil, # does a regular redirect. Works if you are inside a turbo_frame and just want to redirect inside that frame BUT CANNOT STREAM OTHERS ACTIONS ON THE SAME RESPONSE https://github.com/rails/rails/issues/48056
7
+ turbo_redirect_on_success_to: nil, # does a full page redirect (break out of all frames by turbo_power redirect)
8
+ object: nil, # object used in save_action, example: @customer
9
+ id: nil, # if nil: no partial is rendered
10
+ partial: nil, # example: 'customers/form' default: "#{controller_path}/#{id}"
11
+ action: 'replace', # options: append, prepend
12
+ flash_action: action_name, # options: 'update', 'create', otherwise you have to declare a translation in config/locales like "activerecord.success.#{flash_action}" and "activerecord.errors.#{flash_action}"
13
+ locals: {}, # locals used by the partial
14
+ streams_on_success: [
15
+ {
16
+ id: nil,
17
+ partial: 'form',
18
+ locals: {},
19
+ action: 'replace'
20
+ }
21
+ ], # additional partials that should be rendered if save_action succeeded
22
+ streams_on_error: [
23
+ {
24
+ id: nil,
25
+ partial: 'form',
26
+ locals: {},
27
+ action: 'replace'
28
+ }
29
+ ], # additional partials that should be rendered if save_action failed
30
+ add_flash_alerts: [], #=> array of strings
31
+ add_flash_notices: [], #=> array of strings
32
+ flashes_on_success: [], #=> array of strings
33
+ flashes_on_error: [] #=> array of strings
34
+ )
35
+
36
+ unless object
37
+ object = eval("@#{controller_name.classify.underscore}")
38
+ end
39
+
40
+ #== Streams / Partials
41
+
42
+ streams = (id ? [id: id, partial: partial, locals: locals, action: action] : [])
43
+
44
+ if save_action
45
+ response.status = 200
46
+ streams_on_success.each do |s|
47
+ if s.is_a?(Array)
48
+ streams.push(s)
49
+ elsif s.is_a?(Hash) && s[:id].present?
50
+ streams.push(s)
51
+ end
52
+ end
53
+ else
54
+ response.status = :unprocessable_entity
55
+ streams += streams_on_error.select { |s| s[:id].present? }
56
+ end
57
+
58
+ #== FLASHES
59
+
60
+ model_name = object.model_name.human
61
+ if save_action
62
+ flash_notices = if flashes_on_success.present?
63
+ flashes_on_success
64
+ elsif flash_action.to_s == 'create'
65
+ str = I18n.t(
66
+ 'activerecord.success.successfully_created',
67
+ default: '%<model_name>s successfully created'
68
+ )
69
+ [format(str, model_name: model_name)]
70
+ elsif flash_action.to_s == 'update'
71
+ str = I18n.t(
72
+ 'activerecord.success.successfully_updated',
73
+ default: '%<model_name>s successfully updated'
74
+ )
75
+ [format(str, model_name: model_name)]
76
+ else
77
+ str = I18n.t(
78
+ "activerecord.success.#{flash_action}",
79
+ default: '%<model_name>s successfully updated'
80
+ )
81
+ [format(str, model_name: model_name)]
82
+ end
83
+ flash_alerts = []
84
+ else
85
+ flash_alerts = if flashes_on_error.present?
86
+ flashes_on_error
87
+ elsif flash_action.to_s == 'create'
88
+ str = I18n.t(
89
+ 'activerecord.errors.messages.could_not_create',
90
+ default: '%<model_name>s could not be created'
91
+ )
92
+ [format(str, model_name: model_name)]
93
+ elsif flash_action.to_s == 'update'
94
+ str = I18n.t(
95
+ 'activerecord.errors.messages.could_not_update',
96
+ default: '%<model_name>s could not be updated'
97
+ )
98
+ [format(str, model_name: model_name)]
99
+ else
100
+ str = I18n.t(
101
+ "activerecord.errors.messages.#{flash_action}",
102
+ default: '%<model_name>s could not be updated'
103
+ )
104
+ [format(str, model_name: model_name)]
105
+ end
106
+ flash_notices = []
107
+ end
108
+
109
+ flash_notices += add_flash_notices.to_a
110
+ flash_alerts += add_flash_alerts.to_a
111
+ _flash_id = Rails.configuration.x.render_turbo_stream.flash_id
112
+ flash_id = (_flash_id ? _flash_id : "ERROR, MISSING CONFIG => config.x.render_turbo_stream.flash_id")
113
+ flash_partial = Rails.configuration.x.render_turbo_stream.flash_partial
114
+ flash_action = Rails.configuration.x.render_turbo_stream.flash_action
115
+ flash_notices.each do |notice|
116
+ next unless notice.present?
117
+ # inside the flash partial has to be a loop that handles all theese flashes
118
+ flash_stream = {
119
+ id: flash_id,
120
+ partial: flash_partial,
121
+ action: flash_action,
122
+ locals: { success: true, message: notice }
123
+ }
124
+ streams.push(flash_stream)
125
+ end
126
+ flash_alerts.each do |alert|
127
+ next unless alert.present?
128
+ # inside the flash partial has to be a loop that handles all theese flashes
129
+ flash_stream = {
130
+ id: flash_id,
131
+ partial: flash_partial,
132
+ action: flash_action,
133
+ locals: { success: false, message: alert }
134
+ }
135
+ streams.push(flash_stream)
136
+ end
137
+
138
+ #== render
139
+
140
+ if save_action && turbo_redirect_on_success_to.present?
141
+ response.status = 302
142
+ flash[:alert] = flash_alerts
143
+ flash[:notice] = flash_notices
144
+ Rails.logger.debug(" • Set flash[:alert] => #{flash_alerts}") if flash_alerts.present?
145
+ Rails.logger.debug(" • Set flash[:notice] => #{flash_notices}") if flash_notices.present?
146
+ render_turbo_stream([
147
+ [
148
+ :redirect_to,
149
+ turbo_redirect_on_success_to
150
+ ]
151
+ ])
152
+ elsif save_action && redirect_on_success_to.present?
153
+ response.status = 302
154
+ if Rails.configuration.x.render_turbo_stream.use_cable_for_turbo_stream_save && helpers.user_signed_in?
155
+ streams.each do |s|
156
+ next unless s.is_a?(Hash)
157
+ Rails.logger.debug(" • Send by Cable => «#{s}»")
158
+ render_to_me(
159
+ s[:id],
160
+ flash_action,
161
+ partial: s[:partial],
162
+ locals: s[:locals]
163
+ )
164
+ end
165
+
166
+ else
167
+ flash[:alert] = flash_alerts
168
+ flash[:notice] = flash_notices
169
+ Rails.logger.debug(" • Set flash[:alert] => #{flash_alerts}") if flash_alerts.present?
170
+ Rails.logger.debug(" • Set flash[:notice] => #{flash_notices}") if flash_notices.present?
171
+ end
172
+ redirect_to redirect_on_success_to
173
+
174
+ else
175
+ flash.now[:alert] = flash_alerts
176
+ flash.now[:notice] = flash_notices
177
+ render_turbo_stream(streams)
178
+ end
179
+ end
180
+
181
+ def render_turbo_stream(array)
182
+
183
+ ary = []
184
+ array.each do |pr|
185
+ if !pr.present?
186
+ Rails.logger.warn " WARNING render_turbo_stream: Empty element inside attributes: «#{array}»"
187
+ elsif pr.is_a?(Hash)
188
+ props = pr.symbolize_keys
189
+ raise "missing attribute :id in #{props}" if !props[:id].present?
190
+ part = (props[:partial].present? ? props[:partial] : props[:id]).gsub('-', '_')
191
+ partial = (part.to_s.include?('/') ? part : [controller_path, part].join('/'))
192
+ r = props
193
+ r[:action] = (props[:action].present? ? props[:action] : :replace)
194
+ r[:partial] = partial
195
+ r[:type] = 'stream-partial'
196
+ ary.push(r)
197
+ elsif pr.is_a?(Array)
198
+ raise "array has to contain at least one element: #{pr}" unless pr.first.present?
199
+ ary.push(pr)
200
+ else
201
+ raise "ERROR render_turbo_stream invalid type: Only hash or array allowed"
202
+ end
203
+ end
204
+
205
+ if request.format.to_sym == :turbo_stream
206
+ render template: 'render_turbo_stream', locals: { streams: ary }, layout: false, formats: :turbo_stream
207
+ else
208
+ Rails.logger.debug(" • Render Turbo Stream RENDERING AS HTML because request.format => #{request.format}")
209
+ render template: 'render_turbo_stream_request_test', locals: { streams: ary }, layout: false, formats: :html
210
+ end
211
+
212
+ end
213
+
214
+ def stream_partial(
215
+ id,
216
+ partial: nil, #=> default: id
217
+ action: :replace,
218
+ locals: {}
219
+ )
220
+ render_turbo_stream(
221
+ [
222
+ {
223
+ id: id,
224
+ partial: partial,
225
+ action: action,
226
+ locals: locals
227
+ }
228
+ ]
229
+ )
230
+ end
231
+
232
+ end
233
+ end
@@ -0,0 +1,49 @@
1
+ module RenderTurboStream
2
+ module Test
3
+ module Request
4
+ module ChannelHelpers
5
+
6
+ def assert_channel_to_me(user, target_id, action: nil, count: 1, &block)
7
+
8
+ channel = "authenticated-user-#{user.id}"
9
+
10
+ libs = RenderTurboStream::Test::Request::Libs
11
+
12
+ r = libs.select_responses(response, channel, target_id, action, type: :channel, &block)
13
+
14
+ assert(
15
+ r[:responses].length == count,
16
+ libs.assert_error_message(count, r[:responses].length, r[:log])
17
+ )
18
+ end
19
+
20
+ def assert_channel_to_authenticated_group(group, target_id, action: nil, count: 1, &block)
21
+
22
+ channel = "authenticated-group-#{group}"
23
+
24
+ libs = RenderTurboStream::Test::Request::Libs
25
+
26
+ r = libs.select_responses(response, channel, target_id, action, type: :channel, &block)
27
+
28
+ assert(
29
+ r[:responses].length == count,
30
+ libs.assert_error_message(count, r[:responses].length, r[:log])
31
+ )
32
+ end
33
+
34
+ def assert_channel_to_all(target_id, action: nil, count: 1, &block)
35
+
36
+ libs = RenderTurboStream::Test::Request::Libs
37
+
38
+ r = libs.select_responses(response, 'all', target_id, action, type: :channel, &block)
39
+
40
+ assert(
41
+ r[:responses].length == count,
42
+ libs.assert_error_message(count, r[:responses].length, r[:log])
43
+ )
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,84 @@
1
+ module RenderTurboStream
2
+ module Test
3
+ module Request
4
+ module Helpers
5
+
6
+ def assert_once_targeted(*target_ids)
7
+ responses = RenderTurboStream::Test::Request::Libs.all_turbo_responses(response)
8
+ id_counts = {}
9
+ responses.each do |r|
10
+ if r['target'].is_a?(Array)
11
+ r['target'].each do |t|
12
+ id = (t[0] == '#' ? t[1..-1] : t)
13
+ id_counts[t.to_s] ||= 0
14
+ id_counts[t.to_s] += 1
15
+ end
16
+ else
17
+ id = (r['target'][0] == '#' ? r['target'][1..-1] : r['target'])
18
+ id_counts[id] ||= 0
19
+ id_counts[id] += 1
20
+ end
21
+ end
22
+
23
+ assert(id_counts.keys.length == target_ids.length, "You checked for #{target_ids.length} but #{id_counts.keys.length} targeted: #{id_counts.keys.join(', ')}")
24
+ target_ids.each do |id|
25
+ assert(id_counts.key?(id), "The id #{id} is not within the targeted ids: #{id_counts.keys.join(', ')}")
26
+ expect(id_counts[id]).to eq(1)
27
+ assert(id_counts[id] == 1, "The id #{id} is targeted #{id_counts[id]} #{'time'.pluralize(id_counts[id])}")
28
+ end
29
+ end
30
+
31
+ def all_turbo_responses
32
+ RenderTurboStream::Test::Request::Libs.all_turbo_responses(response)
33
+ end
34
+
35
+ def turbo_targets
36
+ RenderTurboStream::Test::Request::Libs.turbo_targets(response)
37
+ end
38
+
39
+ def assert_response(target_id, type = nil, action: nil, count: 1, &block)
40
+
41
+ libs = RenderTurboStream::Test::Request::Libs
42
+
43
+ r = libs.select_responses(response, false, target_id, action, type: type, &block)
44
+
45
+ assert(
46
+ r[:responses].length == count,
47
+ libs.assert_error_message(count, r[:responses].length, r[:log])
48
+ )
49
+ end
50
+
51
+ # Returns the path to which turbo_stream.redirect_to would be applied.
52
+ def turbo_redirect_to
53
+ resps = RenderTurboStream::Test::Request::Libs.all_turbo_responses(response)
54
+ url = nil
55
+ resps.each do |r|
56
+ if r['type'] == 'stream-command'
57
+ if r['array'].first == 'redirect_to'
58
+ if url
59
+ url = 'ERROR: REDIRECT CALLED MORE THAN ONCE'
60
+ else
61
+ url = r['array'].second
62
+ end
63
+ end
64
+ end
65
+ end
66
+ url
67
+ end
68
+
69
+ def assert_stream_response(target_id, action: nil, count: 1, &block)
70
+
71
+ libs = RenderTurboStream::Test::Request::Libs
72
+
73
+ r = libs.select_responses(response, false, target_id, action, type: :stream, &block)
74
+
75
+ assert(
76
+ r[:responses].length == count,
77
+ libs.assert_error_message(count, r[:responses].length, r[:log])
78
+ )
79
+ end
80
+
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,155 @@
1
+ module RenderTurboStream
2
+ module Test
3
+ module Request
4
+ class Libs
5
+ def self.all_turbo_responses(response)
6
+ e = Nokogiri::HTML(response.body).search('#rendered-partials').first
7
+ res = (e.present? ? JSON.parse(e.inner_html) : [])
8
+ response.headers.each do |k, v|
9
+ next unless k.match(/^test-turbo-channel-[\d]+$/)
10
+ h = JSON.parse(v)
11
+ res.push(h)
12
+ end
13
+ res
14
+ end
15
+
16
+ def self.select_responses(response, channel, target_id, action, type: nil, prohibit_multiple_to_same_target: [:replace], &block)
17
+ all = all_turbo_responses(response)
18
+ res = { log: [], responses: [] }
19
+
20
+ if !target_id.present?
21
+ res[:log].push("No target «##{target_id}» provided!")
22
+ elsif prohibit_multiple_to_same_target.present?
23
+ has_prohibited_action = false
24
+ c = all.select do |e|
25
+ if prohibit_multiple_to_same_target.include?(e['action']&.to_sym)
26
+ has_prohibited_action = true
27
+ end
28
+ e['target'] == "##{target_id}"
29
+ end.length
30
+ if c >= 2 && has_prohibited_action
31
+ res[:log].push("Target «##{target_id}» is targeted #{c} #{'time'.pluralize(c)} which is prohibited for the actions #{prohibit_multiple_to_same_target.join(', ')}")
32
+ return res
33
+ end
34
+ end
35
+
36
+ id_matched = false
37
+
38
+ responses = all.select do |a|
39
+ types = [a['type']]
40
+ if ['channel-partial', 'channel-template'].include?(a['type'])
41
+ types.push('channel')
42
+ elsif ['stream-partial'].include?(a['type'])
43
+ types.push('stream')
44
+ elsif ['stream-command'].include?(a['type'])
45
+ types.push('command')
46
+ types.push('stream')
47
+ elsif ['channel-command'].include?(a['type'])
48
+ types.push('command')
49
+ types.push('channel')
50
+ end
51
+
52
+ if target_id.present? && a['target'] == "##{target_id}"
53
+ id_matched = true
54
+ end
55
+
56
+ if target_id.present? && a['target'] != "##{target_id}"
57
+ false
58
+ elsif type.present? && !types.include?(type.to_s)
59
+ res[:log].push("Target «##{target_id}»: Types #{types.join(', ')} not matching with given type «#{type}»")
60
+ false
61
+ elsif channel.present? && a['channel'] != channel.to_s
62
+ res[:log].push("Target «##{target_id}»: Channel #{a['channel']} not matching with given channel «#{channel}»")
63
+ false
64
+ elsif action.present? && a['action'] != action.to_s
65
+ res[:log].push("Target «##{target_id}»: Action #{a['action']} not matching with given action «#{action}»")
66
+ false
67
+ elsif block_given?
68
+ if types.include?('command')
69
+ args = a['array'][1..-1]
70
+ if yield(args)
71
+ true
72
+ else
73
+ res[:log].push("Given block not matching Arguments => «#{args}»")
74
+ false
75
+ end
76
+ else
77
+ nok = Nokogiri::HTML(a['html_response'])
78
+ if yield(nok).present?
79
+ true
80
+ else
81
+ res[:log].push("Target «##{target_id}»: Given block not matching (checked by .present?)")
82
+ false
83
+ end
84
+ end
85
+ else
86
+ true
87
+ end
88
+ end
89
+
90
+ unless id_matched
91
+ res[:log].push("Target «##{target_id}» not found")
92
+ end
93
+
94
+ res[:responses] = responses
95
+ res
96
+ end
97
+
98
+ def self.turbo_targets(response)
99
+ responses = all_turbo_responses(response)
100
+ targets = []
101
+ responses.each do |r|
102
+ if r['target'].is_a?(Array)
103
+ r['target'].each do |t|
104
+ targets.push(t) unless targets.include?(t)
105
+ end
106
+ else
107
+ targets.push(r['target']) unless targets.include?(r['target'])
108
+ end
109
+ end
110
+
111
+ targets
112
+ end
113
+
114
+ def self.assert_error_message(expected, received, log)
115
+ [
116
+ "Expected #{expected} #{'response'.pluralize(expected)} but found #{received}.",
117
+ (log.present? ? "Messages => «#{log.join(', ')}»" : nil)
118
+ ].compact.join(' ')
119
+ end
120
+
121
+ def self.first_arg_is_html_id(method)
122
+ config = Rails.configuration.x.render_turbo_stream.first_argument_is_html_id
123
+ default = [
124
+ :graft,
125
+ :morph,
126
+ :inner_html,
127
+ :insert_adjacent_text,
128
+ :outer_html,
129
+ :text_content,
130
+ :add_css_class,
131
+ :remove_attribute,
132
+ :remove_css_class,
133
+ :set_attribute,
134
+ :set_dataset_attribute,
135
+ :set_property,
136
+ :set_style,
137
+ :set_styles,
138
+ :set_value,
139
+ :dispatch_event,
140
+ :reset_form,
141
+ :clear_storage,
142
+ :scroll_into_view,
143
+ :set_focus,
144
+ :turbo_frame_reload,
145
+ :turbo_frame_set_src,
146
+ :replace,
147
+ :append,
148
+ :prepend
149
+ ]
150
+ (config.present? ? config : default).map { |m| m.to_sym }.include?(method.to_sym)
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -1,3 +1,3 @@
1
1
  module RenderTurboStream
2
- VERSION = "2.1.2"
2
+ VERSION = "3.0.0"
3
3
  end