Skip to content

Commit

Permalink
Slug editor in the dashboard page for a video
Browse files Browse the repository at this point in the history
Adds a modal that allows to change or reset the slug of a video,
conveniently and without having to access the production server console.
  • Loading branch information
danirod committed Sep 23, 2024
1 parent 6961d11 commit 47e420e
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 1 deletion.
10 changes: 10 additions & 0 deletions app/controllers/dashboard/videos_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ def move
end
end

def update_slug
@request = ModalSlug.new(update_slug_params)
@request.update_slug!
redirect_to [:dashboard, @request.video.playlist, @request.video], notice: t('.updated')
end

private

def move!(video, direction)
Expand Down Expand Up @@ -80,5 +86,9 @@ def video_params
def filter_criteria
params.permit(:playlist_id).compact_blank
end

def update_slug_params
params.require(:modal_slug).permit(:id, :slug, :reset)
end
end
end
43 changes: 43 additions & 0 deletions app/javascript/dashboard/slug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Modal } from "bootstrap";
const slugModal = document.querySelector("#slugModal");

if (slugModal) {
const titlePlaceholder = slugModal.querySelector("[data-target-title]");
const idPlaceholder = slugModal.querySelector("[data-target-id]");
const slugPlaceholder = slugModal.querySelector("[data-target-slug]");
const resetCheckbox = slugModal.querySelector("#modal_slug_reset");

resetCheckbox.addEventListener("change", () => {
if (resetCheckbox.checked) {
slugPlaceholder.setAttribute("disabled", "disabled");
} else {
slugPlaceholder.removeAttribute("disabled");
}
});

slugModal.addEventListener("shown.bs.modal", () => {
slugPlaceholder.select();
});

function installSlugModalOpen(node) {
node.addEventListener("click", () => {
const id = node.getAttribute("data-id");
const title = node.getAttribute("data-title");
const slug = node.getAttribute("data-slug");
console.log(`Opening modal for ${id} x ${slug}`);

titlePlaceholder.innerText = title;
idPlaceholder.value = id;
slugPlaceholder.value = slug;

const modal = new Modal(slugModal);
modal.show();
});
}

const slugModalOpen = document.querySelector("#slugModalOpen");

if (slugModalOpen) {
installSlugModalOpen(slugModalOpen);
}
}
1 change: 1 addition & 0 deletions app/javascript/packs/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ import "../dashboard/searches";
import "../dashboard/shownotes";
import "../dashboard/topics";
import "../dashboard/table";
import "../dashboard/slug";
24 changes: 24 additions & 0 deletions app/models/modal_slug.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

class ModalSlug
include ActiveModel::API
include ActiveModel::Attributes

attribute :id, :integer
attribute :slug, :string
attribute :reset, :boolean, default: false
validates :id, :slug, presence: true

def update_slug!
next_slug = if reset
nil
else
slug
end
video.update(slug: next_slug)
end

def video
@video ||= Video.find(id)
end
end
16 changes: 16 additions & 0 deletions app/views/dashboard/videos/_slug.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.modal.fade#slugModal{tabindex: -1, aria: { label: 'slugModalLabel', hidden: true } }
.modal-dialog
.modal-content
.modal-header
%h5.modal-title#slugModalLabel Actualizar identificador
%button.btn-close{type: 'button', data: { 'bs-dismiss' => 'modal' }, aria: { label: 'Cerrar' }}
= simple_form_for ModalSlug.new, url: update_slug_dashboard_videos_path, method: 'put' do |form|
= form.input :id, as: :hidden, input_html: { data: { 'target-id': '' } }
.modal-body
Actualizar el identificador de <strong data-target-title>este vídeo</strong>.
%fieldset.mt-3
= form.input :slug, input_html: {data: { 'target-slug': "" }}
= form.input :reset, as: :boolean, wrapper_html: {class: 'mt-3' }
.modal-footer
%button.btn.btn-secondary{type: 'button', data: { 'bs-dismiss' => 'modal' }} Cancelar
= form.submit 'Actualizar', class: 'btn btn-primary'
10 changes: 10 additions & 0 deletions app/views/dashboard/videos/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
<td><%= t('.twitch_id') %></td>
<td><%= link_to(@video.twitch_id, "https://twitch.tv/videos/#{@video.twitch_id}") if @video.twitch_id.present? %></td>
</tr>
<tr>
<td><%= t('.slug') %></td>
<td>
<%= @video.slug %>
<%= content_tag(:button, id: 'slugModalOpen', data: { id: @video.id, title: @video.title, slug: @video.slug }, class: 'btn btn-outline-secondary btn-sm') do %>
<i class="bi bi-pencil"></i> <%= t('.edit') %>
<% end %>
</td>
<tr>
<td><%= t('.duration') %></td>
<td><%= running_time @video.duration %> (<%= t('.seconds', count: @video.duration) %>)</td>
Expand Down Expand Up @@ -93,3 +101,5 @@
</td>
</tr>
</table>

<%= render 'slug' %>
4 changes: 4 additions & 0 deletions config/locales/dashboard.es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ es:
playlists: Listas
really_destroy: Eliminar una lista no tiene vuelta atrás. Sólo puedes eliminar listas que no tengan vídeos en su interior.
replaced_with: Reemplazada por
slug: Identificador
thumbnail: Miniatura
tags: Etiquetas
title: Título
Expand Down Expand Up @@ -403,6 +404,7 @@ es:
one: '%{count} segundo'
other: '%{count} segundos'
show_notes: Notas del episodio
slug: Identificador
tags: Etiquetas
thumbnail: Miniatura
title: Título
Expand All @@ -412,6 +414,8 @@ es:
youtube_id: ID de YouTube
update:
updated: Vídeo actualizado correctamente.
update_slug:
updated: Identificador del vídeo actualizado correctamente.
layouts:
dashboard:
header:
Expand Down
9 changes: 9 additions & 0 deletions config/locales/model.es.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
---
es:
activemodel:
attributes:
modal_slug:
reset: Reestablecer al valor por defecto
slug: Identificador
models:
modal_slug:
one: Identificador de vídeo
other: Identificadores de vídeo
activerecord:
attributes:
opinion:
Expand Down
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
get :order
put :reorder
end
resources :videos, only: %i[index new create]
resources :videos, only: %i[index new create] do
put :update_slug, path: 'modal/slug', on: :collection
end
resources :playlists do
get :videos, on: :member
get :tags, on: :member
Expand Down
53 changes: 53 additions & 0 deletions spec/features/dashboard/video_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Dashboard video', :js do
let(:user) { create(:user) }
let(:video) { create(:video) }

before do
Capybara.app_host = 'http://dashboard.lvh.me:9080'
Capybara.server_port = 9080
login_as user, scope: :user
end

after do
Capybara.app_host = nil
Capybara.server_port = nil
end

it 'can update the slug of a video' do
video.update(title: 'An updated video', slug: 'original-slug')

visit dashboard_playlist_video_path(video.slug, playlist_id: video.playlist.slug)
find_by_id('slugModalOpen').click

within '#slugModal' do
fill_in 'Identificador', with: 'my-custom-slug'
click_on 'Actualizar'
end

aggregate_failures do
expect(video.reload.slug).to eq 'my-custom-slug'
expect(page).to have_text 'Identificador del vídeo actualizado correctamente.'
end
end

it 'can reset the slug of a video' do
video.update(title: 'An updated video', slug: 'original-slug')

visit dashboard_playlist_video_path(video.slug, playlist_id: video.playlist.slug)
find_by_id('slugModalOpen').click

within '#slugModal' do
check 'Reestablecer al valor por defecto'
click_on 'Actualizar'
end

aggregate_failures do
expect(page).to have_text 'Identificador del vídeo actualizado correctamente.'
expect(video.reload.slug).to eq 'an-updated-video'
end
end
end
22 changes: 22 additions & 0 deletions spec/models/modal_slug_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ModalSlug do
let(:video) { create(:video, title: 'My video', slug: 'old-slug') }
let(:instance) { described_class.new(id: video.id) }

describe 'when changing the slug of a video' do
it 'supports a custom slug' do
instance.slug = 'another-slug'
instance.update_slug!
expect(video.reload.slug).to eq 'another-slug'
end

it 'supports a pre-generated slug' do
instance.reset = true
instance.update_slug!
expect(video.reload.slug).to eq 'my-video'
end
end
end
2 changes: 2 additions & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
config.include Warden::Test::Helpers
config.include ViewComponent::TestHelpers, type: :component

config.include Devise::Test::IntegrationHelpers, type: :request

Capybara.javascript_driver = :selenium_headless
Capybara.enable_aria_label = true

Expand Down
68 changes: 68 additions & 0 deletions spec/requests/dashboard/modal_slug_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Update slug' do
let(:video) do
create(:video, title: 'Original slug').tap do |v|
v.update(slug: 'old-slug')
end
end

before do
host! 'dashboard.lvh.me'
end

it 'passes the slug smoke test' do
expect(video.slug).to eq 'old-slug'
end

describe 'without a login' do
it 'redirects to login' do
put '/videos/modal/slug', params: { modal_slug: { id: video.id, slug: 'new-slug' } }
expect(response).to redirect_to '/users/sign_in'
end

it 'does not change the video' do
put '/videos/modal/slug', params: { modal_slug: { id: video.id, slug: 'new-slug' } }
video.reload
expect(video).to have_attributes(slug: 'old-slug')
end
end

describe 'with a login' do
let(:user) { create(:user) }

before do
sign_in user, scope: :user
end

describe 'when the request is valid for changing attributes' do
it 'redirects to the video page' do
put '/videos/modal/slug', params: { modal_slug: { id: video.id, slug: 'new-slug' } }
video.reload
expect(response).to redirect_to "/playlists/#{video.playlist.slug}/videos/#{video.slug}"
end

it 'updates the video slug' do
put '/videos/modal/slug', params: { modal_slug: { id: video.id, slug: 'new-slug' } }
video.reload
expect(video).to have_attributes(slug: 'new-slug')
end
end

describe 'when the request is valid for resetting attributes' do
it 'redirects to the video page' do
put '/videos/modal/slug', params: { modal_slug: { id: video.id, reset: '1' } }
video.reload
expect(response).to redirect_to "/playlists/#{video.playlist.slug}/videos/#{video.slug}"
end

it 'updates the video slug' do
put '/videos/modal/slug', params: { modal_slug: { id: video.id, reset: '1' } }
video.reload
expect(video).to have_attributes(slug: 'original-slug')
end
end
end
end

0 comments on commit 47e420e

Please sign in to comment.