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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +595 -0
- data/CONFIGURATION.md +487 -0
- data/README.md +612 -88
- data/app/controllers/plan_my_stuff/application_controller.rb +27 -5
- data/app/controllers/plan_my_stuff/comments_controller.rb +50 -19
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +127 -0
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +53 -0
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +129 -0
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +161 -0
- data/app/controllers/plan_my_stuff/issues/testings_controller.rb +82 -0
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +62 -0
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +55 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +53 -70
- data/app/controllers/plan_my_stuff/labels_controller.rb +32 -10
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +88 -0
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +44 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +32 -69
- data/app/controllers/plan_my_stuff/projects_controller.rb +81 -3
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +67 -0
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +49 -0
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +121 -0
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +202 -0
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +371 -0
- data/app/jobs/plan_my_stuff/application_job.rb +8 -0
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +75 -0
- data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +8 -0
- data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/index.html.erb +5 -5
- data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +108 -0
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +11 -6
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +4 -3
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +113 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +4 -3
- data/app/views/plan_my_stuff/issues/show.html.erb +67 -6
- data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
- data/app/views/plan_my_stuff/projects/edit.html.erb +5 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +18 -2
- data/app/views/plan_my_stuff/projects/new.html.erb +5 -0
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +30 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +30 -11
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +10 -0
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +20 -0
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +5 -0
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +5 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +40 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +52 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +36 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
- data/config/routes.rb +43 -15
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +302 -20
- data/lib/plan_my_stuff/application_record.rb +158 -1
- data/lib/plan_my_stuff/approval.rb +88 -0
- data/lib/plan_my_stuff/archive/sweep.rb +85 -0
- data/lib/plan_my_stuff/archive.rb +12 -0
- data/lib/plan_my_stuff/attachment.rb +83 -0
- data/lib/plan_my_stuff/attachment_uploader.rb +245 -0
- data/lib/plan_my_stuff/aws_sns_simulator.rb +116 -0
- data/lib/plan_my_stuff/base_metadata.rb +25 -28
- data/lib/plan_my_stuff/base_project.rb +502 -0
- data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +186 -0
- data/lib/plan_my_stuff/base_project_item.rb +588 -0
- data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
- data/lib/plan_my_stuff/cache.rb +197 -0
- data/lib/plan_my_stuff/client.rb +139 -64
- data/lib/plan_my_stuff/comment.rb +225 -100
- data/lib/plan_my_stuff/comment_metadata.rb +68 -5
- data/lib/plan_my_stuff/configuration.rb +459 -28
- data/lib/plan_my_stuff/custom_fields.rb +96 -12
- data/lib/plan_my_stuff/engine.rb +14 -2
- data/lib/plan_my_stuff/errors.rb +65 -5
- data/lib/plan_my_stuff/graphql/queries.rb +454 -0
- data/lib/plan_my_stuff/issue.rb +1097 -166
- data/lib/plan_my_stuff/issue_extractions/approvals.rb +370 -0
- data/lib/plan_my_stuff/issue_extractions/links.rb +525 -0
- data/lib/plan_my_stuff/issue_extractions/viewers.rb +75 -0
- data/lib/plan_my_stuff/issue_extractions/waiting.rb +171 -0
- data/lib/plan_my_stuff/issue_field.rb +126 -0
- data/lib/plan_my_stuff/issue_field_translation.rb +67 -0
- data/lib/plan_my_stuff/issue_field_value_set.rb +68 -0
- data/lib/plan_my_stuff/issue_metadata.rb +132 -21
- data/lib/plan_my_stuff/label.rb +100 -13
- data/lib/plan_my_stuff/link.rb +144 -0
- data/lib/plan_my_stuff/markdown.rb +13 -7
- data/lib/plan_my_stuff/metadata_parser.rb +51 -12
- data/lib/plan_my_stuff/notifications.rb +148 -0
- data/lib/plan_my_stuff/pipeline/completed_sweep.rb +46 -0
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
- data/lib/plan_my_stuff/pipeline/status.rb +40 -0
- data/lib/plan_my_stuff/pipeline/testing.rb +23 -0
- data/lib/plan_my_stuff/pipeline.rb +310 -0
- data/lib/plan_my_stuff/project.rb +63 -465
- data/lib/plan_my_stuff/project_item.rb +3 -409
- data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
- data/lib/plan_my_stuff/project_metadata.rb +47 -0
- data/lib/plan_my_stuff/reminders/closer.rb +70 -0
- data/lib/plan_my_stuff/reminders/fire.rb +129 -0
- data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
- data/lib/plan_my_stuff/reminders.rb +12 -0
- data/lib/plan_my_stuff/repo.rb +145 -0
- data/lib/plan_my_stuff/test_helpers.rb +265 -25
- data/lib/plan_my_stuff/testing_project.rb +292 -0
- data/lib/plan_my_stuff/testing_project_item.rb +218 -0
- data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
- data/lib/plan_my_stuff/user_resolver.rb +24 -3
- data/lib/plan_my_stuff/verifier.rb +10 -0
- data/lib/plan_my_stuff/version.rb +2 -2
- data/lib/plan_my_stuff/webhook_replayer.rb +292 -0
- data/lib/plan_my_stuff.rb +55 -20
- data/lib/tasks/plan_my_stuff.rake +331 -0
- 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]
|
|
11
|
+
@repo = params[:repo]
|
|
12
12
|
|
|
13
|
-
@issues =
|
|
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 =
|
|
25
|
-
|
|
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 =
|
|
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
|
|
39
|
-
rescue
|
|
40
|
-
|
|
41
|
-
@
|
|
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:
|
|
49
|
+
render(:new, status: PlanMyStuff.unprocessable_status)
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
# GET /issues/:id
|
|
47
53
|
def show
|
|
48
|
-
@issue =
|
|
54
|
+
@issue = PlanMyStuff::Issue.find(params[:id])
|
|
49
55
|
@comments = filter_visible_comments(@issue.comments)
|
|
50
|
-
@
|
|
51
|
-
@
|
|
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 =
|
|
57
|
-
|
|
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 =
|
|
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
|
|
72
|
-
rescue
|
|
73
|
-
|
|
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:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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 =
|
|
15
|
-
|
|
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
|
|
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/:
|
|
22
|
-
def
|
|
23
|
-
issue =
|
|
24
|
-
|
|
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
|
|
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
|