From 43308e28112292d0df35798f83e7dce80864c2a5 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 3 Jun 2024 17:45:23 -0300 Subject: [PATCH 1/7] refactor: deprecar nanoid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit estábamos usando nanoid + concatenación porque los id que genera podían empezar con número o guión, lo que está prohibido en el atributo id. además, la implementación de securerandom es mucho más veloz. https://github.com/radeno/nanoid.rb/issues/67 --- Gemfile | 1 - Gemfile.lock | 2 -- app/helpers/application_helper.rb | 11 +++++++++++ app/views/posts/attributes/_new_array.haml | 4 ++-- .../posts/attributes/_new_belongs_to.haml | 18 +++++++++--------- .../_new_has_and_belongs_to_many.haml | 18 +++++++++--------- app/views/posts/attributes/_new_has_many.haml | 16 ++++++++-------- .../attributes/_new_predefined_array.haml | 4 ++-- .../attributes/_new_predefined_value.haml | 16 ++++++++-------- app/views/posts/new_array.haml | 4 ++-- app/views/posts/new_belongs_to_value.haml | 2 +- app/views/posts/new_has_many_value.haml | 2 +- 12 files changed, 53 insertions(+), 45 deletions(-) diff --git a/Gemfile b/Gemfile index 30794e83..5714b981 100644 --- a/Gemfile +++ b/Gemfile @@ -85,7 +85,6 @@ gem 'rubanok' gem 'after_commit_everywhere', '~> 1.0' gem 'aasm' gem 'que-web' -gem 'nanoid' # database gem 'hairtrigger' diff --git a/Gemfile.lock b/Gemfile.lock index db6b892d..7f5284ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -373,7 +373,6 @@ GEM multi_xml (0.6.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - nanoid (2.0.0) net-imap (0.4.9) date net-protocol @@ -678,7 +677,6 @@ DEPENDENCIES memory_profiler mini_magick mobility - nanoid net-ssh nokogiri pg diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ba92f626..7d1f9c0b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -3,6 +3,17 @@ # Helpers module ApplicationHelper BRACKETS = /[\[\]]/.freeze + ALPHA_LARGE = [*'a'..'z', *'A'..'Z'].freeze + + # Devuelve un indentificador aleatorio que puede usarse como atributo + # HTML. Reemplaza Nanoid. El primer caracter siempre es alfabético. + # + # @return [String] + def random_id + SecureRandom.urlsafe_base64.tap do |s| + s[0] = ALPHA_LARGE.sample + end + end # Devuelve el atributo name de un campo anidado en el formato que # esperan los helpers *_field diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 38ba4eb9..1ad12b8e 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -3,7 +3,7 @@ :ruby id = id_for(base, attribute) name = "#{base}[#{attribute}][]" - form_id = "form-#{Nanoid.generate}" + form_id = random_id %div{ data: { controller: 'modal array enter', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } %template{ data: { target: 'array.placeholder' } } @@ -41,7 +41,7 @@ -# Eliminamos las tildes para poder buscar independientemente de cómo se escriba - metadata.values.sort_by(&:remove_diacritics).each do |value| .mb-2{ data: { target: 'array.item', 'searchable-value': value.remove_diacritics.downcase, value: value } } - = render 'bootstrap/custom_checkbox', name: name, id: "value-#{Nanoid.generate}", value: value, checked: metadata.value.include?(value), content: value + = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: value, checked: metadata.value.include?(value), content: value - content_for :"#{id}_footer" do .input-group.w-auto.flex-grow-1.my-0 diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index 0a2a4c5b..a4867276 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -8,15 +8,15 @@ del formulario principal porque no se pueden anidar. :ruby - id = id_for(base, attribute) + id = random_id name = "#{base}[#{attribute}]" - form_id = "form-#{Nanoid.generate}" - modal_id = "modal-#{Nanoid.generate}" - post_id = "post-#{Nanoid.generate}" - post_form_id = "post-form-#{Nanoid.generate}" - post_modal_id = "post-modal-#{Nanoid.generate}" - post_form_loaded_id = "post-loaded-#{Nanoid.generate}" - value_list_id = "#{id}_body" + form_id = random_id + modal_id = random_id + post_id = random_id + post_form_id = random_id + post_modal_id = random_id + post_form_loaded_id = random_id + value_list_id = random_id %div{ id: modal_id, data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_related_post_path(site) } } %template{ data: { target: 'array.placeholder' } } @@ -55,7 +55,7 @@ .form-group.mb-0{ id: value_list_id } - metadata.values.each_pair do |value, uuid| .mb-2{ data: { target: 'array.item', 'searchable-value': value.remove_diacritics.downcase, value: uuid } } - = render 'bootstrap/custom_checkbox', name: name, id: "value-#{Nanoid.generate}", value: uuid, checked: metadata.value.include?(uuid), content: value, type: 'radio' + = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: uuid, checked: metadata.value.include?(uuid), content: value, type: 'radio' -# Según la definición del campo, si hay un filtro, tenemos que poder diff --git a/app/views/posts/attributes/_new_has_and_belongs_to_many.haml b/app/views/posts/attributes/_new_has_and_belongs_to_many.haml index 0b53f0ae..243178d9 100644 --- a/app/views/posts/attributes/_new_has_and_belongs_to_many.haml +++ b/app/views/posts/attributes/_new_has_and_belongs_to_many.haml @@ -8,15 +8,15 @@ del formulario principal porque no se pueden anidar. :ruby - id = id_for(base, attribute) + id = random_id name = "#{base}[#{attribute}][]" - form_id = "form-#{Nanoid.generate}" - modal_id = "modal-#{Nanoid.generate}" - post_id = "post-#{Nanoid.generate}" - post_form_id = "post-form-#{Nanoid.generate}" - post_modal_id = "post-modal-#{Nanoid.generate}" - post_form_loaded_id = "post-loaded-#{Nanoid.generate}" - value_list_id = "#{id}_body" + form_id = random_id + modal_id = random_id + post_id = random_id + post_form_id = random_id + post_modal_id = random_id + post_form_loaded_id = random_id + value_list_id = random_id %div{ id: modal_id, data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_related_post_path(site) } } %template{ data: { target: 'array.placeholder' } } @@ -55,7 +55,7 @@ .form-group.mb-0{ id: value_list_id } - metadata.values.each_pair do |value, uuid| .mb-2{ data: { target: 'array.item', 'searchable-value': value.remove_diacritics.downcase, value: uuid } } - = render 'bootstrap/custom_checkbox', name: name, id: "value-#{Nanoid.generate}", value: uuid, checked: metadata.value.include?(uuid), content: value + = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: uuid, checked: metadata.value.include?(uuid), content: value -# Según la definición del campo, si hay un filtro, tenemos que poder diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 0b53f0ae..f8312c20 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -10,13 +10,13 @@ :ruby id = id_for(base, attribute) name = "#{base}[#{attribute}][]" - form_id = "form-#{Nanoid.generate}" - modal_id = "modal-#{Nanoid.generate}" - post_id = "post-#{Nanoid.generate}" - post_form_id = "post-form-#{Nanoid.generate}" - post_modal_id = "post-modal-#{Nanoid.generate}" - post_form_loaded_id = "post-loaded-#{Nanoid.generate}" - value_list_id = "#{id}_body" + form_id = random_id + modal_id = random_id + post_id = random_id + post_form_id = random_id + post_modal_id = random_id + post_form_loaded_id = random_id + value_list_id = random_id %div{ id: modal_id, data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_related_post_path(site) } } %template{ data: { target: 'array.placeholder' } } @@ -55,7 +55,7 @@ .form-group.mb-0{ id: value_list_id } - metadata.values.each_pair do |value, uuid| .mb-2{ data: { target: 'array.item', 'searchable-value': value.remove_diacritics.downcase, value: uuid } } - = render 'bootstrap/custom_checkbox', name: name, id: "value-#{Nanoid.generate}", value: uuid, checked: metadata.value.include?(uuid), content: value + = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: uuid, checked: metadata.value.include?(uuid), content: value -# Según la definición del campo, si hay un filtro, tenemos que poder diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index ec251134..2adf44c1 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -5,7 +5,7 @@ :ruby id = id_for(base, attribute) name = "#{base}[#{attribute}][]" - form_id = "form-#{Nanoid.generate}" + form_id = random_id %div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } %template{ data: { target: 'array.placeholder' } } @@ -44,7 +44,7 @@ -# Eliminamos las tildes para poder buscar independientemente de cómo se escriba - metadata.values.each_pair do |value, key| .mb-2{ data: { target: 'array.item', 'searchable-value': value.remove_diacritics.downcase, value: value } } - = render 'bootstrap/custom_checkbox', name: name, id: "value-#{Nanoid.generate}", value: key, checked: metadata.value.include?(key), content: value + = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: key, checked: metadata.value.include?(key), content: value - content_for :"#{id}_footer" do -# Alinear los botones a la derecha diff --git a/app/views/posts/attributes/_new_predefined_value.haml b/app/views/posts/attributes/_new_predefined_value.haml index 42e309c3..c592b20a 100644 --- a/app/views/posts/attributes/_new_predefined_value.haml +++ b/app/views/posts/attributes/_new_predefined_value.haml @@ -10,13 +10,13 @@ :ruby id = id_for(base, attribute) name = "#{base}[#{attribute}]" - form_id = "form-#{Nanoid.generate}" - modal_id = "modal-#{Nanoid.generate}" - post_id = "post-#{Nanoid.generate}" - post_form_id = "post-form-#{Nanoid.generate}" - post_modal_id = "post-modal-#{Nanoid.generate}" - post_form_loaded_id = "post-loaded-#{Nanoid.generate}" - value_list_id = "#{id}_body" + form_id = random_id + modal_id = random_id + post_id = random_id + post_form_id = random_id + post_modal_id = random_id + post_form_loaded_id = random_id + value_list_id = random_id %div{ id: modal_id, data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } %template{ data: { target: 'array.placeholder' } } @@ -54,7 +54,7 @@ .form-group.mb-0{ id: value_list_id } - metadata.values.each_pair do |value, key| .mb-2{ data: { target: 'array.item', 'searchable-value': value.remove_diacritics.downcase, value: value } } - = render 'bootstrap/custom_checkbox', name: name, id: "value-#{Nanoid.generate}", value: key, checked: (metadata.value == key), content: value, type: 'radio' + = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: key, checked: (metadata.value == key), content: value, type: 'radio' - content_for :"#{id}_footer" do = render 'bootstrap/btn', content: t('.accept'), action: 'array#accept modal#hide', class: 'm-0 mr-1' diff --git a/app/views/posts/new_array.haml b/app/views/posts/new_array.haml index 62f74854..77d64b55 100644 --- a/app/views/posts/new_array.haml +++ b/app/views/posts/new_array.haml @@ -1,8 +1,8 @@ -- item_id = "item-#{Nanoid.generate}" +- item_id = random_id .mb-2{ id: item_id, data: { target: 'array.item', 'searchable-value': @value.remove_diacritics.downcase, value: @value } } .d-flex.flex-row.flex-wrap .flex-grow-1 - = render 'bootstrap/custom_checkbox', name: @name, id: "value-#{Nanoid.generate}", value: @value, checked: true, content: @value + = render 'bootstrap/custom_checkbox', name: @name, id: random_id, value: @value, checked: true, content: @value %div %button.btn.btn-sm.m-0{ data: { action: 'array#remove', 'remove-target-param': item_id } }= t('.remove') diff --git a/app/views/posts/new_belongs_to_value.haml b/app/views/posts/new_belongs_to_value.haml index ab324763..d1f9c3f9 100644 --- a/app/views/posts/new_belongs_to_value.haml +++ b/app/views/posts/new_belongs_to_value.haml @@ -1,2 +1,2 @@ .mb-2{ data: { target: 'array.item', 'searchable-value': @value.remove_diacritics.downcase, value: @uuid } } - = render 'bootstrap/custom_checkbox', name: @name, id: "value-#{Nanoid.generate}", value: @uuid, checked: true, content: @value, type: 'radio' + = render 'bootstrap/custom_checkbox', name: @name, id: random_id, value: @uuid, checked: true, content: @value, type: 'radio' diff --git a/app/views/posts/new_has_many_value.haml b/app/views/posts/new_has_many_value.haml index b26acc89..b9846475 100644 --- a/app/views/posts/new_has_many_value.haml +++ b/app/views/posts/new_has_many_value.haml @@ -1,2 +1,2 @@ .mb-2{ data: { target: 'array.item', 'searchable-value': @value.remove_diacritics.downcase, value: @uuid } } - = render 'bootstrap/custom_checkbox', name: @name, id: "value-#{Nanoid.generate}", value: @uuid, checked: true, content: @value + = render 'bootstrap/custom_checkbox', name: @name, id: random_id, value: @uuid, checked: true, content: @value From 6d9609f9444376922af3e4ee5ce2109494c3b7d9 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 3 Jun 2024 17:47:05 -0300 Subject: [PATCH 2/7] feat: no romper relaciones retroactivamente MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit oculta los artículos relacionados que ya tienen una relación establecida, para no romperla sin querer. --- .../metadata/unused_values_concern.rb | 27 +++++++++++++++++++ app/models/metadata_new_belongs_to.rb | 4 ++- app/models/metadata_new_has_many.rb | 4 ++- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 app/models/concerns/metadata/unused_values_concern.rb diff --git a/app/models/concerns/metadata/unused_values_concern.rb b/app/models/concerns/metadata/unused_values_concern.rb new file mode 100644 index 00000000..6f0a0bf1 --- /dev/null +++ b/app/models/concerns/metadata/unused_values_concern.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Metadata + # Hasta ahora veníamos habilitando la opción de romper + # retroactivamente relaciones, sin informar que estaba sucediendo. + # Con este módulo, todas las relaciones que ya tienen una relación + # inversa son ignoradas. + module UnusedValuesConcern + extend ActiveSupport::Concern + + included do + # Genera un Hash de { title | slug => uuid }, excluyendo el Post + # actual y todos los que ya tengan una relación inversa, para no + # romperla. + # + # @return [Hash] + def values + @values ||= posts.map do |p| + next if p.uuid.value == post.uuid.value + next unless inverse? && (p[inverse].empty? || [p[inverse].value].flatten.include?(post.uuid.value)) + + [title(p), p.uuid.value] + end.compact.to_h + end + end + end +end diff --git a/app/models/metadata_new_belongs_to.rb b/app/models/metadata_new_belongs_to.rb index db90cb8f..46d25ce6 100644 --- a/app/models/metadata_new_belongs_to.rb +++ b/app/models/metadata_new_belongs_to.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true # Nueva interfaz -class MetadataNewBelongsTo < MetadataBelongsTo; end +class MetadataNewBelongsTo < MetadataBelongsTo + include Metadata::UnusedValuesConcern +end diff --git a/app/models/metadata_new_has_many.rb b/app/models/metadata_new_has_many.rb index 3bf37d45..e4b3869c 100644 --- a/app/models/metadata_new_has_many.rb +++ b/app/models/metadata_new_has_many.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true # Interfaz nueva para uno a muchos -class MetadataNewHasMany < MetadataHasMany; end +class MetadataNewHasMany < MetadataHasMany + include Metadata::UnusedValuesConcern +end From d4767457ffa664f141113b5c73322fde59406176 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 3 Jun 2024 17:50:06 -0300 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20m=C3=A1s=20espacio=20para=20los=20mo?= =?UTF-8?q?dales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/bootstrap/_modal.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/bootstrap/_modal.haml b/app/views/bootstrap/_modal.haml index 28014c6a..3efd1525 100644 --- a/app/views/bootstrap/_modal.haml +++ b/app/views/bootstrap/_modal.haml @@ -28,7 +28,7 @@ .modal.fade{ tabindex: -1, aria: { hidden: 'true' }, data: { target: 'modal.modal' } } .modal-backdrop.fade{ data: { target: 'modal.backdrop', action: local_assigns[:hide_actions].join(' ') } } - .modal-dialog.modal-dialog-scrollable.modal-dialog-centered + .modal-dialog.modal-dialog-scrollable.modal-dialog-centered.modal-lg .modal-content{ **local_assigns[:modal_content_attributes] } - if (header = yield(:"#{id}_header")).present? .modal-header= header From ddc80a4006d69cb7c9ee2b01f30ad03d940e05ca Mon Sep 17 00:00:00 2001 From: f Date: Mon, 3 Jun 2024 17:50:30 -0300 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20no=20establecer=20ids=20vac=C3=ADos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/posts/attributes/_boolean.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_boolean.haml b/app/views/posts/attributes/_boolean.haml index e07feca4..6928ebd7 100644 --- a/app/views/posts/attributes/_boolean.haml +++ b/app/views/posts/attributes/_boolean.haml @@ -1,5 +1,5 @@ .form-check - = hidden_field_tag "#{base}[#{attribute}]", '0', id: '' + = hidden_field_tag "#{base}[#{attribute}]", '0', id: nil .custom-control.custom-switch = check_box_tag "#{base}[#{attribute}]", '1', metadata.value, class: "custom-control-input #{invalid(post, attribute)}", From 8e272e4ce048dfb6c72ad05805915a85a95e59c0 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 3 Jun 2024 17:50:48 -0300 Subject: [PATCH 5/7] =?UTF-8?q?docs:=20documentar=20los=20par=C3=A1metros?= =?UTF-8?q?=20del=20formulario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/posts/_htmx_form.haml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index 8cc8c938..707b9c51 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -5,6 +5,16 @@ @param :post [Post] @param :locale [Symbol, String] @param :dir [Symbol, String] + + @param [ActionController::StrongParameters] params + @option params [String] :inverse La relación inversa (opcional) + @option params [String] :form El ID del formulario actual, si tiene botones externos, tiene que estar compartido + @option params [String] :swap Método de intercambio del resultado (HTMX) + @option params [String] :target Elemento donde se carga el resultado (HTMX) + @option params [String] :hide ID del modal a esconder vía evento + @option params [String] :show ID del modal a mostrar vía evento + @option params [String] :base La base del formulario, que luego se envía como parámetro a PostService + @option params [String] :attribute El tipo de atributo, para saber qué respuesta generar :ruby except = %i[date] From ff3042da903a5b7edbf7b6adcd742cb748580097 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 3 Jun 2024 17:51:07 -0300 Subject: [PATCH 6/7] fix: asignar el uuid desde el formulario MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit es útil para pre-asignar el uuid de una relación --- app/views/posts/attributes/_uuid.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_uuid.haml b/app/views/posts/attributes/_uuid.haml index 0aab9802..60d74cfc 100644 --- a/app/views/posts/attributes/_uuid.haml +++ b/app/views/posts/attributes/_uuid.haml @@ -1 +1 @@ --# nada += hidden_field_tag "#{base}[#{attribute}]", metadata.value From 96aebb13465f2cdc0efff9eed5c1f044fc3dec3f Mon Sep 17 00:00:00 2001 From: f Date: Mon, 3 Jun 2024 17:52:53 -0300 Subject: [PATCH 7/7] feat: new_has_one --- app/controllers/posts_controller.rb | 27 ++++++---- .../controllers/modal_controller.js | 14 +++++ app/models/metadata_new_has_one.rb | 6 +++ app/views/posts/_htmx_form.haml | 4 +- app/views/posts/_new_has_one.haml | 2 + .../posts/attribute_ro/_new_has_one.haml | 6 +++ app/views/posts/attributes/_new_has_one.haml | 53 +++++++++++++++++++ app/views/posts/new_has_one.haml | 1 + app/views/posts/new_has_one_value.haml | 1 + config/routes.rb | 1 + 10 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 app/models/metadata_new_has_one.rb create mode 100644 app/views/posts/_new_has_one.haml create mode 100644 app/views/posts/attribute_ro/_new_has_one.haml create mode 100644 app/views/posts/attributes/_new_has_one.haml create mode 100644 app/views/posts/new_has_one.haml create mode 100644 app/views/posts/new_has_one_value.haml diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 6d199cd7..282a784a 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -40,10 +40,18 @@ class PostsController < ApplicationController render layout: false end - # El formulario de un Post, si pasamos el uuid, estamos editando, sino + def new_has_one + @uuid = params.require(:value).strip + + @indexed_post = site.indexed_posts.find_by!(post_id: @uuid) + + render layout: false + end + + # El formulario de un Post, si pasamos el UUID, estamos editando, sino # estamos creando. def form - uuid = params.permit(:uuid).try(:[], :uuid) + uuid = params.permit(:uuid).try(:[], :uuid).presence locale @post = @@ -111,7 +119,7 @@ class PostsController < ApplicationController params: params) @post = service.create - if @post.persisted? + if post.persisted? site.touch forget_content end @@ -119,11 +127,11 @@ class PostsController < ApplicationController # @todo Enviar la creación a otro endpoint para evitar tantas # condiciones. if htmx? - if @post.persisted? + if post.persisted? swap_modals - @value = @post.title.value - @uuid = @post.uuid.value + @value = post.title.value + @uuid = post.uuid.value @name = params.require(:name) render render_path_from_attribute, layout: false @@ -133,8 +141,8 @@ class PostsController < ApplicationController render 'posts/form', layout: false, post: post, site: site, **params.permit(:form, :base, :dir, :locale) end - elsif @post.persisted? - redirect_to site_post_path(@site, @post) + elsif post.persisted? + redirect_to site_post_path(site, post) else render 'posts/new' end @@ -235,7 +243,7 @@ class PostsController < ApplicationController # @param triggers [Hash] Otros disparadores def swap_modals(triggers = {}) params.permit(:show, :hide).each_pair do |key, value| - triggers["modal:#{key}"] = { id: value } + triggers["modal:#{key}"] = { id: value } if value.present? end headers['HX-Trigger'] = triggers.to_json if triggers.present? @@ -247,6 +255,7 @@ class PostsController < ApplicationController 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' + when 'new_has_one' then 'posts/new_has_one_value' else 'nothing' end end diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js index 20154f79..0f8deeca 100644 --- a/app/javascript/controllers/modal_controller.js +++ b/app/javascript/controllers/modal_controller.js @@ -18,9 +18,23 @@ export default class extends Controller { window.removeEventListener("modal:hide", this.hideEvent); } + /* + * Abrir otro modal, enviando el ID a toda la ventana. + */ + showAnother(event = undefined) { + event?.preventDefault(); + + if (!event.target?.dataset?.modalShowValue) return; + + window.dispatchEvent(new CustomEvent("modal:show", { detail: { id: event.target.dataset.modalShowValue } })); + } + /* * Podemos enviar la orden de apertura como un click o como un * CustomEvent incluyendo el id del modal como detail. + * + * El elemento clicleable puede tener un valor que se refiera a otro + * modal también. */ show(event = undefined) { event?.preventDefault(); diff --git a/app/models/metadata_new_has_one.rb b/app/models/metadata_new_has_one.rb new file mode 100644 index 00000000..642273e3 --- /dev/null +++ b/app/models/metadata_new_has_one.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# Nueva interfaz para relaciones 1:1 +class MetadataNewHasOne < MetadataHasOne + include Metadata::UnusedValuesConcern +end diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index 707b9c51..f4f4a845 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -61,8 +61,8 @@ = errors.first -# Parámetros para HTMX - %input{ type: 'hidden', name: 'hide', value: params.require((post.errors.empty? ? :show : :hide)) } - %input{ type: 'hidden', name: 'show', value: params.require((post.errors.empty? ? :hide : :show)) } + %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: 'form', value: options[:id] } diff --git a/app/views/posts/_new_has_one.haml b/app/views/posts/_new_has_one.haml new file mode 100644 index 00000000..54a370cd --- /dev/null +++ b/app/views/posts/_new_has_one.haml @@ -0,0 +1,2 @@ += render 'posts/new_related_post', post: post +%input{ type: 'hidden', name: name, value: value } diff --git a/app/views/posts/attribute_ro/_new_has_one.haml b/app/views/posts/attribute_ro/_new_has_one.haml new file mode 100644 index 00000000..425e659e --- /dev/null +++ b/app/views/posts/attribute_ro/_new_has_one.haml @@ -0,0 +1,6 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td{ dir: dir, lang: locale } + - p = metadata.has_one + - if p + = link_to p.title.value, site_post_path(site, p.id) diff --git a/app/views/posts/attributes/_new_has_one.haml b/app/views/posts/attributes/_new_has_one.haml new file mode 100644 index 00000000..5a614990 --- /dev/null +++ b/app/views/posts/attributes/_new_has_one.haml @@ -0,0 +1,53 @@ +-# + Genera un listado de radios entre los que se puede elegir solo uno para + guardar. Podemos elegir entre los artículos ya cargados o agregar uno + nuevo. + + Al agregar uno nuevo, se abre un segundo modal que carga el formulario + correspondiente vía HTMX. El formulario tiene que cargarse por fuera + del formulario principal porque no se pueden anidar. + +:ruby + id = random_id + name = "#{base}[#{attribute}]" + target_id = random_id + form_id = random_id + modal_id = random_id + post_id = random_id + post_form_id = random_id + post_modal_id = random_id + post_form_loaded_id = random_id + value_list_id = random_id + layout = metadata.filter[:layout] + +%div{ data: { controller: 'modal' }} + .form-group + = hidden_field_tag name, '' + .d-flex.align-items-center.justify-content-between + = label_tag id, post_label_t(attribute, post: post), class: 'h3' + = render 'bootstrap/btn', content: t('.edit'), action: 'modal#showAnother', data: { 'modal-show-value': modal_id } + + -# Aquí se reemplaza por la tarjeta y el UUID luego de guardar + .row.row-cols-1.no-gutters.placeholder-glow{ id: target_id } + -# @todo issue-7537 + - if !metadata.empty? && (indexed_post = site.indexed_posts.find_by(post_id: metadata.value)) + = render 'posts/new_has_one', post: indexed_post, name: name, value: metadata.value + +-# + El modal se genera por fuera del formulario, para poder enviar los + datos y recibir su UUID en respuesta. +- content_for :post_form do + %div{ id: modal_id, data: { controller: 'modal' }} + - if layout.is_a?(String) + = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' } do + - content_for :"#{id}_body" do + -# @todo ocultar el modal después de guardar + .placeholder-glow{ 'hx-get': site_posts_form_path(site, layout: layout, base: id, name: name, form: form_id, swap: 'innerHTML', target: target_id, attribute: 'new_has_one', hide: modal_id, uuid: metadata.value), 'hx-trigger': 'load' } + %span.placeholder.w-100.h-100 + + - content_for :"#{id}_footer" do + = render 'bootstrap/btn', form: form_id, content: t('.save'), type: 'submit', class: 'm-0 mt-1 mr-1' + = render 'bootstrap/btn', content: t('.cancel'), action: 'modal#hide', class: 'm-0 mt-1 mr-1' + + - else + Nada diff --git a/app/views/posts/new_has_one.haml b/app/views/posts/new_has_one.haml new file mode 100644 index 00000000..e32f191b --- /dev/null +++ b/app/views/posts/new_has_one.haml @@ -0,0 +1 @@ += render 'posts/new_has_one', post: @indexed_post, name: params.require(:name), value: @uuid diff --git a/app/views/posts/new_has_one_value.haml b/app/views/posts/new_has_one_value.haml new file mode 100644 index 00000000..9f2b660a --- /dev/null +++ b/app/views/posts/new_has_one_value.haml @@ -0,0 +1 @@ += render 'posts/new_has_one', post: @post.to_index, name: params.require(:name), value: @uuid diff --git a/config/routes.rb b/config/routes.rb index 45410782..711e3f24 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,6 +103,7 @@ Rails.application.routes.draw do get :'posts/new_array', to: 'posts#new_array' get :'posts/new_array_value', to: 'posts#new_array_value' get :'posts/new_related_post', to: 'posts#new_related_post' + get :'posts/new_has_one', to: 'posts#new_has_one' get :'posts/form', to: 'posts#form' resources :posts do