plan_my_stuff 0.19.0 → 0.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15988155f3d1798b5b0f43cbb1597f7e8b7ed881feb3db2504da7ecd74ab9c59
4
- data.tar.gz: 35f298dce675a86ca15153a354411c23806e8420374fe5eb4fd7d2c392e0ed9d
3
+ metadata.gz: 0d33c652f54d9d078276d12baac63134e8bba4064185f61626c66fd924ea958a
4
+ data.tar.gz: 54f5c69130890fa5ae807d642cd8d89d2f0d74c4d12b638ed892d8f9b2df22d8
5
5
  SHA512:
6
- metadata.gz: 838ebf5cb77a3467fb291d76acd9b2ded4cc40d88e65673ca41374324736c6cdc11e5bbc0ed7e04eaf0c0bb5ac4f799c3052379cdc87edcd7343dc30be370059
7
- data.tar.gz: b30d829c2255b61485c72d639992f1095f9726b7e6d8952114eef2e44c91f1016c0f05bd3abd238faf0be83a37c8b75ef492208351d33cbbe1f2f689270e7614
6
+ metadata.gz: 36d2a9a1624279d818d6c1a16fcfd51cd8e047e4f02d51044673fecb0ef3433b4a953290eb51deb868b9a8959fe365abf4dcb0645883466941047c6915c070d3
7
+ data.tar.gz: fbadf2e2e8337a7014e148e898291de7ef4453729332f2c8d2609942487ebec7359a97a687e44340edac1a0d7c8e36fdeddcd3cc90c8fe30a108f76f7ba74a49
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Breaking
6
+
7
+ - `PMS::Attachment` no longer accepts/stores a single `url:` attribute. Construct with explicit
8
+ `filename:`, `owner:`, `repo:`, `sha:`, `path:` instead; `#url` is now a derived method that returns the
9
+ GitHub blob viewer URL (`https://github.com/{owner}/{repo}/blob/{sha}/{path}`) rather than the
10
+ `raw.githubusercontent.com` URL. `#to_h` emits the structured fields and no longer includes a `url` key.
11
+ Persisted comment metadata in the legacy `{filename, url}` shape is migrated on read and rewritten in the
12
+ new shape on next save (closes #70).
13
+
14
+ ### Added
15
+
16
+ - Comment bodies now render a visible `<details><summary>attachments (N)</summary>` block listing each
17
+ attachment as a markdown link to its blob URL, between the `pms-metadata` block and the user body. The
18
+ parser strips this block on read so round-trips stay clean.
19
+
20
+ ## 0.20.0
21
+
22
+ ### Breaking
23
+
24
+ - `priority_list` and `priority_list_priority` keys removed from `IssueMetadata` serialization. Existing issues
25
+ migrate to the GitHub Issue Field equivalents (`Priority List`, `Priority List Priority`) on their next save.
26
+
27
+ ### Added
28
+
29
+ - `Issue#priority_list?` and `Issue#priority_list_priority` read the corresponding issue field values directly.
30
+ - `Issue.priority_list(repo:, state:, labels:, page:, per_page:)` convenience method delegates to
31
+ `Issue.list(priority_list: true, ...)`.
32
+ - `Issue.list` and `Issue.count` accept `priority_list:` for server-side filtering via the GitHub issue fields
33
+ API (passes `priority_list: false` raises `ArgumentError` -- the API has no negation qualifier).
34
+
35
+ ### Deprecated
36
+
37
+ - `IssueMetadata#priority_list`, `#priority_list=`, `#priority_list?`, `#priority_list_priority`, and
38
+ `#priority_list_priority=` emit deprecation warnings on each read/write. Legacy writes forward to the issue
39
+ field on `Issue#save!` / `#update!` / `Issue.create!`. The accessors will be removed in 1.0.0.
40
+
3
41
  ## 0.19.0
4
42
 
5
43
  ### Added
@@ -6,33 +6,40 @@ require 'tmpdir'
6
6
  module PlanMyStuff
7
7
  # Value object representing a single attachment record on a +Comment+.
8
8
  # Persisted in +CommentMetadata#attachments+; the gem owns the upload
9
- # (see +PlanMyStuff::AttachmentUploader+) and stores a SHA-pinned
10
- # +raw.githubusercontent.com+ permalink so the file remains reachable
11
- # across later branch rewrites.
9
+ # (see +PlanMyStuff::AttachmentUploader+) and stores the structured
10
+ # location (+owner+/+repo+/+sha+/+path+) of the uploaded file so the
11
+ # blob remains addressable across later branch rewrites.
12
12
  #
13
13
  # Mirrors +PlanMyStuff::Link+ / +PlanMyStuff::Approval+:
14
14
  # +ActiveModel::Attributes+-backed, with +Serializers::JSON+ for
15
15
  # round-trip through the metadata blob.
16
16
  #
17
17
  class Attachment
18
- RAW_URL_REGEX =
19
- %r{\Ahttps://raw\.githubusercontent\.com/(?<owner>[^/\s]+)/(?<repo>[^/\s]+)/(?<sha>[^/\s]+)/(?<path>.+)\z}
20
-
21
18
  include ActiveModel::Model
22
19
  include ActiveModel::Attributes
23
20
  include ActiveModel::Serializers::JSON
24
21
 
25
22
  # @return [String] display filename (user-provided original name)
26
23
  attribute :filename, :string
27
- # @return [String] SHA-pinned raw.githubusercontent.com permalink to the uploaded file
28
- attribute :url, :string
24
+ # @return [String] owner of the attachment repo (e.g. +"BrandsInsurance"+)
25
+ attribute :owner, :string
26
+ # @return [String] attachment repo name
27
+ attribute :repo, :string
28
+ # @return [String] commit SHA pinning the file
29
+ attribute :sha, :string
30
+ # @return [String] path within the attachment repo (no leading slash)
31
+ attribute :path, :string
32
+
33
+ validates :filename, :owner, :repo, :sha, :path, presence: true
29
34
 
30
- validates :filename, presence: true
31
- validates :url, presence: true, format: { with: RAW_URL_REGEX }
35
+ # @return [String] blob-viewer URL for the pinned file
36
+ def url
37
+ "https://github.com/#{owner}/#{repo}/blob/#{sha}/#{path}"
38
+ end
32
39
 
33
40
  # @return [Hash]
34
41
  def to_h
35
- { filename: filename, url: url }
42
+ { filename: filename, owner: owner, repo: repo, sha: sha, path: path }
36
43
  end
37
44
 
38
45
  # @param other [Object]
@@ -56,22 +63,21 @@ module PlanMyStuff
56
63
  # Defaults the destination to +Dir.tmpdir+ joined with +File.basename(filename)+ (directory components
57
64
  # stripped to prevent path traversal).
58
65
  #
59
- # @param path [String, nil] destination path; defaults to +File.join(Dir.tmpdir, File.basename(filename))+
66
+ # @param dest [String, nil] destination path; defaults to +File.join(Dir.tmpdir, File.basename(filename))+
60
67
  #
61
68
  # @return [String] the path the bytes were written to
62
69
  #
63
- def download_to(path = nil)
64
- path ||= File.join(Dir.tmpdir, File.basename(filename.to_s))
65
- match = RAW_URL_REGEX.match(url)
70
+ def download_to(dest = nil)
71
+ dest ||= File.join(Dir.tmpdir, File.basename(filename.to_s))
66
72
  body = PlanMyStuff.client.rest(
67
73
  :contents,
68
- "#{match[:owner]}/#{match[:repo]}",
69
- path: match[:path],
70
- ref: match[:sha],
74
+ "#{owner}/#{repo}",
75
+ path: path,
76
+ ref: sha,
71
77
  accept: 'application/vnd.github.raw',
72
78
  )
73
- File.binwrite(path, body)
74
- path
79
+ File.binwrite(dest, body)
80
+ dest
75
81
  end
76
82
  end
77
83
  end
@@ -6,8 +6,8 @@ require 'securerandom'
6
6
 
7
7
  module PlanMyStuff
8
8
  # Uploads attachment files to a shared attachment repo (+config.attachment_repo+ under +config.organization+) using
9
- # GitHub's Git Data API, returning SHA-pinned +raw.githubusercontent.com+ permalinks wrapped in
10
- # +PlanMyStuff::Attachment+ instances.
9
+ # GitHub's Git Data API, returning +PlanMyStuff::Attachment+ instances populated with the structured
10
+ # +owner+/+repo+/+sha+/+path+ location of each uploaded file.
11
11
  #
12
12
  # One commit per batch (atomic): all files in a single +upload_all!+ call land in the same tree/commit so partial
13
13
  # failures cannot leave +config.main_branch+ in an inconsistent state.
@@ -202,8 +202,13 @@ module PlanMyStuff
202
202
  # @return [PlanMyStuff::Attachment]
203
203
  #
204
204
  def build_attachment(commit_sha:, slot:)
205
- url = "https://raw.githubusercontent.com/#{attachment_repo_full_name}/#{commit_sha}/#{slot[:path]}"
206
- PlanMyStuff::Attachment.new(filename: slot[:filename], url: url)
205
+ PlanMyStuff::Attachment.new(
206
+ filename: slot[:filename],
207
+ owner: PlanMyStuff.configuration.organization,
208
+ repo: PlanMyStuff.configuration.attachment_repo,
209
+ sha: commit_sha,
210
+ path: slot[:path],
211
+ )
207
212
  end
208
213
 
209
214
  # @return [String] +"<organization>/<attachment_repo>"+
@@ -2,6 +2,17 @@
2
2
 
3
3
  module PlanMyStuff
4
4
  class CommentMetadata < PlanMyStuff::BaseMetadata
5
+ LEGACY_URL_REGEXES = [
6
+ %r{\Ahttps://raw\.githubusercontent\.com/(?<owner>[^/\s]+)/(?<repo>[^/\s]+)/(?<sha>[^/\s]+)/(?<path>.+)\z},
7
+ %r{\Ahttps://github\.com/(?<owner>[^/\s]+)/(?<repo>[^/\s]+)/blob/(?<sha>[^/\s]+)/(?<path>.+)\z},
8
+ ].freeze
9
+
10
+ LEGACY_ATTACHMENT_DEPRECATION_MESSAGE =
11
+ 'PlanMyStuff: legacy attachment metadata shape {filename, url} detected. It will continue to parse ' \
12
+ 'until 1.0.0, at which point legacy detection will be removed. New writes use the structured ' \
13
+ '{filename, owner, repo, sha, path} shape introduced in #70; existing entries migrate on their ' \
14
+ 'next write.'
15
+
5
16
  # @return [Boolean] true if this comment holds the issue's body content
6
17
  attr_accessor :issue_body
7
18
  # @return [Array<PlanMyStuff::Attachment>] consuming-app attachment records associated with this comment
@@ -82,9 +93,14 @@ module PlanMyStuff
82
93
  private
83
94
 
84
95
  # Builds a +PlanMyStuff::Attachment+ from each parsed entry.
85
- # Each entry is a +{filename:, url:}+ hash (symbol keys when
86
- # built locally, string keys when read back from GitHub
87
- # metadata) or an existing +PlanMyStuff::Attachment+.
96
+ # Accepts:
97
+ # - an existing +PlanMyStuff::Attachment+
98
+ # - the new shape +{filename:, owner:, repo:, sha:, path:}+
99
+ # - the legacy shape +{filename:, url:}+ where +url+ is a raw
100
+ # +raw.githubusercontent.com+ permalink or a +github.com/.../blob/...+
101
+ # blob-viewer URL; parsed once into structured fields so the
102
+ # next write drops the +url+ key (migrate-on-write).
103
+ #
88
104
  # Malformed hash entries are silently dropped so a single bad
89
105
  # historical entry doesn't crash +Comment.find+. An already-
90
106
  # constructed +Attachment+ that fails validation raises so the
@@ -102,7 +118,7 @@ module PlanMyStuff
102
118
  if entry.is_a?(PlanMyStuff::Attachment)
103
119
  entry
104
120
  elsif entry.respond_to?(:transform_keys)
105
- PlanMyStuff::Attachment.new(entry.transform_keys(&:to_sym))
121
+ PlanMyStuff::Attachment.new(structured_attrs(entry.transform_keys(&:to_sym)))
106
122
  end
107
123
  next if attachment.nil?
108
124
 
@@ -114,5 +130,33 @@ module PlanMyStuff
114
130
  next
115
131
  end
116
132
  end
133
+
134
+ # Returns +attrs+ untouched when it already carries structured
135
+ # fields, otherwise parses a legacy +url+ into
136
+ # +owner+/+repo+/+sha+/+path+. Unparseable URLs yield a hash
137
+ # with no structured fields, leaving +Attachment+ validation to
138
+ # drop the entry.
139
+ #
140
+ # @param attrs [Hash{Symbol=>Object}]
141
+ #
142
+ # @return [Hash{Symbol=>Object}]
143
+ #
144
+ def structured_attrs(attrs)
145
+ return attrs if attrs.key?(:owner)
146
+
147
+ PlanMyStuff.deprecator.warn(LEGACY_ATTACHMENT_DEPRECATION_MESSAGE)
148
+
149
+ url = attrs[:url].to_s
150
+ match = LEGACY_URL_REGEXES.lazy.filter_map { |re| re.match(url) }.first
151
+
152
+ return attrs.except(:url) if match.nil?
153
+
154
+ attrs.except(:url).merge(
155
+ owner: match[:owner],
156
+ repo: match[:repo],
157
+ sha: match[:sha],
158
+ path: match[:path],
159
+ )
160
+ end
117
161
  end
118
162
  end
@@ -296,26 +296,46 @@ module PlanMyStuff
296
296
 
297
297
  # Lists GitHub issues with optional filters and pagination.
298
298
  #
299
+ # @raise [ArgumentError] when +priority_list: false+ is passed
300
+ #
299
301
  # @param repo [Symbol, String, nil] defaults to config.default_repo
300
302
  # @param state [Symbol] :open, :closed, or :all
301
303
  # @param labels [Array<String>]
304
+ # @param priority_list [Boolean, nil] when +true+, restricts to issues whose +Priority List+ issue field is
305
+ # +Yes+ (server-side filter via the +issue_field_values+ query param). +false+ raises +ArgumentError+ -- GitHub
306
+ # has no negation qualifier. Silently dropped when +config.issue_fields_enabled+ is +false+.
302
307
  # @param page [Integer]
303
308
  # @param per_page [Integer]
304
309
  #
305
310
  # @return [Array<PlanMyStuff::Issue>]
306
311
  #
307
- def list(repo: nil, state: :open, labels: [], page: 1, per_page: 25)
312
+ def list(repo: nil, state: :open, labels: [], priority_list: nil, page: 1, per_page: 25)
313
+ if priority_list == false
314
+ raise(ArgumentError, 'priority_list: false is not supported (no GitHub negation qualifier)')
315
+ end
316
+
308
317
  client = PlanMyStuff.client
309
318
  resolved_repo = client.resolve_repo!(repo)
310
319
 
311
320
  params = { state: state.to_s, page: page, per_page: per_page }
312
321
  params[:labels] = labels.sort.join(',') if labels.present?
322
+ if priority_list && PlanMyStuff.configuration.issue_fields_enabled
323
+ params[:issue_field_values] = 'priority-list:Yes'
324
+ end
313
325
 
314
326
  github_issues = client.rest(:list_issues, resolved_repo, **params)
315
327
  filtered = github_issues.reject { |gi| gi.respond_to?(:pull_request) && gi.pull_request }
316
328
  filtered.map { |gi| build(gi, repo: resolved_repo) }
317
329
  end
318
330
 
331
+ # Convenience shortcut for +list(priority_list: true, ...)+. See +.list+ for parameter semantics.
332
+ #
333
+ # @return [Array<PlanMyStuff::Issue>]
334
+ #
335
+ def priority_list(**)
336
+ list(**, priority_list: true)
337
+ end
338
+
319
339
  # Counts GitHub issues matching the given filters without paginating full payloads.
320
340
  #
321
341
  # Uses GitHub's Search API (+search/issues+), which returns +total_count+ in a single
@@ -327,13 +347,23 @@ module PlanMyStuff
327
347
  # - The Search API has its own rate limit (30 req/min authenticated) separate from
328
348
  # the core REST API.
329
349
  #
350
+ # @raise [ArgumentError] when +priority_list: false+ is passed
351
+ #
330
352
  # @param repo [Symbol, String, nil] defaults to config.default_repo
331
353
  # @param state [Symbol] :open, :closed, or :all
332
354
  # @param labels [Array<String>]
355
+ # @param priority_list [Boolean, nil] when +true+, restricts to issues whose +Priority List+ issue field is
356
+ # +Yes+ (server-side filter via the +field.priority-list:Yes+ Search qualifier). +false+ raises
357
+ # +ArgumentError+ -- GitHub has no negation qualifier. Silently dropped when
358
+ # +config.issue_fields_enabled+ is +false+.
333
359
  #
334
360
  # @return [Integer]
335
361
  #
336
- def count(repo: nil, state: :open, labels: [])
362
+ def count(repo: nil, state: :open, labels: [], priority_list: nil)
363
+ if priority_list == false
364
+ raise(ArgumentError, 'priority_list: false is not supported (no GitHub negation qualifier)')
365
+ end
366
+
337
367
  client = PlanMyStuff.client
338
368
  resolved_repo = client.resolve_repo!(repo)
339
369
 
@@ -344,7 +374,12 @@ module PlanMyStuff
344
374
  qualifiers += labels_to_use.map do |label|
345
375
  "label:\"#{label}\""
346
376
  end
347
- client.rest(:search_issues, qualifiers.join(' '), per_page: 1).total_count
377
+ search_options = { per_page: 1 }
378
+ if priority_list && PlanMyStuff.configuration.issue_fields_enabled
379
+ qualifiers << 'field.priority-list:Yes'
380
+ search_options[:advanced_search] = true
381
+ end
382
+ client.rest(:search_issues, qualifiers.join(' '), **search_options).total_count
348
383
  end
349
384
 
350
385
  # Submits one or more pre-built payloads to GitHub's "Import Issues" preview endpoint
@@ -663,6 +698,8 @@ module PlanMyStuff
663
698
  # @return [self]
664
699
  #
665
700
  def save!(user: nil, skip_notification: false)
701
+ forward_legacy_priority_list_metadata!
702
+
666
703
  if new_record?
667
704
  created = self.class.create!(
668
705
  title: title,
@@ -795,6 +832,16 @@ module PlanMyStuff
795
832
  @issue_fields ||= load_issue_fields!
796
833
  end
797
834
 
835
+ # @return [Boolean]
836
+ def priority_list?
837
+ issue_fields['Priority List'] == 'Yes'
838
+ end
839
+
840
+ # @return [Integer, nil]
841
+ def priority_list_priority
842
+ issue_fields['Priority List Priority']
843
+ end
844
+
798
845
  # Bulk-updates GitHub Issue Field values in a single +setIssueFieldValue+ mutation. Each key is the field display
799
846
  # name; values are coerced to the right input fragment based on the field's type. Passing +nil+ as a value clears
800
847
  # that field.
@@ -934,6 +981,28 @@ module PlanMyStuff
934
981
  reload
935
982
  end
936
983
 
984
+ # Forwards a pending legacy +metadata.priority_list+ /
985
+ # +#priority_list_priority+ write into +@pending_issue_fields+ so the next
986
+ # +save!+ persists the values into the +Priority List+ /
987
+ # +Priority List Priority+ GitHub Issue Fields. Caller-supplied
988
+ # +issue_fields:+ entries win on key collision. Silently skipped when
989
+ # +config.issue_fields_enabled+ is +false+.
990
+ #
991
+ # @return [void]
992
+ #
993
+ def forward_legacy_priority_list_metadata!
994
+ return unless PlanMyStuff.configuration.issue_fields_enabled
995
+ return unless metadata.instance_variable_get(:@legacy_priority_list_pending)
996
+
997
+ legacy_pl = metadata.instance_variable_get(:@priority_list)
998
+ legacy_plp = metadata.instance_variable_get(:@priority_list_priority)
999
+
1000
+ legacy_fields = { 'Priority List' => legacy_pl ? 'Yes' : nil }
1001
+ legacy_fields['Priority List Priority'] = legacy_plp unless legacy_plp == -1
1002
+
1003
+ @pending_issue_fields = legacy_fields.merge(@pending_issue_fields || {})
1004
+ end
1005
+
937
1006
  # Applies in-memory updates from an +update!+ kwargs hash. Top-level scalars go through their setters so
938
1007
  # +@body_dirty+ and friends stay in sync; +metadata:+ is merged into +@metadata+ (top-level attrs assigned
939
1008
  # directly, custom_fields merged key-by-key).
@@ -2,14 +2,16 @@
2
2
 
3
3
  module PlanMyStuff
4
4
  class IssueMetadata < PlanMyStuff::BaseMetadata
5
+ PRIORITY_LIST_METADATA_DEPRECATION =
6
+ 'PlanMyStuff: IssueMetadata#priority_list / #priority_list_priority are deprecated. priority_list moved to ' \
7
+ "GitHub Issue Fields ('Priority List' single_select, 'Priority List Priority' number) in 0.20.0. Reads " \
8
+ 'now come from issue_fields; legacy writes forward to set_issue_fields! on save. The metadata accessors ' \
9
+ 'will be removed in 1.0.0.'
10
+
5
11
  # @return [Time, nil] first support action timestamp, nil until set
6
12
  attr_accessor :responded_at
7
13
  # @return [String, nil] user-facing URL in the consuming app
8
14
  attr_accessor :issues_url
9
- # @return [Boolean] whether this issue appears on the priority dashboard
10
- attr_accessor :priority_list
11
- # @return [Integer] sort order on priority dashboard (-1 = unranked)
12
- attr_accessor :priority_list_priority
13
15
  # @return [Array<Integer>] user IDs of non-support users allowed to view internal comments
14
16
  attr_accessor :visibility_allowlist
15
17
  # @return [String, nil] merged PR commit SHA for release tracking
@@ -50,8 +52,14 @@ module PlanMyStuff
50
52
 
51
53
  metadata.responded_at = parse_time(hash[:responded_at])
52
54
  metadata.issues_url = hash[:issues_url]
53
- metadata.priority_list = hash.fetch(:priority_list, false)
54
- metadata.priority_list_priority = hash.fetch(:priority_list_priority, -1)
55
+ if hash.key?(:priority_list) || hash.key?(:priority_list_priority)
56
+ PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
57
+ metadata.instance_variable_set(:@priority_list, hash[:priority_list]) if hash.key?(:priority_list)
58
+ if hash.key?(:priority_list_priority)
59
+ metadata.instance_variable_set(:@priority_list_priority, hash[:priority_list_priority])
60
+ end
61
+ metadata.instance_variable_set(:@legacy_priority_list_pending, true)
62
+ end
55
63
  metadata.visibility_allowlist = Array.wrap(hash[:visibility_allowlist])
56
64
  metadata.commit_sha = hash[:commit_sha]
57
65
  metadata.auto_complete = hash.fetch(:auto_complete, true)
@@ -86,8 +94,6 @@ module PlanMyStuff
86
94
 
87
95
  metadata.responded_at = nil
88
96
  metadata.issues_url = build_issues_url(PlanMyStuff.configuration)
89
- metadata.priority_list = false
90
- metadata.priority_list_priority = -1
91
97
  metadata.visibility_allowlist = []
92
98
  metadata.commit_sha = nil
93
99
  metadata.auto_complete = true
@@ -169,8 +175,6 @@ module PlanMyStuff
169
175
 
170
176
  def initialize
171
177
  super
172
- @priority_list = false
173
- @priority_list_priority = -1
174
178
  @visibility_allowlist = []
175
179
  @auto_complete = true
176
180
  @links = []
@@ -188,9 +192,55 @@ module PlanMyStuff
188
192
  !!auto_complete
189
193
  end
190
194
 
195
+ # @deprecated Use +Issue#priority_list?+. Removed in 1.0.0.
196
+ #
197
+ # @return [Object, nil]
198
+ #
199
+ def priority_list
200
+ PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
201
+ @priority_list
202
+ end
203
+
204
+ # @deprecated Use +Issue#update!(issue_fields: { 'Priority List' => 'Yes' })+. Removed in 1.0.0.
205
+ #
206
+ # @param value [Object]
207
+ #
208
+ # @return [Object]
209
+ #
210
+ def priority_list=(value)
211
+ PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
212
+ @legacy_priority_list_pending = true
213
+ @priority_list = value
214
+ end
215
+
216
+ # @deprecated Use +Issue#priority_list?+. Removed in 1.0.0.
217
+ #
191
218
  # @return [Boolean]
219
+ #
192
220
  def priority_list?
193
- !!priority_list
221
+ PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
222
+ !!@priority_list
223
+ end
224
+
225
+ # @deprecated Use +Issue#priority_list_priority+. Removed in 1.0.0.
226
+ #
227
+ # @return [Object, nil]
228
+ #
229
+ def priority_list_priority
230
+ PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
231
+ @priority_list_priority
232
+ end
233
+
234
+ # @deprecated Use +Issue#update!(issue_fields: { 'Priority List Priority' => N })+. Removed in 1.0.0.
235
+ #
236
+ # @param value [Object]
237
+ #
238
+ # @return [Object]
239
+ #
240
+ def priority_list_priority=(value)
241
+ PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
242
+ @legacy_priority_list_pending = true
243
+ @priority_list_priority = value
194
244
  end
195
245
 
196
246
  # @return [Boolean]
@@ -220,8 +270,6 @@ module PlanMyStuff
220
270
  super.merge(
221
271
  responded_at: PlanMyStuff.format_time(responded_at),
222
272
  issues_url: issues_url,
223
- priority_list: priority_list,
224
- priority_list_priority: priority_list_priority,
225
273
  visibility_allowlist: visibility_allowlist,
226
274
  commit_sha: commit_sha,
227
275
  auto_complete: auto_complete,
@@ -18,6 +18,15 @@ module PlanMyStuff
18
18
  </details>\n*
19
19
  }mx
20
20
 
21
+ # Visible attachments block emitted after the metadata block when the
22
+ # metadata carries non-empty +attachments+ (issue #70). Parse strips it
23
+ # so round-trips stay clean.
24
+ ATTACHMENTS_PATTERN = %r{
25
+ \A<details><summary>attachments\ \(\d+\)</summary>\n\n
26
+ .*?\n\n
27
+ </details>\n*
28
+ }mx
29
+
21
30
  # Legacy format kept for parsing only - existing issues serialized before 0.17.0
22
31
  # used a hidden HTML comment. They migrate to the new format on the next write.
23
32
  LEGACY_METADATA_PATTERN = /\A<!-- pms-metadata:(.*?) -->\n*/m
@@ -40,7 +49,7 @@ module PlanMyStuff
40
49
 
41
50
  match = raw_body.match(pattern)
42
51
  metadata = JSON.parse(match[1], symbolize_names: true)
43
- body = raw_body.sub(pattern, '')
52
+ body = raw_body.sub(pattern, '').sub(ATTACHMENTS_PATTERN, '')
44
53
 
45
54
  { metadata: metadata, body: body }
46
55
  rescue JSON::ParserError
@@ -61,14 +70,37 @@ module PlanMyStuff
61
70
  raise(ArgumentError, "metadata must be a Hash or PlanMyStuff::CustomFields, got #{metadata.class}")
62
71
  end
63
72
 
64
- json =
65
- if metadata.is_a?(PlanMyStuff::CustomFields)
66
- JSON.pretty_generate(metadata.to_h)
67
- else
68
- JSON.pretty_generate(metadata)
69
- end
73
+ hash = metadata.is_a?(PlanMyStuff::CustomFields) ? metadata.to_h : metadata
74
+ json = JSON.pretty_generate(hash)
75
+
76
+ "<details><summary>pms-metadata</summary>\n\n```json\n#{json}\n```\n\n</details>\n\n" \
77
+ "#{attachments_block(hash)}#{body}"
78
+ end
70
79
 
71
- "<details><summary>pms-metadata</summary>\n\n```json\n#{json}\n```\n\n</details>\n\n#{body}"
80
+ # Renders the visible attachments block when +metadata+ carries
81
+ # non-empty +:attachments+, otherwise returns an empty string.
82
+ #
83
+ # @param metadata [Hash]
84
+ #
85
+ # @return [String]
86
+ #
87
+ def attachments_block(metadata)
88
+ attachments = metadata[:attachments]
89
+ return '' if attachments.blank?
90
+
91
+ lines = attachments.map { |a| attachment_line(a) }.join("\n")
92
+ "<details><summary>attachments (#{attachments.size})</summary>\n\n#{lines}\n\n</details>\n\n"
93
+ end
94
+
95
+ # @param attachment [Hash{Symbol=>String}]
96
+ #
97
+ # @return [String]
98
+ #
99
+ def attachment_line(attachment)
100
+ url = "https://github.com/#{attachment[:owner]}/#{attachment[:repo]}" \
101
+ "/blob/#{attachment[:sha]}/#{attachment[:path]}"
102
+ safe_filename = attachment[:filename].to_s.gsub(']', '\]')
103
+ "- [#{safe_filename}](#{url})"
72
104
  end
73
105
 
74
106
  # @param raw_body [String]
@@ -3,7 +3,7 @@
3
3
  module PlanMyStuff
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 19
6
+ MINOR = 21
7
7
  TINY = 0
8
8
 
9
9
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
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.19.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brands Insurance