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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b39dc4e08c17c126407b4b96f698d4718b77f9485f251af2f7a1c642e58a1b8
4
- data.tar.gz: 335dbb910692194950e6ac9bcc904d302b67ddf34d4cf0e21611dd3bf36d675d
3
+ metadata.gz: 0fdaa17653ccaf192c32d3ba21c5eb0ea4954b2a983d1d0d8f3665a9a792d065
4
+ data.tar.gz: d61fb2fba100a20b0e2f40744e2e6c4c936ff4d55c2cbbdf428f8f6d30ed4ea8
5
5
  SHA512:
6
- metadata.gz: 0f9b3a98fd6085704eafe6bf19a90bca60010f7004c28913b6356d96cef8ac08145f82e12a389fde0adb40c2ffa4eb7aca1aa3ea9f2065ed3549a28f6eacffdd
7
- data.tar.gz: 303c6bec1248313856b9dad67234a77344ac1e9ea66e2c90361e498524c9f46f9da142a5c278f2866806be1437fc701097699c67c86890e02757c31632a657cc
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 { |id| id.strip.presence&.to_i }
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]&.to_sym)
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 redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number)) unless can_edit?(@comment)
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
- return redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number)) unless can_edit?(@comment)
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]&.to_sym)
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]&.to_sym
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]&.to_sym)
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]&.to_sym)
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]&.to_sym)
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]&.to_sym)
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]&.to_sym)
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
- return redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id])) unless support_user?
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]&.to_sym)
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
- return redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id])) unless support_user?
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]&.to_sym)
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]&.to_sym)
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]&.to_sym)
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
- <% persisted = issue.persisted? %>
2
- <%= form_with(
3
- url: persisted ? plan_my_stuff.issue_path(issue.number) : plan_my_stuff.issues_path,
4
- method: persisted ? :patch : :post,
5
- scope: :issue,
6
- ) do |form| %>
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
- <% if item.number.present? %>
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
- <% if item.number.present? %>
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
- obj.respond_to?(field) ? obj.public_send(field) : obj[field]
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
- PlanMyStuff.configuration.custom_fields,
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(metadata, user:, visibility: 'internal', custom_fields_data: {})
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
- config.custom_fields,
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(...)
@@ -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
- repo ||= PlanMyStuff.configuration.default_repo
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(metadata, user: user, visibility: visibility, custom_fields_data: custom_fields)
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
- # App-defined field definitions stored in issue/comment metadata.
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
@@ -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 [String] resolved repo path (e.g. "Org/Repo")
28
- attr_accessor :repo
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 = build(result, repo: resolved_repo)
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
- header = body_comment.header
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
- @repo = repo
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(metadata, user: user, visibility: visibility, custom_fields_data: custom_fields)
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)
@@ -26,7 +26,7 @@ module PlanMyStuff
26
26
  when :redcarpet
27
27
  render_redcarpet(text, merged)
28
28
  when nil
29
- "<code>#{text}</code>"
29
+ "<code>#{ERB::Util.html_escape(text)}</code>"
30
30
  else
31
31
  raise(
32
32
  ArgumentError,
@@ -90,20 +90,20 @@ module PlanMyStuff
90
90
  end
91
91
  end
92
92
 
93
- private
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
- # Resolves a project number, falling back to config.default_project_number.
96
- #
97
- # @param project_number [Integer, nil]
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
- PlanMyStuff.configuration.default_project_number ||
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
- attr_accessor :issue
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
- # Resolves a project number, falling back to config.default_project_number.
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
- return project_number if project_number.present?
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: params[:repo]&.to_s || 'TestOrg/TestRepo',
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::TestHelpers.build_issue(number: number, repo: repo&.to_s || 'TestOrg/TestRepo')
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|
@@ -3,8 +3,8 @@
3
3
  module PlanMyStuff
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 1
7
- TINY = 1
6
+ MINOR = 2
7
+ TINY = 0
8
8
 
9
9
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
10
10
  PRE = nil
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.1.1
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-01 00:00:00.000000000 Z
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