plan_my_stuff 0.1.1 → 0.2.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 +27 -0
- data/app/controllers/plan_my_stuff/application_controller.rb +4 -1
- data/app/controllers/plan_my_stuff/comments_controller.rb +24 -6
- data/app/controllers/plan_my_stuff/issues_controller.rb +23 -17
- data/app/controllers/plan_my_stuff/labels_controller.rb +5 -5
- data/app/controllers/plan_my_stuff/project_items_controller.rb +6 -0
- data/app/views/plan_my_stuff/issues/index.html.erb +4 -4
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +10 -6
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/show.html.erb +4 -4
- data/app/views/plan_my_stuff/projects/show.html.erb +4 -4
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +10 -1
- data/lib/plan_my_stuff/application_record.rb +14 -1
- data/lib/plan_my_stuff/base_metadata.rb +23 -4
- data/lib/plan_my_stuff/client.rb +2 -22
- data/lib/plan_my_stuff/comment.rb +15 -0
- data/lib/plan_my_stuff/comment_metadata.rb +8 -2
- data/lib/plan_my_stuff/configuration.rb +36 -1
- data/lib/plan_my_stuff/custom_fields.rb +70 -0
- data/lib/plan_my_stuff/issue.rb +16 -14
- data/lib/plan_my_stuff/issue_metadata.rb +8 -2
- data/lib/plan_my_stuff/markdown.rb +1 -1
- data/lib/plan_my_stuff/project.rb +16 -12
- data/lib/plan_my_stuff/project_item.rb +19 -11
- data/lib/plan_my_stuff/repo.rb +107 -0
- data/lib/plan_my_stuff/test_helpers.rb +10 -2
- data/lib/plan_my_stuff/version.rb +2 -2
- data/lib/plan_my_stuff.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0fdaa17653ccaf192c32d3ba21c5eb0ea4954b2a983d1d0d8f3665a9a792d065
|
|
4
|
+
data.tar.gz: d61fb2fba100a20b0e2f40744e2e6c4c936ff4d55c2cbbdf428f8f6d30ed4ea8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cf7a2e48f07ef85f21725524db9a11bad2d4da8ecf403c1e60a3817a86087d0a06ee98d88d12a2a830584c950c5705ca0693c1c5b8f7fd5229b3ffbc0851174a
|
|
7
|
+
data.tar.gz: 89bc0e780f36649e7abacefe7b0fa65f56d6302f66cc23c49cfb343d4ec8f1190b58cd8f4e59c2ba52a5ab8d199bf459a9060ea69321d0793d3da34f7a4e5294
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Validate `config.custom_fields` on `create!`/`update!` actions
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
|
|
11
|
+
- CodeRabbit improvements
|
|
12
|
+
|
|
13
|
+
## 0.1.2
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Issues' links were looking for the issue in the default repo with the same number
|
|
18
|
+
|
|
19
|
+
## 0.1.1
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Wrong changelog included in publish
|
|
24
|
+
|
|
25
|
+
## 0.1.0
|
|
26
|
+
|
|
27
|
+
- Initial Setup
|
|
@@ -70,7 +70,10 @@ module PlanMyStuff
|
|
|
70
70
|
def parse_viewer_ids(ids_string)
|
|
71
71
|
return [] if ids_string.blank?
|
|
72
72
|
|
|
73
|
-
ids_string.split(',').filter_map
|
|
73
|
+
ids_string.split(',').filter_map do |id|
|
|
74
|
+
token = id.strip
|
|
75
|
+
token.to_i if token.match?(/\A\d+\z/)
|
|
76
|
+
end
|
|
74
77
|
end
|
|
75
78
|
end
|
|
76
79
|
end
|
|
@@ -4,7 +4,7 @@ module PlanMyStuff
|
|
|
4
4
|
class CommentsController < ApplicationController
|
|
5
5
|
# POST /issues/:issue_id/comments
|
|
6
6
|
def create
|
|
7
|
-
@issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]
|
|
7
|
+
@issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
8
8
|
|
|
9
9
|
PMS::Comment.create!(
|
|
10
10
|
issue: @issue,
|
|
@@ -14,25 +14,33 @@ module PlanMyStuff
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
flash[:success] = 'Comment was successfully created.'
|
|
17
|
-
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
17
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# GET /issues/:issue_id/comments/:id/edit
|
|
21
21
|
def edit
|
|
22
22
|
load_comment
|
|
23
23
|
return unless @comment
|
|
24
|
+
return redirect_to_issue if issue_body_comment?
|
|
24
25
|
|
|
25
26
|
@support_user = support_user?
|
|
26
|
-
return
|
|
27
|
+
return if can_edit?(@comment)
|
|
28
|
+
|
|
29
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
# PATCH/PUT /issues/:issue_id/comments/:id
|
|
30
33
|
def update
|
|
31
34
|
load_comment
|
|
32
35
|
return unless @comment
|
|
36
|
+
return redirect_to_issue if issue_body_comment?
|
|
33
37
|
|
|
34
38
|
@support_user = support_user?
|
|
35
|
-
|
|
39
|
+
unless can_edit?(@comment)
|
|
40
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
41
|
+
|
|
42
|
+
return
|
|
43
|
+
end
|
|
36
44
|
|
|
37
45
|
update_attrs = { body: comment_params[:body] }
|
|
38
46
|
update_attrs[:visibility] = comment_params[:visibility].to_sym if @support_user && comment_params[:visibility]
|
|
@@ -40,7 +48,7 @@ module PlanMyStuff
|
|
|
40
48
|
@comment.update!(**update_attrs)
|
|
41
49
|
|
|
42
50
|
flash[:success] = 'Comment was successfully updated.'
|
|
43
|
-
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
51
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
44
52
|
rescue PMS::StaleObjectError
|
|
45
53
|
flash.now[:error] = 'Comment was modified by someone else. Please review the latest changes and try again.'
|
|
46
54
|
render(:edit, status: PMS.unprocessable_status)
|
|
@@ -58,7 +66,7 @@ module PlanMyStuff
|
|
|
58
66
|
# @return [void]
|
|
59
67
|
#
|
|
60
68
|
def load_comment
|
|
61
|
-
@issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]
|
|
69
|
+
@issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
62
70
|
@comment = PMS::Comment.find(params[:id].to_i, issue: @issue)
|
|
63
71
|
end
|
|
64
72
|
|
|
@@ -78,5 +86,15 @@ module PlanMyStuff
|
|
|
78
86
|
|
|
79
87
|
comment.metadata.created_by == PMS::UserResolver.user_id(user)
|
|
80
88
|
end
|
|
89
|
+
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def issue_body_comment?
|
|
92
|
+
@comment.metadata.issue_body?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [void]
|
|
96
|
+
def redirect_to_issue
|
|
97
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
98
|
+
end
|
|
81
99
|
end
|
|
82
100
|
end
|
|
@@ -8,7 +8,7 @@ module PlanMyStuff
|
|
|
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
13
|
@issues = PMS::Issue.list(
|
|
14
14
|
repo: @repo,
|
|
@@ -35,7 +35,7 @@ module PlanMyStuff
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
flash[:success] = 'Issue was successfully created.'
|
|
38
|
-
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
38
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
39
39
|
rescue PMS::ValidationError => e
|
|
40
40
|
@issue = PMS::Issue.new(title: issue_params[:title], body: issue_params[:body])
|
|
41
41
|
@support_user = support_user?
|
|
@@ -45,7 +45,7 @@ module PlanMyStuff
|
|
|
45
45
|
|
|
46
46
|
# GET /issues/:id
|
|
47
47
|
def show
|
|
48
|
-
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]
|
|
48
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
|
|
49
49
|
@comments = filter_visible_comments(@issue.comments)
|
|
50
50
|
@support_user = support_user?
|
|
51
51
|
@current_user_id = pms_current_user.present? ? PMS::UserResolver.user_id(pms_current_user) : nil
|
|
@@ -53,13 +53,13 @@ module PlanMyStuff
|
|
|
53
53
|
|
|
54
54
|
# GET /issues/:id/edit
|
|
55
55
|
def edit
|
|
56
|
-
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]
|
|
56
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
|
|
57
57
|
@support_user = support_user?
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# PATCH/PUT /issues/:id
|
|
61
61
|
def update
|
|
62
|
-
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]
|
|
62
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
|
|
63
63
|
|
|
64
64
|
@issue.update!(
|
|
65
65
|
title: issue_params[:title],
|
|
@@ -68,7 +68,7 @@ module PlanMyStuff
|
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
flash[:success] = 'Issue was successfully updated.'
|
|
71
|
-
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
71
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
72
72
|
rescue PMS::StaleObjectError
|
|
73
73
|
@support_user = support_user?
|
|
74
74
|
flash.now[:error] = 'Issue was modified by someone else. Please review the latest changes and try again.'
|
|
@@ -77,48 +77,54 @@ module PlanMyStuff
|
|
|
77
77
|
|
|
78
78
|
# PATCH /issues/:id/close
|
|
79
79
|
def close
|
|
80
|
-
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]
|
|
80
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
|
|
81
81
|
@issue.update!(state: :closed)
|
|
82
82
|
|
|
83
83
|
flash[:success] = 'Issue was successfully closed.'
|
|
84
|
-
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
84
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# PATCH /issues/:id/reopen
|
|
88
88
|
def reopen
|
|
89
|
-
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]
|
|
89
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
|
|
90
90
|
@issue.update!(state: :open)
|
|
91
91
|
|
|
92
92
|
flash[:success] = 'Issue was successfully reopened.'
|
|
93
|
-
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
93
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
# POST /issues/:id/add_viewers
|
|
97
97
|
def add_viewers
|
|
98
|
-
|
|
98
|
+
unless support_user?
|
|
99
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id], repo: params[:repo]))
|
|
100
|
+
return
|
|
101
|
+
end
|
|
99
102
|
|
|
100
103
|
viewer_ids = parse_viewer_ids(params[:viewer_ids])
|
|
101
104
|
if viewer_ids.blank?
|
|
102
105
|
flash[:error] = 'No valid viewer IDs provided.'
|
|
103
|
-
redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
|
|
106
|
+
redirect_to(plan_my_stuff.edit_issue_path(params[:id], repo: params[:repo]))
|
|
104
107
|
return
|
|
105
108
|
end
|
|
106
109
|
|
|
107
|
-
PMS::Issue.add_viewers(number: params[:id].to_i, user_ids: viewer_ids, repo: params[:repo]
|
|
110
|
+
PMS::Issue.add_viewers(number: params[:id].to_i, user_ids: viewer_ids, repo: params[:repo])
|
|
108
111
|
|
|
109
112
|
flash[:success] = 'Viewers were successfully added.'
|
|
110
|
-
redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
|
|
113
|
+
redirect_to(plan_my_stuff.edit_issue_path(params[:id], repo: params[:repo]))
|
|
111
114
|
end
|
|
112
115
|
|
|
113
116
|
# DELETE /issues/:id/remove_viewer
|
|
114
117
|
def remove_viewer
|
|
115
|
-
|
|
118
|
+
unless support_user?
|
|
119
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id], repo: params[:repo]))
|
|
120
|
+
return
|
|
121
|
+
end
|
|
116
122
|
|
|
117
123
|
viewer_id = params[:viewer_id].to_i
|
|
118
|
-
PMS::Issue.remove_viewers(number: params[:id].to_i, user_ids: [viewer_id], repo: params[:repo]
|
|
124
|
+
PMS::Issue.remove_viewers(number: params[:id].to_i, user_ids: [viewer_id], repo: params[:repo])
|
|
119
125
|
|
|
120
126
|
flash[:success] = 'Viewer was successfully removed.'
|
|
121
|
-
redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
|
|
127
|
+
redirect_to(plan_my_stuff.edit_issue_path(params[:id], repo: params[:repo]))
|
|
122
128
|
end
|
|
123
129
|
|
|
124
130
|
private
|
|
@@ -7,24 +7,24 @@ module PlanMyStuff
|
|
|
7
7
|
labels = parse_labels(params[:label_name])
|
|
8
8
|
if labels.blank?
|
|
9
9
|
flash[:error] = 'Label name is required.'
|
|
10
|
-
redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
|
|
10
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
11
11
|
return
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]
|
|
14
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
15
15
|
PMS::Label.add(issue: issue, labels: labels)
|
|
16
16
|
|
|
17
17
|
flash[:success] = 'Label was successfully added.'
|
|
18
|
-
redirect_to(plan_my_stuff.issue_path(issue.number))
|
|
18
|
+
redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# DELETE /issues/:issue_id/labels/:name
|
|
22
22
|
def remove_from_issue
|
|
23
|
-
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]
|
|
23
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
24
24
|
PMS::Label.remove(issue: issue, labels: [params[:name]])
|
|
25
25
|
|
|
26
26
|
flash[:success] = 'Label was successfully removed.'
|
|
27
|
-
redirect_to(plan_my_stuff.issue_path(issue.number))
|
|
27
|
+
redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -50,6 +50,12 @@ module PlanMyStuff
|
|
|
50
50
|
|
|
51
51
|
# PATCH /projects/:project_id/items/:id/unassign
|
|
52
52
|
def unassign
|
|
53
|
+
if params[:username].blank?
|
|
54
|
+
flash[:error] = 'Username is required to unassign.'
|
|
55
|
+
redirect_to(plan_my_stuff.project_path(params[:project_id]))
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
53
59
|
item = find_project_item
|
|
54
60
|
current_assignees = item.field_values['Assignees'] || []
|
|
55
61
|
remaining = current_assignees - [params[:username]]
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<% @issues.each do |issue| %>
|
|
15
15
|
<tr>
|
|
16
16
|
<td><%= issue.number %></td>
|
|
17
|
-
<td><%= link_to(issue.title, plan_my_stuff.issue_path(issue.number)) %></td>
|
|
17
|
+
<td><%= link_to(issue.title, plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name)) %></td>
|
|
18
18
|
<td><%= issue.state %></td>
|
|
19
19
|
<td><%= issue.labels.join(', ') %></td>
|
|
20
20
|
</tr>
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
|
|
25
25
|
<nav>
|
|
26
26
|
<% if @page > 1 %>
|
|
27
|
-
<%= link_to('Previous', plan_my_stuff.issues_path(page: @page - 1, state: @state, labels: @labels)) %>
|
|
27
|
+
<%= link_to('Previous', plan_my_stuff.issues_path(page: @page - 1, state: @state, labels: @labels, repo: @repo)) %>
|
|
28
28
|
<% end %>
|
|
29
29
|
<% if @issues.size == @per_page %>
|
|
30
|
-
<%= link_to('Next', plan_my_stuff.issues_path(page: @page + 1, state: @state, labels: @labels)) %>
|
|
30
|
+
<%= link_to('Next', plan_my_stuff.issues_path(page: @page + 1, state: @state, labels: @labels, repo: @repo)) %>
|
|
31
31
|
<% end %>
|
|
32
32
|
</nav>
|
|
33
33
|
<% else %>
|
|
34
34
|
<p>No issues found.</p>
|
|
35
35
|
<% end %>
|
|
36
36
|
<br>
|
|
37
|
-
<%= link_to "New Issue", plan_my_stuff.new_issue_path %>
|
|
37
|
+
<%= link_to "New Issue", plan_my_stuff.new_issue_path(repo: @repo) %>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
<%
|
|
2
|
-
|
|
3
|
-
url
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<%
|
|
2
|
+
persisted = issue.persisted?
|
|
3
|
+
url =
|
|
4
|
+
if persisted
|
|
5
|
+
plan_my_stuff.issue_path(issue.number, repo: issue.repo&.full_name)
|
|
6
|
+
else
|
|
7
|
+
plan_my_stuff.issues_path(repo: issue.repo&.full_name)
|
|
8
|
+
end
|
|
9
|
+
%>
|
|
10
|
+
<%= form_with(url: url, method: persisted ? :patch : :post, scope: :issue) do |form| %>
|
|
7
11
|
<div>
|
|
8
12
|
<%= form.label(:title, 'Title') %>
|
|
9
13
|
<%= form.text_field(:title, value: issue.title) %>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<%=
|
|
10
10
|
button_to(
|
|
11
11
|
'Remove',
|
|
12
|
-
plan_my_stuff.remove_viewer_issue_path(issue.number, viewer_id: viewer_id),
|
|
12
|
+
plan_my_stuff.remove_viewer_issue_path(issue.number, viewer_id: viewer_id, repo: issue.repo.full_name),
|
|
13
13
|
method: :delete
|
|
14
14
|
)
|
|
15
15
|
%>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<p>No viewers added.</p>
|
|
21
21
|
<% end %>
|
|
22
22
|
|
|
23
|
-
<%= form_with(url: plan_my_stuff.add_viewers_issue_path(issue.number), method: :post) do |form| %>
|
|
23
|
+
<%= form_with(url: plan_my_stuff.add_viewers_issue_path(issue.number, repo: issue.repo.full_name), method: :post) do |form| %>
|
|
24
24
|
<div>
|
|
25
25
|
<%= form.label(:viewer_ids, 'Add viewer IDs (comma-separated)') %>
|
|
26
26
|
<%= form.text_field(:viewer_ids) %>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<h1><%= @issue.title %> <small>#<%= @issue.number %></small></h1>
|
|
2
2
|
|
|
3
3
|
<p>
|
|
4
|
-
<%= link_to('Edit', plan_my_stuff.edit_issue_path(@issue.number)) %>
|
|
4
|
+
<%= link_to('Edit', plan_my_stuff.edit_issue_path(@issue.number, repo: @issue.repo.full_name)) %>
|
|
5
5
|
<% if @issue.state == 'open' %>
|
|
6
|
-
<%= button_to('Close Issue', plan_my_stuff.close_issue_path(@issue.number), method: :patch) %>
|
|
6
|
+
<%= button_to('Close Issue', plan_my_stuff.close_issue_path(@issue.number, repo: @issue.repo.full_name), method: :patch) %>
|
|
7
7
|
<% else %>
|
|
8
|
-
<%= button_to('Reopen Issue', plan_my_stuff.reopen_issue_path(@issue.number), method: :patch) %>
|
|
8
|
+
<%= button_to('Reopen Issue', plan_my_stuff.reopen_issue_path(@issue.number, repo: @issue.repo.full_name), method: :patch) %>
|
|
9
9
|
<% end %>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<%= PlanMyStuff::Markdown.render(comment.body || '').html_safe %>
|
|
36
36
|
<% if comment.pms_comment? && (@support_user || comment.metadata.created_by == @current_user_id) %>
|
|
37
37
|
<p>
|
|
38
|
-
<%= link_to('Edit', plan_my_stuff.edit_issue_comment_path(@issue.number, comment.id)) %>
|
|
38
|
+
<%= link_to('Edit', plan_my_stuff.edit_issue_comment_path(@issue.number, comment.id, repo: @issue.repo.full_name)) %>
|
|
39
39
|
</p>
|
|
40
40
|
<% end %>
|
|
41
41
|
</div>
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
<% items = @items_by_status[status] || [] %>
|
|
17
17
|
<% items.each do |item| %>
|
|
18
18
|
<div style="border: 1px solid black; margin: 1em">
|
|
19
|
-
<%
|
|
20
|
-
<strong><%= link_to(item.title, plan_my_stuff.issue_path(item.number)) %></strong>
|
|
19
|
+
<% unless item.draft? %>
|
|
20
|
+
<strong><%= link_to(item.title, plan_my_stuff.issue_path(item.number, repo: item.repo.full_name)) %></strong>
|
|
21
21
|
<% else %>
|
|
22
22
|
<strong><%= item.title %></strong>
|
|
23
23
|
<% end %>
|
|
24
|
-
<%
|
|
24
|
+
<% unless item.draft? %>
|
|
25
25
|
<small>#<%= item.number %></small>
|
|
26
26
|
<% end %>
|
|
27
27
|
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
url: plan_my_stuff.unassign_project_item_path(@project.number, item.id),
|
|
47
47
|
method: :patch,
|
|
48
48
|
local: true,
|
|
49
|
-
style: 'display: inline',
|
|
49
|
+
html: { style: 'display: inline' },
|
|
50
50
|
) do |form|
|
|
51
51
|
%>
|
|
52
52
|
<%= form.hidden_field(:username, value: username) %>
|
|
@@ -100,7 +100,16 @@ PMS.configure do |config|
|
|
|
100
100
|
# Supported types: :string, :integer, :boolean, :array, :hash
|
|
101
101
|
# required: true means the key must be present (value can be nil, [], etc.)
|
|
102
102
|
#
|
|
103
|
+
# Shared fields (available on both issues and comments):
|
|
103
104
|
config.custom_fields = {
|
|
104
|
-
notification_recipients: { type: :array, required: true },
|
|
105
|
+
# notification_recipients: { type: :array, required: true },
|
|
105
106
|
}
|
|
107
|
+
|
|
108
|
+
# Issue-only fields (merged on top of shared, context wins on conflicts):
|
|
109
|
+
# config.issue_custom_fields = {
|
|
110
|
+
# ticket_type: { type: :string, required: true },
|
|
111
|
+
# }
|
|
112
|
+
|
|
113
|
+
# Comment-only fields (merged on top of shared, context wins on conflicts):
|
|
114
|
+
# config.comment_custom_fields = {}
|
|
106
115
|
end
|
|
@@ -8,6 +8,19 @@ module PlanMyStuff
|
|
|
8
8
|
class ApplicationRecord
|
|
9
9
|
include ActiveModel::Model
|
|
10
10
|
|
|
11
|
+
class << self
|
|
12
|
+
# Reads a field from an object that may respond to method calls or hash access.
|
|
13
|
+
#
|
|
14
|
+
# @param obj [Object]
|
|
15
|
+
# @param field [Symbol]
|
|
16
|
+
#
|
|
17
|
+
# @return [Object]
|
|
18
|
+
#
|
|
19
|
+
def read_field(obj, field)
|
|
20
|
+
obj.respond_to?(field) ? obj.public_send(field) : obj[field]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
11
24
|
def initialize(**)
|
|
12
25
|
super
|
|
13
26
|
@persisted = false
|
|
@@ -33,7 +46,7 @@ module PlanMyStuff
|
|
|
33
46
|
# @return [Object]
|
|
34
47
|
#
|
|
35
48
|
def read_field(obj, field)
|
|
36
|
-
|
|
49
|
+
self.class.read_field(obj, field)
|
|
37
50
|
end
|
|
38
51
|
end
|
|
39
52
|
end
|
|
@@ -33,10 +33,11 @@ module PlanMyStuff
|
|
|
33
33
|
#
|
|
34
34
|
# @param metadata [BaseMetadata]
|
|
35
35
|
# @param hash [Hash]
|
|
36
|
+
# @param custom_fields_schema [Hash{Symbol => Hash}] merged schema for this context
|
|
36
37
|
#
|
|
37
38
|
# @return [void]
|
|
38
39
|
#
|
|
39
|
-
def apply_common_from_hash(metadata, hash)
|
|
40
|
+
def apply_common_from_hash(metadata, hash, custom_fields_schema)
|
|
40
41
|
metadata.schema_version = hash[:schema_version]
|
|
41
42
|
metadata.gem_version = hash[:gem_version]
|
|
42
43
|
metadata.rails_env = hash[:rails_env]
|
|
@@ -46,7 +47,7 @@ module PlanMyStuff
|
|
|
46
47
|
metadata.created_by = hash[:created_by]
|
|
47
48
|
metadata.visibility = hash.fetch(:visibility, 'internal')
|
|
48
49
|
metadata.custom_fields = CustomFields.new(
|
|
49
|
-
|
|
50
|
+
custom_fields_schema,
|
|
50
51
|
hash[:custom_fields] || {},
|
|
51
52
|
)
|
|
52
53
|
end
|
|
@@ -57,10 +58,17 @@ module PlanMyStuff
|
|
|
57
58
|
# @param user [Object, Integer] user object or user_id
|
|
58
59
|
# @param visibility [String] "public" or "internal"
|
|
59
60
|
# @param custom_fields_data [Hash]
|
|
61
|
+
# @param custom_fields_schema [Hash{Symbol => Hash}] merged schema for this context
|
|
60
62
|
#
|
|
61
63
|
# @return [void]
|
|
62
64
|
#
|
|
63
|
-
def apply_common_build(
|
|
65
|
+
def apply_common_build(
|
|
66
|
+
metadata,
|
|
67
|
+
user:,
|
|
68
|
+
visibility: 'internal',
|
|
69
|
+
custom_fields_data: {},
|
|
70
|
+
custom_fields_schema: {}
|
|
71
|
+
)
|
|
64
72
|
config = PlanMyStuff.configuration
|
|
65
73
|
now = Time.now.utc
|
|
66
74
|
|
|
@@ -74,7 +82,7 @@ module PlanMyStuff
|
|
|
74
82
|
metadata.created_by = resolved.present? ? UserResolver.user_id(resolved) : nil
|
|
75
83
|
metadata.visibility = visibility
|
|
76
84
|
metadata.custom_fields = CustomFields.new(
|
|
77
|
-
|
|
85
|
+
custom_fields_schema,
|
|
78
86
|
custom_fields_data,
|
|
79
87
|
)
|
|
80
88
|
end
|
|
@@ -92,6 +100,7 @@ module PlanMyStuff
|
|
|
92
100
|
|
|
93
101
|
def initialize
|
|
94
102
|
@visibility = 'internal'
|
|
103
|
+
@custom_fields = {}
|
|
95
104
|
end
|
|
96
105
|
|
|
97
106
|
# @return [Hash]
|
|
@@ -119,6 +128,16 @@ module PlanMyStuff
|
|
|
119
128
|
visibility == 'public'
|
|
120
129
|
end
|
|
121
130
|
|
|
131
|
+
# Validates custom fields against the schema.
|
|
132
|
+
#
|
|
133
|
+
# @raise [ActiveModel::ValidationError] if validation fails
|
|
134
|
+
#
|
|
135
|
+
# @return [true]
|
|
136
|
+
#
|
|
137
|
+
def validate_custom_fields!
|
|
138
|
+
custom_fields.validate! if custom_fields.is_a?(PlanMyStuff::CustomFields)
|
|
139
|
+
end
|
|
140
|
+
|
|
122
141
|
# @return [String]
|
|
123
142
|
def to_json(...)
|
|
124
143
|
to_h.to_json(...)
|
data/lib/plan_my_stuff/client.rb
CHANGED
|
@@ -70,34 +70,14 @@ module PlanMyStuff
|
|
|
70
70
|
|
|
71
71
|
# Resolves a repo param to a full "Org/Repo" string.
|
|
72
72
|
#
|
|
73
|
-
# @param repo [Symbol, String, nil] repo key, full string, or nil for default
|
|
73
|
+
# @param repo [Symbol, String, PlanMyStuff::Repo, nil] repo key, full string, Repo instance, or nil for default
|
|
74
74
|
#
|
|
75
75
|
# @return [String] full repo path (e.g. "BrandsInsurance/Element")
|
|
76
76
|
#
|
|
77
77
|
# @raise [ArgumentError] if repo cannot be resolved
|
|
78
78
|
#
|
|
79
79
|
def resolve_repo(repo = nil)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if repo.nil?
|
|
83
|
-
raise(
|
|
84
|
-
PlanMyStuff::ConfigurationError,
|
|
85
|
-
'No repo provided and config.default_repo is not set. ' \
|
|
86
|
-
'Either pass repo: explicitly or set config.default_repo in your initializer.',
|
|
87
|
-
)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
case repo
|
|
91
|
-
when Symbol
|
|
92
|
-
resolved = PlanMyStuff.configuration.repos[repo]
|
|
93
|
-
raise(ArgumentError, "Unknown repo key: #{repo.inspect}") if resolved.nil?
|
|
94
|
-
|
|
95
|
-
resolved
|
|
96
|
-
when String
|
|
97
|
-
repo
|
|
98
|
-
else
|
|
99
|
-
raise(ArgumentError, "Cannot resolve repo: #{repo.inspect}")
|
|
100
|
-
end
|
|
80
|
+
PlanMyStuff::Repo.resolve(repo).full_name
|
|
101
81
|
end
|
|
102
82
|
|
|
103
83
|
private
|
|
@@ -52,6 +52,7 @@ module PlanMyStuff
|
|
|
52
52
|
custom_fields: custom_fields,
|
|
53
53
|
issue_body: issue_body,
|
|
54
54
|
)
|
|
55
|
+
comment_metadata.validate_custom_fields!
|
|
55
56
|
|
|
56
57
|
header = build_header(resolved_user)
|
|
57
58
|
full_body = "#{header}\n\n#{body}"
|
|
@@ -217,6 +218,7 @@ module PlanMyStuff
|
|
|
217
218
|
raise_if_stale!
|
|
218
219
|
|
|
219
220
|
new_body = attrs[:body] || body
|
|
221
|
+
new_body = preserve_header(new_body) if attrs.key?(:body)
|
|
220
222
|
meta_hash = metadata.to_h
|
|
221
223
|
|
|
222
224
|
if attrs.key?(:visibility)
|
|
@@ -293,6 +295,19 @@ module PlanMyStuff
|
|
|
293
295
|
|
|
294
296
|
private
|
|
295
297
|
|
|
298
|
+
# Prepends the existing header to new_body if the comment currently has one.
|
|
299
|
+
#
|
|
300
|
+
# @param new_body [String]
|
|
301
|
+
#
|
|
302
|
+
# @return [String]
|
|
303
|
+
#
|
|
304
|
+
def preserve_header(new_body)
|
|
305
|
+
existing_header = header
|
|
306
|
+
return new_body if existing_header.blank?
|
|
307
|
+
|
|
308
|
+
"#{existing_header}\n\n#{new_body}"
|
|
309
|
+
end
|
|
310
|
+
|
|
296
311
|
# Populates this instance from a GitHub API response.
|
|
297
312
|
#
|
|
298
313
|
# @param github_comment [Object] Octokit comment response
|
|
@@ -14,7 +14,7 @@ module PlanMyStuff
|
|
|
14
14
|
#
|
|
15
15
|
def from_hash(hash)
|
|
16
16
|
metadata = new
|
|
17
|
-
apply_common_from_hash(metadata, hash)
|
|
17
|
+
apply_common_from_hash(metadata, hash, PlanMyStuff.configuration.custom_fields_for(:comment))
|
|
18
18
|
metadata.issue_body = hash[:issue_body] || false
|
|
19
19
|
|
|
20
20
|
metadata
|
|
@@ -31,7 +31,13 @@ module PlanMyStuff
|
|
|
31
31
|
#
|
|
32
32
|
def build(user:, visibility: 'internal', custom_fields: {}, issue_body: false)
|
|
33
33
|
metadata = new
|
|
34
|
-
apply_common_build(
|
|
34
|
+
apply_common_build(
|
|
35
|
+
metadata,
|
|
36
|
+
user: user,
|
|
37
|
+
visibility: visibility,
|
|
38
|
+
custom_fields_data: custom_fields,
|
|
39
|
+
custom_fields_schema: PlanMyStuff.configuration.custom_fields_for(:comment),
|
|
40
|
+
)
|
|
35
41
|
metadata.issue_body = issue_body
|
|
36
42
|
|
|
37
43
|
metadata
|
|
@@ -70,13 +70,28 @@ module PlanMyStuff
|
|
|
70
70
|
# @return [String, nil] recipient address for built-in deferred request notifications
|
|
71
71
|
attr_accessor :deferred_email_to
|
|
72
72
|
|
|
73
|
-
#
|
|
73
|
+
# Shared field definitions stored in issue/comment metadata.
|
|
74
74
|
# Keys are field names, values are hashes with :type and :required.
|
|
75
|
+
# These fields apply to both issues and comments.
|
|
75
76
|
#
|
|
76
77
|
# @return [Hash{Symbol => Hash}]
|
|
77
78
|
#
|
|
78
79
|
attr_accessor :custom_fields
|
|
79
80
|
|
|
81
|
+
# Issue-only field definitions, deep-merged on top of shared custom_fields.
|
|
82
|
+
# Context-specific config wins on key conflicts.
|
|
83
|
+
#
|
|
84
|
+
# @return [Hash{Symbol => Hash}]
|
|
85
|
+
#
|
|
86
|
+
attr_accessor :issue_custom_fields
|
|
87
|
+
|
|
88
|
+
# Comment-only field definitions, deep-merged on top of shared custom_fields.
|
|
89
|
+
# Context-specific config wins on key conflicts.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash{Symbol => Hash}]
|
|
92
|
+
#
|
|
93
|
+
attr_accessor :comment_custom_fields
|
|
94
|
+
|
|
80
95
|
# @return [String, nil] URL prefix for building user-facing ticket URLs in the consuming app
|
|
81
96
|
attr_accessor :issues_url_prefix
|
|
82
97
|
|
|
@@ -100,6 +115,8 @@ module PlanMyStuff
|
|
|
100
115
|
@markdown_options = {}
|
|
101
116
|
@job_classes = {}
|
|
102
117
|
@custom_fields = {}
|
|
118
|
+
@issue_custom_fields = {}
|
|
119
|
+
@comment_custom_fields = {}
|
|
103
120
|
end
|
|
104
121
|
|
|
105
122
|
# Sets the authentication block for engine controllers.
|
|
@@ -132,6 +149,24 @@ module PlanMyStuff
|
|
|
132
149
|
"Missing required PlanMyStuff configuration: #{missing.join(', ')}",
|
|
133
150
|
)
|
|
134
151
|
end
|
|
152
|
+
|
|
153
|
+
# Returns the merged custom fields schema for the given context.
|
|
154
|
+
# Context-specific fields deep-merge on top of shared fields.
|
|
155
|
+
#
|
|
156
|
+
# @param context [Symbol] :issue or :comment
|
|
157
|
+
#
|
|
158
|
+
# @return [Hash{Symbol => Hash}]
|
|
159
|
+
#
|
|
160
|
+
def custom_fields_for(context)
|
|
161
|
+
context_fields =
|
|
162
|
+
case context
|
|
163
|
+
when :issue then issue_custom_fields
|
|
164
|
+
when :comment then comment_custom_fields
|
|
165
|
+
else {}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
custom_fields.deep_merge(context_fields)
|
|
169
|
+
end
|
|
135
170
|
end
|
|
136
171
|
|
|
137
172
|
class ConfigurationError < StandardError
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_model'
|
|
4
|
+
|
|
3
5
|
module PlanMyStuff
|
|
4
6
|
# Dynamic accessor object for app-defined custom fields stored in metadata.
|
|
5
7
|
# Backed by the config.custom_fields schema, provides both hash-style and
|
|
6
8
|
# method-style access to field values.
|
|
9
|
+
#
|
|
10
|
+
# Includes ActiveModel::Validations for type checking, required field
|
|
11
|
+
# enforcement, and unknown field detection.
|
|
7
12
|
class CustomFields
|
|
13
|
+
TYPE_MAP = {
|
|
14
|
+
string: [String],
|
|
15
|
+
integer: [Integer],
|
|
16
|
+
boolean: [TrueClass, FalseClass],
|
|
17
|
+
array: [Array],
|
|
18
|
+
hash: [Hash],
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
include ActiveModel::Validations
|
|
22
|
+
|
|
23
|
+
validate :validate_custom_fields
|
|
24
|
+
|
|
25
|
+
# @return [Hash{Symbol => Hash}]
|
|
26
|
+
attr_reader :schema
|
|
27
|
+
|
|
8
28
|
# @param schema [Hash{Symbol => Hash}] field definitions from config.custom_fields
|
|
9
29
|
# @param data [Hash] parsed field data from metadata JSON
|
|
10
30
|
#
|
|
@@ -61,5 +81,55 @@ module PlanMyStuff
|
|
|
61
81
|
|
|
62
82
|
super
|
|
63
83
|
end
|
|
84
|
+
|
|
85
|
+
# @return [void]
|
|
86
|
+
def validate_custom_fields
|
|
87
|
+
validate_unknown_fields
|
|
88
|
+
validate_required_fields
|
|
89
|
+
validate_field_types
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [void]
|
|
93
|
+
def validate_unknown_fields
|
|
94
|
+
known_keys = @schema.keys
|
|
95
|
+
@data.each_key do |key|
|
|
96
|
+
if known_keys.exclude?(key)
|
|
97
|
+
errors.add(:base, "#{key} is not a recognized custom field")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [void]
|
|
103
|
+
def validate_required_fields
|
|
104
|
+
@schema.each do |field_name, field_config|
|
|
105
|
+
if field_config[:required] && !@data.key?(field_name)
|
|
106
|
+
errors.add(:base, "#{field_name} is required")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @return [void]
|
|
112
|
+
def validate_field_types
|
|
113
|
+
@schema.each do |field_name, field_config|
|
|
114
|
+
next unless @data.key?(field_name)
|
|
115
|
+
|
|
116
|
+
value = @data[field_name]
|
|
117
|
+
next if value.nil?
|
|
118
|
+
|
|
119
|
+
expected_type = field_config[:type]
|
|
120
|
+
ruby_types = TYPE_MAP[expected_type]
|
|
121
|
+
next if ruby_types.nil?
|
|
122
|
+
next if ruby_types.any? { |t| value.is_a?(t) }
|
|
123
|
+
|
|
124
|
+
expected_name =
|
|
125
|
+
if expected_type == :boolean
|
|
126
|
+
'TrueClass/FalseClass'
|
|
127
|
+
else
|
|
128
|
+
ruby_types.first.name
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
errors.add(:base, "#{field_name} must be a #{expected_name}, got #{value.class}")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
64
134
|
end
|
|
65
135
|
end
|
data/lib/plan_my_stuff/issue.rb
CHANGED
|
@@ -24,8 +24,8 @@ module PlanMyStuff
|
|
|
24
24
|
attr_accessor :state
|
|
25
25
|
# @return [Array<String>] label names
|
|
26
26
|
attr_accessor :labels
|
|
27
|
-
# @return [
|
|
28
|
-
|
|
27
|
+
# @return [PlanMyStuff::Repo, nil]
|
|
28
|
+
attr_reader :repo
|
|
29
29
|
|
|
30
30
|
class << self
|
|
31
31
|
# Creates a GitHub issue with PMS metadata embedded in the body.
|
|
@@ -61,6 +61,7 @@ module PlanMyStuff
|
|
|
61
61
|
custom_fields: metadata,
|
|
62
62
|
)
|
|
63
63
|
issue_metadata.visibility_allowlist = Array.wrap(visibility_allowlist)
|
|
64
|
+
issue_metadata.validate_custom_fields!
|
|
64
65
|
|
|
65
66
|
serialized_body = MetadataParser.serialize(issue_metadata.to_h, '')
|
|
66
67
|
|
|
@@ -68,8 +69,9 @@ module PlanMyStuff
|
|
|
68
69
|
options[:labels] = labels if labels.any?
|
|
69
70
|
|
|
70
71
|
result = client.rest(:create_issue, resolved_repo, title, serialized_body, **options)
|
|
72
|
+
number = read_field(result, :number)
|
|
71
73
|
|
|
72
|
-
issue =
|
|
74
|
+
issue = find(number, repo: resolved_repo)
|
|
73
75
|
|
|
74
76
|
if add_to_project.present?
|
|
75
77
|
project_number = resolve_project_number(add_to_project)
|
|
@@ -119,6 +121,10 @@ module PlanMyStuff
|
|
|
119
121
|
merged_custom_fields = (existing_metadata[:custom_fields] || {}).merge(metadata[:custom_fields] || {})
|
|
120
122
|
existing_metadata = existing_metadata.merge(metadata)
|
|
121
123
|
existing_metadata[:custom_fields] = merged_custom_fields
|
|
124
|
+
PlanMyStuff::CustomFields.new(
|
|
125
|
+
PlanMyStuff.configuration.custom_fields_for(:issue),
|
|
126
|
+
merged_custom_fields,
|
|
127
|
+
).validate!
|
|
122
128
|
|
|
123
129
|
existing_metadata[:updated_at] = Time.now.utc.iso8601
|
|
124
130
|
options[:body] = MetadataParser.serialize(existing_metadata, '')
|
|
@@ -264,16 +270,7 @@ module PlanMyStuff
|
|
|
264
270
|
body_comment = issue.body_comment
|
|
265
271
|
raise(PlanMyStuff::Error, "No body comment found on issue ##{number}") if body_comment.nil?
|
|
266
272
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
updated_body =
|
|
270
|
-
if header.present?
|
|
271
|
-
"#{header}\n\n#{new_body}"
|
|
272
|
-
else
|
|
273
|
-
new_body
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
body_comment.update!(body: updated_body)
|
|
273
|
+
body_comment.update!(body: new_body)
|
|
277
274
|
end
|
|
278
275
|
end
|
|
279
276
|
|
|
@@ -285,6 +282,11 @@ module PlanMyStuff
|
|
|
285
282
|
@labels ||= []
|
|
286
283
|
end
|
|
287
284
|
|
|
285
|
+
# @param value [PlanMyStuff::Repo, Symbol, String, nil]
|
|
286
|
+
def repo=(value)
|
|
287
|
+
@repo = value.present? ? PlanMyStuff::Repo.resolve(value) : nil
|
|
288
|
+
end
|
|
289
|
+
|
|
288
290
|
# Persists the issue. Creates if new, updates if persisted.
|
|
289
291
|
#
|
|
290
292
|
# @raise [PlanMyStuff::StaleObjectError] on update if stale
|
|
@@ -411,7 +413,7 @@ module PlanMyStuff
|
|
|
411
413
|
@state = read_field(github_issue, :state)
|
|
412
414
|
@raw_body = read_field(github_issue, :body) || ''
|
|
413
415
|
@labels = extract_labels(github_issue)
|
|
414
|
-
|
|
416
|
+
self.repo = repo
|
|
415
417
|
|
|
416
418
|
parsed = MetadataParser.parse(@raw_body)
|
|
417
419
|
@metadata = IssueMetadata.from_hash(parsed[:metadata])
|
|
@@ -22,7 +22,7 @@ module PlanMyStuff
|
|
|
22
22
|
#
|
|
23
23
|
def from_hash(hash)
|
|
24
24
|
metadata = new
|
|
25
|
-
apply_common_from_hash(metadata, hash)
|
|
25
|
+
apply_common_from_hash(metadata, hash, PlanMyStuff.configuration.custom_fields_for(:issue))
|
|
26
26
|
|
|
27
27
|
metadata.responded_at = parse_time(hash[:responded_at])
|
|
28
28
|
metadata.issues_url = hash[:issues_url]
|
|
@@ -42,7 +42,13 @@ module PlanMyStuff
|
|
|
42
42
|
#
|
|
43
43
|
def build(user:, visibility: 'public', custom_fields: {})
|
|
44
44
|
metadata = new
|
|
45
|
-
apply_common_build(
|
|
45
|
+
apply_common_build(
|
|
46
|
+
metadata,
|
|
47
|
+
user: user,
|
|
48
|
+
visibility: visibility,
|
|
49
|
+
custom_fields_data: custom_fields,
|
|
50
|
+
custom_fields_schema: PlanMyStuff.configuration.custom_fields_for(:issue),
|
|
51
|
+
)
|
|
46
52
|
|
|
47
53
|
metadata.responded_at = nil
|
|
48
54
|
metadata.issues_url = build_issues_url(PlanMyStuff.configuration)
|
|
@@ -90,20 +90,20 @@ module PlanMyStuff
|
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
# Resolves a project number, falling back to config.default_project_number.
|
|
94
|
+
#
|
|
95
|
+
# @param project_number [Integer, nil]
|
|
96
|
+
#
|
|
97
|
+
# @return [Integer]
|
|
98
|
+
#
|
|
99
|
+
def resolve_default_project_number(project_number)
|
|
100
|
+
return project_number if project_number.present?
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
# @return [Integer]
|
|
100
|
-
#
|
|
101
|
-
def resolve_default_project_number(project_number)
|
|
102
|
-
return project_number if project_number.present?
|
|
102
|
+
PlanMyStuff.configuration.default_project_number ||
|
|
103
|
+
raise(ArgumentError, 'project_number is required when config.default_project_number is not set')
|
|
104
|
+
end
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
raise(ArgumentError, 'project_number is required when config.default_project_number is not set')
|
|
106
|
-
end
|
|
106
|
+
private
|
|
107
107
|
|
|
108
108
|
# @return [String]
|
|
109
109
|
def list_query
|
|
@@ -170,6 +170,7 @@ module PlanMyStuff
|
|
|
170
170
|
number
|
|
171
171
|
url
|
|
172
172
|
state
|
|
173
|
+
repository { nameWithOwner }
|
|
173
174
|
}
|
|
174
175
|
... on PullRequest {
|
|
175
176
|
id
|
|
@@ -177,6 +178,7 @@ module PlanMyStuff
|
|
|
177
178
|
number
|
|
178
179
|
url
|
|
179
180
|
state
|
|
181
|
+
repository { nameWithOwner }
|
|
180
182
|
}
|
|
181
183
|
... on DraftIssue {
|
|
182
184
|
id
|
|
@@ -342,6 +344,7 @@ module PlanMyStuff
|
|
|
342
344
|
def parse_project_item(item)
|
|
343
345
|
content = item[:content] || {}
|
|
344
346
|
field_values = item.dig(:fieldValues, :nodes) || []
|
|
347
|
+
repo_name = content.dig(:repository, :nameWithOwner)
|
|
345
348
|
|
|
346
349
|
{
|
|
347
350
|
id: item[:id],
|
|
@@ -351,6 +354,7 @@ module PlanMyStuff
|
|
|
351
354
|
number: content[:number],
|
|
352
355
|
url: content[:url],
|
|
353
356
|
state: content[:state],
|
|
357
|
+
repo: repo_name.present? ? PlanMyStuff::Repo.resolve(repo_name) : nil,
|
|
354
358
|
status: extract_item_status(field_values),
|
|
355
359
|
field_values: parse_field_values(field_values),
|
|
356
360
|
}
|
|
@@ -29,6 +29,8 @@ module PlanMyStuff
|
|
|
29
29
|
attr_accessor :number
|
|
30
30
|
# @return [String, nil]
|
|
31
31
|
attr_accessor :url
|
|
32
|
+
# @return [PlanMyStuff::Repo, nil]
|
|
33
|
+
attr_accessor :repo
|
|
32
34
|
# @return [String, nil]
|
|
33
35
|
attr_accessor :state
|
|
34
36
|
# @return [String, nil]
|
|
@@ -38,7 +40,7 @@ module PlanMyStuff
|
|
|
38
40
|
# @return [PlanMyStuff::Project, nil]
|
|
39
41
|
attr_accessor :project
|
|
40
42
|
# @return [PlanMyStuff::Issue, nil] linked issue (nil for draft items)
|
|
41
|
-
|
|
43
|
+
attr_writer :issue
|
|
42
44
|
|
|
43
45
|
class << self
|
|
44
46
|
# Builds a persisted ProjectItem from parsed item data.
|
|
@@ -56,6 +58,7 @@ module PlanMyStuff
|
|
|
56
58
|
title: item_hash[:title],
|
|
57
59
|
number: item_hash[:number],
|
|
58
60
|
url: item_hash[:url],
|
|
61
|
+
repo: item_hash[:repo],
|
|
59
62
|
state: item_hash[:state],
|
|
60
63
|
status: item_hash[:status],
|
|
61
64
|
field_values: item_hash[:field_values] || {},
|
|
@@ -170,6 +173,7 @@ module PlanMyStuff
|
|
|
170
173
|
item = build(
|
|
171
174
|
{
|
|
172
175
|
id: item_data[:id],
|
|
176
|
+
content_node_id: node_id,
|
|
173
177
|
title: issue.title,
|
|
174
178
|
number: issue.number,
|
|
175
179
|
url: nil,
|
|
@@ -210,6 +214,8 @@ module PlanMyStuff
|
|
|
210
214
|
build(
|
|
211
215
|
{
|
|
212
216
|
id: item_data[:id],
|
|
217
|
+
content_node_id: item_data.dig(:content, :id),
|
|
218
|
+
type: 'DRAFT_ISSUE',
|
|
213
219
|
title: title,
|
|
214
220
|
number: nil,
|
|
215
221
|
url: nil,
|
|
@@ -221,17 +227,9 @@ module PlanMyStuff
|
|
|
221
227
|
)
|
|
222
228
|
end
|
|
223
229
|
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
# @param project_number [Integer, nil]
|
|
227
|
-
#
|
|
228
|
-
# @return [Integer]
|
|
229
|
-
#
|
|
230
|
+
# @see PlanMyStuff::Project.resolve_default_project_number
|
|
230
231
|
def resolve_default_project_number(project_number)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
PlanMyStuff.configuration.default_project_number ||
|
|
234
|
-
raise(ArgumentError, 'project_number is required when config.default_project_number is not set')
|
|
232
|
+
PlanMyStuff::Project.resolve_default_project_number(project_number)
|
|
235
233
|
end
|
|
236
234
|
|
|
237
235
|
# @return [String]
|
|
@@ -261,6 +259,7 @@ module PlanMyStuff
|
|
|
261
259
|
}) {
|
|
262
260
|
projectItem {
|
|
263
261
|
id
|
|
262
|
+
content { ... on DraftIssue { id } }
|
|
264
263
|
}
|
|
265
264
|
}
|
|
266
265
|
}
|
|
@@ -393,6 +392,7 @@ module PlanMyStuff
|
|
|
393
392
|
content_node_id: content_node_id,
|
|
394
393
|
assignees: Array.wrap(assignees),
|
|
395
394
|
draft: draft?,
|
|
395
|
+
repo: repo,
|
|
396
396
|
)
|
|
397
397
|
end
|
|
398
398
|
|
|
@@ -401,6 +401,14 @@ module PlanMyStuff
|
|
|
401
401
|
type == 'DRAFT_ISSUE'
|
|
402
402
|
end
|
|
403
403
|
|
|
404
|
+
# @return [PlanMyStuff::Issue, nil]
|
|
405
|
+
def issue
|
|
406
|
+
return @issue if defined?(@issue)
|
|
407
|
+
return if draft?
|
|
408
|
+
|
|
409
|
+
@issue = PMS::Issue.find(number, repo: repo)
|
|
410
|
+
end
|
|
411
|
+
|
|
404
412
|
private
|
|
405
413
|
|
|
406
414
|
# Marks this record as persisted.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
class Repo
|
|
5
|
+
# @return [Symbol, nil] configured key (e.g. :my_repo)
|
|
6
|
+
attr_reader :key
|
|
7
|
+
|
|
8
|
+
# @return [String] repo name (e.g. "MyRepository")
|
|
9
|
+
attr_reader :name
|
|
10
|
+
|
|
11
|
+
# @return [String] organization name (e.g. "YourOrgName")
|
|
12
|
+
attr_reader :organization
|
|
13
|
+
|
|
14
|
+
delegate :split, to: :to_s
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Builds a Repo instance from a Symbol key, full name String, or nil (default).
|
|
18
|
+
#
|
|
19
|
+
# @param repo [Symbol, String, PlanMyStuff::Repo, nil]
|
|
20
|
+
#
|
|
21
|
+
# @return [PlanMyStuff::Repo]
|
|
22
|
+
#
|
|
23
|
+
def resolve(repo = nil)
|
|
24
|
+
return repo if repo.is_a?(PlanMyStuff::Repo)
|
|
25
|
+
|
|
26
|
+
repo ||= PlanMyStuff.configuration.default_repo
|
|
27
|
+
|
|
28
|
+
if repo.nil?
|
|
29
|
+
raise(
|
|
30
|
+
PlanMyStuff::ConfigurationError,
|
|
31
|
+
'No repo provided and config.default_repo is not set. ' \
|
|
32
|
+
'Either pass repo: explicitly or set config.default_repo in your initializer.',
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
case repo
|
|
37
|
+
when Symbol
|
|
38
|
+
full_name = PlanMyStuff.configuration.repos[repo]
|
|
39
|
+
raise(ArgumentError, "Unknown repo key: #{repo.inspect}") if full_name.nil?
|
|
40
|
+
|
|
41
|
+
from_full_name(full_name, key: repo)
|
|
42
|
+
when String
|
|
43
|
+
key = PlanMyStuff.configuration.repos.key(repo)
|
|
44
|
+
from_full_name(repo, key: key)
|
|
45
|
+
else
|
|
46
|
+
raise(ArgumentError, "Cannot resolve repo: #{repo.inspect}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# @param full_name [String] e.g. "YourOrgName/MyRepository"
|
|
53
|
+
# @param key [Symbol, nil]
|
|
54
|
+
#
|
|
55
|
+
# @return [PlanMyStuff::Repo]
|
|
56
|
+
#
|
|
57
|
+
def from_full_name(full_name, key: nil)
|
|
58
|
+
org, name = full_name.split('/', 2)
|
|
59
|
+
|
|
60
|
+
raise(ArgumentError, "Invalid repo full_name: #{full_name.inspect}") if name.nil?
|
|
61
|
+
|
|
62
|
+
new(name: name, organization: org, key: key)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param name [String]
|
|
67
|
+
# @param organization [String]
|
|
68
|
+
# @param key [Symbol, nil]
|
|
69
|
+
#
|
|
70
|
+
def initialize(name:, organization:, key: nil)
|
|
71
|
+
@key = key
|
|
72
|
+
@name = name
|
|
73
|
+
@organization = organization
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [String] full repo path (e.g. "YourOrgName/MyRepository")
|
|
77
|
+
def full_name
|
|
78
|
+
"#{organization}/#{name}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @see #full_name
|
|
82
|
+
alias to_s full_name
|
|
83
|
+
|
|
84
|
+
# Enables implicit string coercion so Repo instances behave as
|
|
85
|
+
# strings when passed to Octokit or compared with String#==.
|
|
86
|
+
#
|
|
87
|
+
# @see #full_name
|
|
88
|
+
alias to_str full_name
|
|
89
|
+
|
|
90
|
+
# Compares by full_name. Accepts another Repo or a String.
|
|
91
|
+
#
|
|
92
|
+
# @param other [PlanMyStuff::Repo, String, Object]
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
#
|
|
96
|
+
def ==(other)
|
|
97
|
+
case other
|
|
98
|
+
when PlanMyStuff::Repo
|
|
99
|
+
full_name == other.full_name
|
|
100
|
+
when String
|
|
101
|
+
full_name == other
|
|
102
|
+
else
|
|
103
|
+
super
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -316,10 +316,17 @@ module PlanMyStuff
|
|
|
316
316
|
},
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
resolved_repo =
|
|
320
|
+
if params[:repo]
|
|
321
|
+
PlanMyStuff::Repo.resolve(params[:repo]).full_name
|
|
322
|
+
else
|
|
323
|
+
'TestOrg/TestRepo'
|
|
324
|
+
end
|
|
325
|
+
|
|
319
326
|
PlanMyStuff::TestHelpers.build_issue(
|
|
320
327
|
title: params[:title],
|
|
321
328
|
body: params[:body],
|
|
322
|
-
repo:
|
|
329
|
+
repo: resolved_repo,
|
|
323
330
|
labels: params[:labels] || [],
|
|
324
331
|
)
|
|
325
332
|
end
|
|
@@ -330,7 +337,8 @@ module PlanMyStuff
|
|
|
330
337
|
params: { number: number, repo: repo },
|
|
331
338
|
}
|
|
332
339
|
|
|
333
|
-
PlanMyStuff::
|
|
340
|
+
resolved_repo = repo ? PlanMyStuff::Repo.resolve(repo).full_name : 'TestOrg/TestRepo'
|
|
341
|
+
PlanMyStuff::TestHelpers.build_issue(number: number, repo: resolved_repo)
|
|
334
342
|
end
|
|
335
343
|
|
|
336
344
|
issue_mod.define_singleton_method(:list) do |**params|
|
data/lib/plan_my_stuff.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative 'plan_my_stuff/markdown'
|
|
|
19
19
|
require_relative 'plan_my_stuff/metadata_parser'
|
|
20
20
|
require_relative 'plan_my_stuff/project'
|
|
21
21
|
require_relative 'plan_my_stuff/project_item'
|
|
22
|
+
require_relative 'plan_my_stuff/repo'
|
|
22
23
|
require_relative 'plan_my_stuff/user_resolver'
|
|
23
24
|
require_relative 'plan_my_stuff/verifier'
|
|
24
25
|
require_relative 'plan_my_stuff/version'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: plan_my_stuff
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brands Insurance
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -51,6 +51,7 @@ executables: []
|
|
|
51
51
|
extensions: []
|
|
52
52
|
extra_rdoc_files: []
|
|
53
53
|
files:
|
|
54
|
+
- CHANGELOG.md
|
|
54
55
|
- LICENSE
|
|
55
56
|
- README.md
|
|
56
57
|
- app/controllers/plan_my_stuff/application_controller.rb
|
|
@@ -91,6 +92,7 @@ files:
|
|
|
91
92
|
- lib/plan_my_stuff/metadata_parser.rb
|
|
92
93
|
- lib/plan_my_stuff/project.rb
|
|
93
94
|
- lib/plan_my_stuff/project_item.rb
|
|
95
|
+
- lib/plan_my_stuff/repo.rb
|
|
94
96
|
- lib/plan_my_stuff/test_helpers.rb
|
|
95
97
|
- lib/plan_my_stuff/user_resolver.rb
|
|
96
98
|
- lib/plan_my_stuff/verifier.rb
|