b4um_generators 0.2.16 → 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.
@@ -0,0 +1,529 @@
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 SortableGenerator < Rails::Generators::NamedBase
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ class_option :path,
14
+ type: :string,
15
+ default: nil,
16
+ desc: "Creates a separate sortable page under the given path."
17
+
18
+ class << self
19
+ def desc(_description = nil)
20
+ "Adds sortable positioning to an existing B4UM resource."
21
+ end
22
+ end
23
+
24
+ def validate_model
25
+ return if File.exist?(model_path)
26
+
27
+ raise Thor::Error,
28
+ "Model not found: app/models/#{file_name}.rb"
29
+ end
30
+
31
+ def validate_controller
32
+ return if File.exist?(controller_path)
33
+
34
+ raise Thor::Error,
35
+ "Controller not found: app/controllers/#{plural_table_name}_controller.rb"
36
+ end
37
+
38
+ def validate_index_view
39
+ return if File.exist?(index_view_path)
40
+
41
+ raise Thor::Error,
42
+ "Index view not found: app/views/#{plural_table_name}/index.html.erb"
43
+ end
44
+
45
+ def create_position_migration
46
+ return if position_column_exists?
47
+
48
+ migration_template(
49
+ "add_position_migration.rb.tt",
50
+ "db/migrate/add_position_to_#{plural_table_name}.rb"
51
+ )
52
+ end
53
+
54
+ def update_model
55
+ model_content = File.read(model_path)
56
+
57
+ return if model_content.include?("before_create :b4um_set_sortable_position")
58
+
59
+ inject_into_class(
60
+ model_path,
61
+ class_name,
62
+ <<~RUBY.indent(2)
63
+ before_create :b4um_set_sortable_position
64
+ after_destroy :b4um_compact_sortable_positions
65
+
66
+ def b4um_set_sortable_position
67
+ return if position.present?
68
+
69
+ self.position = self.class.maximum(:position).to_i + 1
70
+ end
71
+
72
+ def b4um_compact_sortable_positions
73
+ self.class
74
+ .where("position > ?", position)
75
+ .update_all("position = position - 1")
76
+ end
77
+ RUBY
78
+ )
79
+
80
+ say_status(
81
+ :sortable,
82
+ "#{class_name} now assigns sortable positions",
83
+ :green
84
+ )
85
+ end
86
+
87
+ def update_controller
88
+ controller_content = File.read(controller_path)
89
+
90
+ update_index_order(controller_content)
91
+ add_sort_action
92
+ end
93
+
94
+ def add_sort_route
95
+ routes_path = File.join(
96
+ destination_root,
97
+ "config/routes.rb"
98
+ )
99
+
100
+ unless File.exist?(routes_path)
101
+ raise Thor::Error,
102
+ "Routes not found: config/routes.rb"
103
+ end
104
+
105
+ routes = File.read(routes_path)
106
+
107
+ if options[:path].present?
108
+ add_separate_sort_routes(routes)
109
+ return
110
+ end
111
+
112
+ return if sort_route_exists?(routes)
113
+ return if separate_sort_route_exists?(routes)
114
+
115
+ add_sort_route_to_resources(routes)
116
+ end
117
+
118
+ def create_sortable_controller
119
+ copy_file(
120
+ File.expand_path(
121
+ "templates/sortable_controller.js",
122
+ __dir__
123
+ ),
124
+ "app/javascript/controllers/sortable_controller.js"
125
+ )
126
+ end
127
+
128
+ def create_sortable_partial
129
+ template(
130
+ "_sortable.html.erb.tt",
131
+ File.join(
132
+ "app/views",
133
+ plural_table_name,
134
+ "_sortable.html.erb"
135
+ )
136
+ )
137
+ end
138
+
139
+ def create_sortable_view
140
+ return unless options[:path].present?
141
+
142
+ template(
143
+ "sort.html.erb.tt",
144
+ File.join(
145
+ "app/views",
146
+ plural_table_name,
147
+ "sort.html.erb"
148
+ )
149
+ )
150
+ end
151
+
152
+ def update_index_layout
153
+ return unless File.exist?(index_path)
154
+
155
+ index = File.read(index_path)
156
+
157
+ if options[:path].present?
158
+ restore_index_layout(index)
159
+ else
160
+ use_sortable_index_layout(index)
161
+ end
162
+ end
163
+
164
+ def self.next_migration_number(_dirname)
165
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
166
+ end
167
+
168
+ private
169
+
170
+ def use_sortable_index_layout(index)
171
+ updated_index = index.sub(
172
+ /render\s+["'](?:bento|table|list|alternating|sortable)["']/,
173
+ 'render "sortable"'
174
+ )
175
+
176
+ return if updated_index == index
177
+
178
+ File.write(index_path, updated_index)
179
+ end
180
+
181
+ def restore_index_layout(index)
182
+ return unless index.match?(
183
+ /render\s+["']sortable["']/
184
+ )
185
+
186
+ layout = original_index_layout
187
+
188
+ return unless layout
189
+
190
+ updated_index = index.sub(
191
+ /render\s+["']sortable["']/,
192
+ %(render "#{layout}")
193
+ )
194
+
195
+ File.write(index_path, updated_index)
196
+ end
197
+
198
+ def original_index_layout
199
+ %w[bento table list alternating].find do |layout|
200
+ File.exist?(
201
+ File.join(
202
+ destination_root,
203
+ "app/views",
204
+ plural_table_name,
205
+ "_#{layout}.html.erb"
206
+ )
207
+ )
208
+ end
209
+ end
210
+
211
+ def index_path
212
+ File.join(
213
+ destination_root,
214
+ "app/views",
215
+ plural_table_name,
216
+ "index.html.erb"
217
+ )
218
+ end
219
+
220
+ def model_path
221
+ File.join(
222
+ destination_root,
223
+ "app/models",
224
+ "#{file_name}.rb"
225
+ )
226
+ end
227
+
228
+ def controller_path
229
+ File.join(
230
+ destination_root,
231
+ "app/controllers",
232
+ "#{plural_table_name}_controller.rb"
233
+ )
234
+ end
235
+
236
+ def index_view_path
237
+ File.join(
238
+ destination_root,
239
+ "app/views",
240
+ plural_table_name,
241
+ "index.html.erb"
242
+ )
243
+ end
244
+
245
+ def update_index_order(controller_content)
246
+ return if controller_content.include?(
247
+ "#{class_name}.order(:position, :id)"
248
+ )
249
+
250
+ old_scope = "#{class_name}.all"
251
+
252
+ unless controller_content.include?(old_scope)
253
+ raise Thor::Error,
254
+ "Could not find #{class_name}.all in app/controllers/#{plural_table_name}_controller.rb"
255
+ end
256
+
257
+ gsub_file(
258
+ controller_path,
259
+ old_scope,
260
+ "#{class_name}.order(:position, :id)"
261
+ )
262
+ end
263
+
264
+ def add_sort_action
265
+ controller_content = File.read(controller_path)
266
+
267
+ if controller_content.match?(/^\s*def\s+sort\b/)
268
+ add_sort_page_support(controller_content)
269
+ return
270
+ end
271
+
272
+ get_handling =
273
+ if options[:path].present?
274
+ <<~RUBY
275
+ if request.get?
276
+ @#{plural_table_name} = #{class_name}.order(:position, :id)
277
+ return
278
+ end
279
+
280
+ RUBY
281
+ else
282
+ ""
283
+ end
284
+
285
+ action = <<~RUBY.indent(2)
286
+ def sort
287
+ #{get_handling.indent(2)} record = #{class_name}.find_by(id: params[:id])
288
+ target_position = params[:position].to_i
289
+ maximum_position = #{class_name}.count
290
+
291
+ unless record &&
292
+ target_position.between?(1, maximum_position)
293
+ head :unprocessable_content
294
+ return
295
+ end
296
+
297
+ if target_position == record.position
298
+ head :no_content
299
+ return
300
+ end
301
+
302
+ old_position = record.position
303
+
304
+ #{class_name}.transaction do
305
+ if target_position < old_position
306
+ #{class_name}
307
+ .where(position: target_position...old_position)
308
+ .where.not(id: record.id)
309
+ .update_all("position = position + 1")
310
+ elsif target_position > old_position
311
+ #{class_name}
312
+ .where(position: (old_position + 1)..target_position)
313
+ .where.not(id: record.id)
314
+ .update_all("position = position - 1")
315
+ end
316
+
317
+ record.update_columns(
318
+ position: target_position,
319
+ updated_at: Time.current
320
+ )
321
+ end
322
+
323
+ head :no_content
324
+ end
325
+
326
+ RUBY
327
+
328
+ private_marker = "\n private\n"
329
+
330
+ if controller_content.include?(private_marker)
331
+ insert_into_file(
332
+ controller_path,
333
+ action,
334
+ before: private_marker
335
+ )
336
+ else
337
+ controller_end = /\nend\s*\z/
338
+
339
+ unless controller_content.match?(controller_end)
340
+ raise Thor::Error,
341
+ "Could not find the end of app/controllers/#{plural_table_name}_controller.rb"
342
+ end
343
+
344
+ insert_into_file(
345
+ controller_path,
346
+ "\n#{action}",
347
+ before: controller_end
348
+ )
349
+ end
350
+ end
351
+
352
+ def add_sort_page_support(controller_content)
353
+ return unless options[:path].present?
354
+ return if controller_content.include?("if request.get?")
355
+
356
+ sort_action =
357
+ /^(\s*)def sort\s*$/
358
+
359
+ unless controller_content.match?(sort_action)
360
+ raise Thor::Error,
361
+ "Could not find sort action in app/controllers/#{plural_table_name}_controller.rb"
362
+ end
363
+
364
+ gsub_file(
365
+ controller_path,
366
+ sort_action
367
+ ) do |match|
368
+ indentation = match[/\A\s*/]
369
+
370
+ <<~RUBY.chomp
371
+ #{match}
372
+ #{indentation} if request.get?
373
+ #{indentation} @#{plural_table_name} = #{class_name}.order(:position, :id)
374
+ #{indentation} return
375
+ #{indentation} end
376
+ RUBY
377
+ end
378
+ end
379
+
380
+ def add_separate_sort_routes(routes)
381
+ path = options[:path].to_s
382
+
383
+ remove_default_sort_route(routes)
384
+
385
+ routes = File.read(
386
+ File.join(
387
+ destination_root,
388
+ "config/routes.rb"
389
+ )
390
+ )
391
+
392
+ get_route =
393
+ %(get "#{path}", to: "#{plural_table_name}#sort")
394
+
395
+ patch_route =
396
+ %(patch "#{path}", to: "#{plural_table_name}#sort", as: :sort_#{plural_table_name})
397
+
398
+ routes_to_add = []
399
+
400
+ routes_to_add << get_route unless routes.include?(get_route)
401
+
402
+ routes_to_add << patch_route unless routes.include?(patch_route)
403
+
404
+ return if routes_to_add.empty?
405
+
406
+ routes_end = /^end\s*\z/
407
+
408
+ unless routes.match?(routes_end)
409
+ raise Thor::Error,
410
+ "Could not find the end of config/routes.rb"
411
+ end
412
+
413
+ route_code = <<~RUBY
414
+
415
+ #{routes_to_add.join("\n ")}
416
+ RUBY
417
+
418
+ insert_into_file(
419
+ "config/routes.rb",
420
+ route_code,
421
+ before: routes_end
422
+ )
423
+ end
424
+
425
+ def remove_default_sort_route(routes)
426
+ return unless sort_route_exists?(routes)
427
+
428
+ gsub_file(
429
+ "config/routes.rb",
430
+ /^(\s*)patch :sort, on: :collection\s*\n/,
431
+ ""
432
+ )
433
+ end
434
+
435
+ def sort_route_exists?(routes)
436
+ resource_block = sortable_resource_block(routes)
437
+
438
+ resource_block&.match?(
439
+ /^\s*patch\s+:sort,\s+on:\s+:collection\s*$/
440
+ ) || false
441
+ end
442
+
443
+ def separate_sort_route_exists?(routes)
444
+ routes.match?(
445
+ /^\s*patch\s+["'][^"']+["'],\s+to:\s+["']#{Regexp.escape(plural_table_name)}#sort["']/
446
+ )
447
+ end
448
+
449
+ def sortable_resource_block(routes)
450
+ pattern = /
451
+ ^(?<indent>\s*)
452
+ resources\s+:#{Regexp.escape(plural_table_name)}\s+do\s*$
453
+ (?<body>.*?)
454
+ ^\k<indent>end\s*$
455
+ /mx
456
+
457
+ routes[pattern]
458
+ end
459
+
460
+ def add_sort_route_to_resources(routes)
461
+ block_pattern =
462
+ /^(\s*)resources :#{Regexp.escape(plural_table_name)} do\s*$/
463
+
464
+ plain_pattern =
465
+ /^(\s*)resources :#{Regexp.escape(plural_table_name)}\s*$/
466
+
467
+ if routes.match?(block_pattern)
468
+ gsub_file(
469
+ "config/routes.rb",
470
+ block_pattern
471
+ ) do |match|
472
+ indentation = match[/\A\s*/]
473
+
474
+ [
475
+ match,
476
+ "#{indentation} patch :sort, on: :collection"
477
+ ].join("\n")
478
+ end
479
+ elsif routes.match?(plain_pattern)
480
+ gsub_file(
481
+ "config/routes.rb",
482
+ plain_pattern
483
+ ) do |match|
484
+ indentation = match[/\A\s*/]
485
+
486
+ [
487
+ "#{indentation}resources :#{plural_table_name} do",
488
+ "#{indentation} patch :sort, on: :collection",
489
+ "#{indentation}end"
490
+ ].join("\n")
491
+ end
492
+ else
493
+ raise Thor::Error,
494
+ "Could not find resources :#{plural_table_name} in config/routes.rb"
495
+ end
496
+ end
497
+
498
+ def position_column_exists?
499
+ migration_contains_position? ||
500
+ schema_contains_position?
501
+ end
502
+
503
+ def migration_contains_position?
504
+ Dir.glob(
505
+ File.join(
506
+ destination_root,
507
+ "db/migrate/*_add_position_to_#{plural_table_name}.rb"
508
+ )
509
+ ).any?
510
+ end
511
+
512
+ def schema_contains_position?
513
+ schema_path = File.join(destination_root, "db/schema.rb")
514
+
515
+ return false unless File.exist?(schema_path)
516
+
517
+ schema = File.read(schema_path)
518
+
519
+ table_pattern = /
520
+ create_table\s+"#{Regexp.escape(plural_table_name)}".*?^ end
521
+ /mx
522
+
523
+ table = schema[table_pattern]
524
+
525
+ table&.match?(/t\.integer\s+"position"/) || false
526
+ end
527
+ end
528
+ end
529
+ end
@@ -0,0 +1,40 @@
1
+ <div class="b4um-sortable"
2
+ data-controller="sortable"
3
+ data-sortable-update-url-value="<%%= sort_<%= plural_table_name %>_path %>">
4
+
5
+ <%% <%= plural_table_name %>.each do |<%= singular_table_name %>| %>
6
+ <div class="b4um-sortable__item"
7
+ data-sortable-id="<%%= <%= singular_table_name %>.id %>"
8
+ data-action="dragover->sortable#dragOver drop->sortable#drop">
9
+
10
+ <div class="b4um-sortable__controls">
11
+ <input type="number"
12
+ class="b4um-sortable__position"
13
+ value="<%%= <%= singular_table_name %>.position %>"
14
+ min="1"
15
+ aria-label="Position"
16
+ data-action="change->sortable#positionChanged keydown->sortable#positionKeydown">
17
+
18
+ <button type="button"
19
+ class="b4um-sortable__handle"
20
+ draggable="true"
21
+ aria-label="Move <%= singular_table_name.humanize.downcase %>"
22
+ data-action="dragstart->sortable#dragStart dragend->sortable#dragEnd">
23
+ &#9776;
24
+ </button>
25
+ </div>
26
+
27
+ <div class="b4um-sortable__content">
28
+ <%%= render <%= singular_table_name %>,
29
+ compact: true,
30
+ preview_length: 100 %>
31
+ </div>
32
+
33
+ <div class="b4um-sortable__actions">
34
+ <%%= link_to "Show",
35
+ <%= singular_table_name %>,
36
+ class: "button button--secondary" %>
37
+ </div>
38
+ </div>
39
+ <%% end %>
40
+ </div>
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPositionTo<%= class_name.pluralize %> < ActiveRecord::Migration[8.0]
4
+ def up
5
+ add_column :<%= plural_table_name %>, :position, :integer
6
+
7
+ sortable_record = Class.new(ActiveRecord::Base) do
8
+ self.table_name = "<%= plural_table_name %>"
9
+ end
10
+
11
+ sortable_record.reset_column_information
12
+
13
+ sortable_record.order(:id).find_each.with_index(1) do |record, position|
14
+ record.update_columns(position: position)
15
+ end
16
+
17
+ change_column_null :<%= plural_table_name %>, :position, false
18
+
19
+ add_index :<%= plural_table_name %>, :position
20
+ end
21
+
22
+ def down
23
+ remove_index :<%= plural_table_name %>, :position
24
+ remove_column :<%= plural_table_name %>, :position
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ <h1><%= class_name.pluralize %> sortieren</h1>
2
+
3
+ <%%= render "sortable", <%= plural_table_name %>: @<%= plural_table_name %> %>