plan_my_stuff 0.24.0 → 0.26.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 +31 -0
- data/CONFIGURATION.md +24 -0
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +10 -2
- data/app/controllers/plan_my_stuff/issues/testings_controller.rb +82 -0
- data/app/views/plan_my_stuff/issues/show.html.erb +16 -1
- data/config/routes.rb +5 -0
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +22 -0
- data/lib/plan_my_stuff/configuration.rb +20 -0
- data/lib/plan_my_stuff/issue.rb +18 -5
- data/lib/plan_my_stuff/issue_extractions/waiting.rb +23 -0
- data/lib/plan_my_stuff/issue_field_translation.rb +67 -0
- data/lib/plan_my_stuff/issue_field_value_set.rb +6 -2
- data/lib/plan_my_stuff/pipeline.rb +19 -0
- data/lib/plan_my_stuff/version.rb +1 -1
- 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: 6561200ae8220464aa3e705f21ae4d6695a4c3ae3db222677bb1deddddcd37b7
|
|
4
|
+
data.tar.gz: 98d519079e5c203741de10ce667341246d20335253033357a5d3ae8e1b48feee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c78d58f9322827d0483389f56dd82d02786929b851cfc6bf81bdc11973f7cdb06e3866b12c40054d4db71f44a24f5c817fd41213b374cf96eeec75e4c384b44
|
|
7
|
+
data.tar.gz: 9b1464ab72de0568958a73d75bb79f4d7e4095bbebc9e97dcf5f97445c79eace14f83d65da99de36f61c10f5ca37e178e5ffb2ef59afa941cad8f49b3ba56be2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.26.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `PlanMyStuff::Issue#awaiting_reply?` - returns `true` when the issue's `Issue Status` field is `Waiting on Reply`.
|
|
8
|
+
- `config.issue_field_names` (canonical field name => consumer field name) and `config.issue_field_values` (canonical
|
|
9
|
+
field name => `{ canonical value => consumer value }`) let a consuming org rename the native Issue Fields / option
|
|
10
|
+
labels the gem refers to internally. Translation runs via the new `PlanMyStuff::IssueFieldTranslation` module:
|
|
11
|
+
canonical => consumer on writes and filters (`set_issue_fields!`, `Issue.list` / `.count` `issue_fields:` +
|
|
12
|
+
`priority_list:`), consumer => canonical on reads (`Issue#issue_fields`), so internal comparisons like
|
|
13
|
+
`awaiting_reply?` / `priority_list?` keep working regardless of the org's naming. Unmapped names / values pass
|
|
14
|
+
through unchanged (identity fallback, mirroring `config.pipeline_statuses`).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- The waiting/inactivity lifecycle methods now write the native `Issue Status` issue field alongside their existing
|
|
19
|
+
label/metadata changes (gated on `config.issue_fields_enabled`): `enter_waiting_on_user!` sets `Waiting on Reply`,
|
|
20
|
+
`clear_waiting_on_user!` sets `Open`, and `reopen_by_reply!` sets `Reopened`. No-op when `issue_fields_enabled` is
|
|
21
|
+
`false`.
|
|
22
|
+
|
|
23
|
+
## 0.25.0
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `PlanMyStuff::Pipeline.clear_testing!(project_item, user: nil)` - reverses `request_testing!` by writing the
|
|
28
|
+
`:inactive` Testing field value. Fires `pipeline_testing_cleared.plan_my_stuff` with `field_name`, `value`,
|
|
29
|
+
`user` payload.
|
|
30
|
+
- `PlanMyStuff::Issues::TestingsController` + `POST/DELETE /issues/:issue_id/testing` route (gated on
|
|
31
|
+
`config.pipeline_enabled`). Backs new "Request testing" / "Clear testing" buttons on the issue show view
|
|
32
|
+
(closes #52).
|
|
33
|
+
|
|
3
34
|
## 0.24.0
|
|
4
35
|
|
|
5
36
|
### Added
|
data/CONFIGURATION.md
CHANGED
|
@@ -214,6 +214,8 @@ config.issue_types = {
|
|
|
214
214
|
| Option | Type | Default | Description |
|
|
215
215
|
|---|---|---|---|
|
|
216
216
|
| `issue_fields_enabled` | `Boolean` | `true` | Whether the Issue Fields public preview is wired up for the org. |
|
|
217
|
+
| `issue_field_names` | `Hash{String => String}` | `{}` | Canonical Issue Field name => the consumer org's field name. |
|
|
218
|
+
| `issue_field_values` | `Hash` | `{}` | Per field: canonical value => consumer value (single-select labels). |
|
|
217
219
|
|
|
218
220
|
GitHub Issue Fields are structured per-issue metadata (text, number, date, or single-select)
|
|
219
221
|
configured once at the org level. The preview is rolling out org-by-org. Leave this flag at its
|
|
@@ -229,6 +231,27 @@ With the flag off:
|
|
|
229
231
|
config.issue_fields_enabled = false # org not admitted to the preview yet
|
|
230
232
|
```
|
|
231
233
|
|
|
234
|
+
`issue_field_names` and `issue_field_values` let a consuming org rename the native Issue Fields (and their
|
|
235
|
+
single-select option labels) the gem refers to internally, the same way `pipeline_statuses` aliases pipeline
|
|
236
|
+
statuses. Translation is bidirectional via `PlanMyStuff::IssueFieldTranslation`: canonical => consumer on writes and
|
|
237
|
+
filters, consumer => canonical on reads, so internal comparisons (`awaiting_reply?`, `priority_list?`) keep working.
|
|
238
|
+
Unmapped names / values pass through unchanged.
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
config.issue_field_names = {
|
|
242
|
+
'Issue Status' => 'Status',
|
|
243
|
+
'Priority' => 'Prio',
|
|
244
|
+
}
|
|
245
|
+
config.issue_field_values = {
|
|
246
|
+
'Issue Status' => {
|
|
247
|
+
'Submitted' => 'Triaged',
|
|
248
|
+
'Waiting on Reply' => 'Awaiting Customer',
|
|
249
|
+
'Open' => 'Open',
|
|
250
|
+
'Reopened' => 'Reopened',
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
232
255
|
## Release pipeline
|
|
233
256
|
|
|
234
257
|
| Option | Type | Default | Description |
|
|
@@ -392,6 +415,7 @@ Controllable keys (with gem defaults):
|
|
|
392
415
|
| `:'issues/closures'` | `plan_my_stuff/issues/closures` |
|
|
393
416
|
| `:'issues/viewers'` | `plan_my_stuff/issues/viewers` |
|
|
394
417
|
| `:'issues/takes'` | `plan_my_stuff/issues/takes` |
|
|
418
|
+
| `:'issues/testings'` | `plan_my_stuff/issues/testings` |
|
|
395
419
|
| `:'issues/waitings'` | `plan_my_stuff/issues/waitings` |
|
|
396
420
|
| `:'issues/links'` | `plan_my_stuff/issues/links` |
|
|
397
421
|
| `:'issues/approvals'` | `plan_my_stuff/issues/approvals` |
|
|
@@ -12,7 +12,11 @@ module PlanMyStuff
|
|
|
12
12
|
# POST /issues/:issue_id/closure
|
|
13
13
|
def create
|
|
14
14
|
issue = PlanMyStuff::Issue.find(params[:issue_id])
|
|
15
|
-
|
|
15
|
+
to_update = {}
|
|
16
|
+
if PlanMyStuff.configuration.issue_fields_enabled
|
|
17
|
+
to_update[:issue_fields] = { 'Issue Status' => 'Fixed' }
|
|
18
|
+
end
|
|
19
|
+
issue.update!(state: :closed, **to_update)
|
|
16
20
|
|
|
17
21
|
yield(issue) if block_given?
|
|
18
22
|
return if performed?
|
|
@@ -28,7 +32,11 @@ module PlanMyStuff
|
|
|
28
32
|
# DELETE /issues/:issue_id/closure
|
|
29
33
|
def destroy
|
|
30
34
|
issue = PlanMyStuff::Issue.find(params[:issue_id])
|
|
31
|
-
|
|
35
|
+
to_update = {}
|
|
36
|
+
if PlanMyStuff.configuration.issue_fields_enabled
|
|
37
|
+
to_update[:issue_fields] = { 'Issue Status' => 'Reopened' }
|
|
38
|
+
end
|
|
39
|
+
issue.update!(state: :open, **to_update)
|
|
32
40
|
|
|
33
41
|
yield(issue) if block_given?
|
|
34
42
|
return if performed?
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module Issues
|
|
5
|
+
# Toggles the pipeline +Testing+ custom field on an issue's project item via
|
|
6
|
+
# +PlanMyStuff::Pipeline.request_testing!+ and +PlanMyStuff::Pipeline.clear_testing!+. Backs the "Request testing" /
|
|
7
|
+
# "Clear testing" buttons on the mounted issue show view.
|
|
8
|
+
#
|
|
9
|
+
# POST /issues/:issue_id/testing -> create (flips +Testing+ to its active value)
|
|
10
|
+
# DELETE /issues/:issue_id/testing -> destroy (flips +Testing+ back to inactive)
|
|
11
|
+
#
|
|
12
|
+
class TestingsController < PlanMyStuff::ApplicationController
|
|
13
|
+
before_action :require_support_user
|
|
14
|
+
|
|
15
|
+
# POST /issues/:issue_id/testing
|
|
16
|
+
def create
|
|
17
|
+
issue = PlanMyStuff::Issue.find(params[:issue_id])
|
|
18
|
+
project_item = find_project_item!(issue)
|
|
19
|
+
|
|
20
|
+
PlanMyStuff::Pipeline.request_testing!(project_item, user: pms_current_user)
|
|
21
|
+
|
|
22
|
+
yield(project_item) if block_given?
|
|
23
|
+
return if performed?
|
|
24
|
+
|
|
25
|
+
flash[:success] = "Issue ##{issue.number} marked as in testing."
|
|
26
|
+
redirect_to(plan_my_stuff.issue_path(issue))
|
|
27
|
+
rescue ArgumentError, PlanMyStuff::Error => e
|
|
28
|
+
pms_handle_rescue(e)
|
|
29
|
+
flash[:error] = e.message
|
|
30
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# DELETE /issues/:issue_id/testing
|
|
34
|
+
def destroy
|
|
35
|
+
issue = PlanMyStuff::Issue.find(params[:issue_id])
|
|
36
|
+
project_item = find_project_item!(issue)
|
|
37
|
+
|
|
38
|
+
PlanMyStuff::Pipeline.clear_testing!(project_item, user: pms_current_user)
|
|
39
|
+
|
|
40
|
+
yield(project_item) if block_given?
|
|
41
|
+
return if performed?
|
|
42
|
+
|
|
43
|
+
flash[:success] = "Issue ##{issue.number} testing cleared."
|
|
44
|
+
redirect_to(plan_my_stuff.issue_path(issue))
|
|
45
|
+
rescue ArgumentError, PlanMyStuff::Error => e
|
|
46
|
+
pms_handle_rescue(e)
|
|
47
|
+
flash[:error] = e.message
|
|
48
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Redirects non-support users back to the issue page. Mirrors +Issues::TakesController+'s authorization check.
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
#
|
|
57
|
+
def require_support_user
|
|
58
|
+
return if support_user?
|
|
59
|
+
|
|
60
|
+
redirect_to_unauthorized(
|
|
61
|
+
plan_my_stuff.issue_path(params[:issue_id]),
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Looks up the pipeline project item for the issue, raising if missing. Defensive against a stale view (button
|
|
66
|
+
# rendered before the item was removed) or a race with another tab removing the issue from the pipeline.
|
|
67
|
+
#
|
|
68
|
+
# @raise [PlanMyStuff::Error] if the issue is not in the pipeline
|
|
69
|
+
#
|
|
70
|
+
# @param issue [PlanMyStuff::Issue]
|
|
71
|
+
#
|
|
72
|
+
# @return [PlanMyStuff::ProjectItem]
|
|
73
|
+
#
|
|
74
|
+
def find_project_item!(issue)
|
|
75
|
+
project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue.number)
|
|
76
|
+
return project_item if project_item.present?
|
|
77
|
+
|
|
78
|
+
raise(PlanMyStuff::Error, "Issue ##{issue.number} is not in the pipeline.")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -20,9 +20,24 @@
|
|
|
20
20
|
<% if @support_user && @pipeline_enabled && @pipeline_item.nil? && @issue.assignees.blank? %>
|
|
21
21
|
<%= button_to('Take', plan_my_stuff.issue_take_path(@issue), method: :post) %>
|
|
22
22
|
<% end %>
|
|
23
|
-
<%
|
|
23
|
+
<%
|
|
24
|
+
show_unassign =
|
|
25
|
+
@support_user &&
|
|
26
|
+
@pipeline_enabled &&
|
|
27
|
+
@current_user_login.present? &&
|
|
28
|
+
@issue.assignees.include?(@current_user_login)
|
|
29
|
+
%>
|
|
30
|
+
<% if show_unassign %>
|
|
24
31
|
<%= button_to('Unassign', plan_my_stuff.issue_take_path(@issue), method: :delete) %>
|
|
25
32
|
<% end %>
|
|
33
|
+
<% if @support_user && @pipeline_enabled && @pipeline_item.present? %>
|
|
34
|
+
<% testing_value = @pipeline_item.field_values[PlanMyStuff.configuration.pipeline_testing_field_name] %>
|
|
35
|
+
<% if testing_value == PlanMyStuff.configuration.pipeline_testing_values[:active] %>
|
|
36
|
+
<%= button_to('Clear testing', plan_my_stuff.issue_testing_path(@issue), method: :delete) %>
|
|
37
|
+
<% else %>
|
|
38
|
+
<%= button_to('Request testing', plan_my_stuff.issue_testing_path(@issue), method: :post) %>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% end %>
|
|
26
41
|
<% if @support_user %>
|
|
27
42
|
<%= link_to('Start Testing Project', plan_my_stuff.new_testing_project_path(subject_url: @issue.html_url)) %>
|
|
28
43
|
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -15,6 +15,11 @@ PlanMyStuff::Engine.routes.draw do
|
|
|
15
15
|
only: %i[create destroy],
|
|
16
16
|
controller: config.controller_for(:'issues/takes'),
|
|
17
17
|
)
|
|
18
|
+
resource(
|
|
19
|
+
:testing,
|
|
20
|
+
only: %i[create destroy],
|
|
21
|
+
controller: config.controller_for(:'issues/testings'),
|
|
22
|
+
)
|
|
18
23
|
end
|
|
19
24
|
resources :comments, only: %i[create edit update], controller: config.controller_for(:comments)
|
|
20
25
|
resources :labels, only: %i[create destroy], controller: config.controller_for(:labels)
|
|
@@ -193,6 +193,27 @@ PlanMyStuff.configure do |config|
|
|
|
193
193
|
#
|
|
194
194
|
# config.issue_fields_enabled = false
|
|
195
195
|
|
|
196
|
+
# Rename the native Issue Fields the gem refers to internally to whatever your
|
|
197
|
+
# org actually calls them. Keys are the gem's canonical names; values are your
|
|
198
|
+
# org's field names. Applied on reads, writes, and filters; unmapped names pass
|
|
199
|
+
# through unchanged.
|
|
200
|
+
# config.issue_field_names = {
|
|
201
|
+
# 'Issue Status' => 'Status',
|
|
202
|
+
# 'Priority' => 'Prio',
|
|
203
|
+
# }
|
|
204
|
+
|
|
205
|
+
# Rename single-select option labels, nested by canonical field name. Keys are
|
|
206
|
+
# the gem's canonical values; values are your org's option labels. Unmapped
|
|
207
|
+
# fields / values pass through unchanged.
|
|
208
|
+
# config.issue_field_values = {
|
|
209
|
+
# 'Issue Status' => {
|
|
210
|
+
# 'Submitted' => 'Triaged',
|
|
211
|
+
# 'Waiting on Reply' => 'Awaiting Customer',
|
|
212
|
+
# 'Open' => 'Open',
|
|
213
|
+
# 'Reopened' => 'Reopened',
|
|
214
|
+
# },
|
|
215
|
+
# }
|
|
216
|
+
|
|
196
217
|
# --------------------------------------------------------------------------
|
|
197
218
|
# Boot behavior
|
|
198
219
|
# --------------------------------------------------------------------------
|
|
@@ -348,6 +369,7 @@ PlanMyStuff.configure do |config|
|
|
|
348
369
|
# :'issues/closures' => 'plan_my_stuff/issues/closures'
|
|
349
370
|
# :'issues/viewers' => 'plan_my_stuff/issues/viewers'
|
|
350
371
|
# :'issues/takes' => 'plan_my_stuff/issues/takes'
|
|
372
|
+
# :'issues/testings' => 'plan_my_stuff/issues/testings'
|
|
351
373
|
# :'issues/waitings' => 'plan_my_stuff/issues/waitings'
|
|
352
374
|
# :'issues/links' => 'plan_my_stuff/issues/links'
|
|
353
375
|
# :'issues/approvals' => 'plan_my_stuff/issues/approvals'
|
|
@@ -18,6 +18,7 @@ module PlanMyStuff
|
|
|
18
18
|
'issues/closures': 'plan_my_stuff/issues/closures',
|
|
19
19
|
'issues/viewers': 'plan_my_stuff/issues/viewers',
|
|
20
20
|
'issues/takes': 'plan_my_stuff/issues/takes',
|
|
21
|
+
'issues/testings': 'plan_my_stuff/issues/testings',
|
|
21
22
|
'issues/waitings': 'plan_my_stuff/issues/waitings',
|
|
22
23
|
'issues/links': 'plan_my_stuff/issues/links',
|
|
23
24
|
'issues/approvals': 'plan_my_stuff/issues/approvals',
|
|
@@ -196,6 +197,23 @@ module PlanMyStuff
|
|
|
196
197
|
#
|
|
197
198
|
attr_accessor :pipeline_testing_values
|
|
198
199
|
|
|
200
|
+
# Canonical Issue Field name to consumer field name map. Lets a consuming org rename the native issue fields the
|
|
201
|
+
# gem refers to internally (e.g. +"Issue Status"+ -> +"Status"+) without touching gem code. Applied via
|
|
202
|
+
# +PlanMyStuff::IssueFieldTranslation+ on reads, writes, and filters; unmapped names pass through unchanged.
|
|
203
|
+
#
|
|
204
|
+
# @return [Hash{String => String}]
|
|
205
|
+
#
|
|
206
|
+
attr_accessor :issue_field_names
|
|
207
|
+
|
|
208
|
+
# Canonical Issue Field value translations, nested by canonical field name:
|
|
209
|
+
# +{ 'Issue Status' => { 'Waiting on Reply' => 'Awaiting Customer', ... } }+. Lets a consuming org rename
|
|
210
|
+
# single-select option labels without touching gem code. Applied via +PlanMyStuff::IssueFieldTranslation+;
|
|
211
|
+
# unmapped fields / values pass through unchanged.
|
|
212
|
+
#
|
|
213
|
+
# @return [Hash{String => Hash{String => String}}]
|
|
214
|
+
#
|
|
215
|
+
attr_accessor :issue_field_values
|
|
216
|
+
|
|
199
217
|
# @return [String] branch name that PRs merge into for "Ready for release" transition
|
|
200
218
|
attr_accessor :main_branch
|
|
201
219
|
|
|
@@ -403,6 +421,8 @@ module PlanMyStuff
|
|
|
403
421
|
@pipeline_statuses = {}
|
|
404
422
|
@pipeline_testing_field_name = PlanMyStuff::Pipeline::Testing::FIELD_NAME
|
|
405
423
|
@pipeline_testing_values = PlanMyStuff::Pipeline::Testing::VALUES.dup
|
|
424
|
+
@issue_field_names = {}
|
|
425
|
+
@issue_field_values = {}
|
|
406
426
|
@main_branch = 'main'
|
|
407
427
|
@production_branch = 'production'
|
|
408
428
|
@mount_groups = { webhooks: true, issues: true, projects: true }
|
data/lib/plan_my_stuff/issue.rb
CHANGED
|
@@ -108,6 +108,11 @@ module PlanMyStuff
|
|
|
108
108
|
raise(PlanMyStuff::IssueFieldsNotEnabledError)
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
if issue_fields.present?
|
|
112
|
+
issue_fields = issue_fields.to_h.transform_keys(&:to_s)
|
|
113
|
+
issue_fields['Issue Status'] = 'Submitted' if issue_fields['Issue Status'].blank?
|
|
114
|
+
end
|
|
115
|
+
|
|
111
116
|
if body.blank?
|
|
112
117
|
raise(PlanMyStuff::ValidationError.new('body must be present', field: :body, expected_type: :string))
|
|
113
118
|
end
|
|
@@ -356,7 +361,7 @@ module PlanMyStuff
|
|
|
356
361
|
field_pairs = []
|
|
357
362
|
field_pairs.concat(build_issue_field_filter_pairs(issue_fields)) if issue_fields.present?
|
|
358
363
|
if priority_list && PlanMyStuff.configuration.issue_fields_enabled
|
|
359
|
-
field_pairs
|
|
364
|
+
field_pairs.concat(build_issue_field_filter_pairs('Priority List' => 'Yes'))
|
|
360
365
|
end
|
|
361
366
|
params[:issue_field_values] = field_pairs.join(',') if field_pairs.present?
|
|
362
367
|
|
|
@@ -428,7 +433,7 @@ module PlanMyStuff
|
|
|
428
433
|
field_pairs = []
|
|
429
434
|
field_pairs.concat(build_issue_field_filter_pairs(issue_fields)) if issue_fields.present?
|
|
430
435
|
if priority_list && PlanMyStuff.configuration.issue_fields_enabled
|
|
431
|
-
field_pairs
|
|
436
|
+
field_pairs.concat(build_issue_field_filter_pairs('Priority List' => 'Yes'))
|
|
432
437
|
end
|
|
433
438
|
qualifiers += field_pairs.map { |pair| "field.#{pair}" }
|
|
434
439
|
|
|
@@ -644,11 +649,13 @@ module PlanMyStuff
|
|
|
644
649
|
#
|
|
645
650
|
def build_issue_field_filter_pairs(hash)
|
|
646
651
|
hash.flat_map do |name, value|
|
|
647
|
-
slug = field_filter_slug(name)
|
|
652
|
+
slug = field_filter_slug(PlanMyStuff::IssueFieldTranslation.consumer_field_name(name))
|
|
648
653
|
case value
|
|
649
654
|
when nil then []
|
|
650
655
|
when Range then range_field_filter_pairs(slug, value)
|
|
651
|
-
else
|
|
656
|
+
else
|
|
657
|
+
consumer_value = PlanMyStuff::IssueFieldTranslation.consumer_value(name, value)
|
|
658
|
+
["#{slug}:#{format_field_filter_value(consumer_value)}"]
|
|
652
659
|
end
|
|
653
660
|
end
|
|
654
661
|
end
|
|
@@ -1028,7 +1035,13 @@ module PlanMyStuff
|
|
|
1028
1035
|
raise(PlanMyStuff::IssueFieldsNotEnabledError) unless PlanMyStuff.configuration.issue_fields_enabled
|
|
1029
1036
|
|
|
1030
1037
|
fields_by_name = PlanMyStuff::IssueField.list(org: repo.organization).index_by { |field| field.name.downcase }
|
|
1031
|
-
inputs = updates.map
|
|
1038
|
+
inputs = updates.map do |name, value|
|
|
1039
|
+
build_issue_field_input(
|
|
1040
|
+
fields_by_name,
|
|
1041
|
+
PlanMyStuff::IssueFieldTranslation.consumer_field_name(name),
|
|
1042
|
+
PlanMyStuff::IssueFieldTranslation.consumer_value(name, value),
|
|
1043
|
+
)
|
|
1044
|
+
end
|
|
1032
1045
|
|
|
1033
1046
|
PlanMyStuff.client.graphql(
|
|
1034
1047
|
PlanMyStuff::GraphQL::Queries::SET_ISSUE_FIELD_VALUES,
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
module PlanMyStuff
|
|
4
4
|
module IssueExtractions
|
|
5
5
|
module Waiting
|
|
6
|
+
# @return [Boolean]
|
|
7
|
+
def awaiting_reply?
|
|
8
|
+
issue_fields['Issue Status'] == 'Waiting on Reply'
|
|
9
|
+
end
|
|
10
|
+
|
|
6
11
|
# Marks the issue as waiting on an end-user reply. Sets +metadata.waiting_on_user_at+ to now, (re)computes
|
|
7
12
|
# +metadata.next_reminder_at+, and adds the configured +waiting_on_user_label+ to the issue. Called from
|
|
8
13
|
# +Comment.create!+ when a support user posts a comment with +waiting_on_reply: true+, and from the
|
|
@@ -19,6 +24,11 @@ module PlanMyStuff
|
|
|
19
24
|
PlanMyStuff::Label.ensure!(repo: repo, name: label)
|
|
20
25
|
PlanMyStuff::Label.add!(issue: self, labels: [label], user: user) if labels.exclude?(label)
|
|
21
26
|
|
|
27
|
+
to_update = {}
|
|
28
|
+
if PlanMyStuff.configuration.issue_fields_enabled
|
|
29
|
+
to_update[:issue_fields] = { 'Issue Status' => 'Waiting on Reply' }
|
|
30
|
+
end
|
|
31
|
+
|
|
22
32
|
self.class.update!(
|
|
23
33
|
number: number,
|
|
24
34
|
repo: repo,
|
|
@@ -26,6 +36,7 @@ module PlanMyStuff
|
|
|
26
36
|
waiting_on_user_at: PlanMyStuff.format_time(now),
|
|
27
37
|
next_reminder_at: format_next_reminder_at(from: now),
|
|
28
38
|
},
|
|
39
|
+
**to_update,
|
|
29
40
|
)
|
|
30
41
|
reload
|
|
31
42
|
end
|
|
@@ -42,6 +53,11 @@ module PlanMyStuff
|
|
|
42
53
|
|
|
43
54
|
PlanMyStuff::Label.remove!(issue: self, labels: [label]) if labels.include?(label)
|
|
44
55
|
|
|
56
|
+
to_update = {}
|
|
57
|
+
if PlanMyStuff.configuration.issue_fields_enabled
|
|
58
|
+
to_update[:issue_fields] = { 'Issue Status' => 'Open' }
|
|
59
|
+
end
|
|
60
|
+
|
|
45
61
|
self.class.update!(
|
|
46
62
|
number: number,
|
|
47
63
|
repo: repo,
|
|
@@ -50,6 +66,7 @@ module PlanMyStuff
|
|
|
50
66
|
next_reminder_at:
|
|
51
67
|
metadata.waiting_on_approval_at ? PlanMyStuff.format_time(metadata.next_reminder_at) : nil,
|
|
52
68
|
},
|
|
69
|
+
**to_update,
|
|
53
70
|
)
|
|
54
71
|
reload
|
|
55
72
|
end
|
|
@@ -67,11 +84,17 @@ module PlanMyStuff
|
|
|
67
84
|
inactive_label = PlanMyStuff.configuration.user_inactive_label
|
|
68
85
|
PlanMyStuff::Label.remove!(issue: self, labels: [inactive_label]) if labels.include?(inactive_label)
|
|
69
86
|
|
|
87
|
+
to_update = {}
|
|
88
|
+
if PlanMyStuff.configuration.issue_fields_enabled
|
|
89
|
+
to_update[:issue_fields] = { 'Issue Status' => 'Reopened' }
|
|
90
|
+
end
|
|
91
|
+
|
|
70
92
|
self.class.update!(
|
|
71
93
|
number: number,
|
|
72
94
|
repo: repo,
|
|
73
95
|
state: :open,
|
|
74
96
|
metadata: { closed_by_inactivity: false },
|
|
97
|
+
**to_update,
|
|
75
98
|
)
|
|
76
99
|
reload
|
|
77
100
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Translates between the canonical Issue Field names / values the gem refers to internally (e.g. +"Issue Status"+,
|
|
5
|
+
# +"Waiting on Reply"+) and the names / values a consuming org actually uses on GitHub. Driven by
|
|
6
|
+
# +config.issue_field_names+ (canonical field name => consumer field name) and +config.issue_field_values+
|
|
7
|
+
# (canonical field name => { canonical value => consumer value }).
|
|
8
|
+
#
|
|
9
|
+
# Outbound (canonical -> consumer) translation happens on writes and filters; inbound (consumer -> canonical) on
|
|
10
|
+
# reads, so internal comparisons like +issue_fields['Issue Status'] == 'Waiting on Reply'+ keep working regardless of
|
|
11
|
+
# how the org named the field or option. Unconfigured names / values pass through unchanged (identity fallback,
|
|
12
|
+
# mirroring +config.pipeline_statuses+).
|
|
13
|
+
module IssueFieldTranslation
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# @param canonical [String, Symbol] canonical field name
|
|
17
|
+
#
|
|
18
|
+
# @return [String] the consumer's field name, or the canonical name when unmapped
|
|
19
|
+
#
|
|
20
|
+
def consumer_field_name(canonical)
|
|
21
|
+
PlanMyStuff.configuration.issue_field_names.fetch(canonical.to_s, canonical.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param consumer [String] the consumer's field name (as GitHub returns it)
|
|
25
|
+
#
|
|
26
|
+
# @return [String] the canonical field name, or the consumer name when unmapped
|
|
27
|
+
#
|
|
28
|
+
def canonical_field_name(consumer)
|
|
29
|
+
PlanMyStuff.configuration.issue_field_names.invert.fetch(consumer, consumer)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Translates a canonical value to the consumer's value for the given canonical field. Non-String values (numbers,
|
|
33
|
+
# dates, +nil+) pass through untouched -- only single-select / text labels are translatable.
|
|
34
|
+
#
|
|
35
|
+
# @param canonical_field [String, Symbol] canonical field name
|
|
36
|
+
# @param value [Object, nil] canonical value
|
|
37
|
+
#
|
|
38
|
+
# @return [Object, nil] the consumer's value, or the input when unmapped / non-String
|
|
39
|
+
#
|
|
40
|
+
def consumer_value(canonical_field, value)
|
|
41
|
+
return value unless value.is_a?(String)
|
|
42
|
+
|
|
43
|
+
value_map(canonical_field).fetch(value, value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Translates a consumer value back to the canonical value for the given canonical field.
|
|
47
|
+
#
|
|
48
|
+
# @param canonical_field [String, Symbol] canonical field name
|
|
49
|
+
# @param value [Object, nil] consumer value
|
|
50
|
+
#
|
|
51
|
+
# @return [Object, nil] the canonical value, or the input when unmapped / non-String
|
|
52
|
+
#
|
|
53
|
+
def canonical_value(canonical_field, value)
|
|
54
|
+
return value unless value.is_a?(String)
|
|
55
|
+
|
|
56
|
+
value_map(canonical_field).invert.fetch(value, value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param canonical_field [String, Symbol]
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash{String => String}] canonical-value => consumer-value map for the field (empty when unconfigured)
|
|
62
|
+
#
|
|
63
|
+
def value_map(canonical_field)
|
|
64
|
+
PlanMyStuff.configuration.issue_field_values.fetch(canonical_field.to_s, {})
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -6,7 +6,8 @@ module PlanMyStuff
|
|
|
6
6
|
# as +Date+, number fields as +Float+, single-select fields as the option name +String+, and
|
|
7
7
|
# text fields as the raw +String+.
|
|
8
8
|
#
|
|
9
|
-
# Access is by field
|
|
9
|
+
# Access is by canonical field name (consumer names / values are reverse-translated via
|
|
10
|
+
# +PlanMyStuff::IssueFieldTranslation+ on construction); string and symbol keys both work. Iteration yields
|
|
10
11
|
# +[name, value]+ pairs in the order GitHub returned them.
|
|
11
12
|
class IssueFieldValueSet
|
|
12
13
|
include Enumerable
|
|
@@ -19,7 +20,10 @@ module PlanMyStuff
|
|
|
19
20
|
# @return [PlanMyStuff::IssueFieldValueSet]
|
|
20
21
|
#
|
|
21
22
|
def from_graphql(nodes)
|
|
22
|
-
pairs = Array.wrap(nodes).map
|
|
23
|
+
pairs = Array.wrap(nodes).map do |node|
|
|
24
|
+
canonical_name = PlanMyStuff::IssueFieldTranslation.canonical_field_name(node.dig(:field, :name))
|
|
25
|
+
[canonical_name, PlanMyStuff::IssueFieldTranslation.canonical_value(canonical_name, coerce(node))]
|
|
26
|
+
end
|
|
23
27
|
new(pairs.to_h)
|
|
24
28
|
end
|
|
25
29
|
|
|
@@ -200,6 +200,25 @@ module PlanMyStuff
|
|
|
200
200
|
result
|
|
201
201
|
end
|
|
202
202
|
|
|
203
|
+
# Reverses +request_testing!+ by flipping the +Testing+ custom single-select field back to its inactive value.
|
|
204
|
+
# Not approval-gated -- the forward transition (+request_testing!+) was already gated, so clearing should always
|
|
205
|
+
# succeed (matches +remove!+).
|
|
206
|
+
#
|
|
207
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
208
|
+
# @param user [Object, nil] actor forwarded to the +pipeline_testing_cleared.plan_my_stuff+ payload
|
|
209
|
+
#
|
|
210
|
+
# @return [Hash] mutation result
|
|
211
|
+
#
|
|
212
|
+
def clear_testing!(project_item, user: nil)
|
|
213
|
+
field_name = PlanMyStuff.configuration.pipeline_testing_field_name
|
|
214
|
+
value = PlanMyStuff.configuration.pipeline_testing_values.fetch(:inactive)
|
|
215
|
+
result = project_item.update_single_select_field!(field_name, value)
|
|
216
|
+
|
|
217
|
+
instrument('testing_cleared', project_item, field_name: field_name, value: value, user: user)
|
|
218
|
+
|
|
219
|
+
result
|
|
220
|
+
end
|
|
221
|
+
|
|
203
222
|
# Moves a project item to "Ready for Release".
|
|
204
223
|
#
|
|
205
224
|
# @param project_item [PlanMyStuff::ProjectItem]
|
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.26.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-05-
|
|
11
|
+
date: 2026-05-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -61,6 +61,7 @@ files:
|
|
|
61
61
|
- app/controllers/plan_my_stuff/issues/closures_controller.rb
|
|
62
62
|
- app/controllers/plan_my_stuff/issues/links_controller.rb
|
|
63
63
|
- app/controllers/plan_my_stuff/issues/takes_controller.rb
|
|
64
|
+
- app/controllers/plan_my_stuff/issues/testings_controller.rb
|
|
64
65
|
- app/controllers/plan_my_stuff/issues/viewers_controller.rb
|
|
65
66
|
- app/controllers/plan_my_stuff/issues/waitings_controller.rb
|
|
66
67
|
- app/controllers/plan_my_stuff/issues_controller.rb
|
|
@@ -133,6 +134,7 @@ files:
|
|
|
133
134
|
- lib/plan_my_stuff/issue_extractions/viewers.rb
|
|
134
135
|
- lib/plan_my_stuff/issue_extractions/waiting.rb
|
|
135
136
|
- lib/plan_my_stuff/issue_field.rb
|
|
137
|
+
- lib/plan_my_stuff/issue_field_translation.rb
|
|
136
138
|
- lib/plan_my_stuff/issue_field_value_set.rb
|
|
137
139
|
- lib/plan_my_stuff/issue_metadata.rb
|
|
138
140
|
- lib/plan_my_stuff/label.rb
|