b4um_generators 0.2.11 → 0.2.14

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/README.de.md +127 -1
  4. data/README.md +126 -1
  5. data/lib/b4um_generators/version.rb +1 -1
  6. data/lib/generators/b4um/attachment/attachment_generator.rb +83 -23
  7. data/lib/generators/b4um/authentication/authentication_generator.rb +63 -0
  8. data/lib/generators/b4um/help/help_generator.rb +100 -3
  9. data/lib/generators/b4um/in_place/in_place_generator.rb +1231 -0
  10. data/lib/generators/b4um/in_place/templates/_flash.html.erb +15 -0
  11. data/lib/generators/b4um/in_place/templates/b4um_in_place_helper.rb +7 -0
  12. data/lib/generators/b4um/in_place/templates/display_attachment.html.erb.tt +154 -0
  13. data/lib/generators/b4um/in_place/templates/display_attachments.html.erb.tt +165 -0
  14. data/lib/generators/b4um/in_place/templates/display_boolean.html.erb.tt +18 -0
  15. data/lib/generators/b4um/in_place/templates/display_number.html.erb.tt +18 -0
  16. data/lib/generators/b4um/in_place/templates/display_radio.html.erb.tt +23 -0
  17. data/lib/generators/b4um/in_place/templates/display_rich_text.html.erb.tt +123 -0
  18. data/lib/generators/b4um/in_place/templates/display_select.html.erb.tt +23 -0
  19. data/lib/generators/b4um/in_place/templates/display_text.html.erb.tt +18 -0
  20. data/lib/generators/b4um/in_place/templates/edit_attachment.html.erb.tt +83 -0
  21. data/lib/generators/b4um/in_place/templates/edit_attachments.html.erb.tt +65 -0
  22. data/lib/generators/b4um/in_place/templates/edit_boolean.html.erb.tt +32 -0
  23. data/lib/generators/b4um/in_place/templates/edit_number.html.erb.tt +26 -0
  24. data/lib/generators/b4um/in_place/templates/edit_radio.html.erb.tt +32 -0
  25. data/lib/generators/b4um/in_place/templates/edit_rich_text.html.erb.tt +25 -0
  26. data/lib/generators/b4um/in_place/templates/edit_select.html.erb.tt +28 -0
  27. data/lib/generators/b4um/in_place/templates/edit_text.html.erb.tt +25 -0
  28. data/lib/generators/b4um/in_place/templates/in_place.css +935 -0
  29. data/lib/generators/b4um/in_place/templates/in_place_controller.js +115 -0
  30. data/lib/generators/b4um/install/install_generator.rb +18 -13
  31. data/lib/generators/b4um/install/templates/_flash.html.erb +15 -0
  32. data/lib/generators/b4um/install/templates/_footer.html.erb +6 -1
  33. data/lib/generators/b4um/install/templates/b4um/forms.css +127 -0
  34. data/lib/generators/b4um/install/templates/image_lightbox_controller.js +29 -0
  35. data/lib/generators/b4um/install/templates/rich_text_controller.js +22 -0
  36. data/lib/generators/b4um/install/templates/scroll_to_top_controller.js +12 -0
  37. data/lib/generators/b4um/scaffold/scaffold_generator.rb +106 -0
  38. data/lib/generators/b4um/scaffold/templates/_form.html.erb.tt +29 -0
  39. data/lib/generators/b4um/scaffold/templates/_resource.html.erb.tt +35 -0
  40. data/lib/generators/b4um/scaffold/templates/_table.html.erb.tt +31 -0
  41. data/lib/generators/b4um/trix/trix_generator.rb +104 -6
  42. metadata +25 -1
@@ -0,0 +1,1231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/named_base"
5
+
6
+ module B4um
7
+ module Generators
8
+ class InPlaceGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ argument :fields,
12
+ type: :array,
13
+ required: true,
14
+ banner: "FIELD [FIELD ...]"
15
+
16
+ class_option :select,
17
+ type: :string,
18
+ desc: "Renders a field as a select list. Example: status:Active=Aktiv,Inactive=Inaktiv"
19
+
20
+ class_option :radio,
21
+ type: :string,
22
+ desc: "Renders a field as radio buttons. Example: condition:new=Neu,used=Gebraucht"
23
+
24
+ class << self
25
+ def desc(_description = nil)
26
+ "Adds B4UM in-place editing to an existing resource."
27
+ end
28
+ end
29
+
30
+ def validate_model
31
+ return if File.exist?(model_path)
32
+
33
+ raise Thor::Error,
34
+ "Model not found: app/models/#{file_name}.rb"
35
+ end
36
+
37
+ def validate_fields
38
+ fields.each do |field|
39
+ next if available_field_names.include?(field)
40
+
41
+ raise Thor::Error,
42
+ "Field #{field} was not found on #{class_name}."
43
+ end
44
+ end
45
+
46
+ def validate_editor_options
47
+ configurations = {
48
+ select: select_configuration,
49
+ radio: radio_configuration
50
+ }
51
+
52
+ configurations.each do |option_name, configuration|
53
+ validate_editor_configuration(option_name, configuration)
54
+ end
55
+
56
+ validate_editor_configuration_conflict(configurations)
57
+ end
58
+
59
+ def ensure_flash_support
60
+ flash_path = "app/views/shared/_flash.html.erb"
61
+ full_flash_path = File.join(destination_root, flash_path)
62
+
63
+ unless File.exist?(full_flash_path)
64
+ copy_file(
65
+ "_flash.html.erb",
66
+ flash_path
67
+ )
68
+ end
69
+
70
+ ensure_flash_render_in_layout
71
+ end
72
+
73
+ def create_in_place_helper
74
+ helper_path = "app/helpers/b4um_in_place_helper.rb"
75
+ full_helper_path = File.join(destination_root, helper_path)
76
+
77
+ unless File.exist?(full_helper_path)
78
+ template(
79
+ "b4um_in_place_helper.rb",
80
+ helper_path
81
+ )
82
+ end
83
+
84
+ return unless authentication_installed?
85
+
86
+ helper = File.read(full_helper_path)
87
+
88
+ return if helper.include?(
89
+ "def b4um_in_place_editing_allowed?\n logged_in?"
90
+ )
91
+
92
+ gsub_file(
93
+ helper_path,
94
+ "def b4um_in_place_editing_allowed?\n true",
95
+ "def b4um_in_place_editing_allowed?\n logged_in?"
96
+ )
97
+ end
98
+
99
+ def create_in_place_controller
100
+ copy_file(
101
+ "in_place_controller.js",
102
+ "app/javascript/controllers/in_place_controller.js"
103
+ )
104
+ end
105
+
106
+ def create_in_place_styles
107
+ copy_file(
108
+ "in_place.css",
109
+ "app/assets/stylesheets/b4um/in_place.css"
110
+ )
111
+ end
112
+
113
+ def add_in_place_routes
114
+ fields.each do |field|
115
+ add_in_place_route(field)
116
+ end
117
+ end
118
+
119
+ def add_in_place_fields_constant
120
+ controller_path = File.join(
121
+ "app/controllers",
122
+ "#{plural_table_name}_controller.rb"
123
+ )
124
+
125
+ full_controller_path = File.join(
126
+ destination_root,
127
+ controller_path
128
+ )
129
+
130
+ controller = File.read(full_controller_path)
131
+
132
+ if controller.include?("IN_PLACE_FIELDS = %w[")
133
+ add_fields_to_existing_constant(controller_path, controller)
134
+ else
135
+ create_in_place_fields_constant(controller_path)
136
+ end
137
+ end
138
+
139
+ def add_edit_actions
140
+ fields.each do |field|
141
+ add_edit_action(field)
142
+ end
143
+ end
144
+
145
+ def add_edit_actions_to_set_product
146
+ fields.each do |field|
147
+ add_edit_action_to_set_product(field)
148
+ end
149
+ end
150
+
151
+ def create_edit_views
152
+ fields.each do |field|
153
+ create_edit_view(field)
154
+ end
155
+ end
156
+
157
+ def create_display_partials
158
+ fields.each do |field|
159
+ create_display_partial(field)
160
+ end
161
+ end
162
+
163
+ def integrate_display_partials
164
+ fields.each do |field|
165
+ integrate_display_partial(field)
166
+ end
167
+ end
168
+
169
+ def add_in_place_params
170
+ fields.each do |field|
171
+ add_in_place_params_for(field)
172
+ end
173
+ end
174
+
175
+ def add_update_integration
176
+ add_in_place_update_integration
177
+ end
178
+
179
+ def report_fields
180
+ fields.each do |field|
181
+ say_status(
182
+ :in_place,
183
+ "#{class_name}##{field} (#{field_type(field)})",
184
+ :green
185
+ )
186
+ end
187
+ end
188
+
189
+ def protect_in_place_actions
190
+ return unless authentication_installed?
191
+
192
+ controller_path = File.join(
193
+ "app/controllers",
194
+ "#{plural_table_name}_controller.rb"
195
+ )
196
+
197
+ full_controller_path = File.join(
198
+ destination_root,
199
+ controller_path
200
+ )
201
+
202
+ controller = File.read(full_controller_path)
203
+
204
+ callback =
205
+ "before_action :require_login, except: [:index, :show]"
206
+
207
+ return if controller.include?(callback)
208
+
209
+ inject_into_file(
210
+ controller_path,
211
+ after: /^class .*Controller < ApplicationController\s*$/
212
+ ) do
213
+ "\n #{callback}"
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ def ensure_flash_render_in_layout
220
+ layout_path = "app/views/layouts/application.html.erb"
221
+ full_layout_path = File.join(destination_root, layout_path)
222
+
223
+ return unless File.exist?(full_layout_path)
224
+
225
+ layout = File.read(full_layout_path)
226
+
227
+ return if layout.include?('render "shared/flash"')
228
+
229
+ flash_pattern = /
230
+ \s*<%\s+flash\.each\s+do\s+\|type,\s*message\|\s*%>
231
+ .*?
232
+ <%\s+end\s+%>
233
+ /mx
234
+
235
+ return unless layout.match?(flash_pattern)
236
+
237
+ gsub_file(
238
+ layout_path,
239
+ flash_pattern,
240
+ "\n <%= render \"shared/flash\" %>"
241
+ )
242
+ end
243
+
244
+ def validate_editor_configuration(option_name, configuration)
245
+ return unless options[option_name].present?
246
+
247
+ unless configuration
248
+ raise Thor::Error,
249
+ "Invalid --#{option_name} configuration. " \
250
+ "Expected FIELD:VALUE[,VALUE...]"
251
+ end
252
+
253
+ return if fields.include?(configuration[:field])
254
+
255
+ raise Thor::Error,
256
+ "--#{option_name} field #{configuration[:field]} " \
257
+ "must be one of the generated fields: #{fields.join(", ")}"
258
+ end
259
+
260
+ def validate_editor_configuration_conflict(configurations)
261
+ select = configurations[:select]
262
+ radio = configurations[:radio]
263
+
264
+ return unless select && radio
265
+ return unless select[:field] == radio[:field]
266
+
267
+ raise Thor::Error,
268
+ "Field #{select[:field]} cannot use both --select and --radio"
269
+ end
270
+
271
+ def model_path
272
+ File.join(
273
+ destination_root,
274
+ "app/models",
275
+ "#{file_name}.rb"
276
+ )
277
+ end
278
+
279
+ def model_class
280
+ class_name.constantize
281
+ end
282
+
283
+ def database_columns
284
+ model_class.columns.index_by(&:name)
285
+ end
286
+
287
+ def rich_text_fields
288
+ model_class.reflect_on_all_associations(:has_one)
289
+ .filter_map do |association|
290
+ association.name.to_s.delete_prefix("rich_text_") if
291
+ association.name.to_s.start_with?("rich_text_")
292
+ end
293
+ end
294
+
295
+ def single_attachment_fields
296
+ model_class.reflect_on_all_attachments
297
+ .select { |reflection| reflection.macro == :has_one_attached }
298
+ .map { |reflection| reflection.name.to_s }
299
+ end
300
+
301
+ def multiple_attachment_fields
302
+ model_class.reflect_on_all_attachments
303
+ .select { |reflection| reflection.macro == :has_many_attached }
304
+ .map { |reflection| reflection.name.to_s }
305
+ end
306
+
307
+ def available_field_names
308
+ (
309
+ database_columns.keys +
310
+ rich_text_fields +
311
+ single_attachment_fields +
312
+ multiple_attachment_fields
313
+ ).uniq
314
+ end
315
+
316
+ def field_type(field)
317
+ return :rich_text if rich_text_fields.include?(field)
318
+ return :attachment if single_attachment_fields.include?(field)
319
+ return :attachments if multiple_attachment_fields.include?(field)
320
+
321
+ database_columns.fetch(field).type
322
+ end
323
+
324
+ def select_configuration
325
+ editor_configuration(:select)
326
+ end
327
+
328
+ def radio_configuration
329
+ editor_configuration(:radio)
330
+ end
331
+
332
+ def editor_configuration(option_name)
333
+ value = options[option_name]
334
+ return unless value.present?
335
+
336
+ field, values = value.split(":", 2)
337
+
338
+ return unless field.present? && values.present?
339
+
340
+ choices = editor_choices(values)
341
+ return if choices.empty?
342
+
343
+ {
344
+ field: field,
345
+ values: choices
346
+ }
347
+ end
348
+
349
+ def editor_choices(values)
350
+ values.split(",").filter_map do |choice|
351
+ value, label = choice.strip.split("=", 2)
352
+
353
+ next if value.blank?
354
+
355
+ {
356
+ value: value,
357
+ label: label.presence || value
358
+ }
359
+ end
360
+ end
361
+
362
+ def select_field?(field)
363
+ configuration = select_configuration
364
+
365
+ configuration &&
366
+ configuration[:field] == field &&
367
+ configuration[:values].any?
368
+ end
369
+
370
+ def radio_field?(field)
371
+ configuration = radio_configuration
372
+
373
+ configuration &&
374
+ configuration[:field] == field &&
375
+ configuration[:values].any?
376
+ end
377
+
378
+ def editor_type(field)
379
+ configured_editor_type(field) || default_editor_type(field)
380
+ end
381
+
382
+ def configured_editor_type(field)
383
+ return :radio if radio_field?(field)
384
+ return :select if select_field?(field)
385
+
386
+ nil
387
+ end
388
+
389
+ def default_editor_type(field)
390
+ {
391
+ rich_text: :rich_text,
392
+ attachment: :attachment,
393
+ attachments: :attachments,
394
+ boolean: :boolean,
395
+ integer: :number,
396
+ float: :number,
397
+ decimal: :number
398
+ }.fetch(field_type(field), :text)
399
+ end
400
+
401
+ def authentication_installed?
402
+ application_controller = File.join(
403
+ destination_root,
404
+ "app/controllers/application_controller.rb"
405
+ )
406
+
407
+ return false unless File.exist?(application_controller)
408
+
409
+ File.read(application_controller).include?(
410
+ "helper_method :current_"
411
+ ) &&
412
+ File.read(application_controller).include?(
413
+ ":logged_in?"
414
+ )
415
+ end
416
+
417
+ def add_in_place_route(field)
418
+ routes_path = "config/routes.rb"
419
+ full_routes_path = File.join(destination_root, routes_path)
420
+ routes = File.read(full_routes_path)
421
+ route = "get :edit_#{field}"
422
+
423
+ return if route_exists_for_resource?(routes, route)
424
+
425
+ add_route_to_resources(routes_path, routes, route)
426
+ end
427
+
428
+ def route_exists_for_resource?(routes, route)
429
+ member_match = routes.match(resources_with_member_pattern)
430
+
431
+ if member_match
432
+ start_index = member_match.begin(0)
433
+ indentation = member_match[1]
434
+
435
+ resource_block = routes[
436
+ start_index..
437
+ ][
438
+ /\A.*?^#{Regexp.escape(indentation)}end\s*$/m,
439
+ 0
440
+ ]
441
+
442
+ return resource_block&.include?(route)
443
+ end
444
+
445
+ false
446
+ end
447
+
448
+ def add_route_to_resources(routes_path, routes, route)
449
+ if routes.match?(resources_with_member_pattern)
450
+ add_route_to_member_block(routes_path, route)
451
+ elsif routes.match?(resources_block_pattern)
452
+ add_member_block(routes_path, route)
453
+ elsif routes.match?(plain_resources_pattern)
454
+ convert_plain_resources(routes_path, route)
455
+ else
456
+ raise Thor::Error,
457
+ "Could not find resources :#{plural_table_name} in config/routes.rb"
458
+ end
459
+ end
460
+
461
+ def resources_with_member_pattern
462
+ /
463
+ ^(\s*)resources\ :#{Regexp.escape(plural_table_name)}\ do\n
464
+ \1\ \ member\ do\n
465
+ /x
466
+ end
467
+
468
+ def resources_block_pattern
469
+ /^(\s*)resources :#{Regexp.escape(plural_table_name)} do\n/
470
+ end
471
+
472
+ def plain_resources_pattern
473
+ /^(\s*)resources :#{Regexp.escape(plural_table_name)}\s*$/
474
+ end
475
+
476
+ def add_route_to_member_block(routes_path, route)
477
+ gsub_file(
478
+ routes_path,
479
+ resources_with_member_pattern
480
+ ) do |match|
481
+ indentation = match[/\A\s*/]
482
+
483
+ "#{match}#{indentation} #{route}\n"
484
+ end
485
+ end
486
+
487
+ def add_member_block(routes_path, route)
488
+ gsub_file(
489
+ routes_path,
490
+ resources_block_pattern
491
+ ) do |match|
492
+ indentation = match[/\A\s*/]
493
+
494
+ [
495
+ match.chomp,
496
+ "#{indentation} member do",
497
+ "#{indentation} #{route}",
498
+ "#{indentation} end",
499
+ ""
500
+ ].join("\n")
501
+ end
502
+ end
503
+
504
+ def convert_plain_resources(routes_path, route)
505
+ gsub_file(
506
+ routes_path,
507
+ plain_resources_pattern
508
+ ) do |match|
509
+ indentation = match[/\A\s*/]
510
+
511
+ [
512
+ "#{indentation}resources :#{plural_table_name} do",
513
+ "#{indentation} member do",
514
+ "#{indentation} #{route}",
515
+ "#{indentation} end",
516
+ "#{indentation}end"
517
+ ].join("\n")
518
+ end
519
+ end
520
+
521
+ def create_in_place_fields_constant(controller_path)
522
+ lines = fields.map do |field|
523
+ " #{field}"
524
+ end.join("\n")
525
+
526
+ inject_into_file(
527
+ controller_path,
528
+ after: /^class .*Controller < ApplicationController\s*$/
529
+ ) do
530
+ <<~RUBY
531
+
532
+ IN_PLACE_FIELDS = %w[
533
+ #{lines}
534
+ ].freeze
535
+ RUBY
536
+ end
537
+ end
538
+
539
+ def add_fields_to_existing_constant(controller_path, controller)
540
+ fields.each do |field|
541
+ next if in_place_field_defined?(controller, field)
542
+
543
+ inject_into_file(
544
+ controller_path,
545
+ before: /^\s*\]\.freeze\s*$/
546
+ ) do
547
+ " #{field}\n"
548
+ end
549
+
550
+ controller = File.read(
551
+ File.join(destination_root, controller_path)
552
+ )
553
+ end
554
+ end
555
+
556
+ def in_place_field_defined?(controller, field)
557
+ constant = controller[
558
+ /IN_PLACE_FIELDS = %w\[(.*?)\]\.freeze/m,
559
+ 1
560
+ ]
561
+
562
+ return false unless constant
563
+
564
+ constant.split.include?(field)
565
+ end
566
+
567
+ def add_edit_action(field)
568
+ controller_path = File.join(
569
+ "app/controllers",
570
+ "#{plural_table_name}_controller.rb"
571
+ )
572
+
573
+ full_controller_path = File.join(
574
+ destination_root,
575
+ controller_path
576
+ )
577
+
578
+ controller = File.read(full_controller_path)
579
+
580
+ return if controller.match?(
581
+ /^\s*def edit_#{Regexp.escape(field)}\s*$/
582
+ )
583
+
584
+ inject_into_file(
585
+ controller_path,
586
+ before: /^\s*def create\s*$/
587
+ ) do
588
+ " def edit_#{field}\n end\n\n"
589
+ end
590
+ end
591
+
592
+ def add_edit_action_to_set_product(field)
593
+ controller_path = File.join(
594
+ "app/controllers",
595
+ "#{plural_table_name}_controller.rb"
596
+ )
597
+
598
+ full_controller_path = File.join(
599
+ destination_root,
600
+ controller_path
601
+ )
602
+
603
+ controller = File.read(full_controller_path)
604
+ action = "edit_#{field}"
605
+
606
+ callback = controller[
607
+ /before_action :set_\w+, only: %i\[(.*?)\]/m,
608
+ 0
609
+ ]
610
+
611
+ unless callback
612
+ raise Thor::Error,
613
+ "Could not find the set_#{singular_table_name} before_action in #{controller_path}"
614
+ end
615
+
616
+ return if callback.match?(/\b#{Regexp.escape(action)}\b/)
617
+
618
+ unless callback.match?(/\bupdate\b/)
619
+ raise Thor::Error,
620
+ "Could not find update in the set_#{singular_table_name} before_action"
621
+ end
622
+
623
+ updated_callback = callback.sub(
624
+ /\bupdate\b/,
625
+ "#{action} update"
626
+ )
627
+
628
+ gsub_file(
629
+ controller_path,
630
+ callback,
631
+ updated_callback
632
+ )
633
+ end
634
+
635
+ def add_in_place_update_integration
636
+ controller_path = File.join(
637
+ "app/controllers",
638
+ "#{plural_table_name}_controller.rb"
639
+ )
640
+
641
+ full_controller_path = File.join(
642
+ destination_root,
643
+ controller_path
644
+ )
645
+
646
+ controller = File.read(full_controller_path)
647
+
648
+ original_update_method = controller[
649
+ /^\s*def update\s*$.*?(?=^\s*def \w+)/m,
650
+ 0
651
+ ]
652
+
653
+ unless original_update_method
654
+ raise Thor::Error,
655
+ "Could not find update action in #{controller_path}"
656
+ end
657
+
658
+ update_method = original_update_method
659
+ attachment_blocks = attachment_update_blocks_for_fields
660
+
661
+ update_method = integrate_attachment_updates(
662
+ update_method,
663
+ attachment_blocks
664
+ )
665
+
666
+ update_method = integrate_attachment_reloads(update_method)
667
+ update_method = integrate_turbo_streams(update_method, controller_path)
668
+
669
+ return if update_method == original_update_method
670
+
671
+ gsub_file(
672
+ controller_path,
673
+ original_update_method,
674
+ update_method
675
+ )
676
+ end
677
+
678
+ def integrate_attachment_updates(update_method, attachment_blocks)
679
+ update_method = integrate_before_attachment_updates(
680
+ update_method,
681
+ attachment_blocks[:before_update]
682
+ )
683
+
684
+ integrate_after_attachment_updates(
685
+ update_method,
686
+ attachment_blocks[:after_update]
687
+ )
688
+ end
689
+
690
+ def integrate_before_attachment_updates(update_method, lines)
691
+ lines = lines.reject { |line| update_method.include?(line) }
692
+
693
+ return update_method if lines.empty?
694
+
695
+ original_update_call =
696
+ "if @#{singular_table_name}.update(#{singular_table_name}_params)"
697
+
698
+ in_place_update_call =
699
+ "if @#{singular_table_name}.update(update_params)"
700
+
701
+ update_call =
702
+ if update_method.include?(in_place_update_call)
703
+ in_place_update_call
704
+ elsif update_method.include?(original_update_call)
705
+ original_update_call
706
+ end
707
+
708
+ unless update_call
709
+ raise Thor::Error,
710
+ "Could not find update call in update action"
711
+ end
712
+
713
+ before_update_block = lines.join("\n").indent(4)
714
+
715
+ replacement = <<~RUBY.chomp
716
+ #{before_update_block}
717
+ if @#{singular_table_name}.update(update_params)
718
+ RUBY
719
+
720
+ update_method.sub(update_call, replacement)
721
+ end
722
+
723
+ def integrate_after_attachment_updates(update_method, blocks)
724
+ blocks = blocks.reject { |block| update_method.include?(block) }
725
+
726
+ return update_method if blocks.empty?
727
+
728
+ success_line = update_method[
729
+ /^\s*if @#{Regexp.escape(singular_table_name)}\.update\(update_params\)\s*$/,
730
+ 0
731
+ ]
732
+
733
+ unless success_line
734
+ raise Thor::Error,
735
+ "Could not find attachment update success line"
736
+ end
737
+
738
+ after_update_block = blocks.join("\n\n").indent(8)
739
+
740
+ update_method.sub(
741
+ success_line,
742
+ "#{success_line}\n#{after_update_block}"
743
+ )
744
+ end
745
+
746
+ def integrate_attachment_reloads(update_method)
747
+ attachment_reload_block_for_fields.lines.each do |reload_line|
748
+ reload_line = reload_line.strip
749
+
750
+ next if reload_line.empty?
751
+ next if update_method.include?(reload_line)
752
+
753
+ turbo_success_anchor =
754
+ update_method[
755
+ /^\s*format\.turbo_stream do\s*$.*?flash\.now\[:notice\]/m,
756
+ 0
757
+ ]
758
+
759
+ next unless turbo_success_anchor
760
+
761
+ first_line = turbo_success_anchor.lines.first
762
+
763
+ update_method = update_method.sub(
764
+ first_line,
765
+ "#{first_line} #{reload_line}\n\n"
766
+ )
767
+ end
768
+
769
+ update_method
770
+ end
771
+
772
+ def upgrade_flash_turbo_stream(update_method)
773
+ update_method.sub(
774
+ /turbo_stream\.update\(\s*"flash-messages",/,
775
+ 'turbo_stream.replace("flash-messages",'
776
+ )
777
+ end
778
+
779
+ def integrate_turbo_streams(update_method, controller_path)
780
+ update_method = upgrade_flash_turbo_stream(update_method)
781
+
782
+ return update_method if update_method.include?(
783
+ "IN_PLACE_FIELDS.include?(params[:in_place_field])"
784
+ )
785
+
786
+ success_anchor = update_method[
787
+ /^\s*format\.html \{ redirect_to .*successfully updated.*$/,
788
+ 0
789
+ ]
790
+
791
+ unless success_anchor
792
+ raise Thor::Error,
793
+ "Could not find update success response in #{controller_path}"
794
+ end
795
+
796
+ error_anchor = update_method[
797
+ /^\s*format\.html \{ render :edit, status: :unprocessable_content \}$/,
798
+ 0
799
+ ]
800
+
801
+ unless error_anchor
802
+ raise Thor::Error,
803
+ "Could not find update error response in #{controller_path}"
804
+ end
805
+
806
+ attachment_reload_block = attachment_reload_block_for_fields
807
+
808
+ turbo_stream_block = <<~RUBY.indent(8)
809
+ #{attachment_reload_block}format.turbo_stream do
810
+ flash.now[:notice] = "#{class_name} was successfully updated."
811
+
812
+ streams = [
813
+ turbo_stream.replace(
814
+ "flash-messages",
815
+ partial: "shared/flash"
816
+ )
817
+ ]
818
+
819
+ if IN_PLACE_FIELDS.include?(params[:in_place_field])
820
+ streams << turbo_stream.replace(
821
+ helpers.dom_id(
822
+ @#{singular_table_name},
823
+ params[:in_place_field]
824
+ ),
825
+ partial: "#{plural_table_name}/\#{params[:in_place_field]}",
826
+ locals: { #{singular_table_name}: @#{singular_table_name} }
827
+ )
828
+ end
829
+
830
+ render turbo_stream: streams
831
+ end
832
+
833
+ RUBY
834
+
835
+ error_turbo_stream_block = <<~RUBY.indent(8)
836
+ format.turbo_stream do
837
+ if IN_PLACE_FIELDS.include?(params[:in_place_field])
838
+ render turbo_stream: turbo_stream.replace(
839
+ helpers.dom_id(
840
+ @#{singular_table_name},
841
+ params[:in_place_field]
842
+ ),
843
+ template: "#{plural_table_name}/edit_\#{params[:in_place_field]}"
844
+ ),
845
+ status: :unprocessable_content
846
+ else
847
+ head :unprocessable_content
848
+ end
849
+ end
850
+
851
+ RUBY
852
+
853
+ update_method = update_method.sub(
854
+ success_anchor,
855
+ "#{turbo_stream_block}#{success_anchor}"
856
+ )
857
+
858
+ update_method.sub(
859
+ error_anchor,
860
+ "#{error_turbo_stream_block}#{error_anchor}"
861
+ )
862
+ end
863
+
864
+ def add_in_place_params_for(field)
865
+ case field_type(field)
866
+ when :attachment
867
+ add_single_attachment_params(field)
868
+ when :attachments
869
+ add_multiple_attachment_params(field)
870
+ else
871
+ add_regular_field_param(field)
872
+ end
873
+ end
874
+
875
+ def add_regular_field_param(field)
876
+ controller_path = File.join(
877
+ "app/controllers",
878
+ "#{plural_table_name}_controller.rb"
879
+ )
880
+
881
+ full_controller_path = File.join(
882
+ destination_root,
883
+ controller_path
884
+ )
885
+
886
+ controller = File.read(full_controller_path)
887
+
888
+ params_method = controller[
889
+ /def #{Regexp.escape(singular_table_name)}_params.*?^\s*end/m,
890
+ 0
891
+ ]
892
+
893
+ unless params_method
894
+ raise Thor::Error,
895
+ "Could not find #{singular_table_name}_params in #{controller_path}"
896
+ end
897
+
898
+ parameter = ":#{field}"
899
+
900
+ return if params_method.include?(parameter)
901
+
902
+ updated_method =
903
+ if params_method.match?(/\w+:\s*\[\]/)
904
+ params_method.sub(
905
+ /(?=\w+:\s*\[\])/,
906
+ "#{parameter}, "
907
+ )
908
+ else
909
+ params_method.sub(
910
+ /\]\s*\)\s*$/,
911
+ ", #{parameter}])"
912
+ )
913
+ end
914
+
915
+ gsub_file(
916
+ controller_path,
917
+ params_method,
918
+ updated_method
919
+ )
920
+ end
921
+
922
+ def add_single_attachment_params(field)
923
+ controller_path = File.join(
924
+ "app/controllers",
925
+ "#{plural_table_name}_controller.rb"
926
+ )
927
+
928
+ full_controller_path = File.join(
929
+ destination_root,
930
+ controller_path
931
+ )
932
+
933
+ controller = File.read(full_controller_path)
934
+
935
+ params_method = controller[
936
+ /def #{Regexp.escape(singular_table_name)}_params.*?^\s*end/m,
937
+ 0
938
+ ]
939
+
940
+ unless params_method
941
+ raise Thor::Error,
942
+ "Could not find #{singular_table_name}_params in #{controller_path}"
943
+ end
944
+
945
+ parameters = [
946
+ ":#{field}",
947
+ ":remove_#{field}"
948
+ ]
949
+
950
+ parameters.each do |parameter|
951
+ next if params_method.include?(parameter)
952
+
953
+ updated_method =
954
+ if params_method.match?(/\w+:\s*\[\]/)
955
+ params_method.sub(
956
+ /(?=\w+:\s*\[\])/,
957
+ "#{parameter}, "
958
+ )
959
+ else
960
+ params_method.sub(
961
+ /\]\s*\)\s*$/,
962
+ ", #{parameter}])"
963
+ )
964
+ end
965
+
966
+ gsub_file(
967
+ controller_path,
968
+ params_method,
969
+ updated_method
970
+ )
971
+
972
+ params_method = updated_method
973
+ end
974
+ end
975
+
976
+ def add_multiple_attachment_params(field)
977
+ controller_path = File.join(
978
+ "app/controllers",
979
+ "#{plural_table_name}_controller.rb"
980
+ )
981
+
982
+ full_controller_path = File.join(
983
+ destination_root,
984
+ controller_path
985
+ )
986
+
987
+ controller = File.read(full_controller_path)
988
+
989
+ params_method = controller[
990
+ /def #{Regexp.escape(singular_table_name)}_params.*?^\s*end/m,
991
+ 0
992
+ ]
993
+
994
+ unless params_method
995
+ raise Thor::Error,
996
+ "Could not find #{singular_table_name}_params in #{controller_path}"
997
+ end
998
+
999
+ parameters = [
1000
+ "#{field}: []",
1001
+ "remove_#{field}_ids: []"
1002
+ ]
1003
+
1004
+ parameters.each do |parameter|
1005
+ next if params_method.include?(parameter)
1006
+
1007
+ updated_method = params_method.sub(
1008
+ /\s*\]\s*\)\s*$/,
1009
+ ", #{parameter} ])"
1010
+ )
1011
+
1012
+ gsub_file(
1013
+ controller_path,
1014
+ params_method,
1015
+ updated_method
1016
+ )
1017
+
1018
+ params_method = updated_method
1019
+ end
1020
+ end
1021
+
1022
+ def create_edit_view(field)
1023
+ template_name = edit_template_for(field)
1024
+
1025
+ return unless template_name
1026
+
1027
+ destination = File.join(
1028
+ "app/views",
1029
+ plural_table_name,
1030
+ "edit_#{field}.html.erb"
1031
+ )
1032
+
1033
+ return if File.exist?(
1034
+ File.join(destination_root, destination)
1035
+ )
1036
+
1037
+ @field = field
1038
+
1039
+ template(
1040
+ template_name,
1041
+ destination
1042
+ )
1043
+ ensure
1044
+ @field = nil
1045
+ end
1046
+
1047
+ def edit_template_for(field)
1048
+ {
1049
+ text: "edit_text.html.erb.tt",
1050
+ rich_text: "edit_rich_text.html.erb.tt",
1051
+ boolean: "edit_boolean.html.erb.tt",
1052
+ number: "edit_number.html.erb.tt",
1053
+ select: "edit_select.html.erb.tt",
1054
+ radio: "edit_radio.html.erb.tt",
1055
+ attachment: "edit_attachment.html.erb.tt",
1056
+ attachments: "edit_attachments.html.erb.tt"
1057
+ }[editor_type(field)]
1058
+ end
1059
+
1060
+ def resource_field_range(resource, field)
1061
+ label = "#{field.humanize}:"
1062
+
1063
+ label_position = resource.index(
1064
+ %(<strong class="resource-label">#{label}</strong>)
1065
+ )
1066
+
1067
+ return unless label_position
1068
+
1069
+ field_start = resource.rindex(
1070
+ '<div class="resource-field">',
1071
+ label_position
1072
+ )
1073
+
1074
+ return unless field_start
1075
+
1076
+ position = field_start
1077
+ depth = 0
1078
+
1079
+ while (match = resource.match(%r{<div\b[^>]*>|</div>}, position))
1080
+ if match[0].start_with?("<div")
1081
+ depth += 1
1082
+ else
1083
+ depth -= 1
1084
+
1085
+ return field_start...match.end(0) if depth.zero?
1086
+ end
1087
+
1088
+ position = match.end(0)
1089
+ end
1090
+
1091
+ nil
1092
+ end
1093
+
1094
+ def integrate_display_partial(field)
1095
+ resource_path = File.join(
1096
+ "app/views",
1097
+ plural_table_name,
1098
+ "_#{singular_table_name}.html.erb"
1099
+ )
1100
+
1101
+ full_resource_path = File.join(
1102
+ destination_root,
1103
+ resource_path
1104
+ )
1105
+
1106
+ return unless File.exist?(full_resource_path)
1107
+
1108
+ resource = File.read(full_resource_path)
1109
+
1110
+ render_statement =
1111
+ "<%= render \"#{plural_table_name}/#{field}\", " \
1112
+ "#{singular_table_name}: #{singular_table_name}, " \
1113
+ "compact: local_assigns[:compact] %>"
1114
+
1115
+ return if resource.include?(render_statement)
1116
+
1117
+ field_range = resource_field_range(resource, field)
1118
+
1119
+ unless field_range
1120
+ say_status(
1121
+ :warning,
1122
+ "Could not integrate in-place field #{field} into #{resource_path}",
1123
+ :yellow
1124
+ )
1125
+ return
1126
+ end
1127
+
1128
+ resource[field_range] = render_statement
1129
+
1130
+ File.write(
1131
+ full_resource_path,
1132
+ resource
1133
+ )
1134
+ end
1135
+
1136
+ def create_display_partial(field)
1137
+ template_name = display_template_for(field)
1138
+
1139
+ return unless template_name
1140
+
1141
+ destination = File.join(
1142
+ "app/views",
1143
+ plural_table_name,
1144
+ "_#{field}.html.erb"
1145
+ )
1146
+
1147
+ return if File.exist?(
1148
+ File.join(destination_root, destination)
1149
+ )
1150
+
1151
+ @field = field
1152
+
1153
+ template(
1154
+ template_name,
1155
+ destination
1156
+ )
1157
+ ensure
1158
+ @field = nil
1159
+ end
1160
+
1161
+ def display_template_for(field)
1162
+ {
1163
+ text: "display_text.html.erb.tt",
1164
+ rich_text: "display_rich_text.html.erb.tt",
1165
+ boolean: "display_boolean.html.erb.tt",
1166
+ number: "display_number.html.erb.tt",
1167
+ select: "display_select.html.erb.tt",
1168
+ radio: "display_radio.html.erb.tt",
1169
+ attachment: "display_attachment.html.erb.tt",
1170
+ attachments: "display_attachments.html.erb.tt"
1171
+ }[editor_type(field)]
1172
+ end
1173
+
1174
+ def attachment_update_blocks_for_fields
1175
+ before_update_lines = []
1176
+ after_update_lines = []
1177
+
1178
+ fields.each do |field|
1179
+ case field_type(field)
1180
+ when :attachment
1181
+ remove_variable = "remove_#{field}"
1182
+
1183
+ before_update_lines <<
1184
+ "#{remove_variable} = update_params.delete(\"remove_#{field}\")"
1185
+
1186
+ after_update_lines <<
1187
+ "@#{singular_table_name}.#{field}.purge if #{remove_variable} == \"1\""
1188
+
1189
+ when :attachments
1190
+ new_variable = "new_#{field}"
1191
+ remove_variable = "remove_#{field}_ids"
1192
+
1193
+ before_update_lines <<
1194
+ "#{new_variable} = update_params.delete(\"#{field}\")"
1195
+
1196
+ before_update_lines <<
1197
+ "#{remove_variable} = update_params.delete(\"remove_#{field}_ids\")"
1198
+
1199
+ after_update_lines <<
1200
+ "@#{singular_table_name}.#{field}.attach(#{new_variable}) if #{new_variable}.present?"
1201
+
1202
+ after_update_lines << <<~RUBY.strip
1203
+ if #{remove_variable}.present?
1204
+ @#{singular_table_name}.#{field}.attachments
1205
+ .where(id: #{remove_variable})
1206
+ .find_each(&:purge)
1207
+ end
1208
+ RUBY
1209
+ end
1210
+ end
1211
+
1212
+ {
1213
+ before_update: before_update_lines,
1214
+ after_update: after_update_lines
1215
+ }
1216
+ end
1217
+
1218
+ def attachment_reload_block_for_fields
1219
+ reload_lines = fields.filter_map do |field|
1220
+ next unless field_type(field) == :attachments
1221
+
1222
+ "@#{singular_table_name}.#{field}.reload"
1223
+ end
1224
+
1225
+ return "" if reload_lines.empty?
1226
+
1227
+ "#{reload_lines.join("\n")}\n\n"
1228
+ end
1229
+ end
1230
+ end
1231
+ end