plan_my_stuff 0.3.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 +11 -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/index.html.erb +15 -1
- data/app/views/plan_my_stuff/projects/show.html.erb +10 -5
- 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 +121 -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_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 +171 -50
- data/lib/plan_my_stuff/configuration.rb +210 -10
- 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 +1476 -175
- 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 +30 -693
- 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 +9 -3
- 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 +15 -0
- data/lib/tasks/plan_my_stuff.rake +163 -0
- metadata +50 -2
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Shared base for GitHub Projects V2 item wrappers. Holds attribute
|
|
5
|
+
# definitions, generic build/create/move/update/delete/assign machinery,
|
|
6
|
+
# and core instance helpers.
|
|
7
|
+
#
|
|
8
|
+
# Not meant to be used directly. Concrete subclasses (ProjectItem,
|
|
9
|
+
# TestingProjectItem) add their own domain-specific methods.
|
|
10
|
+
#
|
|
11
|
+
# Class methods:
|
|
12
|
+
# BaseProjectItem.create!(issue) -- add existing issue
|
|
13
|
+
# BaseProjectItem.create!(title, draft: true, body: "...") -- create draft item
|
|
14
|
+
# BaseProjectItem.move_item(item_id:, status:)
|
|
15
|
+
# BaseProjectItem.assign(item_id:, assignee:)
|
|
16
|
+
#
|
|
17
|
+
# Instance methods:
|
|
18
|
+
# item.move_to!("Done")
|
|
19
|
+
# item.assign!("octocat")
|
|
20
|
+
#
|
|
21
|
+
class BaseProjectItem < PlanMyStuff::ApplicationRecord
|
|
22
|
+
# @return [String, nil] GitHub node ID (e.g. "PVTI_...")
|
|
23
|
+
attribute :id, :string
|
|
24
|
+
# @return [String, nil] node ID of the underlying content (Issue, PR, or DraftIssue)
|
|
25
|
+
attribute :content_node_id, :string
|
|
26
|
+
# @return [String, nil] GitHub item type (e.g. "DRAFT_ISSUE", "ISSUE", "PULL_REQUEST")
|
|
27
|
+
attribute :type, :string
|
|
28
|
+
# @return [String, nil]
|
|
29
|
+
attribute :title, :string
|
|
30
|
+
# @return [String, nil] full body as stored on GitHub (draft items only)
|
|
31
|
+
attribute :raw_body, :string
|
|
32
|
+
# @return [String, nil] user-visible body (metadata comment stripped)
|
|
33
|
+
attribute :body, :string
|
|
34
|
+
# @return [PlanMyStuff::ProjectItemMetadata] parsed metadata
|
|
35
|
+
attribute :metadata, default: -> { PlanMyStuff::ProjectItemMetadata.new }
|
|
36
|
+
# @return [Integer, nil]
|
|
37
|
+
attribute :number, :integer
|
|
38
|
+
# @return [String, nil]
|
|
39
|
+
attribute :url, :string
|
|
40
|
+
# @return [PlanMyStuff::Repo, nil]
|
|
41
|
+
attribute :repo
|
|
42
|
+
# @return [String, nil]
|
|
43
|
+
attribute :state, :string
|
|
44
|
+
# @return [String, nil]
|
|
45
|
+
attribute :status, :string
|
|
46
|
+
# @return [Hash]
|
|
47
|
+
attribute :field_values, default: -> { {} }
|
|
48
|
+
# @return [PlanMyStuff::BaseProject, nil]
|
|
49
|
+
attribute :project
|
|
50
|
+
# @return [PlanMyStuff::Issue, nil] linked issue (nil for draft items)
|
|
51
|
+
attribute :issue
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# Builds a persisted item from parsed item data.
|
|
55
|
+
#
|
|
56
|
+
# @param item_hash [Hash] parsed item data (from BaseProject.parse_project_item)
|
|
57
|
+
# @param project [PlanMyStuff::BaseProject] parent project
|
|
58
|
+
#
|
|
59
|
+
# @return [PlanMyStuff::BaseProjectItem]
|
|
60
|
+
#
|
|
61
|
+
def build(item_hash, project:)
|
|
62
|
+
raw_body_val = item_hash[:body]
|
|
63
|
+
|
|
64
|
+
item = new(
|
|
65
|
+
id: item_hash[:id],
|
|
66
|
+
type: item_hash[:type],
|
|
67
|
+
content_node_id: item_hash[:content_node_id],
|
|
68
|
+
title: item_hash[:title],
|
|
69
|
+
raw_body: raw_body_val,
|
|
70
|
+
number: item_hash[:number],
|
|
71
|
+
url: item_hash[:url],
|
|
72
|
+
repo: item_hash[:repo],
|
|
73
|
+
state: item_hash[:state],
|
|
74
|
+
status: item_hash[:status],
|
|
75
|
+
field_values: item_hash[:field_values] || {},
|
|
76
|
+
project: project,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if raw_body_val
|
|
80
|
+
parsed = PlanMyStuff::MetadataParser.parse(raw_body_val)
|
|
81
|
+
item.metadata = PlanMyStuff::ProjectItemMetadata.from_hash(parsed[:metadata] || {})
|
|
82
|
+
item.body = parsed[:body]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
item.instance_variable_set(:@github_response, item_hash[:github_response])
|
|
86
|
+
item.__send__(:persisted!)
|
|
87
|
+
item
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Creates a project item by adding an existing issue or creating a draft.
|
|
91
|
+
#
|
|
92
|
+
# @param issue_or_title [PlanMyStuff::Issue, String] Issue instance (non-draft) or title string (draft)
|
|
93
|
+
# @param draft [Boolean] when true, creates a draft item from a title string
|
|
94
|
+
# @param body [String, nil] body for draft items (ignored for non-draft)
|
|
95
|
+
# @param project_number [Integer, nil] defaults to config.default_project_number
|
|
96
|
+
#
|
|
97
|
+
# @return [PlanMyStuff::BaseProjectItem]
|
|
98
|
+
#
|
|
99
|
+
def create!(issue_or_title, draft: false, body: nil, project_number: nil, user: nil)
|
|
100
|
+
item =
|
|
101
|
+
if draft
|
|
102
|
+
add_draft_item(title: issue_or_title, body: body, project_number: project_number)
|
|
103
|
+
else
|
|
104
|
+
add_item(issue: issue_or_title, project_number: project_number)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
PlanMyStuff::Notifications.instrument('project_item.added', item, user: user)
|
|
108
|
+
item
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Moves a project item to a new status column.
|
|
112
|
+
#
|
|
113
|
+
# @param item_id [String] project item ID (e.g. "PVTI_...")
|
|
114
|
+
# @param status [String] status name, resolved to option ID internally
|
|
115
|
+
# @param project_number [Integer, nil] defaults to config.default_project_number
|
|
116
|
+
#
|
|
117
|
+
# @return [Hash] the updated item
|
|
118
|
+
#
|
|
119
|
+
def move_item(item_id:, status:, project_number: nil)
|
|
120
|
+
project_number = resolve_default_project_number(project_number)
|
|
121
|
+
project = BaseProject.find(project_number)
|
|
122
|
+
|
|
123
|
+
status_field = project.status_field
|
|
124
|
+
option_id = resolve_status_option_id(status_field, status)
|
|
125
|
+
|
|
126
|
+
PlanMyStuff.client.graphql(
|
|
127
|
+
PlanMyStuff::GraphQL::Queries::UPDATE_SINGLE_SELECT_FIELD,
|
|
128
|
+
variables: {
|
|
129
|
+
projectId: project.id,
|
|
130
|
+
itemId: item_id,
|
|
131
|
+
fieldId: status_field[:id],
|
|
132
|
+
optionId: option_id,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Updates a single-select custom field on a project item.
|
|
138
|
+
#
|
|
139
|
+
# @param item_id [String] project item ID (e.g. "PVTI_...")
|
|
140
|
+
# @param field_name [String] single-select field name (e.g. "Pass Mode")
|
|
141
|
+
# @param value [String] option name to select
|
|
142
|
+
# @param project_number [Integer, nil] defaults to config.default_project_number
|
|
143
|
+
#
|
|
144
|
+
# @return [Hash] mutation result
|
|
145
|
+
#
|
|
146
|
+
def update_single_select_field!(item_id:, field_name:, value:, project_number: nil)
|
|
147
|
+
project_number = resolve_default_project_number(project_number)
|
|
148
|
+
project = BaseProject.find(project_number)
|
|
149
|
+
|
|
150
|
+
field = resolve_single_select_field(project, field_name)
|
|
151
|
+
option_id = resolve_status_option_id(field, value)
|
|
152
|
+
|
|
153
|
+
PlanMyStuff.client.graphql(
|
|
154
|
+
PlanMyStuff::GraphQL::Queries::UPDATE_SINGLE_SELECT_FIELD,
|
|
155
|
+
variables: {
|
|
156
|
+
projectId: project.id,
|
|
157
|
+
itemId: item_id,
|
|
158
|
+
fieldId: field[:id],
|
|
159
|
+
optionId: option_id,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Updates a text custom field on a project item.
|
|
165
|
+
#
|
|
166
|
+
# @param item_id [String] project item ID (e.g. "PVTI_...")
|
|
167
|
+
# @param field_name [String] text field name (e.g. "Deployment ID")
|
|
168
|
+
# @param value [String] text value to set
|
|
169
|
+
# @param project_number [Integer, nil] defaults to config.default_project_number
|
|
170
|
+
#
|
|
171
|
+
# @return [Hash] mutation result
|
|
172
|
+
#
|
|
173
|
+
def update_field!(item_id:, field_name:, value:, project_number: nil)
|
|
174
|
+
project_number = resolve_default_project_number(project_number)
|
|
175
|
+
project = BaseProject.find(project_number)
|
|
176
|
+
|
|
177
|
+
field = resolve_text_field(project, field_name)
|
|
178
|
+
|
|
179
|
+
PlanMyStuff.client.graphql(
|
|
180
|
+
PlanMyStuff::GraphQL::Queries::UPDATE_TEXT_FIELD,
|
|
181
|
+
variables: {
|
|
182
|
+
projectId: project.id,
|
|
183
|
+
itemId: item_id,
|
|
184
|
+
fieldId: field[:id],
|
|
185
|
+
value: value,
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Updates a date custom field on a project item.
|
|
191
|
+
#
|
|
192
|
+
# @param item_id [String] project item ID (e.g. "PVTI_...")
|
|
193
|
+
# @param field_name [String] date field name (e.g. "Due Date")
|
|
194
|
+
# @param date [Date, String] date value to set
|
|
195
|
+
# @param project_number [Integer, nil] defaults to config.default_project_number
|
|
196
|
+
#
|
|
197
|
+
# @return [Hash] mutation result
|
|
198
|
+
#
|
|
199
|
+
def update_date_field!(item_id:, field_name:, date:, project_number: nil)
|
|
200
|
+
project_number = resolve_default_project_number(project_number)
|
|
201
|
+
project = BaseProject.find(project_number)
|
|
202
|
+
|
|
203
|
+
field = resolve_text_field(project, field_name)
|
|
204
|
+
|
|
205
|
+
PlanMyStuff.client.graphql(
|
|
206
|
+
PlanMyStuff::GraphQL::Queries::UPDATE_DATE_FIELD,
|
|
207
|
+
variables: {
|
|
208
|
+
projectId: project.id,
|
|
209
|
+
itemId: item_id,
|
|
210
|
+
fieldId: field[:id],
|
|
211
|
+
date: date.to_s,
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Deletes a project item from its parent project. Returns the
|
|
217
|
+
# +deletedItemId+ from the GraphQL response on success.
|
|
218
|
+
#
|
|
219
|
+
# @param item_id [String] project item ID (e.g. "PVTI_...")
|
|
220
|
+
# @param project_number [Integer, nil] defaults to config.default_project_number
|
|
221
|
+
#
|
|
222
|
+
# @return [String] the deleted item ID echoed back by GitHub
|
|
223
|
+
#
|
|
224
|
+
# @raise [PlanMyStuff::APIError] if the GraphQL mutation fails
|
|
225
|
+
#
|
|
226
|
+
def delete_item(item_id:, project_number: nil)
|
|
227
|
+
project_number = resolve_default_project_number(project_number)
|
|
228
|
+
org = PlanMyStuff.configuration.organization
|
|
229
|
+
project_id = resolve_project_id(org, project_number)
|
|
230
|
+
|
|
231
|
+
data = PlanMyStuff.client.graphql(
|
|
232
|
+
PlanMyStuff::GraphQL::Queries::DELETE_PROJECT_ITEM,
|
|
233
|
+
variables: { projectId: project_id, itemId: item_id },
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
deleted_id = data.dig(:deleteProjectV2Item, :deletedItemId)
|
|
237
|
+
raise(PlanMyStuff::APIError, "Failed to delete project item #{item_id}") if deleted_id.nil?
|
|
238
|
+
|
|
239
|
+
deleted_id
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Assigns users to a project item.
|
|
243
|
+
# Issues/PRs use REST via Issue.update!, drafts use GraphQL.
|
|
244
|
+
#
|
|
245
|
+
# @param number [Integer, nil] issue number (nil for drafts)
|
|
246
|
+
# @param content_node_id [String] node ID of the underlying content
|
|
247
|
+
# @param assignees [Array<String>] GitHub usernames
|
|
248
|
+
# @param draft [Boolean] whether the item is a draft issue
|
|
249
|
+
# @param repo [Symbol, String, nil] repo key (for issues)
|
|
250
|
+
#
|
|
251
|
+
# @return [void]
|
|
252
|
+
#
|
|
253
|
+
def assign(number:, content_node_id:, assignees:, draft: false, repo: nil)
|
|
254
|
+
if draft
|
|
255
|
+
client = PlanMyStuff.client
|
|
256
|
+
user_ids = assignees.map do |assignee|
|
|
257
|
+
user_data = client.graphql(
|
|
258
|
+
PlanMyStuff::GraphQL::Queries::USER_NODE_ID,
|
|
259
|
+
variables: { login: assignee },
|
|
260
|
+
)
|
|
261
|
+
user_id = user_data.dig(:user, :id)
|
|
262
|
+
|
|
263
|
+
raise(APIError, "GitHub user not found: #{assignee}") if user_id.nil?
|
|
264
|
+
|
|
265
|
+
user_id
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
client.graphql(
|
|
269
|
+
PlanMyStuff::GraphQL::Queries::ASSIGN_DRAFT,
|
|
270
|
+
variables: { draftIssueId: content_node_id, assigneeIds: user_ids },
|
|
271
|
+
)
|
|
272
|
+
else
|
|
273
|
+
Issue.update!(number: number, repo: repo, assignees: assignees)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private
|
|
278
|
+
|
|
279
|
+
# Adds a GitHub issue to a project board.
|
|
280
|
+
#
|
|
281
|
+
# @param issue [PlanMyStuff::Issue]
|
|
282
|
+
# @param project_number [Integer, nil]
|
|
283
|
+
#
|
|
284
|
+
# @return [PlanMyStuff::BaseProjectItem]
|
|
285
|
+
#
|
|
286
|
+
def add_item(issue:, project_number:)
|
|
287
|
+
project_number = resolve_default_project_number(project_number)
|
|
288
|
+
client = PlanMyStuff.client
|
|
289
|
+
org = PlanMyStuff.configuration.organization
|
|
290
|
+
|
|
291
|
+
github_issue = client.rest(:issue, issue.repo, issue.number)
|
|
292
|
+
node_id = github_issue.respond_to?(:node_id) ? github_issue.node_id : github_issue[:node_id]
|
|
293
|
+
|
|
294
|
+
project_id = resolve_project_id(org, project_number)
|
|
295
|
+
|
|
296
|
+
data = client.graphql(
|
|
297
|
+
PlanMyStuff::GraphQL::Queries::ADD_ITEM,
|
|
298
|
+
variables: { projectId: project_id, contentId: node_id },
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
item_data = data.dig(:addProjectV2ItemById, :item) || {}
|
|
302
|
+
project = BaseProject.find(project_number)
|
|
303
|
+
|
|
304
|
+
item = build(
|
|
305
|
+
{
|
|
306
|
+
id: item_data[:id],
|
|
307
|
+
content_node_id: node_id,
|
|
308
|
+
title: issue.title,
|
|
309
|
+
number: issue.number,
|
|
310
|
+
url: nil,
|
|
311
|
+
state: issue.state,
|
|
312
|
+
status: nil,
|
|
313
|
+
field_values: {},
|
|
314
|
+
github_response: item_data,
|
|
315
|
+
},
|
|
316
|
+
project: project,
|
|
317
|
+
)
|
|
318
|
+
item.issue = issue
|
|
319
|
+
item
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Adds a draft issue to a project board.
|
|
323
|
+
#
|
|
324
|
+
# @param title [String]
|
|
325
|
+
# @param body [String, nil]
|
|
326
|
+
# @param project_number [Integer, nil]
|
|
327
|
+
#
|
|
328
|
+
# @return [PlanMyStuff::BaseProjectItem]
|
|
329
|
+
#
|
|
330
|
+
def add_draft_item(title:, body:, project_number:)
|
|
331
|
+
project_number = resolve_default_project_number(project_number)
|
|
332
|
+
org = PlanMyStuff.configuration.organization
|
|
333
|
+
project_id = resolve_project_id(org, project_number)
|
|
334
|
+
|
|
335
|
+
variables = { projectId: project_id, title: title }
|
|
336
|
+
variables[:body] = body if body.present?
|
|
337
|
+
|
|
338
|
+
data = PlanMyStuff.client.graphql(
|
|
339
|
+
PlanMyStuff::GraphQL::Queries::ADD_DRAFT_ITEM,
|
|
340
|
+
variables: variables,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
item_data = data.dig(:addProjectV2DraftIssue, :projectItem) || {}
|
|
344
|
+
project = BaseProject.find(project_number)
|
|
345
|
+
|
|
346
|
+
build(
|
|
347
|
+
{
|
|
348
|
+
id: item_data[:id],
|
|
349
|
+
content_node_id: item_data.dig(:content, :id),
|
|
350
|
+
type: 'DRAFT_ISSUE',
|
|
351
|
+
title: title,
|
|
352
|
+
body: body,
|
|
353
|
+
number: nil,
|
|
354
|
+
url: nil,
|
|
355
|
+
state: nil,
|
|
356
|
+
status: nil,
|
|
357
|
+
field_values: {},
|
|
358
|
+
github_response: item_data,
|
|
359
|
+
},
|
|
360
|
+
project: project,
|
|
361
|
+
)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# @see PlanMyStuff::Project.resolve_default_project_number
|
|
365
|
+
def resolve_default_project_number(project_number)
|
|
366
|
+
PlanMyStuff::Project.resolve_default_project_number(project_number)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Finds a text field by name on a project. Raises ArgumentError if
|
|
370
|
+
# the field is not found or is a single-select field.
|
|
371
|
+
#
|
|
372
|
+
# @param project [PlanMyStuff::BaseProject]
|
|
373
|
+
# @param field_name [String]
|
|
374
|
+
#
|
|
375
|
+
# @return [Hash]
|
|
376
|
+
#
|
|
377
|
+
def resolve_text_field(project, field_name)
|
|
378
|
+
field = project.fields.find { |f| f[:name] == field_name && f[:options].nil? }
|
|
379
|
+
|
|
380
|
+
unless field
|
|
381
|
+
available = project.fields.select { |f| f[:options].nil? }.pluck(:name).join(', ')
|
|
382
|
+
raise(ArgumentError, "Unknown text field '#{field_name}'. Available: #{available}")
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
field
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# @param project [PlanMyStuff::BaseProject]
|
|
389
|
+
# @param field_name [String]
|
|
390
|
+
#
|
|
391
|
+
# @return [Hash]
|
|
392
|
+
#
|
|
393
|
+
def resolve_single_select_field(project, field_name)
|
|
394
|
+
field = project.fields.find { |f| f[:name] == field_name && f[:options].present? }
|
|
395
|
+
|
|
396
|
+
unless field
|
|
397
|
+
available = project.fields.select { |f| f[:options].present? }.pluck(:name).join(', ')
|
|
398
|
+
raise(ArgumentError, "Unknown single-select field '#{field_name}'. Available: #{available}")
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
field
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Resolves a status name to its option ID.
|
|
405
|
+
#
|
|
406
|
+
# @param status_field [Hash]
|
|
407
|
+
# @param status_name [String]
|
|
408
|
+
#
|
|
409
|
+
# @return [String]
|
|
410
|
+
#
|
|
411
|
+
def resolve_status_option_id(status_field, status_name)
|
|
412
|
+
option = status_field[:options].find { |o| o[:name] == status_name }
|
|
413
|
+
|
|
414
|
+
unless option
|
|
415
|
+
available = status_field[:options].pluck(:name).join(', ')
|
|
416
|
+
raise(ArgumentError, "Unknown status '#{status_name}'. Available: #{available}")
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
option[:id]
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Resolves a project number to its node ID.
|
|
423
|
+
#
|
|
424
|
+
# @param org [String]
|
|
425
|
+
# @param project_number [Integer]
|
|
426
|
+
#
|
|
427
|
+
# @return [String]
|
|
428
|
+
#
|
|
429
|
+
def resolve_project_id(org, project_number)
|
|
430
|
+
data = PlanMyStuff.client.graphql(
|
|
431
|
+
PlanMyStuff::GraphQL::Queries::PROJECT_ID,
|
|
432
|
+
variables: { org: org, number: project_number },
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
data.dig(:organization, :projectV2, :id)
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Moves this item to a new status column on its parent project.
|
|
440
|
+
#
|
|
441
|
+
# @param status [String] status name (e.g. "In Progress", "Done")
|
|
442
|
+
#
|
|
443
|
+
# @return [Hash] mutation result
|
|
444
|
+
#
|
|
445
|
+
def move_to!(status, user: nil)
|
|
446
|
+
previous_status = self.status
|
|
447
|
+
result = self.class.move_item(
|
|
448
|
+
project_number: project.number,
|
|
449
|
+
item_id: id,
|
|
450
|
+
status: status,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
PlanMyStuff::Notifications.instrument(
|
|
454
|
+
'project_item.status_changed',
|
|
455
|
+
self,
|
|
456
|
+
user: user,
|
|
457
|
+
status: status,
|
|
458
|
+
previous_status: previous_status,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
result
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Updates a text custom field on this item.
|
|
465
|
+
#
|
|
466
|
+
# @param field_name [String] text field name (e.g. "Deployment ID")
|
|
467
|
+
# @param value [String] text value to set
|
|
468
|
+
#
|
|
469
|
+
# @return [Hash] mutation result
|
|
470
|
+
#
|
|
471
|
+
def update_field!(field_name, value)
|
|
472
|
+
self.class.update_field!(
|
|
473
|
+
project_number: project.number,
|
|
474
|
+
item_id: id,
|
|
475
|
+
field_name: field_name,
|
|
476
|
+
value: value,
|
|
477
|
+
)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Deletes this item from its parent project. Marks the in-memory
|
|
481
|
+
# instance as destroyed so +destroyed?+ returns true and +persisted?+
|
|
482
|
+
# returns false.
|
|
483
|
+
#
|
|
484
|
+
# No-op if the instance is already destroyed.
|
|
485
|
+
#
|
|
486
|
+
# @return [String, nil] the deleted item ID, or nil if already destroyed
|
|
487
|
+
#
|
|
488
|
+
# @raise [PlanMyStuff::APIError] if the GraphQL mutation fails
|
|
489
|
+
#
|
|
490
|
+
def destroy!(user: nil)
|
|
491
|
+
return if destroyed?
|
|
492
|
+
|
|
493
|
+
deleted_id = self.class.delete_item(
|
|
494
|
+
project_number: project.number,
|
|
495
|
+
item_id: id,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
PlanMyStuff::Notifications.instrument('project_item.removed', self, user: user)
|
|
499
|
+
destroyed!
|
|
500
|
+
deleted_id
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Assigns users to this item on its parent project.
|
|
504
|
+
#
|
|
505
|
+
# @param assignees [String, Array<String>] GitHub username(s)
|
|
506
|
+
#
|
|
507
|
+
# @return [void]
|
|
508
|
+
#
|
|
509
|
+
def assign!(assignees, user: nil)
|
|
510
|
+
assignee_list = Array.wrap(assignees)
|
|
511
|
+
|
|
512
|
+
self.class.assign(
|
|
513
|
+
number: number,
|
|
514
|
+
content_node_id: content_node_id,
|
|
515
|
+
assignees: assignee_list,
|
|
516
|
+
draft: draft?,
|
|
517
|
+
repo: repo,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
PlanMyStuff::Notifications.instrument(
|
|
521
|
+
'project_item.assigned',
|
|
522
|
+
self,
|
|
523
|
+
user: user,
|
|
524
|
+
assignees: assignee_list,
|
|
525
|
+
)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Serializes the project item to a JSON-safe hash, excluding the back-reference
|
|
529
|
+
# to the parent project to prevent recursive serialization cycles.
|
|
530
|
+
#
|
|
531
|
+
# @return [Hash]
|
|
532
|
+
#
|
|
533
|
+
def as_json(options = {})
|
|
534
|
+
merged_except = Array.wrap(options[:except]) + ['project']
|
|
535
|
+
merged_methods = Array.wrap(options[:methods]) + [:draft?]
|
|
536
|
+
super(options.merge(except: merged_except, methods: merged_methods)).merge(
|
|
537
|
+
'project_id' => project&.id,
|
|
538
|
+
'project_number' => project&.number,
|
|
539
|
+
)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# @return [Boolean]
|
|
543
|
+
def draft?
|
|
544
|
+
type == 'DRAFT_ISSUE'
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# @return [void]
|
|
548
|
+
def issue=(val)
|
|
549
|
+
@issue_assigned = true
|
|
550
|
+
super
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# @return [PlanMyStuff::Issue, nil]
|
|
554
|
+
def issue
|
|
555
|
+
return super if @issue_assigned
|
|
556
|
+
return if draft?
|
|
557
|
+
|
|
558
|
+
self.issue = PMS::Issue.find(number, repo: repo)
|
|
559
|
+
super
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Shared base for project-style metadata (regular and testing). Holds the
|
|
5
|
+
# +kind+ dispatch field and serialization hook. Not instantiated directly;
|
|
6
|
+
# use +ProjectMetadata+ or +TestingProjectMetadata+.
|
|
7
|
+
class BaseProjectMetadata < BaseMetadata
|
|
8
|
+
# @return [String, nil] dispatch key: "project" or "testing"
|
|
9
|
+
attr_accessor :kind
|
|
10
|
+
|
|
11
|
+
# @return [Hash]
|
|
12
|
+
def to_h
|
|
13
|
+
super.merge(kind: kind)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|