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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/CODE_OF_CONDUCT.md +123 -0
- data/CONTRIBUTING.md +44 -0
- data/MIT-LICENSE +20 -0
- data/README.md +335 -0
- data/Rakefile +7 -0
- data/SECURITY.md +18 -0
- data/app/assets/builds/reviewkit/application.css +2 -0
- data/app/assets/javascripts/reviewkit/application.js +12 -0
- data/app/assets/javascripts/reviewkit/controllers/file_nav_controller.js +24 -0
- data/app/assets/javascripts/reviewkit/controllers/review_index_controller.js +84 -0
- data/app/assets/tailwind/reviewkit/application.css +865 -0
- data/app/controllers/reviewkit/application_controller.rb +80 -0
- data/app/controllers/reviewkit/comments_controller.rb +147 -0
- data/app/controllers/reviewkit/review_threads_controller.rb +277 -0
- data/app/controllers/reviewkit/reviews_controller.rb +142 -0
- data/app/helpers/reviewkit/application_helper.rb +12 -0
- data/app/helpers/reviewkit/asset_helper.rb +39 -0
- data/app/helpers/reviewkit/diff_helper.rb +230 -0
- data/app/helpers/reviewkit/flash_helper.rb +36 -0
- data/app/helpers/reviewkit/frame_helper.rb +37 -0
- data/app/helpers/reviewkit/icon_helper.rb +107 -0
- data/app/helpers/reviewkit/review_thread_helper.rb +54 -0
- data/app/models/concerns/reviewkit/notifies_lifecycle_events.rb +39 -0
- data/app/models/reviewkit/application_record.rb +7 -0
- data/app/models/reviewkit/comment.rb +29 -0
- data/app/models/reviewkit/current.rb +7 -0
- data/app/models/reviewkit/document.rb +79 -0
- data/app/models/reviewkit/review.rb +66 -0
- data/app/models/reviewkit/review_thread.rb +75 -0
- data/app/services/reviewkit/diffs/intraline_budget.rb +40 -0
- data/app/services/reviewkit/diffs/intraline_diff.rb +220 -0
- data/app/services/reviewkit/diffs/split_diff.rb +112 -0
- data/app/services/reviewkit/reviews/create.rb +57 -0
- data/app/views/layouts/reviewkit/application.html.erb +15 -0
- data/app/views/reviewkit/comments/_comment.html.erb +53 -0
- data/app/views/reviewkit/comments/_edit_form.html.erb +26 -0
- data/app/views/reviewkit/comments/_form.html.erb +16 -0
- data/app/views/reviewkit/review_threads/_bucket.html.erb +53 -0
- data/app/views/reviewkit/review_threads/_bucket_frame.html.erb +13 -0
- data/app/views/reviewkit/review_threads/_bucket_row.html.erb +55 -0
- data/app/views/reviewkit/review_threads/_edit_form.html.erb +29 -0
- data/app/views/reviewkit/review_threads/_thread.html.erb +87 -0
- data/app/views/reviewkit/reviews/_document.html.erb +41 -0
- data/app/views/reviewkit/reviews/_document_split.html.erb +73 -0
- data/app/views/reviewkit/reviews/_document_unified.html.erb +57 -0
- data/app/views/reviewkit/reviews/_edit_form.html.erb +35 -0
- data/app/views/reviewkit/reviews/_index_content.html.erb +160 -0
- data/app/views/reviewkit/reviews/_review_sidebar.html.erb +70 -0
- data/app/views/reviewkit/reviews/_show_content.html.erb +164 -0
- data/app/views/reviewkit/reviews/index.html.erb +11 -0
- data/app/views/reviewkit/reviews/show.html.erb +11 -0
- data/app/views/reviewkit/shared/_flash.html.erb +10 -0
- data/bin/console +4 -0
- data/bin/lint +4 -0
- data/bin/rails +14 -0
- data/bin/setup +9 -0
- data/bin/test +4 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20260331181500_create_reviewkit_reviews.rb +19 -0
- data/db/migrate/20260331181600_create_reviewkit_documents.rb +23 -0
- data/db/migrate/20260331181700_create_reviewkit_review_threads.rb +23 -0
- data/db/migrate/20260331181800_create_reviewkit_comments.rb +15 -0
- data/db/migrate/20260401093000_add_description_to_reviewkit_reviews.rb +7 -0
- data/lib/generators/reviewkit/controllers/controllers_generator.rb +24 -0
- data/lib/generators/reviewkit/controllers/templates/comments_controller_extension.rb +13 -0
- data/lib/generators/reviewkit/controllers/templates/review_threads_controller_extension.rb +13 -0
- data/lib/generators/reviewkit/controllers/templates/reviews_controller_extension.rb +19 -0
- data/lib/generators/reviewkit/install/install_generator.rb +52 -0
- data/lib/generators/reviewkit/install/templates/importmap.rb +3 -0
- data/lib/generators/reviewkit/install/templates/reviewkit.rb +19 -0
- data/lib/generators/reviewkit/models/models_generator.rb +24 -0
- data/lib/generators/reviewkit/models/templates/comment_extension.rb +21 -0
- data/lib/generators/reviewkit/models/templates/review_extension.rb +22 -0
- data/lib/generators/reviewkit/models/templates/review_thread_extension.rb +21 -0
- data/lib/generators/reviewkit/views/views_generator.rb +15 -0
- data/lib/reviewkit/configuration.rb +33 -0
- data/lib/reviewkit/engine.rb +67 -0
- data/lib/reviewkit/version.rb +5 -0
- data/lib/reviewkit.rb +26 -0
- data/lib/tasks/reviewkit_tasks.rake +12 -0
- data/sig/reviewkit.rbs +129 -0
- 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,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
|