plan_my_stuff 0.9.0 → 0.10.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.
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module IssueExtractions
5
+ module Waiting
6
+ # Marks the issue as waiting on an end-user reply. Sets +metadata.waiting_on_user_at+ to now, (re)computes
7
+ # +metadata.next_reminder_at+, and adds the configured +waiting_on_user_label+ to the issue. Called from
8
+ # +Comment.create!+ when a support user posts a comment with +waiting_on_reply: true+, and from the
9
+ # +Issues::WaitingsController+ toggle.
10
+ #
11
+ # @param user [Object, nil] actor for the label notification event
12
+ #
13
+ # @return [self]
14
+ #
15
+ def enter_waiting_on_user!(user: nil)
16
+ now = Time.now.utc
17
+ label = PlanMyStuff.configuration.waiting_on_user_label
18
+
19
+ PlanMyStuff::Label.ensure!(repo: repo, name: label)
20
+ PlanMyStuff::Label.add!(issue: self, labels: [label], user: user) if labels.exclude?(label)
21
+
22
+ self.class.update!(
23
+ number: number,
24
+ repo: repo,
25
+ metadata: {
26
+ waiting_on_user_at: PlanMyStuff.format_time(now),
27
+ next_reminder_at: format_next_reminder_at(from: now),
28
+ },
29
+ )
30
+ reload
31
+ end
32
+
33
+ # Clears the waiting-on-user state: removes the label, clears +metadata.waiting_on_user_at+, and clears
34
+ # +metadata.next_reminder_at+ unless a waiting-on-approval timer is still active. No-ops if the issue is not
35
+ # currently waiting on a user reply.
36
+ #
37
+ # @return [self]
38
+ #
39
+ def clear_waiting_on_user!
40
+ label = PlanMyStuff.configuration.waiting_on_user_label
41
+ return self if metadata.waiting_on_user_at.nil? && labels.exclude?(label)
42
+
43
+ PlanMyStuff::Label.remove!(issue: self, labels: [label]) if labels.include?(label)
44
+
45
+ self.class.update!(
46
+ number: number,
47
+ repo: repo,
48
+ metadata: {
49
+ waiting_on_user_at: nil,
50
+ next_reminder_at:
51
+ metadata.waiting_on_approval_at ? PlanMyStuff.format_time(metadata.next_reminder_at) : nil,
52
+ },
53
+ )
54
+ reload
55
+ end
56
+
57
+ # Reopens an issue that was auto-closed by the inactivity sweep, clears +metadata.closed_by_inactivity+, and
58
+ # emits +plan_my_stuff.issue.reopened_by_reply+ carrying the reopening comment. Does not emit the regular
59
+ # +issue.reopened+ event \- subscribers that specifically care about this flow subscribe to the dedicated event.
60
+ #
61
+ # @param comment [PlanMyStuff::Comment] the reopening comment
62
+ # @param user [Object, nil] actor for the notification event
63
+ #
64
+ # @return [self]
65
+ #
66
+ def reopen_by_reply!(comment:, user: nil)
67
+ inactive_label = PlanMyStuff.configuration.user_inactive_label
68
+ PlanMyStuff::Label.remove!(issue: self, labels: [inactive_label]) if labels.include?(inactive_label)
69
+
70
+ self.class.update!(
71
+ number: number,
72
+ repo: repo,
73
+ state: :open,
74
+ metadata: { closed_by_inactivity: false },
75
+ )
76
+ reload
77
+
78
+ PlanMyStuff::Notifications.instrument(
79
+ 'issue.reopened_by_reply',
80
+ self,
81
+ user: user,
82
+ comment: comment,
83
+ )
84
+ self
85
+ end
86
+
87
+ private
88
+
89
+ # Formats the next reminder time as an ISO 8601 UTC string, using per-issue +metadata.reminder_days+ when set
90
+ # or +config.reminder_days+ otherwise. Returns +nil+ when the effective schedule is empty.
91
+ #
92
+ # @param from [Time] baseline timestamp
93
+ #
94
+ # @return [String, nil]
95
+ #
96
+ def format_next_reminder_at(from:)
97
+ days = metadata.reminder_days.presence || PlanMyStuff.configuration.reminder_days
98
+ return if days.empty?
99
+
100
+ PlanMyStuff.format_time(from + days.first.days)
101
+ end
102
+
103
+ # When an issue is transitioning from open to closed, strips both waiting labels from the outgoing labels
104
+ # array and clears the waiting-related timestamps on +metadata+ so a single save writes both state change and
105
+ # cleanup. No-op for any other transition.
106
+ #
107
+ # @param attrs [Hash] the kwargs hash being assembled for +Issue.update!+; mutated in place
108
+ #
109
+ # @return [void]
110
+ #
111
+ def clear_waiting_state_on_close(attrs)
112
+ return unless state_changed?
113
+ return unless state_was == 'open'
114
+ return unless state == 'closed'
115
+
116
+ return if metadata.waiting_on_user_at.blank? && metadata.waiting_on_approval_at.blank?
117
+
118
+ waiting_labels = [
119
+ PlanMyStuff.configuration.waiting_on_user_label,
120
+ PlanMyStuff.configuration.waiting_on_approval_label,
121
+ ]
122
+ attrs[:labels] = Array.wrap(attrs[:labels]) - waiting_labels
123
+
124
+ metadata.waiting_on_user_at = nil
125
+ metadata.waiting_on_approval_at = nil
126
+ metadata.next_reminder_at = nil
127
+ end
128
+
129
+ # When an inactivity-closed issue is being reopened, strips the +user_inactive_label+ from the outgoing labels
130
+ # and clears +metadata.closed_by_inactivity+ so the save writes both. No-op for any other transition or for
131
+ # reopens of non-inactive closes.
132
+ #
133
+ # @param attrs [Hash] the kwargs hash being assembled for +Issue.update!+; mutated in place
134
+ #
135
+ # @return [void]
136
+ #
137
+ def clear_inactivity_state_on_reopen(attrs)
138
+ return unless state_changed?
139
+ return unless state_was == 'closed'
140
+ return unless state == 'open'
141
+ return unless metadata.closed_by_inactivity
142
+
143
+ attrs[:labels] = Array.wrap(attrs[:labels]) - [PlanMyStuff.configuration.user_inactive_label]
144
+ metadata.closed_by_inactivity = false
145
+ end
146
+ end
147
+ end
148
+ end
@@ -3,7 +3,7 @@
3
3
  module PlanMyStuff
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 9
6
+ MINOR = 10
7
7
  TINY = 0
8
8
 
9
9
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plan_my_stuff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brands Insurance
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-01 00:00:00.000000000 Z
11
+ date: 2026-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -113,6 +113,7 @@ files:
113
113
  - lib/plan_my_stuff/aws_sns_simulator.rb
114
114
  - lib/plan_my_stuff/base_metadata.rb
115
115
  - lib/plan_my_stuff/base_project.rb
116
+ - lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb
116
117
  - lib/plan_my_stuff/base_project_item.rb
117
118
  - lib/plan_my_stuff/base_project_metadata.rb
118
119
  - lib/plan_my_stuff/cache.rb
@@ -125,6 +126,10 @@ files:
125
126
  - lib/plan_my_stuff/errors.rb
126
127
  - lib/plan_my_stuff/graphql/queries.rb
127
128
  - lib/plan_my_stuff/issue.rb
129
+ - lib/plan_my_stuff/issue_extractions/approvals.rb
130
+ - lib/plan_my_stuff/issue_extractions/links.rb
131
+ - lib/plan_my_stuff/issue_extractions/viewers.rb
132
+ - lib/plan_my_stuff/issue_extractions/waiting.rb
128
133
  - lib/plan_my_stuff/issue_metadata.rb
129
134
  - lib/plan_my_stuff/label.rb
130
135
  - lib/plan_my_stuff/link.rb