diff --git a/app/controllers/collaborations_controller.rb b/app/controllers/collaborations_controller.rb index 2caa1272..8140b03e 100644 --- a/app/controllers/collaborations_controller.rb +++ b/app/controllers/collaborations_controller.rb @@ -5,9 +5,10 @@ # No necesitamos autenticación aun class CollaborationsController < ApplicationController include Pundit + include StrongParamsHelper def collaborate - @site = Site.find_by_name(params[:site_id]) + @site = Site.find_by_name(pluck_param(:site_id)) authorize Collaboration.new(@site) @invitade = current_usuarie || @site.usuaries.build @@ -21,7 +22,7 @@ class CollaborationsController < ApplicationController # # * Si le usuarie existe y no está logueade, pedirle la contraseña def accept_collaboration - @site = Site.find_by_name(params[:site_id]) + @site = Site.find_by_name(pluck_param(:site_id)) authorize Collaboration.new(@site) @invitade = current_usuarie diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 70ba2e54..4aa2c8d0 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -2,6 +2,8 @@ # Controlador para artículos class PostsController < ApplicationController + include StrongParamsHelper + before_action :authenticate_usuarie! before_action :service_for_direct_upload, only: %i[new edit] @@ -17,9 +19,9 @@ class PostsController < ApplicationController # @todo Mover a tu propio scope def new_array - @value = params.require(:value).strip - @name = params.require(:name).strip - id = params.require(:id).strip + @value = pluck_param(:value) + @name = pluck_param(:name) + id = pluck_param(:id) headers['HX-Trigger-After-Swap'] = 'htmx:resetForm' @@ -27,13 +29,13 @@ class PostsController < ApplicationController end def new_array_value - @value = params.require(:value).strip + @value = pluck_param(:value) render layout: false end def new_related_post - @uuid = params.require(:value).strip + @uuid = pluck_param(:value) @indexed_post = site.indexed_posts.find_by!(post_id: @uuid) @@ -41,7 +43,7 @@ class PostsController < ApplicationController end def new_has_one - @uuid = params.require(:value).strip + @uuid = pluck_param(:value) @indexed_post = site.indexed_posts.find_by!(post_id: @uuid) @@ -51,7 +53,7 @@ class PostsController < ApplicationController # El formulario de un Post, si pasamos el UUID, estamos editando, sino # estamos creando. def form - uuid = params.permit(:uuid).try(:[], :uuid).presence + uuid = pluck_param(:uuid, optional: true) locale @post = @@ -59,7 +61,31 @@ class PostsController < ApplicationController site.indexed_posts.find_by!(post_id: uuid).post else # @todo Usar la base de datos - site.posts(lang: locale).build(layout: params.require(:layout)) + site.posts(lang: locale).build(layout: pluck_param(:layout)) + end + + swap_modals + + render layout: false + end + + # Genera un modal completo + # + # @todo recibir el atributo anterior + # @param :uuid [String] UUID del post (opcional) + # @param :layout [String] El layout a cargar (opcional) + def modal + uuid = pluck_param(:uuid, optional: true) + locale + + # @todo hacer que si el uuid no existe se genera un post, para poder + # pasar el uuid sabiendolo + @post = + if uuid.present? + site.indexed_posts.find_by!(post_id: uuid).post + else + # @todo Usar la base de datos + site.posts(lang: locale).build(layout: pluck_param(:layout)) end swap_modals @@ -107,7 +133,7 @@ class PostsController < ApplicationController def new authorize Post - @post = site.posts(lang: locale).build(layout: params[:layout]) + @post = site.posts(lang: locale).build(layout: pluck_param(:layout)) breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), '' end @@ -128,17 +154,17 @@ class PostsController < ApplicationController # condiciones. if htmx? if post.persisted? - triggers = { 'notification:show' => { 'id' => params.permit(:saved).values.first } } + triggers = { 'notification:show' => { 'id' => pluck_param(:saved, optional: true) } } swap_modals(triggers) @value = post.title.value @uuid = post.uuid.value - @name = params.require(:name) + @name = pluck_param(:name) render render_path_from_attribute, layout: false else - headers['HX-Retarget'] = "##{params.require(:form)}" + headers['HX-Retarget'] = "##{pluck_param(:form)}" headers['HX-Reswap'] = 'outerHTML' render 'posts/form', layout: false, post: post, site: site, **params.permit(:form, :base, :dir, :locale) @@ -156,6 +182,16 @@ class PostsController < ApplicationController breadcrumb 'posts.edit', '' end + # Este endpoint se encarga de actualizar el post. Si el post se edita + # desde el formulario principal, re-renderizamos el formulario si hay + # errores o enviamos a otro lado al guardar. + # + # Si los datos llegaron por HTMX, hay que regenerar el formulario + # y reemplazarlo en su modal (?) o responder con su tarjeta para + # reemplazarla donde sea que esté. + # + # @todo la re-renderización del formulario no es necesaria si tenemos + # validación client-side. def update authorize post @@ -171,15 +207,26 @@ class PostsController < ApplicationController if htmx? if post.persisted? - triggers = { 'notification:show' => params.permit(:saved).values.first } + triggers = { 'notification:show' => pluck_param(:saved, optional: true) } swap_modals(triggers) @value = post.title.value @uuid = post.uuid.value - @name = params.require(:name) - render render_path_from_attribute, layout: false + if (result_id = pluck_param(:result_id, optional: true)) + headers['HX-Retarget'] = "##{result_id}" + headers['HX-Reswap'] = 'outerHTML' + + @indexed_post = site.indexed_posts.find_by_post_id(post.uuid.value) + + render 'posts/new_related_post', layout: false + # @todo Confirmar que esta ruta no esté transitada + else + @name = pluck_param(:name) + + render render_path_from_attribute, layout: false + end else headers['HX-Retarget'] = "##{params.require(:form)}" headers['HX-Reswap'] = 'outerHTML' @@ -272,7 +319,7 @@ class PostsController < ApplicationController # @return [String] def render_path_from_attribute - case params.require(:attribute) + case pluck_param(:attribute) when 'new_has_many' then 'posts/new_has_many_value' when 'new_belongs_to' then 'posts/new_belongs_to_value' when 'new_has_and_belongs_to_many' then 'posts/new_has_many_value' diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index baef3b05..3d074aed 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -155,9 +155,17 @@ module ApplicationHelper private + # Obtiene la traducción desde el esquema en el idioma actual, o por + # defecto en el idioma del sitio. De lo contrario trae una traducción + # genérica. + # + # Si el idioma por defecto tiene un String vacía, se asume que no + # texto. + # + # @return [String,nil] def post_t(*attribute, post:, type:) post.layout.metadata.dig(*attribute, type.to_s, I18n.locale.to_s).presence || - post.layout.metadata.dig(*attribute, type.to_s, post.site.default_locale.to_s).presence || + post.layout.metadata.dig(*attribute, type.to_s, post.site.default_locale.to_s) || I18n.t("posts.attributes.#{attribute.join('.')}.#{type}").presence end end diff --git a/app/helpers/strong_params_helper.rb b/app/helpers/strong_params_helper.rb new file mode 100644 index 00000000..f248cc50 --- /dev/null +++ b/app/helpers/strong_params_helper.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Métodos reutilizables para trabajar con StrongParams +module StrongParamsHelper + + # Obtiene el valor de un param + # + # @todo No hay una forma mejor de hacer esto? + # @param param [Symbol] + # @param :optional [Bool] + # @return [nil,String] + def pluck_param(param, optional: false) + if optional + params.permit(param).values.first.presence + else + params.require(param).presence + end + end +end diff --git a/app/javascript/controllers/array_controller.js b/app/javascript/controllers/array_controller.js index fea690d2..e8f15e8c 100644 --- a/app/javascript/controllers/array_controller.js +++ b/app/javascript/controllers/array_controller.js @@ -103,6 +103,8 @@ export default class extends Controller { this.currentTarget.innerHTML = ""; this.originalValue = []; + const signal = window.abortController?.signal; + for (const itemTarget of this.itemTargets) { if (!itemTarget.dataset.value) continue; if (!this.isChecked(itemTarget)) continue; @@ -114,8 +116,7 @@ export default class extends Controller { this.currentTarget.appendChild(placeholder); - // TODO: Renderizarlas todas juntas - fetch(this.newArrayValueURL) + fetch(this.newArrayValueURL, { signal }) .then((response) => response.text()) .then((body) => { const template = document.createElement("template"); diff --git a/app/javascript/controllers/htmx_controller.js b/app/javascript/controllers/htmx_controller.js new file mode 100644 index 00000000..9b013c52 --- /dev/null +++ b/app/javascript/controllers/htmx_controller.js @@ -0,0 +1,107 @@ +import { Controller } from "stimulus"; + +/* + * Un controlador que imita a HTMX + */ +export default class extends Controller { + connect() { + // @todo Convertir en