plan_my_stuff 0.11.0 → 0.12.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 +20 -0
- data/CONFIGURATION.md +20 -0
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +10 -0
- data/lib/plan_my_stuff/configuration.rb +10 -0
- data/lib/plan_my_stuff/errors.rb +11 -0
- data/lib/plan_my_stuff/graphql/queries.rb +50 -0
- data/lib/plan_my_stuff/issue.rb +101 -2
- data/lib/plan_my_stuff/issue_field.rb +126 -0
- data/lib/plan_my_stuff/issue_field_value_set.rb +64 -0
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d23d00fb8f2addae60d416beb76a19f81fdf0bb0ce5235453262317c6e351c94
|
|
4
|
+
data.tar.gz: a83cf20f6347ae27df34133d20b0b10646f5a96346616d0b5594ea4bd965c10f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4cb5444db19f7a551f7296d96145db4e4be90d7897235abd877771dbbc15ffcd3361a4faade94fa521f7e491828c794854d2b0ac97b76e38e7c122f4fc63d07
|
|
7
|
+
data.tar.gz: a46060793225471181698152cf2681d8be3f64252a845aa2f9e0a838ffc1977f88c81f2b60b28507c8e8a7e7eb64e67016d163d7353d6c9dc9f5faa9b5f8e140
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `PlanMyStuff::IssueField` for org-level GitHub Issue Field definitions (public preview).
|
|
8
|
+
Exposes `.list`, `.find` (case-insensitive), and `#option_id_for!` for resolving
|
|
9
|
+
single-select option names.
|
|
10
|
+
- `PlanMyStuff::Issue#issue_fields` returns a hash-like `IssueFieldValueSet` view of the
|
|
11
|
+
field values on an issue. Values are coerced to native types (`Date` for date fields,
|
|
12
|
+
`Float` for numbers, the option name `String` for single-selects).
|
|
13
|
+
- `PlanMyStuff::Issue#set_issue_fields!(updates)` writes one or more field values in a
|
|
14
|
+
single GraphQL mutation; passing `nil` as a value clears the field.
|
|
15
|
+
- `issue_fields:` kwarg on `Issue.create!`, `Issue.update!`, `Issue#save!`, and
|
|
16
|
+
`Issue#update!` so callers can set field values inline with create/update instead of
|
|
17
|
+
following up with an explicit `set_issue_fields!` call.
|
|
18
|
+
- `Configuration#issue_fields_enabled` (default `true`, opt-out). Set to `false` if your
|
|
19
|
+
org has not been admitted to the Issue Fields preview - with the flag off,
|
|
20
|
+
`Issue#issue_fields` returns an empty set and the write paths raise
|
|
21
|
+
`IssueFieldsNotEnabledError` instead of letting a raw GraphQL error surface.
|
|
22
|
+
|
|
3
23
|
## 0.11.0
|
|
4
24
|
|
|
5
25
|
### Added
|
data/CONFIGURATION.md
CHANGED
|
@@ -192,6 +192,26 @@ config.issue_types = {
|
|
|
192
192
|
}
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
## Issue Fields (public preview)
|
|
196
|
+
|
|
197
|
+
| Option | Type | Default | Description |
|
|
198
|
+
|---|---|---|---|
|
|
199
|
+
| `issue_fields_enabled` | `Boolean` | `true` | Whether the Issue Fields public preview is wired up for the org. |
|
|
200
|
+
|
|
201
|
+
GitHub Issue Fields are structured per-issue metadata (text, number, date, or single-select)
|
|
202
|
+
configured once at the org level. The preview is rolling out org-by-org. Leave this flag at its
|
|
203
|
+
default (`true`) once your org has been admitted; flip to `false` to keep the gem from issuing
|
|
204
|
+
calls that would otherwise return raw GraphQL errors.
|
|
205
|
+
|
|
206
|
+
With the flag off:
|
|
207
|
+
|
|
208
|
+
- `Issue#issue_fields` returns an empty `IssueFieldValueSet` without making a request.
|
|
209
|
+
- `Issue#set_issue_fields!(...)` and `IssueField.list` raise `IssueFieldsNotEnabledError`.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
config.issue_fields_enabled = false # org not admitted to the preview yet
|
|
213
|
+
```
|
|
214
|
+
|
|
195
215
|
## Release pipeline
|
|
196
216
|
|
|
197
217
|
| Option | Type | Default | Description |
|
|
@@ -168,6 +168,16 @@ PlanMyStuff.configure do |config|
|
|
|
168
168
|
# 'Feature' => 'Enhancement',
|
|
169
169
|
# }
|
|
170
170
|
|
|
171
|
+
# --------------------------------------------------------------------------
|
|
172
|
+
# Issue Fields (public preview)
|
|
173
|
+
# --------------------------------------------------------------------------
|
|
174
|
+
# GitHub Issue Fields is a per-org public preview. Default is true (opt-out).
|
|
175
|
+
# Flip to false if your org has not been admitted to the preview - read paths
|
|
176
|
+
# return an empty set and write paths raise IssueFieldsNotEnabledError instead
|
|
177
|
+
# of letting a raw GraphQL error bubble.
|
|
178
|
+
#
|
|
179
|
+
# config.issue_fields_enabled = false
|
|
180
|
+
|
|
171
181
|
# --------------------------------------------------------------------------
|
|
172
182
|
# Release pipeline
|
|
173
183
|
# --------------------------------------------------------------------------
|
|
@@ -356,6 +356,15 @@ module PlanMyStuff
|
|
|
356
356
|
#
|
|
357
357
|
attr_accessor :attachment_repo
|
|
358
358
|
|
|
359
|
+
# Whether GitHub's Issue Fields (public preview) are wired up for the configured org. Defaults to +true+ (opt-out):
|
|
360
|
+
# when +false+, +Issue#issue_fields+ returns an empty +IssueFieldValueSet+ without making a request and
|
|
361
|
+
# +Issue#set_issue_fields!+ / +IssueField.list+ raise +IssueFieldsNotEnabledError+. Set to +false+ if your org has
|
|
362
|
+
# not been admitted to the Issue Fields preview, to avoid raw GraphQL errors from GitHub.
|
|
363
|
+
#
|
|
364
|
+
# @return [Boolean]
|
|
365
|
+
#
|
|
366
|
+
attr_accessor :issue_fields_enabled
|
|
367
|
+
|
|
359
368
|
# @return [Configuration]
|
|
360
369
|
def initialize
|
|
361
370
|
@repos = {}
|
|
@@ -394,6 +403,7 @@ module PlanMyStuff
|
|
|
394
403
|
@archived_label = 'archived'
|
|
395
404
|
@pipeline_completion_purge_enabled = true
|
|
396
405
|
@pipeline_completion_ttl_hours = 24
|
|
406
|
+
@issue_fields_enabled = true
|
|
397
407
|
@process_aws_webhooks = Rails.env.production?
|
|
398
408
|
@sns_verifier_class = ::Aws::SNS::MessageVerifier if defined?(::Aws::SNS::MessageVerifier)
|
|
399
409
|
@sns_verifier_error =
|
data/lib/plan_my_stuff/errors.rb
CHANGED
|
@@ -106,6 +106,17 @@ module PlanMyStuff
|
|
|
106
106
|
class LockedIssueError < PlanMyStuff::Error
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# Raised when an Issue Fields API call is attempted while
|
|
110
|
+
# +config.issue_fields_enabled+ is +false+. Consumers whose org has not been
|
|
111
|
+
# admitted to the Issue Fields public preview flip the flag off; this error
|
|
112
|
+
# surfaces faster (and with a clearer message) than the underlying
|
|
113
|
+
# +GraphQLError+ that GitHub would otherwise return.
|
|
114
|
+
class IssueFieldsNotEnabledError < PlanMyStuff::Error
|
|
115
|
+
def initialize(message = nil)
|
|
116
|
+
super(message || 'Issue Fields are disabled; set config.issue_fields_enabled = true to enable')
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
109
120
|
# Raised by +PlanMyStuff::Pipeline+ forward transitions when the linked
|
|
110
121
|
# +Issue+ has any pending manager approvals.
|
|
111
122
|
class PendingApprovalsError < PlanMyStuff::ValidationError
|
|
@@ -399,6 +399,56 @@ module PlanMyStuff
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
GRAPHQL
|
|
402
|
+
|
|
403
|
+
# --- Issue Fields (org-level public preview) -----------------------
|
|
404
|
+
|
|
405
|
+
LIST_ORG_ISSUE_FIELDS = <<~GRAPHQL
|
|
406
|
+
query($org: String!) {
|
|
407
|
+
organization(login: $org) {
|
|
408
|
+
issueFields(first: 50) {
|
|
409
|
+
nodes {
|
|
410
|
+
__typename
|
|
411
|
+
... on IssueFieldText { id name description }
|
|
412
|
+
... on IssueFieldNumber { id name description }
|
|
413
|
+
... on IssueFieldDate { id name description }
|
|
414
|
+
... on IssueFieldSingleSelect {
|
|
415
|
+
id name description
|
|
416
|
+
options { id name description color }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
GRAPHQL
|
|
423
|
+
|
|
424
|
+
READ_ISSUE_FIELD_VALUES = <<~GRAPHQL
|
|
425
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
426
|
+
repository(owner: $owner, name: $name) {
|
|
427
|
+
issue(number: $number) {
|
|
428
|
+
issueFieldValues(first: 50) {
|
|
429
|
+
nodes {
|
|
430
|
+
__typename
|
|
431
|
+
... on IssueFieldTextValue { value field { ... on IssueFieldText { id name } } }
|
|
432
|
+
... on IssueFieldNumberValue { value field { ... on IssueFieldNumber { id name } } }
|
|
433
|
+
... on IssueFieldDateValue { value field { ... on IssueFieldDate { id name } } }
|
|
434
|
+
... on IssueFieldSingleSelectValue {
|
|
435
|
+
name optionId
|
|
436
|
+
field { ... on IssueFieldSingleSelect { id name } }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
GRAPHQL
|
|
444
|
+
|
|
445
|
+
SET_ISSUE_FIELD_VALUES = <<~GRAPHQL
|
|
446
|
+
mutation($issueId: ID!, $issueFields: [IssueFieldCreateOrUpdateInput!]!) {
|
|
447
|
+
setIssueFieldValue(input: { issueId: $issueId, issueFields: $issueFields }) {
|
|
448
|
+
issue { number }
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
GRAPHQL
|
|
402
452
|
end
|
|
403
453
|
end
|
|
404
454
|
end
|
data/lib/plan_my_stuff/issue.rb
CHANGED
|
@@ -84,6 +84,9 @@ module PlanMyStuff
|
|
|
84
84
|
# @param visibility_allowlist [Array<Integer>] user IDs for internal comment access
|
|
85
85
|
# @param issue_type [String, nil] GitHub issue type name (e.g. +"Bug"+, +"Feature"+). Must match a type
|
|
86
86
|
# configured on the org. +nil+ creates the issue with no type.
|
|
87
|
+
# @param issue_fields [Hash{String,Symbol => Object,nil}, nil] GitHub Issue Field values to apply after the
|
|
88
|
+
# issue is created. Delegates to +#set_issue_fields!+, so the same coercion rules and
|
|
89
|
+
# +IssueFieldsNotEnabledError+ behavior apply. +nil+ or an empty hash is a no-op.
|
|
87
90
|
# @param attachments [Array] files to upload to +config.attachment_repo+ and record on the body comment. Each
|
|
88
91
|
# entry may be an uploaded-file object responding to +#path+ and +#original_filename+ (e.g. Rails
|
|
89
92
|
# +ActionDispatch::Http::UploadedFile+), a String/Pathname path to a local file, or a pre-built
|
|
@@ -103,8 +106,13 @@ module PlanMyStuff
|
|
|
103
106
|
visibility: 'public',
|
|
104
107
|
visibility_allowlist: [],
|
|
105
108
|
issue_type: nil,
|
|
109
|
+
issue_fields: nil,
|
|
106
110
|
attachments: []
|
|
107
111
|
)
|
|
112
|
+
if issue_fields.present? && !PlanMyStuff.configuration.issue_fields_enabled
|
|
113
|
+
raise(PlanMyStuff::IssueFieldsNotEnabledError)
|
|
114
|
+
end
|
|
115
|
+
|
|
108
116
|
if body.blank?
|
|
109
117
|
raise(PlanMyStuff::ValidationError.new('body must be present', field: :body, expected_type: :string))
|
|
110
118
|
end
|
|
@@ -160,6 +168,8 @@ module PlanMyStuff
|
|
|
160
168
|
attachments: attachments,
|
|
161
169
|
)
|
|
162
170
|
|
|
171
|
+
issue.set_issue_fields!(issue_fields) if issue_fields.present?
|
|
172
|
+
|
|
163
173
|
issue.reload
|
|
164
174
|
PlanMyStuff::Notifications.instrument('issue.created', issue, user: user)
|
|
165
175
|
issue
|
|
@@ -187,6 +197,9 @@ module PlanMyStuff
|
|
|
187
197
|
# @param issue_type [String, nil] GitHub issue type name. Pass a String to set, +nil+ to clear, or omit the
|
|
188
198
|
# kwarg to leave the current type untouched. (+nil+-vs-omitted is differentiated by the private
|
|
189
199
|
# +ISSUE_TYPE_UNCHANGED+ sentinel.)
|
|
200
|
+
# @param issue_fields [Hash{String,Symbol => Object,nil}, nil] GitHub Issue Field values to apply after the
|
|
201
|
+
# PATCH (or instead of it, when no other attrs are provided). Delegates to +#set_issue_fields!+, so the same
|
|
202
|
+
# coercion rules and +IssueFieldsNotEnabledError+ behavior apply. +nil+ or an empty hash is a no-op.
|
|
190
203
|
#
|
|
191
204
|
# @return [Object]
|
|
192
205
|
#
|
|
@@ -199,7 +212,8 @@ module PlanMyStuff
|
|
|
199
212
|
labels: nil,
|
|
200
213
|
state: nil,
|
|
201
214
|
assignees: nil,
|
|
202
|
-
issue_type: ISSUE_TYPE_UNCHANGED
|
|
215
|
+
issue_type: ISSUE_TYPE_UNCHANGED,
|
|
216
|
+
issue_fields: nil
|
|
203
217
|
)
|
|
204
218
|
client = PlanMyStuff.client
|
|
205
219
|
resolved_repo = client.resolve_repo!(repo)
|
|
@@ -236,7 +250,8 @@ module PlanMyStuff
|
|
|
236
250
|
|
|
237
251
|
update_body_comment!(number, resolved_repo, body) if body
|
|
238
252
|
|
|
239
|
-
|
|
253
|
+
updated_issue = find(number, repo: resolved_repo).set_issue_fields!(issue_fields) if issue_fields.present?
|
|
254
|
+
return updated_issue if options.none?
|
|
240
255
|
|
|
241
256
|
result = client.rest(:update_issue, resolved_repo, number, **options)
|
|
242
257
|
store_etag_to_cache(client, resolved_repo, number, result, cache_writer: :write_issue)
|
|
@@ -557,6 +572,7 @@ module PlanMyStuff
|
|
|
557
572
|
visibility: metadata.visibility,
|
|
558
573
|
visibility_allowlist: Array.wrap(metadata.visibility_allowlist),
|
|
559
574
|
issue_type: issue_type,
|
|
575
|
+
issue_fields: @pending_issue_fields,
|
|
560
576
|
)
|
|
561
577
|
hydrate_from_issue(created)
|
|
562
578
|
else
|
|
@@ -565,6 +581,7 @@ module PlanMyStuff
|
|
|
565
581
|
instrument_update(captured_changes, user) unless skip_notification
|
|
566
582
|
end
|
|
567
583
|
|
|
584
|
+
@pending_issue_fields = nil
|
|
568
585
|
self
|
|
569
586
|
end
|
|
570
587
|
|
|
@@ -666,6 +683,42 @@ module PlanMyStuff
|
|
|
666
683
|
safe_read_field(github_response, :id)
|
|
667
684
|
end
|
|
668
685
|
|
|
686
|
+
# Returns a hash-like view of GitHub Issue Field values currently set on this issue. Reads on first access and
|
|
687
|
+
# memoizes; +set_issue_fields!+ invalidates the cache. Returns an empty set without making a request when
|
|
688
|
+
# +config.issue_fields_enabled+ is +false+.
|
|
689
|
+
#
|
|
690
|
+
# @return [PlanMyStuff::IssueFieldValueSet]
|
|
691
|
+
#
|
|
692
|
+
def issue_fields
|
|
693
|
+
@issue_fields ||= load_issue_fields!
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Bulk-updates GitHub Issue Field values in a single +setIssueFieldValue+ mutation. Each key is the field display
|
|
697
|
+
# name; values are coerced to the right input fragment based on the field's type. Passing +nil+ as a value clears
|
|
698
|
+
# that field.
|
|
699
|
+
#
|
|
700
|
+
# @raise [PlanMyStuff::IssueFieldsNotEnabledError] when +config.issue_fields_enabled+ is +false+
|
|
701
|
+
# @raise [PlanMyStuff::Error] when a referenced field name does not exist on the org
|
|
702
|
+
#
|
|
703
|
+
# @param updates [Hash{String,Symbol => Object,nil}]
|
|
704
|
+
#
|
|
705
|
+
# @return [self]
|
|
706
|
+
#
|
|
707
|
+
def set_issue_fields!(updates)
|
|
708
|
+
raise(PlanMyStuff::IssueFieldsNotEnabledError) unless PlanMyStuff.configuration.issue_fields_enabled
|
|
709
|
+
|
|
710
|
+
fields_by_name = PlanMyStuff::IssueField.list(org: repo.organization).index_by { |field| field.name.downcase }
|
|
711
|
+
inputs = updates.map { |name, value| build_issue_field_input(fields_by_name, name, value) }
|
|
712
|
+
|
|
713
|
+
PlanMyStuff.client.graphql(
|
|
714
|
+
PlanMyStuff::GraphQL::Queries::SET_ISSUE_FIELD_VALUES,
|
|
715
|
+
variables: { issueId: github_node_id, issueFields: inputs },
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
@issue_fields = nil
|
|
719
|
+
self
|
|
720
|
+
end
|
|
721
|
+
|
|
669
722
|
private
|
|
670
723
|
|
|
671
724
|
# Populates this instance from a GitHub API response.
|
|
@@ -695,6 +748,7 @@ module PlanMyStuff
|
|
|
695
748
|
@body_dirty = false
|
|
696
749
|
persisted!
|
|
697
750
|
@comments = nil
|
|
751
|
+
@issue_fields = nil
|
|
698
752
|
invalidate_links_cache!
|
|
699
753
|
end
|
|
700
754
|
|
|
@@ -722,6 +776,7 @@ module PlanMyStuff
|
|
|
722
776
|
self.metadata = other.metadata
|
|
723
777
|
persisted!
|
|
724
778
|
@comments = nil
|
|
779
|
+
@issue_fields = nil
|
|
725
780
|
invalidate_links_cache!
|
|
726
781
|
end
|
|
727
782
|
|
|
@@ -764,6 +819,7 @@ module PlanMyStuff
|
|
|
764
819
|
}
|
|
765
820
|
attrs[:body] = body if @body_dirty
|
|
766
821
|
attrs[:assignees] = @pending_assignees unless @pending_assignees.nil?
|
|
822
|
+
attrs[:issue_fields] = @pending_issue_fields if @pending_issue_fields.present?
|
|
767
823
|
attrs[:issue_type] = issue_type if issue_type_changed?
|
|
768
824
|
|
|
769
825
|
clear_waiting_state_on_close(attrs)
|
|
@@ -789,6 +845,7 @@ module PlanMyStuff
|
|
|
789
845
|
self.body = attrs[:body] if attrs.key?(:body)
|
|
790
846
|
self.issue_type = attrs[:issue_type] if attrs.key?(:issue_type)
|
|
791
847
|
@pending_assignees = attrs[:assignees] if attrs.key?(:assignees)
|
|
848
|
+
@pending_issue_fields = attrs[:issue_fields] if attrs.key?(:issue_fields)
|
|
792
849
|
apply_metadata_attrs(attrs[:metadata]) if attrs.key?(:metadata)
|
|
793
850
|
end
|
|
794
851
|
|
|
@@ -905,5 +962,47 @@ module PlanMyStuff
|
|
|
905
962
|
|
|
906
963
|
id
|
|
907
964
|
end
|
|
965
|
+
|
|
966
|
+
# @return [PlanMyStuff::IssueFieldValueSet]
|
|
967
|
+
def load_issue_fields!
|
|
968
|
+
return PlanMyStuff::IssueFieldValueSet.new({}) unless PlanMyStuff.configuration.issue_fields_enabled
|
|
969
|
+
|
|
970
|
+
data = PlanMyStuff.client.graphql(
|
|
971
|
+
PlanMyStuff::GraphQL::Queries::READ_ISSUE_FIELD_VALUES,
|
|
972
|
+
variables: { owner: repo.organization, name: repo.name, number: number },
|
|
973
|
+
)
|
|
974
|
+
nodes = data.dig(:repository, :issue, :issueFieldValues, :nodes)
|
|
975
|
+
PlanMyStuff::IssueFieldValueSet.from_graphql(nodes)
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# Builds one element of the +issueFields+ argument to the +setIssueFieldValue+ mutation. Looks up the field
|
|
979
|
+
# definition in the provided hash to pick the right input fragment and (for single-select) resolve the option ID.
|
|
980
|
+
#
|
|
981
|
+
# @raise [PlanMyStuff::Error] if the field name is unknown on the org
|
|
982
|
+
#
|
|
983
|
+
# @param fields_by_name [Hash{String => PlanMyStuff::IssueField}] fields keyed by downcased display name
|
|
984
|
+
# @param name [String, Symbol]
|
|
985
|
+
# @param value [Object, nil]
|
|
986
|
+
#
|
|
987
|
+
# @return [Hash]
|
|
988
|
+
#
|
|
989
|
+
def build_issue_field_input(fields_by_name, name, value)
|
|
990
|
+
field = fields_by_name[name.to_s.downcase]
|
|
991
|
+
raise(PlanMyStuff::Error, "Unknown Issue Field #{name.inspect}") if field.nil?
|
|
992
|
+
|
|
993
|
+
return { fieldId: field.id, delete: true } if value.nil?
|
|
994
|
+
|
|
995
|
+
case field.type
|
|
996
|
+
when :single_select then { fieldId: field.id, singleSelectOptionId: field.option_id_for!(value) }
|
|
997
|
+
when :date then { fieldId: field.id, dateValue: value.to_date.iso8601 }
|
|
998
|
+
when :number
|
|
999
|
+
unless value.is_a?(Numeric)
|
|
1000
|
+
raise(PlanMyStuff::Error, "Issue Field #{name.inspect} expects Numeric, got #{value.inspect}")
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
{ fieldId: field.id, numberValue: value.to_f }
|
|
1004
|
+
when :text then { fieldId: field.id, textValue: value.to_s }
|
|
1005
|
+
end
|
|
1006
|
+
end
|
|
908
1007
|
end
|
|
909
1008
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Value object wrapping an organization-level GitHub Issue Field definition (public preview).
|
|
5
|
+
# Issue Fields are structured per-issue metadata (text, number, date, or single-select)
|
|
6
|
+
# configured once at the org level and applied across all of the org's repositories.
|
|
7
|
+
#
|
|
8
|
+
# Read-only on the gem side: callers manage field *definitions* through the GitHub UI, while
|
|
9
|
+
# the gem only handles field *values* on individual issues (see +Issue#issue_fields+).
|
|
10
|
+
class IssueField
|
|
11
|
+
# GraphQL +__typename+ -> normalized type symbol used internally.
|
|
12
|
+
TYPES = {
|
|
13
|
+
IssueFieldText: :text,
|
|
14
|
+
IssueFieldNumber: :number,
|
|
15
|
+
IssueFieldDate: :date,
|
|
16
|
+
IssueFieldSingleSelect: :single_select,
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# @return [String] GraphQL node ID, e.g. +"IFSS_kgDOAAGskA"+
|
|
20
|
+
attr_reader :id
|
|
21
|
+
|
|
22
|
+
# @return [String] display name (e.g. +"Priority"+)
|
|
23
|
+
attr_reader :name
|
|
24
|
+
|
|
25
|
+
# @return [Symbol] one of +:text+, +:number+, +:date+, +:single_select+
|
|
26
|
+
attr_reader :type
|
|
27
|
+
|
|
28
|
+
# @return [String, nil]
|
|
29
|
+
attr_reader :description
|
|
30
|
+
|
|
31
|
+
# @return [Array<Hash>] for +:single_select+, the option list as returned by GraphQL with symbol keys
|
|
32
|
+
# (+id+, +name+, +description+, +color+). Empty for other field types.
|
|
33
|
+
attr_reader :options
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Lists Issue Field definitions configured on the org.
|
|
37
|
+
#
|
|
38
|
+
# @raise [PlanMyStuff::IssueFieldsNotEnabledError] if +config.issue_fields_enabled+ is +false+
|
|
39
|
+
#
|
|
40
|
+
# @param org [String, nil] org login; defaults to +config.organization+
|
|
41
|
+
#
|
|
42
|
+
# @return [Array<PlanMyStuff::IssueField>]
|
|
43
|
+
#
|
|
44
|
+
def list(org: nil)
|
|
45
|
+
ensure_enabled!
|
|
46
|
+
|
|
47
|
+
org_login = org || PlanMyStuff.configuration.organization
|
|
48
|
+
data = PlanMyStuff.client.graphql(
|
|
49
|
+
PlanMyStuff::GraphQL::Queries::LIST_ORG_ISSUE_FIELDS,
|
|
50
|
+
variables: { org: org_login },
|
|
51
|
+
)
|
|
52
|
+
Array.wrap(data.dig(:organization, :issueFields, :nodes)).map { |node| from_graphql(node) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param name [String, Symbol]
|
|
56
|
+
# @param org [String, nil]
|
|
57
|
+
#
|
|
58
|
+
# @return [PlanMyStuff::IssueField, nil]
|
|
59
|
+
#
|
|
60
|
+
def find(name, org: nil)
|
|
61
|
+
list(org: org).find { |field| field.name.casecmp?(name.to_s) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param node [Hash] one node from +LIST_ORG_ISSUE_FIELDS+
|
|
65
|
+
#
|
|
66
|
+
# @return [PlanMyStuff::IssueField]
|
|
67
|
+
#
|
|
68
|
+
def from_graphql(node)
|
|
69
|
+
typename = node[:__typename]
|
|
70
|
+
type = TYPES[typename.to_sym] if typename
|
|
71
|
+
raise(PlanMyStuff::Error, "Unknown Issue Field typename: #{typename.inspect}") if type.nil?
|
|
72
|
+
|
|
73
|
+
new(
|
|
74
|
+
id: node.fetch(:id),
|
|
75
|
+
name: node.fetch(:name),
|
|
76
|
+
type: type,
|
|
77
|
+
description: node[:description],
|
|
78
|
+
options: Array.wrap(node[:options]),
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# @raise [PlanMyStuff::IssueFieldsNotEnabledError]
|
|
85
|
+
#
|
|
86
|
+
# @return [void]
|
|
87
|
+
#
|
|
88
|
+
def ensure_enabled!
|
|
89
|
+
return if PlanMyStuff.configuration.issue_fields_enabled
|
|
90
|
+
|
|
91
|
+
raise(PlanMyStuff::IssueFieldsNotEnabledError)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @param id [String]
|
|
96
|
+
# @param name [String]
|
|
97
|
+
# @param type [Symbol]
|
|
98
|
+
# @param description [String, nil]
|
|
99
|
+
# @param options [Array<Hash>]
|
|
100
|
+
#
|
|
101
|
+
def initialize(id:, name:, type:, description: nil, options: [])
|
|
102
|
+
@id = id
|
|
103
|
+
@name = name
|
|
104
|
+
@type = type
|
|
105
|
+
@description = description
|
|
106
|
+
@options = options
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Resolves a single-select option name to its GraphQL node ID.
|
|
110
|
+
#
|
|
111
|
+
# @raise [PlanMyStuff::Error] if this field is not a single-select, or the option name is unknown
|
|
112
|
+
#
|
|
113
|
+
# @param option_name [String, Symbol]
|
|
114
|
+
#
|
|
115
|
+
# @return [String]
|
|
116
|
+
#
|
|
117
|
+
def option_id_for!(option_name)
|
|
118
|
+
raise(PlanMyStuff::Error, "Field #{name.inspect} is not a single-select") unless type == :single_select
|
|
119
|
+
|
|
120
|
+
match = options.find { |option| option.fetch(:name).casecmp?(option_name.to_s) }
|
|
121
|
+
raise(PlanMyStuff::Error, "Unknown option #{option_name.inspect} for field #{name.inspect}") if match.nil?
|
|
122
|
+
|
|
123
|
+
match.fetch(:id)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Hash-like read-side view of GitHub Issue Field values on a single +Issue+. Returned by
|
|
5
|
+
# +Issue#issue_fields+. Values are coerced into Ruby types on construction: date fields come back
|
|
6
|
+
# as +Date+, number fields as +Float+, single-select fields as the option name +String+, and
|
|
7
|
+
# text fields as the raw +String+.
|
|
8
|
+
#
|
|
9
|
+
# Access is by field display name; string and symbol keys both work. Iteration yields
|
|
10
|
+
# +[name, value]+ pairs in the order GitHub returned them.
|
|
11
|
+
class IssueFieldValueSet
|
|
12
|
+
include Enumerable
|
|
13
|
+
|
|
14
|
+
delegate :empty?, to: :@hash
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# @param nodes [Array<Hash>, nil] +issueFieldValues.nodes+ from the GraphQL read query
|
|
18
|
+
#
|
|
19
|
+
# @return [PlanMyStuff::IssueFieldValueSet]
|
|
20
|
+
#
|
|
21
|
+
def from_graphql(nodes)
|
|
22
|
+
pairs = Array.wrap(nodes).map { |node| [node.dig(:field, :name), coerce(node)] }
|
|
23
|
+
new(pairs.to_h)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param node [Hash]
|
|
27
|
+
#
|
|
28
|
+
# @return [Object]
|
|
29
|
+
#
|
|
30
|
+
def coerce(node)
|
|
31
|
+
case node[:__typename].to_s
|
|
32
|
+
when 'IssueFieldDateValue' then Date.parse(node.fetch(:value))
|
|
33
|
+
when 'IssueFieldNumberValue' then node.fetch(:value).to_f
|
|
34
|
+
when 'IssueFieldSingleSelectValue' then node.fetch(:name)
|
|
35
|
+
else node.fetch(:value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param hash [Hash{String => Object}]
|
|
41
|
+
#
|
|
42
|
+
def initialize(hash)
|
|
43
|
+
@hash = hash
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param name [String, Symbol] field display name
|
|
47
|
+
#
|
|
48
|
+
# @return [Object, nil]
|
|
49
|
+
#
|
|
50
|
+
def [](name)
|
|
51
|
+
@hash[name.to_s]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Hash{String => Object}] copy of the underlying hash
|
|
55
|
+
def to_h
|
|
56
|
+
@hash.dup
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Enumerator, void]
|
|
60
|
+
def each(&)
|
|
61
|
+
@hash.each(&)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/plan_my_stuff.rb
CHANGED
|
@@ -24,6 +24,8 @@ require_relative 'plan_my_stuff/engine' if defined?(Rails)
|
|
|
24
24
|
require_relative 'plan_my_stuff/errors'
|
|
25
25
|
require_relative 'plan_my_stuff/graphql/queries'
|
|
26
26
|
require_relative 'plan_my_stuff/issue'
|
|
27
|
+
require_relative 'plan_my_stuff/issue_field'
|
|
28
|
+
require_relative 'plan_my_stuff/issue_field_value_set'
|
|
27
29
|
require_relative 'plan_my_stuff/issue_metadata'
|
|
28
30
|
require_relative 'plan_my_stuff/label'
|
|
29
31
|
require_relative 'plan_my_stuff/link'
|
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.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brands Insurance
|
|
@@ -132,6 +132,8 @@ files:
|
|
|
132
132
|
- lib/plan_my_stuff/issue_extractions/links.rb
|
|
133
133
|
- lib/plan_my_stuff/issue_extractions/viewers.rb
|
|
134
134
|
- lib/plan_my_stuff/issue_extractions/waiting.rb
|
|
135
|
+
- lib/plan_my_stuff/issue_field.rb
|
|
136
|
+
- lib/plan_my_stuff/issue_field_value_set.rb
|
|
135
137
|
- lib/plan_my_stuff/issue_metadata.rb
|
|
136
138
|
- lib/plan_my_stuff/label.rb
|
|
137
139
|
- lib/plan_my_stuff/link.rb
|