From 4bc90209563ef4f01ca86ca3ca64ac3cb5778fe3 Mon Sep 17 00:00:00 2001 From: Mac Siri Date: Tue, 24 Apr 2018 14:30:52 -0400 Subject: [PATCH] Implement comment preview feature (#223) * Create #preview endpoint for CommentController * Create comment preview button WIP * Implement preview feature for reply comment W * Style preview button CSS WIP * Style preview button CSS WIP * Tweak markdown toggle logic and add styling * Update comment feature spec * Add error handling for comment preview & fix lint * Extract handleCommentPreview into it's own file * Adjust functionality for comment preview [WIP] * Modify comment preview functionality and CSS * Adjust code style based on linter --- app/assets/javascripts/initializePage.js.erb | 1 + .../initializers/initializeCommentPreview.js | 50 +++++++++++++++ .../initializeCommentsPage.js.erb | 3 + .../utilities/buildCommentFormHTML.js.erb | 14 +++- app/assets/javascripts/utilities/sendFetch.js | 12 ++++ app/assets/stylesheets/comments.scss | 64 +++++++++++++++---- app/controllers/comments_controller.rb | 59 ++++++++++------- app/labor/hex_comparer.rb | 4 +- app/views/comments/_form.html.erb | 7 +- config/routes.rb | 1 + spec/features/user_fills_out_comment_spec.rb | 48 +++++++++----- spec/requests/comments_spec.rb | 24 +++++++ 12 files changed, 226 insertions(+), 61 deletions(-) create mode 100644 app/assets/javascripts/initializers/initializeCommentPreview.js diff --git a/app/assets/javascripts/initializePage.js.erb b/app/assets/javascripts/initializePage.js.erb index 9c513224a..0eba87429 100644 --- a/app/assets/javascripts/initializePage.js.erb +++ b/app/assets/javascripts/initializePage.js.erb @@ -37,6 +37,7 @@ function callInitalizers(){ initializeAnalytics(); initializeCommentDropdown(); initializeSettings(); + initializeCommentPreview(); initializeAdditionalContentBoxes(); if (!initializeLiveArticle.called){ initializeLiveArticle(); diff --git a/app/assets/javascripts/initializers/initializeCommentPreview.js b/app/assets/javascripts/initializers/initializeCommentPreview.js new file mode 100644 index 000000000..a9d3cffe9 --- /dev/null +++ b/app/assets/javascripts/initializers/initializeCommentPreview.js @@ -0,0 +1,50 @@ +function initializeCommentPreview() { + if (document.getElementById('preview-button')) { + document.getElementById('preview-button').onclick = handleCommentPreview; + } +} + +function handleCommentPreview(e) { + e.preventDefault(); + var form = e.target.parentElement.parentElement; + var textArea = form.querySelector('textarea'); + if (textArea.value !== '') { + var previewDiv = form.querySelector('.comment-preview-div'); + var previewButton = form.querySelector('.comment-action-preview'); + if (previewButton.innerHTML.indexOf('PREVIEW') > -1) { + textArea.classList.toggle('preview-loading'); + getAndShowPreview(previewDiv, textArea); + previewButton.innerHTML = 'MARKDOWN'; + } else { + previewDiv.classList.toggle('preview-toggle'); + textArea.classList.toggle('preview-toggle'); + previewButton.innerHTML = 'PREVIEW'; + } + } +} + +function getAndShowPreview(previewDiv, textArea) { + var commentMarkdown = textArea.value; + var payload = JSON.stringify({ + comment: { + body_markdown: commentMarkdown, + }, + }); + getCsrfToken() + .then(sendFetch('comment-preview', payload)) + .then(function(response) { + return response.json(); + }) + .then(successCb) + .catch(function(err) { + console.log('error!'); + console.log(err); + }); + + function successCb(body) { + previewDiv.classList.toggle('preview-toggle'); + textArea.classList.toggle('preview-loading'); + textArea.classList.toggle('preview-toggle'); + previewDiv.innerHTML = body.processed_html; + } +} diff --git a/app/assets/javascripts/initializers/initializeCommentsPage.js.erb b/app/assets/javascripts/initializers/initializeCommentsPage.js.erb index 51e6d796d..9128e4365 100644 --- a/app/assets/javascripts/initializers/initializeCommentsPage.js.erb +++ b/app/assets/javascripts/initializers/initializeCommentsPage.js.erb @@ -228,6 +228,9 @@ function handleCommentSubmit(event) { mainCommentsForm.classList.remove("submitting"); textArea.value = ""; textArea.classList = ""; + var preview = document.getElementById("preview-div"); + preview.classList.add("preview-toggle"); + preview.innerHTML = ""; var container = document.getElementById("comment-trees-container"); container.insertBefore(newNode, container.firstChild); } diff --git a/app/assets/javascripts/utilities/buildCommentFormHTML.js.erb b/app/assets/javascripts/utilities/buildCommentFormHTML.js.erb index c216f177f..2b79323fc 100644 --- a/app/assets/javascripts/utilities/buildCommentFormHTML.js.erb +++ b/app/assets/javascripts/utilities/buildCommentFormHTML.js.erb @@ -9,6 +9,10 @@ function buildCommentFormHTML(commentableId, commentableType, parentId) { ' } var randomIdNumber = Math.floor(Math.random() * 1991) + + var previewDiv = '
' + var previewButton = '' + return '
\ \ \ @@ -16,6 +20,7 @@ function buildCommentFormHTML(commentableId, commentableType, parentId) { \ \ \ + '+previewDiv+'\ '+codeOfConductHTML+'\ \ \ @@ -29,8 +34,11 @@ function buildCommentFormHTML(commentableId, commentableType, parentId) { \ \ \ - CANCEL\ - \ +
\ + CANCEL\ + '+previewButton+'\ + \ +
\
'; } @@ -38,4 +46,4 @@ function cancel(event, el) { event.preventDefault(); replaceActionButts(el.parentNode.parentNode.parentNode.parentNode); initializeCommentsPage(); -} \ No newline at end of file +} diff --git a/app/assets/javascripts/utilities/sendFetch.js b/app/assets/javascripts/utilities/sendFetch.js index 40a8522e9..9e0ba27cd 100644 --- a/app/assets/javascripts/utilities/sendFetch.js +++ b/app/assets/javascripts/utilities/sendFetch.js @@ -61,6 +61,18 @@ function sendFetch(switchStatement, body) { credentials: 'same-origin', }); }; + case 'comment-preview': + return function(csrfToken) { + return window.fetch('/comments/preview', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken, + }, + body: body, + credentials: 'same-origin', + }); + }; default: console.log('A wrong switchStatement was used.'); break; diff --git a/app/assets/stylesheets/comments.scss b/app/assets/stylesheets/comments.scss index 4b1c7dd67..4647c777f 100644 --- a/app/assets/stylesheets/comments.scss +++ b/app/assets/stylesheets/comments.scss @@ -125,7 +125,7 @@ a.header-link{ position: absolute; left:calc(0.2vw + 43px); bottom:0px; - width:calc(100% - 145px); + width: initial; text-align:left; .uploaded-image { vertical-align:11px; @@ -248,9 +248,57 @@ a.header-link{ &.embiggened-max{ height:calc(30vw + 18px); } + &.preview-loading{ + background: white url(image_path('loading-ellipsis.svg')) no-repeat center center; + background-size: 50px; + } + } + .preview-toggle { + display: none; + } + .comment-preview-div { + padding: 10px 20px 2px; + min-height:calc(13vw + 6px); + text-align: left; + font-size: 19px; + font-family: $helvetica; + background: lighten($yellow, 19%); + box-shadow: $shadow; + width: calc(100% - 80px); + margin: 19px auto 8px; + border: 2px solid $yellow; + p.loading-message{ + opacity:0.6; + } + pre { + overflow-x: auto; + } } .actions{ text-align:right; + + .comment-action-button { + font-family: $helvetica-condensed; + font-stretch:condensed; + color:white; + border:0px; + font-size:13px; + font-weight:500; + margin-top:3px; + padding:5px 12px; + border-radius:3px; + cursor:pointer; + appearance: none; + } + + .comment-action-preview { + background: $dark-gray; + z-index: 10; + } + } + .reply-actions { + margin-bottom: 0; + padding-bottom: 0; } .code-of-conduct { display: block; @@ -272,17 +320,6 @@ a.header-link{ input[type="submit"]{ margin-right:19px; background: $bold-blue; - font-family: $helvetica-condensed; - font-stretch:condensed; - color:white; - border:0px; - font-size:13px; - font-weight:500; - margin-top:3px; - padding:5px 12px; - border-radius:3px; - cursor:pointer; - appearance: none; } &.submitting{ input[type="submit"]{ @@ -677,7 +714,7 @@ a.header-link{ height:20px; } .editor-image-upload{ - width:calc(100% - 150px); + width: initial; text-align:left; .uploaded-image { width: calc( 93% - 54px); @@ -747,6 +784,7 @@ a.header-link{ input[type="submit"]{ padding:5px 8px; margin-bottom:5px; + margin-right:7px; } .cancel{ diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index d9dc1d4ab..a8b0219a5 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,7 +1,8 @@ class CommentsController < ApplicationController - before_action :set_comment, only: [ :update, :destroy] + before_action :set_comment, only: %i[update destroy] before_action :set_cache_control_headers, only: [:index] - before_action :raise_banned, only: [:create,:update] + before_action :raise_banned, only: %i[create update] + before_action :authenticate_user!, only: %i[preview create] # GET /comments # GET /comments.json @@ -16,7 +17,7 @@ class CommentsController < ApplicationController if @podcast @user = @podcast - @commentable = @user.podcast_episodes.find_by_slug(params[:slug]) or not_found + (@commentable = @user.podcast_episodes.find_by_slug(params[:slug])) || not_found else @user = User.find_by_username(params[:username]) || Organization.find_by_slug(params[:username]) || @@ -51,10 +52,6 @@ class CommentsController < ApplicationController # POST /comments # POST /comments.json def create - unless current_user - redirect_to "/" - return - end raise if RateLimitChecker.new(current_user).limit_by_situation("comment_creation") @comment = Comment.new(comment_params) @comment.user_id = current_user.id @@ -101,7 +98,7 @@ class CommentsController < ApplicationController # PATCH/PUT /comments/1.json def update raise unless @comment.user_id == current_user.id - if @comment.update(comment_update_params.merge({ edited_at: DateTime.now })) + if @comment.update(comment_update_params.merge(edited_at: DateTime.now)) Mention.create_all(@comment) redirect_to "#{@comment.commentable.path}/comments/#{@comment.id_code_generated}", notice: "Comment was successfully updated." else @@ -125,29 +122,43 @@ class CommentsController < ApplicationController end def delete_confirm - unless current_user && Comment.where(id: params[:id_code].to_i(26), user_id: current_user.id ).first + unless current_user && Comment.where(id: params[:id_code].to_i(26), user_id: current_user.id).first @comment = Comment.find(params[:id_code].to_i(26)) redirect_to @comment.path return end - @comment = Comment.where(id: params[:id_code].to_i(26), user_id: current_user.id ).first + @comment = Comment.where(id: params[:id_code].to_i(26), user_id: current_user.id).first + end + + def preview + begin + fixed_body_markdown = MarkdownFixer.fix_for_preview(comment_params[:body_markdown]) + parsed_markdown = MarkdownParser.new(fixed_body_markdown) + processed_html = parsed_markdown.finalize + rescue StandardError => e + processed_html = "

😔 There was a error in your markdown


#{e}

" + end + respond_to do |format| + format.json { render json: { processed_html: processed_html }, status: 200 } + end end private - # Use callbacks to share common setup or constraints between actions. - def set_comment - @comment = Comment.find(params[:id]) - end - # Never trust parameters from the scary internet, only allow the white list through. - def comment_params - params.require(:comment).permit(:body_markdown, - :commentable_id, - :commentable_type, - :parent_id) - end + # Use callbacks to share common setup or constraints between actions. + def set_comment + @comment = Comment.find(params[:id]) + end - def comment_update_params - params.require(:comment).permit(:body_markdown) - end + # Never trust parameters from the scary internet, only allow the white list through. + def comment_params + params.require(:comment).permit(:body_markdown, + :commentable_id, + :commentable_type, + :parent_id) + end + + def comment_update_params + params.require(:comment).permit(:body_markdown) + end end diff --git a/app/labor/hex_comparer.rb b/app/labor/hex_comparer.rb index 3bd9a09a2..0ebc982e3 100644 --- a/app/labor/hex_comparer.rb +++ b/app/labor/hex_comparer.rb @@ -1,14 +1,14 @@ class HexComparer attr_accessor :hexes, :amount - def initialize(hexes, amount=1) + def initialize(hexes, amount = 1) @hexes = hexes @amount = amount end def order hexes.sort - end + end def smallest order.first diff --git a/app/views/comments/_form.html.erb b/app/views/comments/_form.html.erb index 6bca72a94..58bfc5e82 100644 --- a/app/views/comments/_form.html.erb +++ b/app/views/comments/_form.html.erb @@ -26,7 +26,9 @@ id: "text-area", readonly: !user_signed_in?, autofocus: @comment.persisted?, - required: true %> + required: true + %> +
@@ -45,7 +47,8 @@ <% end %>
- <%= f.submit "SUBMIT", id: "submit-button", onclick:"validateField(event)" %> + + <%= f.submit "SUBMIT", id: "submit-button", onclick:"validateField(event)", class:"comment-action-button" %>
<% end %> diff --git a/config/routes.rb b/config/routes.rb index 9f9a63681..75ada0b74 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -155,6 +155,7 @@ Rails.application.routes.draw do get "/sponsors" => "pages#sponsors" get "/search" => "stories#search" post "articles/preview" => "articles#preview" + post "comments/preview" => "comments#preview" get "/freestickers" => "giveaways#new" get "/freestickers/edit" => "giveaways#edit" get "/scholarship", to: redirect("/p/scholarships") diff --git a/spec/features/user_fills_out_comment_spec.rb b/spec/features/user_fills_out_comment_spec.rb index b975b03d7..54a6f352a 100644 --- a/spec/features/user_fills_out_comment_spec.rb +++ b/spec/features/user_fills_out_comment_spec.rb @@ -1,30 +1,44 @@ require "rails_helper" -feature "Creating Comment" do +RSpec.describe "Creating Comment", type: :feature, js: true do let(:user) { create(:user) } let(:raw_comment) { Faker::Lorem.paragraph } let(:article) do create(:article, user_id: user.id, show_comments: true) end - background do + before do login_via_session_as user end - # scenario "User fills out comment box normally" do - # visit article.path.to_s - # fill_in "text-area", with: raw_comment - # click_button("SUBMIT") - # expect(page).to have_css(".comment-action-buttons") - # expect(page).to have_text(raw_comment) - # end + it "User fills out comment box normally" do + visit article.path.to_s + fill_in "text-area", with: raw_comment + find(".checkbox").click + click_button("SUBMIT") + expect(page).to have_text(raw_comment) + end - # scenario "User replies to a comment" do - # create(:comment, commentable_id: article.id, user_id: user.id) - # visit article.path.to_s - # find(".toggle-reply-form").click - # fill_in "text-area", with: raw_comment - # click_button("SUBMIT") - # expect(page).to have_text(raw_comment) - # end + it "User fill out commen box then click previews and submit" do + visit article.path.to_s + fill_in "text-area", with: raw_comment + find(".checkbox").click + click_button("PREVIEW") + expect(page).to have_text(raw_comment) + expect(page).to have_text("MARKDOWN") + click_button("MARKDOWN") + # expect(page).to have_text(raw_comment) + expect(page).to have_text("PREVIEW") + click_button("SUBMIT") + expect(page).to have_text(raw_comment) + end + + it "User replies to a comment" do + create(:comment, commentable_id: article.id, user_id: user.id) + visit article.path.to_s + find(".toggle-reply-form").click + find(:xpath, "//div[@class='actions']/form[@class='new_comment']/textarea").set(raw_comment) + find(:xpath, "//div[contains(@class, 'reply-actions')]/input[@name='commit']").click + expect(page).to have_text(raw_comment) + end end diff --git a/spec/requests/comments_spec.rb b/spec/requests/comments_spec.rb index 372f64f5c..97b9e82c7 100644 --- a/spec/requests/comments_spec.rb +++ b/spec/requests/comments_spec.rb @@ -62,4 +62,28 @@ RSpec.describe "Comments", type: :request do end end end + + describe "POST /comments/preview" do + it "returns 401 if user is not logged in" do + post "/comments/preview", params: { comment: { body_markdown: "hi"}}, headers: { HTTP_ACCEPT: "application/json" } + expect(response).to have_http_status(401) + end + + context "when logged-in" do + before do + login_as user + post "/comments/preview", + params: { comment: { body_markdown: "hi" } }, + headers: { HTTP_ACCEPT: "application/json" } + end + + it "returns 200 on good request" do + expect(response).to have_http_status(200) + end + + it "returns json" do + expect(response.content_type).to eq("application/json") + end + end + end end