plan_my_stuff 0.2.0 → 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/app/controllers/plan_my_stuff/projects_controller.rb +54 -0
- 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 +2 -0
- data/config/routes.rb +1 -1
- data/lib/plan_my_stuff/application_record.rb +23 -0
- data/lib/plan_my_stuff/base_metadata.rb +0 -11
- data/lib/plan_my_stuff/comment.rb +7 -8
- data/lib/plan_my_stuff/configuration.rb +46 -0
- data/lib/plan_my_stuff/issue.rb +7 -5
- data/lib/plan_my_stuff/project.rb +264 -7
- data/lib/plan_my_stuff/project_metadata.rb +41 -0
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff.rb +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b5b0d6a9e0582853ebf435a48b49fabb8dc9bd1227bddc6f2c3467d74872c80
|
|
4
|
+
data.tar.gz: 3e1df8e562940c9c1435c2edc4b88c7b85c2c2c8309b95702f8f6d3f978bdf19
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fdc842d462d7e04c163560c475bd6519e04b4b5f57b2684be5137ed32ff0bc9e257d9f09e7ab1cda06e6106065e38e4a1c80f0fea96d812841141cc14cbed4a6
|
|
7
|
+
data.tar.gz: 9e4954b597a281e4c68e1ea9ecbe2748efbcac31062f7fd3b925b887dc0616015df27df8c0122fc4165621227f5eefa13ead1ce0583cbe9dcd377ecaf0e47e23
|
|
@@ -7,11 +7,65 @@ module PlanMyStuff
|
|
|
7
7
|
@projects = PMS::Project.list.reject(&:closed)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
# GET /projects/new
|
|
11
|
+
def new
|
|
12
|
+
@project = PMS::Project.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# POST /projects
|
|
16
|
+
def create
|
|
17
|
+
@project = PMS::Project.create!(
|
|
18
|
+
title: project_params[:title],
|
|
19
|
+
readme: project_params[:readme] || '',
|
|
20
|
+
description: project_params[:description],
|
|
21
|
+
user: pms_current_user,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
flash[:success] = 'Project was successfully created.'
|
|
25
|
+
redirect_to(plan_my_stuff.project_path(@project.number))
|
|
26
|
+
rescue PMS::ValidationError => e
|
|
27
|
+
@project = PMS::Project.new(
|
|
28
|
+
title: project_params[:title],
|
|
29
|
+
readme: project_params[:readme],
|
|
30
|
+
)
|
|
31
|
+
flash.now[:error] = e.message
|
|
32
|
+
render(:new, status: PMS.unprocessable_status)
|
|
33
|
+
end
|
|
34
|
+
|
|
10
35
|
# GET /projects/:id
|
|
11
36
|
def show
|
|
12
37
|
@project = PMS::Project.find(params[:id].to_i)
|
|
13
38
|
@statuses = @project.statuses.pluck(:name)
|
|
14
39
|
@items_by_status = @project.items.group_by(&:status)
|
|
15
40
|
end
|
|
41
|
+
|
|
42
|
+
# GET /projects/:id/edit
|
|
43
|
+
def edit
|
|
44
|
+
@project = PMS::Project.find(params[:id].to_i)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# PATCH/PUT /projects/:id
|
|
48
|
+
def update
|
|
49
|
+
@project = PMS::Project.find(params[:id].to_i)
|
|
50
|
+
|
|
51
|
+
@project.update!(
|
|
52
|
+
title: project_params[:title],
|
|
53
|
+
readme: project_params[:readme],
|
|
54
|
+
description: project_params[:description],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
flash[:success] = 'Project was successfully updated.'
|
|
58
|
+
redirect_to(plan_my_stuff.project_path(@project.number))
|
|
59
|
+
rescue PMS::StaleObjectError
|
|
60
|
+
flash.now[:error] = 'Project was modified by someone else. Please review the latest changes and try again.'
|
|
61
|
+
render(:edit, status: PMS.unprocessable_status)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# @return [ActionController::Parameters]
|
|
67
|
+
def project_params
|
|
68
|
+
params.require(:project).permit(:title, :readme, :description)
|
|
69
|
+
end
|
|
16
70
|
end
|
|
17
71
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<%
|
|
2
|
+
persisted = project.persisted?
|
|
3
|
+
url =
|
|
4
|
+
if persisted
|
|
5
|
+
plan_my_stuff.project_path(project.number)
|
|
6
|
+
else
|
|
7
|
+
plan_my_stuff.projects_path
|
|
8
|
+
end
|
|
9
|
+
%>
|
|
10
|
+
<%= form_with(url: url, method: persisted ? :patch : :post, scope: :project) do |form| %>
|
|
11
|
+
<div>
|
|
12
|
+
<%= form.label(:title, 'Title') %>
|
|
13
|
+
<%= form.text_field(:title, value: project.title, required: true) %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div>
|
|
17
|
+
<%= form.label(:description, 'Description') %>
|
|
18
|
+
<%= form.text_field(:description, value: project.description) %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div>
|
|
22
|
+
<%= form.label(:readme, 'Readme') %>
|
|
23
|
+
<%= form.text_area(:readme, rows: 8, value: project.readme) %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div>
|
|
27
|
+
<%= form.submit(persisted ? 'Update Project' : 'Create Project') %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -13,7 +13,7 @@ PlanMyStuff::Engine.routes.draw do
|
|
|
13
13
|
delete 'labels/:name', to: 'labels#remove_from_issue', as: :remove_label
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
resources :projects,
|
|
16
|
+
resources :projects, except: %i[destroy] do
|
|
17
17
|
resources :items, only: %i[create], controller: 'project_items' do
|
|
18
18
|
member do
|
|
19
19
|
patch :move
|
|
@@ -48,5 +48,28 @@ module PlanMyStuff
|
|
|
48
48
|
def read_field(obj, field)
|
|
49
49
|
self.class.read_field(obj, field)
|
|
50
50
|
end
|
|
51
|
+
|
|
52
|
+
# Reads a field from an object, returning nil if the field does not exist.
|
|
53
|
+
#
|
|
54
|
+
# @param obj [Object]
|
|
55
|
+
# @param field [Symbol]
|
|
56
|
+
#
|
|
57
|
+
# @return [Object, nil]
|
|
58
|
+
#
|
|
59
|
+
def safe_read_field(obj, field)
|
|
60
|
+
read_field(obj, field)
|
|
61
|
+
rescue NameError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Time, nil]
|
|
66
|
+
def parse_github_time(value)
|
|
67
|
+
return if value.nil?
|
|
68
|
+
return value.utc if value.is_a?(Time)
|
|
69
|
+
|
|
70
|
+
Time.parse(value.to_s).utc
|
|
71
|
+
rescue ArgumentError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
51
74
|
end
|
|
52
75
|
end
|
|
@@ -15,10 +15,6 @@ module PlanMyStuff
|
|
|
15
15
|
attr_accessor :rails_env
|
|
16
16
|
# @return [String, nil] consuming app name from config
|
|
17
17
|
attr_accessor :app_name
|
|
18
|
-
# @return [Time, nil] timestamp of creation
|
|
19
|
-
attr_accessor :created_at
|
|
20
|
-
# @return [Time, nil] timestamp of last update
|
|
21
|
-
attr_accessor :updated_at
|
|
22
18
|
# @return [Integer, nil] consuming app's user ID of the creator
|
|
23
19
|
attr_accessor :created_by
|
|
24
20
|
# @return [String] "public" or "internal"
|
|
@@ -42,8 +38,6 @@ module PlanMyStuff
|
|
|
42
38
|
metadata.gem_version = hash[:gem_version]
|
|
43
39
|
metadata.rails_env = hash[:rails_env]
|
|
44
40
|
metadata.app_name = hash[:app_name]
|
|
45
|
-
metadata.created_at = parse_time(hash[:created_at])
|
|
46
|
-
metadata.updated_at = parse_time(hash[:updated_at])
|
|
47
41
|
metadata.created_by = hash[:created_by]
|
|
48
42
|
metadata.visibility = hash.fetch(:visibility, 'internal')
|
|
49
43
|
metadata.custom_fields = CustomFields.new(
|
|
@@ -70,14 +64,11 @@ module PlanMyStuff
|
|
|
70
64
|
custom_fields_schema: {}
|
|
71
65
|
)
|
|
72
66
|
config = PlanMyStuff.configuration
|
|
73
|
-
now = Time.now.utc
|
|
74
67
|
|
|
75
68
|
metadata.schema_version = self::SCHEMA_VERSION
|
|
76
69
|
metadata.gem_version = PlanMyStuff::VERSION::STRING
|
|
77
70
|
metadata.rails_env = (defined?(Rails) && Rails.respond_to?(:env)) ? Rails.env.to_s : nil
|
|
78
71
|
metadata.app_name = config.app_name
|
|
79
|
-
metadata.created_at = now
|
|
80
|
-
metadata.updated_at = now
|
|
81
72
|
resolved = UserResolver.resolve(user)
|
|
82
73
|
metadata.created_by = resolved.present? ? UserResolver.user_id(resolved) : nil
|
|
83
74
|
metadata.visibility = visibility
|
|
@@ -110,8 +101,6 @@ module PlanMyStuff
|
|
|
110
101
|
gem_version: gem_version,
|
|
111
102
|
rails_env: rails_env,
|
|
112
103
|
app_name: app_name,
|
|
113
|
-
created_at: format_time(created_at),
|
|
114
|
-
updated_at: format_time(updated_at),
|
|
115
104
|
created_by: created_by,
|
|
116
105
|
visibility: visibility,
|
|
117
106
|
custom_fields: custom_fields.to_h,
|
|
@@ -20,6 +20,8 @@ module PlanMyStuff
|
|
|
20
20
|
attr_accessor :body
|
|
21
21
|
# @return [PlanMyStuff::Issue] parent issue
|
|
22
22
|
attr_accessor :issue
|
|
23
|
+
# @return [Time, nil] GitHub's updated_at timestamp
|
|
24
|
+
attr_reader :updated_at
|
|
23
25
|
# @param value [Symbol, String, nil]
|
|
24
26
|
attr_writer :visibility
|
|
25
27
|
|
|
@@ -224,7 +226,6 @@ module PlanMyStuff
|
|
|
224
226
|
if attrs.key?(:visibility)
|
|
225
227
|
new_visibility = attrs[:visibility].to_s
|
|
226
228
|
meta_hash[:visibility] = new_visibility
|
|
227
|
-
meta_hash[:updated_at] = Time.now.utc.iso8601
|
|
228
229
|
end
|
|
229
230
|
|
|
230
231
|
serialized = MetadataParser.serialize(meta_hash, new_body)
|
|
@@ -318,6 +319,7 @@ module PlanMyStuff
|
|
|
318
319
|
def hydrate_from_github(github_comment, issue:)
|
|
319
320
|
@id = read_field(github_comment, :id)
|
|
320
321
|
@raw_body = read_field(github_comment, :body)
|
|
322
|
+
@updated_at = parse_github_time(safe_read_field(github_comment, :updated_at))
|
|
321
323
|
@issue = issue
|
|
322
324
|
|
|
323
325
|
parsed = MetadataParser.parse(@raw_body)
|
|
@@ -337,6 +339,7 @@ module PlanMyStuff
|
|
|
337
339
|
@id = other.id
|
|
338
340
|
@body = other.body
|
|
339
341
|
@raw_body = other.raw_body
|
|
342
|
+
@updated_at = other.updated_at
|
|
340
343
|
@issue = other.issue
|
|
341
344
|
@metadata = other.metadata
|
|
342
345
|
@visibility = other.visibility
|
|
@@ -352,15 +355,11 @@ module PlanMyStuff
|
|
|
352
355
|
#
|
|
353
356
|
def raise_if_stale!
|
|
354
357
|
return if new_record?
|
|
355
|
-
return if
|
|
358
|
+
return if updated_at.nil?
|
|
356
359
|
|
|
357
360
|
github_comment = PlanMyStuff.client.rest(:issue_comment, issue.repo, id)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
)
|
|
361
|
-
remote_metadata = CommentMetadata.from_hash(parsed[:metadata])
|
|
362
|
-
remote_time = remote_metadata.updated_at
|
|
363
|
-
local_time = metadata.updated_at
|
|
361
|
+
remote_time = parse_github_time(safe_read_field(github_comment, :updated_at))
|
|
362
|
+
local_time = updated_at
|
|
364
363
|
|
|
365
364
|
return if remote_time.nil?
|
|
366
365
|
return if local_time && remote_time.to_i == local_time.to_i
|
|
@@ -92,12 +92,51 @@ module PlanMyStuff
|
|
|
92
92
|
#
|
|
93
93
|
attr_accessor :comment_custom_fields
|
|
94
94
|
|
|
95
|
+
# Project-only field definitions, deep-merged on top of shared custom_fields.
|
|
96
|
+
# Context-specific config wins on key conflicts.
|
|
97
|
+
#
|
|
98
|
+
# @return [Hash{Symbol => Hash}]
|
|
99
|
+
#
|
|
100
|
+
attr_accessor :project_custom_fields
|
|
101
|
+
|
|
95
102
|
# @return [String, nil] URL prefix for building user-facing ticket URLs in the consuming app
|
|
96
103
|
attr_accessor :issues_url_prefix
|
|
97
104
|
|
|
98
105
|
# @return [String, nil] name of the consuming app, stored in metadata (e.g. "Atlas")
|
|
99
106
|
attr_accessor :app_name
|
|
100
107
|
|
|
108
|
+
# @return [Boolean] whether the release pipeline feature is enabled
|
|
109
|
+
attr_accessor :pipeline_enabled
|
|
110
|
+
|
|
111
|
+
# @return [Integer, nil] GitHub Projects V2 number for the pipeline board (falls back to default_project_number)
|
|
112
|
+
attr_accessor :pipeline_project_number
|
|
113
|
+
|
|
114
|
+
# @return [String, nil] HMAC secret for GitHub webhook signature verification (required when webhooks mounted)
|
|
115
|
+
attr_accessor :webhook_secret
|
|
116
|
+
|
|
117
|
+
# @return [String, nil] shared secret for AWS webhook signature verification (required when AWS webhook mounted)
|
|
118
|
+
attr_accessor :aws_webhook_secret
|
|
119
|
+
|
|
120
|
+
# Canonical status name to display alias map. Allows consuming apps to rename
|
|
121
|
+
# pipeline statuses (e.g. "Submitted" to "Triaged").
|
|
122
|
+
#
|
|
123
|
+
# @return [Hash{String => String}]
|
|
124
|
+
#
|
|
125
|
+
attr_accessor :pipeline_statuses
|
|
126
|
+
|
|
127
|
+
# @return [String] branch name that PRs merge into for "Ready for release" transition
|
|
128
|
+
attr_accessor :main_branch
|
|
129
|
+
|
|
130
|
+
# @return [String] branch name that triggers deployment when a PR merges
|
|
131
|
+
attr_accessor :production_branch
|
|
132
|
+
|
|
133
|
+
# Per-group route mounting toggles. Keys: :webhooks, :issues, :projects.
|
|
134
|
+
# Set a key to false to skip mounting that route group.
|
|
135
|
+
#
|
|
136
|
+
# @return [Hash{Symbol => Boolean}]
|
|
137
|
+
#
|
|
138
|
+
attr_accessor :mount_groups
|
|
139
|
+
|
|
101
140
|
# Named repo configs. Set via config.repos[:element] = 'BrandsInsurance/Element'.
|
|
102
141
|
#
|
|
103
142
|
# @return [Hash{Symbol => String}]
|
|
@@ -117,6 +156,12 @@ module PlanMyStuff
|
|
|
117
156
|
@custom_fields = {}
|
|
118
157
|
@issue_custom_fields = {}
|
|
119
158
|
@comment_custom_fields = {}
|
|
159
|
+
@project_custom_fields = {}
|
|
160
|
+
@pipeline_enabled = true
|
|
161
|
+
@pipeline_statuses = {}
|
|
162
|
+
@main_branch = 'main'
|
|
163
|
+
@production_branch = 'production'
|
|
164
|
+
@mount_groups = { webhooks: true, issues: true, projects: true }
|
|
120
165
|
end
|
|
121
166
|
|
|
122
167
|
# Sets the authentication block for engine controllers.
|
|
@@ -162,6 +207,7 @@ module PlanMyStuff
|
|
|
162
207
|
case context
|
|
163
208
|
when :issue then issue_custom_fields
|
|
164
209
|
when :comment then comment_custom_fields
|
|
210
|
+
when :project then project_custom_fields
|
|
165
211
|
else {}
|
|
166
212
|
end
|
|
167
213
|
|
data/lib/plan_my_stuff/issue.rb
CHANGED
|
@@ -24,6 +24,8 @@ module PlanMyStuff
|
|
|
24
24
|
attr_accessor :state
|
|
25
25
|
# @return [Array<String>] label names
|
|
26
26
|
attr_accessor :labels
|
|
27
|
+
# @return [Time, nil] GitHub's updated_at timestamp
|
|
28
|
+
attr_reader :updated_at
|
|
27
29
|
# @return [PlanMyStuff::Repo, nil]
|
|
28
30
|
attr_reader :repo
|
|
29
31
|
|
|
@@ -126,7 +128,6 @@ module PlanMyStuff
|
|
|
126
128
|
merged_custom_fields,
|
|
127
129
|
).validate!
|
|
128
130
|
|
|
129
|
-
existing_metadata[:updated_at] = Time.now.utc.iso8601
|
|
130
131
|
options[:body] = MetadataParser.serialize(existing_metadata, '')
|
|
131
132
|
end
|
|
132
133
|
|
|
@@ -250,7 +251,6 @@ module PlanMyStuff
|
|
|
250
251
|
existing_metadata = parsed[:metadata]
|
|
251
252
|
allowlist = Array.wrap(existing_metadata[:visibility_allowlist])
|
|
252
253
|
existing_metadata[:visibility_allowlist] = yield(allowlist)
|
|
253
|
-
existing_metadata[:updated_at] = Time.now.utc.iso8601
|
|
254
254
|
|
|
255
255
|
new_body = MetadataParser.serialize(existing_metadata, parsed[:body])
|
|
256
256
|
client.rest(:update_issue, resolved_repo, number, body: new_body)
|
|
@@ -412,6 +412,7 @@ module PlanMyStuff
|
|
|
412
412
|
@title = read_field(github_issue, :title)
|
|
413
413
|
@state = read_field(github_issue, :state)
|
|
414
414
|
@raw_body = read_field(github_issue, :body) || ''
|
|
415
|
+
@updated_at = parse_github_time(safe_read_field(github_issue, :updated_at))
|
|
415
416
|
@labels = extract_labels(github_issue)
|
|
416
417
|
self.repo = repo
|
|
417
418
|
|
|
@@ -434,6 +435,7 @@ module PlanMyStuff
|
|
|
434
435
|
@state = other.state
|
|
435
436
|
@body = other.instance_variable_get(:@body)
|
|
436
437
|
@raw_body = other.raw_body
|
|
438
|
+
@updated_at = other.updated_at
|
|
437
439
|
@labels = other.labels
|
|
438
440
|
@repo = other.repo
|
|
439
441
|
@metadata = other.metadata
|
|
@@ -450,11 +452,11 @@ module PlanMyStuff
|
|
|
450
452
|
#
|
|
451
453
|
def raise_if_stale!
|
|
452
454
|
return if new_record?
|
|
453
|
-
return if
|
|
455
|
+
return if updated_at.nil?
|
|
454
456
|
|
|
455
457
|
remote = self.class.find(number, repo: repo)
|
|
456
|
-
remote_time = remote.
|
|
457
|
-
local_time =
|
|
458
|
+
remote_time = remote.updated_at
|
|
459
|
+
local_time = updated_at
|
|
458
460
|
|
|
459
461
|
return if remote_time.nil?
|
|
460
462
|
return if local_time && remote_time.to_i == local_time.to_i
|
|
@@ -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.
|
|
@@ -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 {
|
|
@@ -408,6 +495,60 @@ module PlanMyStuff
|
|
|
408
495
|
|
|
409
496
|
data.dig(:organization, :projectV2, :id)
|
|
410
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
|
|
411
552
|
end
|
|
412
553
|
|
|
413
554
|
# @see super
|
|
@@ -415,6 +556,11 @@ module PlanMyStuff
|
|
|
415
556
|
@id = attrs.delete(:id)
|
|
416
557
|
@number = attrs.delete(:number)
|
|
417
558
|
@closed = attrs.delete(:closed)
|
|
559
|
+
@metadata = PlanMyStuff::ProjectMetadata.new
|
|
560
|
+
@raw_readme = nil
|
|
561
|
+
@readme = nil
|
|
562
|
+
@description = nil
|
|
563
|
+
@updated_at = nil
|
|
418
564
|
super
|
|
419
565
|
@statuses ||= []
|
|
420
566
|
@fields ||= []
|
|
@@ -435,6 +581,58 @@ module PlanMyStuff
|
|
|
435
581
|
field
|
|
436
582
|
end
|
|
437
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
|
+
|
|
438
636
|
private
|
|
439
637
|
|
|
440
638
|
# Populates this instance from a list query node (summary only).
|
|
@@ -447,8 +645,10 @@ module PlanMyStuff
|
|
|
447
645
|
@id = node[:id]
|
|
448
646
|
@number = node[:number]
|
|
449
647
|
@title = node[:title]
|
|
648
|
+
@description = node[:shortDescription]
|
|
450
649
|
@url = node[:url]
|
|
451
650
|
@closed = node[:closed]
|
|
651
|
+
@updated_at = parse_github_time(node[:updatedAt])
|
|
452
652
|
@persisted = true
|
|
453
653
|
end
|
|
454
654
|
|
|
@@ -465,8 +665,15 @@ module PlanMyStuff
|
|
|
465
665
|
@id = graphql_project[:id]
|
|
466
666
|
@number = graphql_project[:number]
|
|
467
667
|
@title = graphql_project[:title]
|
|
668
|
+
@description = graphql_project[:shortDescription]
|
|
468
669
|
@url = graphql_project[:url]
|
|
469
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]
|
|
470
677
|
|
|
471
678
|
fields_nodes = graphql_project.dig(:fields, :nodes) || []
|
|
472
679
|
@statuses = extract_statuses(fields_nodes)
|
|
@@ -477,6 +684,56 @@ module PlanMyStuff
|
|
|
477
684
|
@persisted = true
|
|
478
685
|
end
|
|
479
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
|
+
|
|
480
737
|
# Extracts status options from the "Status" single-select field.
|
|
481
738
|
#
|
|
482
739
|
# @param fields_nodes [Array<Hash>]
|
|
@@ -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
|
data/lib/plan_my_stuff.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative 'plan_my_stuff/markdown'
|
|
|
19
19
|
require_relative 'plan_my_stuff/metadata_parser'
|
|
20
20
|
require_relative 'plan_my_stuff/project'
|
|
21
21
|
require_relative 'plan_my_stuff/project_item'
|
|
22
|
+
require_relative 'plan_my_stuff/project_metadata'
|
|
22
23
|
require_relative 'plan_my_stuff/repo'
|
|
23
24
|
require_relative 'plan_my_stuff/user_resolver'
|
|
24
25
|
require_relative 'plan_my_stuff/verifier'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: plan_my_stuff
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brands Insurance
|
|
@@ -69,7 +69,10 @@ files:
|
|
|
69
69
|
- app/views/plan_my_stuff/issues/partials/_labels.html.erb
|
|
70
70
|
- app/views/plan_my_stuff/issues/partials/_viewers.html.erb
|
|
71
71
|
- app/views/plan_my_stuff/issues/show.html.erb
|
|
72
|
+
- app/views/plan_my_stuff/projects/edit.html.erb
|
|
72
73
|
- app/views/plan_my_stuff/projects/index.html.erb
|
|
74
|
+
- app/views/plan_my_stuff/projects/new.html.erb
|
|
75
|
+
- app/views/plan_my_stuff/projects/partials/_form.html.erb
|
|
73
76
|
- app/views/plan_my_stuff/projects/show.html.erb
|
|
74
77
|
- config/routes.rb
|
|
75
78
|
- lib/generators/plan_my_stuff/install/install_generator.rb
|
|
@@ -92,6 +95,7 @@ files:
|
|
|
92
95
|
- lib/plan_my_stuff/metadata_parser.rb
|
|
93
96
|
- lib/plan_my_stuff/project.rb
|
|
94
97
|
- lib/plan_my_stuff/project_item.rb
|
|
98
|
+
- lib/plan_my_stuff/project_metadata.rb
|
|
95
99
|
- lib/plan_my_stuff/repo.rb
|
|
96
100
|
- lib/plan_my_stuff/test_helpers.rb
|
|
97
101
|
- lib/plan_my_stuff/user_resolver.rb
|