plan_my_stuff 0.7.0 → 0.8.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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/README.md +100 -103
  4. data/app/controllers/plan_my_stuff/application_controller.rb +22 -3
  5. data/app/controllers/plan_my_stuff/comments_controller.rb +14 -16
  6. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +23 -13
  7. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +7 -5
  8. data/app/controllers/plan_my_stuff/issues/links_controller.rb +14 -18
  9. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +99 -28
  10. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +13 -5
  11. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +7 -5
  12. data/app/controllers/plan_my_stuff/issues_controller.rb +24 -28
  13. data/app/controllers/plan_my_stuff/labels_controller.rb +21 -5
  14. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +13 -6
  15. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +5 -4
  16. data/app/controllers/plan_my_stuff/project_items_controller.rb +30 -5
  17. data/app/controllers/plan_my_stuff/projects_controller.rb +16 -16
  18. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +21 -11
  19. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +9 -4
  20. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +30 -14
  21. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +50 -17
  22. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +32 -49
  23. data/app/jobs/plan_my_stuff/application_job.rb +2 -3
  24. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +15 -22
  25. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  26. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +1 -0
  27. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  28. data/app/views/plan_my_stuff/issues/index.html.erb +2 -2
  29. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  30. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +23 -2
  31. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +1 -0
  32. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -1
  33. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +50 -7
  34. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -1
  35. data/app/views/plan_my_stuff/issues/show.html.erb +5 -2
  36. data/app/views/plan_my_stuff/partials/_flash.html.erb +4 -0
  37. data/app/views/plan_my_stuff/projects/edit.html.erb +1 -3
  38. data/app/views/plan_my_stuff/projects/index.html.erb +1 -1
  39. data/app/views/plan_my_stuff/projects/new.html.erb +1 -3
  40. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +1 -0
  41. data/app/views/plan_my_stuff/projects/show.html.erb +13 -3
  42. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +1 -3
  43. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +1 -3
  44. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +1 -3
  45. data/app/views/plan_my_stuff/testing_projects/new.html.erb +1 -3
  46. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +4 -3
  47. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +1 -0
  48. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +1 -0
  49. data/app/views/plan_my_stuff/testing_projects/show.html.erb +2 -2
  50. data/config/routes.rb +2 -2
  51. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +51 -2
  52. data/lib/plan_my_stuff/approval.rb +12 -4
  53. data/lib/plan_my_stuff/aws_sns_simulator.rb +12 -6
  54. data/lib/plan_my_stuff/base_metadata.rb +4 -15
  55. data/lib/plan_my_stuff/base_project.rb +68 -55
  56. data/lib/plan_my_stuff/base_project_item.rb +61 -57
  57. data/lib/plan_my_stuff/base_project_metadata.rb +1 -1
  58. data/lib/plan_my_stuff/client.rb +136 -48
  59. data/lib/plan_my_stuff/comment.rb +57 -57
  60. data/lib/plan_my_stuff/comment_metadata.rb +1 -1
  61. data/lib/plan_my_stuff/configuration.rb +95 -82
  62. data/lib/plan_my_stuff/errors.rb +10 -10
  63. data/lib/plan_my_stuff/graphql/queries.rb +1 -1
  64. data/lib/plan_my_stuff/issue.rb +467 -333
  65. data/lib/plan_my_stuff/issue_metadata.rb +10 -10
  66. data/lib/plan_my_stuff/label.rb +32 -16
  67. data/lib/plan_my_stuff/link.rb +15 -15
  68. data/lib/plan_my_stuff/markdown.rb +12 -6
  69. data/lib/plan_my_stuff/metadata_parser.rb +3 -1
  70. data/lib/plan_my_stuff/notifications.rb +1 -1
  71. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +2 -2
  72. data/lib/plan_my_stuff/pipeline/issue_linker.rb +1 -1
  73. data/lib/plan_my_stuff/pipeline.rb +61 -83
  74. data/lib/plan_my_stuff/project.rb +4 -4
  75. data/lib/plan_my_stuff/project_item_metadata.rb +1 -1
  76. data/lib/plan_my_stuff/project_metadata.rb +1 -1
  77. data/lib/plan_my_stuff/reminders/closer.rb +1 -1
  78. data/lib/plan_my_stuff/reminders/fire.rb +3 -3
  79. data/lib/plan_my_stuff/reminders/sweep.rb +4 -4
  80. data/lib/plan_my_stuff/repo.rb +12 -6
  81. data/lib/plan_my_stuff/test_helpers.rb +11 -11
  82. data/lib/plan_my_stuff/testing_project.rb +12 -11
  83. data/lib/plan_my_stuff/testing_project_item.rb +11 -9
  84. data/lib/plan_my_stuff/testing_project_metadata.rb +2 -2
  85. data/lib/plan_my_stuff/version.rb +1 -1
  86. data/lib/plan_my_stuff/webhook_replayer.rb +14 -2
  87. data/lib/plan_my_stuff.rb +26 -2
  88. data/lib/tasks/plan_my_stuff.rake +33 -20
  89. metadata +3 -2
@@ -5,7 +5,7 @@ require 'openssl'
5
5
  module PlanMyStuff
6
6
  module Webhooks
7
7
  class GithubController < ActionController::API
8
- before_action :verify_signature!
8
+ before_action :verify_signature
9
9
 
10
10
  # POST /webhooks/github
11
11
  def create
@@ -29,7 +29,7 @@ module PlanMyStuff
29
29
  #
30
30
  # @return [void]
31
31
  #
32
- def verify_signature!
32
+ def verify_signature
33
33
  body = request.body.read
34
34
  request.body.rewind
35
35
 
@@ -81,21 +81,15 @@ module PlanMyStuff
81
81
  end
82
82
  end
83
83
 
84
- # Adds the issue to the pipeline project at "Started" the first
85
- # time it is assigned. Re-assigns and additional assignees are
86
- # ignored -- if the issue is already in the pipeline we do not
87
- # touch its status.
84
+ # Adds the issue to the pipeline project at "Started" the first time it is assigned. Re-assigns and
85
+ # additional assignees are ignored -- if the issue is already in the pipeline we do not touch its status.
88
86
  #
89
87
  # Skipped paths (no project item is created):
90
- # - Issue is closed (assignment changes on a closed issue
91
- # must not alter the pipeline)
92
- # - Issue has pending approvals (creating + +take!+ would
93
- # either orphan an item or 500 on the approval guard)
88
+ # - Issue is closed (assignment changes on a closed issue must not alter the pipeline)
89
+ # - Issue has pending approvals (creating + +take!+ would either orphan an item or 500 on the approval guard)
94
90
  #
95
- # GitHub already records the issue assignment (that's what
96
- # triggered this webhook), so the gem does not call +assign!+
97
- # on the project item -- that would clobber co-assignees on
98
- # the underlying issue.
91
+ # GitHub already records the issue assignment (that's what triggered this webhook), so the gem does not
92
+ # call +assign!+ on the project item -- that would clobber co-assignees on the underlying issue.
99
93
  #
100
94
  # @return [void]
101
95
  #
@@ -123,21 +117,18 @@ module PlanMyStuff
123
117
  return
124
118
  end
125
119
 
126
- number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
120
+ number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
127
121
  project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number)
128
122
  PlanMyStuff::Pipeline.take!(project_item)
129
123
  end
130
124
 
131
- # Removes the issue from the pipeline project when the LAST
132
- # assignee is removed. If any assignees remain, the webhook is
133
- # a no-op (only one of N was removed). If the issue isn't in
134
- # the pipeline at all, also a no-op (logged at info).
125
+ # Removes the issue from the pipeline project when the LAST assignee is removed. If any assignees remain,
126
+ # the webhook is a no-op (only one of N was removed). If the issue isn't in the pipeline at all, also a
127
+ # no-op (logged at info).
135
128
  #
136
- # Closed issues are skipped: assignment changes on a closed
137
- # issue must not alter the pipeline. Items already on the
138
- # release cycle (Ready for Release, Release in Progress,
139
- # Completed) are also locked -- once on the release path an
140
- # item should not come off via webhook.
129
+ # Closed issues are skipped: assignment changes on a closed issue must not alter the pipeline. Items
130
+ # already on the release cycle (Ready for Release, Release in Progress, Completed) are also locked --
131
+ # once on the release path an item should not come off via webhook.
141
132
  #
142
133
  # @return [void]
143
134
  #
@@ -145,7 +136,7 @@ module PlanMyStuff
145
136
  return if issue_params[:state] == 'closed'
146
137
 
147
138
  remaining_assignees = Array.wrap(issue_params[:assignees])
148
- return if remaining_assignees.any?
139
+ return if remaining_assignees.present?
149
140
 
150
141
  issue_number = issue_params.fetch(:number)
151
142
  project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
@@ -169,16 +160,13 @@ module PlanMyStuff
169
160
  PlanMyStuff::Pipeline.remove!(project_item)
170
161
  end
171
162
 
172
- # Handles GitHub's +projects_v2_item+ webhook event. Mirrors a dev
173
- # dragging an item from "Submitted" to "Started" on the project
174
- # board into a +Pipeline.take!+ call so the
175
- # +plan_my_stuff.pipeline.started+ event fires.
163
+ # Handles GitHub's +projects_v2_item+ webhook event. Mirrors a dev dragging an item from "Submitted" to
164
+ # "Started" on the project board into a +Pipeline.take!+ call so the +plan_my_stuff.pipeline.started+
165
+ # event fires.
176
166
  #
177
- # Only acts on +action: 'edited'+ where the Status single-select
178
- # field changed on the pipeline project. No-ops when the new
179
- # status is anything other than "Started", when the +from+ status
180
- # is already "Started" (loop guard from +move_to!+ triggering
181
- # another webhook), or when the item cannot be located on the
167
+ # Only acts on +action: 'edited'+ where the Status single-select field changed on the pipeline project.
168
+ # No-ops when the new status is anything other than "Started", when the +from+ status is already "Started"
169
+ # (loop guard from +move_to!+ triggering another webhook), or when the item cannot be located on the
182
170
  # pipeline project.
183
171
  #
184
172
  # @return [void]
@@ -189,7 +177,7 @@ module PlanMyStuff
189
177
  item_project_node_id = payload_params.dig(:projects_v2_item, :project_node_id)
190
178
  return if item_project_node_id.blank?
191
179
 
192
- pipeline_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
180
+ pipeline_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
193
181
  project = PlanMyStuff::Project.find(pipeline_number)
194
182
  return unless project.id == item_project_node_id
195
183
 
@@ -231,11 +219,9 @@ module PlanMyStuff
231
219
  def handle_pull_request
232
220
  action = payload_params.fetch(:action)
233
221
 
234
- # Only PRs targeting +main+ drive pipeline transitions on
235
- # open/draft/reopen. PRs into +production+ are deploy PRs
236
- # (often auto-created as drafts) and must not bump items
237
- # back to "Started". The +closed+ branch handles its own
238
- # base-ref routing.
222
+ # Only PRs targeting +main+ drive pipeline transitions on open/draft/reopen. PRs into +production+ are
223
+ # deploy PRs (often auto-created as drafts) and must not bump items back to "Started". The +closed+
224
+ # branch handles its own base-ref routing.
239
225
  if %w[opened ready_for_review reopened converted_to_draft].include?(action)
240
226
  base_ref = pull_request_params.dig(:base, :ref)
241
227
  return unless base_ref == PlanMyStuff.configuration.main_branch
@@ -253,11 +239,9 @@ module PlanMyStuff
253
239
  end
254
240
  end
255
241
 
256
- # Opening a PR as a draft is a soft "I've started working on this"
257
- # signal. For each linked issue, when the issue is not yet in the
258
- # pipeline, add it at "Started"; when the issue has no assignees,
259
- # assign the PR author. Already-in-pipeline items are NOT moved
260
- # (a draft open is not a status change).
242
+ # Opening a PR as a draft is a soft "I've started working on this" signal. For each linked issue, when
243
+ # the issue is not yet in the pipeline, add it at "Started"; when the issue has no assignees, assign the
244
+ # PR author. Already-in-pipeline items are NOT moved (a draft open is not a status change).
261
245
  #
262
246
  # @return [void]
263
247
  #
@@ -283,9 +267,8 @@ module PlanMyStuff
283
267
  end
284
268
  end
285
269
 
286
- # Adds the issue to the pipeline at "Started". Skipped when the
287
- # issue has pending approvals (Pipeline.take!'s guard would
288
- # otherwise leave an orphan project item behind).
270
+ # Adds the issue to the pipeline at "Started". Skipped when the issue has pending approvals
271
+ # (Pipeline.take!'s guard would otherwise leave an orphan project item behind).
289
272
  #
290
273
  # @param issue [PlanMyStuff::Issue]
291
274
  #
@@ -297,7 +280,7 @@ module PlanMyStuff
297
280
  return
298
281
  end
299
282
 
300
- number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
283
+ number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
301
284
  project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number)
302
285
  PlanMyStuff::Pipeline.take!(project_item)
303
286
  end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- # Base class for all PMS gem jobs. Subclasses +::ActiveJob::Base+ so
5
- # the gem doesn't depend on the consuming app having an
6
- # +ApplicationJob+ constant.
4
+ # Base class for all PMS gem jobs. Subclasses +::ActiveJob::Base+ so the gem doesn't depend on the consuming app
5
+ # having an +ApplicationJob+ constant.
7
6
  class ApplicationJob < ::ActiveJob::Base
8
7
  end
9
8
  end
@@ -1,28 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- # Daily-cadence sweep job for reminder dispatch + inactivity auto-close.
5
- # Consuming apps enqueue it once; the job self-requeues after each
6
- # perform so the schedule persists without requiring a cron/whenever
7
- # setup. Queue-adapter-agnostic: runs through ActiveJob's
8
- # +set(wait_until:).perform_later+ so any backend works.
4
+ # Daily-cadence sweep job for reminder dispatch + inactivity auto-close. Consuming apps enqueue it once; the job
5
+ # self-requeues after each perform so the schedule persists without requiring a cron/whenever setup.
6
+ # Queue-adapter-agnostic: runs through ActiveJob's +set(wait_until:).perform_later+ so any backend works.
9
7
  class RemindersSweepJob < PlanMyStuff::ApplicationJob
10
8
  queue_as :default
11
9
 
12
- # Only try once per enqueue. Without this, Delayed-style adapters
13
- # retry up to 25 times on error and our +around_perform+ +ensure+
14
- # re-enqueues a follow-up run on every attempt, causing geometric
15
- # duplicate pile-up. With +attempts: 1+, exactly one perform per
16
- # +perform_later+; if it fails, the follow-up scheduled in +ensure+
17
- # takes over tomorrow.
10
+ # Only try once per enqueue. Without this, Delayed-style adapters retry up to 25 times on error and our
11
+ # +around_perform+ +ensure+ re-enqueues a follow-up run on every attempt, causing geometric duplicate
12
+ # pile-up. With +attempts: 1+, exactly one perform per +perform_later+; if it fails, the follow-up scheduled
13
+ # in +ensure+ takes over tomorrow.
18
14
  retry_on StandardError, attempts: 1
19
15
 
20
16
  around_perform :requeue_for_next_run
21
17
 
22
18
  class << self
23
- # Next sweep time. Default: 6:30am Eastern tomorrow (today if the
24
- # current time is before 6:30am ET). Apps wanting a different
25
- # cadence override this on a subclass.
19
+ # Next sweep time. Default: 6:30am Eastern tomorrow (today if the current time is before 6:30am ET). Apps
20
+ # wanting a different cadence override this on a subclass.
26
21
  #
27
22
  # @return [Time] UTC
28
23
  #
@@ -33,9 +28,8 @@ module PlanMyStuff
33
28
  target.utc
34
29
  end
35
30
 
36
- # Schedules a sweep for +next_run+. Used by the after-perform
37
- # self-requeue and the +plan_my_stuff:reminders:sweep+ rake task so
38
- # both kick off runs on the same cadence (instead of "now").
31
+ # Schedules a sweep for +next_run+. Used by the after-perform self-requeue and the
32
+ # +plan_my_stuff:reminders:sweep+ rake task so both kick off runs on the same cadence (instead of "now").
39
33
  #
40
34
  # @param repo [Symbol, String]
41
35
  #
@@ -54,15 +48,14 @@ module PlanMyStuff
54
48
  @repo_arg = repo
55
49
  PlanMyStuff::Reminders::Sweep.new(repo: repo).call
56
50
  PlanMyStuff::Archive::Sweep.new(repo: repo).call
57
- PlanMyStuff::Pipeline::CompletedSweep.perform
51
+ PlanMyStuff::Pipeline::CompletedSweep.perform!
58
52
  end
59
53
 
60
54
  private
61
55
 
62
- # Runs inside +around_perform+. Re-enqueues the next run in an
63
- # +ensure+ so a perform error still schedules the next one. Skips
64
- # requeue when perform never captured a repo arg (deserialization
65
- # error or direct +.perform+ call with missing kwargs).
56
+ # Runs inside +around_perform+. Re-enqueues the next run in an +ensure+ so a perform error still schedules
57
+ # the next one. Skips requeue when perform never captured a repo arg (deserialization error or direct
58
+ # +.perform+ call with missing kwargs).
66
59
  #
67
60
  # @return [void]
68
61
  #
@@ -1,8 +1,6 @@
1
1
  <h1>Edit Comment</h1>
2
2
 
3
- <% if flash[:error].present? %>
4
- <p style="color: red;"><%= flash[:error] %></p>
5
- <% end %>
3
+ <%= render({ partial: 'plan_my_stuff/partials/flash' }) %>
6
4
 
7
5
  <%=
8
6
  render({
@@ -1,3 +1,4 @@
1
+ <%# locals: (issue:, comment:, support_user:) %>
1
2
  <%
2
3
  persisted = comment.persisted?
3
4
  url =
@@ -1,10 +1,8 @@
1
1
  <h1>Edit Issue #<%= @issue.number %></h1>
2
2
 
3
- <% if flash[:error].present? %>
4
- <p style="color: red;"><%= flash[:error] %></p>
5
- <% end %>
3
+ <%= render({ partial: 'plan_my_stuff/partials/flash' }) %>
6
4
 
7
- <%= render({ partial: 'plan_my_stuff/issues/partials/form', locals: { issue: @issue, support_user: @support_user } }) %>
5
+ <%= render({ partial: 'plan_my_stuff/issues/partials/form', locals: { issue: @issue } }) %>
8
6
 
9
7
  <% if @support_user %>
10
8
  <hr>
@@ -1,6 +1,6 @@
1
1
  <h1>Issues</h1>
2
2
 
3
- <% if @issues.any? %>
3
+ <% if @issues.present? %>
4
4
  <table>
5
5
  <thead>
6
6
  <tr>
@@ -34,4 +34,4 @@
34
34
  <p>No issues found.</p>
35
35
  <% end %>
36
36
  <br>
37
- <%= link_to "New Issue", plan_my_stuff.new_issue_path(repo: @repo) %>
37
+ <%= link_to('New Issue', plan_my_stuff.new_issue_path(repo: @repo)) %>
@@ -1,7 +1,5 @@
1
1
  <h1>New Issue</h1>
2
2
 
3
- <% if flash[:error].present? %>
4
- <p style="color: red;"><%= flash[:error] %></p>
5
- <% end %>
3
+ <%= render({ partial: 'plan_my_stuff/partials/flash' }) %>
6
4
 
7
- <%= render({ partial: 'plan_my_stuff/issues/partials/form', locals: { issue: @issue, support_user: @support_user } }) %>
5
+ <%= render({ partial: 'plan_my_stuff/issues/partials/form', locals: { issue: @issue } }) %>
@@ -1,6 +1,8 @@
1
+ <%# locals: (issue:, support_user:, current_user_id_local:) %>
1
2
  <%
2
3
  approvers = issue.approvers
3
4
  pending_count = issue.pending_approvals.size
5
+ rejected_count = issue.rejected_approvals.size
4
6
  current_user_id = current_user_id_local
5
7
  %>
6
8
 
@@ -12,7 +14,12 @@
12
14
  <% if issue.fully_approved? %>
13
15
  <p><strong>Fully approved</strong></p>
14
16
  <% else %>
15
- <p><strong><%= pending_count %> of <%= approvers.size %> approval(s) pending</strong></p>
17
+ <p>
18
+ <strong><%= pending_count %> of <%= approvers.size %> approval(s) pending</strong>
19
+ <% if rejected_count.positive? %>
20
+ (<%= rejected_count %> rejected)
21
+ <% end %>
22
+ </p>
16
23
  <% end %>
17
24
 
18
25
  <ul>
@@ -24,6 +31,11 @@
24
31
  <% if approval.approved_at %>
25
32
  at <%= approval.approved_at.iso8601 %>
26
33
  <% end %>
34
+ <% elsif approval.rejected? %>
35
+ rejected
36
+ <% if approval.rejected_at %>
37
+ at <%= approval.rejected_at.iso8601 %>
38
+ <% end %>
27
39
  <% else %>
28
40
  pending
29
41
  <% end %>
@@ -38,9 +50,18 @@
38
50
  form: { style: 'display:inline' },
39
51
  )
40
52
  %>
53
+ <%=
54
+ button_to(
55
+ 'Reject',
56
+ plan_my_stuff.issue_approval_path(issue.number, approval.user_id, repo: issue.repo.full_name),
57
+ method: :patch,
58
+ params: { approval: { status: 'rejected' } },
59
+ form: { style: 'display:inline' },
60
+ )
61
+ %>
41
62
  <% end %>
42
63
 
43
- <% if approval.approved? && (approval.user_id == current_user_id || support_user) %>
64
+ <% if !approval.pending? && (approval.user_id == current_user_id || support_user) %>
44
65
  <%=
45
66
  button_to(
46
67
  'Revoke',
@@ -1,3 +1,4 @@
1
+ <%# locals: (issue:) %>
1
2
  <%
2
3
  persisted = issue.persisted?
3
4
  url =
@@ -1,4 +1,5 @@
1
- <% if issue.labels.any? %>
1
+ <%# locals: (issue:) %>
2
+ <% if issue.labels.present? %>
2
3
  <% issue.labels.each do |label| %>
3
4
  <span>
4
5
  <%= label %>
@@ -1,14 +1,57 @@
1
+ <%# locals: (issue:, support_user:) %>
1
2
  <%
2
3
  # read_only sections have no add form; remove_only sections allow X but
3
4
  # no add (blocking is created from the blocked side; duplicate_of is
4
5
  # created via mark_duplicate! and cleared by reopening on GitHub).
5
6
  sections = [
6
- { type: 'parent', label: 'Parent', single: true, add_form: true, removable: true, targets: [issue.parent].compact },
7
- { type: 'sub_ticket', label: 'Sub-tickets', single: false, add_form: true, removable: true, targets: issue.sub_tickets },
8
- { type: 'blocked_by', label: 'Blocked by', single: false, add_form: true, removable: true, targets: issue.blocked_by },
9
- { type: 'blocking', label: 'Blocking', single: false, add_form: false, removable: false, targets: issue.blocking },
10
- { type: 'related', label: 'Related', single: false, add_form: true, removable: true, targets: issue.related },
11
- { type: 'duplicate_of', label: 'Duplicate of', single: true, add_form: false, removable: false, targets: [issue.duplicate_of].compact },
7
+ {
8
+ type: 'parent',
9
+ label: 'Parent',
10
+ single: true,
11
+ add_form: true,
12
+ removable: true,
13
+ targets: [issue.parent].compact
14
+ },
15
+ {
16
+ type: 'sub_ticket',
17
+ label: 'Sub-tickets',
18
+ single: false,
19
+ add_form: true,
20
+ removable: true,
21
+ targets: issue.sub_tickets
22
+ },
23
+ {
24
+ type: 'blocked_by',
25
+ label: 'Blocked by',
26
+ single: false,
27
+ add_form: true,
28
+ removable: true,
29
+ targets: issue.blocked_by
30
+ },
31
+ {
32
+ type: 'blocking',
33
+ label: 'Blocking',
34
+ single: false,
35
+ add_form: false,
36
+ removable: false,
37
+ targets: issue.blocking
38
+ },
39
+ {
40
+ type: 'related',
41
+ label: 'Related',
42
+ single: false,
43
+ add_form: true,
44
+ removable: true,
45
+ targets: issue.related
46
+ },
47
+ {
48
+ type: 'duplicate_of',
49
+ label: 'Duplicate of',
50
+ single: true,
51
+ add_form: false,
52
+ removable: false,
53
+ targets: [issue.duplicate_of].compact
54
+ },
12
55
  ]
13
56
  visible_sections = support_user ? sections : sections.select { |s| s[:type] == 'related' }
14
57
  %>
@@ -18,7 +61,7 @@
18
61
  <% visible_sections.each do |section| %>
19
62
  <div>
20
63
  <h4><%= section[:label] %></h4>
21
- <% if section[:targets].any? %>
64
+ <% if section[:targets].present? %>
22
65
  <ul>
23
66
  <% section[:targets].each do |target| %>
24
67
  <li>
@@ -1,7 +1,8 @@
1
+ <%# locals: (issue:) %>
1
2
  <h2>Manage Viewers</h2>
2
3
 
3
4
  <% viewers = issue.metadata.visibility_allowlist %>
4
- <% if viewers.any? %>
5
+ <% if viewers.present? %>
5
6
  <ul>
6
7
  <% viewers.each do |viewer_id| %>
7
8
  <li>
@@ -17,9 +17,12 @@
17
17
  <%= button_to('Mark waiting', plan_my_stuff.issue_waiting_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
18
18
  <% end %>
19
19
  <% end %>
20
- <% if @support_user && @pipeline_enabled && @pipeline_item.nil? %>
20
+ <% if @support_user && @pipeline_enabled && @pipeline_item.nil? && @issue.assignees.blank? %>
21
21
  <%= button_to('Take', plan_my_stuff.issue_take_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
22
22
  <% end %>
23
+ <% if @support_user && @pipeline_enabled && @current_user_login.present? && @issue.assignees.include?(@current_user_login) %>
24
+ <%= button_to('Release', plan_my_stuff.issue_take_path(@issue.number, repo: @issue.repo.full_name), method: :delete) %>
25
+ <% end %>
23
26
  <% if @support_user %>
24
27
  <%= link_to('Start Testing Project', plan_my_stuff.new_testing_project_path(subject_url: @issue.html_url)) %>
25
28
  <% end %>
@@ -67,7 +70,7 @@
67
70
 
68
71
  <h2>Comments (<%= @comments.size %>)</h2>
69
72
 
70
- <% if @comments.any? %>
73
+ <% if @comments.present? %>
71
74
  <% @comments.each do |comment| %>
72
75
  <div>
73
76
  <%= PlanMyStuff::Markdown.render(comment.body || '').html_safe %>
@@ -0,0 +1,4 @@
1
+ <%# locals: () %>
2
+ <% if flash[:error].present? %>
3
+ <p role="alert" style="color: red;"><%= flash[:error] %></p>
4
+ <% end %>
@@ -1,7 +1,5 @@
1
1
  <h1>Edit Project #<%= @project.number %></h1>
2
2
 
3
- <% if flash[:error].present? %>
4
- <p style="color: red;"><%= flash[:error] %></p>
5
- <% end %>
3
+ <%= render({ partial: 'plan_my_stuff/partials/flash' }) %>
6
4
 
7
5
  <%= render({ partial: 'plan_my_stuff/projects/partials/form', locals: { project: @project } }) %>
@@ -6,7 +6,7 @@
6
6
  <%= link_to('Testing', plan_my_stuff.projects_path(filter: 'testing')) %>
7
7
  </p>
8
8
 
9
- <% if @projects.any? %>
9
+ <% if @projects.present? %>
10
10
  <ul>
11
11
  <% @projects.each do |project| %>
12
12
  <li>
@@ -1,7 +1,5 @@
1
1
  <h1>New Project</h1>
2
2
 
3
- <% if flash[:error].present? %>
4
- <p style="color: red;"><%= flash[:error] %></p>
5
- <% end %>
3
+ <%= render({ partial: 'plan_my_stuff/partials/flash' }) %>
6
4
 
7
5
  <%= render({ partial: 'plan_my_stuff/projects/partials/form', locals: { project: @project } }) %>
@@ -1,3 +1,4 @@
1
+ <%# locals: (project:) %>
1
2
  <%
2
3
  persisted = project.persisted?
3
4
  url =
@@ -7,7 +7,7 @@
7
7
  <% end %>
8
8
  </p>
9
9
 
10
- <% if @statuses.any? %>
10
+ <% if @statuses.present? %>
11
11
  <table>
12
12
  <thead>
13
13
  <tr>
@@ -43,7 +43,7 @@
43
43
  <% end %>
44
44
 
45
45
  <% assignees = item.field_values['Assignees'] || [] %>
46
- <% if assignees.any? %>
46
+ <% if assignees.present? %>
47
47
  <div>
48
48
  <% assignees.each do |username| %>
49
49
  <span>
@@ -57,7 +57,7 @@
57
57
  ) do |form|
58
58
  %>
59
59
  <%= form.hidden_field(:username, value: username) %>
60
- <%= form.submit('×', title: "Unassign #{username}") %>
60
+ <%= form.submit('x', title: "Unassign #{username}") %>
61
61
  <% end %>
62
62
  </span>
63
63
  <% end %>
@@ -74,6 +74,16 @@
74
74
  <%= form.text_field(:assignee, placeholder: 'GitHub username') %>
75
75
  <%= form.submit('Assign') %>
76
76
  <% end %>
77
+
78
+ <%=
79
+ form_with(
80
+ url: plan_my_stuff.project_item_path(@project.number, item.id),
81
+ method: :delete,
82
+ local: true,
83
+ ) do |form|
84
+ %>
85
+ <%= form.submit('Remove from project') %>
86
+ <% end %>
77
87
  </div>
78
88
  <% end %>
79
89
  <% if items.empty? %>
@@ -1,8 +1,6 @@
1
1
  <h1>Add Item to <%= @project.title %></h1>
2
2
 
3
- <% if flash[:error].present? %>
4
- <p style="color: red;"><%= flash[:error] %></p>
5
- <% end %>
3
+ <%= render({ partial: 'plan_my_stuff/partials/flash' }) %>
6
4
 
7
5
  <%=
8
6
  render({
@@ -1,8 +1,6 @@
1
1
  <h1>Fail Item</h1>
2
2
 
3
- <% if flash[:error].present? %>
4
- <p style="color: red;"><%= flash[:error] %></p>
5
- <% end %>
3
+ <%= render({ partial: 'plan_my_stuff/partials/flash' }) %>
6
4
 
7
5
  <%= form_with(
8
6
  url: plan_my_stuff.testing_project_item_result_path(@project_number, @item_id),
@@ -1,7 +1,5 @@
1
1
  <h1>Edit Testing Project #<%= @project.number %></h1>
2
2
 
3
- <% if flash[:error].present? %>
4
- <p style="color: red;"><%= flash[:error] %></p>
5
- <% end %>
3
+ <%= render({ partial: 'plan_my_stuff/partials/flash' }) %>
6
4
 
7
5
  <%= render({ partial: 'plan_my_stuff/testing_projects/partials/form', locals: { project: @project } }) %>
@@ -1,7 +1,5 @@
1
1
  <h1>New Testing Project</h1>
2
2
 
3
- <% if flash[:error].present? %>
4
- <p style="color: red;"><%= flash[:error] %></p>
5
- <% end %>
3
+ <%= render({ partial: 'plan_my_stuff/partials/flash' }) %>
6
4
 
7
5
  <%= render({ partial: 'plan_my_stuff/testing_projects/partials/form', locals: { project: @project } }) %>
@@ -1,3 +1,4 @@
1
+ <%# locals: (project:) %>
1
2
  <%
2
3
  persisted = project.persisted?
3
4
  url =
@@ -20,17 +21,17 @@
20
21
 
21
22
  <div>
22
23
  <%= form.label(:subject_urls, 'Subject URLs (one per line)') %>
23
- <%= form.text_area(:subject_urls, rows: 4, value: project.metadata.subject_urls.join("\n")) %>
24
+ <%= form.text_area(:subject_urls, rows: 4, value: Array.wrap(project.metadata&.subject_urls).join("\n")) %>
24
25
  </div>
25
26
 
26
27
  <div>
27
28
  <%= form.label(:due_date, 'Due Date') %>
28
- <%= form.date_field(:due_date, value: project.metadata.due_date) %>
29
+ <%= form.date_field(:due_date, value: project.metadata&.due_date&.strftime('%F')) %>
29
30
  </div>
30
31
 
31
32
  <div>
32
33
  <%= form.label(:deadline_miss_reason, 'Deadline Miss Reason') %>
33
- <%= form.text_field(:deadline_miss_reason, value: project.metadata.deadline_miss_reason) %>
34
+ <%= form.text_field(:deadline_miss_reason, value: project.metadata&.deadline_miss_reason) %>
34
35
  </div>
35
36
 
36
37
  <div>
@@ -1,3 +1,4 @@
1
+ <%# locals: (item:, project:, statuses:) %>
1
2
  <div style="border: 1px solid black; margin: 1em; padding: 0.5em">
2
3
  <% if item.draft? %>
3
4
  <strong><%= item.title %></strong>