plan_my_stuff 0.1.1 → 0.3.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 +27 -0
- data/app/controllers/plan_my_stuff/application_controller.rb +4 -1
- data/app/controllers/plan_my_stuff/comments_controller.rb +24 -6
- data/app/controllers/plan_my_stuff/issues_controller.rb +23 -17
- data/app/controllers/plan_my_stuff/labels_controller.rb +5 -5
- data/app/controllers/plan_my_stuff/project_items_controller.rb +6 -0
- data/app/controllers/plan_my_stuff/projects_controller.rb +54 -0
- data/app/views/plan_my_stuff/issues/index.html.erb +4 -4
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +10 -6
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/show.html.erb +4 -4
- data/app/views/plan_my_stuff/projects/edit.html.erb +7 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +2 -0
- 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 +6 -4
- data/config/routes.rb +1 -1
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +10 -1
- data/lib/plan_my_stuff/application_record.rb +37 -1
- data/lib/plan_my_stuff/base_metadata.rb +23 -15
- data/lib/plan_my_stuff/client.rb +2 -22
- data/lib/plan_my_stuff/comment.rb +22 -8
- data/lib/plan_my_stuff/comment_metadata.rb +8 -2
- data/lib/plan_my_stuff/configuration.rb +82 -1
- data/lib/plan_my_stuff/custom_fields.rb +70 -0
- data/lib/plan_my_stuff/issue.rb +23 -19
- data/lib/plan_my_stuff/issue_metadata.rb +8 -2
- data/lib/plan_my_stuff/markdown.rb +1 -1
- data/lib/plan_my_stuff/project.rb +280 -19
- data/lib/plan_my_stuff/project_item.rb +19 -11
- data/lib/plan_my_stuff/project_metadata.rb +41 -0
- data/lib/plan_my_stuff/repo.rb +107 -0
- data/lib/plan_my_stuff/test_helpers.rb +10 -2
- data/lib/plan_my_stuff/version.rb +2 -2
- data/lib/plan_my_stuff.rb +2 -0
- metadata +8 -2
|
@@ -18,11 +18,21 @@ module PlanMyStuff
|
|
|
18
18
|
attr_reader :number
|
|
19
19
|
# @return [Boolean] whether the project is closed
|
|
20
20
|
attr_reader :closed
|
|
21
|
+
# @return [PlanMyStuff::ProjectMetadata] parsed metadata (empty when no PMS metadata present)
|
|
22
|
+
attr_reader :metadata
|
|
23
|
+
# @return [String] full readme as stored on GitHub
|
|
24
|
+
attr_reader :raw_readme
|
|
25
|
+
# @return [String, nil] project short description (from shortDescription)
|
|
26
|
+
attr_reader :description
|
|
27
|
+
# @return [Time, nil] GitHub's updatedAt timestamp
|
|
28
|
+
attr_reader :updated_at
|
|
21
29
|
|
|
22
30
|
# @return [String] project title
|
|
23
31
|
attr_accessor :title
|
|
24
32
|
# @return [String] project URL
|
|
25
33
|
attr_accessor :url
|
|
34
|
+
# @return [String, nil] user-visible readme content (without metadata comment)
|
|
35
|
+
attr_accessor :readme
|
|
26
36
|
# @return [Array<Hash>] status options ({id:, name:})
|
|
27
37
|
attr_accessor :statuses
|
|
28
38
|
# @return [Array<Hash>] all field definitions
|
|
@@ -35,25 +45,97 @@ module PlanMyStuff
|
|
|
35
45
|
attr_accessor :has_next_page
|
|
36
46
|
|
|
37
47
|
class << self
|
|
38
|
-
# Creates a new project in the configured organization.
|
|
48
|
+
# Creates a new project in the configured organization with PMS metadata.
|
|
39
49
|
#
|
|
40
50
|
# @param title [String]
|
|
51
|
+
# @param user [Object, Integer, nil] user object or user_id
|
|
52
|
+
# @param visibility [String] "public" or "internal"
|
|
53
|
+
# @param custom_fields [Hash] app-defined field values
|
|
54
|
+
# @param readme [String] user-visible readme content
|
|
55
|
+
# @param description [String, nil] project short description
|
|
41
56
|
#
|
|
42
|
-
# @return [
|
|
57
|
+
# @return [PlanMyStuff::Project]
|
|
43
58
|
#
|
|
44
|
-
def create(title:)
|
|
45
|
-
|
|
59
|
+
def create!(title:, user: nil, visibility: 'internal', custom_fields: {}, readme: '', description: nil)
|
|
60
|
+
org = PlanMyStuff.configuration.organization
|
|
61
|
+
|
|
62
|
+
project_metadata = PlanMyStuff::ProjectMetadata.build(
|
|
63
|
+
user: user,
|
|
64
|
+
visibility: visibility,
|
|
65
|
+
custom_fields: custom_fields,
|
|
66
|
+
)
|
|
67
|
+
project_metadata.validate_custom_fields!
|
|
68
|
+
|
|
69
|
+
org_id = resolve_org_id(org)
|
|
70
|
+
data = PlanMyStuff.client.graphql(
|
|
71
|
+
create_mutation,
|
|
72
|
+
variables: { input: { ownerId: org_id, title: title } },
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
new_project = data.dig(:createProjectV2, :projectV2) || {}
|
|
76
|
+
project_id = new_project[:id]
|
|
77
|
+
project_number = new_project[:number]
|
|
78
|
+
|
|
79
|
+
serialized_readme = PlanMyStuff::MetadataParser.serialize(project_metadata.to_h, readme)
|
|
80
|
+
update_input = { projectId: project_id, readme: serialized_readme }
|
|
81
|
+
update_input[:shortDescription] = description if description.present?
|
|
82
|
+
|
|
83
|
+
PlanMyStuff.client.graphql(
|
|
84
|
+
update_mutation,
|
|
85
|
+
variables: { input: update_input },
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
find(project_number)
|
|
46
89
|
end
|
|
47
90
|
|
|
48
91
|
# Updates an existing project.
|
|
49
92
|
#
|
|
50
93
|
# @param project_number [Integer]
|
|
51
94
|
# @param title [String, nil]
|
|
95
|
+
# @param readme [String, nil] user-visible readme content (metadata preserved)
|
|
96
|
+
# @param description [String, nil] project short description
|
|
97
|
+
# @param metadata [Hash, nil] custom fields to merge into existing metadata
|
|
52
98
|
#
|
|
53
|
-
# @return [
|
|
99
|
+
# @return [PlanMyStuff::Project]
|
|
54
100
|
#
|
|
55
|
-
def update(project_number:, title: nil)
|
|
56
|
-
|
|
101
|
+
def update!(project_number:, title: nil, readme: nil, description: nil, metadata: nil)
|
|
102
|
+
org = PlanMyStuff.configuration.organization
|
|
103
|
+
project_id = resolve_project_id(org, project_number)
|
|
104
|
+
|
|
105
|
+
update_input = { projectId: project_id }
|
|
106
|
+
update_input[:title] = title unless title.nil?
|
|
107
|
+
update_input[:shortDescription] = description unless description.nil?
|
|
108
|
+
|
|
109
|
+
if metadata.present? || !readme.nil?
|
|
110
|
+
current = find(project_number)
|
|
111
|
+
parsed = PlanMyStuff::MetadataParser.parse(current.raw_readme)
|
|
112
|
+
existing_metadata = parsed[:metadata]
|
|
113
|
+
|
|
114
|
+
if metadata.present?
|
|
115
|
+
# Seed with fresh metadata when project has no existing PMS metadata
|
|
116
|
+
if existing_metadata[:schema_version].blank?
|
|
117
|
+
existing_metadata = PlanMyStuff::ProjectMetadata.build(user: nil).to_h
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
merged_custom_fields = (existing_metadata[:custom_fields] || {}).merge(metadata[:custom_fields] || {})
|
|
121
|
+
existing_metadata = existing_metadata.merge(metadata)
|
|
122
|
+
existing_metadata[:custom_fields] = merged_custom_fields
|
|
123
|
+
PlanMyStuff::CustomFields.new(
|
|
124
|
+
PlanMyStuff.configuration.custom_fields_for(:project),
|
|
125
|
+
merged_custom_fields,
|
|
126
|
+
).validate!
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
body = readme.nil? ? parsed[:body] : readme
|
|
130
|
+
update_input[:readme] = PlanMyStuff::MetadataParser.serialize(existing_metadata, body)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
PlanMyStuff.client.graphql(
|
|
134
|
+
update_mutation,
|
|
135
|
+
variables: { input: update_input },
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
find(project_number)
|
|
57
139
|
end
|
|
58
140
|
|
|
59
141
|
# Lists all projects in the configured organization.
|
|
@@ -90,20 +172,20 @@ module PlanMyStuff
|
|
|
90
172
|
end
|
|
91
173
|
end
|
|
92
174
|
|
|
93
|
-
|
|
175
|
+
# Resolves a project number, falling back to config.default_project_number.
|
|
176
|
+
#
|
|
177
|
+
# @param project_number [Integer, nil]
|
|
178
|
+
#
|
|
179
|
+
# @return [Integer]
|
|
180
|
+
#
|
|
181
|
+
def resolve_default_project_number(project_number)
|
|
182
|
+
return project_number if project_number.present?
|
|
94
183
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
# @return [Integer]
|
|
100
|
-
#
|
|
101
|
-
def resolve_default_project_number(project_number)
|
|
102
|
-
return project_number if project_number.present?
|
|
184
|
+
PlanMyStuff.configuration.default_project_number ||
|
|
185
|
+
raise(ArgumentError, 'project_number is required when config.default_project_number is not set')
|
|
186
|
+
end
|
|
103
187
|
|
|
104
|
-
|
|
105
|
-
raise(ArgumentError, 'project_number is required when config.default_project_number is not set')
|
|
106
|
-
end
|
|
188
|
+
private
|
|
107
189
|
|
|
108
190
|
# @return [String]
|
|
109
191
|
def list_query
|
|
@@ -115,8 +197,10 @@ module PlanMyStuff
|
|
|
115
197
|
id
|
|
116
198
|
number
|
|
117
199
|
title
|
|
200
|
+
shortDescription
|
|
118
201
|
url
|
|
119
202
|
closed
|
|
203
|
+
updatedAt
|
|
120
204
|
}
|
|
121
205
|
}
|
|
122
206
|
}
|
|
@@ -133,8 +217,11 @@ module PlanMyStuff
|
|
|
133
217
|
id
|
|
134
218
|
number
|
|
135
219
|
title
|
|
220
|
+
shortDescription
|
|
221
|
+
readme
|
|
136
222
|
url
|
|
137
223
|
closed
|
|
224
|
+
updatedAt
|
|
138
225
|
fields(first: 50) {
|
|
139
226
|
nodes {
|
|
140
227
|
... on ProjectV2SingleSelectField {
|
|
@@ -170,6 +257,7 @@ module PlanMyStuff
|
|
|
170
257
|
number
|
|
171
258
|
url
|
|
172
259
|
state
|
|
260
|
+
repository { nameWithOwner }
|
|
173
261
|
}
|
|
174
262
|
... on PullRequest {
|
|
175
263
|
id
|
|
@@ -177,6 +265,7 @@ module PlanMyStuff
|
|
|
177
265
|
number
|
|
178
266
|
url
|
|
179
267
|
state
|
|
268
|
+
repository { nameWithOwner }
|
|
180
269
|
}
|
|
181
270
|
... on DraftIssue {
|
|
182
271
|
id
|
|
@@ -342,6 +431,7 @@ module PlanMyStuff
|
|
|
342
431
|
def parse_project_item(item)
|
|
343
432
|
content = item[:content] || {}
|
|
344
433
|
field_values = item.dig(:fieldValues, :nodes) || []
|
|
434
|
+
repo_name = content.dig(:repository, :nameWithOwner)
|
|
345
435
|
|
|
346
436
|
{
|
|
347
437
|
id: item[:id],
|
|
@@ -351,6 +441,7 @@ module PlanMyStuff
|
|
|
351
441
|
number: content[:number],
|
|
352
442
|
url: content[:url],
|
|
353
443
|
state: content[:state],
|
|
444
|
+
repo: repo_name.present? ? PlanMyStuff::Repo.resolve(repo_name) : nil,
|
|
354
445
|
status: extract_item_status(field_values),
|
|
355
446
|
field_values: parse_field_values(field_values),
|
|
356
447
|
}
|
|
@@ -404,6 +495,60 @@ module PlanMyStuff
|
|
|
404
495
|
|
|
405
496
|
data.dig(:organization, :projectV2, :id)
|
|
406
497
|
end
|
|
498
|
+
|
|
499
|
+
# Resolves an organization login to its node ID.
|
|
500
|
+
#
|
|
501
|
+
# @param org [String]
|
|
502
|
+
#
|
|
503
|
+
# @return [String]
|
|
504
|
+
#
|
|
505
|
+
def resolve_org_id(org)
|
|
506
|
+
data = PlanMyStuff.client.graphql(
|
|
507
|
+
org_id_query,
|
|
508
|
+
variables: { org: org },
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
data.dig(:organization, :id)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# @return [String]
|
|
515
|
+
def org_id_query
|
|
516
|
+
<<~GRAPHQL
|
|
517
|
+
query($org: String!) {
|
|
518
|
+
organization(login: $org) {
|
|
519
|
+
id
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
GRAPHQL
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# @return [String]
|
|
526
|
+
def create_mutation
|
|
527
|
+
<<~GRAPHQL
|
|
528
|
+
mutation($input: CreateProjectV2Input!) {
|
|
529
|
+
createProjectV2(input: $input) {
|
|
530
|
+
projectV2 {
|
|
531
|
+
id
|
|
532
|
+
number
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
GRAPHQL
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# @return [String]
|
|
540
|
+
def update_mutation
|
|
541
|
+
<<~GRAPHQL
|
|
542
|
+
mutation($input: UpdateProjectV2Input!) {
|
|
543
|
+
updateProjectV2(input: $input) {
|
|
544
|
+
projectV2 {
|
|
545
|
+
id
|
|
546
|
+
number
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
GRAPHQL
|
|
551
|
+
end
|
|
407
552
|
end
|
|
408
553
|
|
|
409
554
|
# @see super
|
|
@@ -411,6 +556,11 @@ module PlanMyStuff
|
|
|
411
556
|
@id = attrs.delete(:id)
|
|
412
557
|
@number = attrs.delete(:number)
|
|
413
558
|
@closed = attrs.delete(:closed)
|
|
559
|
+
@metadata = PlanMyStuff::ProjectMetadata.new
|
|
560
|
+
@raw_readme = nil
|
|
561
|
+
@readme = nil
|
|
562
|
+
@description = nil
|
|
563
|
+
@updated_at = nil
|
|
414
564
|
super
|
|
415
565
|
@statuses ||= []
|
|
416
566
|
@fields ||= []
|
|
@@ -431,6 +581,58 @@ module PlanMyStuff
|
|
|
431
581
|
field
|
|
432
582
|
end
|
|
433
583
|
|
|
584
|
+
# @return [Boolean]
|
|
585
|
+
def pms_project?
|
|
586
|
+
metadata.schema_version.present?
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Persists the project. Creates if new, updates if persisted.
|
|
590
|
+
#
|
|
591
|
+
# @raise [PlanMyStuff::StaleObjectError] on update if stale
|
|
592
|
+
#
|
|
593
|
+
# @return [self]
|
|
594
|
+
#
|
|
595
|
+
def save!
|
|
596
|
+
if new_record?
|
|
597
|
+
created = self.class.create!(title: title, readme: readme || '')
|
|
598
|
+
hydrate_from_project(created)
|
|
599
|
+
else
|
|
600
|
+
update!(title: title, readme: readme)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
self
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# Updates this project on GitHub. Raises StaleObjectError if the remote
|
|
607
|
+
# has been modified since this instance was loaded.
|
|
608
|
+
#
|
|
609
|
+
# @param attrs [Hash] attributes to update (title:, readme:, description:, metadata:)
|
|
610
|
+
#
|
|
611
|
+
# @raise [PlanMyStuff::StaleObjectError] if remote updated_at differs from local
|
|
612
|
+
#
|
|
613
|
+
# @return [self]
|
|
614
|
+
#
|
|
615
|
+
def update!(**attrs)
|
|
616
|
+
raise_if_stale!
|
|
617
|
+
|
|
618
|
+
self.class.update!(
|
|
619
|
+
project_number: number,
|
|
620
|
+
**attrs,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
reload
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Re-fetches this project from GitHub and updates all local attributes.
|
|
627
|
+
#
|
|
628
|
+
# @return [self]
|
|
629
|
+
#
|
|
630
|
+
def reload
|
|
631
|
+
fresh = self.class.find(number)
|
|
632
|
+
hydrate_from_project(fresh)
|
|
633
|
+
self
|
|
634
|
+
end
|
|
635
|
+
|
|
434
636
|
private
|
|
435
637
|
|
|
436
638
|
# Populates this instance from a list query node (summary only).
|
|
@@ -443,8 +645,10 @@ module PlanMyStuff
|
|
|
443
645
|
@id = node[:id]
|
|
444
646
|
@number = node[:number]
|
|
445
647
|
@title = node[:title]
|
|
648
|
+
@description = node[:shortDescription]
|
|
446
649
|
@url = node[:url]
|
|
447
650
|
@closed = node[:closed]
|
|
651
|
+
@updated_at = parse_github_time(node[:updatedAt])
|
|
448
652
|
@persisted = true
|
|
449
653
|
end
|
|
450
654
|
|
|
@@ -461,8 +665,15 @@ module PlanMyStuff
|
|
|
461
665
|
@id = graphql_project[:id]
|
|
462
666
|
@number = graphql_project[:number]
|
|
463
667
|
@title = graphql_project[:title]
|
|
668
|
+
@description = graphql_project[:shortDescription]
|
|
464
669
|
@url = graphql_project[:url]
|
|
465
670
|
@closed = graphql_project[:closed]
|
|
671
|
+
@updated_at = parse_github_time(graphql_project[:updatedAt])
|
|
672
|
+
|
|
673
|
+
@raw_readme = graphql_project[:readme] || ''
|
|
674
|
+
parsed = PlanMyStuff::MetadataParser.parse(@raw_readme)
|
|
675
|
+
@metadata = PlanMyStuff::ProjectMetadata.from_hash(parsed[:metadata])
|
|
676
|
+
@readme = parsed[:body]
|
|
466
677
|
|
|
467
678
|
fields_nodes = graphql_project.dig(:fields, :nodes) || []
|
|
468
679
|
@statuses = extract_statuses(fields_nodes)
|
|
@@ -473,6 +684,56 @@ module PlanMyStuff
|
|
|
473
684
|
@persisted = true
|
|
474
685
|
end
|
|
475
686
|
|
|
687
|
+
# Copies attributes from another Project instance into self.
|
|
688
|
+
#
|
|
689
|
+
# @param other [PlanMyStuff::Project]
|
|
690
|
+
#
|
|
691
|
+
# @return [void]
|
|
692
|
+
#
|
|
693
|
+
def hydrate_from_project(other)
|
|
694
|
+
@id = other.id
|
|
695
|
+
@number = other.number
|
|
696
|
+
@title = other.title
|
|
697
|
+
@description = other.description
|
|
698
|
+
@url = other.url
|
|
699
|
+
@closed = other.closed
|
|
700
|
+
@updated_at = other.updated_at
|
|
701
|
+
@raw_readme = other.raw_readme
|
|
702
|
+
@readme = other.readme
|
|
703
|
+
@metadata = other.metadata
|
|
704
|
+
@statuses = other.statuses
|
|
705
|
+
@fields = other.fields
|
|
706
|
+
@items = other.items
|
|
707
|
+
@next_cursor = other.next_cursor
|
|
708
|
+
@has_next_page = other.has_next_page
|
|
709
|
+
@persisted = true
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Raises StaleObjectError if the remote project has been modified
|
|
713
|
+
# since this instance was loaded.
|
|
714
|
+
#
|
|
715
|
+
# @raise [PlanMyStuff::StaleObjectError]
|
|
716
|
+
#
|
|
717
|
+
# @return [void]
|
|
718
|
+
#
|
|
719
|
+
def raise_if_stale!
|
|
720
|
+
return if new_record?
|
|
721
|
+
return if updated_at.nil?
|
|
722
|
+
|
|
723
|
+
remote = self.class.find(number)
|
|
724
|
+
remote_time = remote.updated_at
|
|
725
|
+
local_time = updated_at
|
|
726
|
+
|
|
727
|
+
return if remote_time.nil?
|
|
728
|
+
return if local_time && remote_time.to_i == local_time.to_i
|
|
729
|
+
|
|
730
|
+
raise(PlanMyStuff::StaleObjectError.new(
|
|
731
|
+
"Project ##{number} has been modified remotely",
|
|
732
|
+
local_updated_at: local_time,
|
|
733
|
+
remote_updated_at: remote_time,
|
|
734
|
+
))
|
|
735
|
+
end
|
|
736
|
+
|
|
476
737
|
# Extracts status options from the "Status" single-select field.
|
|
477
738
|
#
|
|
478
739
|
# @param fields_nodes [Array<Hash>]
|
|
@@ -29,6 +29,8 @@ module PlanMyStuff
|
|
|
29
29
|
attr_accessor :number
|
|
30
30
|
# @return [String, nil]
|
|
31
31
|
attr_accessor :url
|
|
32
|
+
# @return [PlanMyStuff::Repo, nil]
|
|
33
|
+
attr_accessor :repo
|
|
32
34
|
# @return [String, nil]
|
|
33
35
|
attr_accessor :state
|
|
34
36
|
# @return [String, nil]
|
|
@@ -38,7 +40,7 @@ module PlanMyStuff
|
|
|
38
40
|
# @return [PlanMyStuff::Project, nil]
|
|
39
41
|
attr_accessor :project
|
|
40
42
|
# @return [PlanMyStuff::Issue, nil] linked issue (nil for draft items)
|
|
41
|
-
|
|
43
|
+
attr_writer :issue
|
|
42
44
|
|
|
43
45
|
class << self
|
|
44
46
|
# Builds a persisted ProjectItem from parsed item data.
|
|
@@ -56,6 +58,7 @@ module PlanMyStuff
|
|
|
56
58
|
title: item_hash[:title],
|
|
57
59
|
number: item_hash[:number],
|
|
58
60
|
url: item_hash[:url],
|
|
61
|
+
repo: item_hash[:repo],
|
|
59
62
|
state: item_hash[:state],
|
|
60
63
|
status: item_hash[:status],
|
|
61
64
|
field_values: item_hash[:field_values] || {},
|
|
@@ -170,6 +173,7 @@ module PlanMyStuff
|
|
|
170
173
|
item = build(
|
|
171
174
|
{
|
|
172
175
|
id: item_data[:id],
|
|
176
|
+
content_node_id: node_id,
|
|
173
177
|
title: issue.title,
|
|
174
178
|
number: issue.number,
|
|
175
179
|
url: nil,
|
|
@@ -210,6 +214,8 @@ module PlanMyStuff
|
|
|
210
214
|
build(
|
|
211
215
|
{
|
|
212
216
|
id: item_data[:id],
|
|
217
|
+
content_node_id: item_data.dig(:content, :id),
|
|
218
|
+
type: 'DRAFT_ISSUE',
|
|
213
219
|
title: title,
|
|
214
220
|
number: nil,
|
|
215
221
|
url: nil,
|
|
@@ -221,17 +227,9 @@ module PlanMyStuff
|
|
|
221
227
|
)
|
|
222
228
|
end
|
|
223
229
|
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
# @param project_number [Integer, nil]
|
|
227
|
-
#
|
|
228
|
-
# @return [Integer]
|
|
229
|
-
#
|
|
230
|
+
# @see PlanMyStuff::Project.resolve_default_project_number
|
|
230
231
|
def resolve_default_project_number(project_number)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
PlanMyStuff.configuration.default_project_number ||
|
|
234
|
-
raise(ArgumentError, 'project_number is required when config.default_project_number is not set')
|
|
232
|
+
PlanMyStuff::Project.resolve_default_project_number(project_number)
|
|
235
233
|
end
|
|
236
234
|
|
|
237
235
|
# @return [String]
|
|
@@ -261,6 +259,7 @@ module PlanMyStuff
|
|
|
261
259
|
}) {
|
|
262
260
|
projectItem {
|
|
263
261
|
id
|
|
262
|
+
content { ... on DraftIssue { id } }
|
|
264
263
|
}
|
|
265
264
|
}
|
|
266
265
|
}
|
|
@@ -393,6 +392,7 @@ module PlanMyStuff
|
|
|
393
392
|
content_node_id: content_node_id,
|
|
394
393
|
assignees: Array.wrap(assignees),
|
|
395
394
|
draft: draft?,
|
|
395
|
+
repo: repo,
|
|
396
396
|
)
|
|
397
397
|
end
|
|
398
398
|
|
|
@@ -401,6 +401,14 @@ module PlanMyStuff
|
|
|
401
401
|
type == 'DRAFT_ISSUE'
|
|
402
402
|
end
|
|
403
403
|
|
|
404
|
+
# @return [PlanMyStuff::Issue, nil]
|
|
405
|
+
def issue
|
|
406
|
+
return @issue if defined?(@issue)
|
|
407
|
+
return if draft?
|
|
408
|
+
|
|
409
|
+
@issue = PMS::Issue.find(number, repo: repo)
|
|
410
|
+
end
|
|
411
|
+
|
|
404
412
|
private
|
|
405
413
|
|
|
406
414
|
# Marks this record as persisted.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
class ProjectMetadata < BaseMetadata
|
|
5
|
+
class << self
|
|
6
|
+
# Builds a ProjectMetadata from a parsed hash (e.g. from MetadataParser)
|
|
7
|
+
#
|
|
8
|
+
# @param hash [Hash]
|
|
9
|
+
#
|
|
10
|
+
# @return [PlanMyStuff::ProjectMetadata]
|
|
11
|
+
#
|
|
12
|
+
def from_hash(hash)
|
|
13
|
+
metadata = new
|
|
14
|
+
apply_common_from_hash(metadata, hash, PlanMyStuff.configuration.custom_fields_for(:project))
|
|
15
|
+
|
|
16
|
+
metadata
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Builds a new ProjectMetadata for project creation, auto-filling gem defaults
|
|
20
|
+
#
|
|
21
|
+
# @param user [Object, Integer] user object or user_id
|
|
22
|
+
# @param visibility [String] "public" or "internal"
|
|
23
|
+
# @param custom_fields [Hash] app-defined field values
|
|
24
|
+
#
|
|
25
|
+
# @return [PlanMyStuff::ProjectMetadata]
|
|
26
|
+
#
|
|
27
|
+
def build(user:, visibility: 'internal', custom_fields: {})
|
|
28
|
+
metadata = new
|
|
29
|
+
apply_common_build(
|
|
30
|
+
metadata,
|
|
31
|
+
user: user,
|
|
32
|
+
visibility: visibility,
|
|
33
|
+
custom_fields_data: custom_fields,
|
|
34
|
+
custom_fields_schema: PlanMyStuff.configuration.custom_fields_for(:project),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
metadata
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
class Repo
|
|
5
|
+
# @return [Symbol, nil] configured key (e.g. :my_repo)
|
|
6
|
+
attr_reader :key
|
|
7
|
+
|
|
8
|
+
# @return [String] repo name (e.g. "MyRepository")
|
|
9
|
+
attr_reader :name
|
|
10
|
+
|
|
11
|
+
# @return [String] organization name (e.g. "YourOrgName")
|
|
12
|
+
attr_reader :organization
|
|
13
|
+
|
|
14
|
+
delegate :split, to: :to_s
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Builds a Repo instance from a Symbol key, full name String, or nil (default).
|
|
18
|
+
#
|
|
19
|
+
# @param repo [Symbol, String, PlanMyStuff::Repo, nil]
|
|
20
|
+
#
|
|
21
|
+
# @return [PlanMyStuff::Repo]
|
|
22
|
+
#
|
|
23
|
+
def resolve(repo = nil)
|
|
24
|
+
return repo if repo.is_a?(PlanMyStuff::Repo)
|
|
25
|
+
|
|
26
|
+
repo ||= PlanMyStuff.configuration.default_repo
|
|
27
|
+
|
|
28
|
+
if repo.nil?
|
|
29
|
+
raise(
|
|
30
|
+
PlanMyStuff::ConfigurationError,
|
|
31
|
+
'No repo provided and config.default_repo is not set. ' \
|
|
32
|
+
'Either pass repo: explicitly or set config.default_repo in your initializer.',
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
case repo
|
|
37
|
+
when Symbol
|
|
38
|
+
full_name = PlanMyStuff.configuration.repos[repo]
|
|
39
|
+
raise(ArgumentError, "Unknown repo key: #{repo.inspect}") if full_name.nil?
|
|
40
|
+
|
|
41
|
+
from_full_name(full_name, key: repo)
|
|
42
|
+
when String
|
|
43
|
+
key = PlanMyStuff.configuration.repos.key(repo)
|
|
44
|
+
from_full_name(repo, key: key)
|
|
45
|
+
else
|
|
46
|
+
raise(ArgumentError, "Cannot resolve repo: #{repo.inspect}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# @param full_name [String] e.g. "YourOrgName/MyRepository"
|
|
53
|
+
# @param key [Symbol, nil]
|
|
54
|
+
#
|
|
55
|
+
# @return [PlanMyStuff::Repo]
|
|
56
|
+
#
|
|
57
|
+
def from_full_name(full_name, key: nil)
|
|
58
|
+
org, name = full_name.split('/', 2)
|
|
59
|
+
|
|
60
|
+
raise(ArgumentError, "Invalid repo full_name: #{full_name.inspect}") if name.nil?
|
|
61
|
+
|
|
62
|
+
new(name: name, organization: org, key: key)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param name [String]
|
|
67
|
+
# @param organization [String]
|
|
68
|
+
# @param key [Symbol, nil]
|
|
69
|
+
#
|
|
70
|
+
def initialize(name:, organization:, key: nil)
|
|
71
|
+
@key = key
|
|
72
|
+
@name = name
|
|
73
|
+
@organization = organization
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [String] full repo path (e.g. "YourOrgName/MyRepository")
|
|
77
|
+
def full_name
|
|
78
|
+
"#{organization}/#{name}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @see #full_name
|
|
82
|
+
alias to_s full_name
|
|
83
|
+
|
|
84
|
+
# Enables implicit string coercion so Repo instances behave as
|
|
85
|
+
# strings when passed to Octokit or compared with String#==.
|
|
86
|
+
#
|
|
87
|
+
# @see #full_name
|
|
88
|
+
alias to_str full_name
|
|
89
|
+
|
|
90
|
+
# Compares by full_name. Accepts another Repo or a String.
|
|
91
|
+
#
|
|
92
|
+
# @param other [PlanMyStuff::Repo, String, Object]
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
#
|
|
96
|
+
def ==(other)
|
|
97
|
+
case other
|
|
98
|
+
when PlanMyStuff::Repo
|
|
99
|
+
full_name == other.full_name
|
|
100
|
+
when String
|
|
101
|
+
full_name == other
|
|
102
|
+
else
|
|
103
|
+
super
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -316,10 +316,17 @@ module PlanMyStuff
|
|
|
316
316
|
},
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
resolved_repo =
|
|
320
|
+
if params[:repo]
|
|
321
|
+
PlanMyStuff::Repo.resolve(params[:repo]).full_name
|
|
322
|
+
else
|
|
323
|
+
'TestOrg/TestRepo'
|
|
324
|
+
end
|
|
325
|
+
|
|
319
326
|
PlanMyStuff::TestHelpers.build_issue(
|
|
320
327
|
title: params[:title],
|
|
321
328
|
body: params[:body],
|
|
322
|
-
repo:
|
|
329
|
+
repo: resolved_repo,
|
|
323
330
|
labels: params[:labels] || [],
|
|
324
331
|
)
|
|
325
332
|
end
|
|
@@ -330,7 +337,8 @@ module PlanMyStuff
|
|
|
330
337
|
params: { number: number, repo: repo },
|
|
331
338
|
}
|
|
332
339
|
|
|
333
|
-
PlanMyStuff::
|
|
340
|
+
resolved_repo = repo ? PlanMyStuff::Repo.resolve(repo).full_name : 'TestOrg/TestRepo'
|
|
341
|
+
PlanMyStuff::TestHelpers.build_issue(number: number, repo: resolved_repo)
|
|
334
342
|
end
|
|
335
343
|
|
|
336
344
|
issue_mod.define_singleton_method(:list) do |**params|
|