From c150acbd6fca89782310890b1cf9993cbde8ed38 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 15:15:55 -0300 Subject: [PATCH] feat: pertenece a --- app/controllers/posts_controller.rb | 2 +- app/models/metadata_new_belongs_to.rb | 4 + app/views/bootstrap/_custom_checkbox.haml | 2 +- app/views/posts/_htmx_form.haml | 1 + .../posts/attribute_ro/_new_belongs_to.haml | 6 ++ .../posts/attributes/_new_belongs_to.haml | 100 ++++++++++++++++++ app/views/posts/attributes/_new_has_many.haml | 1 + app/views/posts/new_belongs_to_value.haml | 2 + 8 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 app/models/metadata_new_belongs_to.rb create mode 100644 app/views/posts/attribute_ro/_new_belongs_to.haml create mode 100644 app/views/posts/attributes/_new_belongs_to.haml create mode 100644 app/views/posts/new_belongs_to_value.haml diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 28295793..bf030ee7 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -126,7 +126,7 @@ class PostsController < ApplicationController @uuid = @post.uuid.value @name = params.require(:name) - render 'posts/new_has_many_value', layout: false + render "posts/#{params.require(:attribute)}_value", layout: false else headers['HX-Retarget'] = "##{params.require(:form)}" headers['HX-Reswap'] = 'outerHTML' diff --git a/app/models/metadata_new_belongs_to.rb b/app/models/metadata_new_belongs_to.rb new file mode 100644 index 00000000..db90cb8f --- /dev/null +++ b/app/models/metadata_new_belongs_to.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Nueva interfaz +class MetadataNewBelongsTo < MetadataBelongsTo; end diff --git a/app/views/bootstrap/_custom_checkbox.haml b/app/views/bootstrap/_custom_checkbox.haml index c8cd1b41..ae35caf5 100644 --- a/app/views/bootstrap/_custom_checkbox.haml +++ b/app/views/bootstrap/_custom_checkbox.haml @@ -3,7 +3,7 @@ checkbox_attributes = local_assigns.slice(:id, :type, :name, :value, :required, :checked) checkbox_attributes[:type] ||= 'checkbox' -.custom-control.custom-checkbox +.custom-control{ class: "custom-#{checkbox_attributes[:type]}" } %input.custom-control-input{ **checkbox_attributes } %label.custom-control-label{ for: id, aria: { describedby: help_id } }= content - if (block = yield).present? diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index a288aa55..03e82e32 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -58,6 +58,7 @@ %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) } - if params[:inverse].present? diff --git a/app/views/posts/attribute_ro/_new_belongs_to.haml b/app/views/posts/attribute_ro/_new_belongs_to.haml new file mode 100644 index 00000000..c7e06be8 --- /dev/null +++ b/app/views/posts/attribute_ro/_new_belongs_to.haml @@ -0,0 +1,6 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td{ dir: dir, lang: locale } + - p = metadata.belongs_to + - if p + = link_to p.title.value, site_post_path(site, p.id) diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml new file mode 100644 index 00000000..d30d22dd --- /dev/null +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -0,0 +1,100 @@ +-# + 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 = 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" + +%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) } } + .form-group + = hidden_field_tag name, '' + = label_tag id, post_label_t(attribute, post: post) + -# Mostramos la lista de valores actuales. + + Al aceptar el modal, se vacía el listado y se completa en base a + renderizaciones con HTMX. Para poder hacer eso, tenemos que poder + acceder a todos los items dentro del modal (como array.item) y + enviar el valor al endpoint que devuelve uno por uno. Esto lo + tenemos disponible en Stimulus, pero queremos usar HTMX o técnica + similar para poder renderizar del lado del servidor. + + Para poder cancelar, mantenemos el estado original y desactivamos + o activamos los ítemes según estén incluidos en esa lista o no. + .row.row-cols-3.row-cols-md-4{ data: { target: 'array.current' } } + - metadata.values.slice(*metadata.value).each do |value| + = render 'posts/new_array_value', value: value + + = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + + = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do + - content_for :"#{id}_header" do + .form-group.flex-grow-1.mb-0 + = label_tag id, post_label_t(attribute, post: post) + %input.form-control{ data: { target: 'array.search', action: 'input->array#search' }, type: 'search', placeholder: t('.filter') } + + - content_for :"#{id}_body" do + .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' + + -# + Según la definición del campo, si hay un filtro, tenemos que poder + elegir qué tipo de esquema queremos o si hay uno solo, siempre + vamos a enviar ese. Si no hay ninguno, tendríamos que poder elegir + entre todos los esquemas. + + - content_for :"#{id}_footer" do + - layout = metadata.filter[:layout] + - if layout.is_a?(String) + %input{ type: 'hidden', name: 'layout', value: layout, form: post_form_id } + = render 'bootstrap/btn', content: t('.add', layout: site.layouts[layout].humanized_name), form: post_form_id, type: 'submit', class: 'm-0 mr-1' + - else + - layouts = layout&.map { |x| site.layouts[x] } + - layouts ||= site.layouts.values + .input-group.w-auto.flex-grow-1.my-0 + %select.form-control{ form: post_form_id, name: 'layout' } + - layouts.each do |layout| + %option{ value: layout.name }= layout.humanized_name + .input-group-append + = render 'bootstrap/btn', content: t('.add', layout: ''), form: post_form_id, type: 'submit', class: 'mb-0 mr-0' + = render 'bootstrap/btn', content: t('.accept'), action: 'array#accept modal#hide', class: 'm-0 mr-1' + = render 'bootstrap/btn', content: t('.cancel'), action: 'array#cancel modal#hide', class: 'm-0' + +-# + Este segundo modal es el que carga los formularios de + creación/modificación de artículos relacionados. Se envía a post_form + para que sea externo al formulario actual. +- content_for :post_form do + %form{ id: post_form_id, 'hx-get': site_posts_form_path(site), 'hx-target': "##{post_form_loaded_id}" } + %input{ type: 'hidden', name: 'show', value: post_modal_id } + %input{ type: 'hidden', name: 'hide', value: modal_id } + %input{ type: 'hidden', name: 'target', value: value_list_id } + %input{ type: 'hidden', name: 'swap', value: 'beforeend' } + %input{ type: 'hidden', name: 'base', value: id } + %input{ type: 'hidden', name: 'name', value: name } + %input{ type: 'hidden', name: 'form', value: form_id } + %input{ type: 'hidden', name: 'attribute', value: metadata.type } + - if metadata.inverse? + %input{ type: 'hidden', name: 'inverse', value: metadata.inverse } + %div{ id: post_modal_id, data: { controller: 'modal' } } + = render 'bootstrap/modal', id: post_id, modal_content_attributes: { class: 'h-100' } do + - content_for :"#{post_id}_body" do + %div{ id: post_form_loaded_id } + - content_for :"#{post_id}_footer" do + = render 'bootstrap/btn', form: form_id, content: t('.save'), type: 'submit' + -# @todo: Volver al otro modal + = render 'bootstrap/btn', content: t('.cancel'), action: 'modal#hide' diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 54eb8d40..724f8247 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -87,6 +87,7 @@ %input{ type: 'hidden', name: 'base', value: id } %input{ type: 'hidden', name: 'name', value: name } %input{ type: 'hidden', name: 'form', value: form_id } + %input{ type: 'hidden', name: 'attribute', value: metadata.type } - if metadata.inverse? %input{ type: 'hidden', name: 'inverse', value: metadata.inverse } %div{ id: post_modal_id, data: { controller: 'modal' } } diff --git a/app/views/posts/new_belongs_to_value.haml b/app/views/posts/new_belongs_to_value.haml new file mode 100644 index 00000000..ab324763 --- /dev/null +++ b/app/views/posts/new_belongs_to_value.haml @@ -0,0 +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'