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