From f1968a0b49d7cbc13551c2963693445dabb5fca1 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 10 Jul 2024 16:48:57 -0300 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20permitir=20que=20el=20idioma=20por?= =?UTF-8?q?=20defecto=20tenga=20strings=20vac=C3=ADas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/application_helper.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 From 5f53abe78aa43e177167ca4a5e26f620d10e4873 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 10 Jul 2024 17:04:59 -0300 Subject: [PATCH 2/8] feat: pluck_param nos ayuda a encontrar un param semanticamente --- app/controllers/collaborations_controller.rb | 5 +-- app/controllers/posts_controller.rb | 32 +++++++++++--------- app/helpers/strong_params_helper.rb | 20 ++++++++++++ app/views/devise/shared/_links.haml | 2 +- app/views/posts/_htmx_form.haml | 32 ++++++++++---------- app/views/posts/form.haml | 2 +- app/views/posts/new_has_one.haml | 2 +- app/views/posts/new_has_one_value.haml | 2 +- app/views/sites/build.haml | 2 +- 9 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 app/helpers/strong_params_helper.rb 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..3ea29c55 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,7 @@ 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 @@ -107,7 +109,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 +130,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) @@ -171,13 +173,13 @@ 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) + @name = pluck_param(:name) render render_path_from_attribute, layout: false else @@ -272,7 +274,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/strong_params_helper.rb b/app/helpers/strong_params_helper.rb new file mode 100644 index 00000000..8f8a26bf --- /dev/null +++ b/app/helpers/strong_params_helper.rb @@ -0,0 +1,20 @@ +# 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] + # @param :params [StrongParameters] + # @return [nil,String] + def pluck_param(param, optional: false, params: params) + if optional + params.permit(param).values.first.presence + else + params.require(param).presence + end + end +end diff --git a/app/views/devise/shared/_links.haml b/app/views/devise/shared/_links.haml index 5d5c0b67..a4c99c03 100644 --- a/app/views/devise/shared/_links.haml +++ b/app/views/devise/shared/_links.haml @@ -1,6 +1,6 @@ %hr/ -- locale = params.permit(:locale) +- locale = pluck_param(:locale, optional: true) - if controller_name != 'sessions' = link_to t('.sign_in'), new_session_path(resource_name, params: locale), diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index f6704695..bd786cc6 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -18,22 +18,22 @@ :ruby except = %i[date] - if (inverse = params.permit(:inverse).try(:[], :inverse).presence) + if (inverse = pluck_param(:inverse, optional: true)) except << inverse.to_sym end options = { - id: params.require(:form), + id: pluck_param(:form), multipart: true, class: 'form post ', - 'hx-swap': params.require(:swap), - 'hx-target': "##{params.require(:target)}", + 'hx-swap': pluck_param(:swap), + 'hx-target': "##{pluck_param(:target)}", 'hx-validate': true, data: { controller: 'form-validation', action: 'form-validation#submit', - 'form-validation-submitting-id-value': params.permit(:submitting).values.first, - 'form-validation-invalid-id-value': params.permit(:invalid).values.first, + 'form-validation-submitting-id-value': pluck_param(:submitting, optional: true), + 'form-validation-invalid-id-value': pluck_param(:invalid, optional: true), } } @@ -68,20 +68,20 @@ = errors.first -# Parámetros para HTMX - %input{ type: 'hidden', name: 'hide', value: params.permit((post.errors.empty? ? :show : :hide)).try(:values).try(:first) } - %input{ type: 'hidden', name: 'show', value: params.permit((post.errors.empty? ? :hide : :show)).try(:values).try(:first) } - %input{ type: 'hidden', name: 'name', value: params.require(:name) } - %input{ type: 'hidden', name: 'base', value: params.require(:base) } + %input{ type: 'hidden', name: 'hide', value: pluck_param((post.errors.empty? ? :show : :hide), optional: true) } + %input{ type: 'hidden', name: 'show', value: pluck_param((post.errors.empty? ? :hide : :show), optional: true) } + %input{ type: 'hidden', name: 'name', value: pluck_param(:name) } + %input{ type: 'hidden', name: 'base', value: pluck_param(:base) } %input{ type: 'hidden', name: 'form', value: options[:id] } %input{ type: 'hidden', name: 'dir', value: dir } %input{ type: 'hidden', name: 'locale', value: locale } - %input{ type: 'hidden', name: 'attribute', value: params.require(:attribute) } - %input{ type: 'hidden', name: 'target', value: params.require(:target) } - %input{ type: 'hidden', name: 'swap', value: params.require(:swap) } + %input{ type: 'hidden', name: 'attribute', value: pluck_param(:attribute) } + %input{ type: 'hidden', name: 'target', value: pluck_param(:target) } + %input{ type: 'hidden', name: 'swap', value: pluck_param(:swap) } - if params[:inverse].present? - %input{ type: 'hidden', name: 'inverse', value: params.require(:inverse) } + %input{ type: 'hidden', name: 'inverse', value: pluck_param(:inverse) } - if params[:saved].present? - %input{ type: 'hidden', name: 'saved', value: params.require(:saved) } + %input{ type: 'hidden', name: 'saved', value: pluck_param(:saved) } = hidden_field_tag "#{base}[layout]", post.layout.name @@ -92,6 +92,6 @@ Enviamos valores vacíos o arrastrados desde el formulario anterior para los atributos ignorados - except.each do |attr| - %input{ type: 'hidden', name: "#{base}[#{attr}]", value: params[attr].presence } + %input{ type: 'hidden', name: "#{base}[#{attr}]", value: pluck_param(attr, optional: true) } = yield(:post_form) diff --git a/app/views/posts/form.haml b/app/views/posts/form.haml index df4369e1..bb63aaeb 100644 --- a/app/views/posts/form.haml +++ b/app/views/posts/form.haml @@ -4,4 +4,4 @@ @param :site [Site] @param :post [Post] -= render 'posts/htmx_form', site: @site, post: @post, locale: @locale, dir: t("locales.#{@locale}.dir"), base: params.require(:base) += render 'posts/htmx_form', site: @site, post: @post, locale: @locale, dir: t("locales.#{@locale}.dir"), base: pluck_param(:base) diff --git a/app/views/posts/new_has_one.haml b/app/views/posts/new_has_one.haml index e32f191b..8f65ff08 100644 --- a/app/views/posts/new_has_one.haml +++ b/app/views/posts/new_has_one.haml @@ -1 +1 @@ -= render 'posts/new_has_one', post: @indexed_post, name: params.require(:name), value: @uuid += render 'posts/new_has_one', post: @indexed_post, name: pluck_param(:name), value: @uuid diff --git a/app/views/posts/new_has_one_value.haml b/app/views/posts/new_has_one_value.haml index 6eece5dc..06065518 100644 --- a/app/views/posts/new_has_one_value.haml +++ b/app/views/posts/new_has_one_value.haml @@ -1 +1 @@ -= render 'posts/new_has_one', post: @post.to_index, name: params.require(:name), value: @uuid, modal_id: params.require(:show) += render 'posts/new_has_one', post: @post.to_index, name: pluck_param(:name), value: @uuid, modal_id: pluck_param(:show) diff --git a/app/views/sites/build.haml b/app/views/sites/build.haml index c2becec0..2743c265 100644 --- a/app/views/sites/build.haml +++ b/app/views/sites/build.haml @@ -1 +1 @@ -= render 'sites/build', site: @site, class: params.permit(:class)[:class] += render 'sites/build', site: @site, class: pluck_param(:class, optional: true) From 255ea763b4e38db29407bdb9dcd125821706e573 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 10 Jul 2024 17:06:29 -0300 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20poder=20cancelar=20cuando=20se=20es?= =?UTF-8?q?t=C3=A1=20haciendo=20un=20fetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit si salimos de la página hay que poder cancelar todos los fetchs pendientes sin producir errores --- app/javascript/controllers/array_controller.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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"); From 5b9cd27a79b6305b169cd0f77338f72067bf1dee Mon Sep 17 00:00:00 2001 From: f Date: Wed, 10 Jul 2024 17:08:47 -0300 Subject: [PATCH 4/8] refactor: convertir lista de errores en un componente --- app/views/posts/_errors.haml | 19 +++++++++++++++++++ app/views/posts/_form.haml | 28 +++++++++------------------- app/views/posts/_htmx_form.haml | 20 +------------------- 3 files changed, 29 insertions(+), 38 deletions(-) create mode 100644 app/views/posts/_errors.haml diff --git a/app/views/posts/_errors.haml b/app/views/posts/_errors.haml new file mode 100644 index 00000000..3b0a89dd --- /dev/null +++ b/app/views/posts/_errors.haml @@ -0,0 +1,19 @@ +- unless post.errors.empty? + - title = t('.errors.title') + - help = t('.errors.help') + = render 'bootstrap/alert' do + %h4= title + %p= help + + %ul + - post.errors.each do |attribute, errors| + - if errors.size > 1 + %li + %strong= post_label_t attribute, post: post + %ul + - errors.each do |error| + %li= error + - else + %li + %strong= post_label_t attribute, post: post + = errors.first diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index 3e09cb72..1c9c623e 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -1,22 +1,4 @@ -- unless post.errors.empty? - - title = t('.errors.title') - - help = t('.errors.help') - = render 'bootstrap/alert' do - %h4= title - %p= help - - %ul - - post.errors.each do |attribute, errors| - - if errors.size > 1 - %li - %strong= post_label_t attribute, post: post - %ul - - errors.each do |error| - %li= error - - else - %li - %strong= post_label_t attribute, post: post - = errors.first += render 'errors', post: post -# TODO: habilitar form_for :ruby @@ -55,3 +37,11 @@ -# Formularios usados por los modales = yield(:post_form) + +-# + Acumulador de formularios dinámicos, se van cargando a medida que se + necesitan en lugar de recursivamente. + + Nunca se eliminan los modales una vez que se cargan para poder tener + historial de cambios. +%div{ data: { controller: 'htmx', action: 'htmx:getUrl@window->htmx#beforeend' } } diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index bd786cc6..75967a40 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -47,25 +47,7 @@ end = form_tag url, **options do - - unless post.errors.empty? - - title = t('.errors.title') - - help = t('.errors.help') - = render 'bootstrap/alert' do - %h4= title - %p= help - - %ul - - post.errors.each do |attribute, errors| - - if errors.size > 1 - %li - %strong= post_label_t attribute, post: post - %ul - - errors.each do |error| - %li= error - - else - %li - %strong= post_label_t attribute, post: post - = errors.first + = render 'errors', post: post -# Parámetros para HTMX %input{ type: 'hidden', name: 'hide', value: pluck_param((post.errors.empty? ? :show : :hide), optional: true) } From 922fff29eae1977a068039e1162fda603f484d1b Mon Sep 17 00:00:00 2001 From: f Date: Wed, 10 Jul 2024 17:12:07 -0300 Subject: [PATCH 5/8] feat: un potencial reemplazo de htmx en stimulus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit htmx duplica mucho código y no hace monitoreo de modificaciones en el código sin grandes parches. con esto podemos ir reemplazando htmx por algo que nos resulta más cómodo y eventualmente pasar a turbo. --- app/javascript/controllers/htmx_controller.js | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 app/javascript/controllers/htmx_controller.js 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