good_job 4.13.0 → 4.13.1

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/app/controllers/good_job/batches_controller.rb +1 -1
  4. data/app/controllers/good_job/cron_entries_controller.rb +3 -3
  5. data/app/controllers/good_job/frontends_controller.rb +15 -10
  6. data/app/controllers/good_job/jobs_controller.rb +7 -7
  7. data/app/controllers/good_job/pauses_controller.rb +2 -2
  8. data/app/frontend/good_job/application.js +23 -9
  9. data/app/frontend/good_job/modules/form_controller.js +11 -0
  10. data/app/frontend/good_job/vendor/turbo.js +34 -0
  11. data/app/models/good_job/batch.rb +2 -0
  12. data/app/models/good_job/batch_record.rb +2 -0
  13. data/app/models/good_job/job.rb +2 -2
  14. data/app/models/good_job/process.rb +2 -0
  15. data/app/models/good_job/setting.rb +2 -0
  16. data/app/views/good_job/batches/_jobs.erb +4 -4
  17. data/app/views/good_job/batches/_table.erb +2 -2
  18. data/app/views/good_job/batches/show.html.erb +1 -1
  19. data/app/views/good_job/cron_entries/index.html.erb +2 -2
  20. data/app/views/good_job/jobs/_table.erb +12 -9
  21. data/app/views/good_job/jobs/show.html.erb +4 -4
  22. data/app/views/good_job/pauses/_group.html.erb +1 -1
  23. data/app/views/good_job/pauses/_pause.html.erb +1 -1
  24. data/app/views/good_job/shared/_filter.erb +4 -14
  25. data/app/views/good_job/shared/_navbar.erb +6 -6
  26. data/app/views/layouts/good_job/application.html.erb +10 -8
  27. data/config/brakeman.ignore +25 -25
  28. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +1 -1
  29. data/lib/generators/good_job/templates/update/migrations/05_add_index_good_jobs_finished_at_for_cleanup.rb.erb +17 -0
  30. data/lib/generators/good_job/templates/update/migrations/06_remove_extraneous_finished_at_index.rb.erb +17 -0
  31. data/lib/good_job/version.rb +1 -1
  32. data/lib/good_job.rb +1 -1
  33. metadata +5 -3
  34. data/app/frontend/good_job/modules/document_ready.js +0 -7
  35. data/app/frontend/good_job/vendor/rails_ujs.js +0 -7
@@ -68,26 +68,26 @@
68
68
  <ul class="dropdown-menu shadow" aria-labelledby="<%= dom_id(job, :actions) %>">
69
69
  <li>
70
70
  <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
71
- <%= link_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t(".actions.reschedule"), data: { confirm: t(".actions.confirm_reschedule"), disable: true } do %>
71
+ <%= button_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t(".actions.reschedule"), data: { turbo_confirm: t(".actions.confirm_reschedule") } do %>
72
72
  <%= render_icon "skip_forward" %>
73
73
  <%= t "good_job.actions.reschedule" %>
74
74
  <% end %>
75
75
  </li>
76
76
  <li>
77
77
  <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
78
- <%= link_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: t(".actions.discard"), data: { confirm: t(".actions.confirm_discard"), disable: true } do %>
78
+ <%= button_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: t(".actions.discard"), data: { turbo_confirm: t(".actions.confirm_discard") } do %>
79
79
  <%= render_icon "stop" %>
80
80
  <%= t "good_job.actions.discard" %>
81
81
  <% end %>
82
82
  </li>
83
83
  <li>
84
- <%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t(".actions.retry"), data: { confirm: t(".actions.confirm_retry"), disable: true } do %>
84
+ <%= button_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t(".actions.retry"), data: { turbo_confirm: t(".actions.confirm_retry") } do %>
85
85
  <%= render_icon "arrow_clockwise" %>
86
86
  <%= t "good_job.actions.retry" %>
87
87
  <% end %>
88
88
  </li>
89
89
  <li>
90
- <%= link_to job_path(job.id), method: :delete, class: "dropdown-item #{'disabled' unless job.finished?}", title: t(".actions.destroy"), data: { confirm: t(".actions.confirm_destroy"), disable: true } do %>
90
+ <%= button_to job_path(job.id), method: :delete, class: "dropdown-item #{'disabled' unless job.finished?}", title: t(".actions.destroy"), data: { turbo_confirm: t(".actions.confirm_destroy") } do %>
91
91
  <%= render_icon "trash" %>
92
92
  <%= t "good_job.actions.destroy" %>
93
93
  <% end %>
@@ -58,9 +58,9 @@
58
58
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.batch.jobs" %></div>
59
59
  <%= batch.jobs.size %>
60
60
  </div>
61
- <div class="col text-end">
61
+ <div class="col text-end d-flex flex-row justify-content-end">
62
62
  <% if batch.discarded? %>
63
- <%= link_to retry_batch_path(batch), method: :put, class: "btn btn-sm btn-outline-primary", title: t("good_job.batches.actions.retry"), data: { confirm: t("good_job.batches.actions.confirm_retry") } do %>
63
+ <%= button_to retry_batch_path(batch), method: :put, class: "btn btn-sm btn-outline-primary", title: t("good_job.batches.actions.retry"), data: { turbo_confirm: t("good_job.batches.actions.confirm_retry") } do %>
64
64
  <%= render_icon "arrow_clockwise" %>
65
65
  <%= t "good_job.batches.actions.retry" %>
66
66
  <% end %>
@@ -12,7 +12,7 @@
12
12
  </div>
13
13
  <div class="col text-end">
14
14
  <% if @batch.discarded? %>
15
- <%= button_to retry_batch_path(@batch), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.batches.actions.retry") }, title: t("good_job.batches.actions.retry"), data: { confirm: t("good_job.batches.actions.confirm_retry") } do %>
15
+ <%= button_to retry_batch_path(@batch), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.batches.actions.retry") }, title: t("good_job.batches.actions.retry"), data: { turbo_confirm: t("good_job.batches.actions.confirm_retry") } do %>
16
16
  <%= render_icon "arrow_clockwise" %>
17
17
  <%= t "good_job.actions.retry" %>
18
18
  <% end %>
@@ -58,11 +58,11 @@
58
58
  <% end %>
59
59
 
60
60
  <% if cron_entry.enabled? %>
61
- <%= button_to disable_cron_entry_path(cron_entry), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.cron_entries.actions.disable") }, title: t("good_job.cron_entries.actions.disable"), data: { confirm: t("good_job.cron_entries.actions.confirm_disable") } do %>
61
+ <%= button_to disable_cron_entry_path(cron_entry), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.cron_entries.actions.disable") }, title: t("good_job.cron_entries.actions.disable"), data: { turbo_confirm: t("good_job.cron_entries.actions.confirm_disable") } do %>
62
62
  <%= render_icon "pause" %>
63
63
  <% end %>
64
64
  <% else %>
65
- <%= button_to enable_cron_entry_path(cron_entry), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.cron_entries.actions.enable") }, title: t("good_job.cron_entries.actions.enable"), data: { confirm: t("good_job.cron_entries.actions.confirm_enable") } do %>
65
+ <%= button_to enable_cron_entry_path(cron_entry), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.cron_entries.actions.enable") }, title: t("good_job.cron_entries.actions.enable"), data: { turbo_confirm: t("good_job.cron_entries.actions.confirm_enable") } do %>
66
66
  <%= render_icon "play" %>
67
67
  <% end %>
68
68
  <% end %>
@@ -1,3 +1,6 @@
1
+ <%= form_with(id: "job_action_form", method: :put, model: false, local: true) { nil } %>
2
+ <%= form_with(id: "job_destroy_form", method: :delete, model: false, local: true) { nil } %>
3
+
1
4
  <%= form_with(model: false, url: mass_update_jobs_path(filter.to_params), method: :put, local: true, data: { "checkbox-toggle": "job_ids" }) do |form| %>
2
5
  <div class="my-3 card" data-gj-poll-replace id="jobs-table">
3
6
  <div class="list-group list-group-flush text-nowrap table-jobs" role="table">
@@ -8,16 +11,16 @@
8
11
  <%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-all": "job_ids" }) %>
9
12
  <%= label_tag('toggle_job_ids', t(".toggle_all_jobs"), class: "visually-hidden") %>
10
13
  </div>
11
- <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'ms-1 btn btn-sm btn-outline-secondary', title: t(".actions.reschedule_all"), data: { confirm: t(".actions.confirm_reschedule_all"), disable: true } do %>
14
+ <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'ms-1 btn btn-sm btn-outline-secondary', title: t(".actions.reschedule_all"), data: { turbo_confirm: t(".actions.confirm_reschedule_all") } do %>
12
15
  <span class="me-1"><%= render_icon "skip_forward" %></span> <%= t "good_job.actions.reschedule" %>
13
16
  <% end %>
14
17
 
15
- <%= form.button type: 'submit', name: 'mass_action', value: 'retry', class: 'btn btn-sm btn-outline-secondary', title: t(".actions.retry_all"), data: { confirm: t(".actions.confirm_retry_all"), disable: true } do %>
18
+ <%= form.button type: 'submit', name: 'mass_action', value: 'retry', class: 'btn btn-sm btn-outline-secondary', title: t(".actions.retry_all"), data: { turbo_confirm: t(".actions.confirm_retry_all") } do %>
16
19
  <span class="me-1"><%= render_icon "arrow_clockwise" %></span> <%= t "good_job.actions.retry" %>
17
20
  <% end %>
18
21
 
19
22
  <div class="btn-group" role="group">
20
- <%= form.button type: 'submit', name: 'mass_action', value: 'discard', class: 'btn btn-sm btn-outline-secondary', title: t(".actions.discard_all"), data: { confirm: t(".actions.confirm_discard_all"), disable: true } do %>
23
+ <%= form.button type: 'submit', name: 'mass_action', value: 'discard', class: 'btn btn-sm btn-outline-secondary', title: t(".actions.discard_all"), data: { turbo_confirm: t(".actions.confirm_discard_all") } do %>
21
24
  <span class="me-1"><%= render_icon "stop" %></span> <%= t "good_job.actions.discard" %>
22
25
  <% end %>
23
26
  <button id="destroy-dropdown-toggle" type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
@@ -25,7 +28,7 @@
25
28
  </button>
26
29
  <ul class="dropdown-menu" aria-labelledby="destroy-dropdown-toggle">
27
30
  <li>
28
- <%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn dropdown-item', title: t(".actions.destroy_all"), data: { confirm: t(".actions.confirm_destroy_all"), disable: true } do %>
31
+ <%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn dropdown-item', title: t(".actions.destroy_all"), data: { turbo_confirm: t(".actions.confirm_destroy_all") } do %>
29
32
  <span class="me-1"><%= render_icon "trash" %></span> <%= t "good_job.actions.destroy" %>
30
33
  <% end %>
31
34
  </li>
@@ -125,33 +128,33 @@
125
128
  <ul class="dropdown-menu shadow" aria-labelledby="<%= dom_id(job, :actions) %>">
126
129
  <li>
127
130
  <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
128
- <%= link_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t("good_job.jobs.actions.reschedule"), data: { confirm: t("good_job.jobs.actions.confirm_reschedule"), disable: true } do %>
131
+ <%= tag.button form: "job_action_form", formaction: reschedule_job_path(job.id), class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t("good_job.jobs.actions.reschedule"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_reschedule") } do %>
129
132
  <%= render_icon "skip_forward" %>
130
133
  <%= t "good_job.actions.reschedule" %>
131
134
  <% end %>
132
135
  </li>
133
136
  <li>
134
137
  <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
135
- <%= link_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: t("good_job.jobs.actions.discard"), data: { confirm: t("good_job.jobs.actions.confirm_discard"), disable: true } do %>
138
+ <%= tag.button form: "job_action_form", formaction: discard_job_path(job.id), class: "dropdown-item #{'disabled' unless job_discardable}", title: t("good_job.jobs.actions.discard"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_discard") } do %>
136
139
  <%= render_icon "stop" %>
137
140
  <%= t "good_job.actions.discard" %>
138
141
  <% end %>
139
142
  </li>
140
143
  <li>
141
144
  <% job_force_discardable = job.status.in? [:running] %>
142
- <%= link_to force_discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_force_discardable}", title: t("good_job.jobs.actions.force_discard"), data: { confirm: t("good_job.jobs.actions.confirm_force_discard"), disable: true } do %>
145
+ <%= tag.button form: "job_action_form", formaction: force_discard_job_path(job.id), class: "dropdown-item #{'disabled' unless job_force_discardable}", title: t("good_job.jobs.actions.force_discard"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_force_discard") } do %>
143
146
  <%= render_icon "eject" %>
144
147
  <%= t "good_job.actions.force_discard" %>
145
148
  <% end %>
146
149
  </li>
147
150
  <li>
148
- <%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t("good_job.jobs.actions.retry"), data: { confirm: t("good_job.jobs.actions.confirm_retry"), disable: true } do %>
151
+ <%= tag.button form: "job_action_form", formaction: retry_job_path(job.id), class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t("good_job.jobs.actions.retry"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_retry") } do %>
149
152
  <%= render_icon "arrow_clockwise" %>
150
153
  <%= t "good_job.actions.retry" %>
151
154
  <% end %>
152
155
  </li>
153
156
  <li>
154
- <%= link_to job_path(job.id), method: :delete, class: "dropdown-item #{'disabled' unless job.status.in? [:discarded, :succeeded]}", title: t("good_job.jobs.actions.destroy"), data: { confirm: t("good_job.jobs.actions.confirm_destroy"), disable: true } do %>
157
+ <%= tag.button form: "job_destroy_form", formaction: job_path(job.id), class: "dropdown-item #{'disabled' unless job.status.in? [:discarded, :succeeded]}", title: t("good_job.jobs.actions.destroy"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_destroy") } do %>
155
158
  <%= render_icon "trash" %>
156
159
  <%= t "good_job.actions.destroy" %>
157
160
  <% end %>
@@ -33,28 +33,28 @@
33
33
  form_class: "d-inline-block",
34
34
  aria: { label: t("good_job.jobs.actions.reschedule") },
35
35
  title: t("good_job.jobs.actions.reschedule"),
36
- data: { confirm: t("good_job.jobs.actions.confirm_reschedule") } do %>
36
+ data: { turbo_confirm: t("good_job.jobs.actions.confirm_reschedule") } do %>
37
37
  <%= render_icon "skip_forward" %>
38
38
  <%= t "good_job.actions.reschedule" %>
39
39
  <% end %>
40
40
  <% end %>
41
41
 
42
42
  <% if @job.status.in? [:scheduled, :retried, :queued] %>
43
- <%= button_to discard_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.discard") }, title: t("good_job.jobs.actions.discard"), data: { confirm: t("good_job.jobs.actions.confirm_discard") } do %>
43
+ <%= button_to discard_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.discard") }, title: t("good_job.jobs.actions.discard"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_discard") } do %>
44
44
  <%= render_icon "stop" %>
45
45
  <%= t "good_job.actions.discard" %>
46
46
  <% end %>
47
47
  <% end %>
48
48
 
49
49
  <% if @job.status == :discarded %>
50
- <%= button_to retry_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.retry") }, title: t("good_job.jobs.actions.retry"), data: { confirm: t("good_job.jobs.actions.confirm_retry") } do %>
50
+ <%= button_to retry_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.retry") }, title: t("good_job.jobs.actions.retry"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_retry") } do %>
51
51
  <%= render_icon "arrow_clockwise" %>
52
52
  <%= t "good_job.actions.retry" %>
53
53
  <% end %>
54
54
  <% end %>
55
55
 
56
56
  <% if @job.status.in? [:discarded, :succeeded] %>
57
- <%= button_to job_path(@job.id), method: :delete, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.destroy") }, title: t("good_job.jobs.actions.destroy"), data: { confirm: t("good_job.jobs.actions.confirm_destroy") } do %>
57
+ <%= button_to job_path(@job.id), method: :delete, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.destroy") }, title: t("good_job.jobs.actions.destroy"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_destroy") } do %>
58
58
  <%= render_icon "trash" %>
59
59
  <%= t "good_job.actions.destroy" %>
60
60
  <% end %>
@@ -16,7 +16,7 @@
16
16
  { action: :destroy, type: type, value: value },
17
17
  method: :delete,
18
18
  class: 'btn btn-sm btn-outline-primary',
19
- data: { confirm: t('good_job.pauses.index.confirm_unpause', value: value) }
19
+ data: { turbo_confirm: t('good_job.pauses.index.confirm_unpause', value: value) }
20
20
  ) do %>
21
21
  <%= render_icon "play" %>
22
22
  <%= t("good_job.pauses.index.unpause") %>
@@ -4,5 +4,5 @@
4
4
  { action: :destroy, type: type, value: value },
5
5
  method: :delete,
6
6
  class: 'btn btn-sm btn-outline-primary',
7
- data: { confirm: t('good_job.pauses.index.confirm_unpause', value: value) } %>
7
+ data: { turbo_confirm: t('good_job.pauses.index.confirm_unpause', value: value) } %>
8
8
  </li>
@@ -4,14 +4,14 @@
4
4
  <h2 class="pt-3 pb-2"><%= title %></h2>
5
5
  </div>
6
6
 
7
- <%= form_with(model: false, url: "", method: :get, local: true, id: "filter_form", class: "") do |form| %>
7
+ <%= form_with(model: false, url: "", method: :get, local: true, data: { controller: "form" }) do |form| %>
8
8
  <%= hidden_field_tag :poll, params[:poll] %>
9
9
  <%= hidden_field_tag :state, params[:state] %>
10
10
  <%= hidden_field_tag :locale, params[:locale] if params[:locale] %>
11
11
  <div class="d-md-flex flex-row w-100">
12
12
  <div class="me-md-2 mb-2 mb-md-0">
13
13
  <%= label_tag "job_queue_filter", t(".queue_name"), class: "visually-hidden" %>
14
- <select name="queue_name" id="job_queue_filter" class="form-select form-select-sm">
14
+ <select name="queue_name" id="job_queue_filter" class="form-select form-select-sm" data-action="change->form#submit">
15
15
  <option value="" <%= "selected='selected'" if params[:queue_name].blank? %>><%= t ".all_queues" %></option>
16
16
 
17
17
  <% filter.queues.each do |name, count| %>
@@ -22,7 +22,7 @@
22
22
 
23
23
  <div class="me-md-2 mb-2 mb-md-0">
24
24
  <%= label_tag "job_class_filter", t(".job_name"), class: "visually-hidden" %>
25
- <select name="job_class" id="job_class_filter" class="form-select form-select-sm">
25
+ <select name="job_class" id="job_class_filter" class="form-select form-select-sm" data-action="change->form#submit">
26
26
  <option value="" <%= "selected='selected'" if params[:job_class].blank? %>><%= t ".all_jobs" %></option>
27
27
 
28
28
  <% filter.job_classes.each do |name, count| %>
@@ -56,20 +56,10 @@
56
56
  <li class="nav-item">
57
57
  <%= link_to filter.to_params(state: name), class: "nav-link #{'active' if params[:state] == name}" do %>
58
58
  <%= t(name, scope: 'good_job.status') %>
59
- <span data-async-values-target="value" data-async-values-key="<%= name %>" data-async-values-zero-class="text-bg-secondary" class="badge text-bg-primary rounded-pill d-none"></span>
59
+ <span data-async-values-target="value" data-async-values-key="<%= name %>" data-async-values-zero-class="bg-secondary" class="badge text-bg-primary rounded-pill d-none" id="filter_state_<%= name %>" data-turbo-permanent></span>
60
60
  <% end %>
61
61
  </li>
62
62
  <% end %>
63
63
  </ul>
64
64
  </div>
65
-
66
- <script nonce="<%= content_security_policy_nonce %>">
67
- document.addEventListener("DOMContentLoaded", () => {
68
- document.querySelectorAll("#job_class_filter, #job_queue_filter").forEach((filter) => {
69
- filter.addEventListener("change", () => {
70
- document.querySelector("#filter_form").submit();
71
- });
72
- })
73
- })
74
- </script>
75
65
  </div>
@@ -18,25 +18,25 @@
18
18
  <li class="nav-item">
19
19
  <%= link_to jobs_path, class: ["nav-link", ("active" if controller_name == 'jobs')] do %>
20
20
  <%= t(".jobs") %>
21
- <span data-async-values-target="value" data-async-values-key="jobs_count" class="badge text-bg-secondary rounded-pill d-none"></span>
21
+ <span data-async-values-target="value" data-async-values-key="jobs_count" class="badge text-bg-secondary rounded-pill d-none" id="navbar_jobs_count" data-turbo-permanent></span>
22
22
  <% end %>
23
23
  </li>
24
24
  <li class="nav-item">
25
25
  <%= link_to batches_path, class: ["nav-link", ("active" if controller_name == 'batches')] do %>
26
26
  <%= t ".batches" %>
27
- <span data-async-values-target="value" data-async-values-key="batches_count" class="badge text-bg-secondary rounded-pill d-none"></span>
27
+ <span data-async-values-target="value" data-async-values-key="batches_count" class="badge text-bg-secondary rounded-pill d-none" id="navbar_batches_count" data-turbo-permanent></span>
28
28
  <% end %>
29
29
  </li>
30
30
  <li class="nav-item">
31
31
  <%= link_to cron_entries_path, class: ["nav-link", ("active" if controller_name == 'cron_entries')] do %>
32
32
  <%= t(".cron_schedules") %>
33
- <span data-async-values-target="value" data-async-values-key="cron_entries_count" class="badge text-bg-secondary rounded-pill d-none"></span>
33
+ <span data-async-values-target="value" data-async-values-key="cron_entries_count" class="badge text-bg-secondary rounded-pill d-none" id="navbar_cron_entries_count" data-turbo-permanent></span>
34
34
  <% end %>
35
35
  </li>
36
36
  <li class="nav-item">
37
37
  <%= link_to processes_path, class: ["nav-link", ("active" if controller_name == 'processes')] do %>
38
38
  <%= t(".processes") %>
39
- <span data-async-values-target="value" data-async-values-key="processes_count" data-async-values-zero-class="text-bg-danger" class="badge text-bg-secondary rounded-pill d-none"></span>
39
+ <span data-async-values-target="value" data-async-values-key="processes_count" data-async-values-zero-class="bg-danger" class="badge text-bg-secondary rounded-pill d-none" id="navbar_processes_count" data-turbo-permanent></span>
40
40
  <% end %>
41
41
  </li>
42
42
  <li class="nav-item">
@@ -47,13 +47,13 @@
47
47
  <li class="nav-item">
48
48
  <%= link_to pauses_path, class: ["nav-link", ("active" if controller_name == 'pauses')] do %>
49
49
  <%= t(".pauses") %>
50
- <span data-async-values-target="value" data-async-values-key="pauses_count" data-async-values-zero-class="d-none" class="badge text-bg-warning rounded-pill d-none"></span>
50
+ <span data-async-values-target="value" data-async-values-key="pauses_count" data-async-values-zero-class="d-none" class="badge text-bg-warning rounded-pill d-none" id="navbar_pauses_count" data-turbo-permanent></span>
51
51
  <% end %>
52
52
  </li>
53
53
  <li class="nav-item">
54
54
  <%= link_to cleaner_index_path, class: ["nav-link", ("active" if controller_name == 'cleaner')] do %>
55
55
  <%= t(".cleaner") %>
56
- <span data-async-values-target="value" data-async-values-key="discarded_count" class="badge text-bg-secondary rounded-pill d-none"></span>
56
+ <span data-async-values-target="value" data-async-values-key="discarded_count" class="badge text-bg-secondary rounded-pill d-none" id="navbar_discarded_count" data-turbo-permanent></span>
57
57
  <% end %>
58
58
  </li>
59
59
  </ul>
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="<%= I18n.locale %>" data-bs-theme="auto">
2
+ <html lang="<%= I18n.locale %>" data-bs-theme="auto" data-turbo-unloaded>
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
@@ -18,18 +18,20 @@
18
18
  </script>
19
19
 
20
20
  <%# Do not use asset tag helpers to avoid paths being overriden by config.asset_host %>
21
- <%= tag.link rel: "stylesheet", href: frontend_static_path(:bootstrap, format: :css, locale: nil), nonce: content_security_policy_nonce %>
22
- <%= tag.link rel: "stylesheet", href: frontend_static_path(:style, format: :css, locale: nil), nonce: content_security_policy_nonce %>
21
+ <%= tag.link rel: "stylesheet", href: frontend_static_path(:bootstrap, format: :css, locale: nil), nonce: content_security_policy_nonce, data: { turbo_track: "reload" } %>
22
+ <%= tag.link rel: "stylesheet", href: frontend_static_path(:style, format: :css, locale: nil), nonce: content_security_policy_nonce, data: { turbo_track: "reload" } %>
23
23
  <%= tag.script "", src: frontend_static_path(:bootstrap, format: :js, locale: nil), nonce: content_security_policy_nonce %>
24
24
  <%= tag.script "", src: frontend_static_path(:chartjs, format: :js, locale: nil), nonce: content_security_policy_nonce %>
25
- <%= tag.script "", src: frontend_static_path(:rails_ujs, format: :js, locale: nil), nonce: content_security_policy_nonce %>
26
25
  <%= tag.script "", src: frontend_static_path(:es_module_shims, format: :js, locale: nil), async: true, nonce: content_security_policy_nonce %>
26
+
27
27
  <% importmaps = GoodJob::FrontendsController.js_modules.keys.index_with { |module_name| frontend_module_path(module_name, format: :js, locale: nil) } %>
28
- <%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce) %>
29
- <%= tag.script "", type: "module", nonce: content_security_policy_nonce do %> import "application"; <% end %>
28
+ <%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce, data: { turbo_track: "reload" }) %>
29
+ <% importmaps.each do |_module_name, path| %>
30
+ <%= tag.link rel: "modulepreload", href: path, nonce: content_security_policy_nonce %>
31
+ <% end %>
32
+
33
+ <%= tag.script "import \"application\";".html_safe, type: "module", nonce: content_security_policy_nonce %>
30
34
 
31
- <title>Good Job Dashboard</title>
32
- <%= tag.link rel: "icon", href: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 0 100 100"><text y=".90em" font-size="90">👍</text></svg>' %>
33
35
  <%= csrf_meta_tags %>
34
36
  <%= csp_meta_tag %>
35
37
 
@@ -7,7 +7,7 @@
7
7
  "check_name": "Render",
8
8
  "message": "Render path contains parameter value",
9
9
  "file": "app/controllers/good_job/frontends_controller.rb",
10
- "line": 47,
10
+ "line": 52,
11
11
  "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
12
12
  "code": "render(file => (self.class.js_modules[params[:id].to_sym] or raise(ActionController::RoutingError, \"Not Found\")), {})",
13
13
  "render_path": null,
@@ -23,29 +23,6 @@
23
23
  ],
24
24
  "note": "Files are explicitly enumerated in the array"
25
25
  },
26
- {
27
- "warning_type": "Dynamic Render Path",
28
- "warning_code": 15,
29
- "fingerprint": "b0c2888c9b217671d90d0141b49b036af3b2a70c63b02968cc97ae2052c86272",
30
- "check_name": "Render",
31
- "message": "Render path contains parameter value",
32
- "file": "app/controllers/good_job/frontends_controller.rb",
33
- "line": 41,
34
- "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
35
- "code": "render(file => ({ :css => ({ :bootstrap => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"bootstrap\", \"bootstrap.min.css\"), :style => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"style.css\") }), :js => ({ :bootstrap => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"bootstrap\", \"bootstrap.bundle.min.js\"), :chartjs => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"chartjs\", \"chart.min.js\"), :es_module_shims => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"es_module_shims.js\"), :rails_ujs => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"rails_ujs.js\") }), :svg => ({ :icons => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"icons.svg\") }) }.dig(params[:format].to_sym, params[:id].to_sym) or raise(ActionController::RoutingError, \"Not Found\")), {})",
36
- "render_path": null,
37
- "location": {
38
- "type": "method",
39
- "class": "GoodJob::FrontendsController",
40
- "method": "static"
41
- },
42
- "user_input": "params[:id].to_sym",
43
- "confidence": "Weak",
44
- "cwe_id": [
45
- 22
46
- ],
47
- "note": "Files are explicitly enumerated in the array"
48
- },
49
26
  {
50
27
  "warning_type": "Dangerous Eval",
51
28
  "warning_code": 13,
@@ -115,7 +92,30 @@
115
92
  77
116
93
  ],
117
94
  "note": ""
95
+ },
96
+ {
97
+ "warning_type": "Dynamic Render Path",
98
+ "warning_code": 15,
99
+ "fingerprint": "ebc35a48169b7ee1f2e1b024f3bf6abe7138924a3a318ab2ad405c2b6e9bbb55",
100
+ "check_name": "Render",
101
+ "message": "Render path contains parameter value",
102
+ "file": "app/controllers/good_job/frontends_controller.rb",
103
+ "line": 46,
104
+ "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
105
+ "code": "render(file => ({ :css => ({ :bootstrap => asset_path(\"vendor\", \"bootstrap\", \"bootstrap.min.css\"), :style => asset_path(\"style.css\") }), :js => ({ :bootstrap => asset_path(\"vendor\", \"bootstrap\", \"bootstrap.bundle.min.js\"), :chartjs => asset_path(\"vendor\", \"chartjs\", \"chart.min.js\"), :es_module_shims => asset_path(\"vendor\", \"es_module_shims.js\"), :turbo => asset_path(\"vendor\", \"turbo.js\") }), :svg => ({ :icons => asset_path(\"icons.svg\") }) }.dig(params[:format].to_sym, params[:id].to_sym) or raise(ActionController::RoutingError, \"Not Found\")), {})",
106
+ "render_path": null,
107
+ "location": {
108
+ "type": "method",
109
+ "class": "GoodJob::FrontendsController",
110
+ "method": "static"
111
+ },
112
+ "user_input": "params[:id].to_sym",
113
+ "confidence": "Weak",
114
+ "cwe_id": [
115
+ 22
116
+ ],
117
+ "note": ""
118
118
  }
119
119
  ],
120
- "brakeman_version": "7.0.2"
120
+ "brakeman_version": "7.1.1"
121
121
  }
@@ -85,7 +85,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
85
85
  add_index :good_jobs, [:concurrency_key, :created_at], name: :index_good_jobs_on_concurrency_key_and_created_at
86
86
  add_index :good_jobs, [:cron_key, :created_at], where: "(cron_key IS NOT NULL)", name: :index_good_jobs_on_cron_key_and_created_at_cond
87
87
  add_index :good_jobs, [:cron_key, :cron_at], where: "(cron_key IS NOT NULL)", unique: true, name: :index_good_jobs_on_cron_key_and_cron_at_cond
88
- add_index :good_jobs, [:finished_at], where: "retried_good_job_id IS NULL AND finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at
88
+ add_index :good_jobs, [:finished_at], where: "finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at_only
89
89
  add_index :good_jobs, [:priority, :created_at], order: { priority: "DESC NULLS LAST", created_at: :asc },
90
90
  where: "finished_at IS NULL", name: :index_good_jobs_jobs_on_priority_created_at_when_unfinished
91
91
  add_index :good_jobs, [:priority, :created_at], order: { priority: "ASC NULLS LAST", created_at: :asc },
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddIndexGoodJobsFinishedAtForCleanup < ActiveRecord::Migration<%= migration_version %>
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ reversible do |dir|
8
+ dir.up do
9
+ # Ensure this incremental update migration is idempotent
10
+ # with monolithic install migration.
11
+ return if connection.index_exists? :good_jobs, [:finished_at], name: :index_good_jobs_jobs_on_finished_at_only
12
+ end
13
+ end
14
+
15
+ add_index :good_jobs, [:finished_at], where: "finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at_only, algorithm: :concurrently
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveExtraneousFinishedAtIndex < ActiveRecord::Migration<%= migration_version %>
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ reversible do |dir|
8
+ dir.up do
9
+ # Ensure this incremental update migration is idempotent
10
+ # with monolithic install migration.
11
+ return unless connection.index_exists? :good_jobs, [:finished_at], name: :index_good_jobs_jobs_on_finished_at
12
+ end
13
+ end
14
+
15
+ remove_index :good_jobs, [:finished_at], where: "retried_good_job_id IS NULL AND finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at, algorithm: :concurrently
16
+ end
17
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '4.13.0'
5
+ VERSION = '4.13.1'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
data/lib/good_job.rb CHANGED
@@ -290,7 +290,7 @@ module GoodJob
290
290
  # For use in tests/CI to validate GoodJob is up-to-date.
291
291
  # @return [Boolean]
292
292
  def self.migrated?
293
- GoodJob::Job.job_class_index_migrated?
293
+ GoodJob::Job.historic_finished_at_index_migrated?
294
294
  end
295
295
 
296
296
  # Pause job execution for a given queue or job class.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.13.0
4
+ version: 4.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
@@ -269,7 +269,7 @@ files:
269
269
  - app/frontend/good_job/modules/async_values_controller.js
270
270
  - app/frontend/good_job/modules/charts.js
271
271
  - app/frontend/good_job/modules/checkbox_toggle.js
272
- - app/frontend/good_job/modules/document_ready.js
272
+ - app/frontend/good_job/modules/form_controller.js
273
273
  - app/frontend/good_job/modules/html_legend_plugin.js
274
274
  - app/frontend/good_job/modules/live_poll.js
275
275
  - app/frontend/good_job/modules/popovers.js
@@ -280,8 +280,8 @@ files:
280
280
  - app/frontend/good_job/vendor/bootstrap/bootstrap.min.css
281
281
  - app/frontend/good_job/vendor/chartjs/chart.min.js
282
282
  - app/frontend/good_job/vendor/es_module_shims.js
283
- - app/frontend/good_job/vendor/rails_ujs.js
284
283
  - app/frontend/good_job/vendor/stimulus.js
284
+ - app/frontend/good_job/vendor/turbo.js
285
285
  - app/helpers/good_job/application_helper.rb
286
286
  - app/helpers/good_job/icons_helper.rb
287
287
  - app/models/concerns/good_job/advisory_lockable.rb
@@ -352,6 +352,8 @@ files:
352
352
  - lib/generators/good_job/templates/update/migrations/02_add_jobs_finished_at_to_good_job_batches.rb.erb
353
353
  - lib/generators/good_job/templates/update/migrations/03_add_index_good_jobs_concurrency_key_created_at.rb.erb
354
354
  - lib/generators/good_job/templates/update/migrations/04_add_index_good_jobs_job_class.rb.erb
355
+ - lib/generators/good_job/templates/update/migrations/05_add_index_good_jobs_finished_at_for_cleanup.rb.erb
356
+ - lib/generators/good_job/templates/update/migrations/06_remove_extraneous_finished_at_index.rb.erb
355
357
  - lib/generators/good_job/update_generator.rb
356
358
  - lib/good_job.rb
357
359
  - lib/good_job/active_job_extensions/batches.rb
@@ -1,7 +0,0 @@
1
- export default function documentReady(callback) {
2
- if (document.readyState !== "loading") {
3
- callback();
4
- } else {
5
- document.addEventListener("DOMContentLoaded", callback);
6
- }
7
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Minified by jsDelivr using Terser v5.15.1.
3
- * Original file: /npm/@rails/ujs@7.0.4-2/lib/assets/compiled/rails-ujs.js
4
- *
5
- * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6
- */
7
- (function(){(function(){(function(){this.Rails={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]",buttonClickSelector:{selector:"button[data-remote]:not([form]), button[data-confirm]:not([form])",exclude:"form button"},inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form:not([data-turbo=true])",formInputClickSelector:"form:not([data-turbo=true]) input[type=submit], form:not([data-turbo=true]) input[type=image], form:not([data-turbo=true]) button[type=submit], form:not([data-turbo=true]) button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])",formDisableSelector:"input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled",formEnableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled",fileInputSelector:"input[name][type=file]:not([disabled])",linkDisableSelector:"a[data-disable-with], a[data-disable]",buttonDisableSelector:"button[data-remote][data-disable-with], button[data-remote][data-disable]"}}).call(this)}).call(this);var t=this.Rails;(function(){(function(){var e;e=null,t.loadCSPNonce=function(){var t;return e=null!=(t=document.querySelector("meta[name=csp-nonce]"))?t.content:void 0},t.cspNonce=function(){return null!=e?e:t.loadCSPNonce()}}).call(this),function(){var e;e=Element.prototype.matches||Element.prototype.matchesSelector||Element.prototype.mozMatchesSelector||Element.prototype.msMatchesSelector||Element.prototype.oMatchesSelector||Element.prototype.webkitMatchesSelector,t.matches=function(t,n){return null!=n.exclude?e.call(t,n.selector)&&!e.call(t,n.exclude):e.call(t,n)},t.getData=function(t,e){var n;return null!=(n=t._ujsData)?n[e]:void 0},t.setData=function(t,e,n){return null==t._ujsData&&(t._ujsData={}),t._ujsData[e]=n},t.$=function(t){return Array.prototype.slice.call(document.querySelectorAll(t))}}.call(this),function(){var e,n,a;e=t.$,a=t.csrfToken=function(){var t;return(t=document.querySelector("meta[name=csrf-token]"))&&t.content},n=t.csrfParam=function(){var t;return(t=document.querySelector("meta[name=csrf-param]"))&&t.content},t.CSRFProtection=function(t){var e;if(null!=(e=a()))return t.setRequestHeader("X-CSRF-Token",e)},t.refreshCSRFTokens=function(){var t,r;if(r=a(),t=n(),null!=r&&null!=t)return e('form input[name="'+t+'"]').forEach((function(t){return t.value=r}))}}.call(this),function(){var e,n,a,r;a=t.matches,"function"!=typeof(e=window.CustomEvent)&&((e=function(t,e){var n;return(n=document.createEvent("CustomEvent")).initCustomEvent(t,e.bubbles,e.cancelable,e.detail),n}).prototype=window.Event.prototype,r=e.prototype.preventDefault,e.prototype.preventDefault=function(){var t;return t=r.call(this),this.cancelable&&!this.defaultPrevented&&Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}}),t}),n=t.fire=function(t,n,a){var r;return r=new e(n,{bubbles:!0,cancelable:!0,detail:a}),t.dispatchEvent(r),!r.defaultPrevented},t.stopEverything=function(t){return n(t.target,"ujs:everythingStopped"),t.preventDefault(),t.stopPropagation(),t.stopImmediatePropagation()},t.delegate=function(t,e,n,r){return t.addEventListener(n,(function(t){var n;for(n=t.target;n instanceof Element&&!a(n,e);)n=n.parentNode;if(n instanceof Element&&!1===r.call(n,t))return t.preventDefault(),t.stopPropagation()}))}}.call(this),function(){var e,n,a,r,o,i;r=t.cspNonce,n=t.CSRFProtection,t.fire,e={"*":"*/*",text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript",script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},t.ajax=function(t){var e;return t=o(t),e=a(t,(function(){var n,a;return a=i(null!=(n=e.response)?n:e.responseText,e.getResponseHeader("Content-Type")),2===Math.floor(e.status/100)?"function"==typeof t.success&&t.success(a,e.statusText,e):"function"==typeof t.error&&t.error(a,e.statusText,e),"function"==typeof t.complete?t.complete(e,e.statusText):void 0})),!(null!=t.beforeSend&&!t.beforeSend(e,t))&&(e.readyState===XMLHttpRequest.OPENED?e.send(t.data):void 0)},o=function(t){return t.url=t.url||location.href,t.type=t.type.toUpperCase(),"GET"===t.type&&t.data&&(t.url.indexOf("?")<0?t.url+="?"+t.data:t.url+="&"+t.data),null==e[t.dataType]&&(t.dataType="*"),t.accept=e[t.dataType],"*"!==t.dataType&&(t.accept+=", */*; q=0.01"),t},a=function(t,e){var a;return(a=new XMLHttpRequest).open(t.type,t.url,!0),a.setRequestHeader("Accept",t.accept),"string"==typeof t.data&&a.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8"),t.crossDomain||(a.setRequestHeader("X-Requested-With","XMLHttpRequest"),n(a)),a.withCredentials=!!t.withCredentials,a.onreadystatechange=function(){if(a.readyState===XMLHttpRequest.DONE)return e(a)},a},i=function(t,e){var n,a;if("string"==typeof t&&"string"==typeof e)if(e.match(/\bjson\b/))try{t=JSON.parse(t)}catch(t){}else if(e.match(/\b(?:java|ecma)script\b/))(a=document.createElement("script")).setAttribute("nonce",r()),a.text=t,document.head.appendChild(a).parentNode.removeChild(a);else if(e.match(/\b(xml|html|svg)\b/)){n=new DOMParser,e=e.replace(/;.+/,"");try{t=n.parseFromString(t,e)}catch(t){}}return t},t.href=function(t){return t.href},t.isCrossDomain=function(t){var e,n;(e=document.createElement("a")).href=location.href,n=document.createElement("a");try{return n.href=t,!((!n.protocol||":"===n.protocol)&&!n.host||e.protocol+"//"+e.host==n.protocol+"//"+n.host)}catch(t){return t,!0}}}.call(this),function(){var e,n;e=t.matches,n=function(t){return Array.prototype.slice.call(t)},t.serializeElement=function(t,a){var r,o;return r=[t],e(t,"form")&&(r=n(t.elements)),o=[],r.forEach((function(t){if(t.name&&!t.disabled&&!e(t,"fieldset[disabled] *"))return e(t,"select")?n(t.options).forEach((function(e){if(e.selected)return o.push({name:t.name,value:e.value})})):t.checked||-1===["radio","checkbox","submit"].indexOf(t.type)?o.push({name:t.name,value:t.value}):void 0})),a&&o.push(a),o.map((function(t){return null!=t.name?encodeURIComponent(t.name)+"="+encodeURIComponent(t.value):t})).join("&")},t.formElements=function(t,a){return e(t,"form")?n(t.elements).filter((function(t){return e(t,a)})):n(t.querySelectorAll(a))}}.call(this),function(){var e,n,a;n=t.fire,a=t.stopEverything,t.handleConfirm=function(t){if(!e(this))return a(t)},t.confirm=function(t,e){return confirm(t)},e=function(e){var a,r,o;if(!(o=e.getAttribute("data-confirm")))return!0;if(a=!1,n(e,"confirm")){try{a=t.confirm(o,e)}catch(t){}r=n(e,"confirm:complete",[a])}return a&&r}}.call(this),function(){var e,n,a,r,o,i,l,u,c,s,d,m;s=t.matches,u=t.getData,d=t.setData,m=t.stopEverything,l=t.formElements,t.handleDisabledElement=function(t){if(this,this.disabled)return m(t)},t.enableElement=function(e){var n;if(e instanceof Event){if(c(e))return;n=e.target}else n=e;return s(n,t.linkDisableSelector)?i(n):s(n,t.buttonDisableSelector)||s(n,t.formEnableSelector)?r(n):s(n,t.formSubmitSelector)?o(n):void 0},t.disableElement=function(r){var o;return o=r instanceof Event?r.target:r,s(o,t.linkDisableSelector)?a(o):s(o,t.buttonDisableSelector)||s(o,t.formDisableSelector)?e(o):s(o,t.formSubmitSelector)?n(o):void 0},a=function(t){var e;if(!u(t,"ujs:disabled"))return null!=(e=t.getAttribute("data-disable-with"))&&(d(t,"ujs:enable-with",t.innerHTML),t.innerHTML=e),t.addEventListener("click",m),d(t,"ujs:disabled",!0)},i=function(t){var e;return null!=(e=u(t,"ujs:enable-with"))&&(t.innerHTML=e,d(t,"ujs:enable-with",null)),t.removeEventListener("click",m),d(t,"ujs:disabled",null)},n=function(n){return l(n,t.formDisableSelector).forEach(e)},e=function(t){var e;if(!u(t,"ujs:disabled"))return null!=(e=t.getAttribute("data-disable-with"))&&(s(t,"button")?(d(t,"ujs:enable-with",t.innerHTML),t.innerHTML=e):(d(t,"ujs:enable-with",t.value),t.value=e)),t.disabled=!0,d(t,"ujs:disabled",!0)},o=function(e){return l(e,t.formEnableSelector).forEach(r)},r=function(t){var e;return null!=(e=u(t,"ujs:enable-with"))&&(s(t,"button")?t.innerHTML=e:t.value=e,d(t,"ujs:enable-with",null)),t.disabled=!1,d(t,"ujs:disabled",null)},c=function(t){var e,n;return null!=(null!=(n=null!=(e=t.detail)?e[0]:void 0)?n.getResponseHeader("X-Xhr-Redirect"):void 0)}}.call(this),function(){var e;e=t.stopEverything,t.handleMethod=function(n){var a,r,o,i,l,u,c;if(c=(u=this).getAttribute("data-method"))return l=t.href(u),r=t.csrfToken(),a=t.csrfParam(),o=document.createElement("form"),i="<input name='_method' value='"+c+"' type='hidden' />",null==a||null==r||t.isCrossDomain(l)||(i+="<input name='"+a+"' value='"+r+"' type='hidden' />"),i+='<input type="submit" />',o.method="post",o.action=l,o.target=u.target,o.innerHTML=i,o.style.display="none",document.body.appendChild(o),o.querySelector('[type="submit"]').click(),e(n)}}.call(this),function(){var e,n,a,r,o,i,l,u,c,s=[].slice;i=t.matches,a=t.getData,u=t.setData,n=t.fire,c=t.stopEverything,e=t.ajax,r=t.isCrossDomain,l=t.serializeElement,o=function(t){var e;return null!=(e=t.getAttribute("data-remote"))&&"false"!==e},t.handleRemote=function(d){var m,f,p,b,h,v,S;return!o(b=this)||(n(b,"ajax:before")?(S=b.getAttribute("data-with-credentials"),p=b.getAttribute("data-type")||"script",i(b,t.formSubmitSelector)?(m=a(b,"ujs:submit-button"),h=a(b,"ujs:submit-button-formmethod")||b.method,v=a(b,"ujs:submit-button-formaction")||b.getAttribute("action")||location.href,"GET"===h.toUpperCase()&&(v=v.replace(/\?.*$/,"")),"multipart/form-data"===b.enctype?(f=new FormData(b),null!=m&&f.append(m.name,m.value)):f=l(b,m),u(b,"ujs:submit-button",null),u(b,"ujs:submit-button-formmethod",null),u(b,"ujs:submit-button-formaction",null)):i(b,t.buttonClickSelector)||i(b,t.inputChangeSelector)?(h=b.getAttribute("data-method"),v=b.getAttribute("data-url"),f=l(b,b.getAttribute("data-params"))):(h=b.getAttribute("data-method"),v=t.href(b),f=b.getAttribute("data-params")),e({type:h||"GET",url:v,data:f,dataType:p,beforeSend:function(t,e){return n(b,"ajax:beforeSend",[t,e])?n(b,"ajax:send",[t]):(n(b,"ajax:stopped"),!1)},success:function(){var t;return t=1<=arguments.length?s.call(arguments,0):[],n(b,"ajax:success",t)},error:function(){var t;return t=1<=arguments.length?s.call(arguments,0):[],n(b,"ajax:error",t)},complete:function(){var t;return t=1<=arguments.length?s.call(arguments,0):[],n(b,"ajax:complete",t)},crossDomain:r(v),withCredentials:null!=S&&"false"!==S}),c(d)):(n(b,"ajax:stopped"),!1))},t.formSubmitButtonClick=function(t){var e,n;if(n=(e=this).form)return e.name&&u(n,"ujs:submit-button",{name:e.name,value:e.value}),u(n,"ujs:formnovalidate-button",e.formNoValidate),u(n,"ujs:submit-button-formaction",e.getAttribute("formaction")),u(n,"ujs:submit-button-formmethod",e.getAttribute("formmethod"))},t.preventInsignificantClick=function(t){var e,n,a;if(this,a=(this.getAttribute("data-method")||"GET").toUpperCase(),e=this.getAttribute("data-params"),n=(t.metaKey||t.ctrlKey)&&"GET"===a&&!e,null!=t.button&&0!==t.button||n)return t.stopImmediatePropagation()}}.call(this),function(){var e,n,a,r,o,i,l,u,c,s,d,m,f,p,b;if(i=t.fire,a=t.delegate,u=t.getData,e=t.$,b=t.refreshCSRFTokens,n=t.CSRFProtection,f=t.loadCSPNonce,o=t.enableElement,r=t.disableElement,s=t.handleDisabledElement,c=t.handleConfirm,p=t.preventInsignificantClick,m=t.handleRemote,l=t.formSubmitButtonClick,d=t.handleMethod,"undefined"!=typeof jQuery&&null!==jQuery&&null!=jQuery.ajax){if(jQuery.rails)throw new Error("If you load both jquery_ujs and rails-ujs, use rails-ujs only.");jQuery.rails=t,jQuery.ajaxPrefilter((function(t,e,a){if(!t.crossDomain)return n(a)}))}t.start=function(){if(window._rails_loaded)throw new Error("rails-ujs has already been loaded!");return window.addEventListener("pageshow",(function(){return e(t.formEnableSelector).forEach((function(t){if(u(t,"ujs:disabled"))return o(t)})),e(t.linkDisableSelector).forEach((function(t){if(u(t,"ujs:disabled"))return o(t)}))})),a(document,t.linkDisableSelector,"ajax:complete",o),a(document,t.linkDisableSelector,"ajax:stopped",o),a(document,t.buttonDisableSelector,"ajax:complete",o),a(document,t.buttonDisableSelector,"ajax:stopped",o),a(document,t.linkClickSelector,"click",p),a(document,t.linkClickSelector,"click",s),a(document,t.linkClickSelector,"click",c),a(document,t.linkClickSelector,"click",r),a(document,t.linkClickSelector,"click",m),a(document,t.linkClickSelector,"click",d),a(document,t.buttonClickSelector,"click",p),a(document,t.buttonClickSelector,"click",s),a(document,t.buttonClickSelector,"click",c),a(document,t.buttonClickSelector,"click",r),a(document,t.buttonClickSelector,"click",m),a(document,t.inputChangeSelector,"change",s),a(document,t.inputChangeSelector,"change",c),a(document,t.inputChangeSelector,"change",m),a(document,t.formSubmitSelector,"submit",s),a(document,t.formSubmitSelector,"submit",c),a(document,t.formSubmitSelector,"submit",m),a(document,t.formSubmitSelector,"submit",(function(t){return setTimeout((function(){return r(t)}),13)})),a(document,t.formSubmitSelector,"ajax:send",r),a(document,t.formSubmitSelector,"ajax:complete",o),a(document,t.formInputClickSelector,"click",p),a(document,t.formInputClickSelector,"click",s),a(document,t.formInputClickSelector,"click",c),a(document,t.formInputClickSelector,"click",l),document.addEventListener("DOMContentLoaded",b),document.addEventListener("DOMContentLoaded",f),window._rails_loaded=!0},window.Rails===t&&i(document,"rails:attachBindings")&&t.start()}.call(this)}).call(this),"object"==typeof module&&module.exports?module.exports=t:"function"==typeof define&&define.amd&&define(t)}).call(this);