reviewkit 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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/CODE_OF_CONDUCT.md +123 -0
  4. data/CONTRIBUTING.md +44 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +335 -0
  7. data/Rakefile +7 -0
  8. data/SECURITY.md +18 -0
  9. data/app/assets/builds/reviewkit/application.css +2 -0
  10. data/app/assets/javascripts/reviewkit/application.js +12 -0
  11. data/app/assets/javascripts/reviewkit/controllers/file_nav_controller.js +24 -0
  12. data/app/assets/javascripts/reviewkit/controllers/review_index_controller.js +84 -0
  13. data/app/assets/tailwind/reviewkit/application.css +865 -0
  14. data/app/controllers/reviewkit/application_controller.rb +80 -0
  15. data/app/controllers/reviewkit/comments_controller.rb +147 -0
  16. data/app/controllers/reviewkit/review_threads_controller.rb +277 -0
  17. data/app/controllers/reviewkit/reviews_controller.rb +142 -0
  18. data/app/helpers/reviewkit/application_helper.rb +12 -0
  19. data/app/helpers/reviewkit/asset_helper.rb +39 -0
  20. data/app/helpers/reviewkit/diff_helper.rb +230 -0
  21. data/app/helpers/reviewkit/flash_helper.rb +36 -0
  22. data/app/helpers/reviewkit/frame_helper.rb +37 -0
  23. data/app/helpers/reviewkit/icon_helper.rb +107 -0
  24. data/app/helpers/reviewkit/review_thread_helper.rb +54 -0
  25. data/app/models/concerns/reviewkit/notifies_lifecycle_events.rb +39 -0
  26. data/app/models/reviewkit/application_record.rb +7 -0
  27. data/app/models/reviewkit/comment.rb +29 -0
  28. data/app/models/reviewkit/current.rb +7 -0
  29. data/app/models/reviewkit/document.rb +79 -0
  30. data/app/models/reviewkit/review.rb +66 -0
  31. data/app/models/reviewkit/review_thread.rb +75 -0
  32. data/app/services/reviewkit/diffs/intraline_budget.rb +40 -0
  33. data/app/services/reviewkit/diffs/intraline_diff.rb +220 -0
  34. data/app/services/reviewkit/diffs/split_diff.rb +112 -0
  35. data/app/services/reviewkit/reviews/create.rb +57 -0
  36. data/app/views/layouts/reviewkit/application.html.erb +15 -0
  37. data/app/views/reviewkit/comments/_comment.html.erb +53 -0
  38. data/app/views/reviewkit/comments/_edit_form.html.erb +26 -0
  39. data/app/views/reviewkit/comments/_form.html.erb +16 -0
  40. data/app/views/reviewkit/review_threads/_bucket.html.erb +53 -0
  41. data/app/views/reviewkit/review_threads/_bucket_frame.html.erb +13 -0
  42. data/app/views/reviewkit/review_threads/_bucket_row.html.erb +55 -0
  43. data/app/views/reviewkit/review_threads/_edit_form.html.erb +29 -0
  44. data/app/views/reviewkit/review_threads/_thread.html.erb +87 -0
  45. data/app/views/reviewkit/reviews/_document.html.erb +41 -0
  46. data/app/views/reviewkit/reviews/_document_split.html.erb +73 -0
  47. data/app/views/reviewkit/reviews/_document_unified.html.erb +57 -0
  48. data/app/views/reviewkit/reviews/_edit_form.html.erb +35 -0
  49. data/app/views/reviewkit/reviews/_index_content.html.erb +160 -0
  50. data/app/views/reviewkit/reviews/_review_sidebar.html.erb +70 -0
  51. data/app/views/reviewkit/reviews/_show_content.html.erb +164 -0
  52. data/app/views/reviewkit/reviews/index.html.erb +11 -0
  53. data/app/views/reviewkit/reviews/show.html.erb +11 -0
  54. data/app/views/reviewkit/shared/_flash.html.erb +10 -0
  55. data/bin/console +4 -0
  56. data/bin/lint +4 -0
  57. data/bin/rails +14 -0
  58. data/bin/setup +9 -0
  59. data/bin/test +4 -0
  60. data/config/importmap.rb +6 -0
  61. data/config/routes.rb +24 -0
  62. data/db/migrate/20260331181500_create_reviewkit_reviews.rb +19 -0
  63. data/db/migrate/20260331181600_create_reviewkit_documents.rb +23 -0
  64. data/db/migrate/20260331181700_create_reviewkit_review_threads.rb +23 -0
  65. data/db/migrate/20260331181800_create_reviewkit_comments.rb +15 -0
  66. data/db/migrate/20260401093000_add_description_to_reviewkit_reviews.rb +7 -0
  67. data/lib/generators/reviewkit/controllers/controllers_generator.rb +24 -0
  68. data/lib/generators/reviewkit/controllers/templates/comments_controller_extension.rb +13 -0
  69. data/lib/generators/reviewkit/controllers/templates/review_threads_controller_extension.rb +13 -0
  70. data/lib/generators/reviewkit/controllers/templates/reviews_controller_extension.rb +19 -0
  71. data/lib/generators/reviewkit/install/install_generator.rb +52 -0
  72. data/lib/generators/reviewkit/install/templates/importmap.rb +3 -0
  73. data/lib/generators/reviewkit/install/templates/reviewkit.rb +19 -0
  74. data/lib/generators/reviewkit/models/models_generator.rb +24 -0
  75. data/lib/generators/reviewkit/models/templates/comment_extension.rb +21 -0
  76. data/lib/generators/reviewkit/models/templates/review_extension.rb +22 -0
  77. data/lib/generators/reviewkit/models/templates/review_thread_extension.rb +21 -0
  78. data/lib/generators/reviewkit/views/views_generator.rb +15 -0
  79. data/lib/reviewkit/configuration.rb +33 -0
  80. data/lib/reviewkit/engine.rb +67 -0
  81. data/lib/reviewkit/version.rb +5 -0
  82. data/lib/reviewkit.rb +26 -0
  83. data/lib/tasks/reviewkit_tasks.rake +12 -0
  84. data/sig/reviewkit.rbs +129 -0
  85. metadata +238 -0
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewkit
4
+ class ApplicationController < ActionController::Base
5
+ helper ::ApplicationHelper if defined?(::ApplicationHelper)
6
+ helper_method :reviewkit_current_actor,
7
+ :reviewkit_engine_layout?,
8
+ :reviewkit_frame_request?,
9
+ :reviewkit_requested_frame_id
10
+
11
+ around_action :set_reviewkit_current_attributes
12
+ layout :reviewkit_layout
13
+
14
+ private
15
+
16
+ def reviewkit_current_actor
17
+ @reviewkit_current_actor ||= Reviewkit.config.current_actor.call(self)
18
+ end
19
+
20
+ def reviewkit_frame_request?
21
+ reviewkit_requested_frame_id.present?
22
+ end
23
+
24
+ def reviewkit_requested_frame_id
25
+ request.headers["Turbo-Frame"].presence || params[:reviewkit_frame_id].presence
26
+ end
27
+
28
+ def reviewkit_layout
29
+ return false if reviewkit_frame_request?
30
+
31
+ @reviewkit_layout ||= begin
32
+ configured_layout = Reviewkit.config.layout
33
+ return configured_layout if configured_layout.present? && configured_layout != Reviewkit::Configuration::DEFAULT_LAYOUT
34
+
35
+ if host_layout_override?("reviewkit/application")
36
+ "reviewkit/application"
37
+ elsif host_layout_override?("application")
38
+ "application"
39
+ else
40
+ Reviewkit::Configuration::DEFAULT_LAYOUT
41
+ end
42
+ end
43
+ end
44
+
45
+ def reviewkit_engine_layout?
46
+ reviewkit_layout == Reviewkit::Configuration::DEFAULT_LAYOUT
47
+ end
48
+
49
+ def authorize_reviewkit!(action, record = nil, **context)
50
+ allowed = Reviewkit.config.authorize_action.call(self, action, record, **context)
51
+ raise Reviewkit::AuthorizationError, "Forbidden action: #{action}" unless allowed
52
+ end
53
+
54
+ def host_layout_override?(layout_name)
55
+ basename = layout_name.split("/").last
56
+ relative_directory = layout_name.include?("/") ? File.join("app/views/layouts", File.dirname(layout_name)) : "app/views/layouts"
57
+
58
+ Dir.glob(Rails.root.join(relative_directory, "#{basename}.*")).any?
59
+ end
60
+
61
+ def set_reviewkit_current_attributes
62
+ Reviewkit::Current.set(
63
+ actor: reviewkit_current_actor,
64
+ controller: self,
65
+ source: self.class.name
66
+ ) do
67
+ yield
68
+ end
69
+ end
70
+
71
+ def handle_authorization_error
72
+ respond_to do |format|
73
+ format.html { redirect_back fallback_location: main_app.respond_to?(:root_path) ? main_app.root_path : "/", alert: "You are not authorized to access that review." }
74
+ format.turbo_stream { head :forbidden }
75
+ format.any { head :forbidden }
76
+ end
77
+ end
78
+ rescue_from Reviewkit::AuthorizationError, with: :handle_authorization_error
79
+ end
80
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewkit
4
+ class CommentsController < ApplicationController
5
+ before_action :set_thread
6
+ before_action :set_comment, only: %i[show edit update destroy]
7
+
8
+ def create
9
+ authorize_reviewkit!(:comment, @thread.review)
10
+
11
+ @comment = @thread.comments.build(comment_attributes.merge(author: reviewkit_current_actor, metadata: {}))
12
+
13
+ @comment.save
14
+
15
+ render_thread_bucket(status: @comment.persisted? ? :ok : :unprocessable_content)
16
+ end
17
+
18
+ def show
19
+ authorize_reviewkit!(:show, @thread.review)
20
+ render_comment_frame
21
+ end
22
+
23
+ def edit
24
+ authorize_comment_management!(:edit_comment)
25
+ render_edit_frame
26
+ end
27
+
28
+ def update
29
+ authorize_comment_management!(:update_comment)
30
+
31
+ if @comment.update(comment_attributes)
32
+ render_comment_frame
33
+ else
34
+ render_edit_frame(status: :unprocessable_content)
35
+ end
36
+ end
37
+
38
+ def destroy
39
+ authorize_comment_management!(:destroy_comment)
40
+
41
+ if @thread.comments.size == 1
42
+ @thread.destroy!
43
+ else
44
+ @comment.destroy!
45
+ end
46
+
47
+ render_thread_bucket(status: :ok)
48
+ end
49
+
50
+ protected
51
+
52
+ def review_thread_scope
53
+ ReviewThread.includes(:review, :document, :comments)
54
+ end
55
+
56
+ def comment_scope(thread)
57
+ thread.comments
58
+ end
59
+
60
+ def permitted_comment_attributes
61
+ %i[body]
62
+ end
63
+
64
+ def comment_request_attributes
65
+ %i[frame_id view]
66
+ end
67
+
68
+ def comment_frame_redirect_path(comment)
69
+ review_path(comment.review_thread.review, anchor: helpers.reviewkit_document_anchor(comment.review_thread.document))
70
+ end
71
+
72
+ private
73
+
74
+ def set_thread
75
+ @thread = review_thread_scope.find(params[:review_thread_id])
76
+ end
77
+
78
+ def set_comment
79
+ @comment = comment_scope(@thread).find(params[:id])
80
+ end
81
+
82
+ def comment_params
83
+ params.fetch(:comment, ActionController::Parameters.new).permit(*(permitted_comment_attributes + comment_request_attributes))
84
+ end
85
+
86
+ def comment_attributes
87
+ comment_params.to_h.symbolize_keys.slice(*permitted_comment_attributes)
88
+ end
89
+
90
+ def authorize_comment_management!(action)
91
+ authorize_reviewkit!(action, @comment)
92
+ raise Reviewkit::AuthorizationError, "Forbidden action: #{action}" unless comment_manageable?(@comment)
93
+ end
94
+
95
+ def comment_manageable?(comment)
96
+ comment.author == reviewkit_current_actor
97
+ end
98
+
99
+ def render_comment_frame(status: :ok)
100
+ if reviewkit_frame_request?
101
+ render partial: "reviewkit/comments/comment", locals: { comment: @comment }, status: status
102
+ else
103
+ redirect_to comment_frame_redirect_path(@comment)
104
+ end
105
+ end
106
+
107
+ def render_edit_frame(status: :ok)
108
+ if reviewkit_frame_request?
109
+ render partial: "reviewkit/comments/edit_form", locals: { comment: @comment }, status: status
110
+ else
111
+ redirect_to comment_frame_redirect_path(@comment)
112
+ end
113
+ end
114
+
115
+ def render_thread_bucket(status:)
116
+ @review = @thread.review
117
+ @document = @thread.document
118
+ @row = @document.diff_rows.find { |row| row.fetch("line_code") == @thread.line_code }
119
+ @threads = @review.review_threads.includes(:comments)
120
+ .where(document: @document, line_code: @thread.line_code)
121
+ .order(:created_at)
122
+
123
+ respond_to do |format|
124
+ format.turbo_stream do
125
+ render turbo_stream: ::Turbo::Streams::TagBuilder.new(view_context).replace(
126
+ helpers.reviewkit_thread_bucket_row_id(@document, @thread.line_code),
127
+ partial: "reviewkit/review_threads/bucket_row",
128
+ locals: {
129
+ colspan: comment_params[:view] == "unified" ? 4 : 6,
130
+ composer_open: false,
131
+ composer_side: nil,
132
+ document: @document,
133
+ frame_id: comment_params[:frame_id].presence || helpers.dom_id(@review, :review),
134
+ line_code: @thread.line_code,
135
+ review: @review,
136
+ row: @row,
137
+ thread_errors: @comment&.errors&.full_messages || [],
138
+ threads: @threads,
139
+ view_mode: comment_params[:view] == "unified" ? "unified" : "split"
140
+ }
141
+ ), status: status
142
+ end
143
+ format.html { redirect_to review_path(@review, anchor: helpers.reviewkit_document_anchor(@document)) }
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewkit
4
+ class ReviewThreadsController < ApplicationController
5
+ before_action :set_review, only: :create
6
+ before_action :set_thread, only: %i[show edit update destroy resolve reopen mark_outdated]
7
+
8
+ def create
9
+ authorize_reviewkit!(:comment, @review)
10
+
11
+ document = review_documents_scope.find(review_thread_params.fetch(:document_id))
12
+
13
+ ReviewThread.transaction do
14
+ @thread = @review.review_threads.build(
15
+ build_review_thread_attributes(document)
16
+ )
17
+ @thread.save!
18
+
19
+ @thread.comments.create!(
20
+ author: reviewkit_current_actor,
21
+ body: starter_comment_body,
22
+ metadata: {}
23
+ )
24
+ end
25
+
26
+ render_thread_row(document, @thread.line_code)
27
+ rescue ActiveRecord::RecordInvalid => error
28
+ render_thread_row(
29
+ document,
30
+ review_thread_params.fetch(:line_code),
31
+ status: :unprocessable_content,
32
+ thread_errors: error.record.errors.full_messages,
33
+ composer_open: true,
34
+ composer_side: review_thread_params[:side]
35
+ )
36
+ end
37
+
38
+ def show
39
+ authorize_reviewkit!(:show, @thread.review)
40
+ render_thread_frame
41
+ end
42
+
43
+ def edit
44
+ authorize_thread_management!(:edit_thread)
45
+ render_edit_frame
46
+ end
47
+
48
+ def update
49
+ authorize_thread_management!(:update_thread)
50
+
51
+ if starter_comment.update(thread_params)
52
+ @thread.reload
53
+ render_thread_frame
54
+ else
55
+ render_edit_frame(status: :unprocessable_content)
56
+ end
57
+ end
58
+
59
+ def destroy
60
+ authorize_thread_management!(:destroy_thread)
61
+ document = @thread.document
62
+ line_code = @thread.line_code
63
+ @thread.destroy!
64
+
65
+ render_thread_row(document, line_code)
66
+ end
67
+
68
+ def resolve
69
+ authorize_reviewkit!(:resolve, @thread)
70
+ @thread.resolve!
71
+ render_thread_row(@thread.document, @thread.line_code)
72
+ end
73
+
74
+ def reopen
75
+ authorize_reviewkit!(:reopen, @thread)
76
+ @thread.reopen!
77
+ render_thread_row(@thread.document, @thread.line_code)
78
+ end
79
+
80
+ def mark_outdated
81
+ authorize_reviewkit!(:update_thread_status, @thread)
82
+ @thread.mark_outdated!
83
+ render_thread_row(@thread.document, @thread.line_code)
84
+ end
85
+
86
+ protected
87
+
88
+ def review_scope
89
+ Review.all
90
+ end
91
+
92
+ def review_thread_scope
93
+ ReviewThread.includes(:review, :document, :comments)
94
+ end
95
+
96
+ def permitted_review_thread_attributes
97
+ []
98
+ end
99
+
100
+ def review_thread_request_attributes
101
+ %i[
102
+ body
103
+ document_id
104
+ frame_id
105
+ line_code
106
+ new_line
107
+ new_text
108
+ old_line
109
+ old_text
110
+ side
111
+ view
112
+ ]
113
+ end
114
+
115
+ def permitted_review_thread_update_attributes
116
+ %i[body]
117
+ end
118
+
119
+ def review_documents_scope
120
+ @review.documents
121
+ end
122
+
123
+ def build_review_thread_attributes(document)
124
+ {
125
+ document:,
126
+ line_code: review_thread_params.fetch(:line_code),
127
+ metadata: line_metadata,
128
+ new_line: integer_or_nil(review_thread_params[:new_line]),
129
+ old_line: integer_or_nil(review_thread_params[:old_line]),
130
+ side: review_thread_params.fetch(:side)
131
+ }.merge(review_thread_model_attributes)
132
+ end
133
+
134
+ def thread_redirect_path(thread)
135
+ review_path(thread.review, anchor: helpers.reviewkit_document_anchor(thread.document))
136
+ end
137
+
138
+ def thread_row_redirect_path(review, document, line_code:, composer_open:, composer_side:)
139
+ review_path(
140
+ review,
141
+ document_id: document.id,
142
+ open_thread: composer_open ? line_code : nil,
143
+ thread_side: composer_side,
144
+ view: view_mode
145
+ )
146
+ end
147
+
148
+ private
149
+
150
+ def set_review
151
+ @review = review_scope.find(params[:review_id])
152
+ end
153
+
154
+ def set_thread
155
+ @thread = review_thread_scope.find(params[:id])
156
+ end
157
+
158
+ def starter_comment
159
+ @starter_comment ||= @thread.comments.order(:created_at, :id).first!
160
+ end
161
+
162
+ def review_thread_params
163
+ params.require(:review_thread).permit(*(review_thread_request_attributes + permitted_review_thread_attributes))
164
+ end
165
+
166
+ def thread_params
167
+ params.require(:review_thread).permit(*permitted_review_thread_update_attributes)
168
+ end
169
+
170
+ def review_thread_model_attributes
171
+ review_thread_params.to_h.symbolize_keys.slice(*permitted_review_thread_attributes)
172
+ end
173
+
174
+ def starter_comment_body
175
+ review_thread_params.fetch(:body)
176
+ end
177
+
178
+ def integer_or_nil(value)
179
+ value.present? ? value.to_i : nil
180
+ end
181
+
182
+ def line_metadata
183
+ {
184
+ "old_text" => review_thread_params[:old_text].to_s,
185
+ "new_text" => review_thread_params[:new_text].to_s
186
+ }
187
+ end
188
+
189
+ def authorize_thread_management!(action)
190
+ authorize_reviewkit!(action, @thread)
191
+ raise Reviewkit::AuthorizationError, "Forbidden action: #{action}" unless helpers.reviewkit_thread_manageable?(@thread)
192
+ end
193
+
194
+ def render_thread_frame(status: :ok)
195
+ if reviewkit_frame_request?
196
+ render partial: "reviewkit/review_threads/thread",
197
+ locals: {
198
+ frame_id: requested_frame_id(@thread.review),
199
+ review: @thread.review,
200
+ thread: @thread,
201
+ view_mode: view_mode
202
+ },
203
+ status: status
204
+ else
205
+ redirect_to thread_redirect_path(@thread)
206
+ end
207
+ end
208
+
209
+ def render_edit_frame(status: :ok)
210
+ if reviewkit_frame_request?
211
+ render partial: "reviewkit/review_threads/edit_form",
212
+ locals: {
213
+ comment: starter_comment,
214
+ frame_id: requested_frame_id(@thread.review),
215
+ review: @thread.review,
216
+ thread: @thread,
217
+ view_mode: view_mode
218
+ },
219
+ status: status
220
+ else
221
+ redirect_to thread_redirect_path(@thread)
222
+ end
223
+ end
224
+
225
+ def render_thread_row(document, line_code, status: :ok, thread_errors: [], composer_open: false, composer_side: nil)
226
+ review = document.review
227
+ row = document.diff_rows.find { |diff_row| diff_row.fetch("line_code") == line_code }
228
+ threads = review.review_threads.includes(:comments)
229
+ .where(document: document, line_code: line_code)
230
+ .order(:created_at)
231
+
232
+ respond_to do |format|
233
+ format.turbo_stream do
234
+ render turbo_stream: ::Turbo::Streams::TagBuilder.new(view_context).replace(
235
+ helpers.reviewkit_thread_bucket_row_id(document, line_code),
236
+ partial: "reviewkit/review_threads/bucket_row",
237
+ locals: {
238
+ colspan: view_mode == "unified" ? 4 : 6,
239
+ composer_open: composer_open || thread_errors.present?,
240
+ composer_side: composer_side,
241
+ document: document,
242
+ frame_id: requested_frame_id(review),
243
+ line_code: line_code,
244
+ review: review,
245
+ row: row,
246
+ thread_errors: thread_errors,
247
+ threads: threads,
248
+ view_mode: view_mode
249
+ }
250
+ ), status: status
251
+ end
252
+ format.html do
253
+ redirect_to thread_row_redirect_path(
254
+ review,
255
+ document,
256
+ line_code:,
257
+ composer_open: composer_open || thread_errors.present?,
258
+ composer_side:
259
+ )
260
+ end
261
+ end
262
+ end
263
+
264
+ def requested_frame_id(review)
265
+ params[:frame_id].presence || request_review_thread_param(:frame_id).presence || helpers.dom_id(review, :review)
266
+ end
267
+
268
+ def view_mode
269
+ requested_view = params[:view].presence || request_review_thread_param(:view).presence
270
+ requested_view == "unified" ? "unified" : "split"
271
+ end
272
+
273
+ def request_review_thread_param(key)
274
+ review_thread_params[key] if params[:review_thread].present?
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewkit
4
+ class ReviewsController < ApplicationController
5
+ before_action :set_review, only: %i[show edit update approve reject destroy]
6
+ before_action :set_review_display_state, only: %i[show edit update approve reject]
7
+ before_action :set_thread_index, only: %i[show edit update approve reject]
8
+
9
+ def index
10
+ @reviews = reviews_scope.includes(*review_index_includes)
11
+ authorize_reviewkit!(:index, Review)
12
+ end
13
+
14
+ def show
15
+ authorize_reviewkit!(:show, @review)
16
+ end
17
+
18
+ def edit
19
+ authorize_reviewkit!(:edit_review, @review)
20
+ @editing_review = true
21
+ render :show
22
+ end
23
+
24
+ def update
25
+ authorize_reviewkit!(:update_review, @review)
26
+
27
+ if @review.update(review_params)
28
+ redirect_to review_redirect_path(@review), status: :see_other, notice: "Review updated."
29
+ else
30
+ @editing_review = true
31
+ render :show, status: :unprocessable_content
32
+ end
33
+ end
34
+
35
+ def approve
36
+ authorize_reviewkit!(:approve, @review)
37
+ transition_review!(:approve!, :notice, "Review approved.")
38
+ end
39
+
40
+ def reject
41
+ authorize_reviewkit!(:reject, @review)
42
+ transition_review!(:reject!, :alert, "Review rejected.")
43
+ end
44
+
45
+ def destroy
46
+ authorize_reviewkit!(:destroy, @review)
47
+ @review.destroy!
48
+
49
+ redirect_to reviews_index_path, status: :see_other, notice: "Review deleted."
50
+ end
51
+
52
+ protected
53
+
54
+ def reviews_scope
55
+ Review.order(updated_at: :desc)
56
+ end
57
+
58
+ def review_scope
59
+ Review.includes(review_includes)
60
+ end
61
+
62
+ def review_index_includes
63
+ [ :documents, :review_threads ]
64
+ end
65
+
66
+ def review_includes
67
+ [ { documents: { review_threads: :comments } }, { review_threads: :comments } ]
68
+ end
69
+
70
+ def permitted_review_attributes
71
+ %i[title description]
72
+ end
73
+
74
+ def review_redirect_path(review)
75
+ review_path(review, document_id: @selected_document&.id, view: @view_mode)
76
+ end
77
+
78
+ def reviews_index_path
79
+ reviews_path
80
+ end
81
+
82
+ def review_transition_failure_message(review)
83
+ review.errors.full_messages.to_sentence.presence || "Unable to update the review."
84
+ end
85
+
86
+ private
87
+
88
+ def set_review
89
+ @review = review_scope.find(params[:id])
90
+ end
91
+
92
+ def set_review_display_state
93
+ @view_mode = permitted_view_mode
94
+ @selected_document = selected_document
95
+ @open_thread_line_code = params[:open_thread].presence
96
+ @open_thread_side = permitted_thread_side
97
+ end
98
+
99
+ def permitted_view_mode
100
+ params[:view] == "unified" ? "unified" : "split"
101
+ end
102
+
103
+ def selected_document
104
+ requested_document = review_documents_scope.find_by(id: params[:document_id]) if params[:document_id].present?
105
+ requested_document || review_documents_scope.first
106
+ end
107
+
108
+ def permitted_thread_side
109
+ return "old" if params[:thread_side] == "old"
110
+ return "new" if params[:thread_side] == "new"
111
+
112
+ nil
113
+ end
114
+
115
+ def set_thread_index
116
+ @thread_index = selected_document_threads_scope.group_by { |thread| [ thread.document_id, thread.line_code ] }
117
+ end
118
+
119
+ def review_params
120
+ params.require(:review).permit(*permitted_review_attributes)
121
+ end
122
+
123
+ def transition_review!(method_name, flash_type, message)
124
+ @review.public_send(method_name)
125
+ redirect_to review_redirect_path(@review), status: :see_other, flash_type => message
126
+ rescue ActiveRecord::RecordInvalid => error
127
+ @review = error.record
128
+ flash.now[:alert] = review_transition_failure_message(@review)
129
+ render :show, status: :unprocessable_content
130
+ end
131
+
132
+ def review_documents_scope
133
+ @review.documents
134
+ end
135
+
136
+ def selected_document_threads_scope
137
+ @review.review_threads.includes(:comments)
138
+ .where(document: @selected_document)
139
+ .order(:created_at)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewkit
4
+ module ApplicationHelper
5
+ include AssetHelper
6
+ include DiffHelper
7
+ include FlashHelper
8
+ include FrameHelper
9
+ include IconHelper
10
+ include ReviewThreadHelper
11
+ end
12
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewkit
4
+ module AssetHelper
5
+ include Importmap::ImportmapTagsHelper
6
+
7
+ def reviewkit_assets(importmap: false, entry_point: "reviewkit/application")
8
+ tags = [ reviewkit_stylesheet_tag ]
9
+
10
+ if importmap
11
+ tags << javascript_importmap_tags(entry_point)
12
+ else
13
+ tags << javascript_import_module_tag(entry_point)
14
+ end
15
+
16
+ safe_join(tags, "\n")
17
+ end
18
+
19
+ def reviewkit_page_assets(entry_point: "reviewkit/application")
20
+ if controller.respond_to?(:reviewkit_engine_layout?, true) && controller.send(:reviewkit_engine_layout?)
21
+ reviewkit_assets(importmap: true, entry_point: entry_point)
22
+ else
23
+ reviewkit_stylesheet_tag
24
+ end
25
+ end
26
+
27
+ def reviewkit_page_module_tag(entry_point: "reviewkit/application")
28
+ return "".html_safe if controller.respond_to?(:reviewkit_engine_layout?, true) && controller.send(:reviewkit_engine_layout?)
29
+
30
+ javascript_import_module_tag(entry_point)
31
+ end
32
+
33
+ private
34
+
35
+ def reviewkit_stylesheet_tag
36
+ stylesheet_link_tag("reviewkit/application", media: "all", "data-turbo-track": "reload")
37
+ end
38
+ end
39
+ end