plan_my_stuff 0.2.0 → 0.4.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 +10 -0
- data/README.md +569 -38
- data/app/controllers/plan_my_stuff/comments_controller.rb +5 -1
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +102 -0
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +37 -0
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +127 -0
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +88 -0
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +48 -0
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +47 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +22 -55
- data/app/controllers/plan_my_stuff/labels_controller.rb +4 -4
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +75 -0
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +40 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +0 -75
- data/app/controllers/plan_my_stuff/projects_controller.rb +65 -1
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +54 -0
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +39 -0
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +93 -0
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +148 -0
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +284 -0
- data/app/jobs/plan_my_stuff/application_job.rb +9 -0
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +81 -0
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +7 -0
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +87 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +70 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/show.html.erb +46 -3
- data/app/views/plan_my_stuff/projects/edit.html.erb +7 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +17 -1
- data/app/views/plan_my_stuff/projects/new.html.erb +7 -0
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +29 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +11 -4
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +12 -0
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +22 -0
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +7 -0
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +7 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +39 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +51 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +35 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
- data/config/routes.rb +38 -15
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +138 -5
- data/lib/plan_my_stuff/application_record.rb +144 -0
- data/lib/plan_my_stuff/approval.rb +80 -0
- data/lib/plan_my_stuff/archive/sweep.rb +85 -0
- data/lib/plan_my_stuff/archive.rb +14 -0
- data/lib/plan_my_stuff/aws_sns_simulator.rb +110 -0
- data/lib/plan_my_stuff/base_metadata.rb +0 -11
- data/lib/plan_my_stuff/base_project.rb +661 -0
- data/lib/plan_my_stuff/base_project_item.rb +562 -0
- data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
- data/lib/plan_my_stuff/cache.rb +197 -0
- data/lib/plan_my_stuff/client.rb +7 -0
- data/lib/plan_my_stuff/comment.rb +174 -54
- data/lib/plan_my_stuff/configuration.rb +254 -8
- data/lib/plan_my_stuff/custom_fields.rb +31 -17
- data/lib/plan_my_stuff/engine.rb +0 -4
- data/lib/plan_my_stuff/errors.rb +49 -0
- data/lib/plan_my_stuff/graphql/queries.rb +392 -0
- data/lib/plan_my_stuff/issue.rb +1477 -174
- data/lib/plan_my_stuff/issue_metadata.rb +122 -0
- data/lib/plan_my_stuff/label.rb +82 -11
- data/lib/plan_my_stuff/link.rb +144 -0
- data/lib/plan_my_stuff/notifications.rb +142 -0
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
- data/lib/plan_my_stuff/pipeline/status.rb +44 -0
- data/lib/plan_my_stuff/pipeline.rb +293 -0
- data/lib/plan_my_stuff/project.rb +62 -468
- data/lib/plan_my_stuff/project_item.rb +3 -417
- data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
- data/lib/plan_my_stuff/project_metadata.rb +47 -0
- data/lib/plan_my_stuff/reminders/closer.rb +70 -0
- data/lib/plan_my_stuff/reminders/fire.rb +129 -0
- data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
- data/lib/plan_my_stuff/reminders.rb +16 -0
- data/lib/plan_my_stuff/test_helpers.rb +260 -15
- data/lib/plan_my_stuff/testing_project.rb +291 -0
- data/lib/plan_my_stuff/testing_project_item.rb +184 -0
- data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
- data/lib/plan_my_stuff/user_resolver.rb +8 -3
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff/webhook_replayer.rb +280 -0
- data/lib/plan_my_stuff.rb +16 -0
- data/lib/tasks/plan_my_stuff.rake +163 -0
- metadata +54 -2
|
@@ -1,508 +1,102 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PlanMyStuff
|
|
4
|
-
# Wraps a GitHub Projects V2 project
|
|
5
|
-
#
|
|
4
|
+
# Wraps a regular (non-testing) GitHub Projects V2 project.
|
|
5
|
+
# Inherits all shared find/list/update/hydrate behavior from BaseProject.
|
|
6
|
+
# Adds +create!+ for regular project creation, and overrides +find+/+list+
|
|
7
|
+
# to exclude testing projects. For testing projects use +TestingProject+;
|
|
8
|
+
# for either type use +BaseProject.find+/+list+.
|
|
6
9
|
#
|
|
7
10
|
# Follows an ActiveRecord-style pattern:
|
|
8
11
|
# - `Project.find` / `Project.list` return persisted instances
|
|
9
12
|
# - `ProjectItem.add_item` / `ProjectItem.add_draft_item` for adding items
|
|
10
13
|
# - `ProjectItem.move_item` / `ProjectItem.assign` for item mutations
|
|
11
|
-
class Project < PlanMyStuff::
|
|
12
|
-
MAX_AUTO_PAGINATE_ITEMS = 500
|
|
13
|
-
ITEMS_PER_PAGE = 100
|
|
14
|
-
|
|
15
|
-
# @return [String] GitHub node ID
|
|
16
|
-
attr_reader :id
|
|
17
|
-
# @return [Integer] project number
|
|
18
|
-
attr_reader :number
|
|
19
|
-
# @return [Boolean] whether the project is closed
|
|
20
|
-
attr_reader :closed
|
|
21
|
-
|
|
22
|
-
# @return [String] project title
|
|
23
|
-
attr_accessor :title
|
|
24
|
-
# @return [String] project URL
|
|
25
|
-
attr_accessor :url
|
|
26
|
-
# @return [Array<Hash>] status options ({id:, name:})
|
|
27
|
-
attr_accessor :statuses
|
|
28
|
-
# @return [Array<Hash>] all field definitions
|
|
29
|
-
attr_accessor :fields
|
|
30
|
-
# @return [Array<PlanMyStuff::ProjectItem>] project items
|
|
31
|
-
attr_accessor :items
|
|
32
|
-
# @return [String, nil] cursor for next page (only in cursor mode)
|
|
33
|
-
attr_accessor :next_cursor
|
|
34
|
-
# @return [Boolean, nil] whether more pages exist (only in cursor mode)
|
|
35
|
-
attr_accessor :has_next_page
|
|
36
|
-
|
|
14
|
+
class Project < PlanMyStuff::BaseProject
|
|
37
15
|
class << self
|
|
38
|
-
# Creates a new project in the configured organization.
|
|
16
|
+
# Creates a new regular project in the configured organization with PMS metadata.
|
|
39
17
|
#
|
|
40
18
|
# @param title [String]
|
|
19
|
+
# @param user [Object, Integer, nil] user object or user_id
|
|
20
|
+
# @param visibility [String] "public" or "internal"
|
|
21
|
+
# @param custom_fields [Hash] app-defined field values
|
|
22
|
+
# @param readme [String] user-visible readme content
|
|
23
|
+
# @param description [String, nil] project short description
|
|
41
24
|
#
|
|
42
|
-
# @return [
|
|
43
|
-
#
|
|
44
|
-
def create(title:)
|
|
45
|
-
raise(NotImplementedError, "#{name}.create is not yet implemented")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Updates an existing project.
|
|
49
|
-
#
|
|
50
|
-
# @param project_number [Integer]
|
|
51
|
-
# @param title [String, nil]
|
|
52
|
-
#
|
|
53
|
-
# @return [Object]
|
|
54
|
-
#
|
|
55
|
-
def update(project_number:, title: nil)
|
|
56
|
-
raise(NotImplementedError, "#{name}.update is not yet implemented")
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Lists all projects in the configured organization.
|
|
60
|
-
#
|
|
61
|
-
# @return [Array<PlanMyStuff::Project>]
|
|
25
|
+
# @return [PlanMyStuff::Project]
|
|
62
26
|
#
|
|
63
|
-
def
|
|
27
|
+
def create!(title:, user: nil, visibility: 'internal', custom_fields: {}, readme: '', description: nil)
|
|
64
28
|
org = PlanMyStuff.configuration.organization
|
|
65
|
-
data = PlanMyStuff.client.graphql(list_query, variables: { org: org })
|
|
66
|
-
|
|
67
|
-
nodes = data.dig(:organization, :projectsV2, :nodes) || []
|
|
68
29
|
|
|
69
|
-
|
|
30
|
+
project_metadata = PlanMyStuff::ProjectMetadata.build(
|
|
31
|
+
user: user,
|
|
32
|
+
visibility: visibility,
|
|
33
|
+
custom_fields: custom_fields,
|
|
34
|
+
)
|
|
35
|
+
project_metadata.validate_custom_fields!
|
|
36
|
+
|
|
37
|
+
org_id = resolve_org_id(org)
|
|
38
|
+
data = PlanMyStuff.client.graphql(
|
|
39
|
+
PlanMyStuff::GraphQL::Queries::CREATE_PROJECT,
|
|
40
|
+
variables: { input: { ownerId: org_id, title: title } },
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
new_project = data.dig(:createProjectV2, :projectV2) || {}
|
|
44
|
+
project_id = new_project[:id]
|
|
45
|
+
project_number = new_project[:number]
|
|
46
|
+
|
|
47
|
+
serialized_readme = PlanMyStuff::MetadataParser.serialize(project_metadata.to_h, readme)
|
|
48
|
+
update_input = { projectId: project_id, readme: serialized_readme }
|
|
49
|
+
update_input[:shortDescription] = description if description.present?
|
|
50
|
+
|
|
51
|
+
PlanMyStuff.client.graphql(
|
|
52
|
+
PlanMyStuff::GraphQL::Queries::UPDATE_PROJECT,
|
|
53
|
+
variables: { input: update_input },
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
find(project_number)
|
|
70
57
|
end
|
|
71
58
|
|
|
72
|
-
# Finds a project by number
|
|
59
|
+
# Finds a regular project by number. Raises if the project is a testing
|
|
60
|
+
# project - call +TestingProject.find+ for those, or +BaseProject.find+
|
|
61
|
+
# if you want either type.
|
|
73
62
|
#
|
|
74
63
|
# @param number [Integer]
|
|
75
64
|
# @param paginate [Symbol] :auto (default) or :cursor
|
|
76
|
-
# @param cursor [String, nil] pagination cursor for :cursor mode
|
|
65
|
+
# @param cursor [String, nil] pagination cursor for :cursor mode
|
|
66
|
+
#
|
|
67
|
+
# @raise [ArgumentError] if the project is a testing project
|
|
77
68
|
#
|
|
78
69
|
# @return [PlanMyStuff::Project]
|
|
79
70
|
#
|
|
80
71
|
def find(number, paginate: :auto, cursor: nil)
|
|
81
|
-
|
|
72
|
+
project = super
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
find_auto_paginated(org, number)
|
|
86
|
-
when :cursor
|
|
87
|
-
find_with_cursor(org, number, cursor: cursor)
|
|
88
|
-
else
|
|
89
|
-
raise(ArgumentError, "Unknown paginate mode: #{paginate.inspect}. Use :auto or :cursor")
|
|
74
|
+
unless project.is_a?(self)
|
|
75
|
+
raise(ArgumentError, "Project ##{number} is a testing project; use PlanMyStuff::TestingProject.find")
|
|
90
76
|
end
|
|
77
|
+
|
|
78
|
+
project
|
|
91
79
|
end
|
|
92
80
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
81
|
+
# Lists all regular projects in the configured organization.
|
|
82
|
+
# Testing projects are excluded; use +TestingProject.list+ or
|
|
83
|
+
# +BaseProject.list+ to reach them.
|
|
96
84
|
#
|
|
97
|
-
# @return [
|
|
85
|
+
# @return [Array<PlanMyStuff::Project>]
|
|
98
86
|
#
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
PlanMyStuff.configuration.default_project_number ||
|
|
103
|
-
raise(ArgumentError, 'project_number is required when config.default_project_number is not set')
|
|
87
|
+
def list
|
|
88
|
+
super.select { |p| p.is_a?(self) }
|
|
104
89
|
end
|
|
105
|
-
|
|
106
|
-
private
|
|
107
|
-
|
|
108
|
-
# @return [String]
|
|
109
|
-
def list_query
|
|
110
|
-
<<~GRAPHQL
|
|
111
|
-
query($org: String!) {
|
|
112
|
-
organization(login: $org) {
|
|
113
|
-
projectsV2(first: 100) {
|
|
114
|
-
nodes {
|
|
115
|
-
id
|
|
116
|
-
number
|
|
117
|
-
title
|
|
118
|
-
url
|
|
119
|
-
closed
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
GRAPHQL
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# @return [String]
|
|
128
|
-
def find_query
|
|
129
|
-
<<~GRAPHQL
|
|
130
|
-
query($org: String!, $number: Int!, $cursor: String) {
|
|
131
|
-
organization(login: $org) {
|
|
132
|
-
projectV2(number: $number) {
|
|
133
|
-
id
|
|
134
|
-
number
|
|
135
|
-
title
|
|
136
|
-
url
|
|
137
|
-
closed
|
|
138
|
-
fields(first: 50) {
|
|
139
|
-
nodes {
|
|
140
|
-
... on ProjectV2SingleSelectField {
|
|
141
|
-
id
|
|
142
|
-
name
|
|
143
|
-
options {
|
|
144
|
-
id
|
|
145
|
-
name
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
... on ProjectV2Field {
|
|
149
|
-
id
|
|
150
|
-
name
|
|
151
|
-
}
|
|
152
|
-
... on ProjectV2IterationField {
|
|
153
|
-
id
|
|
154
|
-
name
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
items(first: #{ITEMS_PER_PAGE}, after: $cursor) {
|
|
159
|
-
pageInfo {
|
|
160
|
-
hasNextPage
|
|
161
|
-
endCursor
|
|
162
|
-
}
|
|
163
|
-
nodes {
|
|
164
|
-
id
|
|
165
|
-
type
|
|
166
|
-
content {
|
|
167
|
-
... on Issue {
|
|
168
|
-
id
|
|
169
|
-
title
|
|
170
|
-
number
|
|
171
|
-
url
|
|
172
|
-
state
|
|
173
|
-
repository { nameWithOwner }
|
|
174
|
-
}
|
|
175
|
-
... on PullRequest {
|
|
176
|
-
id
|
|
177
|
-
title
|
|
178
|
-
number
|
|
179
|
-
url
|
|
180
|
-
state
|
|
181
|
-
repository { nameWithOwner }
|
|
182
|
-
}
|
|
183
|
-
... on DraftIssue {
|
|
184
|
-
id
|
|
185
|
-
title
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
fieldValues(first: 20) {
|
|
189
|
-
nodes {
|
|
190
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
191
|
-
name
|
|
192
|
-
field {
|
|
193
|
-
... on ProjectV2SingleSelectField {
|
|
194
|
-
name
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
... on ProjectV2ItemFieldTextValue {
|
|
199
|
-
text
|
|
200
|
-
field {
|
|
201
|
-
... on ProjectV2Field {
|
|
202
|
-
name
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
... on ProjectV2ItemFieldUserValue {
|
|
207
|
-
users(first: 10) {
|
|
208
|
-
nodes {
|
|
209
|
-
login
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
field {
|
|
213
|
-
... on ProjectV2Field {
|
|
214
|
-
name
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
GRAPHQL
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# @return [String]
|
|
229
|
-
def project_id_query
|
|
230
|
-
<<~GRAPHQL
|
|
231
|
-
query($org: String!, $number: Int!) {
|
|
232
|
-
organization(login: $org) {
|
|
233
|
-
projectV2(number: $number) {
|
|
234
|
-
id
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
GRAPHQL
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# Builds a summary Project from a list query node.
|
|
242
|
-
#
|
|
243
|
-
# @param node [Hash]
|
|
244
|
-
#
|
|
245
|
-
# @return [PlanMyStuff::Project]
|
|
246
|
-
#
|
|
247
|
-
def build_summary(node)
|
|
248
|
-
project = new
|
|
249
|
-
project.__send__(:hydrate_summary, node)
|
|
250
|
-
project
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
# Builds a detailed Project from a find query response.
|
|
254
|
-
#
|
|
255
|
-
# @param graphql_project [Hash]
|
|
256
|
-
# @param items [Array<Hash>]
|
|
257
|
-
# @param next_cursor [String, nil]
|
|
258
|
-
# @param has_next_page [Boolean, nil]
|
|
259
|
-
#
|
|
260
|
-
# @return [PlanMyStuff::Project]
|
|
261
|
-
#
|
|
262
|
-
def build_detail(graphql_project, items:, next_cursor: nil, has_next_page: nil)
|
|
263
|
-
project = new
|
|
264
|
-
project.__send__(
|
|
265
|
-
:hydrate_detail,
|
|
266
|
-
graphql_project,
|
|
267
|
-
items: items,
|
|
268
|
-
next_cursor: next_cursor,
|
|
269
|
-
has_next_page: has_next_page,
|
|
270
|
-
)
|
|
271
|
-
project
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# @param org [String]
|
|
275
|
-
# @param number [Integer]
|
|
276
|
-
#
|
|
277
|
-
# @return [PlanMyStuff::Project]
|
|
278
|
-
#
|
|
279
|
-
def find_auto_paginated(org, number)
|
|
280
|
-
all_items = []
|
|
281
|
-
cursor = nil
|
|
282
|
-
raw_project = nil
|
|
283
|
-
|
|
284
|
-
loop do
|
|
285
|
-
page = fetch_project_page(org, number, cursor)
|
|
286
|
-
raw_project ||= page[:raw]
|
|
287
|
-
all_items.concat(page[:items])
|
|
288
|
-
|
|
289
|
-
break if !page[:has_next_page] || all_items.length >= MAX_AUTO_PAGINATE_ITEMS
|
|
290
|
-
|
|
291
|
-
cursor = page[:next_cursor]
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
build_detail(raw_project, items: all_items)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
# @param org [String]
|
|
298
|
-
# @param number [Integer]
|
|
299
|
-
# @param cursor [String, nil]
|
|
300
|
-
#
|
|
301
|
-
# @return [PlanMyStuff::Project]
|
|
302
|
-
#
|
|
303
|
-
def find_with_cursor(org, number, cursor:)
|
|
304
|
-
page = fetch_project_page(org, number, cursor)
|
|
305
|
-
build_detail(
|
|
306
|
-
page[:raw],
|
|
307
|
-
items: page[:items],
|
|
308
|
-
next_cursor: page[:next_cursor],
|
|
309
|
-
has_next_page: page[:has_next_page],
|
|
310
|
-
)
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
# Fetches a single page of project data. Returns a lightweight hash
|
|
314
|
-
# for pagination loop consumption (not a Project instance).
|
|
315
|
-
#
|
|
316
|
-
# @param org [String]
|
|
317
|
-
# @param number [Integer]
|
|
318
|
-
# @param cursor [String, nil]
|
|
319
|
-
#
|
|
320
|
-
# @return [Hash] with :raw, :items, :next_cursor, :has_next_page
|
|
321
|
-
#
|
|
322
|
-
def fetch_project_page(org, number, cursor)
|
|
323
|
-
variables = { org: org, number: number }
|
|
324
|
-
variables[:cursor] = cursor if cursor
|
|
325
|
-
|
|
326
|
-
data = PlanMyStuff.client.graphql(find_query, variables: variables)
|
|
327
|
-
|
|
328
|
-
raw_project = data.dig(:organization, :projectV2)
|
|
329
|
-
page_info = raw_project.dig(:items, :pageInfo) || {}
|
|
330
|
-
items_data = raw_project.dig(:items, :nodes) || []
|
|
331
|
-
|
|
332
|
-
{
|
|
333
|
-
raw: raw_project,
|
|
334
|
-
items: items_data.map { |item| parse_project_item(item) },
|
|
335
|
-
next_cursor: page_info[:endCursor],
|
|
336
|
-
has_next_page: page_info[:hasNextPage],
|
|
337
|
-
}
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
# @param item [Hash] raw GraphQL project item node
|
|
341
|
-
#
|
|
342
|
-
# @return [Hash]
|
|
343
|
-
#
|
|
344
|
-
def parse_project_item(item)
|
|
345
|
-
content = item[:content] || {}
|
|
346
|
-
field_values = item.dig(:fieldValues, :nodes) || []
|
|
347
|
-
repo_name = content.dig(:repository, :nameWithOwner)
|
|
348
|
-
|
|
349
|
-
{
|
|
350
|
-
id: item[:id],
|
|
351
|
-
type: item[:type],
|
|
352
|
-
content_node_id: content[:id],
|
|
353
|
-
title: content[:title],
|
|
354
|
-
number: content[:number],
|
|
355
|
-
url: content[:url],
|
|
356
|
-
state: content[:state],
|
|
357
|
-
repo: repo_name.present? ? PlanMyStuff::Repo.resolve(repo_name) : nil,
|
|
358
|
-
status: extract_item_status(field_values),
|
|
359
|
-
field_values: parse_field_values(field_values),
|
|
360
|
-
}
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
# @param field_values [Array<Hash>]
|
|
364
|
-
#
|
|
365
|
-
# @return [String, nil]
|
|
366
|
-
#
|
|
367
|
-
def extract_item_status(field_values)
|
|
368
|
-
status_value = field_values.find { |fv| fv.dig(:field, :name) == 'Status' }
|
|
369
|
-
|
|
370
|
-
status_value&.dig(:name)
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
# @param field_values [Array<Hash>]
|
|
374
|
-
#
|
|
375
|
-
# @return [Hash]
|
|
376
|
-
#
|
|
377
|
-
def parse_field_values(field_values)
|
|
378
|
-
result = {}
|
|
379
|
-
|
|
380
|
-
field_values.each do |fv|
|
|
381
|
-
field_name = fv.dig(:field, :name)
|
|
382
|
-
next unless field_name
|
|
383
|
-
|
|
384
|
-
value = fv[:name] || fv[:text]
|
|
385
|
-
users_node = fv[:users]
|
|
386
|
-
if users_node
|
|
387
|
-
value = (users_node[:nodes] || []).map { |u| u[:login] }
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
result[field_name] = value
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
result
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
# Resolves a project number to its node ID.
|
|
397
|
-
#
|
|
398
|
-
# @param org [String]
|
|
399
|
-
# @param project_number [Integer]
|
|
400
|
-
#
|
|
401
|
-
# @return [String]
|
|
402
|
-
#
|
|
403
|
-
def resolve_project_id(org, project_number)
|
|
404
|
-
data = PlanMyStuff.client.graphql(
|
|
405
|
-
project_id_query,
|
|
406
|
-
variables: { org: org, number: project_number },
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
data.dig(:organization, :projectV2, :id)
|
|
410
|
-
end
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
# @see super
|
|
414
|
-
def initialize(**attrs)
|
|
415
|
-
@id = attrs.delete(:id)
|
|
416
|
-
@number = attrs.delete(:number)
|
|
417
|
-
@closed = attrs.delete(:closed)
|
|
418
|
-
super
|
|
419
|
-
@statuses ||= []
|
|
420
|
-
@fields ||= []
|
|
421
|
-
@items ||= []
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
# Returns the Status single-select field definition.
|
|
425
|
-
#
|
|
426
|
-
# @return [Hash] with :id and :options keys
|
|
427
|
-
#
|
|
428
|
-
# @raise [PlanMyStuff::APIError] if no Status field exists
|
|
429
|
-
#
|
|
430
|
-
def status_field
|
|
431
|
-
field = fields.find { |f| f[:name] == 'Status' && f[:options] }
|
|
432
|
-
|
|
433
|
-
raise(APIError, "No 'Status' field found on project ##{number}") unless field
|
|
434
|
-
|
|
435
|
-
field
|
|
436
90
|
end
|
|
437
91
|
|
|
438
92
|
private
|
|
439
93
|
|
|
440
|
-
#
|
|
441
|
-
#
|
|
442
|
-
# @param node [Hash]
|
|
443
|
-
#
|
|
444
|
-
# @return [void]
|
|
445
|
-
#
|
|
446
|
-
def hydrate_summary(node)
|
|
447
|
-
@id = node[:id]
|
|
448
|
-
@number = node[:number]
|
|
449
|
-
@title = node[:title]
|
|
450
|
-
@url = node[:url]
|
|
451
|
-
@closed = node[:closed]
|
|
452
|
-
@persisted = true
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
# Populates this instance from a detailed find query response.
|
|
456
|
-
#
|
|
457
|
-
# @param graphql_project [Hash]
|
|
458
|
-
# @param items [Array<Hash>]
|
|
459
|
-
# @param next_cursor [String, nil]
|
|
460
|
-
# @param has_next_page [Boolean, nil]
|
|
461
|
-
#
|
|
462
|
-
# @return [void]
|
|
463
|
-
#
|
|
464
|
-
def hydrate_detail(graphql_project, items:, next_cursor: nil, has_next_page: nil)
|
|
465
|
-
@id = graphql_project[:id]
|
|
466
|
-
@number = graphql_project[:number]
|
|
467
|
-
@title = graphql_project[:title]
|
|
468
|
-
@url = graphql_project[:url]
|
|
469
|
-
@closed = graphql_project[:closed]
|
|
470
|
-
|
|
471
|
-
fields_nodes = graphql_project.dig(:fields, :nodes) || []
|
|
472
|
-
@statuses = extract_statuses(fields_nodes)
|
|
473
|
-
@fields = extract_fields(fields_nodes)
|
|
474
|
-
@items = items.map { |item_hash| ProjectItem.build(item_hash, project: self) }
|
|
475
|
-
@next_cursor = next_cursor
|
|
476
|
-
@has_next_page = has_next_page
|
|
477
|
-
@persisted = true
|
|
478
|
-
end
|
|
479
|
-
|
|
480
|
-
# Extracts status options from the "Status" single-select field.
|
|
481
|
-
#
|
|
482
|
-
# @param fields_nodes [Array<Hash>]
|
|
94
|
+
# Returns the item class used to build items for testing projects.
|
|
483
95
|
#
|
|
484
|
-
# @return [
|
|
96
|
+
# @return [Class]
|
|
485
97
|
#
|
|
486
|
-
def
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
return [] unless status_field
|
|
490
|
-
|
|
491
|
-
(status_field[:options] || []).map do |opt|
|
|
492
|
-
{ id: opt[:id], name: opt[:name] }
|
|
493
|
-
end
|
|
494
|
-
end
|
|
495
|
-
|
|
496
|
-
# @param fields_nodes [Array<Hash>]
|
|
497
|
-
#
|
|
498
|
-
# @return [Array<Hash>]
|
|
499
|
-
#
|
|
500
|
-
def extract_fields(fields_nodes)
|
|
501
|
-
fields_nodes.map do |f|
|
|
502
|
-
field = { id: f[:id], name: f[:name] }
|
|
503
|
-
field[:options] = f[:options].map { |o| { id: o[:id], name: o[:name] } } if f[:options]
|
|
504
|
-
field
|
|
505
|
-
end
|
|
98
|
+
def item_class
|
|
99
|
+
PlanMyStuff::ProjectItem
|
|
506
100
|
end
|
|
507
101
|
end
|
|
508
102
|
end
|