plan_my_stuff 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/app/controllers/plan_my_stuff/application_controller.rb +4 -1
  4. data/app/controllers/plan_my_stuff/comments_controller.rb +24 -6
  5. data/app/controllers/plan_my_stuff/issues_controller.rb +23 -17
  6. data/app/controllers/plan_my_stuff/labels_controller.rb +5 -5
  7. data/app/controllers/plan_my_stuff/project_items_controller.rb +6 -0
  8. data/app/controllers/plan_my_stuff/projects_controller.rb +54 -0
  9. data/app/views/plan_my_stuff/issues/index.html.erb +4 -4
  10. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +10 -6
  11. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -2
  12. data/app/views/plan_my_stuff/issues/show.html.erb +4 -4
  13. data/app/views/plan_my_stuff/projects/edit.html.erb +7 -0
  14. data/app/views/plan_my_stuff/projects/index.html.erb +2 -0
  15. data/app/views/plan_my_stuff/projects/new.html.erb +7 -0
  16. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +29 -0
  17. data/app/views/plan_my_stuff/projects/show.html.erb +6 -4
  18. data/config/routes.rb +1 -1
  19. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +10 -1
  20. data/lib/plan_my_stuff/application_record.rb +37 -1
  21. data/lib/plan_my_stuff/base_metadata.rb +23 -15
  22. data/lib/plan_my_stuff/client.rb +2 -22
  23. data/lib/plan_my_stuff/comment.rb +22 -8
  24. data/lib/plan_my_stuff/comment_metadata.rb +8 -2
  25. data/lib/plan_my_stuff/configuration.rb +82 -1
  26. data/lib/plan_my_stuff/custom_fields.rb +70 -0
  27. data/lib/plan_my_stuff/issue.rb +23 -19
  28. data/lib/plan_my_stuff/issue_metadata.rb +8 -2
  29. data/lib/plan_my_stuff/markdown.rb +1 -1
  30. data/lib/plan_my_stuff/project.rb +280 -19
  31. data/lib/plan_my_stuff/project_item.rb +19 -11
  32. data/lib/plan_my_stuff/project_metadata.rb +41 -0
  33. data/lib/plan_my_stuff/repo.rb +107 -0
  34. data/lib/plan_my_stuff/test_helpers.rb +10 -2
  35. data/lib/plan_my_stuff/version.rb +2 -2
  36. data/lib/plan_my_stuff.rb +2 -0
  37. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b39dc4e08c17c126407b4b96f698d4718b77f9485f251af2f7a1c642e58a1b8
4
- data.tar.gz: 335dbb910692194950e6ac9bcc904d302b67ddf34d4cf0e21611dd3bf36d675d
3
+ metadata.gz: 9b5b0d6a9e0582853ebf435a48b49fabb8dc9bd1227bddc6f2c3467d74872c80
4
+ data.tar.gz: 3e1df8e562940c9c1435c2edc4b88c7b85c2c2c8309b95702f8f6d3f978bdf19
5
5
  SHA512:
6
- metadata.gz: 0f9b3a98fd6085704eafe6bf19a90bca60010f7004c28913b6356d96cef8ac08145f82e12a389fde0adb40c2ffa4eb7aca1aa3ea9f2065ed3549a28f6eacffdd
7
- data.tar.gz: 303c6bec1248313856b9dad67234a77344ac1e9ea66e2c90361e498524c9f46f9da142a5c278f2866806be1437fc701097699c67c86890e02757c31632a657cc
6
+ metadata.gz: fdc842d462d7e04c163560c475bd6519e04b4b5f57b2684be5137ed32ff0bc9e257d9f09e7ab1cda06e6106065e38e4a1c80f0fea96d812841141cc14cbed4a6
7
+ data.tar.gz: 9e4954b597a281e4c68e1ea9ecbe2748efbcac31062f7fd3b925b887dc0616015df27df8c0122fc4165621227f5eefa13ead1ce0583cbe9dcd377ecaf0e47e23
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Added
6
+
7
+ - Validate `config.custom_fields` on `create!`/`update!` actions
8
+
9
+ ### Changes
10
+
11
+ - CodeRabbit improvements
12
+
13
+ ## 0.1.2
14
+
15
+ ### Fixed
16
+
17
+ - Issues' links were looking for the issue in the default repo with the same number
18
+
19
+ ## 0.1.1
20
+
21
+ ### Fixed
22
+
23
+ - Wrong changelog included in publish
24
+
25
+ ## 0.1.0
26
+
27
+ - Initial Setup
@@ -70,7 +70,10 @@ module PlanMyStuff
70
70
  def parse_viewer_ids(ids_string)
71
71
  return [] if ids_string.blank?
72
72
 
73
- ids_string.split(',').filter_map { |id| id.strip.presence&.to_i }
73
+ ids_string.split(',').filter_map do |id|
74
+ token = id.strip
75
+ token.to_i if token.match?(/\A\d+\z/)
76
+ end
74
77
  end
75
78
  end
76
79
  end
@@ -4,7 +4,7 @@ module PlanMyStuff
4
4
  class CommentsController < ApplicationController
5
5
  # POST /issues/:issue_id/comments
6
6
  def create
7
- @issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
7
+ @issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
8
8
 
9
9
  PMS::Comment.create!(
10
10
  issue: @issue,
@@ -14,25 +14,33 @@ module PlanMyStuff
14
14
  )
15
15
 
16
16
  flash[:success] = 'Comment was successfully created.'
17
- redirect_to(plan_my_stuff.issue_path(@issue.number))
17
+ redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
18
18
  end
19
19
 
20
20
  # GET /issues/:issue_id/comments/:id/edit
21
21
  def edit
22
22
  load_comment
23
23
  return unless @comment
24
+ return redirect_to_issue if issue_body_comment?
24
25
 
25
26
  @support_user = support_user?
26
- return redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number)) unless can_edit?(@comment)
27
+ return if can_edit?(@comment)
28
+
29
+ redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
27
30
  end
28
31
 
29
32
  # PATCH/PUT /issues/:issue_id/comments/:id
30
33
  def update
31
34
  load_comment
32
35
  return unless @comment
36
+ return redirect_to_issue if issue_body_comment?
33
37
 
34
38
  @support_user = support_user?
35
- return redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number)) unless can_edit?(@comment)
39
+ unless can_edit?(@comment)
40
+ redirect_to_unauthorized(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
41
+
42
+ return
43
+ end
36
44
 
37
45
  update_attrs = { body: comment_params[:body] }
38
46
  update_attrs[:visibility] = comment_params[:visibility].to_sym if @support_user && comment_params[:visibility]
@@ -40,7 +48,7 @@ module PlanMyStuff
40
48
  @comment.update!(**update_attrs)
41
49
 
42
50
  flash[:success] = 'Comment was successfully updated.'
43
- redirect_to(plan_my_stuff.issue_path(@issue.number))
51
+ redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
44
52
  rescue PMS::StaleObjectError
45
53
  flash.now[:error] = 'Comment was modified by someone else. Please review the latest changes and try again.'
46
54
  render(:edit, status: PMS.unprocessable_status)
@@ -58,7 +66,7 @@ module PlanMyStuff
58
66
  # @return [void]
59
67
  #
60
68
  def load_comment
61
- @issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
69
+ @issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
62
70
  @comment = PMS::Comment.find(params[:id].to_i, issue: @issue)
63
71
  end
64
72
 
@@ -78,5 +86,15 @@ module PlanMyStuff
78
86
 
79
87
  comment.metadata.created_by == PMS::UserResolver.user_id(user)
80
88
  end
89
+
90
+ # @return [Boolean]
91
+ def issue_body_comment?
92
+ @comment.metadata.issue_body?
93
+ end
94
+
95
+ # @return [void]
96
+ def redirect_to_issue
97
+ redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
98
+ end
81
99
  end
82
100
  end
@@ -8,7 +8,7 @@ module PlanMyStuff
8
8
  @per_page = (params[:per_page] || 25).to_i
9
9
  @state = (params[:state] || 'open').to_sym
10
10
  @labels = params[:labels].present? ? Array.wrap(params[:labels]) : []
11
- @repo = params[:repo]&.to_sym
11
+ @repo = params[:repo]
12
12
 
13
13
  @issues = PMS::Issue.list(
14
14
  repo: @repo,
@@ -35,7 +35,7 @@ module PlanMyStuff
35
35
  )
36
36
 
37
37
  flash[:success] = 'Issue was successfully created.'
38
- redirect_to(plan_my_stuff.issue_path(@issue.number))
38
+ redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
39
39
  rescue PMS::ValidationError => e
40
40
  @issue = PMS::Issue.new(title: issue_params[:title], body: issue_params[:body])
41
41
  @support_user = support_user?
@@ -45,7 +45,7 @@ module PlanMyStuff
45
45
 
46
46
  # GET /issues/:id
47
47
  def show
48
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
48
+ @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
49
49
  @comments = filter_visible_comments(@issue.comments)
50
50
  @support_user = support_user?
51
51
  @current_user_id = pms_current_user.present? ? PMS::UserResolver.user_id(pms_current_user) : nil
@@ -53,13 +53,13 @@ module PlanMyStuff
53
53
 
54
54
  # GET /issues/:id/edit
55
55
  def edit
56
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
56
+ @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
57
57
  @support_user = support_user?
58
58
  end
59
59
 
60
60
  # PATCH/PUT /issues/:id
61
61
  def update
62
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
62
+ @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
63
63
 
64
64
  @issue.update!(
65
65
  title: issue_params[:title],
@@ -68,7 +68,7 @@ module PlanMyStuff
68
68
  )
69
69
 
70
70
  flash[:success] = 'Issue was successfully updated.'
71
- redirect_to(plan_my_stuff.issue_path(@issue.number))
71
+ redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
72
72
  rescue PMS::StaleObjectError
73
73
  @support_user = support_user?
74
74
  flash.now[:error] = 'Issue was modified by someone else. Please review the latest changes and try again.'
@@ -77,48 +77,54 @@ module PlanMyStuff
77
77
 
78
78
  # PATCH /issues/:id/close
79
79
  def close
80
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
80
+ @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
81
81
  @issue.update!(state: :closed)
82
82
 
83
83
  flash[:success] = 'Issue was successfully closed.'
84
- redirect_to(plan_my_stuff.issue_path(@issue.number))
84
+ redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
85
85
  end
86
86
 
87
87
  # PATCH /issues/:id/reopen
88
88
  def reopen
89
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo]&.to_sym)
89
+ @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
90
90
  @issue.update!(state: :open)
91
91
 
92
92
  flash[:success] = 'Issue was successfully reopened.'
93
- redirect_to(plan_my_stuff.issue_path(@issue.number))
93
+ redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
94
94
  end
95
95
 
96
96
  # POST /issues/:id/add_viewers
97
97
  def add_viewers
98
- return redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id])) unless support_user?
98
+ unless support_user?
99
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id], repo: params[:repo]))
100
+ return
101
+ end
99
102
 
100
103
  viewer_ids = parse_viewer_ids(params[:viewer_ids])
101
104
  if viewer_ids.blank?
102
105
  flash[:error] = 'No valid viewer IDs provided.'
103
- redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
106
+ redirect_to(plan_my_stuff.edit_issue_path(params[:id], repo: params[:repo]))
104
107
  return
105
108
  end
106
109
 
107
- PMS::Issue.add_viewers(number: params[:id].to_i, user_ids: viewer_ids, repo: params[:repo]&.to_sym)
110
+ PMS::Issue.add_viewers(number: params[:id].to_i, user_ids: viewer_ids, repo: params[:repo])
108
111
 
109
112
  flash[:success] = 'Viewers were successfully added.'
110
- redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
113
+ redirect_to(plan_my_stuff.edit_issue_path(params[:id], repo: params[:repo]))
111
114
  end
112
115
 
113
116
  # DELETE /issues/:id/remove_viewer
114
117
  def remove_viewer
115
- return redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id])) unless support_user?
118
+ unless support_user?
119
+ redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id], repo: params[:repo]))
120
+ return
121
+ end
116
122
 
117
123
  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)
124
+ PMS::Issue.remove_viewers(number: params[:id].to_i, user_ids: [viewer_id], repo: params[:repo])
119
125
 
120
126
  flash[:success] = 'Viewer was successfully removed.'
121
- redirect_to(plan_my_stuff.edit_issue_path(params[:id]))
127
+ redirect_to(plan_my_stuff.edit_issue_path(params[:id], repo: params[:repo]))
122
128
  end
123
129
 
124
130
  private
@@ -7,24 +7,24 @@ module PlanMyStuff
7
7
  labels = parse_labels(params[:label_name])
8
8
  if labels.blank?
9
9
  flash[:error] = 'Label name is required.'
10
- redirect_to(plan_my_stuff.issue_path(params[:issue_id]))
10
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
11
11
  return
12
12
  end
13
13
 
14
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
14
+ issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
15
15
  PMS::Label.add(issue: issue, labels: labels)
16
16
 
17
17
  flash[:success] = 'Label was successfully added.'
18
- redirect_to(plan_my_stuff.issue_path(issue.number))
18
+ redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
19
19
  end
20
20
 
21
21
  # DELETE /issues/:issue_id/labels/:name
22
22
  def remove_from_issue
23
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo]&.to_sym)
23
+ issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
24
24
  PMS::Label.remove(issue: issue, labels: [params[:name]])
25
25
 
26
26
  flash[:success] = 'Label was successfully removed.'
27
- redirect_to(plan_my_stuff.issue_path(issue.number))
27
+ redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
28
28
  end
29
29
  end
30
30
  end
@@ -50,6 +50,12 @@ module PlanMyStuff
50
50
 
51
51
  # PATCH /projects/:project_id/items/:id/unassign
52
52
  def unassign
53
+ if params[:username].blank?
54
+ flash[:error] = 'Username is required to unassign.'
55
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
56
+ return
57
+ end
58
+
53
59
  item = find_project_item
54
60
  current_assignees = item.field_values['Assignees'] || []
55
61
  remaining = current_assignees - [params[:username]]
@@ -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
@@ -14,7 +14,7 @@
14
14
  <% @issues.each do |issue| %>
15
15
  <tr>
16
16
  <td><%= issue.number %></td>
17
- <td><%= link_to(issue.title, plan_my_stuff.issue_path(issue.number)) %></td>
17
+ <td><%= link_to(issue.title, plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name)) %></td>
18
18
  <td><%= issue.state %></td>
19
19
  <td><%= issue.labels.join(', ') %></td>
20
20
  </tr>
@@ -24,14 +24,14 @@
24
24
 
25
25
  <nav>
26
26
  <% if @page > 1 %>
27
- <%= link_to('Previous', plan_my_stuff.issues_path(page: @page - 1, state: @state, labels: @labels)) %>
27
+ <%= link_to('Previous', plan_my_stuff.issues_path(page: @page - 1, state: @state, labels: @labels, repo: @repo)) %>
28
28
  <% end %>
29
29
  <% if @issues.size == @per_page %>
30
- <%= link_to('Next', plan_my_stuff.issues_path(page: @page + 1, state: @state, labels: @labels)) %>
30
+ <%= link_to('Next', plan_my_stuff.issues_path(page: @page + 1, state: @state, labels: @labels, repo: @repo)) %>
31
31
  <% end %>
32
32
  </nav>
33
33
  <% else %>
34
34
  <p>No issues found.</p>
35
35
  <% end %>
36
36
  <br>
37
- <%= link_to "New Issue", plan_my_stuff.new_issue_path %>
37
+ <%= link_to "New Issue", plan_my_stuff.new_issue_path(repo: @repo) %>
@@ -1,9 +1,13 @@
1
- <% persisted = issue.persisted? %>
2
- <%= form_with(
3
- url: persisted ? plan_my_stuff.issue_path(issue.number) : plan_my_stuff.issues_path,
4
- method: persisted ? :patch : :post,
5
- scope: :issue,
6
- ) do |form| %>
1
+ <%
2
+ persisted = issue.persisted?
3
+ url =
4
+ if persisted
5
+ plan_my_stuff.issue_path(issue.number, repo: issue.repo&.full_name)
6
+ else
7
+ plan_my_stuff.issues_path(repo: issue.repo&.full_name)
8
+ end
9
+ %>
10
+ <%= form_with(url: url, method: persisted ? :patch : :post, scope: :issue) do |form| %>
7
11
  <div>
8
12
  <%= form.label(:title, 'Title') %>
9
13
  <%= form.text_field(:title, value: issue.title) %>
@@ -9,7 +9,7 @@
9
9
  <%=
10
10
  button_to(
11
11
  'Remove',
12
- plan_my_stuff.remove_viewer_issue_path(issue.number, viewer_id: viewer_id),
12
+ plan_my_stuff.remove_viewer_issue_path(issue.number, viewer_id: viewer_id, repo: issue.repo.full_name),
13
13
  method: :delete
14
14
  )
15
15
  %>
@@ -20,7 +20,7 @@
20
20
  <p>No viewers added.</p>
21
21
  <% end %>
22
22
 
23
- <%= form_with(url: plan_my_stuff.add_viewers_issue_path(issue.number), method: :post) do |form| %>
23
+ <%= form_with(url: plan_my_stuff.add_viewers_issue_path(issue.number, repo: issue.repo.full_name), method: :post) do |form| %>
24
24
  <div>
25
25
  <%= form.label(:viewer_ids, 'Add viewer IDs (comma-separated)') %>
26
26
  <%= form.text_field(:viewer_ids) %>
@@ -1,11 +1,11 @@
1
1
  <h1><%= @issue.title %> <small>#<%= @issue.number %></small></h1>
2
2
 
3
3
  <p>
4
- <%= link_to('Edit', plan_my_stuff.edit_issue_path(@issue.number)) %>
4
+ <%= link_to('Edit', plan_my_stuff.edit_issue_path(@issue.number, repo: @issue.repo.full_name)) %>
5
5
  <% if @issue.state == 'open' %>
6
- <%= button_to('Close Issue', plan_my_stuff.close_issue_path(@issue.number), method: :patch) %>
6
+ <%= button_to('Close Issue', plan_my_stuff.close_issue_path(@issue.number, repo: @issue.repo.full_name), method: :patch) %>
7
7
  <% else %>
8
- <%= button_to('Reopen Issue', plan_my_stuff.reopen_issue_path(@issue.number), method: :patch) %>
8
+ <%= button_to('Reopen Issue', plan_my_stuff.reopen_issue_path(@issue.number, repo: @issue.repo.full_name), method: :patch) %>
9
9
  <% end %>
10
10
  </p>
11
11
 
@@ -35,7 +35,7 @@
35
35
  <%= PlanMyStuff::Markdown.render(comment.body || '').html_safe %>
36
36
  <% if comment.pms_comment? && (@support_user || comment.metadata.created_by == @current_user_id) %>
37
37
  <p>
38
- <%= link_to('Edit', plan_my_stuff.edit_issue_comment_path(@issue.number, comment.id)) %>
38
+ <%= link_to('Edit', plan_my_stuff.edit_issue_comment_path(@issue.number, comment.id, repo: @issue.repo.full_name)) %>
39
39
  </p>
40
40
  <% end %>
41
41
  </div>
@@ -0,0 +1,7 @@
1
+ <h1>Edit Project #<%= @project.number %></h1>
2
+
3
+ <% if flash[:error].present? %>
4
+ <p style="color: red;"><%= flash[:error] %></p>
5
+ <% end %>
6
+
7
+ <%= render({ partial: 'plan_my_stuff/projects/partials/form', locals: { project: @project } }) %>
@@ -11,3 +11,5 @@
11
11
  <% else %>
12
12
  <p>No projects found.</p>
13
13
  <% end %>
14
+
15
+ <p><%= link_to('New Project', plan_my_stuff.new_project_path) %></p>
@@ -0,0 +1,7 @@
1
+ <h1>New Project</h1>
2
+
3
+ <% if flash[:error].present? %>
4
+ <p style="color: red;"><%= flash[:error] %></p>
5
+ <% end %>
6
+
7
+ <%= render({ partial: 'plan_my_stuff/projects/partials/form', locals: { project: @project } }) %>
@@ -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 %>
@@ -1,5 +1,7 @@
1
1
  <h1><%= @project.title %></h1>
2
2
 
3
+ <p><%= link_to('Edit', plan_my_stuff.edit_project_path(@project.number)) %></p>
4
+
3
5
  <% if @statuses.any? %>
4
6
  <table>
5
7
  <thead>
@@ -16,12 +18,12 @@
16
18
  <% items = @items_by_status[status] || [] %>
17
19
  <% items.each do |item| %>
18
20
  <div style="border: 1px solid black; margin: 1em">
19
- <% if item.number.present? %>
20
- <strong><%= link_to(item.title, plan_my_stuff.issue_path(item.number)) %></strong>
21
+ <% unless item.draft? %>
22
+ <strong><%= link_to(item.title, plan_my_stuff.issue_path(item.number, repo: item.repo.full_name)) %></strong>
21
23
  <% else %>
22
24
  <strong><%= item.title %></strong>
23
25
  <% end %>
24
- <% if item.number.present? %>
26
+ <% unless item.draft? %>
25
27
  <small>#<%= item.number %></small>
26
28
  <% end %>
27
29
 
@@ -46,7 +48,7 @@
46
48
  url: plan_my_stuff.unassign_project_item_path(@project.number, item.id),
47
49
  method: :patch,
48
50
  local: true,
49
- style: 'display: inline',
51
+ html: { style: 'display: inline' },
50
52
  ) do |form|
51
53
  %>
52
54
  <%= form.hidden_field(:username, value: username) %>
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, only: %i[index show] do
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
@@ -100,7 +100,16 @@ PMS.configure do |config|
100
100
  # Supported types: :string, :integer, :boolean, :array, :hash
101
101
  # required: true means the key must be present (value can be nil, [], etc.)
102
102
  #
103
+ # Shared fields (available on both issues and comments):
103
104
  config.custom_fields = {
104
- notification_recipients: { type: :array, required: true },
105
+ # notification_recipients: { type: :array, required: true },
105
106
  }
107
+
108
+ # Issue-only fields (merged on top of shared, context wins on conflicts):
109
+ # config.issue_custom_fields = {
110
+ # ticket_type: { type: :string, required: true },
111
+ # }
112
+
113
+ # Comment-only fields (merged on top of shared, context wins on conflicts):
114
+ # config.comment_custom_fields = {}
106
115
  end
@@ -8,6 +8,19 @@ module PlanMyStuff
8
8
  class ApplicationRecord
9
9
  include ActiveModel::Model
10
10
 
11
+ class << self
12
+ # Reads a field from an object that may respond to method calls or hash access.
13
+ #
14
+ # @param obj [Object]
15
+ # @param field [Symbol]
16
+ #
17
+ # @return [Object]
18
+ #
19
+ def read_field(obj, field)
20
+ obj.respond_to?(field) ? obj.public_send(field) : obj[field]
21
+ end
22
+ end
23
+
11
24
  def initialize(**)
12
25
  super
13
26
  @persisted = false
@@ -33,7 +46,30 @@ module PlanMyStuff
33
46
  # @return [Object]
34
47
  #
35
48
  def read_field(obj, field)
36
- obj.respond_to?(field) ? obj.public_send(field) : obj[field]
49
+ self.class.read_field(obj, field)
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
37
73
  end
38
74
  end
39
75
  end