b4um_generators 0.1.1 → 0.2.7

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,514 @@
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 AttachmentGenerator < Rails::Generators::NamedBase
9
+ argument :attachment,
10
+ type: :string,
11
+ required: true,
12
+ banner: "ATTACHMENT"
13
+
14
+ class_option :multiple,
15
+ type: :boolean,
16
+ default: false,
17
+ desc: "Adds multiple attachments with has_many_attached."
18
+
19
+ class << self
20
+ def desc(_description = nil)
21
+ "Adds an attachment to an existing model."
22
+ end
23
+ end
24
+
25
+ def validate_model
26
+ return if File.exist?(model_path)
27
+
28
+ raise Thor::Error,
29
+ "Model not found: app/models/#{file_name}.rb"
30
+ end
31
+
32
+ def add_attachment
33
+ model_content = File.read(model_path)
34
+ declaration = attachment_declaration
35
+
36
+ return if model_content.include?(declaration)
37
+
38
+ inject_into_class(
39
+ model_path,
40
+ class_name,
41
+ " #{declaration}\n"
42
+ )
43
+
44
+ say_status(
45
+ :attachment,
46
+ "#{class_name} now has #{attachment}",
47
+ :green
48
+ )
49
+ end
50
+
51
+ def update_form
52
+ return unless File.exist?(form_path)
53
+
54
+ content = File.read(form_path)
55
+
56
+ return if content.include?("form.file_field :#{attachment}")
57
+
58
+ marker = ' <div class="form-actions">'
59
+
60
+ return unless content.include?(marker)
61
+
62
+ insert_into_file(
63
+ form_path,
64
+ attachment_form,
65
+ before: marker
66
+ )
67
+ end
68
+
69
+ def update_controller_params
70
+ return unless File.exist?(controller_path)
71
+
72
+ if options[:multiple]
73
+ update_multiple_attachment_params
74
+ else
75
+ update_single_attachment_params
76
+ end
77
+ end
78
+
79
+ def update_resource
80
+ return unless File.exist?(resource_path)
81
+
82
+ content = File.read(resource_path)
83
+
84
+ return if content.include?(
85
+ %(<strong class="resource-label">#{attachment.humanize}:</strong>)
86
+ )
87
+
88
+ closing_tag = "\n</div>\n"
89
+
90
+ return unless content.end_with?(closing_tag)
91
+
92
+ resource = attachment_resource
93
+
94
+ content = content.sub(
95
+ %r{</div>\n\z},
96
+ "#{resource}</div>\n"
97
+ )
98
+
99
+ File.write(resource_path, content)
100
+
101
+ say_status(
102
+ :insert,
103
+ resource_path.sub("#{destination_root}/", ""),
104
+ :green
105
+ )
106
+ end
107
+
108
+ private
109
+
110
+ def update_single_attachment_params
111
+ content = File.read(controller_path)
112
+
113
+ return if attachment_permitted?(content)
114
+
115
+ pattern = /
116
+ params\.expect\(
117
+ #{Regexp.escape(file_name)}:\s*
118
+ \[
119
+ (?<attributes>[^\]]*)
120
+ \]
121
+ \)
122
+ /mx
123
+
124
+ return unless content.match?(pattern)
125
+
126
+ gsub_file(controller_path, pattern) do |match|
127
+ match.sub(
128
+ /\]\)$/,
129
+ ", :#{attachment}])"
130
+ )
131
+ end
132
+ end
133
+
134
+ def update_multiple_attachment_params
135
+ content = File.read(controller_path)
136
+
137
+ update_multiple_permitted_params(content)
138
+ update_multiple_attachment_action
139
+ end
140
+
141
+ def update_multiple_permitted_params(content)
142
+ return if multiple_attachment_permitted?(content)
143
+
144
+ pattern = /
145
+ params\.expect\(
146
+ #{Regexp.escape(file_name)}:\s*
147
+ \[
148
+ (?<attributes>[^\]]*)
149
+ \]
150
+ \)
151
+ /mx
152
+
153
+ return unless content.match?(pattern)
154
+
155
+ gsub_file(controller_path, pattern) do |match|
156
+ match.sub(
157
+ /\]\)$/,
158
+ ", #{attachment}: [], remove_#{attachment}_ids: []])"
159
+ )
160
+ end
161
+ end
162
+
163
+ def multiple_attachment_permitted?(content)
164
+ content.include?(
165
+ "#{attachment}: [], remove_#{attachment}_ids: []"
166
+ )
167
+ end
168
+
169
+ def update_multiple_attachment_action
170
+ content = File.read(controller_path)
171
+
172
+ return if content.include?(
173
+ "new_#{attachment} = update_params.delete(\"#{attachment}\")"
174
+ )
175
+
176
+ setup = [
177
+ " def update",
178
+ " update_params = #{file_name}_params",
179
+ " new_#{attachment} = update_params.delete(\"#{attachment}\")",
180
+ " remove_#{attachment}_ids = update_params.delete(\"remove_#{attachment}_ids\")",
181
+ "",
182
+ " respond_to do |format|",
183
+ ""
184
+ ].join("\n")
185
+
186
+ gsub_file(
187
+ controller_path,
188
+ " def update\n respond_to do |format|\n",
189
+ setup
190
+ )
191
+
192
+ attachment_handling = [
193
+ " if @#{file_name}.update(update_params)",
194
+ " @#{file_name}.#{attachment}.attach(new_#{attachment}) if new_#{attachment}.present?",
195
+ "",
196
+ " if remove_#{attachment}_ids.present?",
197
+ " @#{file_name}.#{attachment}.attachments",
198
+ " .where(id: remove_#{attachment}_ids)",
199
+ " .find_each(&:purge)",
200
+ " end",
201
+ ""
202
+ ].join("\n")
203
+
204
+ gsub_file(
205
+ controller_path,
206
+ " if @#{file_name}.update(#{file_name}_params)\n",
207
+ attachment_handling
208
+ )
209
+ end
210
+
211
+ def attachment_declaration
212
+ if options[:multiple]
213
+ "has_many_attached :#{attachment}"
214
+ else
215
+ "has_one_attached :#{attachment}"
216
+ end
217
+ end
218
+
219
+ def model_path
220
+ File.join(
221
+ destination_root,
222
+ "app/models",
223
+ "#{file_name}.rb"
224
+ )
225
+ end
226
+
227
+ def form_path
228
+ File.join(
229
+ destination_root,
230
+ "app/views",
231
+ plural_name,
232
+ "_form.html.erb"
233
+ )
234
+ end
235
+
236
+ def resource_path
237
+ File.join(
238
+ destination_root,
239
+ "app/views",
240
+ plural_name,
241
+ "_#{file_name}.html.erb"
242
+ )
243
+ end
244
+
245
+ def attachment_resource
246
+ if options[:multiple]
247
+ multiple_attachment_resource
248
+ else
249
+ single_attachment_resource
250
+ end
251
+ end
252
+
253
+ def single_attachment_resource
254
+ <<~ERB.indent(2)
255
+ <div class="resource-field">
256
+ <strong class="resource-label">#{attachment.humanize}:</strong>
257
+
258
+ <div class="resource-image"
259
+ data-controller="image-lightbox"
260
+ data-action="keydown->image-lightbox#keydown">
261
+
262
+ <% if #{file_name}.#{attachment}.attached? %>
263
+ <button type="button"
264
+ class="resource-image__button"
265
+ data-image-lightbox-target="item"
266
+ data-image-lightbox-url="<%= url_for(#{file_name}.#{attachment}) %>"
267
+ data-image-lightbox-alt="<%= #{file_name}.#{attachment}.filename.to_s %>"
268
+ data-action="click->image-lightbox#open">
269
+
270
+ <%= image_tag #{file_name}.#{attachment},
271
+ class: "resource-image__image",
272
+ alt: #{file_name}.#{attachment}.filename.to_s %>
273
+ </button>
274
+
275
+ <div class="image-lightbox"
276
+ data-image-lightbox-target="overlay"
277
+ data-action="click->image-lightbox#closeOnBackground"
278
+ tabindex="-1"
279
+ role="dialog"
280
+ aria-modal="true"
281
+ aria-label="Image preview"
282
+ hidden>
283
+
284
+ <div class="image-lightbox__counter"
285
+ data-image-lightbox-target="counter"></div>
286
+
287
+ <button type="button"
288
+ class="image-lightbox__close"
289
+ aria-label="Close image"
290
+ data-action="click->image-lightbox#close">
291
+ &times;
292
+ </button>
293
+
294
+ <div class="image-lightbox__stage"
295
+ data-image-lightbox-target="stage"
296
+ data-action="pointerdown->image-lightbox#pointerDown pointermove->image-lightbox#pointerMove pointerup->image-lightbox#pointerUp pointercancel->image-lightbox#pointerCancel">
297
+ <img class="image-lightbox__image"
298
+ data-image-lightbox-target="image"
299
+ alt="">
300
+ </div>
301
+
302
+ <div class="image-lightbox__caption"
303
+ data-image-lightbox-target="caption"></div>
304
+ </div>
305
+ <% else %>
306
+ <span class="resource-value resource-value--empty">No image</span>
307
+ <% end %>
308
+ </div>
309
+ </div>
310
+ ERB
311
+ end
312
+
313
+ def multiple_attachment_resource
314
+ <<~ERB.indent(2)
315
+ <div class="resource-field">
316
+ <strong class="resource-label">#{attachment.humanize}:</strong>
317
+
318
+ <div class="resource-images"
319
+ data-controller="image-lightbox"
320
+ data-action="keydown->image-lightbox#keydown">
321
+
322
+ <% if #{file_name}.#{attachment}.attached? %>
323
+ <% #{file_name}.#{attachment}.each do |image| %>
324
+ <button type="button"
325
+ class="resource-images__button"
326
+ data-image-lightbox-target="item"
327
+ data-image-lightbox-url="<%= url_for(image) %>"
328
+ data-image-lightbox-alt="<%= image.filename.to_s %>"
329
+ data-action="click->image-lightbox#open">
330
+
331
+ <%= image_tag image,
332
+ class: "resource-images__image",
333
+ alt: image.filename.to_s %>
334
+ </button>
335
+ <% end %>
336
+
337
+ <div class="image-lightbox"
338
+ data-image-lightbox-target="overlay"
339
+ data-action="click->image-lightbox#closeOnBackground"
340
+ tabindex="-1"
341
+ role="dialog"
342
+ aria-modal="true"
343
+ aria-label="Image gallery"
344
+ hidden>
345
+
346
+ <div class="image-lightbox__counter"
347
+ data-image-lightbox-target="counter"></div>
348
+
349
+ <button type="button"
350
+ class="image-lightbox__close"
351
+ aria-label="Close image"
352
+ data-action="click->image-lightbox#close">
353
+ &times;
354
+ </button>
355
+
356
+ <button type="button"
357
+ class="image-lightbox__previous"
358
+ aria-label="Previous image"
359
+ data-action="click->image-lightbox#previous">
360
+ &#8249;
361
+ </button>
362
+
363
+ <div class="image-lightbox__stage"
364
+ data-image-lightbox-target="stage"
365
+ data-action="pointerdown->image-lightbox#pointerDown pointermove->image-lightbox#pointerMove pointerup->image-lightbox#pointerUp pointercancel->image-lightbox#pointerCancel">
366
+ <img class="image-lightbox__image"
367
+ data-image-lightbox-target="image"
368
+ alt="">
369
+ </div>
370
+
371
+ <button type="button"
372
+ class="image-lightbox__next"
373
+ aria-label="Next image"
374
+ data-action="click->image-lightbox#next">
375
+ &#8250;
376
+ </button>
377
+
378
+ <div class="image-lightbox__caption"
379
+ data-image-lightbox-target="caption"></div>
380
+ </div>
381
+ <% else %>
382
+ <span class="resource-value resource-value--empty">No images</span>
383
+ <% end %>
384
+ </div>
385
+ </div>
386
+ ERB
387
+ end
388
+
389
+ def controller_path
390
+ File.join(
391
+ destination_root,
392
+ "app/controllers",
393
+ "#{plural_name}_controller.rb"
394
+ )
395
+ end
396
+
397
+ def attachment_permitted?(content)
398
+ content.match?(
399
+ /params\.expect\(#{Regexp.escape(file_name)}:.*:#{Regexp.escape(attachment)}.*\)/m
400
+ )
401
+ end
402
+
403
+ def attachment_form
404
+ if options[:multiple]
405
+ multiple_attachment_form
406
+ else
407
+ single_attachment_form
408
+ end
409
+ end
410
+
411
+ def single_attachment_form
412
+ <<~ERB.indent(2)
413
+ <div class="form-field form-field--file"
414
+ data-controller="image-preview">
415
+
416
+ <div class="form-file-control">
417
+ <%= form.label :#{attachment},
418
+ class: "form-file-label" %>
419
+
420
+ <%= form.file_field :#{attachment},
421
+ class: "form-file-input",
422
+ accept: "image/*",
423
+ data: {
424
+ image_preview_target: "input",
425
+ action: "change->image-preview#preview"
426
+ } %>
427
+ </div>
428
+
429
+ <div class="form-image-preview"
430
+ data-image-preview-target="container"
431
+ <%= "hidden" unless #{file_name}.#{attachment}.attached? %>>
432
+
433
+ <% if #{file_name}.#{attachment}.attached? %>
434
+ <%= image_tag #{file_name}.#{attachment},
435
+ class: "form-image-preview__image",
436
+ alt: #{file_name}.#{attachment}.filename.to_s,
437
+ data: {
438
+ image_preview_target: "image"
439
+ } %>
440
+ <% else %>
441
+ <img class="form-image-preview__image"
442
+ data-image-preview-target="image"
443
+ alt="Image preview">
444
+ <% end %>
445
+ </div>
446
+ </div>
447
+
448
+ ERB
449
+ end
450
+
451
+ def multiple_attachment_form
452
+ <<~ERB.indent(2)
453
+ <div class="form-field form-field--file"
454
+ data-controller="image-preview">
455
+
456
+ <div class="form-file-control">
457
+ <%= form.label :#{attachment},
458
+ class: "form-file-label" %>
459
+
460
+ <% #{file_name}.#{attachment}.each do |image| %>
461
+ <%= form.hidden_field :#{attachment},
462
+ multiple: true,
463
+ value: image.signed_id %>
464
+ <% end %>
465
+
466
+ <%= form.file_field :#{attachment},
467
+ class: "form-file-input",
468
+ accept: "image/*",
469
+ multiple: true,
470
+ data: {
471
+ image_preview_target: "input",
472
+ action: "change->image-preview#previewMultiple"
473
+ } %>
474
+ </div>
475
+
476
+ <div class="form-image-preview form-image-preview--multiple"
477
+ data-image-preview-target="container"
478
+ <%= "hidden" unless #{file_name}.#{attachment}.attached? %>>
479
+
480
+ <% #{file_name}.#{attachment}.each do |image| %>
481
+ <div class="form-image-preview__item"
482
+ role="button"
483
+ tabindex="0"
484
+ aria-pressed="false"
485
+ data-image-preview-target="existingItem"
486
+ data-action="click->image-preview#toggleRemoval keydown->image-preview#toggleRemovalWithKeyboard">
487
+
488
+ <%= image_tag image,
489
+ class: "form-image-preview__image",
490
+ alt: image.filename.to_s %>
491
+
492
+ <%= check_box_tag(
493
+ "#{file_name}[remove_#{attachment}_ids][]",
494
+ image.id,
495
+ false,
496
+ class: "form-image-preview__remove-checkbox",
497
+ data: {
498
+ image_preview_target: "removeCheckbox"
499
+ }
500
+ ) %>
501
+
502
+ <div class="form-image-preview__remove-status">
503
+ Will be removed
504
+ </div>
505
+ </div>
506
+ <% end %>
507
+ </div>
508
+ </div>
509
+
510
+ ERB
511
+ end
512
+ end
513
+ end
514
+ end