plan_my_stuff 0.17.0 → 0.18.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: fe4f92dc92eca010b6fc7f53f4ce7701c6852420c9a537fdf0902e591c3309ad
4
- data.tar.gz: b167fd42a3b5a37fc897bebf8f3696ed2cd863ac7049359445a329d9a7861988
3
+ metadata.gz: bded03d75c7bd996c62c647b038a588a4286628e7f041e385d4464ea87955505
4
+ data.tar.gz: 5b778d271d85a1ad5681d8617dec41875d5664d0c3ea861ab9421bef962de4d2
5
5
  SHA512:
6
- metadata.gz: 746332e19a507f81dc0ee902a87664dcb0a55e3170d688d878b14faf508bfb67cc5dd3236a4961c6b15768fd47e7489b6b0cdbe5bdd5d328bcf9836f805d561d
7
- data.tar.gz: 26a20b3318c99f34360bba90eee5698242f4043043991bf53d59e55b2507516452a5094c14366b19f6f53a140ff0b8a41f326a7048c13c4077f447eb2a083513
6
+ metadata.gz: db446cfa7623c4091f67cecd89ab7dfe84207237e601fd53f4a73401fc02d2019072801c326d281febb54373817cb87d35c60e2929a2ec425f1aca3e7623ef49
7
+ data.tar.gz: b7f4328f697271829202ee201f4c05a9e1553bc95d50b721cfc3132462c0401428b87e22bd79cdeb4962e249bea991b3f5d39cab9cb1a3c048ef13a647e31312
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Breaking
6
+
7
+ - `PMS::Issue#to_param` now returns `"Nickname-number"` (e.g. `"Rawr-1234"`) instead of the default ActiveModel id.
8
+ Single-issue URLs change from `/issues/1234?repo=rawr` to `/issues/Rawr-1234`. Consuming apps using
9
+ `youtrack_issue_path(@issue)` work natively; any hand-rolled URLs that passed `@issue.number` plus a `repo:`
10
+ query param must switch to `@issue` (or `@issue.to_param`). The mounted engine's controllers and views have
11
+ been updated to the new shape, as has the markdown link embedded in the GitHub issue body via
12
+ `Issue#user_link`.
13
+
14
+ ### Added
15
+
16
+ - `config.repo_nicknames` (default `{}`) -- Symbol-keyed map from repo key to the human-readable label used as
17
+ the `Issue#to_param` prefix. Missing keys fall back to `key.to_s.titleize`, so `:rawr` -> `"Rawr"` is free;
18
+ only divergent ones (e.g. `safety: 'Compliance'`) need an entry.
19
+ - `Configuration#repo_nickname_for(key)` accessor, `Repo#nickname`, and `Repo.from_nickname!` for the inverse
20
+ lookup used by `Issue.from_param`.
21
+ - `Issue#to_param` and `Issue.from_param` -- the latter parses `"Nickname-1234"` back into `[Repo, Integer]`.
22
+ - `Issue.find` first arg now accepts a nickname-id String (e.g. `"Rawr-1234"`) in addition to Integer / digit-
23
+ String + `repo:` kwarg. The nickname form ignores `repo:`.
24
+
3
25
  ## 0.17.0
4
26
 
5
27
  ### Added
data/CONFIGURATION.md CHANGED
@@ -37,13 +37,17 @@ config.organization = 'YourOrg'
37
37
  |---|---|---|---|
38
38
  | `repos` | `Hash{Symbol => String}` | `{}` | Named repo configs mapping a key to an `Org/Repo` string. |
39
39
  | `default_repo` | `Symbol, nil` | `nil` | Repo key used when callers omit the `repo:` param. |
40
+ | `repo_nicknames` | `Hash{Symbol => String}` | `{}` | `Issue#to_param` prefix override (default `key.titleize`). |
40
41
 
41
42
  ```ruby
42
43
  config.repos = { element: 'YourOrg/Element', underwriter: 'YourOrg/Underwriter' }
43
44
  config.default_repo = :element
45
+ config.repo_nicknames = { safety: 'Compliance' } # :element -> "Element", :underwriter -> "Underwriter" come free
44
46
  ```
45
47
 
46
- `repos` can be mutated via `config.repos[:key] = '...'` or set via `config.repos = { key: 'MyOrg/MyRepo' }`
48
+ `repos` can be mutated via `config.repos[:key] = '...'` or set via `config.repos = { key: 'MyOrg/MyRepo' }`.
49
+ `Issue#to_param` then returns `"Element-1234"` / `"Compliance-567"`, encoding both repo and number in a single
50
+ URL segment so `youtrack_issue_path(@issue)` works without a `repo:` query param.
47
51
 
48
52
  ## Projects
49
53
 
@@ -4,7 +4,7 @@ module PlanMyStuff
4
4
  class CommentsController < PlanMyStuff::ApplicationController
5
5
  # POST /issues/:issue_id/comments
6
6
  def create
7
- @issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
7
+ @issue = PlanMyStuff::Issue.find(params[:issue_id])
8
8
 
9
9
  PlanMyStuff::Comment.create!(
10
10
  issue: @issue,
@@ -15,11 +15,11 @@ module PlanMyStuff
15
15
  )
16
16
 
17
17
  flash[:success] = 'Comment was successfully created.'
18
- redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
18
+ redirect_to(plan_my_stuff.issue_path(@issue))
19
19
  rescue PlanMyStuff::LockedIssueError => e
20
20
  pms_handle_rescue(e)
21
21
  flash[:error] = 'This issue is locked; no new comments can be posted.'
22
- redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
22
+ redirect_to(plan_my_stuff.issue_path(@issue))
23
23
  end
24
24
 
25
25
  # GET /issues/:issue_id/comments/:id/edit
@@ -29,7 +29,7 @@ module PlanMyStuff
29
29
  return redirect_to_issue if issue_body_comment?
30
30
  return if can_edit?(@comment)
31
31
 
32
- redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
32
+ redirect_to_unauthorized(plan_my_stuff.issue_path(@issue))
33
33
  end
34
34
 
35
35
  # PATCH/PUT /issues/:issue_id/comments/:id
@@ -39,7 +39,7 @@ module PlanMyStuff
39
39
  return redirect_to_issue if issue_body_comment?
40
40
 
41
41
  unless can_edit?(@comment)
42
- redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
42
+ redirect_to_unauthorized(plan_my_stuff.issue_path(@issue))
43
43
 
44
44
  return
45
45
  end
@@ -50,7 +50,7 @@ module PlanMyStuff
50
50
  @comment.update!(**update_attrs, user: pms_current_user)
51
51
 
52
52
  flash[:success] = 'Comment was successfully updated.'
53
- redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
53
+ redirect_to(plan_my_stuff.issue_path(@issue))
54
54
  rescue PlanMyStuff::StaleObjectError => e
55
55
  pms_handle_rescue(e)
56
56
  flash.now[:error] = 'Comment was modified by someone else. Please review the latest changes and try again.'
@@ -69,7 +69,7 @@ module PlanMyStuff
69
69
  # @return [void]
70
70
  #
71
71
  def load_comment
72
- @issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
72
+ @issue = PlanMyStuff::Issue.find(params[:issue_id])
73
73
  @comment = PlanMyStuff::Comment.find(params[:id].to_i, issue: @issue)
74
74
  end
75
75
 
@@ -96,7 +96,7 @@ module PlanMyStuff
96
96
 
97
97
  # @return [void]
98
98
  def redirect_to_issue
99
- redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
99
+ redirect_to(plan_my_stuff.issue_path(@issue))
100
100
  end
101
101
  end
102
102
  end
@@ -23,7 +23,7 @@ module PlanMyStuff
23
23
  return
24
24
  end
25
25
 
26
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
26
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
27
27
  issue.request_approvals!(user_ids: user_ids, user: pms_current_user)
28
28
 
29
29
  flash[:success] = 'Approvers were successfully added.'
@@ -44,7 +44,7 @@ module PlanMyStuff
44
44
  return
45
45
  end
46
46
 
47
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
47
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
48
48
  caller_id = pms_current_user.present? ? PlanMyStuff::UserResolver.user_id(pms_current_user) : nil
49
49
 
50
50
  case status
@@ -85,7 +85,7 @@ module PlanMyStuff
85
85
  return
86
86
  end
87
87
 
88
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
88
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
89
89
  issue.remove_approvers!(user_ids: [params[:id].to_i], user: pms_current_user)
90
90
 
91
91
  flash[:success] = 'Approver was successfully removed.'
@@ -105,7 +105,7 @@ module PlanMyStuff
105
105
 
106
106
  # @return [String]
107
107
  def show_path
108
- plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo])
108
+ plan_my_stuff.issue_path(params[:issue_id])
109
109
  end
110
110
  end
111
111
  end
@@ -11,28 +11,28 @@ module PlanMyStuff
11
11
  class ClosuresController < PlanMyStuff::ApplicationController
12
12
  # POST /issues/:issue_id/closure
13
13
  def create
14
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
14
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
15
15
  issue.update!(state: :closed)
16
16
 
17
17
  flash[:success] = 'Issue was successfully closed.'
18
- redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
18
+ redirect_to(plan_my_stuff.issue_path(issue))
19
19
  rescue PlanMyStuff::Error, ArgumentError => e
20
20
  pms_handle_rescue(e)
21
21
  flash[:error] = e.message
22
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
22
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
23
23
  end
24
24
 
25
25
  # DELETE /issues/:issue_id/closure
26
26
  def destroy
27
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
27
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
28
28
  issue.update!(state: :open)
29
29
 
30
30
  flash[:success] = 'Issue was successfully reopened.'
31
- redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
31
+ redirect_to(plan_my_stuff.issue_path(issue))
32
32
  rescue PlanMyStuff::Error, ArgumentError => e
33
33
  pms_handle_rescue(e)
34
34
  flash[:error] = e.message
35
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
35
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
36
36
  end
37
37
  end
38
38
  end
@@ -21,38 +21,38 @@ module PlanMyStuff
21
21
  def create
22
22
  type = link_params[:type].to_s
23
23
  unless dispatch_allowed?(type, :add)
24
- redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
24
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
25
25
  return
26
26
  end
27
27
 
28
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
28
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
29
29
  link = add_link(issue, type)
30
30
 
31
31
  flash[:success] = "Linked #{link}"
32
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
32
+ redirect_to(plan_my_stuff.issue_path(issue))
33
33
  rescue PlanMyStuff::ValidationError, ActiveModel::ValidationError, ArgumentError => e
34
34
  pms_handle_rescue(e)
35
35
  flash[:error] = e.message
36
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
36
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
37
37
  end
38
38
 
39
39
  # DELETE /issues/:issue_id/links/:id
40
40
  def destroy
41
41
  type, repo, number = parse_composite_id(params[:id])
42
42
  unless dispatch_allowed?(type, :remove)
43
- redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
43
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
44
44
  return
45
45
  end
46
46
 
47
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
47
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
48
48
  remove_link(issue, type, repo: repo, number: number)
49
49
 
50
50
  flash[:success] = "Unlinked #{repo}##{number}"
51
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
51
+ redirect_to(plan_my_stuff.issue_path(issue))
52
52
  rescue PlanMyStuff::ValidationError, ActiveModel::ValidationError, ArgumentError => e
53
53
  pms_handle_rescue(e)
54
54
  flash[:error] = e.message
55
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
55
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
56
56
  end
57
57
 
58
58
  private
@@ -11,37 +11,31 @@ module PlanMyStuff
11
11
 
12
12
  # POST /issues/:issue_id/take
13
13
  def create
14
- issue_number = params[:issue_id].to_i
15
- repo = params[:repo]
16
-
17
- issue = PlanMyStuff::Issue.find(issue_number, repo: repo)
14
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
18
15
  guard_already_taken!(issue)
19
16
 
20
- project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
17
+ project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue.number)
21
18
  project_item ||= add_to_pipeline(issue)
22
19
 
23
20
  PlanMyStuff::Pipeline.take!(project_item)
24
21
  assign_current_user(project_item)
25
- flash[:success] ||= "Issue ##{issue_number} taken."
22
+ flash[:success] ||= "Issue ##{issue.number} taken."
26
23
 
27
- redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
24
+ redirect_to(plan_my_stuff.issue_path(issue))
28
25
  rescue ArgumentError, PlanMyStuff::Error => e
29
26
  pms_handle_rescue(e)
30
27
  flash[:error] = e.message
31
- redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
28
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
32
29
  end
33
30
 
34
31
  # DELETE /issues/:issue_id/take
35
32
  def destroy
36
- issue_number = params[:issue_id].to_i
37
- repo = params[:repo]
38
-
39
- issue = PlanMyStuff::Issue.find(issue_number, repo: repo)
33
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
40
34
  login = current_user_login
41
35
  guard_release!(issue, login)
42
36
 
43
37
  remaining = issue.assignees - [login]
44
- project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
38
+ project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue.number)
45
39
 
46
40
  if project_item.present?
47
41
  if remaining.empty?
@@ -54,12 +48,12 @@ module PlanMyStuff
54
48
  issue.update!(assignees: remaining)
55
49
  end
56
50
 
57
- flash[:success] = "Issue ##{issue_number} released."
58
- redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
51
+ flash[:success] = "Issue ##{issue.number} released."
52
+ redirect_to(plan_my_stuff.issue_path(issue))
59
53
  rescue ArgumentError, PlanMyStuff::Error => e
60
54
  pms_handle_rescue(e)
61
55
  flash[:error] = e.message
62
- redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
56
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
63
57
  end
64
58
 
65
59
  private
@@ -72,7 +66,7 @@ module PlanMyStuff
72
66
  return if support_user?
73
67
 
74
68
  redirect_to_unauthorized(
75
- plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]),
69
+ plan_my_stuff.issue_path(params[:issue_id]),
76
70
  )
77
71
  end
78
72
 
@@ -12,44 +12,44 @@ module PlanMyStuff
12
12
  # POST /issues/:issue_id/viewers
13
13
  def create
14
14
  unless support_user?
15
- redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
15
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
16
16
  return
17
17
  end
18
18
 
19
19
  viewer_ids = parse_viewer_ids(params[:viewer_ids])
20
20
  if viewer_ids.blank?
21
21
  flash[:error] = 'No valid viewer IDs provided.'
22
- redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
22
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
23
23
  return
24
24
  end
25
25
 
26
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
26
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
27
27
  issue.add_viewers!(user_ids: viewer_ids, user: pms_current_user)
28
28
 
29
29
  flash[:success] = 'Viewers were successfully added.'
30
- redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
30
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
31
31
  rescue PlanMyStuff::Error, Octokit::Error => e
32
32
  pms_handle_rescue(e)
33
33
  flash[:error] = e.message
34
- redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
34
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
35
35
  end
36
36
 
37
37
  # DELETE /issues/:issue_id/viewers/:id
38
38
  def destroy
39
39
  unless support_user?
40
- redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
40
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
41
41
  return
42
42
  end
43
43
 
44
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
44
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
45
45
  issue.remove_viewers!(user_ids: [params[:id].to_i], user: pms_current_user)
46
46
 
47
47
  flash[:success] = 'Viewer was successfully removed.'
48
- redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
48
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
49
49
  rescue PlanMyStuff::Error, Octokit::Error => e
50
50
  pms_handle_rescue(e)
51
51
  flash[:error] = e.message
52
- redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
52
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id]))
53
53
  end
54
54
  end
55
55
  end
@@ -12,37 +12,37 @@ module PlanMyStuff
12
12
  # POST /issues/:issue_id/waiting
13
13
  def create
14
14
  unless support_user?
15
- redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
15
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
16
16
  return
17
17
  end
18
18
 
19
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
19
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
20
20
  issue.enter_waiting_on_user!(user: pms_current_user)
21
21
 
22
22
  flash[:success] = 'Issue marked as waiting on user reply.'
23
- redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
23
+ redirect_to(plan_my_stuff.issue_path(issue))
24
24
  rescue PlanMyStuff::Error, ArgumentError => e
25
25
  pms_handle_rescue(e)
26
26
  flash[:error] = e.message
27
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
27
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
28
28
  end
29
29
 
30
30
  # DELETE /issues/:issue_id/waiting
31
31
  def destroy
32
32
  unless support_user?
33
- redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
33
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id]))
34
34
  return
35
35
  end
36
36
 
37
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
37
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
38
38
  issue.clear_waiting_on_user!
39
39
 
40
40
  flash[:success] = 'Waiting-on-user state cleared.'
41
- redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
41
+ redirect_to(plan_my_stuff.issue_path(issue))
42
42
  rescue PlanMyStuff::Error, ArgumentError => e
43
43
  pms_handle_rescue(e)
44
44
  flash[:error] = e.message
45
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
45
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
46
46
  end
47
47
  end
48
48
  end
@@ -34,7 +34,7 @@ module PlanMyStuff
34
34
  )
35
35
 
36
36
  flash[:success] = 'Issue was successfully created.'
37
- redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
37
+ redirect_to(plan_my_stuff.issue_path(@issue))
38
38
  rescue PlanMyStuff::ValidationError => e
39
39
  pms_handle_rescue(e)
40
40
  @issue = PlanMyStuff::Issue.new(title: issue_params[:title], body: issue_params[:body])
@@ -44,7 +44,7 @@ module PlanMyStuff
44
44
 
45
45
  # GET /issues/:id
46
46
  def show
47
- @issue = PlanMyStuff::Issue.find(params[:id].to_i, repo: params[:repo])
47
+ @issue = PlanMyStuff::Issue.find(params[:id])
48
48
  @comments = filter_visible_comments(@issue.comments)
49
49
  @current_user_id = pms_current_user.present? ? PlanMyStuff::UserResolver.user_id(pms_current_user) : nil
50
50
  @current_user_login = PlanMyStuff.configuration.github_login_for[@current_user_id]
@@ -54,12 +54,12 @@ module PlanMyStuff
54
54
 
55
55
  # GET /issues/:id/edit
56
56
  def edit
57
- @issue = PlanMyStuff::Issue.find(params[:id].to_i, repo: params[:repo])
57
+ @issue = PlanMyStuff::Issue.find(params[:id])
58
58
  end
59
59
 
60
60
  # PATCH/PUT /issues/:id
61
61
  def update
62
- @issue = PlanMyStuff::Issue.find(params[:id].to_i, repo: params[:repo])
62
+ @issue = PlanMyStuff::Issue.find(params[:id])
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, repo: @issue.repo.full_name))
71
+ redirect_to(plan_my_stuff.issue_path(@issue))
72
72
  rescue PlanMyStuff::StaleObjectError => e
73
73
  pms_handle_rescue(e)
74
74
  flash.now[:error] = 'Issue was modified by someone else. Please review the latest changes and try again.'
@@ -7,40 +7,40 @@ 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], repo: params[:repo]))
10
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
11
11
  return
12
12
  end
13
13
 
14
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
14
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
15
15
 
16
16
  missing = labels.reject { |label| PlanMyStuff::Label.exists?(repo: issue.repo, name: label) }
17
17
  if missing.any?
18
18
  flash[:error] = "Label#{'s' if missing.size > 1} not found in repo: #{missing.join(', ')}"
19
- redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
19
+ redirect_to(plan_my_stuff.issue_path(issue))
20
20
  return
21
21
  end
22
22
 
23
23
  PlanMyStuff::Label.add!(issue: issue, labels: labels)
24
24
 
25
25
  flash[:success] = 'Label was successfully added.'
26
- redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
26
+ redirect_to(plan_my_stuff.issue_path(issue))
27
27
  rescue PlanMyStuff::Error, Octokit::Error => e
28
28
  pms_handle_rescue(e)
29
29
  flash[:error] = e.message
30
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
30
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
31
31
  end
32
32
 
33
33
  # DELETE /issues/:issue_id/labels/:id
34
34
  def destroy
35
- issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
35
+ issue = PlanMyStuff::Issue.find(params[:issue_id])
36
36
  PlanMyStuff::Label.remove!(issue: issue, labels: [params[:id]])
37
37
 
38
38
  flash[:success] = 'Label was successfully removed.'
39
- redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
39
+ redirect_to(plan_my_stuff.issue_path(issue))
40
40
  rescue PlanMyStuff::Error, Octokit::Error => e
41
41
  pms_handle_rescue(e)
42
42
  flash[:error] = e.message
43
- redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
43
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
44
44
  end
45
45
  end
46
46
  end
@@ -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, repo: issue.repo.full_name)) %></td>
17
+ <td><%= link_to(issue.title, plan_my_stuff.issue_path(issue)) %></td>
18
18
  <td><%= issue.state %></td>
19
19
  <td><%= issue.labels.join(', ') %></td>
20
20
  </tr>
@@ -44,7 +44,7 @@
44
44
  <%=
45
45
  button_to(
46
46
  'Approve',
47
- plan_my_stuff.issue_approval_path(issue.number, approval.user_id, repo: issue.repo.full_name),
47
+ plan_my_stuff.issue_approval_path(issue, approval.user_id),
48
48
  method: :patch,
49
49
  params: { approval: { status: 'approved' } },
50
50
  form: { style: 'display:inline' },
@@ -53,7 +53,7 @@
53
53
  <%=
54
54
  button_to(
55
55
  'Reject',
56
- plan_my_stuff.issue_approval_path(issue.number, approval.user_id, repo: issue.repo.full_name),
56
+ plan_my_stuff.issue_approval_path(issue, approval.user_id),
57
57
  method: :patch,
58
58
  params: { approval: { status: 'rejected' } },
59
59
  form: { style: 'display:inline' },
@@ -65,7 +65,7 @@
65
65
  <%=
66
66
  button_to(
67
67
  'Revoke',
68
- plan_my_stuff.issue_approval_path(issue.number, approval.user_id, repo: issue.repo.full_name),
68
+ plan_my_stuff.issue_approval_path(issue, approval.user_id),
69
69
  method: :patch,
70
70
  params: { approval: { status: 'pending' } },
71
71
  form: { style: 'display:inline' },
@@ -77,7 +77,7 @@
77
77
  <%=
78
78
  button_to(
79
79
  'Remove',
80
- plan_my_stuff.issue_approval_path(issue.number, approval.user_id, repo: issue.repo.full_name),
80
+ plan_my_stuff.issue_approval_path(issue, approval.user_id),
81
81
  method: :delete,
82
82
  form: { style: 'display:inline' },
83
83
  )
@@ -94,7 +94,7 @@
94
94
  <%=
95
95
  form_with(
96
96
  scope: :approval,
97
- url: plan_my_stuff.issue_approvals_path(issue.number, repo: issue.repo.full_name),
97
+ url: plan_my_stuff.issue_approvals_path(issue),
98
98
  method: :post,
99
99
  local: true,
100
100
  ) do |form|
@@ -3,7 +3,7 @@
3
3
  persisted = issue.persisted?
4
4
  url =
5
5
  if persisted
6
- plan_my_stuff.issue_path(issue.number, repo: issue.repo&.full_name)
6
+ plan_my_stuff.issue_path(issue)
7
7
  else
8
8
  plan_my_stuff.issues_path(repo: issue.repo&.full_name)
9
9
  end
@@ -68,7 +68,7 @@
68
68
  <%=
69
69
  link_to(
70
70
  "#{target.repo.full_name}##{target.number} - #{target.title}",
71
- plan_my_stuff.issue_path(target.number, repo: target.repo.full_name),
71
+ plan_my_stuff.issue_path(target),
72
72
  )
73
73
  %>
74
74
  <% if section[:removable] %>
@@ -78,7 +78,7 @@
78
78
  <%=
79
79
  button_to(
80
80
  'Remove',
81
- plan_my_stuff.issue_link_path(issue.number, link_id, repo: issue.repo.full_name),
81
+ plan_my_stuff.issue_link_path(issue, link_id),
82
82
  method: :delete,
83
83
  form: { style: 'display:inline' },
84
84
  )
@@ -95,7 +95,7 @@
95
95
  <%=
96
96
  form_with(
97
97
  scope: :link,
98
- url: plan_my_stuff.issue_links_path(issue.number, repo: issue.repo.full_name),
98
+ url: plan_my_stuff.issue_links_path(issue),
99
99
  method: :post,
100
100
  local: true,
101
101
  ) do |form|
@@ -10,7 +10,7 @@
10
10
  <%=
11
11
  button_to(
12
12
  'Remove',
13
- plan_my_stuff.issue_viewer_path(issue.number, viewer_id, repo: issue.repo.full_name),
13
+ plan_my_stuff.issue_viewer_path(issue, viewer_id),
14
14
  method: :delete
15
15
  )
16
16
  %>
@@ -21,7 +21,7 @@
21
21
  <p>No viewers added.</p>
22
22
  <% end %>
23
23
 
24
- <%= form_with(url: plan_my_stuff.issue_viewers_path(issue.number, repo: issue.repo.full_name), method: :post) do |form| %>
24
+ <%= form_with(url: plan_my_stuff.issue_viewers_path(issue), method: :post) do |form| %>
25
25
  <div>
26
26
  <%= form.label(:viewer_ids, 'Add viewer IDs (comma-separated)') %>
27
27
  <%= form.text_field(:viewer_ids) %>
@@ -1,27 +1,27 @@
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, repo: @issue.repo.full_name)) %>
4
+ <%= link_to('Edit', plan_my_stuff.edit_issue_path(@issue)) %>
5
5
  <% if @support_user && @issue.html_url.present? %>
6
6
  <%= link_to('View on GitHub', @issue.html_url, target: '_blank', rel: 'noopener') %>
7
7
  <% end %>
8
8
  <% if @issue.state == 'open' %>
9
- <%= button_to('Close Issue', plan_my_stuff.issue_closure_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
9
+ <%= button_to('Close Issue', plan_my_stuff.issue_closure_path(@issue), method: :post) %>
10
10
  <% else %>
11
- <%= button_to('Reopen Issue', plan_my_stuff.issue_closure_path(@issue.number, repo: @issue.repo.full_name), method: :delete) %>
11
+ <%= button_to('Reopen Issue', plan_my_stuff.issue_closure_path(@issue), method: :delete) %>
12
12
  <% end %>
13
13
  <% if @support_user && @issue.state == 'open' %>
14
14
  <% if @issue.metadata.waiting_on_user_at.present? %>
15
- <%= button_to('Mark replied', plan_my_stuff.issue_waiting_path(@issue.number, repo: @issue.repo.full_name), method: :delete) %>
15
+ <%= button_to('Mark replied', plan_my_stuff.issue_waiting_path(@issue), method: :delete) %>
16
16
  <% else %>
17
- <%= button_to('Mark waiting', plan_my_stuff.issue_waiting_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
17
+ <%= button_to('Mark waiting', plan_my_stuff.issue_waiting_path(@issue), method: :post) %>
18
18
  <% end %>
19
19
  <% end %>
20
20
  <% if @support_user && @pipeline_enabled && @pipeline_item.nil? && @issue.assignees.blank? %>
21
- <%= button_to('Take', plan_my_stuff.issue_take_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
21
+ <%= button_to('Take', plan_my_stuff.issue_take_path(@issue), method: :post) %>
22
22
  <% end %>
23
23
  <% if @support_user && @pipeline_enabled && @current_user_login.present? && @issue.assignees.include?(@current_user_login) %>
24
- <%= button_to('Unassign', plan_my_stuff.issue_take_path(@issue.number, repo: @issue.repo.full_name), method: :delete) %>
24
+ <%= button_to('Unassign', plan_my_stuff.issue_take_path(@issue), method: :delete) %>
25
25
  <% end %>
26
26
  <% if @support_user %>
27
27
  <%= link_to('Start Testing Project', plan_my_stuff.new_testing_project_path(subject_url: @issue.html_url)) %>
@@ -76,7 +76,7 @@
76
76
  <%= PlanMyStuff::Markdown.render(comment.body || '').html_safe %>
77
77
  <% if comment.pms_comment? && (@support_user || comment.metadata.created_by == @current_user_id) %>
78
78
  <p>
79
- <%= link_to('Edit', plan_my_stuff.edit_issue_comment_path(@issue.number, comment.id, repo: @issue.repo.full_name)) %>
79
+ <%= link_to('Edit', plan_my_stuff.edit_issue_comment_path(@issue, comment.id)) %>
80
80
  </p>
81
81
  <% end %>
82
82
  <% if @support_user && comment.html_url.present? %>
@@ -24,7 +24,9 @@
24
24
  <% items.each do |item| %>
25
25
  <div style="border: 1px solid black; margin: 1em">
26
26
  <% unless item.draft? %>
27
- <strong><%= link_to(item.title, plan_my_stuff.issue_path(item.number, repo: item.repo.full_name)) %></strong>
27
+ <strong>
28
+ <%= link_to(item.title, plan_my_stuff.issue_path(item.issue.to_param)) %>
29
+ </strong>
28
30
  <% else %>
29
31
  <strong><%= item.title %></strong>
30
32
  <% end %>
@@ -3,7 +3,7 @@
3
3
  <% if item.draft? %>
4
4
  <strong><%= item.title %></strong>
5
5
  <% else %>
6
- <strong><%= link_to(item.title, plan_my_stuff.issue_path(item.number, repo: item.repo.full_name)) %></strong>
6
+ <strong><%= link_to(item.title, plan_my_stuff.issue_path(item.issue.to_param)) %></strong>
7
7
  <small>#<%= item.number %></small>
8
8
  <% end %>
9
9
 
@@ -24,6 +24,12 @@ PlanMyStuff.configure do |config|
24
24
  # config.repos[:underwriter] = 'YourOrganization/Underwriter'
25
25
  # config.default_repo = :element
26
26
 
27
+ # Human-readable prefix used as the `Issue#to_param` so URLs read
28
+ # `/issues/Element-1234` instead of `/issues/1234`. Missing keys fall back
29
+ # to `key.to_s.titleize`; only repos whose key doesn't titleize cleanly
30
+ # need an entry.
31
+ # config.repo_nicknames = { safety: 'Compliance' }
32
+
27
33
  # --------------------------------------------------------------------------
28
34
  # Projects
29
35
  # --------------------------------------------------------------------------
@@ -348,6 +348,14 @@ module PlanMyStuff
348
348
  #
349
349
  attr_accessor :repos
350
350
 
351
+ # Human-readable repo names used as the +to_param+ prefix on +PlanMyStuff::Issue+ instances. Symbol-keyed
352
+ # against +repos+ -- missing keys fall back to +key.to_s.titleize+, so only entries that diverge from a simple
353
+ # +titleize+ of the key (e.g. +:safety+ -> +"Compliance"+) need to be listed.
354
+ #
355
+ # @return [Hash{Symbol => String}]
356
+ #
357
+ attr_accessor :repo_nicknames
358
+
351
359
  # Bare repo name (under +config.organization+) that stores uploaded attachment binaries. Defaults to
352
360
  # +'pms-attachments'+. The repo must exist; the uploader does not create it. Attachments commit onto
353
361
  # +config.main_branch+ and live under +<repo_key_or_name>/issue-<number>/<uuid>.<ext>+.
@@ -368,6 +376,7 @@ module PlanMyStuff
368
376
  # @return [Configuration]
369
377
  def initialize
370
378
  @repos = {}
379
+ @repo_nicknames = {}
371
380
  @attachment_repo = 'pms-attachments'
372
381
  @user_class = 'User'
373
382
  @display_name_method = :to_s
@@ -435,12 +444,14 @@ module PlanMyStuff
435
444
  missing << 'access_token' if access_token.nil? || access_token.to_s.strip.empty?
436
445
  missing << 'organization' if organization.nil? || organization.to_s.strip.empty?
437
446
 
438
- return if missing.empty?
447
+ if missing.present?
448
+ raise(
449
+ PlanMyStuff::ConfigurationError,
450
+ "Missing required PlanMyStuff configuration: #{missing.join(', ')}",
451
+ )
452
+ end
439
453
 
440
- raise(
441
- PlanMyStuff::ConfigurationError,
442
- "Missing required PlanMyStuff configuration: #{missing.join(', ')}",
443
- )
454
+ validate_repo_nicknames!
444
455
  end
445
456
 
446
457
  # Returns the merged custom fields schema for the given context. Context-specific fields deep-merge on top of
@@ -477,6 +488,51 @@ module PlanMyStuff
477
488
  path = controllers[key] || DEFAULT_CONTROLLERS.fetch(key)
478
489
  path.start_with?('/') ? path : "/#{path}"
479
490
  end
491
+
492
+ # Human-readable nickname for a repo key, used as the +to_param+ prefix on +PlanMyStuff::Issue+ instances. Falls
493
+ # back to +key.to_s.titleize+ when no explicit entry exists in +repo_nicknames+.
494
+ #
495
+ # @param key [Symbol, String]
496
+ #
497
+ # @return [String]
498
+ #
499
+ def repo_nickname_for(key)
500
+ repo_nicknames[key&.to_sym] || key.to_s.titleize
501
+ end
502
+
503
+ private
504
+
505
+ # Resolved nicknames feed directly into +Issue#to_param+ and route +:id+ tokens, so collisions or chars outside
506
+ # +[A-Za-z0-9_]+ (notably +-+, the nickname/number separator) break URL round-tripping.
507
+ #
508
+ # @raise [ConfigurationError] when any resolved nickname collides with another or contains non-token chars
509
+ #
510
+ # @return [void]
511
+ #
512
+ def validate_repo_nicknames!
513
+ resolved = repos.keys.index_with { |key| repo_nickname_for(key).to_s }
514
+
515
+ invalid = resolved.reject { |_key, nickname| nickname.match?(/\A[A-Za-z0-9_]+\z/) }
516
+ if invalid.present?
517
+ pairs = invalid.map { |key, nickname| "#{key.inspect} => #{nickname.inspect}" }
518
+ raise(
519
+ PlanMyStuff::ConfigurationError,
520
+ "Invalid repo nickname(s) (must match /\\A[A-Za-z0-9_]+\\z/): #{pairs.join(', ')}",
521
+ )
522
+ end
523
+
524
+ dupes = resolved.group_by { |_key, nickname| nickname }.select { |_n, entries| entries.size > 1 }
525
+ return if dupes.blank?
526
+
527
+ details = dupes.map do |nickname, entries|
528
+ keys = entries.map { |entry| entry.first.inspect }.join(', ')
529
+ "#{nickname.inspect} (#{keys})"
530
+ end
531
+ raise(
532
+ PlanMyStuff::ConfigurationError,
533
+ "Duplicate repo nickname(s): #{details.join('; ')}",
534
+ )
535
+ end
480
536
  end
481
537
 
482
538
  class ConfigurationError < StandardError
@@ -255,16 +255,20 @@ module PlanMyStuff
255
255
 
256
256
  # Finds a single GitHub issue by number and parses its PMS metadata.
257
257
  #
258
+ # Accepts a numeric id (Integer or all-digit String) plus an optional +repo:+ kwarg, or a nickname-id String
259
+ # (e.g. +"Rawr-1234"+) where the repo is encoded in the prefix and +repo:+ is ignored.
260
+ #
258
261
  # @raise [Octokit::NotFound] when the issue number resolves to a pull request
262
+ # @raise [ArgumentError] when a nickname-id String references an unknown repo nickname
259
263
  #
260
- # @param number [Integer]
261
- # @param repo [Symbol, String, nil] defaults to config.default_repo
264
+ # @param id_or_number [Integer, String]
265
+ # @param repo [Symbol, String, nil] defaults to config.default_repo; ignored when +id_or_number+ is a nickname id
262
266
  #
263
267
  # @return [PlanMyStuff::Issue]
264
268
  #
265
- def find(number, repo: nil)
269
+ def find(id_or_number, repo: nil)
270
+ number, resolved_repo = resolve_find_args(id_or_number, repo)
266
271
  client = PlanMyStuff.client
267
- resolved_repo = client.resolve_repo!(repo)
268
272
 
269
273
  github_issue =
270
274
  fetch_with_etag_cache(
@@ -393,8 +397,68 @@ module PlanMyStuff
393
397
  raise(PlanMyStuff::APIError.new(e.message, status: e.respond_to?(:response_status) ? e.response_status : nil))
394
398
  end
395
399
 
400
+ # @raise [ArgumentError] when +repo+ resolves to a Repo with no configured key (cannot reverse-resolve through
401
+ # +Repo.from_nickname!+, so the resulting token would not round-trip through +Issue.find+ / +from_param+)
402
+ #
403
+ # @param number [Integer]
404
+ # @param repo [String] full repo path, e.g. +"BrandsInsurance/Element"+
405
+ #
406
+ # @return [String]
407
+ #
408
+ def to_param(number, repo)
409
+ return if number.blank?
410
+ return if repo.blank?
411
+
412
+ repo_obj = PlanMyStuff::Repo.resolve!(repo)
413
+ if repo_obj.key.nil?
414
+ raise(
415
+ ArgumentError,
416
+ "Repo #{repo_obj.full_name.inspect} is not configured in config.repos; " \
417
+ 'cannot build reversible Issue#to_param token',
418
+ )
419
+ end
420
+
421
+ "#{repo_obj.nickname}-#{number}"
422
+ end
423
+
424
+ # Parses an +Issue#to_param+ string of the form +"Nickname-1234"+ back into +[Repo, Integer]+. The repo is
425
+ # looked up via +PlanMyStuff::Repo.from_nickname!+, which scans +config.repos+ for the key whose
426
+ # +config.repo_nickname_for+ matches.
427
+ #
428
+ # @raise [ArgumentError] when +param+ does not match the +"Prefix-1234"+ shape or the prefix is not a known
429
+ # repo nickname
430
+ #
431
+ # @param param [String]
432
+ #
433
+ # @return [Array(PlanMyStuff::Repo, Integer)]
434
+ #
435
+ def from_param(param)
436
+ match = param.to_s.match(/\A(?<nickname>.+)-(?<number>\d+)\z/)
437
+ raise(ArgumentError, "Invalid issue param: #{param.inspect}") if match.nil?
438
+
439
+ [PlanMyStuff::Repo.from_nickname!(match[:nickname]), match[:number].to_i]
440
+ end
441
+
396
442
  private
397
443
 
444
+ # Splits the +Issue.find+ first arg into +[number, resolved_repo_full_name]+. A nickname-id String like
445
+ # +"Rawr-1234"+ is decoded via +from_param+ (repo derived from the prefix; +repo:+ kwarg ignored). All other
446
+ # inputs (Integer, all-digit String) fall through to the existing +client.resolve_repo!+ path with the kwarg.
447
+ #
448
+ # @param id_or_number [Integer, String]
449
+ # @param repo [Symbol, String, PlanMyStuff::Repo, nil]
450
+ #
451
+ # @return [Array(Integer, String)]
452
+ #
453
+ def resolve_find_args(id_or_number, repo)
454
+ if id_or_number.is_a?(String) && !id_or_number.match?(/\A\d+\z/)
455
+ repo_obj, number = from_param(id_or_number)
456
+ [number, repo_obj.full_name]
457
+ else
458
+ [id_or_number.to_i, PlanMyStuff.client.resolve_repo!(repo)]
459
+ end
460
+ end
461
+
398
462
  # Resolves an +issue_type:+ kwarg to the literal display name GitHub expects. Two stages: a Symbol is first
399
463
  # looked up in the gem-side +ISSUE_TYPE_NICKNAMES+ to get a canonical name; then the canonical (or
400
464
  # directly-provided String) name is passed through +config.issue_types+ for org-specific renames. Missing
@@ -443,9 +507,8 @@ module PlanMyStuff
443
507
  end
444
508
 
445
509
  # Builds the visible body string written to GitHub for an issue: a markdown link to the consuming-app
446
- # per-issue URL (carrying the repo as a +?repo=+ query param so the consuming app knows which repo this issue
447
- # lives in), labelled with the GitHub +Org/Repo#number+. Returns +""+ when either +config.issues_url_prefix+
448
- # or +number+ is missing.
510
+ # per-issue URL (in +Issue#to_param+ form, e.g. +"/issues/Rawr-1234"+), labelled with the GitHub
511
+ # +Org/Repo#number+. Returns +""+ when either +config.issues_url_prefix+ or +number+ is missing.
449
512
  #
450
513
  # @param number [Integer]
451
514
  # @param repo [String] full repo path, e.g. +"BrandsInsurance/Element"+
@@ -456,7 +519,8 @@ module PlanMyStuff
456
519
  prefix = PlanMyStuff.configuration.issues_url_prefix
457
520
  return '' if prefix.blank? || number.blank?
458
521
 
459
- url = "#{prefix.to_s.chomp('/')}/#{number}?repo=#{URI.encode_www_form_component(repo)}"
522
+ to_par = to_param(number, repo)
523
+ url = "#{prefix.to_s.chomp('/')}/#{to_par}"
460
524
  "[#{repo}##{number}](#{url})"
461
525
  end
462
526
 
@@ -526,17 +590,29 @@ module PlanMyStuff
526
590
  @body_dirty = true
527
591
  end
528
592
 
529
- # @return [String, nil] per-issue URL in the consuming app (+config.issues_url_prefix+ + +"/"+ + +number+ +
530
- # +"?repo=Org/Repo"+, or +nil+ when either prefix or number is missing). Also rendered as the destination of
531
- # the markdown link in the GitHub issue body.
593
+ # Single-segment URL token combining repo nickname and issue number, used by Rails route helpers
594
+ # (+youtrack_issue_path(@issue)+ -> +"/issues/Rawr-1234"+). Returns +nil+ for new records or when +number+ or
595
+ # +repo+ is unset; +Issue.from_param+ parses the same shape back into +[Repo, Integer]+.
596
+ #
597
+ # @return [String, nil]
598
+ #
599
+ def to_param
600
+ return if new_record?
601
+
602
+ self.class.to_param(number, repo)
603
+ end
604
+
605
+ # @return [String, nil] per-issue URL in the consuming app (+config.issues_url_prefix+ + +"/"+ + +to_param+, or
606
+ # +nil+ when prefix, number, or repo is missing). Also rendered as the destination of the markdown link in
607
+ # the GitHub issue body.
532
608
  def user_link
533
609
  prefix = PlanMyStuff.configuration.issues_url_prefix
534
- return if prefix.blank? || number.blank?
610
+ return if prefix.blank?
535
611
 
536
- base = "#{prefix.to_s.chomp('/')}/#{number}"
537
- return base if repo.blank?
612
+ to_par = to_param
613
+ return if to_par.blank?
538
614
 
539
- "#{base}?repo=#{URI.encode_www_form_component(repo.full_name)}"
615
+ "#{prefix.to_s.chomp('/')}/#{to_par}"
540
616
  end
541
617
 
542
618
  # Tags the issue with the configured +archived_label+, removes it from every Projects V2 board it belongs to,
@@ -55,6 +55,23 @@ module PlanMyStuff
55
55
  end
56
56
  end
57
57
 
58
+ # Reverse lookup for the +Issue#to_param+ prefix: finds the configured repo whose nickname (per
59
+ # +config.repo_nickname_for+) matches +nickname+ and returns its +Repo+ instance.
60
+ #
61
+ # @raise [ArgumentError] if no configured repo has the given nickname
62
+ #
63
+ # @param nickname [String]
64
+ #
65
+ # @return [PlanMyStuff::Repo]
66
+ #
67
+ def from_nickname!(nickname)
68
+ config = PlanMyStuff.configuration
69
+ match = config.repos.keys.find { |key| config.repo_nickname_for(key) == nickname }
70
+ raise(ArgumentError, "Unknown repo nickname: #{nickname.inspect}") if match.nil?
71
+
72
+ resolve!(match)
73
+ end
74
+
58
75
  private
59
76
 
60
77
  # @raise [ArgumentError] if full_name is not in "Org/Repo" format
@@ -88,6 +105,17 @@ module PlanMyStuff
88
105
  "#{organization}/#{name}"
89
106
  end
90
107
 
108
+ # Human-readable repo label used as the +Issue#to_param+ prefix. Resolves through +config.repo_nickname_for+
109
+ # when this repo carries a configured +key+; falls back to the bare repo +name+ for unconfigured repos.
110
+ #
111
+ # @return [String]
112
+ #
113
+ def nickname
114
+ return PlanMyStuff.configuration.repo_nickname_for(key) if key
115
+
116
+ name
117
+ end
118
+
91
119
  # @see #full_name
92
120
  alias to_s full_name
93
121
 
@@ -3,7 +3,7 @@
3
3
  module PlanMyStuff
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 17
6
+ MINOR = 18
7
7
  TINY = 0
8
8
 
9
9
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plan_my_stuff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brands Insurance