Skip to content

Commit

Permalink
Feature: Move Dashboard project solutions to Hotwire.
Browse files Browse the repository at this point in the history
Because:
* We are replacing our React components with Hotwire.

This commit:
* Adds a title slot to project submission items - we display the lesson title when on the dashboard and the solutions users name when on a lesson page.
* Adds a users project submissions controller - when updating a solution from the dashboard we need to use a user specific endpoint to ensure the correct solution item is returned.
* Automatically like your own project when it is created - We need to disable liking your own submissions, at least for now:
  * The likes endpoint needs to return a full item component at the moment to allow sorting to work. This conflicts with needing different item titles on the dashboard and lesson pages. We'll be able to enable again soon ™️. The next phase of this project is to add a sorting feature to the solutions list, we'll be able to update just the like counter when that is built.
  * The compromise is to automatically like your project so nobody ever has to see their project sitting at 0 - kind of like how Reddit handles new posts.
* A few performance improvements by preloading lesson and user solutions when fetching solutions.
  • Loading branch information
KevinMulhern committed Jul 21, 2023
1 parent 36f9cb1 commit f252410
Show file tree
Hide file tree
Showing 25 changed files with 242 additions and 82 deletions.
8 changes: 4 additions & 4 deletions app/components/project_submissions/item_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<div id="<%= dom_id(project_submission) %>" data-id="<%= project_submission.id %>" data-sort-target="item" data-sort-code="<%= sort_code %>">
<div data-test-id="submission-item" class="relative py-6 border-solid border-t border-gray-300 flex flex-col md:flex-row justify-between md:items-center">

<div class="flex items-center mb-6 md:mb-0">
<%= render ProjectSubmissions::LikeComponent.new(project_submission) %>
<p class="truncate max-w-xs lgs:max-w-lg font-medium text-lg break-words"><%= project_submission.user.username %></p>
<div class="flex items-center mb-4 md:mb-0">
<%= render ProjectSubmissions::LikeComponent.new(project_submission:, current_users_submission: current_users_submission?) %>
<%= title %>
</div>

<div class="flex flex-row md:items-center">
Expand All @@ -27,7 +27,7 @@
class="hidden absolute right-0 z-10 mt-2 w-32 origin-top-right rounded-md bg-white dark:bg-gray-700 py-2 shadow-lg ring-1 ring-gray-900/5 dark:ring-gray-300/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="options-menu-0-button" tabindex="-1">

<% if project_submission.user == current_user %>
<%= link_to edit_lesson_v2_project_submission_path(project_submission.lesson, project_submission), class: 'text-gray-700 dark:text-gray-300 group flex items-center px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-gray-200', role: 'menuitem', tabindex: '-1', data: { turbo_frame: 'modal', test_id: 'edit-submission', action: 'click->visibility#off'} do %>
<%= link_to edit_path, class: 'text-gray-700 dark:text-gray-300 group flex items-center px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-gray-200', role: 'menuitem', tabindex: '-1', data: { turbo_frame: 'modal', test_id: 'edit-submission', action: 'click->visibility#off'} do %>
<%= inline_svg_tag 'icons/pencil-square.svg', class: 'mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300', aria: true, title: 'edit', desc: 'edit icon' %>
Edit
<% end %>
Expand Down
16 changes: 14 additions & 2 deletions app/components/project_submissions/item_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ class ItemComponent < ApplicationComponent
CURRENT_USER_SORT_CODE = 10_000_000 # current user's submission should always be first

with_collection_parameter :project_submission
renders_one :title, ProjectSubmissions::TitleComponent

def initialize(project_submission:, current_user:)
def initialize(project_submission:, current_user:, edit_path: nil)
@project_submission = project_submission
@current_user = current_user
@edit_path = edit_path
end

def render?
Expand All @@ -17,10 +19,20 @@ def render?

attr_reader :project_submission, :current_user

def current_users_submission?
project_submission.user == current_user
end

def sort_code
return CURRENT_USER_SORT_CODE if project_submission.user == current_user
return CURRENT_USER_SORT_CODE if current_users_submission?

project_submission.cached_votes_total
end

def edit_path
return @edit_path if @edit_path.present?

edit_lesson_v2_project_submission_path(project_submission.lesson, project_submission)
end
end
end
2 changes: 1 addition & 1 deletion app/components/project_submissions/like_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= turbo_frame_tag dom_id(project_submission, :likes) do %>
<%= button_to project_submission_v2_like_path(project_submission), method: http_action, class: 'text-gray-400 mr-4 flex items-center hint--top', data: { test_id: 'like-submission' }, aria: { label: 'Like submission' } do %>
<%= button_to project_submission_v2_like_path(project_submission), method: http_action, disabled: current_users_submission, class: "text-gray-400 mr-4 flex items-center #{'hint--top' unless current_users_submission}", data: { test_id: 'like-submission' }, aria: { label: 'Like submission' } do %>
<span class="mr-1" data-test-id="like-count"><%= project_submission.cached_votes_total %></span>
<%= inline_svg_tag 'icons/heart.svg', class: "h-5 w-5 #{bg_color_class}", aria: true, title: 'heart', desc: 'heart icon' %>
<% end %>
Expand Down
9 changes: 6 additions & 3 deletions app/components/project_submissions/like_component.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
module ProjectSubmissions
class LikeComponent < ApplicationComponent
def initialize(project_submission)
def initialize(project_submission:, current_users_submission: false)
@project_submission = project_submission
@current_users_submission = current_users_submission
end

private

attr_reader :project_submission
attr_reader :project_submission, :current_users_submission

def http_action
project_submission.liked? ? :delete : :post
end

def bg_color_class
project_submission.liked? ? 'text-teal-700' : 'text-gray-400'
return 'text-teal-700' if current_users_submission || project_submission.liked?

'text-gray-400'
end
end
end
5 changes: 5 additions & 0 deletions app/components/project_submissions/title_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% if url.present? %>
<%= link_to title, url, class: 'truncate max-w-xs lgs:max-w-lg font-medium text-lg break-words hover:text-gray-800', data: { turbo_frame: '_top' } %>
<% else %>
<p class="truncate max-w-xs lgs:max-w-lg font-medium text-lg break-words"><%= title %></p>
<% end %>
12 changes: 12 additions & 0 deletions app/components/project_submissions/title_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module ProjectSubmissions
class TitleComponent < ApplicationComponent
def initialize(title:, url: nil)
@title = title
@url = url
end

private

attr_reader :title, :url
end
end
2 changes: 2 additions & 0 deletions app/controllers/lessons/v2_project_submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def create

respond_to do |format|
if @project_submission.save
@project_submission.like!(current_user)

format.html { redirect_to lesson_path(@lesson), notice: 'Project submitted' }
format.turbo_stream
else
Expand Down
27 changes: 27 additions & 0 deletions app/controllers/users/project_submissions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Users
class ProjectSubmissionsController < ApplicationController
before_action :authenticate_user!

def edit
@project_submission = current_user.project_submissions.find(params[:id])
end

def update
@project_submission = current_user.project_submissions.find(params[:id])

respond_to do |format|
if @project_submission.update(project_submission_params)
format.turbo_stream
else
format.html { render :edit, status: :unprocessable_entity }
end
end
end

private

def project_submission_params
params.require(:project_submission).permit(:repo_url, :live_preview_url, :is_public)
end
end
end
1 change: 1 addition & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ class UsersController < ApplicationController

def show
@courses = current_user.path.courses
@project_submissions = current_user.project_submissions.includes(:lesson).order(created_at: :desc)
end
end
1 change: 1 addition & 0 deletions app/queries/lesson_project_submissions_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def current_user_submission
def public_submissions
lesson.project_submissions
.only_public
.includes(:user)
.not_removed_by_admin
.where.not(user: current_user)
.order(cached_votes_total: :desc, created_at: :desc)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<%= turbo_stream.prepend 'submissions-list' do %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission: @project_submission, current_user:) %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission: @project_submission, current_user:) do |component| %>
<%= component.with_title(title: current_user.username, url: dashboard_path) %>
<% end %>
<% end %>
<%= turbo_stream.remove 'project-solutions-blank-state' %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/lessons/v2_project_submissions/edit.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<%= render ModalComponent.new(title: 'Update your project') do %>
<%= render 'lessons/v2_project_submissions/form', project_submission: @project_submission, url: lesson_v2_project_submission_path(@lesson, @project_submission) %>
<%= render 'project_submissions/form', project_submission: @project_submission, url: lesson_v2_project_submission_path(@lesson, @project_submission) %>
<% end %>
11 changes: 9 additions & 2 deletions app/views/lessons/v2_project_submissions/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@

<%= turbo_frame_tag 'submissions-list', data: { test_id: 'submissions-list', controller: 'sort' } do %>
<% if @lesson.project_submissions.any? %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission: @current_user_submission, current_user:) %>
<%= render ProjectSubmissions::ItemComponent.with_collection(@project_submissions, current_user:) %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission: @current_user_submission, current_user:) do |component| %>
<%= component.with_title(title: current_user.username, url: dashboard_path) %>
<% end %>
<% @project_submissions.each do |project_submission| %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission:, current_user:) do |component| %>
<%= component.with_title(title: project_submission.user.username) %>
<% end %>
<% end %>
<% else %>
<% render 'lessons/v2_project_submissions/blank_state' %>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/lessons/v2_project_submissions/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<%= render ModalComponent.new(title: 'Submit your project') do %>
<%= render 'lessons/v2_project_submissions/form', project_submission: @project_submission, url: lesson_v2_project_submissions_path(@lesson) %>
<%= render 'project_submissions/form', project_submission: @project_submission, url: lesson_v2_project_submissions_path(@lesson) %>
<% end %>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<%= turbo_stream.replace @project_submission do %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission: @project_submission, current_user:) %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission: @project_submission, current_user:) do |component| %>
<%= component.with_title(title: current_user.username, url: dashboard_path) %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<%= turbo_stream.replace @project_submission do %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission: @project_submission, current_user:) %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission: @project_submission, current_user:) do |component| %>
<%= component.with_title(title: @project_submission.user.username) %>
<% end %>
<% end %>
28 changes: 19 additions & 9 deletions app/views/users/_project_submissions.html.erb
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
<div class="mb-12">
<h2 class="text-gray-700 text-2xl font-medium text-center mb-4 dark:text-gray-300">Project Submissions</h2>

<% if project_submissions.any? %>
<%= react_component(
'project-submissions/user-project-submissions',
{
userId: current_user&.id,
submissions: project_submissions.map { |submission| ProjectSubmissionSerializer.as_json(submission, current_user) },
}
) %>
<% if project_submissions.any? %>
<% if Feature.enabled?(:v2_project_submissions, current_user) %>
<div data-test-id="user-submissions-list">
<% project_submissions.each do |project_submission| %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission:, current_user:, edit_path: edit_users_project_submission_path(project_submission)) do |component| %>
<%= component.with_title(title: project_submission.lesson.display_title, url: lesson_path(project_submission.lesson)) %>
<% end %>
<% end %>
</div>
<% else %>
<h3 class="text-gray-500 text-center text-lg dark:text-gray-400">No submissions yet</h3>
<%= react_component(
'project-submissions/user-project-submissions',
{
userId: current_user&.id,
submissions: project_submissions.map { |submission| ProjectSubmissionSerializer.as_json(submission, current_user) },
}
) %>
<% end %>
<% else %>
<h3 class="text-gray-500 text-center text-lg dark:text-gray-400">No submissions yet</h3>
<% end %>
</div>
3 changes: 3 additions & 0 deletions app/views/users/project_submissions/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= render ModalComponent.new(title: 'Update your project') do %>
<%= render 'project_submissions/form', project_submission: @project_submission, url: users_project_submission_path(@project_submission) %>
<% end %>
5 changes: 5 additions & 0 deletions app/views/users/project_submissions/update.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<%= turbo_stream.replace @project_submission do %>
<%= render ProjectSubmissions::ItemComponent.new(project_submission: @project_submission, current_user:, edit_path: edit_users_project_submission_path(@project_submission)) do |component| %>
<%= component.with_title(title: @project_submission.lesson.display_title, url: lesson_path(@project_submission.lesson)) %>
<% end %>
<% end %>
2 changes: 1 addition & 1 deletion app/views/users/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<%= render partial: 'skill', collection: @courses, as: :course %>
</div>

<%= render 'project_submissions', project_submissions: current_user.project_submissions %>
<%= render 'project_submissions', project_submissions: @project_submissions %>
</div>

<%= render 'shared/bottom_cta',
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
resources :paths, only: :create
resources :progress, only: :destroy
resource :profile, only: %i[edit update]
resources :project_submissions, only: %i[edit update]
end

namespace :lessons do
Expand Down
75 changes: 20 additions & 55 deletions spec/system/v2_lesson_project_submissions/like_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,32 @@
let(:user) { create(:user) }
let(:lesson) { create(:lesson, :project) }

context "when liking other users' submissions" do
before do
Flipper.enable(:v2_project_submissions)
create(:project_submission, lesson:)
before do
Flipper.enable(:v2_project_submissions)
create(:project_submission, lesson:)

sign_in(user)
visit lesson_path(lesson)
end

after do
Flipper.disable(:v2_project_submissions)
end

it 'you can like another users submission' do
within(:test_project_submission, 1) do
expect(find(:test_id, 'like-count')).to have_content('0')
find(:test_id, 'like-submission').click
expect(find(:test_id, 'like-count')).to have_content('1')
end
end

it 'you can unlike another users submission' do
within(:test_project_submission, 1) do
find(:test_id, 'like-submission').click
expect(find(:test_id, 'like-count')).to have_content('1')
find(:test_id, 'like-submission').click
expect(find(:test_id, 'like-count')).to have_content('0')
end
end
sign_in(user)
visit lesson_path(lesson)
end

context 'when liking your own submission' do
before do
Flipper.enable(:v2_project_submissions)
create(:project_submission, lesson:, user:)

sign_in(user)
visit lesson_path(lesson)
end

after do
Flipper.disable(:v2_project_submissions)
end
after do
Flipper.disable(:v2_project_submissions)
end

it 'you can like your submission' do
within(:test_project_submission, 1) do |submission|
expect(submission).to have_content(user.username)
expect(find(:test_id, 'like-count')).to have_content('0')
find(:test_id, 'like-submission').click
expect(find(:test_id, 'like-count')).to have_content('1')
end
it 'you can like another users submission' do
within(:test_project_submission, 1) do
expect(find(:test_id, 'like-count')).to have_content('0')
find(:test_id, 'like-submission').click
expect(find(:test_id, 'like-count')).to have_content('1')
end
end

it 'you can unlike your submission' do
within(:test_project_submission, 1) do |submission|
expect(submission).to have_content(user.username)
find(:test_id, 'like-submission').click
expect(find(:test_id, 'like-count')).to have_content('1')
find(:test_id, 'like-submission').click
expect(find(:test_id, 'like-count')).to have_content('0')
end
it 'you can unlike another users submission' do
within(:test_project_submission, 1) do
find(:test_id, 'like-submission').click
expect(find(:test_id, 'like-count')).to have_content('1')
find(:test_id, 'like-submission').click
expect(find(:test_id, 'like-count')).to have_content('0')
end
end
end
35 changes: 35 additions & 0 deletions spec/system/v2_user_project_submissions/delete_submission_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'rails_helper'

RSpec.describe 'Deleting a Project Submission on the Dashboard' do
let(:user) { create(:user) }
let(:lesson) { create(:lesson, :project) }

before do
Flipper.enable(:v2_project_submissions)

create(:project_submission, user:, lesson:)
sign_in(user)
visit dashboard_path
end

after do
Flipper.disable(:v2_project_submissions)
end

it 'successfully deletes a submission' do
sleep 0.1 # it will not open the dropdown without this
within(:test_id, 'user-submissions-list') do
expect(page).to have_content(lesson.title)
end

find(:test_id, 'submission-action-menu-btn').click

page.accept_confirm do
find(:test_id, 'delete-submission').click
end

within(:test_id, 'user-submissions-list') do
expect(page).not_to have_content(lesson.title)
end
end
end
Loading

0 comments on commit f252410

Please sign in to comment.