plan_my_stuff 0.1.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 +7 -0
- data/LICENSE +28 -0
- data/README.md +284 -0
- data/app/controllers/plan_my_stuff/application_controller.rb +76 -0
- data/app/controllers/plan_my_stuff/comments_controller.rb +82 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +145 -0
- data/app/controllers/plan_my_stuff/labels_controller.rb +30 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +93 -0
- data/app/controllers/plan_my_stuff/projects_controller.rb +17 -0
- data/app/views/plan_my_stuff/comments/edit.html.erb +16 -0
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +32 -0
- data/app/views/plan_my_stuff/issues/edit.html.erb +12 -0
- data/app/views/plan_my_stuff/issues/index.html.erb +37 -0
- data/app/views/plan_my_stuff/issues/new.html.erb +7 -0
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +41 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +23 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +32 -0
- data/app/views/plan_my_stuff/issues/show.html.erb +58 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +13 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +101 -0
- data/config/routes.rb +25 -0
- data/lib/generators/plan_my_stuff/install/install_generator.rb +38 -0
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +106 -0
- data/lib/generators/plan_my_stuff/views/views_generator.rb +22 -0
- data/lib/plan_my_stuff/application_record.rb +39 -0
- data/lib/plan_my_stuff/base_metadata.rb +136 -0
- data/lib/plan_my_stuff/client.rb +143 -0
- data/lib/plan_my_stuff/comment.rb +360 -0
- data/lib/plan_my_stuff/comment_metadata.rb +56 -0
- data/lib/plan_my_stuff/configuration.rb +139 -0
- data/lib/plan_my_stuff/custom_fields.rb +65 -0
- data/lib/plan_my_stuff/engine.rb +11 -0
- data/lib/plan_my_stuff/errors.rb +87 -0
- data/lib/plan_my_stuff/issue.rb +486 -0
- data/lib/plan_my_stuff/issue_metadata.rb +111 -0
- data/lib/plan_my_stuff/label.rb +59 -0
- data/lib/plan_my_stuff/markdown.rb +83 -0
- data/lib/plan_my_stuff/metadata_parser.rb +53 -0
- data/lib/plan_my_stuff/project.rb +504 -0
- data/lib/plan_my_stuff/project_item.rb +414 -0
- data/lib/plan_my_stuff/test_helpers.rb +501 -0
- data/lib/plan_my_stuff/user_resolver.rb +61 -0
- data/lib/plan_my_stuff/verifier.rb +102 -0
- data/lib/plan_my_stuff/version.rb +19 -0
- data/lib/plan_my_stuff.rb +69 -0
- data/lib/tasks/plan_my_stuff.rake +23 -0
- metadata +126 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 03731d4c3b552639604aa37465eb9f2246eb6077ec40144b218b4afa2aefdc01
|
|
4
|
+
data.tar.gz: 0277ebbdc6a0f783c90cb1bcf2d73c6dfd79447ad52f1df8d547d217cf80e1bf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3897dad435fd7b13780d3ac6ac3e25f9d15e49f58e8517c05ff6b28b78b7efd16216e4c9dbf0275afadf842821a9a006dd0e1c86b862e10927e197f190029f6a
|
|
7
|
+
data.tar.gz: 1a973153e39fc469580eac16dca37debb9780416b1cc7b50f9dffd440a6f64f4383fca30f24e72fdc02173c5875938c9181febd0226de8ecc8023c9907ac10db
|
data/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Brands Insurance Agency, Inc.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# plan_my_stuff
|
|
2
|
+
|
|
3
|
+
<!-- Badges (update once CI and RubyGems are configured) -->
|
|
4
|
+
<!--  -->
|
|
5
|
+
<!--  -->
|
|
6
|
+
<!--  -->
|
|
7
|
+
|
|
8
|
+
A Rails engine gem that provides a GitHub-backed ticketing and project tracking layer for internal Rails apps. Wraps GitHub Issues and Projects V2 via Octokit, handling issue CRUD, comments with visibility controls, project board management, and a configurable request gateway.
|
|
9
|
+
|
|
10
|
+
**Ruby:** >= 3.3 | **Rails:** >= 6.1, < 8 | **API client:** Octokit
|
|
11
|
+
|
|
12
|
+
> [!IMPORTANT]
|
|
13
|
+
> **commonmarker version compatibility:** This gem requires commonmarker v1.0+ (pinned `~> 2.7`). The v0.23 to v1.0 upgrade was a full rewrite with breaking changes:
|
|
14
|
+
> - Module renamed: `CommonMarker` to `Commonmarker`
|
|
15
|
+
> - Methods renamed: `render_html` to `to_html`, `render_doc` to `parse`
|
|
16
|
+
> - Options changed from array of symbols (`[:HARDBREAKS]`) to nested hash (`options: { render: { hardbreaks: true } }`)
|
|
17
|
+
> - Underlying engine changed from C (libcmark-gfm) to Rust (comrak)
|
|
18
|
+
>
|
|
19
|
+
> If your app already uses commonmarker < 1.0, you will need to upgrade. Alternatively, configure `markdown_renderer = :redcarpet` or `nil` to avoid the dependency entirely.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Add to your Gemfile:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'plan_my_stuff'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then run:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bundle install
|
|
33
|
+
rails generate plan_my_stuff:install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This creates `config/initializers/plan_my_stuff.rb` with documented configuration options.
|
|
37
|
+
|
|
38
|
+
Mount the engine in `config/routes.rb`:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
mount PlanMyStuff::Engine, at: '/tickets'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Markdown renderer
|
|
45
|
+
|
|
46
|
+
The gem supports three markdown rendering options. The chosen gem must be in your Gemfile - `plan_my_stuff` does not declare either as a runtime dependency.
|
|
47
|
+
|
|
48
|
+
| Option | Gemfile requirement | Config |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| commonmarker (default) | `gem 'commonmarker', '~> 2.7'` | `config.markdown_renderer = :commonmarker` |
|
|
51
|
+
| redcarpet | `gem 'redcarpet'` | `config.markdown_renderer = :redcarpet` |
|
|
52
|
+
| None (raw) | Nothing | `config.markdown_renderer = nil` |
|
|
53
|
+
|
|
54
|
+
### Overriding views
|
|
55
|
+
|
|
56
|
+
Copy the default views into your app for customization:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
rails generate plan_my_stuff:views
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# config/initializers/plan_my_stuff.rb
|
|
66
|
+
PlanMyStuff.configure do |config|
|
|
67
|
+
# Auth (PAT from a bot account with repo + project scopes)
|
|
68
|
+
config.access_token = ENV['PMS_GITHUB_TOKEN']
|
|
69
|
+
|
|
70
|
+
# Organization
|
|
71
|
+
config.organization = 'YourOrganization'
|
|
72
|
+
|
|
73
|
+
# Named repo configs
|
|
74
|
+
config.repos[:marketing_website] = 'YourOrganization/MarketingWebsite'
|
|
75
|
+
config.repos[:cms_website] = 'YourOrganization/CMSWebsite'
|
|
76
|
+
config.default_repo = :cms_website
|
|
77
|
+
|
|
78
|
+
# Default project board
|
|
79
|
+
config.default_project_number = 123
|
|
80
|
+
|
|
81
|
+
# User class (your app's model)
|
|
82
|
+
config.user_class = 'User'
|
|
83
|
+
config.display_name_method = :full_name
|
|
84
|
+
config.user_id_method = :id
|
|
85
|
+
|
|
86
|
+
# Support role check (symbol or proc)
|
|
87
|
+
config.support_method = :support?
|
|
88
|
+
|
|
89
|
+
# Markdown rendering (:commonmarker, :redcarpet, or nil)
|
|
90
|
+
config.markdown_renderer = :commonmarker
|
|
91
|
+
|
|
92
|
+
# Request gateway (proc or nil; nil = always send)
|
|
93
|
+
config.should_send_request = nil
|
|
94
|
+
|
|
95
|
+
# Background jobs (when gateway defers a request)
|
|
96
|
+
config.job_classes = {
|
|
97
|
+
create_ticket: 'PmsCreateTicketJob',
|
|
98
|
+
post_comment: 'PmsPostCommentJob',
|
|
99
|
+
update_status: 'PmsUpdateStatusJob'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Deferred request notifications
|
|
103
|
+
config.deferred_notifier = nil
|
|
104
|
+
config.deferred_email_from = 'noreply@example.com'
|
|
105
|
+
config.deferred_email_to = 'support@example.com'
|
|
106
|
+
|
|
107
|
+
# Custom fields (stored in issue/comment metadata)
|
|
108
|
+
config.custom_fields = {
|
|
109
|
+
ticket_type: { type: :string },
|
|
110
|
+
notification_recipients: { type: :array }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# App name (appears in metadata)
|
|
114
|
+
config.app_name = 'MyApp'
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The `PMS` alias is available for brevity: `PMS.configure`, `PMS::Issue.find`, etc.
|
|
119
|
+
|
|
120
|
+
## Usage
|
|
121
|
+
|
|
122
|
+
### Issues
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# Create
|
|
126
|
+
issue = PMS::Issue.create!(
|
|
127
|
+
title: 'Login page broken on Safari',
|
|
128
|
+
body: 'Users report blank screen after clicking login...',
|
|
129
|
+
repo: :cms_website,
|
|
130
|
+
labels: ['bug', 'app:cms_website'],
|
|
131
|
+
user: current_user,
|
|
132
|
+
add_to_project: true
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Find
|
|
136
|
+
issue = PMS::Issue.find(repo: :cms_website, number: 123)
|
|
137
|
+
issue.title
|
|
138
|
+
issue.body # body without metadata
|
|
139
|
+
issue.metadata # PlanMyStuff::IssueMetadata
|
|
140
|
+
issue.visible_to?(user) # visibility check
|
|
141
|
+
issue.comments # all comments
|
|
142
|
+
issue.pms_comments # only PMS-created comments
|
|
143
|
+
|
|
144
|
+
# List
|
|
145
|
+
issues = PMS::Issue.list(repo: :cms_website, state: :open, labels: ['bug'])
|
|
146
|
+
|
|
147
|
+
# Update
|
|
148
|
+
issue.update!(title: 'Updated title', labels: ['bug', 'P1'])
|
|
149
|
+
|
|
150
|
+
# Close / Reopen
|
|
151
|
+
issue.update!(state: :closed)
|
|
152
|
+
issue.update!(state: :open)
|
|
153
|
+
|
|
154
|
+
# Viewer management (visibility allowlist)
|
|
155
|
+
PMS::Issue.add_viewers(repo: :cms_website, number: 123, user_ids: [5, 12])
|
|
156
|
+
PMS::Issue.remove_viewers(repo: :cms_website, number: 123, user_ids: [5])
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Comments
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
# Create
|
|
163
|
+
comment = PMS::Comment.create!(
|
|
164
|
+
repo: :cms_website,
|
|
165
|
+
issue_number: 123,
|
|
166
|
+
body: 'Deployed fix to staging, please retest.',
|
|
167
|
+
user: current_user,
|
|
168
|
+
visibility: :public # or :internal (support-only)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# List
|
|
172
|
+
comments = PMS::Comment.list(repo: :cms_website, issue_number: 123)
|
|
173
|
+
comments = PMS::Comment.list(repo: :cms_website, issue_number: 123, pms_only: true)
|
|
174
|
+
|
|
175
|
+
comment.body # visible text
|
|
176
|
+
comment.metadata # PlanMyStuff::CommentMetadata (nil for non-PMS comments)
|
|
177
|
+
comment.visibility # :public, :internal, or nil
|
|
178
|
+
comment.pms_comment? # true/false
|
|
179
|
+
comment.visible_to?(user)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Labels
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
PMS::Label.add(repo: :cms_website, issue_number: 123, labels: ['in-progress'])
|
|
186
|
+
PMS::Label.remove(repo: :cms_website, issue_number: 123, labels: ['triage'])
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Projects
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# List all projects
|
|
193
|
+
projects = PMS::Project.list
|
|
194
|
+
|
|
195
|
+
# View a project board
|
|
196
|
+
project = PMS::Project.find(14)
|
|
197
|
+
project.title
|
|
198
|
+
project.statuses # [{id: "ae429385", name: "On Deck"}, ...]
|
|
199
|
+
project.items # Array<PMS::ProjectItem>
|
|
200
|
+
|
|
201
|
+
# Move item to a status column
|
|
202
|
+
item = project.items.first
|
|
203
|
+
item.move_to!('In Review')
|
|
204
|
+
|
|
205
|
+
# Assign item
|
|
206
|
+
item.assign!('octocat')
|
|
207
|
+
|
|
208
|
+
# Add existing issue to project
|
|
209
|
+
PMS::Project.add_item(project_number: 14, issue_repo: :cms_website, issue_number: 123)
|
|
210
|
+
|
|
211
|
+
# Add draft item
|
|
212
|
+
PMS::Project.add_draft_item(project_number: 14, title: 'Draft task', body: 'Details...')
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Metadata
|
|
216
|
+
|
|
217
|
+
All state lives on GitHub. Issue and comment metadata is stored as a hidden HTML comment on the first line of the body:
|
|
218
|
+
|
|
219
|
+
```html
|
|
220
|
+
<!-- pms-metadata:{"gem_version":"0.0.0","app_name":"MyApp","created_at":"2026-03-24T10:00:00Z",...} -->
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This is invisible when rendered on GitHub and parsed automatically by the gem into typed objects (`PMS::IssueMetadata`, `PMS::CommentMetadata`).
|
|
224
|
+
|
|
225
|
+
## Verify setup
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
rails plan_my_stuff:verify
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Checks token validity, org access, repo access, and project access.
|
|
232
|
+
|
|
233
|
+
## Testing
|
|
234
|
+
|
|
235
|
+
The gem provides test helpers for consuming apps:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
# spec/spec_helper.rb (or rails_helper.rb)
|
|
239
|
+
require 'plan_my_stuff/test_helpers'
|
|
240
|
+
|
|
241
|
+
RSpec.configure do |config|
|
|
242
|
+
config.include PlanMyStuff::TestHelpers
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Available in tests:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
PlanMyStuff.test_mode! # stubs all API calls
|
|
250
|
+
|
|
251
|
+
build_issue(title: 'Test issue')
|
|
252
|
+
build_comment(body: 'Test comment')
|
|
253
|
+
build_project(title: 'Test board')
|
|
254
|
+
|
|
255
|
+
expect_pms_issue_created(title: 'Test issue')
|
|
256
|
+
expect_pms_comment_created(body: 'Test comment')
|
|
257
|
+
expect_pms_item_moved(status: 'In Review')
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Engine routes
|
|
261
|
+
|
|
262
|
+
The engine provides these routes (all under the configured mount point):
|
|
263
|
+
|
|
264
|
+
| Route | Action |
|
|
265
|
+
|---|---|
|
|
266
|
+
| `GET /` | Issue index |
|
|
267
|
+
| `GET /issues/new` | New issue form |
|
|
268
|
+
| `POST /issues` | Create issue |
|
|
269
|
+
| `GET /issues/:id` | Issue detail with comments |
|
|
270
|
+
| `GET /issues/:id/edit` | Edit issue form |
|
|
271
|
+
| `PATCH /issues/:id` | Update issue |
|
|
272
|
+
| `PATCH /issues/:id/close` | Close issue |
|
|
273
|
+
| `PATCH /issues/:id/reopen` | Reopen issue |
|
|
274
|
+
| `POST /issues/:id/comments` | Create comment |
|
|
275
|
+
| `GET /issues/:id/comments/:id/edit` | Edit comment |
|
|
276
|
+
| `PATCH /issues/:id/comments/:id` | Update comment |
|
|
277
|
+
| `POST /issues/:id/labels` | Add label |
|
|
278
|
+
| `DELETE /issues/:id/labels/:name` | Remove label |
|
|
279
|
+
| `GET /projects` | Project list |
|
|
280
|
+
| `GET /projects/:id` | Project board |
|
|
281
|
+
| `POST /projects/:id/items` | Add item to project |
|
|
282
|
+
| `PATCH /projects/:id/items/:id/move` | Move item |
|
|
283
|
+
| `PATCH /projects/:id/items/:id/assign` | Assign item |
|
|
284
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
class ApplicationController < ::ApplicationController
|
|
5
|
+
protect_from_forgery with: :exception
|
|
6
|
+
helper Rails.application.routes.url_helpers
|
|
7
|
+
|
|
8
|
+
before_action :authenticate_pms_user!
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Hook for consuming app authentication. Override this method in your
|
|
13
|
+
# ApplicationController, or configure via PlanMyStuff.configure:
|
|
14
|
+
#
|
|
15
|
+
# PlanMyStuff.configure do |config|
|
|
16
|
+
# config.authenticate_with do
|
|
17
|
+
# redirect_to login_path unless current_user
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
def authenticate_pms_user!
|
|
22
|
+
pms_auth = PlanMyStuff.configuration.authenticate_with
|
|
23
|
+
instance_exec(&pms_auth) if pms_auth
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the current user for PMS visibility checks.
|
|
27
|
+
# Delegates to the consuming app's current_user method.
|
|
28
|
+
#
|
|
29
|
+
# @return [Object, nil]
|
|
30
|
+
#
|
|
31
|
+
def pms_current_user
|
|
32
|
+
return current_user if respond_to?(:current_user, true)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def support_user?
|
|
37
|
+
pms_current_user.present? && PMS::UserResolver.support?(pms_current_user)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Redirects non-support users back with an error.
|
|
41
|
+
#
|
|
42
|
+
# @param path [String] redirect destination
|
|
43
|
+
# @param message [String]
|
|
44
|
+
#
|
|
45
|
+
# @return [void]
|
|
46
|
+
#
|
|
47
|
+
def redirect_to_unauthorized(path, message: 'You do not have permission to perform this action.')
|
|
48
|
+
flash[:error] = message
|
|
49
|
+
redirect_to(path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Splits a comma-separated labels string into an array.
|
|
53
|
+
#
|
|
54
|
+
# @param labels_string [String, nil]
|
|
55
|
+
#
|
|
56
|
+
# @return [Array<String>]
|
|
57
|
+
#
|
|
58
|
+
def parse_labels(labels_string)
|
|
59
|
+
return [] if labels_string.blank?
|
|
60
|
+
|
|
61
|
+
labels_string.split(',').filter_map { |l| l.strip.presence }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Parses a comma-separated string of viewer IDs into an array of integers.
|
|
65
|
+
#
|
|
66
|
+
# @param ids_string [String, nil]
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Integer>]
|
|
69
|
+
#
|
|
70
|
+
def parse_viewer_ids(ids_string)
|
|
71
|
+
return [] if ids_string.blank?
|
|
72
|
+
|
|
73
|
+
ids_string.split(',').filter_map { |id| id.strip.presence&.to_i }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
class CommentsController < ApplicationController
|
|
5
|
+
# POST /issues/:issue_id/comments
|
|
6
|
+
def create
|
|
7
|
+
@issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
|
|
8
|
+
|
|
9
|
+
PMS::Comment.create!(
|
|
10
|
+
issue: @issue,
|
|
11
|
+
body: comment_params[:body],
|
|
12
|
+
user: pms_current_user,
|
|
13
|
+
visibility: comment_params[:visibility]&.to_sym || :public,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
flash[:success] = 'Comment was successfully created.'
|
|
17
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# GET /issues/:issue_id/comments/:id/edit
|
|
21
|
+
def edit
|
|
22
|
+
load_comment
|
|
23
|
+
return unless @comment
|
|
24
|
+
|
|
25
|
+
@support_user = support_user?
|
|
26
|
+
return redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number)) unless can_edit?(@comment)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# PATCH/PUT /issues/:issue_id/comments/:id
|
|
30
|
+
def update
|
|
31
|
+
load_comment
|
|
32
|
+
return unless @comment
|
|
33
|
+
|
|
34
|
+
@support_user = support_user?
|
|
35
|
+
return redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number)) unless can_edit?(@comment)
|
|
36
|
+
|
|
37
|
+
update_attrs = { body: comment_params[:body] }
|
|
38
|
+
update_attrs[:visibility] = comment_params[:visibility].to_sym if @support_user && comment_params[:visibility]
|
|
39
|
+
|
|
40
|
+
@comment.update!(**update_attrs)
|
|
41
|
+
|
|
42
|
+
flash[:success] = 'Comment was successfully updated.'
|
|
43
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
44
|
+
rescue PMS::StaleObjectError
|
|
45
|
+
flash.now[:error] = 'Comment was modified by someone else. Please review the latest changes and try again.'
|
|
46
|
+
render(:edit, status: PMS.unprocessable_status)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# @return [ActionController::Parameters]
|
|
52
|
+
def comment_params
|
|
53
|
+
params.require(:comment).permit(:body, :visibility)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Loads the issue and comment from params.
|
|
57
|
+
#
|
|
58
|
+
# @return [void]
|
|
59
|
+
#
|
|
60
|
+
def load_comment
|
|
61
|
+
@issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
|
|
62
|
+
@comment = PMS::Comment.find(params[:id].to_i, issue: @issue)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns true if the current user can edit the given comment.
|
|
66
|
+
# Support users can edit any comment. Regular users can only edit
|
|
67
|
+
# their own comments.
|
|
68
|
+
#
|
|
69
|
+
# @param comment [PlanMyStuff::Comment]
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
#
|
|
73
|
+
def can_edit?(comment)
|
|
74
|
+
return true if support_user?
|
|
75
|
+
|
|
76
|
+
user = pms_current_user
|
|
77
|
+
return false if user.blank?
|
|
78
|
+
|
|
79
|
+
comment.metadata.created_by == PMS::UserResolver.user_id(user)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
class IssuesController < ApplicationController
|
|
5
|
+
# GET /issues
|
|
6
|
+
def index
|
|
7
|
+
@page = (params[:page] || 1).to_i
|
|
8
|
+
@per_page = (params[:per_page] || 25).to_i
|
|
9
|
+
@state = (params[:state] || 'open').to_sym
|
|
10
|
+
@labels = params[:labels].present? ? Array.wrap(params[:labels]) : []
|
|
11
|
+
@repo = params[:repo]&.to_sym
|
|
12
|
+
|
|
13
|
+
@issues = PMS::Issue.list(
|
|
14
|
+
repo: @repo,
|
|
15
|
+
state: @state,
|
|
16
|
+
labels: @labels,
|
|
17
|
+
page: @page,
|
|
18
|
+
per_page: @per_page,
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# GET /issues/new
|
|
23
|
+
def new
|
|
24
|
+
@issue = PMS::Issue.new
|
|
25
|
+
@support_user = support_user?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# POST /issues
|
|
29
|
+
def create
|
|
30
|
+
@issue = PMS::Issue.create!(
|
|
31
|
+
title: issue_params[:title],
|
|
32
|
+
body: issue_params[:body],
|
|
33
|
+
labels: parse_labels(issue_params[:labels]),
|
|
34
|
+
user: pms_current_user,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
flash[:success] = 'Issue was successfully created.'
|
|
38
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
39
|
+
rescue PMS::ValidationError => e
|
|
40
|
+
@issue = PMS::Issue.new(title: issue_params[:title], body: issue_params[:body])
|
|
41
|
+
@support_user = support_user?
|
|
42
|
+
flash.now[:error] = e.message
|
|
43
|
+
render(:new, status: PMS.unprocessable_status)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# GET /issues/:id
|
|
47
|
+
def show
|
|
48
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
|
|
49
|
+
@comments = filter_visible_comments(@issue.comments)
|
|
50
|
+
@support_user = support_user?
|
|
51
|
+
@current_user_id = pms_current_user.present? ? PMS::UserResolver.user_id(pms_current_user) : nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# GET /issues/:id/edit
|
|
55
|
+
def edit
|
|
56
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
|
|
57
|
+
@support_user = support_user?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# PATCH/PUT /issues/:id
|
|
61
|
+
def update
|
|
62
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
|
|
63
|
+
|
|
64
|
+
@issue.update!(
|
|
65
|
+
title: issue_params[:title],
|
|
66
|
+
body: issue_params[:body],
|
|
67
|
+
labels: parse_labels(issue_params[:labels]),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
flash[:success] = 'Issue was successfully updated.'
|
|
71
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
72
|
+
rescue PMS::StaleObjectError
|
|
73
|
+
@support_user = support_user?
|
|
74
|
+
flash.now[:error] = 'Issue was modified by someone else. Please review the latest changes and try again.'
|
|
75
|
+
render(:edit, status: PMS.unprocessable_status)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# PATCH /issues/:id/close
|
|
79
|
+
def close
|
|
80
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
|
|
81
|
+
@issue.update!(state: :closed)
|
|
82
|
+
|
|
83
|
+
flash[:success] = 'Issue was successfully closed.'
|
|
84
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# PATCH /issues/:id/reopen
|
|
88
|
+
def reopen
|
|
89
|
+
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
|
|
90
|
+
@issue.update!(state: :open)
|
|
91
|
+
|
|
92
|
+
flash[:success] = 'Issue was successfully reopened.'
|
|
93
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# POST /issues/:id/add_viewers
|
|
97
|
+
def add_viewers
|
|
98
|
+
return redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id])) unless support_user?
|
|
99
|
+
|
|
100
|
+
viewer_ids = parse_viewer_ids(params[:viewer_ids])
|
|
101
|
+
if viewer_ids.blank?
|
|
102
|
+
flash[:error] = 'No valid viewer IDs provided.'
|
|
103
|
+
redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
PMS::Issue.add_viewers(number: params[:id].to_i, user_ids: viewer_ids, repo: params[:repo]&.to_sym)
|
|
108
|
+
|
|
109
|
+
flash[:success] = 'Viewers were successfully added.'
|
|
110
|
+
redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# DELETE /issues/:id/remove_viewer
|
|
114
|
+
def remove_viewer
|
|
115
|
+
return redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id])) unless support_user?
|
|
116
|
+
|
|
117
|
+
viewer_id = params[:viewer_id].to_i
|
|
118
|
+
PMS::Issue.remove_viewers(number: params[:id].to_i, user_ids: [viewer_id], repo: params[:repo]&.to_sym)
|
|
119
|
+
|
|
120
|
+
flash[:success] = 'Viewer was successfully removed.'
|
|
121
|
+
redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# @return [ActionController::Parameters]
|
|
127
|
+
def issue_params
|
|
128
|
+
params.require(:issue).permit(:title, :body, :labels)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Filters comments to only those visible to the current user.
|
|
132
|
+
# Falls back to showing all comments if no current_user is available.
|
|
133
|
+
#
|
|
134
|
+
# @param comments [Array<PlanMyStuff::Comment>]
|
|
135
|
+
#
|
|
136
|
+
# @return [Array<PlanMyStuff::Comment>]
|
|
137
|
+
#
|
|
138
|
+
def filter_visible_comments(comments)
|
|
139
|
+
user = pms_current_user
|
|
140
|
+
return comments unless user
|
|
141
|
+
|
|
142
|
+
comments.select { |comment| comment.visible_to?(user) }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
class LabelsController < ApplicationController
|
|
5
|
+
# POST /issues/:issue_id/labels
|
|
6
|
+
def add_to_issue
|
|
7
|
+
labels = parse_labels(params[:label_name])
|
|
8
|
+
if labels.blank?
|
|
9
|
+
flash[:error] = 'Label name is required.'
|
|
10
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
|
|
15
|
+
PMS::Label.add(issue: issue, labels: labels)
|
|
16
|
+
|
|
17
|
+
flash[:success] = 'Label was successfully added.'
|
|
18
|
+
redirect_to(plan_my_stuff.issue_path(issue.number))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# DELETE /issues/:issue_id/labels/:name
|
|
22
|
+
def remove_from_issue
|
|
23
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
|
|
24
|
+
PMS::Label.remove(issue: issue, labels: [params[:name]])
|
|
25
|
+
|
|
26
|
+
flash[:success] = 'Label was successfully removed.'
|
|
27
|
+
redirect_to(plan_my_stuff.issue_path(issue.number))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|