plan_my_stuff 0.1.0 → 1.0.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +595 -0
  3. data/CONFIGURATION.md +487 -0
  4. data/README.md +612 -88
  5. data/app/controllers/plan_my_stuff/application_controller.rb +27 -5
  6. data/app/controllers/plan_my_stuff/comments_controller.rb +50 -19
  7. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +127 -0
  8. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +53 -0
  9. data/app/controllers/plan_my_stuff/issues/links_controller.rb +129 -0
  10. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +161 -0
  11. data/app/controllers/plan_my_stuff/issues/testings_controller.rb +82 -0
  12. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +62 -0
  13. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +55 -0
  14. data/app/controllers/plan_my_stuff/issues_controller.rb +53 -70
  15. data/app/controllers/plan_my_stuff/labels_controller.rb +32 -10
  16. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +88 -0
  17. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +44 -0
  18. data/app/controllers/plan_my_stuff/project_items_controller.rb +32 -69
  19. data/app/controllers/plan_my_stuff/projects_controller.rb +81 -3
  20. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +67 -0
  21. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +49 -0
  22. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +121 -0
  23. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +202 -0
  24. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +371 -0
  25. data/app/jobs/plan_my_stuff/application_job.rb +8 -0
  26. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +75 -0
  27. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  28. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +8 -0
  29. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  30. data/app/views/plan_my_stuff/issues/index.html.erb +5 -5
  31. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  32. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +108 -0
  33. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +11 -6
  34. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +4 -3
  35. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +113 -0
  36. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +4 -3
  37. data/app/views/plan_my_stuff/issues/show.html.erb +67 -6
  38. data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
  39. data/app/views/plan_my_stuff/projects/edit.html.erb +5 -0
  40. data/app/views/plan_my_stuff/projects/index.html.erb +18 -2
  41. data/app/views/plan_my_stuff/projects/new.html.erb +5 -0
  42. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +30 -0
  43. data/app/views/plan_my_stuff/projects/show.html.erb +30 -11
  44. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +10 -0
  45. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +20 -0
  46. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +5 -0
  47. data/app/views/plan_my_stuff/testing_projects/new.html.erb +5 -0
  48. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +40 -0
  49. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +52 -0
  50. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +36 -0
  51. data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
  52. data/config/routes.rb +43 -15
  53. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +302 -20
  54. data/lib/plan_my_stuff/application_record.rb +158 -1
  55. data/lib/plan_my_stuff/approval.rb +88 -0
  56. data/lib/plan_my_stuff/archive/sweep.rb +85 -0
  57. data/lib/plan_my_stuff/archive.rb +12 -0
  58. data/lib/plan_my_stuff/attachment.rb +83 -0
  59. data/lib/plan_my_stuff/attachment_uploader.rb +245 -0
  60. data/lib/plan_my_stuff/aws_sns_simulator.rb +116 -0
  61. data/lib/plan_my_stuff/base_metadata.rb +25 -28
  62. data/lib/plan_my_stuff/base_project.rb +502 -0
  63. data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +186 -0
  64. data/lib/plan_my_stuff/base_project_item.rb +588 -0
  65. data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
  66. data/lib/plan_my_stuff/cache.rb +197 -0
  67. data/lib/plan_my_stuff/client.rb +139 -64
  68. data/lib/plan_my_stuff/comment.rb +225 -100
  69. data/lib/plan_my_stuff/comment_metadata.rb +68 -5
  70. data/lib/plan_my_stuff/configuration.rb +459 -28
  71. data/lib/plan_my_stuff/custom_fields.rb +96 -12
  72. data/lib/plan_my_stuff/engine.rb +14 -2
  73. data/lib/plan_my_stuff/errors.rb +65 -5
  74. data/lib/plan_my_stuff/graphql/queries.rb +454 -0
  75. data/lib/plan_my_stuff/issue.rb +1097 -166
  76. data/lib/plan_my_stuff/issue_extractions/approvals.rb +370 -0
  77. data/lib/plan_my_stuff/issue_extractions/links.rb +525 -0
  78. data/lib/plan_my_stuff/issue_extractions/viewers.rb +75 -0
  79. data/lib/plan_my_stuff/issue_extractions/waiting.rb +171 -0
  80. data/lib/plan_my_stuff/issue_field.rb +126 -0
  81. data/lib/plan_my_stuff/issue_field_translation.rb +67 -0
  82. data/lib/plan_my_stuff/issue_field_value_set.rb +68 -0
  83. data/lib/plan_my_stuff/issue_metadata.rb +132 -21
  84. data/lib/plan_my_stuff/label.rb +100 -13
  85. data/lib/plan_my_stuff/link.rb +144 -0
  86. data/lib/plan_my_stuff/markdown.rb +13 -7
  87. data/lib/plan_my_stuff/metadata_parser.rb +51 -12
  88. data/lib/plan_my_stuff/notifications.rb +148 -0
  89. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +46 -0
  90. data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
  91. data/lib/plan_my_stuff/pipeline/status.rb +40 -0
  92. data/lib/plan_my_stuff/pipeline/testing.rb +23 -0
  93. data/lib/plan_my_stuff/pipeline.rb +310 -0
  94. data/lib/plan_my_stuff/project.rb +63 -465
  95. data/lib/plan_my_stuff/project_item.rb +3 -409
  96. data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
  97. data/lib/plan_my_stuff/project_metadata.rb +47 -0
  98. data/lib/plan_my_stuff/reminders/closer.rb +70 -0
  99. data/lib/plan_my_stuff/reminders/fire.rb +129 -0
  100. data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
  101. data/lib/plan_my_stuff/reminders.rb +12 -0
  102. data/lib/plan_my_stuff/repo.rb +145 -0
  103. data/lib/plan_my_stuff/test_helpers.rb +265 -25
  104. data/lib/plan_my_stuff/testing_project.rb +292 -0
  105. data/lib/plan_my_stuff/testing_project_item.rb +218 -0
  106. data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
  107. data/lib/plan_my_stuff/user_resolver.rb +24 -3
  108. data/lib/plan_my_stuff/verifier.rb +10 -0
  109. data/lib/plan_my_stuff/version.rb +2 -2
  110. data/lib/plan_my_stuff/webhook_replayer.rb +292 -0
  111. data/lib/plan_my_stuff.rb +55 -20
  112. data/lib/tasks/plan_my_stuff.rake +331 -0
  113. metadata +99 -4
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module Issues
5
+ # Toggles the pipeline +Testing+ custom field on an issue's project item via
6
+ # +PlanMyStuff::Pipeline.request_testing!+ and +PlanMyStuff::Pipeline.clear_testing!+. Backs the "Request testing" /
7
+ # "Clear testing" buttons on the mounted issue show view.
8
+ #
9
+ # POST /issues/:issue_id/testing -> create (flips +Testing+ to its active value)
10
+ # DELETE /issues/:issue_id/testing -> destroy (flips +Testing+ back to inactive)
11
+ #
12
+ class TestingsController < PlanMyStuff::ApplicationController
13
+ before_action :require_support_user
14
+
15
+ # POST /issues/:issue_id/testing
16
+ def create
17
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
18
+ project_item = find_project_item!(issue)
19
+
20
+ PlanMyStuff::Pipeline.request_testing!(project_item, user: pms_current_user)
21
+
22
+ yield(project_item) if block_given?
23
+ return if performed?
24
+
25
+ flash[:success] = "Issue ##{issue.number} marked as in testing."
26
+ redirect_to(plan_my_stuff.issue_path(issue))
27
+ rescue ArgumentError, PlanMyStuff::Error => e
28
+ pms_handle_rescue(e)
29
+ flash[:error] = e.message
30
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
31
+ end
32
+
33
+ # DELETE /issues/:issue_id/testing
34
+ def destroy
35
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
36
+ project_item = find_project_item!(issue)
37
+
38
+ PlanMyStuff::Pipeline.clear_testing!(project_item, user: pms_current_user)
39
+
40
+ yield(project_item) if block_given?
41
+ return if performed?
42
+
43
+ flash[:success] = "Issue ##{issue.number} testing cleared."
44
+ redirect_to(plan_my_stuff.issue_path(issue))
45
+ rescue ArgumentError, PlanMyStuff::Error => e
46
+ pms_handle_rescue(e)
47
+ flash[:error] = e.message
48
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
49
+ end
50
+
51
+ private
52
+
53
+ # Redirects non-support users back to the issue page. Mirrors +Issues::TakesController+'s authorization check.
54
+ #
55
+ # @return [void]
56
+ #
57
+ def require_support_user
58
+ return if support_user?
59
+
60
+ redirect_to_unauthorized(
61
+ plan_my_stuff.issue_path(params[:issue_id]),
62
+ )
63
+ end
64
+
65
+ # Looks up the pipeline project item for the issue, raising if missing. Defensive against a stale view (button
66
+ # rendered before the item was removed) or a race with another tab removing the issue from the pipeline.
67
+ #
68
+ # @raise [PlanMyStuff::Error] if the issue is not in the pipeline
69
+ #
70
+ # @param issue [PlanMyStuff::Issue]
71
+ #
72
+ # @return [PlanMyStuff::ProjectItem]
73
+ #
74
+ def find_project_item!(issue)
75
+ project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue.number)
76
+ return project_item if project_item.present?
77
+
78
+ raise(PlanMyStuff::Error, "Issue ##{issue.number} is not in the pipeline.")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module Issues
5
+ # Handles adding and removing viewers from the visibility allowlist via CRUD-style routes.
6
+ # Backs the viewer management UI on the issue edit view (T-045).
7
+ #
8
+ # POST /issues/:issue_id/viewers -> create (adds viewer(s))
9
+ # DELETE /issues/:issue_id/viewers/:id -> destroy (removes a viewer)
10
+ #
11
+ class ViewersController < PlanMyStuff::ApplicationController
12
+ # POST /issues/:issue_id/viewers
13
+ def create
14
+ unless support_user?
15
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
16
+ return
17
+ end
18
+
19
+ viewer_ids = parse_viewer_ids(params[:viewer_ids])
20
+ if viewer_ids.blank?
21
+ flash[:error] = 'No valid viewer IDs provided.'
22
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
23
+ return
24
+ end
25
+
26
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
27
+ issue.add_viewers!(user_ids: viewer_ids, user: pms_current_user)
28
+
29
+ yield(issue) if block_given?
30
+ return if performed?
31
+
32
+ flash[:success] = 'Viewers were successfully added.'
33
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
34
+ rescue PlanMyStuff::Error, Octokit::Error => e
35
+ pms_handle_rescue(e)
36
+ flash[:error] = e.message
37
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
38
+ end
39
+
40
+ # DELETE /issues/:issue_id/viewers/:id
41
+ def destroy
42
+ unless support_user?
43
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
44
+ return
45
+ end
46
+
47
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
48
+ issue.remove_viewers!(user_ids: [params[:id].to_i], user: pms_current_user)
49
+
50
+ yield(issue) if block_given?
51
+ return if performed?
52
+
53
+ flash[:success] = 'Viewer was successfully removed.'
54
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
55
+ rescue PlanMyStuff::Error, Octokit::Error => e
56
+ pms_handle_rescue(e)
57
+ flash[:error] = e.message
58
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module Issues
5
+ # Toggles the waiting-on-user state on an issue via CRUD-style routes.
6
+ # Backs the "Mark waiting" / "Mark replied" button on the issue show view.
7
+ #
8
+ # POST /issues/:issue_id/waiting -> create (enters waiting-on-user)
9
+ # DELETE /issues/:issue_id/waiting -> destroy (clears waiting-on-user)
10
+ #
11
+ class WaitingsController < PlanMyStuff::ApplicationController
12
+ # POST /issues/:issue_id/waiting
13
+ def create
14
+ unless support_user?
15
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
16
+ return
17
+ end
18
+
19
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
20
+ issue.enter_waiting_on_user!(user: pms_current_user)
21
+
22
+ yield(issue) if block_given?
23
+ return if performed?
24
+
25
+ flash[:success] = 'Issue marked as waiting on user reply.'
26
+ redirect_to(plan_my_stuff.issue_path(issue))
27
+ rescue PlanMyStuff::Error, ArgumentError => e
28
+ pms_handle_rescue(e)
29
+ flash[:error] = e.message
30
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
31
+ end
32
+
33
+ # DELETE /issues/:issue_id/waiting
34
+ def destroy
35
+ unless support_user?
36
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
37
+ return
38
+ end
39
+
40
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
41
+ issue.clear_waiting_on_user!
42
+
43
+ yield(issue) if block_given?
44
+ return if performed?
45
+
46
+ flash[:success] = 'Waiting-on-user state cleared.'
47
+ redirect_to(plan_my_stuff.issue_path(issue))
48
+ rescue PlanMyStuff::Error, ArgumentError => e
49
+ pms_handle_rescue(e)
50
+ flash[:error] = e.message
51
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,65 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class IssuesController < ApplicationController
4
+ class IssuesController < PlanMyStuff::ApplicationController
5
5
  # GET /issues
6
6
  def index
7
7
  @page = (params[:page] || 1).to_i
8
8
  @per_page = (params[:per_page] || 25).to_i
9
9
  @state = (params[:state] || 'open').to_sym
10
10
  @labels = params[:labels].present? ? Array.wrap(params[:labels]) : []
11
- @repo = params[:repo]&.to_sym
11
+ @repo = params[:repo]
12
12
 
13
- @issues = PMS::Issue.list(
13
+ @issues = PlanMyStuff::Issue.list(
14
14
  repo: @repo,
15
15
  state: @state,
16
16
  labels: @labels,
17
17
  page: @page,
18
18
  per_page: @per_page,
19
19
  )
20
+
21
+ yield(@issues) if block_given?
20
22
  end
21
23
 
22
24
  # GET /issues/new
23
25
  def new
24
- @issue = PMS::Issue.new
25
- @support_user = support_user?
26
+ @issue = PlanMyStuff::Issue.new
27
+
28
+ yield(@issue) if block_given?
26
29
  end
27
30
 
28
31
  # POST /issues
29
32
  def create
30
- @issue = PMS::Issue.create!(
33
+ @issue = PlanMyStuff::Issue.create!(
31
34
  title: issue_params[:title],
32
35
  body: issue_params[:body],
33
36
  labels: parse_labels(issue_params[:labels]),
34
37
  user: pms_current_user,
35
38
  )
36
39
 
40
+ yield(@issue) if block_given?
41
+ return if performed?
42
+
37
43
  flash[:success] = 'Issue was successfully created.'
38
- redirect_to(plan_my_stuff.issue_path(@issue.number))
39
- rescue PMS::ValidationError => e
40
- @issue = PMS::Issue.new(title: issue_params[:title], body: issue_params[:body])
41
- @support_user = support_user?
44
+ redirect_to(plan_my_stuff.issue_path(@issue))
45
+ rescue PlanMyStuff::ValidationError => e
46
+ pms_handle_rescue(e)
47
+ @issue = PlanMyStuff::Issue.new(title: issue_params[:title], body: issue_params[:body])
42
48
  flash.now[:error] = e.message
43
- render(:new, status: PMS.unprocessable_status)
49
+ render(:new, status: PlanMyStuff.unprocessable_status)
44
50
  end
45
51
 
46
52
  # GET /issues/:id
47
53
  def show
48
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
54
+ @issue = PlanMyStuff::Issue.find(params[:id])
49
55
  @comments = filter_visible_comments(@issue.comments)
50
- @support_user = support_user?
51
- @current_user_id = pms_current_user.present? ? PMS::UserResolver.user_id(pms_current_user) : nil
56
+ @current_user_id = pms_current_user.present? ? PlanMyStuff::UserResolver.user_id(pms_current_user) : nil
57
+ @current_user_login = PlanMyStuff.configuration.github_login_for[@current_user_id]
58
+ @pipeline_enabled = PlanMyStuff.configuration.pipeline_enabled
59
+ @pipeline_item = load_pipeline_item(@issue.number) if @pipeline_enabled
60
+
61
+ yield(@issue) if block_given?
52
62
  end
53
63
 
54
64
  # GET /issues/:id/edit
55
65
  def edit
56
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
57
- @support_user = support_user?
66
+ @issue = PlanMyStuff::Issue.find(params[:id])
67
+
68
+ yield(@issue) if block_given?
58
69
  end
59
70
 
60
71
  # PATCH/PUT /issues/:id
61
72
  def update
62
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
73
+ @issue = PlanMyStuff::Issue.find(params[:id])
63
74
 
64
75
  @issue.update!(
65
76
  title: issue_params[:title],
@@ -67,58 +78,19 @@ module PlanMyStuff
67
78
  labels: parse_labels(issue_params[:labels]),
68
79
  )
69
80
 
81
+ yield(@issue) if block_given?
82
+ return if performed?
83
+
70
84
  flash[:success] = 'Issue was successfully updated.'
71
- redirect_to(plan_my_stuff.issue_path(@issue.number))
72
- rescue PMS::StaleObjectError
73
- @support_user = support_user?
85
+ redirect_to(plan_my_stuff.issue_path(@issue))
86
+ rescue PlanMyStuff::StaleObjectError => e
87
+ pms_handle_rescue(e)
74
88
  flash.now[:error] = 'Issue was modified by someone else. Please review the latest changes and try again.'
75
- render(:edit, status: PMS.unprocessable_status)
76
- end
77
-
78
- # PATCH /issues/:id/close
79
- def close
80
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
81
- @issue.update!(state: :closed)
82
-
83
- flash[:success] = 'Issue was successfully closed.'
84
- redirect_to(plan_my_stuff.issue_path(@issue.number))
85
- end
86
-
87
- # PATCH /issues/:id/reopen
88
- def reopen
89
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
90
- @issue.update!(state: :open)
91
-
92
- flash[:success] = 'Issue was successfully reopened.'
93
- redirect_to(plan_my_stuff.issue_path(@issue.number))
94
- end
95
-
96
- # POST /issues/:id/add_viewers
97
- def add_viewers
98
- return redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id])) unless support_user?
99
-
100
- viewer_ids = parse_viewer_ids(params[:viewer_ids])
101
- if viewer_ids.blank?
102
- flash[:error] = 'No valid viewer IDs provided.'
103
- redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
104
- return
105
- end
106
-
107
- PMS::Issue.add_viewers(number: params[:id].to_i, user_ids: viewer_ids, repo: params[:repo]&.to_sym)
108
-
109
- flash[:success] = 'Viewers were successfully added.'
110
- redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
111
- end
112
-
113
- # DELETE /issues/:id/remove_viewer
114
- def remove_viewer
115
- return redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id])) unless support_user?
116
-
117
- viewer_id = params[:viewer_id].to_i
118
- PMS::Issue.remove_viewers(number: params[:id].to_i, user_ids: [viewer_id], repo: params[:repo]&.to_sym)
119
-
120
- flash[:success] = 'Viewer was successfully removed.'
121
- redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
89
+ render(:edit, status: PlanMyStuff.unprocessable_status)
90
+ rescue PlanMyStuff::ValidationError => e
91
+ pms_handle_rescue(e)
92
+ flash.now[:error] = e.message
93
+ render(:edit, status: PlanMyStuff.unprocessable_status)
122
94
  end
123
95
 
124
96
  private
@@ -129,7 +101,6 @@ module PlanMyStuff
129
101
  end
130
102
 
131
103
  # Filters comments to only those visible to the current user.
132
- # Falls back to showing all comments if no current_user is available.
133
104
  #
134
105
  # @param comments [Array<PlanMyStuff::Comment>]
135
106
  #
@@ -137,9 +108,21 @@ module PlanMyStuff
137
108
  #
138
109
  def filter_visible_comments(comments)
139
110
  user = pms_current_user
140
- return comments unless user
141
-
142
111
  comments.select { |comment| comment.visible_to?(user) }
143
112
  end
113
+
114
+ # Looks up the pipeline ProjectItem for this issue, if any. Returns nil when the item cannot be loaded for any
115
+ # reason -- issue show should never break because the pipeline lookup failed.
116
+ #
117
+ # @param issue_number [Integer]
118
+ #
119
+ # @return [PlanMyStuff::ProjectItem, nil]
120
+ #
121
+ def load_pipeline_item(issue_number)
122
+ PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
123
+ rescue ArgumentError, PlanMyStuff::Error => e
124
+ Rails.logger.warn("[PlanMyStuff] Failed to load pipeline item for issue ##{issue_number}: #{e.message}")
125
+ nil
126
+ end
144
127
  end
145
128
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class LabelsController < ApplicationController
4
+ class LabelsController < PlanMyStuff::ApplicationController
5
5
  # POST /issues/:issue_id/labels
6
- def add_to_issue
6
+ def create
7
7
  labels = parse_labels(params[:label_name])
8
8
  if labels.blank?
9
9
  flash[:error] = 'Label name is required.'
@@ -11,20 +11,42 @@ module PlanMyStuff
11
11
  return
12
12
  end
13
13
 
14
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
15
- PMS::Label.add(issue: issue, labels: labels)
14
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
15
+
16
+ missing = labels.reject { |label| PlanMyStuff::Label.exists?(repo: issue.repo, name: label) }
17
+ if missing.any?
18
+ flash[:error] = "Label#{'s' if missing.size > 1} not found in repo: #{missing.join(', ')}"
19
+ redirect_to(plan_my_stuff.issue_path(issue))
20
+ return
21
+ end
22
+
23
+ PlanMyStuff::Label.add!(issue: issue, labels: labels)
24
+
25
+ yield(issue) if block_given?
26
+ return if performed?
16
27
 
17
28
  flash[:success] = 'Label was successfully added.'
18
- redirect_to(plan_my_stuff.issue_path(issue.number))
29
+ redirect_to(plan_my_stuff.issue_path(issue))
30
+ rescue PlanMyStuff::Error, Octokit::Error => e
31
+ pms_handle_rescue(e)
32
+ flash[:error] = e.message
33
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
19
34
  end
20
35
 
21
- # DELETE /issues/:issue_id/labels/:name
22
- def remove_from_issue
23
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
24
- PMS::Label.remove(issue: issue, labels: [params[:name]])
36
+ # DELETE /issues/:issue_id/labels/:id
37
+ def destroy
38
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
39
+ PlanMyStuff::Label.remove!(issue: issue, labels: [params[:id]])
40
+
41
+ yield(issue) if block_given?
42
+ return if performed?
25
43
 
26
44
  flash[:success] = 'Label was successfully removed.'
27
- redirect_to(plan_my_stuff.issue_path(issue.number))
45
+ redirect_to(plan_my_stuff.issue_path(issue))
46
+ rescue PlanMyStuff::Error, Octokit::Error => e
47
+ pms_handle_rescue(e)
48
+ flash[:error] = e.message
49
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
28
50
  end
29
51
  end
30
52
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module ProjectItems
5
+ # Handles assigning and unassigning project item assignees via CRUD-style routes.
6
+ # Backs the assign/unassign forms on the project board view (T-047).
7
+ #
8
+ # PATCH /projects/:project_id/items/:item_id/assignment -> update (assign)
9
+ # DELETE /projects/:project_id/items/:item_id/assignment -> destroy (unassign)
10
+ #
11
+ class AssignmentsController < PlanMyStuff::ApplicationController
12
+ # PATCH /projects/:project_id/items/:item_id/assignment
13
+ def update
14
+ item = find_project_item
15
+ assignees = parse_assignees(params[:assignee])
16
+
17
+ item.assign!(assignees)
18
+
19
+ yield(item) if block_given?
20
+ return if performed?
21
+
22
+ flash[:success] =
23
+ if assignees.present?
24
+ "Item assigned to #{assignees.join(', ')}."
25
+ else
26
+ 'All assignees removed.'
27
+ end
28
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
29
+ rescue ArgumentError, PlanMyStuff::Error => e
30
+ pms_handle_rescue(e)
31
+ flash[:error] = e.message
32
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
33
+ end
34
+
35
+ # DELETE /projects/:project_id/items/:item_id/assignment
36
+ def destroy
37
+ if params[:username].blank?
38
+ flash[:error] = 'Username is required to unassign.'
39
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
40
+ return
41
+ end
42
+
43
+ item = find_project_item
44
+ current_assignees = item.field_values['Assignees'] || []
45
+ remaining = current_assignees - [params[:username]]
46
+
47
+ item.assign!(remaining)
48
+
49
+ yield(item) if block_given?
50
+ return if performed?
51
+
52
+ flash[:success] = "#{params[:username]} unassigned."
53
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
54
+ rescue ArgumentError, PlanMyStuff::Error => e
55
+ pms_handle_rescue(e)
56
+ flash[:error] = e.message
57
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
58
+ end
59
+
60
+ private
61
+
62
+ # Finds the project item by item_id within the given project.
63
+ #
64
+ # @return [PlanMyStuff::ProjectItem]
65
+ #
66
+ def find_project_item
67
+ project = PlanMyStuff::Project.find(params[:project_id].to_i)
68
+ item = project.items.find { |i| i.id == params[:item_id] }
69
+
70
+ raise(PlanMyStuff::APIError, "Item not found: #{params[:item_id]}") unless item
71
+
72
+ item
73
+ end
74
+
75
+ # Splits a comma-separated assignees string into an array.
76
+ #
77
+ # @param assignees_string [String, nil]
78
+ #
79
+ # @return [Array<String>]
80
+ #
81
+ def parse_assignees(assignees_string)
82
+ return [] if assignees_string.blank?
83
+
84
+ assignees_string.split(',').filter_map { |a| a.strip.presence }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module ProjectItems
5
+ # Handles moving a project item to a new status column via CRUD-style routes.
6
+ # Backs the status dropdown on the project board view (T-046).
7
+ #
8
+ # PATCH /projects/:project_id/items/:item_id/status -> update (moves to new status)
9
+ #
10
+ class StatusesController < PlanMyStuff::ApplicationController
11
+ # PATCH /projects/:project_id/items/:item_id/status
12
+ def update
13
+ item = find_project_item
14
+
15
+ item.move_to!(params[:status])
16
+
17
+ yield(item) if block_given?
18
+ return if performed?
19
+
20
+ flash[:success] = "Item moved to #{params[:status]}."
21
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
22
+ rescue ArgumentError, PlanMyStuff::Error => e
23
+ pms_handle_rescue(e)
24
+ flash[:error] = e.message
25
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
26
+ end
27
+
28
+ private
29
+
30
+ # Finds the project item by item_id within the given project.
31
+ #
32
+ # @return [PlanMyStuff::ProjectItem]
33
+ #
34
+ def find_project_item
35
+ project = PlanMyStuff::Project.find(params[:project_id].to_i)
36
+ item = project.items.find { |i| i.id == params[:item_id] }
37
+
38
+ raise(PlanMyStuff::APIError, "Item not found: #{params[:item_id]}") unless item
39
+
40
+ item
41
+ end
42
+ end
43
+ end
44
+ end