From 8384a8a32ad1fadb2fa6c5ec4fe71312cffb9768 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 25 Mar 2024 14:29:05 -0300 Subject: [PATCH 001/161] feat: darle formato al html guardado #14417 --- Gemfile | 1 + Gemfile.lock | 2 ++ app/models/metadata_content.rb | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 466ec079..995cb849 100644 --- a/Gemfile +++ b/Gemfile @@ -76,6 +76,7 @@ gem 'webpacker' gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' gem 'kaminari' gem 'device_detector' +gem 'htmlbeautifier' # database gem 'hairtrigger' diff --git a/Gemfile.lock b/Gemfile.lock index 78563c84..862880b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -255,6 +255,7 @@ GEM heapy (0.2.0) thor hiredis (0.6.3-x86_64-linux-musl) + htmlbeautifier (1.4.2) http_parser.rb (0.8.0-x86_64-linux-musl) httparty (0.21.0) mini_mime (>= 1.0.0) @@ -616,6 +617,7 @@ DEPENDENCIES haml-lint hamlit-rails hiredis + htmlbeautifier httparty icalendar image_processing diff --git a/app/models/metadata_content.rb b/app/models/metadata_content.rb index 761518e8..444ee2fe 100644 --- a/app/models/metadata_content.rb +++ b/app/models/metadata_content.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'htmlbeautifier' + # Se encarga del contenido del artículo y quizás otros campos que # requieran texto largo. class MetadataContent < MetadataTemplate @@ -86,7 +88,7 @@ class MetadataContent < MetadataTemplate end end - html.to_s.html_safe + HtmlBeautifier.beautify(html.to_s).html_safe end # Limpia estilos en base a una lista de permitidos From 9e95dedb5c5b09644f12b3452ce9fc438541a5ef Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 14:53:10 -0300 Subject: [PATCH 002/161] =?UTF-8?q?feat:=20vamos=20a=20usar=20nanoid=20par?= =?UTF-8?q?a=20generar=20ids=20=C3=BAnicos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 1 + Gemfile.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Gemfile b/Gemfile index 5714b981..30794e83 100644 --- a/Gemfile +++ b/Gemfile @@ -85,6 +85,7 @@ 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 7f5284ef..db6b892d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -373,6 +373,7 @@ 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 @@ -677,6 +678,7 @@ DEPENDENCIES memory_profiler mini_magick mobility + nanoid net-ssh nokogiri pg From 5f5026bccabdd97f970c845bddc6e355d0b7fc6e Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 14:57:13 -0300 Subject: [PATCH 003/161] =?UTF-8?q?fix:=20solo=20usar=20fuertecita=20dentr?= =?UTF-8?q?o=20de=20los=20dise=C3=B1os?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/application.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 4d1d0848..68b610bc 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -318,13 +318,13 @@ svg { } } -.custom-control-label { - font-weight: bold; -} - .designs { .design { margin-top: 1rem; + + .custom-control-label { + font-weight: bold; + } } } From c75deaf030f11bcae796e5110bc649a3869766cb Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 14:59:23 -0300 Subject: [PATCH 004/161] feat: poder modificar atributos del custom-input --- app/views/bootstrap/_custom_checkbox.haml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/bootstrap/_custom_checkbox.haml b/app/views/bootstrap/_custom_checkbox.haml index 0c3ff3a6..a2cf6c27 100644 --- a/app/views/bootstrap/_custom_checkbox.haml +++ b/app/views/bootstrap/_custom_checkbox.haml @@ -1,6 +1,9 @@ - help_id = "#{id}_help" +- checkbox_attributes = local_assigns.slice(:id, :type, :name, :value, :required, :checked) +- checkbox_attributes[:type] ||= 'checkbox' .custom-control.custom-checkbox - %input.custom-control-input{ id: id, type: 'checkbox', name: name, value: value, required: required } + %input.custom-control-input{ **checkbox_attributes } %label.custom-control-label{ for: id, aria: { describedby: help_id } }= content - %small.form-text.text-muted{ id: help_id }= yield + - if (block = yield).present? + %small.form-text.text-muted{ id: help_id }= block From 8fe964686bfc7c46e5b89636c9fd3b47d23dc6e7 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 15:00:08 -0300 Subject: [PATCH 005/161] feat: eliminar tildes de una string, sin transliterar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit usamos esto porque es más rápido y podemos tener un proceso similar en js sin implementar transliteración --- app/lib/core_extensions/string/remove_diacritics.rb | 12 ++++++++++++ config/initializers/core_extensions.rb | 1 + 2 files changed, 13 insertions(+) create mode 100644 app/lib/core_extensions/string/remove_diacritics.rb diff --git a/app/lib/core_extensions/string/remove_diacritics.rb b/app/lib/core_extensions/string/remove_diacritics.rb new file mode 100644 index 00000000..679db13d --- /dev/null +++ b/app/lib/core_extensions/string/remove_diacritics.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module CoreExtensions + module String + # Elimina tildes + module RemoveDiacritics + def remove_diacritics + unicode_normalize(:nfd).gsub(/[^\x00-\x7F]/, '') + end + end + end +end diff --git a/config/initializers/core_extensions.rb b/config/initializers/core_extensions.rb index 7d1eab9e..6861da45 100644 --- a/config/initializers/core_extensions.rb +++ b/config/initializers/core_extensions.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true String.include CoreExtensions::String::StripTags +String.include CoreExtensions::String::RemoveDiacritics Jekyll::Document.include CoreExtensions::Jekyll::Document::Path Jekyll::DataReader.include Jekyll::Readers::DataReaderDecorator From ee459b2d4e75980eba343d1686599667eff7a427 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 15:06:44 -0300 Subject: [PATCH 006/161] feat: modal de boostrap 4.6 --- app/assets/stylesheets/application.scss | 1 + .../controllers/modal_controller.js | 40 +++++++++++++++++++ app/views/bootstrap/_modal.haml | 38 ++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 app/javascript/controllers/modal_controller.js create mode 100644 app/views/bootstrap/_modal.haml diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 68b610bc..7d9c8c9d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -20,6 +20,7 @@ $form-feedback-valid-color: $black; $form-feedback-invalid-color: $magenta; $form-feedback-icon-valid-color: $black; $component-active-bg: $magenta; +$zindex-modal-backdrop: 0; $spacers: ( 2-plus: 0.75rem diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js new file mode 100644 index 00000000..3a614b80 --- /dev/null +++ b/app/javascript/controllers/modal_controller.js @@ -0,0 +1,40 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + static targets = ["modal", "backdrop"]; + + show(event = undefined) { + event?.preventDefault(); + + this.modalTarget.style.display = "block"; + this.backdropTarget.style.display = "block"; + this.modalTarget.setAttribute("role", "dialog"); + this.modalTarget.setAttribute("aria-modal", true); + this.modalTarget.removeAttribute("aria-hidden"); + + window.document.body.classList.add("modal-open"); + + setTimeout(() => { + this.modalTarget.classList.add("show"); + this.backdropTarget.classList.add("show"); + }, 1); + } + + hide(event = undefined) { + event?.preventDefault(); + + this.backdropTarget.classList.remove("show"); + this.modalTarget.classList.remove("show"); + + this.modalTarget.setAttribute("aria-hidden", true); + this.modalTarget.removeAttribute("role"); + this.modalTarget.removeAttribute("aria-modal"); + + setTimeout(() => { + this.modalTarget.style.display = ""; + this.backdropTarget.style.display = ""; + }, 500); + + window.document.body.classList.remove("modal-open"); + } +} diff --git a/app/views/bootstrap/_modal.haml b/app/views/bootstrap/_modal.haml new file mode 100644 index 00000000..b57fc94e --- /dev/null +++ b/app/views/bootstrap/_modal.haml @@ -0,0 +1,38 @@ +-# + # Modal + + @see {https://getbootstrap.com/docs/4.6/components/modal/} + @see {https://github.com/bullet-train-co/nice_partials/issues/99} + @param :id [String] El ID del modal + @param :modal_content_attributes [Hash] Atributos para el contenido del modal + @param :hide_actions [Array] Acciones al ocultar el modal + @yield :ID_body Contenido + @yield :ID_header Contenido del header (opcional) + @yield :ID_footer Contenido del pie (opcional) + @example + = render 'bootstrap/modal', id: 'algo' do |partial| + - content_for :algo_header do + = 'título' + - content_for :algo_body do + = 'contenido' + - content_for :algo_footer do + = 'pie' + +- local_assigns[:hide_actions] ||= [] +- local_assigns[:hide_actions] << 'click->modal#hide' +- local_assigns[:modal_content_attributes] ||= {} + +.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-content{ **local_assigns[:modal_content_attributes] } + - if (header = yield(:"#{id}_header")).present? + .modal-header= header + + .modal-body= yield(:"#{id}_body") + + .modal-footer.flex-nowrap + - if (footer = yield(:"#{id}_footer")) + = footer + - else + %button.btn.btn-secondary.m-0{ type: 'button', data: { action: 'modal#hide' } }= t('.close') From 7dbe12ed66c851b3f3b3de5e9b4bd1d38c0aed26 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 15:07:22 -0300 Subject: [PATCH 007/161] fix: no permitir que htmx haga peticiones fuera del panel --- app/javascript/packs/application.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index e10e2b5d..c523b0f0 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -42,3 +42,4 @@ Turbolinks.start() ActiveStorage.start() window.htmx = require('htmx.org/dist/htmx.js') +window.htmx.config.selfRequestsOnly = true; From 76de2e543e1cc0562937e89545c2e57909481e31 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 15:17:47 -0300 Subject: [PATCH 008/161] =?UTF-8?q?feat:=20un=20bot=C3=B3n=20accionable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/bootstrap/_btn.haml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/views/bootstrap/_btn.haml diff --git a/app/views/bootstrap/_btn.haml b/app/views/bootstrap/_btn.haml new file mode 100644 index 00000000..22f60b3e --- /dev/null +++ b/app/views/bootstrap/_btn.haml @@ -0,0 +1,12 @@ +-# + Un botón + + @param :content [String] Contenido + @param :action [String] Acción de Stimulus + @param :target [String] Objetivo de Stimulus + @param [Hash] Atributos en bruto, con mayor prioridad que action y target +- attributes = local_assigns.to_h.except(:content) +- attributes[:data] ||= {} +- attributes[:data][:action] ||= local_assigns[:action] +- attributes[:data][:target] ||= local_assigns[:target] +%button.btn.btn-secondary{ type: 'button', **attributes }= content From ae06e3e788a8c5f92ccd735bc81baa1db7e309e9 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 15:24:08 -0300 Subject: [PATCH 009/161] =?UTF-8?q?feat:=20redise=C3=B1ar=20arrays=20#1506?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/posts_controller.rb | 17 +++ .../controllers/array_controller.js | 120 ++++++++++++++++++ app/javascript/etc/htmx_abort.js | 4 + app/models/metadata_new_array.rb | 6 + app/views/layouts/application.html.haml | 1 + app/views/posts/_new_array_value.haml | 2 + app/views/posts/attributes/_new_array.haml | 58 +++++++++ app/views/posts/new_array.haml | 8 ++ app/views/posts/new_array_value.haml | 1 + config/locales/en.yml | 9 ++ config/locales/es.yml | 9 ++ config/routes.rb | 4 + 12 files changed, 239 insertions(+) create mode 100644 app/javascript/controllers/array_controller.js create mode 100644 app/models/metadata_new_array.rb create mode 100644 app/views/posts/_new_array_value.haml create mode 100644 app/views/posts/attributes/_new_array.haml create mode 100644 app/views/posts/new_array.haml create mode 100644 app/views/posts/new_array_value.haml diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 057c3068..f241dfb1 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -15,6 +15,23 @@ class PostsController < ApplicationController { locale: locale } end + # @todo Mover a tu propio scope + def new_array + @value = params.require(:value).strip + @name = params.require(:name).strip + id = params.require(:id).strip + + headers['HX-Trigger-After-Swap'] = 'htmx:resetForm' + + render layout: false + end + + def new_array_value + @value = params.require(:value).strip + + render layout: false + end + def index authorize Post diff --git a/app/javascript/controllers/array_controller.js b/app/javascript/controllers/array_controller.js new file mode 100644 index 00000000..66cc2290 --- /dev/null +++ b/app/javascript/controllers/array_controller.js @@ -0,0 +1,120 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + static targets = ["item", "search", "current"]; + + connect() { + // TODO: Stimulus >1 + this.newArrayValueURL = new URL(window.location.origin); + this.newArrayValueURL.pathname = this.element.dataset.arrayNewArrayValue; + this.originalValue = JSON.parse(this.element.dataset.arrayOriginalValue); + } + + /* + * Al eliminar el ítem, buscamos por su ID y lo eliminamos del + * documento. + */ + remove(event) { + // TODO: Stimulus >1 + event.preventDefault(); + + this.itemTargets + .find((x) => x.id === event.target.dataset.removeTargetParam) + ?.remove(); + } + + /* + * Al buscar, eliminamos las tildes y mayúsculas para no depender de + * cómo se escribió. + * + * Luego buscamos eso en el valor limpio, ignorando los items que ya + * están activados. + * + * Si el término de búsqueda está vacío, volvemos a la lista original. + */ + search(event) { + const needle = this.searchTarget.value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .trim(); + + if (needle) { + for (const itemTarget of this.itemTargets) { + itemTarget.style.display = + itemTarget.querySelector("input")?.checked || + itemTarget.dataset.searchableValue.includes(needle) + ? "" + : "none"; + } + } else { + for (const itemTarget of this.itemTargets) { + itemTarget.style.display = ""; + } + } + } + + /* + * Obtiene el input de un elemento + * + * @param [HTMLElement] + * @return [HTMLElement,nil] + */ + inputFrom(target) { + if (target.tagName === "INPUT") return target; + + return target.querySelector("input"); + } + + /* + * Detecta si el item es o contiene un checkbox/radio activado. + * + * @param [HTMLElement] + * @return [Bool] + */ + isChecked(itemTarget) { + return this.inputFrom(itemTarget)?.checked || false; + } + + /* + * Al cancelar, se vuelve al estado original de la lista + */ + cancel(event) { + for (const itemTarget of this.itemTargets) { + const input = this.inputFrom(itemTarget); + + input.checked = this.originalValue.includes(itemTarget.dataset.value); + } + } + + /* + * Al aceptar, se envía todo el listado de valores nuevos al _backend_ + * para que devuelva la representación de cada ítem en HTML. Además, + * se guarda el nuevo valor como la lista original, para la próxima + * cancelación. + */ + accept(event) { + this.currentTarget.innerHTML = ""; + this.originalValue = []; + + for (const itemTarget of this.itemTargets) { + if (!itemTarget.dataset.value) continue; + if (!this.isChecked(itemTarget)) continue; + + this.originalValue.push(itemTarget.dataset.value); + this.newArrayValueURL.searchParams.set("value", itemTarget.dataset.value); + + // TODO: Renderizarlas todas juntas + fetch(this.newArrayValueURL) + .then((response) => response.text()) + .then((body) => + this.currentTarget.insertAdjacentHTML("beforeend", body), + ); + } + + // TODO: Stimulus >1 + this.element.dataset.arrayOriginalValue = JSON.stringify( + this.originalValue, + ); + } +} diff --git a/app/javascript/etc/htmx_abort.js b/app/javascript/etc/htmx_abort.js index 308d0315..75e497ba 100644 --- a/app/javascript/etc/htmx_abort.js +++ b/app/javascript/etc/htmx_abort.js @@ -5,3 +5,7 @@ document.addEventListener("turbolinks:click", () => { window.htmx.trigger(hx, "htmx:abort"); } }); + +document.addEventListener("htmx:resetForm", (event) => { + event.target.reset(); +}); diff --git a/app/models/metadata_new_array.rb b/app/models/metadata_new_array.rb new file mode 100644 index 00000000..a76ff9f2 --- /dev/null +++ b/app/models/metadata_new_array.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# Implementa la nueva interfaz de +class MetadataNewArray < MetadataArray + +end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index eaa15eb4..65d3f777 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -25,6 +25,7 @@ = render 'layouts/breadcrumb' = render 'layouts/flash' + = yield(:post_form) = yield - if flash[:js] diff --git a/app/views/posts/_new_array_value.haml b/app/views/posts/_new_array_value.haml new file mode 100644 index 00000000..75c5bf4d --- /dev/null +++ b/app/views/posts/_new_array_value.haml @@ -0,0 +1,2 @@ +.col + %p= value diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml new file mode 100644 index 00000000..79ba22e2 --- /dev/null +++ b/app/views/posts/attributes/_new_array.haml @@ -0,0 +1,58 @@ +-# + Genera un listado de checkboxes entre los que se puede elegir para guardar +- id = "#{base}_#{attribute}" +- name = "#{base}[#{attribute}][]" +- form_id = "form-#{Nanoid.generate}" + +%div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_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-1.row-cols-md-2{ data: { target: 'array.current' } } + - metadata.value.sort_by(&:remove_diacritics).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: "#{id}_body" } + -# 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 + + - content_for :"#{id}_footer" do + .input-group.w-auto.flex-grow-1.my-0 + %input.form-control{form: form_id, name: 'value', type: 'text', placeholder: t('.add_new')} + .input-group-append + = render 'bootstrap/btn', content: t('.add'), form: 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' + + -# Los formularios para HTMX se colocan por fuera del formulario + principal, porque HTML5 no soporta formularios anidados. Los campos + quedan unidos al formulario por su atributo `id`. + + Al enviar el formulario se obtiene una nueva opción con el valor + y se la agrega al final del listado. + - content_for :post_form do + %form{ id: form_id, 'hx-get': site_posts_new_array_path(site), 'hx-target': "##{id}_body", 'hx-swap': 'beforeend' } + %input{ type: 'hidden', name: 'name', value: name } + %input{ type: 'hidden', name: 'id', value: form_id } diff --git a/app/views/posts/new_array.haml b/app/views/posts/new_array.haml new file mode 100644 index 00000000..81cdcdea --- /dev/null +++ b/app/views/posts/new_array.haml @@ -0,0 +1,8 @@ +- item_id = "item-#{Nanoid.generate}" + +.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 + %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_array_value.haml b/app/views/posts/new_array_value.haml new file mode 100644 index 00000000..c71ed3b5 --- /dev/null +++ b/app/views/posts/new_array_value.haml @@ -0,0 +1 @@ += render 'posts/new_array_value', value: @value diff --git a/config/locales/en.yml b/config/locales/en.yml index 5e9a2377..88b1db19 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -721,7 +721,16 @@ en: save_draft: 'Save as draft' invalid_help: 'Some fields need attention! Please search for the fields marked as invalid.' sending_help: Saving, please wait... + new_array: + remove: "Remove" attributes: + new_array: + edit: "Edit" + filter: "Start typing to filter..." + add_new: "Add new option" + add: "Add" + accept: "Accept" + cancel: "Cancel" add: Add lang: label: Language diff --git a/config/locales/es.yml b/config/locales/es.yml index a07b3799..4945c19f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -729,7 +729,16 @@ es: save_draft: 'Guardar como borrador' invalid_help: '¡Te faltan completar algunos campos! Busca los que estén marcados como inválidos' sending_help: Guardando, por favor espera... + new_array: + remove: "Eliminar" attributes: + new_array: + edit: "Editar" + filter: "Empezá a escribir para filtrar..." + add_new: "Agregar nueva opción" + add: "Agregar" + accept: "Aceptar" + cancel: "Cancelar" add: Agregar lang: label: Idioma diff --git a/config/routes.rb b/config/routes.rb index 9d5c974a..eb20edce 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -99,6 +99,10 @@ Rails.application.routes.draw do nested do scope '/(:locale)', constraint: /[a-z]{2}(-[A-Z]{2})?/ do post :'posts/reorder', to: 'posts#reorder' + + get :'posts/new_array', to: 'posts#new_array' + get :'posts/new_array_value', to: 'posts#new_array_value' + resources :posts do get 'p/:page', action: :index, on: :collection get :preview, to: 'posts#preview' From b3a1673795ed738404d66c375b336cd8ff5cc841 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 15:30:25 -0300 Subject: [PATCH 010/161] chore: linting --- app/models/metadata_new_array.rb | 3 +-- app/views/bootstrap/_btn.haml | 9 +++++---- app/views/bootstrap/_custom_checkbox.haml | 7 ++++--- app/views/bootstrap/_modal.haml | 7 ++++--- app/views/posts/attributes/_new_array.haml | 11 ++++++----- app/views/posts/new_array.haml | 2 +- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/app/models/metadata_new_array.rb b/app/models/metadata_new_array.rb index a76ff9f2..65993be2 100644 --- a/app/models/metadata_new_array.rb +++ b/app/models/metadata_new_array.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# Implementa la nueva interfaz de +# Implementa la nueva interfaz de gestión de valores class MetadataNewArray < MetadataArray - end diff --git a/app/views/bootstrap/_btn.haml b/app/views/bootstrap/_btn.haml index 22f60b3e..1bbebb26 100644 --- a/app/views/bootstrap/_btn.haml +++ b/app/views/bootstrap/_btn.haml @@ -5,8 +5,9 @@ @param :action [String] Acción de Stimulus @param :target [String] Objetivo de Stimulus @param [Hash] Atributos en bruto, con mayor prioridad que action y target -- attributes = local_assigns.to_h.except(:content) -- attributes[:data] ||= {} -- attributes[:data][:action] ||= local_assigns[:action] -- attributes[:data][:target] ||= local_assigns[:target] +:ruby + attributes = local_assigns.to_h.except(:content) + attributes[:data] ||= {} + attributes[:data][:action] ||= local_assigns[:action] + attributes[:data][:target] ||= local_assigns[:target] %button.btn.btn-secondary{ type: 'button', **attributes }= content diff --git a/app/views/bootstrap/_custom_checkbox.haml b/app/views/bootstrap/_custom_checkbox.haml index a2cf6c27..c8cd1b41 100644 --- a/app/views/bootstrap/_custom_checkbox.haml +++ b/app/views/bootstrap/_custom_checkbox.haml @@ -1,6 +1,7 @@ -- help_id = "#{id}_help" -- checkbox_attributes = local_assigns.slice(:id, :type, :name, :value, :required, :checked) -- checkbox_attributes[:type] ||= 'checkbox' +:ruby + help_id = "#{id}_help" + checkbox_attributes = local_assigns.slice(:id, :type, :name, :value, :required, :checked) + checkbox_attributes[:type] ||= 'checkbox' .custom-control.custom-checkbox %input.custom-control-input{ **checkbox_attributes } diff --git a/app/views/bootstrap/_modal.haml b/app/views/bootstrap/_modal.haml index b57fc94e..f6dafc2a 100644 --- a/app/views/bootstrap/_modal.haml +++ b/app/views/bootstrap/_modal.haml @@ -18,9 +18,10 @@ - content_for :algo_footer do = 'pie' -- local_assigns[:hide_actions] ||= [] -- local_assigns[:hide_actions] << 'click->modal#hide' -- local_assigns[:modal_content_attributes] ||= {} +:ruby + local_assigns[:hide_actions] ||= [] + local_assigns[:hide_actions] << 'click->modal#hide' + local_assigns[:modal_content_attributes] ||= {} .modal.fade{ tabindex: -1, aria: { hidden: 'true' }, data: { target: 'modal.modal' } } .modal-backdrop.fade{ data: { target: 'modal.backdrop', action: local_assigns[:hide_actions].join(' ') } } diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 79ba22e2..394345a6 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -1,8 +1,9 @@ -# Genera un listado de checkboxes entre los que se puede elegir para guardar -- id = "#{base}_#{attribute}" -- name = "#{base}[#{attribute}][]" -- form_id = "form-#{Nanoid.generate}" +:ruby + id = "#{base}_#{attribute}" + name = "#{base}[#{attribute}][]" + form_id = "form-#{Nanoid.generate}" %div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } .form-group @@ -29,7 +30,7 @@ - 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') } + %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: "#{id}_body" } @@ -40,7 +41,7 @@ - content_for :"#{id}_footer" do .input-group.w-auto.flex-grow-1.my-0 - %input.form-control{form: form_id, name: 'value', type: 'text', placeholder: t('.add_new')} + %input.form-control{ form: form_id, name: 'value', type: 'text', placeholder: t('.add_new') } .input-group-append = render 'bootstrap/btn', content: t('.add'), form: 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' diff --git a/app/views/posts/new_array.haml b/app/views/posts/new_array.haml index 81cdcdea..62f74854 100644 --- a/app/views/posts/new_array.haml +++ b/app/views/posts/new_array.haml @@ -5,4 +5,4 @@ .flex-grow-1 = render 'bootstrap/custom_checkbox', name: @name, id: "value-#{Nanoid.generate}", value: @value, checked: true, content: @value %div - %button.btn.btn-sm.m-0{ data: { action: 'array#remove', 'remove-target-param': item_id }}= t('.remove') + %button.btn.btn-sm.m-0{ data: { action: 'array#remove', 'remove-target-param': item_id } }= t('.remove') From 87f95cf7e50381c5f63a1781e87c8b10e077bd16 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 15:35:17 -0300 Subject: [PATCH 011/161] feat: poder ver los cambios hechos --- app/views/posts/attribute_ro/_new_array.haml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/views/posts/attribute_ro/_new_array.haml diff --git a/app/views/posts/attribute_ro/_new_array.haml b/app/views/posts/attribute_ro/_new_array.haml new file mode 100644 index 00000000..20a0a545 --- /dev/null +++ b/app/views/posts/attribute_ro/_new_array.haml @@ -0,0 +1,8 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + - if metadata.value.respond_to? :each + - metadata.value.each do |v| + %span.badge.badge-primary= v + - else + %span.badge.badge-primary{ lang: locale, dir: dir }= metadata.value From 21ccedc63950cec43a929fc131f1d29b1530c1af Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 17:00:09 -0300 Subject: [PATCH 012/161] fix: es necesario nice_partials para no tener que hacer yield --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 30794e83..500face1 100644 --- a/Gemfile +++ b/Gemfile @@ -86,6 +86,7 @@ gem 'after_commit_everywhere', '~> 1.0' gem 'aasm' gem 'que-web' gem 'nanoid' +gem 'nice_partials' # database gem 'hairtrigger' diff --git a/Gemfile.lock b/Gemfile.lock index db6b892d..a0d203d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -385,6 +385,8 @@ GEM net-protocol net-ssh (7.2.1) netaddr (2.0.6) + nice_partials (0.10.0) + actionview (>= 4.2.6) nio4r (2.7.0-x86_64-linux-musl) nokogiri (1.16.0-x86_64-linux-musl) mini_portile2 (~> 2.8.2) @@ -680,6 +682,7 @@ DEPENDENCIES mobility nanoid net-ssh + nice_partials nokogiri pg pg_search From c780466c7bb54a3ed7bce29fb83d348737c5818d Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 17:12:05 -0300 Subject: [PATCH 013/161] feat: a veces no se pueden agregar opciones nuevas --- app/models/metadata_new_predefined_array.rb | 4 ++ .../attribute_ro/_new_predefined_array.haml | 5 ++ .../attributes/_new_predefined_array.haml | 48 +++++++++++++++++++ config/locales/en.yml | 5 ++ config/locales/es.yml | 5 ++ 5 files changed, 67 insertions(+) create mode 100644 app/models/metadata_new_predefined_array.rb create mode 100644 app/views/posts/attribute_ro/_new_predefined_array.haml create mode 100644 app/views/posts/attributes/_new_predefined_array.haml diff --git a/app/models/metadata_new_predefined_array.rb b/app/models/metadata_new_predefined_array.rb new file mode 100644 index 00000000..8c15155f --- /dev/null +++ b/app/models/metadata_new_predefined_array.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Nueva interfaz para arrays predefinidos +class MetadataNewPredefinedArray < MetadataPredefinedArray; end diff --git a/app/views/posts/attribute_ro/_new_predefined_array.haml b/app/views/posts/attribute_ro/_new_predefined_array.haml new file mode 100644 index 00000000..88a82626 --- /dev/null +++ b/app/views/posts/attribute_ro/_new_predefined_array.haml @@ -0,0 +1,5 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + - metadata.value.each do |v| + %span.badge.badge-primary{ dir: dir, lang: locale }= metadata.values.key v diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml new file mode 100644 index 00000000..5203c12b --- /dev/null +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -0,0 +1,48 @@ +-# + Genera un listado de checkboxes entre los que se puede elegir para + guardar, pero no se pueden agregar nuevos. + +:ruby + id = "#{base}_#{attribute}" + name = "#{base}[#{attribute}][]" + form_id = "form-#{Nanoid.generate}" + +%div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_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-1.row-cols-md-2{ data: { target: 'array.current' } } + - metadata.values.slice(*metadata.value).each_key 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: "#{id}_body" } + -# 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 + + - content_for :"#{id}_footer" do + -# Alinear los botones a la derecha + .flex-grow-1 + = 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' diff --git a/config/locales/en.yml b/config/locales/en.yml index 88b1db19..375a52b5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -724,6 +724,11 @@ en: new_array: remove: "Remove" attributes: + new_predefined_array: + edit: "Edit" + filter: "Start typing to filter..." + accept: "Accept" + cancel: "Cancel" new_array: edit: "Edit" filter: "Start typing to filter..." diff --git a/config/locales/es.yml b/config/locales/es.yml index 4945c19f..a7ae423b 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -732,6 +732,11 @@ es: new_array: remove: "Eliminar" attributes: + new_predefined_array: + edit: "Editar" + filter: "Empezá a escribir para filtrar..." + accept: "Aceptar" + cancel: "Cancelar" new_array: edit: "Editar" filter: "Empezá a escribir para filtrar..." From 1e413e57f5f5a9271bbf43a647672747db59ec9e Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 17:47:18 -0300 Subject: [PATCH 014/161] =?UTF-8?q?feat:=20relaci=C3=B3n=20de=201:n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/metadata_new_has_many.rb | 4 ++ .../posts/attribute_ro/_new_has_many.haml | 6 +++ app/views/posts/attributes/_new_has_many.haml | 44 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 app/models/metadata_new_has_many.rb create mode 100644 app/views/posts/attribute_ro/_new_has_many.haml create mode 100644 app/views/posts/attributes/_new_has_many.haml diff --git a/app/models/metadata_new_has_many.rb b/app/models/metadata_new_has_many.rb new file mode 100644 index 00000000..3bf37d45 --- /dev/null +++ b/app/models/metadata_new_has_many.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Interfaz nueva para uno a muchos +class MetadataNewHasMany < MetadataHasMany; end diff --git a/app/views/posts/attribute_ro/_new_has_many.haml b/app/views/posts/attribute_ro/_new_has_many.haml new file mode 100644 index 00000000..d6b51a7a --- /dev/null +++ b/app/views/posts/attribute_ro/_new_has_many.haml @@ -0,0 +1,6 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + %ul{ dir: dir, lang: locale } + - metadata.has_many.each do |p| + %li= link_to p.title.value, site_post_path(site, p.id) diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml new file mode 100644 index 00000000..9477998d --- /dev/null +++ b/app/views/posts/attributes/_new_has_many.haml @@ -0,0 +1,44 @@ +-# + Genera un listado de checkboxes entre los que se puede elegir para guardar +:ruby + id = "#{base}_#{attribute}" + name = "#{base}[#{attribute}][]" + form_id = "form-#{Nanoid.generate}" + +%div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_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-1.row-cols-md-2{ 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: "#{id}_body" } + - 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 + + - content_for :"#{id}_footer" do + .flex-grow-1 + = 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' From f89595aff1bfbff3d0507c7383b20934ecd6b79c Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 17:58:44 -0300 Subject: [PATCH 015/161] feat: generar la lista con links --- app/controllers/posts_controller.rb | 8 ++++++++ app/views/posts/_new_related_post.haml | 2 ++ app/views/posts/attributes/_new_has_many.haml | 8 ++++---- config/routes.rb | 1 + 4 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 app/views/posts/_new_related_post.haml diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index f241dfb1..aad3855d 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -32,6 +32,14 @@ class PostsController < ApplicationController render layout: false end + def new_related_post + @uuid = params.require(:value).strip + + @indexed_post = site.indexed_posts.find_by!(post_id: @uuid) + + render layout: false + end + def index authorize Post diff --git a/app/views/posts/_new_related_post.haml b/app/views/posts/_new_related_post.haml new file mode 100644 index 00000000..f3cc2a94 --- /dev/null +++ b/app/views/posts/_new_related_post.haml @@ -0,0 +1,2 @@ +.col + %p= link_to post.title, post.path diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 9477998d..f8ec89d4 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -5,7 +5,7 @@ name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" -%div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } +%div{ 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) @@ -34,9 +34,9 @@ - content_for :"#{id}_body" do .form-group.mb-0{ id: "#{id}_body" } - - 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 + - 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 - content_for :"#{id}_footer" do .flex-grow-1 diff --git a/config/routes.rb b/config/routes.rb index eb20edce..be50fafd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -102,6 +102,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' resources :posts do get 'p/:page', action: :index, on: :collection From 03b847c946a0a9524e695fd709693a24161eac6f Mon Sep 17 00:00:00 2001 From: f Date: Fri, 17 May 2024 18:00:37 -0300 Subject: [PATCH 016/161] fix: traducciones --- config/locales/en.yml | 5 +++++ config/locales/es.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 375a52b5..6a2eb7d8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -724,6 +724,11 @@ en: new_array: remove: "Remove" attributes: + new_has_many: + edit: "Edit" + filter: "Start typing to filter..." + accept: "Accept" + cancel: "Cancel" new_predefined_array: edit: "Edit" filter: "Start typing to filter..." diff --git a/config/locales/es.yml b/config/locales/es.yml index a7ae423b..1da0edf9 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -732,6 +732,11 @@ es: new_array: remove: "Eliminar" attributes: + new_has_many: + edit: "Editar" + filter: "Empezá a escribir para filtrar..." + accept: "Aceptar" + cancel: "Cancelar" new_predefined_array: edit: "Editar" filter: "Empezá a escribir para filtrar..." From 4876c61d10301e23889e4afe627e73fc53d1709f Mon Sep 17 00:00:00 2001 From: f Date: Sat, 18 May 2024 12:10:37 -0300 Subject: [PATCH 017/161] =?UTF-8?q?feat:=20cargar=20el=20formulario=20del?= =?UTF-8?q?=20post=20sin=20decoraci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/posts_controller.rb | 16 ++++++++++++++++ app/views/posts/form.haml | 7 +++++++ config/routes.rb | 1 + 3 files changed, 24 insertions(+) create mode 100644 app/views/posts/form.haml diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index aad3855d..d956fd2c 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -40,6 +40,22 @@ class PostsController < ApplicationController 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) + + @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: params.require(:layout)) + end + + render layout: false + end + def index authorize Post diff --git a/app/views/posts/form.haml b/app/views/posts/form.haml new file mode 100644 index 00000000..36d15fde --- /dev/null +++ b/app/views/posts/form.haml @@ -0,0 +1,7 @@ +-# + El formulario sin ninguna decoración, para incluir dentro de otros + elementos. + + @param :site [Site] + @param :post [Post] += render 'posts/form', site: @site, post: @post diff --git a/config/routes.rb b/config/routes.rb index be50fafd..45410782 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/form', to: 'posts#form' resources :posts do get 'p/:page', action: :index, on: :collection From 71b87bfc0b39551e5638836143b17a8a0431d7e9 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 18 May 2024 18:03:01 -0300 Subject: [PATCH 018/161] feat: poder abrir el modal con un evento --- .../controllers/modal_controller.js | 19 +++++++++++++++++++ app/views/posts/attributes/_new_has_many.haml | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js index 3a614b80..011c1254 100644 --- a/app/javascript/controllers/modal_controller.js +++ b/app/javascript/controllers/modal_controller.js @@ -3,8 +3,27 @@ import { Controller } from "stimulus"; export default class extends Controller { static targets = ["modal", "backdrop"]; + // TODO: Stimulus >1 + connect() { + this.showEvent = this.show.bind(this); + + window.addEventListener("modal:show", this.showEvent); + } + + // TODO: Stimulus >1 + disconnect() { + window.removeEventListener("modal:show", this.showEvent); + } + + /* + * Podemos enviar la orden de apertura como un click o como un + * CustomEvent incluyendo el id del modal como detail. + */ show(event = undefined) { event?.preventDefault(); + const modalId = event?.detail?.id; + + if (modalId && this.element.id !== modalId) return; this.modalTarget.style.display = "block"; this.backdropTarget.style.display = "block"; diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index f8ec89d4..76cf299f 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -4,8 +4,9 @@ id = "#{base}_#{attribute}" name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" + modal_id = "modal-#{Nanoid.generate}" -%div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_related_post_path(site) } } +%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) From 71a15ae693f25210feebcfc82cf033c1d7fc9efa Mon Sep 17 00:00:00 2001 From: f Date: Sat, 18 May 2024 18:29:34 -0300 Subject: [PATCH 019/161] =?UTF-8?q?feat:=20desde=20el=20modal,=20tener=20i?= =?UTF-8?q?nterfaz=20para=20agregar=20un=20art=C3=ADculo=20nuevo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/posts/attributes/_new_has_many.haml | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 76cf299f..27d682c4 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -5,6 +5,7 @@ name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" modal_id = "modal-#{Nanoid.generate}" + post_form_id = "post-form-#{Nanoid.generate}" %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 @@ -39,7 +40,28 @@ .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 + -# + 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 - .flex-grow-1 + - 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' + +- content_for :post_form do + %form{ id: post_form_id } From 8cafe32ce7650f10624631c427cad6b46163f62e Mon Sep 17 00:00:00 2001 From: f Date: Sat, 18 May 2024 18:48:49 -0300 Subject: [PATCH 020/161] =?UTF-8?q?feat:=20poder=20ocultar=20con=20un=20ev?= =?UTF-8?q?ento=20tambi=C3=A9n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/controllers/modal_controller.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js index 011c1254..20154f79 100644 --- a/app/javascript/controllers/modal_controller.js +++ b/app/javascript/controllers/modal_controller.js @@ -6,13 +6,16 @@ export default class extends Controller { // TODO: Stimulus >1 connect() { this.showEvent = this.show.bind(this); + this.hideEvent = this.hide.bind(this); window.addEventListener("modal:show", this.showEvent); + window.addEventListener("modal:hide", this.hideEvent); } // TODO: Stimulus >1 disconnect() { window.removeEventListener("modal:show", this.showEvent); + window.removeEventListener("modal:hide", this.hideEvent); } /* @@ -41,6 +44,9 @@ export default class extends Controller { hide(event = undefined) { event?.preventDefault(); + const modalId = event?.detail?.id; + + if (modalId && this.element.id !== modalId) return; this.backdropTarget.classList.remove("show"); this.modalTarget.classList.remove("show"); From 4f08a0e5f2e2ac47bbc181392b6569ecf8d98498 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 18 May 2024 18:49:16 -0300 Subject: [PATCH 021/161] =?UTF-8?q?fixup!=20feat:=20desde=20el=20modal,=20?= =?UTF-8?q?tener=20interfaz=20para=20agregar=20un=20art=C3=ADculo=20nuevo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/metadata_related_posts.rb | 12 +++++++----- config/locales/en.yml | 1 + config/locales/es.yml | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/models/metadata_related_posts.rb b/app/models/metadata_related_posts.rb index 42d1381b..2728020e 100644 --- a/app/models/metadata_related_posts.rb +++ b/app/models/metadata_related_posts.rb @@ -26,6 +26,13 @@ class MetadataRelatedPosts < MetadataArray posts.where(uuid: value).map(&:title).map(&:value) end + # Encuentra el filtro + # + # @return [Hash] + def filter + layout.metadata.dig(name, 'filter')&.to_h&.symbolize_keys || {} + end + private # Obtiene todos los posts y opcionalmente los filtra @@ -37,11 +44,6 @@ class MetadataRelatedPosts < MetadataArray "#{post&.title&.value || post&.slug&.value} #{post&.date&.value.strftime('%F')} (#{post.layout.humanized_name})" end - # Encuentra el filtro - def filter - layout.metadata.dig(name, 'filter')&.to_h&.symbolize_keys || {} - end - def sanitize(uuid) super(uuid.map do |u| u.to_s.gsub(/[^a-f0-9\-]/i, '') diff --git a/config/locales/en.yml b/config/locales/en.yml index 6a2eb7d8..ebc7b9c2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -729,6 +729,7 @@ en: filter: "Start typing to filter..." accept: "Accept" cancel: "Cancel" + add: "Add %{layout}" new_predefined_array: edit: "Edit" filter: "Start typing to filter..." diff --git a/config/locales/es.yml b/config/locales/es.yml index 1da0edf9..e56a09e5 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -737,6 +737,7 @@ es: filter: "Empezá a escribir para filtrar..." accept: "Aceptar" cancel: "Cancelar" + add: "Agregar %{layout}" new_predefined_array: edit: "Editar" filter: "Empezá a escribir para filtrar..." From a10da6fa1c5d20ac2c582bdc9f24e7ffaa16e876 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 18 May 2024 18:50:00 -0300 Subject: [PATCH 022/161] feat: al agregar uno nuevo, cerrar el modal y abrir el formulario --- app/controllers/posts_controller.rb | 7 ++++++ app/views/posts/attributes/_new_has_many.haml | 24 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index d956fd2c..93a9983b 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -53,6 +53,13 @@ class PostsController < ApplicationController site.posts(lang: locale).build(layout: params.require(:layout)) end + triggers = {} + params.permit(:show, :hide).each_pair do |key, value| + triggers["modal:#{key}"] = { id: value } + end + + headers['HX-Trigger-After-Swap'] = triggers.to_json if triggers.present? + render layout: false end diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 27d682c4..f349fd6c 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -1,11 +1,21 @@ -# - Genera un listado de checkboxes entre los que se puede elegir para guardar + Genera un listado de checkboxes entre los que se puede elegir 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 = "#{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}" %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 @@ -63,5 +73,15 @@ = 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 } + %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 } + %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 } From 1624c62a1288735fb7028b0b8d848bf3d63b2785 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 20 May 2024 14:11:13 -0300 Subject: [PATCH 023/161] feat: poder modificar la base de los parametros del post --- app/services/post_service.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 84f58dad..4f5427ca 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -13,7 +13,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do post.draft.value = true if site.invitade? usuarie post.assign_attributes(post_params) - params.require(:post).permit(:slug).tap do |p| + params.require(base).permit(:slug).tap do |p| post.slug.value = p[:slug] if p[:slug].present? end @@ -82,7 +82,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do # # { uuid => 2, uuid => 1, uuid => 0 } def reorder - reorder = params.require(:post).permit(reorder: {})&.dig(:reorder)&.transform_values(&:to_i) + reorder = params.require(base).permit(reorder: {})&.dig(:reorder)&.transform_values(&:to_i) posts = site.posts(lang: locale).where(uuid: reorder.keys) files = posts.map do |post| @@ -105,6 +105,13 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do private + # La base donde buscar los parámetros + # + # @return [Symbol] + def base + @base ||= params.permit(:base).try(:[], :base).try(:to_sym) || :post + end + # Una lista de archivos a modificar # # @return [Set] @@ -126,7 +133,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do # Solo permitir cambiar estos atributos de cada articulo def post_params - @post_params ||= params.require(:post).permit(post.params).to_h + @post_params ||= params.require(base).permit(post.params).to_h end # Eliminar metadatos internos From 89f80229199d30347033e5cde6fa58322c4e4b0d Mon Sep 17 00:00:00 2001 From: f Date: Mon, 20 May 2024 14:11:39 -0300 Subject: [PATCH 024/161] =?UTF-8?q?fix:=20recuperar=20de=20errores=20solo?= =?UTF-8?q?=20en=20producci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit en desarrollo los queremos ver! --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 117be995..f217c098 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,7 +2,7 @@ # Forma de ingreso a Sutty class ApplicationController < ActionController::Base - include ExceptionHandler + include ExceptionHandler if Rails.env.production? include Pundit::Authorization protect_from_forgery with: :null_session, prepend: true From 47655c3975560c68cd2c6336592a14292126dc20 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 20 May 2024 14:12:00 -0300 Subject: [PATCH 025/161] feat: poder detectar cuando una peticion viene de htmx --- app/controllers/application_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f217c098..0fc2440f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -117,4 +117,9 @@ class ApplicationController < ActionController::Base session[:usuarie_return_to] = request.fullpath end + + # Detecta si una petición fue hecha por HTMX + def htmx? + request.headers.key? 'HX-Request' + end end From 56003508a79964eb92ea393cd22c935b730b492c Mon Sep 17 00:00:00 2001 From: f Date: Mon, 20 May 2024 14:12:53 -0300 Subject: [PATCH 026/161] feat: poder crear un post relacionado in situ --- app/controllers/posts_controller.rb | 33 ++++++++++++--- app/views/posts/_htmx_form.haml | 41 +++++++++++++++++++ app/views/posts/attributes/_new_has_many.haml | 7 +++- app/views/posts/form.haml | 2 +- app/views/posts/new_has_many_value.haml | 2 + 5 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 app/views/posts/_htmx_form.haml create mode 100644 app/views/posts/new_has_many_value.haml diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 93a9983b..ff3ecb8e 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -44,6 +44,7 @@ class PostsController < ApplicationController # estamos creando. def form uuid = params.permit(:uuid).try(:[], :uuid) + locale @post = if uuid.present? @@ -53,12 +54,7 @@ class PostsController < ApplicationController site.posts(lang: locale).build(layout: params.require(:layout)) end - triggers = {} - params.permit(:show, :hide).each_pair do |key, value| - triggers["modal:#{key}"] = { id: value } - end - - headers['HX-Trigger-After-Swap'] = triggers.to_json if triggers.present? + swap_modals render layout: false end @@ -118,7 +114,23 @@ class PostsController < ApplicationController if @post.persisted? site.touch forget_content + end + # @todo Enviar la creación a otro endpoint para evitar tantas + # condiciones. + if htmx? + if @post.persisted? + swap_modals + + @value = @post.title.value + @uuid = @post.uuid.value + @name = params.require(:name) + + render 'posts/new_has_many_value', layout: false + else + # @todo Mostrar errores + end + elsif @post.persisted? redirect_to site_post_path(@site, @post) else render 'posts/new' @@ -216,4 +228,13 @@ class PostsController < ApplicationController def service_for_direct_upload session[:service_name] = site.name.to_sym end + + # @param triggers [Hash] Otros disparadores + def swap_modals(triggers = {}) + params.permit(:show, :hide).each_pair do |key, value| + triggers["modal:#{key}"] = { id: value } + end + + headers['HX-Trigger'] = triggers.to_json if triggers.present? + end end diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml new file mode 100644 index 00000000..25c45aad --- /dev/null +++ b/app/views/posts/_htmx_form.haml @@ -0,0 +1,41 @@ +-# + El formulario del artículo, con HTMX activado. + + @param :site [Site] + @param :post [Post] + @param :locale [Symbol, String] + @param :dir [Symbol, String] +:ruby + options = { + multipart: true, + class: 'form post ', + 'hx-swap': params.require(:swap), + 'hx-target': "##{params.require(:target)}" + } + + if post.new? + url = options[:'hx-post'] = site_posts_path(site, locale: locale) + options[:class] += 'new' + else + url = options[:'hx-patch'] = site_post_path(site, post.id, locale: locale) + options[:method] = :patch + options[:class] += 'edit' + end + += form_tag url, **options do + -# Parámetros para HTMX + %input{ type: 'hidden', name: 'hide', value: params.require(:show) } + %input{ type: 'hidden', name: 'show', value: params.require(:hide) } + %input{ type: 'hidden', name: 'name', value: params.require(:name) } + %input{ type: 'hidden', name: 'base', value: params.require(:base) } + + -# Botones de guardado + = render 'posts/submit', site: site, post: post + + = hidden_field_tag "#{base}[layout]", post.layout.name + + -# Dibuja cada atributo + = render 'posts/attributes', site: site, post: post, dir: dir, base: base, locale: locale + + -# Botones de guardado + = render 'posts/submit', site: site, post: post diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index f349fd6c..16d68322 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -16,6 +16,7 @@ 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 @@ -45,7 +46,7 @@ %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: "#{id}_body" } + .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 @@ -81,6 +82,10 @@ %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 } %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 diff --git a/app/views/posts/form.haml b/app/views/posts/form.haml index 36d15fde..df4369e1 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/form', site: @site, post: @post += render 'posts/htmx_form', site: @site, post: @post, locale: @locale, dir: t("locales.#{@locale}.dir"), base: params.require(:base) diff --git a/app/views/posts/new_has_many_value.haml b/app/views/posts/new_has_many_value.haml new file mode 100644 index 00000000..b26acc89 --- /dev/null +++ b/app/views/posts/new_has_many_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 From 35527700767df148aef25eaffe5ff7eebddf37cc Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 15:23:24 -0300 Subject: [PATCH 027/161] fix: eliminar nice_partials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a veces hacía que se rendericen los campos dos veces! --- Gemfile | 1 - Gemfile.lock | 3 --- app/views/bootstrap/_modal.haml | 3 +++ 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 500face1..30794e83 100644 --- a/Gemfile +++ b/Gemfile @@ -86,7 +86,6 @@ gem 'after_commit_everywhere', '~> 1.0' gem 'aasm' gem 'que-web' gem 'nanoid' -gem 'nice_partials' # database gem 'hairtrigger' diff --git a/Gemfile.lock b/Gemfile.lock index a0d203d5..db6b892d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -385,8 +385,6 @@ GEM net-protocol net-ssh (7.2.1) netaddr (2.0.6) - nice_partials (0.10.0) - actionview (>= 4.2.6) nio4r (2.7.0-x86_64-linux-musl) nokogiri (1.16.0-x86_64-linux-musl) mini_portile2 (~> 2.8.2) @@ -682,7 +680,6 @@ DEPENDENCIES mobility nanoid net-ssh - nice_partials nokogiri pg pg_search diff --git a/app/views/bootstrap/_modal.haml b/app/views/bootstrap/_modal.haml index f6dafc2a..28014c6a 100644 --- a/app/views/bootstrap/_modal.haml +++ b/app/views/bootstrap/_modal.haml @@ -23,6 +23,9 @@ local_assigns[:hide_actions] << 'click->modal#hide' local_assigns[:modal_content_attributes] ||= {} +-# XXX: Necesario para poder generar todas las demás += yield + .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 From c279d09f4601b1db12fb5896a13183013cb9a652 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 15:24:32 -0300 Subject: [PATCH 028/161] fix: los id no llevan corchetes --- app/views/posts/attributes/_new_array.haml | 2 +- app/views/posts/attributes/_new_has_many.haml | 2 +- app/views/posts/attributes/_new_predefined_array.haml | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 394345a6..692a6898 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -1,7 +1,7 @@ -# Genera un listado de checkboxes entre los que se puede elegir para guardar :ruby - id = "#{base}_#{attribute}" + id = "#{base.gsub(/[\[\]]/, '_')}_#{attribute}".squeeze('_') name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 16d68322..48230759 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -8,7 +8,7 @@ del formulario principal porque no se pueden anidar. :ruby - id = "#{base}_#{attribute}" + id = "#{base.gsub(/[\[\]]/, '_')}_#{attribute}".squeeze('_') name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" modal_id = "modal-#{Nanoid.generate}" diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index 5203c12b..218a2cc2 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -3,7 +3,8 @@ guardar, pero no se pueden agregar nuevos. :ruby - id = "#{base}_#{attribute}" + # @todo Convertir en un helper + id = "#{base.gsub(/[\[\]]/, '_')}_#{attribute}".squeeze('_') name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" From 8e9847c69c46be31f85c9f84f8d117bbdc02b131 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 15:24:49 -0300 Subject: [PATCH 029/161] fix: usar la base en todos lados --- app/services/post_service.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 4f5427ca..628d1b63 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -18,11 +18,11 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do end # Crea los posts anidados - create_nested_posts! post, params[:post] + create_nested_posts! post, params[base] post.save update_related_posts - commit(action: :created, add: files) + commit(action: :created, add: files) if post.valid? update_site_license! @@ -46,14 +46,14 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do def update post.usuaries << usuarie - params[:post][:draft] = true if site.invitade? usuarie + params[base][:draft] = true if site.invitade? usuarie # Eliminar ("mover") el archivo si cambió de ubicación. if post.update(post_params) rm = [] rm << post.path.value_was if post.path.changed? - create_nested_posts! post, params[:post] + create_nested_posts! post, params[base] update_related_posts # Es importante que el artículo se guarde primero y luego los @@ -144,11 +144,11 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do end def locale - params.dig(:post, :lang)&.to_sym || I18n.locale + params.dig(base, :lang)&.to_sym || I18n.locale end def layout - params.dig(:post, :layout) || params[:layout] + params.dig(base, :layout) || params[:layout] end # Actualiza los artículos relacionados según los métodos que los From 7d366a25b5b804cf2d5778b567a3239d4be4b379 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 15:40:00 -0300 Subject: [PATCH 030/161] fix: traducciones --- app/views/posts/attributes/_new_array.haml | 2 +- config/locales/en.yml | 26 +++++++--------------- config/locales/es.yml | 26 +++++++--------------- 3 files changed, 17 insertions(+), 37 deletions(-) diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 692a6898..763da9f8 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -43,7 +43,7 @@ .input-group.w-auto.flex-grow-1.my-0 %input.form-control{ form: form_id, name: 'value', type: 'text', placeholder: t('.add_new') } .input-group-append - = render 'bootstrap/btn', content: t('.add'), form: form_id, type: 'submit', class: 'mb-0 mr-0' + = render 'bootstrap/btn', content: t('.add', layout: ''), form: 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' diff --git a/config/locales/en.yml b/config/locales/en.yml index ebc7b9c2..9f4f7493 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -724,24 +724,6 @@ en: new_array: remove: "Remove" attributes: - new_has_many: - edit: "Edit" - filter: "Start typing to filter..." - accept: "Accept" - cancel: "Cancel" - add: "Add %{layout}" - new_predefined_array: - edit: "Edit" - filter: "Start typing to filter..." - accept: "Accept" - cancel: "Cancel" - new_array: - edit: "Edit" - filter: "Start typing to filter..." - add_new: "Add new option" - add: "Add" - accept: "Accept" - cancel: "Cancel" add: Add lang: label: Language @@ -954,3 +936,11 @@ en: title: "Publications" indexed_posts: deleted: "Deleted indexed post %{path} from %{site} (records: %{records})" + bootstrap: + modal: + accept: "Accept" + add: "Add %{layout}" + add_new: "Add new option" + cancel: "Cancel" + edit: "Edit" + filter: "Start typing to filter..." diff --git a/config/locales/es.yml b/config/locales/es.yml index e56a09e5..b85ac397 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -732,24 +732,6 @@ es: new_array: remove: "Eliminar" attributes: - new_has_many: - edit: "Editar" - filter: "Empezá a escribir para filtrar..." - accept: "Aceptar" - cancel: "Cancelar" - add: "Agregar %{layout}" - new_predefined_array: - edit: "Editar" - filter: "Empezá a escribir para filtrar..." - accept: "Aceptar" - cancel: "Cancelar" - new_array: - edit: "Editar" - filter: "Empezá a escribir para filtrar..." - add_new: "Agregar nueva opción" - add: "Agregar" - accept: "Aceptar" - cancel: "Cancelar" add: Agregar lang: label: Idioma @@ -962,3 +944,11 @@ es: title: "Publicaciones" indexed_posts: deleted: "Eliminado artículo %{path} de %{site} (filas: %{records})" + bootstrap: + modal: + accept: "Aceptar" + add: "Agregar %{layout}" + add_new: "Agregar nueva opción" + cancel: "Cancelar" + edit: "Editar" + filter: "Empezá a escribir para filtrar..." From 01e1692c715f44027ae3a96afe18fd652093ae88 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 16:10:37 -0300 Subject: [PATCH 031/161] fix: parcial faltante --- app/views/posts/new_related_post.haml | 1 + 1 file changed, 1 insertion(+) create mode 100644 app/views/posts/new_related_post.haml diff --git a/app/views/posts/new_related_post.haml b/app/views/posts/new_related_post.haml new file mode 100644 index 00000000..b0623039 --- /dev/null +++ b/app/views/posts/new_related_post.haml @@ -0,0 +1 @@ += render 'posts/new_related_post', post: @indexed_post From 6673660fb6c9033fa006ec93633b222eedec1dca Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 17:15:23 -0300 Subject: [PATCH 032/161] fix: no enviar el formulario de filtro --- app/javascript/controllers/enter_controller.js | 10 ++++++++++ app/views/posts/attributes/_new_array.haml | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 app/javascript/controllers/enter_controller.js diff --git a/app/javascript/controllers/enter_controller.js b/app/javascript/controllers/enter_controller.js new file mode 100644 index 00000000..763dd08c --- /dev/null +++ b/app/javascript/controllers/enter_controller.js @@ -0,0 +1,10 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + /* + * Previene el envío de un formulario al presionar enter + */ + prevent(event) { + if (event.key == "Enter") event.preventDefault(); + } +} diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 763da9f8..08fc6dee 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -5,7 +5,7 @@ name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" -%div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } +%div{ data: { controller: 'modal array enter', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } .form-group = hidden_field_tag name, '' = label_tag id, post_label_t(attribute, post: post) @@ -30,7 +30,7 @@ - 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') } + %input.form-control{ data: { target: 'array.search', action: 'input->array#search keydown->enter#prevent' }, type: 'search', placeholder: t('.filter') } - content_for :"#{id}_body" do .form-group.mb-0{ id: "#{id}_body" } From 1e13fc462102378f88963bf723ad55bad5a7d550 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 17:47:02 -0300 Subject: [PATCH 033/161] =?UTF-8?q?refactor:=20no=20repetir=20c=C3=B3digo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/application_helper.rb | 10 ++++++++++ app/views/posts/attributes/_new_array.haml | 2 +- app/views/posts/attributes/_new_has_many.haml | 2 +- app/views/posts/attributes/_new_predefined_array.haml | 3 +-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fcbd4074..c83b2894 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,6 +2,8 @@ # Helpers module ApplicationHelper + BRACKETS = /[\[\]]/ + # Devuelve el atributo name de un campo anidado en el formato que # esperan los helpers *_field # @@ -19,6 +21,14 @@ module ApplicationHelper [root, name] end + # Obtiene un ID + # + # @param base [String] + # @param attribute [String, Symbol] + def id_for(base, attribute) + "#{base.gsub(BRACKETS, '_')}_#{attribute}".squeeze('_') + end + def plain_field_name_for(*names) root, name = field_name_for(*names) diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 08fc6dee..b4407c0d 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -1,7 +1,7 @@ -# Genera un listado de checkboxes entre los que se puede elegir para guardar :ruby - id = "#{base.gsub(/[\[\]]/, '_')}_#{attribute}".squeeze('_') + id = id_for(base, attribute) name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 48230759..9382e2af 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -8,7 +8,7 @@ del formulario principal porque no se pueden anidar. :ruby - id = "#{base.gsub(/[\[\]]/, '_')}_#{attribute}".squeeze('_') + id = id_for(base, attribute) name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" modal_id = "modal-#{Nanoid.generate}" diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index 218a2cc2..36f1ae2f 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -3,8 +3,7 @@ guardar, pero no se pueden agregar nuevos. :ruby - # @todo Convertir en un helper - id = "#{base.gsub(/[\[\]]/, '_')}_#{attribute}".squeeze('_') + id = id_for(base, attribute) name = "#{base}[#{attribute}][]" form_id = "form-#{Nanoid.generate}" From d5d31a6d947950cfb6239cdd25148897db5e8248 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 17:47:21 -0300 Subject: [PATCH 034/161] =?UTF-8?q?feat:=20bot=C3=B3n=20de=20guardar=20en?= =?UTF-8?q?=20el=20pie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/posts/_htmx_form.haml | 7 +------ app/views/posts/attributes/_new_has_many.haml | 2 ++ config/locales/en.yml | 1 + config/locales/es.yml | 1 + 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index 25c45aad..cd482b35 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -7,6 +7,7 @@ @param :dir [Symbol, String] :ruby options = { + id: base, multipart: true, class: 'form post ', 'hx-swap': params.require(:swap), @@ -29,13 +30,7 @@ %input{ type: 'hidden', name: 'name', value: params.require(:name) } %input{ type: 'hidden', name: 'base', value: params.require(:base) } - -# Botones de guardado - = render 'posts/submit', site: site, post: post - = hidden_field_tag "#{base}[layout]", post.layout.name -# Dibuja cada atributo = render 'posts/attributes', site: site, post: post, dir: dir, base: base, locale: locale - - -# Botones de guardado - = render 'posts/submit', site: site, post: post diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 9382e2af..275d0335 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -90,3 +90,5 @@ = 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: id, content: t('.save'), type: 'submit' diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f4f7493..8fbfd536 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -944,3 +944,4 @@ en: cancel: "Cancel" edit: "Edit" filter: "Start typing to filter..." + save: "Save" diff --git a/config/locales/es.yml b/config/locales/es.yml index b85ac397..52ade5b2 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -952,3 +952,4 @@ es: cancel: "Cancelar" edit: "Editar" filter: "Empezá a escribir para filtrar..." + save: "Guardar" From c1f1571891e203e2c9d3411e41a082a39b83bbf0 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 18:19:59 -0300 Subject: [PATCH 035/161] feat: ignorar algunos campos (cherry picked from commit 9b3db70bf424e24c96e1c724721da9bf072dc27c) --- app/models/post.rb | 24 +++++++++---------- app/views/posts/_attributes.haml | 3 ++- app/views/posts/_htmx_form.haml | 8 ++++++- app/views/posts/attributes/_new_has_many.haml | 2 ++ 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/models/post.rb b/app/models/post.rb index 327df3e2..e6910e18 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -247,18 +247,6 @@ class Post @metadata[:created_at] ||= MetadataCreatedAt.new(document: document, site: site, layout: layout, name: :created_at, type: :created_at, post: self, required: true) end - # Detecta si es un atributo válido o no, a partir de la tabla de la - # plantilla - def attribute?(mid) - included = DEFAULT_ATTRIBUTES.include?(mid) || - PRIVATE_ATTRIBUTES.include?(mid) || - PUBLIC_ATTRIBUTES.include?(mid) - - included = attributes.include? mid if !included && singleton_class.method_defined?(:attributes) - - included - end - # Devuelve los strong params para el layout. # # XXX: Nos gustaría no tener que instanciar Metadata acá, pero depende @@ -430,6 +418,18 @@ class Post @nested_attributes ||= attributes.map { |a| self[a] }.select(&:nested?).map(&:name) end + # Detecta si es un atributo válido o no, a partir de la tabla de la + # plantilla + def attribute?(mid) + included = DEFAULT_ATTRIBUTES.include?(mid) || + PRIVATE_ATTRIBUTES.include?(mid) || + PUBLIC_ATTRIBUTES.include?(mid) + + included = attributes.include? mid if !included && singleton_class.method_defined?(:attributes) + + included + end + private # Levanta un error si al construir el artículo no pasamos un atributo. diff --git a/app/views/posts/_attributes.haml b/app/views/posts/_attributes.haml index ed958d08..d3c205dc 100644 --- a/app/views/posts/_attributes.haml +++ b/app/views/posts/_attributes.haml @@ -4,7 +4,8 @@ @param post [Post] @param site [Site] @param dir [String] -- post.attributes.each do |attribute| + @param except [Array] +- (post.attributes - local_assigns[:except].to_a).each do |attribute| - metadata = post[attribute] - type = metadata.type diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index cd482b35..cfc4071a 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -6,6 +6,12 @@ @param :locale [Symbol, String] @param :dir [Symbol, String] :ruby + except = %i[date] + + if (inverse = params.permit(:inverse).try(:[], :inverse).presence) + except << inverse.to_sym + end + options = { id: base, multipart: true, @@ -33,4 +39,4 @@ = hidden_field_tag "#{base}[layout]", post.layout.name -# Dibuja cada atributo - = render 'posts/attributes', site: site, post: post, dir: dir, base: base, locale: locale + = render 'posts/attributes', site: site, post: post, dir: dir, base: base, locale: locale, except: except diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 275d0335..a9225032 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -86,6 +86,8 @@ %input{ type: 'hidden', name: 'swap', value: 'beforeend' } %input{ type: 'hidden', name: 'base', value: id } %input{ type: 'hidden', name: 'name', value: name } + - 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 From bb8ef69b49b45c5442d225502aed607bed608f36 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 23 May 2024 18:20:20 -0300 Subject: [PATCH 036/161] feat: mostrar los posts relacionados como tarjetas (cherry picked from commit ba9c0ad953602a1c42d3145e426fa6da3cf5b628) --- app/views/bootstrap/_card.haml | 9 +++++++++ app/views/bootstrap/_responsive.haml | 4 ++++ app/views/posts/_new_related_post.haml | 16 ++++++++++++++-- app/views/posts/attributes/_new_has_many.haml | 2 +- 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 app/views/bootstrap/_card.haml create mode 100644 app/views/bootstrap/_responsive.haml diff --git a/app/views/bootstrap/_card.haml b/app/views/bootstrap/_card.haml new file mode 100644 index 00000000..e7e1f108 --- /dev/null +++ b/app/views/bootstrap/_card.haml @@ -0,0 +1,9 @@ +.card{ **local_assigns.except(:image, :description) } + - if local_assigns[:image] + = render 'bootstrap/responsive' do + = image_tag url_for(local_assigns[:image]), alt: local_assigns[:description], class: 'img-fluid' + + .card-body + .card-title= title + + = yield diff --git a/app/views/bootstrap/_responsive.haml b/app/views/bootstrap/_responsive.haml new file mode 100644 index 00000000..1b51de97 --- /dev/null +++ b/app/views/bootstrap/_responsive.haml @@ -0,0 +1,4 @@ +- local_assigns[:ratio] ||= '1by1' + +.embed-responsive{ class: "embed-responsive-#{local_assigns[:ratio]}" } + .embed-responsive-item= yield diff --git a/app/views/posts/_new_related_post.haml b/app/views/posts/_new_related_post.haml index f3cc2a94..8425ad3e 100644 --- a/app/views/posts/_new_related_post.haml +++ b/app/views/posts/_new_related_post.haml @@ -1,2 +1,14 @@ -.col - %p= link_to post.title, post.path +:ruby + image = nil + description = nil + + if post.post.attribute?(:image) && (image = post.post.image.static_file) + description = post.post.image.value['description'] + end + +.col.mb-3 + = render('bootstrap/card', image: image, description: description, title: post.title, class: 'h-100') do + - if post.post.attribute?(:description) + %p.card-text= post.post.description.value + + = link_to 'Editar', post.path, class: 'btn btn-secondary' diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index a9225032..a5d62a84 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -33,7 +33,7 @@ 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-1.row-cols-md-2{ data: { target: 'array.current' } } + .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 From 8c6a70af2b94735f834671b468e5b79cfd14a335 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 24 May 2024 13:10:55 -0300 Subject: [PATCH 037/161] fix: generar los formularios asociados --- app/views/layouts/application.html.haml | 1 - app/views/posts/_form.haml | 3 +++ app/views/posts/_htmx_form.haml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 65d3f777..eaa15eb4 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -25,7 +25,6 @@ = render 'layouts/breadcrumb' = render 'layouts/flash' - = yield(:post_form) = yield - if flash[:js] diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index 92bee939..c04f062f 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -45,3 +45,6 @@ -# Botones de guardado = render 'posts/submit', site: site, post: post + +-# Formularios usados por los modales += yield(:post_form) diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index cfc4071a..378278f7 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -40,3 +40,5 @@ -# Dibuja cada atributo = render 'posts/attributes', site: site, post: post, dir: dir, base: base, locale: locale, except: except + += yield(:post_form) From b4467e4d569d515753b260e5909dbc93988ee385 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 24 May 2024 13:12:56 -0300 Subject: [PATCH 038/161] refactor: agregar definiciones por defecto al post --- app/models/post.rb | 59 ++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/app/models/post.rb b/app/models/post.rb index e6910e18..70773023 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -13,8 +13,19 @@ class Post # Otros atributos que no vienen en los metadatos PRIVATE_ATTRIBUTES = %i[path slug attributes errors].freeze PUBLIC_ATTRIBUTES = %i[lang date uuid created_at].freeze + ALIASED_ATTRIBUTES = %i[locale].freeze ATTR_SUFFIXES = %w[? =].freeze + ATTRIBUTE_DEFINITIONS = { + 'lang' => { 'type' => 'lang', 'required' => true }, + 'date' => { 'type' => 'document_date', 'required' => true }, + 'uuid' => { 'type' => 'uuid', 'required' => true }, + 'created_at' => { 'type' => 'created_at', 'required' => true }, + 'slug' => { 'type' => 'slug', 'required' => true }, + 'path' => { 'type' => 'path', 'required' => true }, + 'locale' => { 'alias' => 'lang' }, + } + class PostError < StandardError; end class UnknownAttributeError < PostError; end @@ -49,10 +60,12 @@ class Post @layout = args[:layout] @site = args[:site] @document = args[:document] - @attributes = layout.attributes + PUBLIC_ATTRIBUTES + @attributes = (layout.attributes + PUBLIC_ATTRIBUTES).uniq @errors = {} @metadata = {} + layout.metadata = ATTRIBUTE_DEFINITIONS.merge(layout.metadata).with_indifferent_access + # Leer el documento si existe # @todo Asignar todos los valores a self[:value] luego de leer document&.read! unless new? @@ -195,6 +208,10 @@ class Post define_singleton_method(name) do template = layout.metadata[name.to_s] + if template.key?('alias') + return public_send(template['alias'].to_sym) + end + @metadata[name] ||= MetadataFactory.build(document: document, post: self, @@ -210,43 +227,6 @@ class Post public_send name end - # TODO: Mover a method_missing - def slug - @metadata[:slug] ||= MetadataSlug.new(document: document, site: site, layout: layout, name: :slug, type: :slug, - post: self, required: true) - end - - # TODO: Mover a method_missing - def date - @metadata[:date] ||= MetadataDocumentDate.new(document: document, site: site, layout: layout, name: :date, - type: :document_date, post: self, required: true) - end - - # TODO: Mover a method_missing - def path - @metadata[:path] ||= MetadataPath.new(document: document, site: site, layout: layout, name: :path, type: :path, - post: self, required: true) - end - - # TODO: Mover a method_missing - def lang - @metadata[:lang] ||= MetadataLang.new(document: document, site: site, layout: layout, name: :lang, type: :lang, - post: self, required: true) - end - - alias locale lang - - # TODO: Mover a method_missing - def uuid - @metadata[:uuid] ||= MetadataUuid.new(document: document, site: site, layout: layout, name: :uuid, type: :uuid, - post: self, required: true) - end - - # La fecha de creación inmodificable del post - def created_at - @metadata[:created_at] ||= MetadataCreatedAt.new(document: document, site: site, layout: layout, name: :created_at, type: :created_at, post: self, required: true) - end - # Devuelve los strong params para el layout. # # XXX: Nos gustaría no tener que instanciar Metadata acá, pero depende @@ -423,7 +403,8 @@ class Post def attribute?(mid) included = DEFAULT_ATTRIBUTES.include?(mid) || PRIVATE_ATTRIBUTES.include?(mid) || - PUBLIC_ATTRIBUTES.include?(mid) + PUBLIC_ATTRIBUTES.include?(mid) || + ALIASED_ATTRIBUTES.include?(mid) included = attributes.include? mid if !included && singleton_class.method_defined?(:attributes) From 780d26f79aa4fb5b87f8ccbcc58f8aea5ccc39e5 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 24 May 2024 13:13:58 -0300 Subject: [PATCH 039/161] feat: title es un atributo obligatorio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit si no existe en el esquema, generar un título en base a sus valores --- app/models/metadata_array.rb | 6 +++++- app/models/metadata_belongs_to.rb | 4 ++++ app/models/metadata_predefined_value.rb | 4 ++++ app/models/metadata_related_posts.rb | 4 ++++ app/models/metadata_slug.rb | 2 +- app/models/metadata_string.rb | 4 ++++ app/models/metadata_template.rb | 5 +++++ app/models/metadata_text.rb | 6 +++++- app/models/metadata_title.rb | 18 ++++++++++++++++++ app/models/post.rb | 3 ++- app/views/posts/attribute_ro/_title.haml | 3 +++ app/views/posts/attributes/_title.haml | 1 + 12 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 app/models/metadata_title.rb create mode 100644 app/views/posts/attribute_ro/_title.haml create mode 100644 app/views/posts/attributes/_title.haml diff --git a/app/models/metadata_array.rb b/app/models/metadata_array.rb index 368aa546..a8a6e555 100644 --- a/app/models/metadata_array.rb +++ b/app/models/metadata_array.rb @@ -19,8 +19,12 @@ class MetadataArray < MetadataTemplate true && !private? end + def titleize? + true + end + def to_s - value.join(', ') + value.select(&:present?).join(', ') end # Obtiene el valor desde el documento, convirtiéndolo a Array si no lo diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb index be1fa670..7dec4f94 100644 --- a/app/models/metadata_belongs_to.rb +++ b/app/models/metadata_belongs_to.rb @@ -13,6 +13,10 @@ class MetadataBelongsTo < MetadataRelatedPosts '' end + def to_s + belongs_to.try(:title).try(:value).to_s + end + # Obtiene el valor desde el documento. # # @return [String] diff --git a/app/models/metadata_predefined_value.rb b/app/models/metadata_predefined_value.rb index 9cf36382..dbdb1e48 100644 --- a/app/models/metadata_predefined_value.rb +++ b/app/models/metadata_predefined_value.rb @@ -10,6 +10,10 @@ class MetadataPredefinedValue < MetadataString @values ||= layout.dig(:metadata, name, 'values', I18n.locale.to_s)&.invert || {} end + def to_s + values.invert[value].to_s + end + private # Solo permite almacenar los valores predefinidos. diff --git a/app/models/metadata_related_posts.rb b/app/models/metadata_related_posts.rb index 2728020e..9e84c446 100644 --- a/app/models/metadata_related_posts.rb +++ b/app/models/metadata_related_posts.rb @@ -22,6 +22,10 @@ class MetadataRelatedPosts < MetadataArray false end + def titleize? + false + end + def indexable_values posts.where(uuid: value).map(&:title).map(&:value) end diff --git a/app/models/metadata_slug.rb b/app/models/metadata_slug.rb index b0fe8cec..417cfa0b 100644 --- a/app/models/metadata_slug.rb +++ b/app/models/metadata_slug.rb @@ -25,7 +25,7 @@ require 'jekyll/utils' class MetadataSlug < MetadataTemplate # Trae el slug desde el título si existe o una string al azar def default_value - title ? Jekyll::Utils.slugify(title, mode: site.slugify_mode) : SecureRandom.uuid + Jekyll::Utils.slugify(title || SecureRandom.uuid, mode: site.slugify_mode) end def value diff --git a/app/models/metadata_string.rb b/app/models/metadata_string.rb index c1d888b1..cb3ad264 100644 --- a/app/models/metadata_string.rb +++ b/app/models/metadata_string.rb @@ -11,6 +11,10 @@ class MetadataString < MetadataTemplate true && !private? end + def titleize? + true + end + private # No se permite HTML en las strings diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index c762220e..29391ac6 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -16,6 +16,11 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, false end + # El valor puede ser parte de un título auto-generado + def titleize? + false + end + def inspect "#<#{self.class} site=#{site.name.inspect} post=#{post.id.inspect} value=#{value.inspect}>" end diff --git a/app/models/metadata_text.rb b/app/models/metadata_text.rb index 103bcd0a..3ac336bb 100644 --- a/app/models/metadata_text.rb +++ b/app/models/metadata_text.rb @@ -1,4 +1,8 @@ # frozen_string_literal: true # Un campo de texto largo -class MetadataText < MetadataString; end +class MetadataText < MetadataString + def titleize? + false + end +end diff --git a/app/models/metadata_title.rb b/app/models/metadata_title.rb new file mode 100644 index 00000000..c913878b --- /dev/null +++ b/app/models/metadata_title.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# El título es obligatorio para todos los Post, si el esquema no lo +# incluye, tenemos que poder generar un valor legible por humanes. +class MetadataTitle < MetadataString + # Obtener todos los valores de texto del artículo y generar un título + # en base a eso. + # + # @return [String] + def default_value + @default_value ||= + post.attributes.select do |attr| + post[attr].titleize? + end.map do |attr| + post[attr].to_s + end.compact.join(' ').strip.squeeze(' ') + end +end diff --git a/app/models/post.rb b/app/models/post.rb index 70773023..64669524 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -12,11 +12,12 @@ class Post DEFAULT_ATTRIBUTES = %i[site document layout].freeze # Otros atributos que no vienen en los metadatos PRIVATE_ATTRIBUTES = %i[path slug attributes errors].freeze - PUBLIC_ATTRIBUTES = %i[lang date uuid created_at].freeze + PUBLIC_ATTRIBUTES = %i[title lang date uuid created_at].freeze ALIASED_ATTRIBUTES = %i[locale].freeze ATTR_SUFFIXES = %w[? =].freeze ATTRIBUTE_DEFINITIONS = { + 'title' => { 'type' => 'title', 'required' => true }, 'lang' => { 'type' => 'lang', 'required' => true }, 'date' => { 'type' => 'document_date', 'required' => true }, 'uuid' => { 'type' => 'uuid', 'required' => true }, diff --git a/app/views/posts/attribute_ro/_title.haml b/app/views/posts/attribute_ro/_title.haml new file mode 100644 index 00000000..67642e2c --- /dev/null +++ b/app/views/posts/attribute_ro/_title.haml @@ -0,0 +1,3 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td{ dir: dir, lang: locale }= metadata.value diff --git a/app/views/posts/attributes/_title.haml b/app/views/posts/attributes/_title.haml new file mode 100644 index 00000000..d94afdbb --- /dev/null +++ b/app/views/posts/attributes/_title.haml @@ -0,0 +1 @@ +-# From 9c8834faccba6722e91ef8b6c69ba098401c007c Mon Sep 17 00:00:00 2001 From: f Date: Fri, 24 May 2024 13:22:28 -0300 Subject: [PATCH 040/161] chore: rubocop --- app/helpers/application_helper.rb | 2 +- app/models/metadata_belongs_to.rb | 2 +- app/models/metadata_related_posts.rb | 4 ++-- app/models/post.rb | 13 +++++-------- app/services/post_service.rb | 22 +++++++++++----------- config/initializers/core_extensions.rb | 3 ++- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c83b2894..ba92f626 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,7 +2,7 @@ # Helpers module ApplicationHelper - BRACKETS = /[\[\]]/ + BRACKETS = /[\[\]]/.freeze # Devuelve el atributo name de un campo anidado en el formato que # esperan los helpers *_field diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb index 7dec4f94..16b0e2ed 100644 --- a/app/models/metadata_belongs_to.rb +++ b/app/models/metadata_belongs_to.rb @@ -101,6 +101,6 @@ class MetadataBelongsTo < MetadataRelatedPosts end def sanitize(uuid) - uuid.to_s.gsub(/[^a-f0-9\-]/i, '') + uuid.to_s.gsub(/[^a-f0-9-]/i, '') end end diff --git a/app/models/metadata_related_posts.rb b/app/models/metadata_related_posts.rb index 9e84c446..6ca09f7d 100644 --- a/app/models/metadata_related_posts.rb +++ b/app/models/metadata_related_posts.rb @@ -45,12 +45,12 @@ class MetadataRelatedPosts < MetadataArray end def title(post) - "#{post&.title&.value || post&.slug&.value} #{post&.date&.value.strftime('%F')} (#{post.layout.humanized_name})" + "#{post&.title&.value || post&.slug&.value} #{post&.date&.value&.strftime('%F')} (#{post.layout.humanized_name})" end def sanitize(uuid) super(uuid.map do |u| - u.to_s.gsub(/[^a-f0-9\-]/i, '') + u.to_s.gsub(/[^a-f0-9-]/i, '') end) end end diff --git a/app/models/post.rb b/app/models/post.rb index 64669524..a1758464 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -24,8 +24,8 @@ class Post 'created_at' => { 'type' => 'created_at', 'required' => true }, 'slug' => { 'type' => 'slug', 'required' => true }, 'path' => { 'type' => 'path', 'required' => true }, - 'locale' => { 'alias' => 'lang' }, - } + 'locale' => { 'alias' => 'lang' } + }.freeze class PostError < StandardError; end class UnknownAttributeError < PostError; end @@ -141,6 +141,7 @@ class Post src = element.attributes['src'] next unless src&.value&.start_with? 'public/' + file = MetadataFile.new(site: site, post: self, document: document, layout: layout) file.value['path'] = src.value @@ -202,16 +203,12 @@ class Post def method_missing(name, *_args) # Limpiar el nombre del atributo, para que todos los ayudantes # reciban el método en limpio - unless attribute? name - raise NoMethodError, I18n.t('exceptions.post.no_method', method: name) - end + raise NoMethodError, I18n.t('exceptions.post.no_method', method: name) unless attribute? name define_singleton_method(name) do template = layout.metadata[name.to_s] - if template.key?('alias') - return public_send(template['alias'].to_sym) - end + return public_send(template['alias'].to_sym) if template.key?('alias') @metadata[name] ||= MetadataFactory.build(document: document, diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 628d1b63..4d32cba9 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -180,24 +180,24 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do # Si les usuaries modifican o crean una licencia, considerarla # personalizada en el panel. def update_site_license! - if site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom? - site.update licencia: Licencia.find_by_icons('custom') - end + return unless site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom? + + site.update licencia: Licencia.find_by_icons('custom') end # Encuentra todos los posts anidados y los crea o modifica def create_nested_posts!(post, params) post.nested_attributes.each do |nested_attribute| - nested_metadata = post[nested_attribute] - # @todo find_or_initialize - nested_post = nested_metadata.has_one || site.posts(lang: post.lang.value).build(layout: nested_metadata.nested) - nested_params = params.require(nested_attribute).permit(nested_post.params).to_hash + nested_metadata = post[nested_attribute] + # @todo find_or_initialize + nested_post = nested_metadata.has_one || site.posts(lang: post.lang.value).build(layout: nested_metadata.nested) + nested_params = params.require(nested_attribute).permit(nested_post.params).to_hash - # Completa la relación 1:1 - nested_params[nested_metadata.inverse.to_s] = post.uuid.value - post[nested_attribute].value = nested_post.uuid.value + # Completa la relación 1:1 + nested_params[nested_metadata.inverse.to_s] = post.uuid.value + post[nested_attribute].value = nested_post.uuid.value - files << nested_post.path.absolute if nested_post.update(nested_params) + files << nested_post.path.absolute if nested_post.update(nested_params) end end end diff --git a/config/initializers/core_extensions.rb b/config/initializers/core_extensions.rb index 6861da45..024a26ab 100644 --- a/config/initializers/core_extensions.rb +++ b/config/initializers/core_extensions.rb @@ -126,7 +126,8 @@ module Jekyll unless spec I18n.with_locale(locale) do - raise Jekyll::Errors::InvalidThemeName, I18n.t('activerecord.errors.models.site.attributes.design_id.missing_gem', theme: name) + raise Jekyll::Errors::InvalidThemeName, + I18n.t('activerecord.errors.models.site.attributes.design_id.missing_gem', theme: name) rescue Jekyll::Errors::InvalidThemeName => e ExceptionNotifier.notify_exception(e, data: { theme: name, site: File.basename(site.source) }) raise From a84ba934cb02657f6c49f9bc84ad60da659d4837 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 24 May 2024 13:23:30 -0300 Subject: [PATCH 041/161] chore: haml-lint --- app/views/posts/_attributes.haml | 8 ++++---- app/views/posts/_attributes_nested.haml | 7 ++++--- app/views/posts/_form.haml | 3 +-- app/views/posts/_htmx_form.haml | 2 +- app/views/posts/attributes/_title.haml | 1 - app/views/posts/show.haml | 4 ++-- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/views/posts/_attributes.haml b/app/views/posts/_attributes.haml index d3c205dc..03887866 100644 --- a/app/views/posts/_attributes.haml +++ b/app/views/posts/_attributes.haml @@ -11,7 +11,7 @@ - cache [metadata, I18n.locale] do = render("posts/attributes/#{type}", - base: base, post: post, attribute: attribute, - metadata: metadata, site: site, - dir: dir, locale: locale, - autofocus: (post.attributes.first == attribute)) + base: base, post: post, attribute: attribute, + metadata: metadata, site: site, + dir: dir, locale: locale, + autofocus: (post.attributes.first == attribute)) diff --git a/app/views/posts/_attributes_nested.haml b/app/views/posts/_attributes_nested.haml index 83bcc51c..5036b9c7 100644 --- a/app/views/posts/_attributes_nested.haml +++ b/app/views/posts/_attributes_nested.haml @@ -9,10 +9,11 @@ - next if attribute == :date - next if attribute == :draft - next if attribute == inverse + - metadata = post[attribute] - cache [post, metadata, I18n.locale] do = render "posts/attributes/#{metadata.type}", - base: base, post: post, attribute: attribute, - metadata: metadata, site: site, - dir: dir, locale: locale, autofocus: false + base: base, post: post, attribute: attribute, + metadata: metadata, site: site, + dir: dir, locale: locale, autofocus: false diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index c04f062f..8c006274 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -33,8 +33,7 @@ - dir = t("locales.#{@locale}.dir") -# Comienza el formulario -= form_tag url, method: method, class: 'form post ' + extra_class, multipart: true do - += form_tag url, method: method, class: "form post #{extra_class}", multipart: true do -# Botones de guardado = render 'posts/submit', site: site, post: post diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index 378278f7..a16e68dd 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -13,7 +13,7 @@ end options = { - id: base, + id: base, multipart: true, class: 'form post ', 'hx-swap': params.require(:swap), diff --git a/app/views/posts/attributes/_title.haml b/app/views/posts/attributes/_title.haml index d94afdbb..e69de29b 100644 --- a/app/views/posts/attributes/_title.haml +++ b/app/views/posts/attributes/_title.haml @@ -1 +0,0 @@ --# diff --git a/app/views/posts/show.haml b/app/views/posts/show.haml index f44e11ed..12db05a4 100644 --- a/app/views/posts/show.haml +++ b/app/views/posts/show.haml @@ -4,7 +4,7 @@ .col-md-8 %article.content.table-responsive-md = link_to t('posts.edit_post'), - edit_site_post_path(@site, @post.id), - class: 'btn btn-secondary btn-block' + edit_site_post_path(@site, @post.id), + class: 'btn btn-secondary btn-block' = render 'table', dir: dir, site: @site, locale: @locale, post: @post, title: t('.front_matter') From 9a7de79e4992420e3050311243433ea41abce2d2 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 24 May 2024 13:43:08 -0300 Subject: [PATCH 042/161] fix: guardar geo como hash plano --- app/models/metadata_geo.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/metadata_geo.rb b/app/models/metadata_geo.rb index ba11f337..d475edf9 100644 --- a/app/models/metadata_geo.rb +++ b/app/models/metadata_geo.rb @@ -14,7 +14,7 @@ class MetadataGeo < MetadataTemplate return true unless changed? return true if empty? - self[:value] = value.transform_values(&:to_f) + self[:value] = value.transform_values(&:to_f).to_h self[:value] = encrypt(value) if private? true From 32a2172e5bb4f53f940eb2a8fbdc40bbd21617bd Mon Sep 17 00:00:00 2001 From: f Date: Fri, 24 May 2024 13:43:30 -0300 Subject: [PATCH 043/161] fix: evitar auto-referenciarse --- app/models/metadata_title.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/metadata_title.rb b/app/models/metadata_title.rb index c913878b..ef097473 100644 --- a/app/models/metadata_title.rb +++ b/app/models/metadata_title.rb @@ -3,6 +3,10 @@ # El título es obligatorio para todos los Post, si el esquema no lo # incluye, tenemos que poder generar un valor legible por humanes. class MetadataTitle < MetadataString + def titleize? + false + end + # Obtener todos los valores de texto del artículo y generar un título # en base a eso. # From d347e177e4af7fb8e716b78ed03b868ab92a485b Mon Sep 17 00:00:00 2001 From: f Date: Fri, 24 May 2024 13:43:43 -0300 Subject: [PATCH 044/161] fix: no memoizar, para poder obtener el valor nuevo siempre --- app/models/metadata_title.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/models/metadata_title.rb b/app/models/metadata_title.rb index ef097473..729a71fd 100644 --- a/app/models/metadata_title.rb +++ b/app/models/metadata_title.rb @@ -12,11 +12,10 @@ class MetadataTitle < MetadataString # # @return [String] def default_value - @default_value ||= - post.attributes.select do |attr| - post[attr].titleize? - end.map do |attr| - post[attr].to_s - end.compact.join(' ').strip.squeeze(' ') + post.attributes.select do |attr| + post[attr].titleize? + end.map do |attr| + post[attr].to_s + end.compact.join(' ').strip.squeeze(' ') end end From f0769427b7821f55b35aff4bb3e908f7e0a3d176 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 15:12:03 -0300 Subject: [PATCH 045/161] fix: usar un nombre unico para el formulario en el modal --- app/views/posts/_htmx_form.haml | 2 +- app/views/posts/attributes/_new_has_many.haml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index a16e68dd..acb5585e 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -13,7 +13,7 @@ end options = { - id: base, + id: params.require(:form), multipart: true, class: 'form post ', 'hx-swap': params.require(:swap), diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index a5d62a84..54eb8d40 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -86,6 +86,7 @@ %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 } - if metadata.inverse? %input{ type: 'hidden', name: 'inverse', value: metadata.inverse } %div{ id: post_modal_id, data: { controller: 'modal' } } @@ -93,4 +94,4 @@ - content_for :"#{post_id}_body" do %div{ id: post_form_loaded_id } - content_for :"#{post_id}_footer" do - = render 'bootstrap/btn', form: id, content: t('.save'), type: 'submit' + = render 'bootstrap/btn', form: form_id, content: t('.save'), type: 'submit' From e2514a7f10aafa54746d626b469019c66ade5e37 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 15:13:11 -0300 Subject: [PATCH 046/161] feat: mostrar errores en los formularios --- app/controllers/posts_controller.rb | 5 ++++- app/views/posts/_htmx_form.haml | 31 +++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index ff3ecb8e..28295793 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -128,7 +128,10 @@ class PostsController < ApplicationController render 'posts/new_has_many_value', layout: false else - # @todo Mostrar errores + headers['HX-Retarget'] = "##{params.require(:form)}" + headers['HX-Reswap'] = 'outerHTML' + + 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) diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index acb5585e..a288aa55 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -30,11 +30,38 @@ 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 + -# Parámetros para HTMX - %input{ type: 'hidden', name: 'hide', value: params.require(:show) } - %input{ type: 'hidden', name: 'show', value: params.require(:hide) } + %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: 'name', value: params.require(:name) } %input{ type: 'hidden', name: 'base', value: params.require(: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: 'target', value: params.require(:target) } + %input{ type: 'hidden', name: 'swap', value: params.require(:swap) } + - if params[:inverse].present? + %input{ type: 'hidden', name: 'inverse', value: params.require(:inverse) } = hidden_field_tag "#{base}[layout]", post.layout.name From 105b2a1f1c583d8ba449eb2765a66f3d5ce9e7c8 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 15:13:40 -0300 Subject: [PATCH 047/161] fix: no fallar si el campo inverso es anidado --- app/services/post_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 4d32cba9..93daaa45 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -189,6 +189,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do def create_nested_posts!(post, params) post.nested_attributes.each do |nested_attribute| nested_metadata = post[nested_attribute] + next unless params[nested_metadata].present? # @todo find_or_initialize nested_post = nested_metadata.has_one || site.posts(lang: post.lang.value).build(layout: nested_metadata.nested) nested_params = params.require(nested_attribute).permit(nested_post.params).to_hash From c150acbd6fca89782310890b1cf9993cbde8ed38 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 15:15:55 -0300 Subject: [PATCH 048/161] 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' From d8a7816c65d157034975dc0449787de8b22d54e5 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 15:16:24 -0300 Subject: [PATCH 049/161] fix: cancelar el formulario --- app/views/posts/attributes/_new_has_many.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 724f8247..e35ef453 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -96,3 +96,5 @@ %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' From 865cc8fc18fd2e90e8e8190a83bfe2ccf632086c Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 15:28:17 -0300 Subject: [PATCH 050/161] =?UTF-8?q?fix:=20enviar=20los=20atributos=20ignor?= =?UTF-8?q?ados=20como=20valores=20vac=C3=ADos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/posts/_htmx_form.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index 03e82e32..8cc8c938 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -69,4 +69,8 @@ -# Dibuja cada atributo = render 'posts/attributes', site: site, post: post, dir: dir, base: base, locale: locale, except: except + -# Enviamos valores vacíos para los atributos ocultos + - except.each do |attr| + %input{ type: 'hidden', name: "#{base}[#{attr}]", value: "" } + = yield(:post_form) From 23a9bbf8cad9890b25a424add36848d3a5eafa33 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 16:38:56 -0300 Subject: [PATCH 051/161] fix: alinear los botones --- app/views/posts/attributes/_new_array.haml | 30 +++++++++---------- .../posts/attributes/_new_belongs_to.haml | 29 +++++++++--------- app/views/posts/attributes/_new_has_many.haml | 30 +++++++++---------- .../attributes/_new_predefined_array.haml | 29 +++++++++--------- 4 files changed, 60 insertions(+), 58 deletions(-) diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index b4407c0d..8369f335 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -8,23 +8,23 @@ %div{ data: { controller: 'modal array enter', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } .form-group = hidden_field_tag name, '' - = label_tag id, post_label_t(attribute, post: post) - -# Mostramos la lista de valores actuales. + .d-flex.align-items-center.justify-content-between + = label_tag id, post_label_t(attribute, post: post) + = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + .row.row-cols-1.row-cols-md-2{ data: { target: 'array.current' } } + -# 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. + 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-1.row-cols-md-2{ data: { target: 'array.current' } } - - metadata.value.sort_by(&:remove_diacritics).each do |value| - = render 'posts/new_array_value', value: value - - = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + Para poder cancelar, mantenemos el estado original y desactivamos + o activamos los ítemes según estén incluidos en esa lista o no. + - metadata.value.sort_by(&:remove_diacritics).each do |value| + = render 'posts/new_array_value', value: value = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do - content_for :"#{id}_header" do diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index d30d22dd..381b0e7b 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -21,23 +21,24 @@ %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. + .d-flex.align-items-center.justify-content-between + = label_tag id, post_label_t(attribute, post: post) + = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' - 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. + -# Mostramos la lista de valores actuales. - 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 + 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. - = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + 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/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do - content_for :"#{id}_header" do diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index e35ef453..3aef2b86 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -21,23 +21,23 @@ %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. + .d-flex.align-items-center.justify-content-between + = label_tag id, post_label_t(attribute, post: post) + = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + -# 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. + 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' + 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/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do - content_for :"#{id}_header" do diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index 36f1ae2f..09d8f7ce 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -10,23 +10,24 @@ %div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } .form-group = hidden_field_tag name, '' - = label_tag id, post_label_t(attribute, post: post) - -# Mostramos la lista de valores actuales. + .d-flex.align-items-center.justify-content-between + = label_tag id, post_label_t(attribute, post: post) + = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' - 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. + -# Mostramos la lista de valores actuales. - 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-1.row-cols-md-2{ data: { target: 'array.current' } } - - metadata.values.slice(*metadata.value).each_key do |value| - = render 'posts/new_array_value', value: value + 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. - = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + 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-1.row-cols-md-2{ data: { target: 'array.current' } } + - metadata.values.slice(*metadata.value).each_key do |value| + = render 'posts/new_array_value', value: value = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do - content_for :"#{id}_header" do From 0d147c87bb6e67d653622459499aec8c11905358 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 16:40:09 -0300 Subject: [PATCH 052/161] =?UTF-8?q?feat:=20mostrar=20que=20est=C3=A1=20car?= =?UTF-8?q?gando=20algo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/application.scss | 30 +++++++++++++++++++ .../controllers/array_controller.js | 15 +++++++--- app/views/posts/attributes/_new_array.haml | 6 +++- .../posts/attributes/_new_belongs_to.haml | 6 +++- app/views/posts/attributes/_new_has_many.haml | 6 +++- .../attributes/_new_predefined_array.haml | 6 +++- 6 files changed, 61 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 7d9c8c9d..7a082ae8 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -622,3 +622,33 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1); } } } + +// https://getbootstrap.com/docs/5.1/components/placeholders/ +.placeholder { + display: inline-block; + min-height: $spacer; + cursor: wait; + vertical-align: middle; + opacity: .5; + background-color: $grey; + animation: placeholder-glow 2s ease-in-out infinite; +} + +.placeholder-glow { + .placeholder { + -webkit-animation: placeholder-glow 2s ease-in-out infinite; + animation: placeholder-glow 2s ease-in-out infinite; + } + + @-webkit-keyframes placeholder-glow { + 50% { + opacity: .2; + } + } + + @keyframes placeholder-glow { + 50% { + opacity: .2; + } + } +} diff --git a/app/javascript/controllers/array_controller.js b/app/javascript/controllers/array_controller.js index 66cc2290..19d00ccd 100644 --- a/app/javascript/controllers/array_controller.js +++ b/app/javascript/controllers/array_controller.js @@ -1,7 +1,7 @@ import { Controller } from "stimulus"; export default class extends Controller { - static targets = ["item", "search", "current"]; + static targets = ["item", "search", "current", "placeholder"]; connect() { // TODO: Stimulus >1 @@ -104,12 +104,19 @@ export default class extends Controller { this.originalValue.push(itemTarget.dataset.value); this.newArrayValueURL.searchParams.set("value", itemTarget.dataset.value); + const placeholder = this.placeholderTarget.content.firstElementChild.cloneNode(true); + + this.currentTarget.appendChild(placeholder); + // TODO: Renderizarlas todas juntas fetch(this.newArrayValueURL) .then((response) => response.text()) - .then((body) => - this.currentTarget.insertAdjacentHTML("beforeend", body), - ); + .then((body) => { + const template = document.createElement("template"); + template.innerHTML = body; + + placeholder.replaceWith(template.content.firstElementChild); + }); } // TODO: Stimulus >1 diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 8369f335..71c409a2 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -6,12 +6,15 @@ form_id = "form-#{Nanoid.generate}" %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' } } + .col.mb-3{ 'aria-hidden': 'true' } + %span.placeholder.w-100 + .form-group = hidden_field_tag name, '' .d-flex.align-items-center.justify-content-between = label_tag id, post_label_t(attribute, post: post) = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' - .row.row-cols-1.row-cols-md-2{ data: { target: 'array.current' } } -# Mostramos la lista de valores actuales. Al aceptar el modal, se vacía el listado y se completa en base a @@ -23,6 +26,7 @@ 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-1.row-cols-md-2.no-gutters.placeholder-glow{ data: { target: 'array.current' } } - metadata.value.sort_by(&:remove_diacritics).each do |value| = render 'posts/new_array_value', value: value diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index 381b0e7b..afbc8bdd 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -19,6 +19,10 @@ 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) } } + %template{ data: { target: 'array.placeholder' } } + .col.p-3{ 'aria-hidden': 'true' } + %span.placeholder.w-100 + .form-group = hidden_field_tag name, '' .d-flex.align-items-center.justify-content-between @@ -36,7 +40,7 @@ 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' } } + .row.row-cols-3.row-cols-md-4.no-gutters.placeholder-glow{ data: { target: 'array.current' } } - metadata.values.slice(*metadata.value).each do |value| = render 'posts/new_array_value', value: value diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 3aef2b86..143ad2d1 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -19,6 +19,10 @@ 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) } } + %template{ data: { target: 'array.placeholder' } } + .col.p-3{ 'aria-hidden': 'true' } + %span.placeholder.w-100 + .form-group = hidden_field_tag name, '' .d-flex.align-items-center.justify-content-between @@ -35,7 +39,7 @@ 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' } } + .row.row-cols-3.row-cols-md-4.no-gutters.placeholder-glow{ data: { target: 'array.current' } } - metadata.values.slice(*metadata.value).each do |value| = render 'posts/new_array_value', value: value diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index 09d8f7ce..ec251134 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -8,6 +8,10 @@ form_id = "form-#{Nanoid.generate}" %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' } } + .col.mb-3{ 'aria-hidden': 'true' } + %span.placeholder.w-100 + .form-group = hidden_field_tag name, '' .d-flex.align-items-center.justify-content-between @@ -25,7 +29,7 @@ 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-1.row-cols-md-2{ data: { target: 'array.current' } } + .row.row-cols-1.row-cols-md-2.no-gutters.placeholder-glow{ data: { target: 'array.current' } } - metadata.values.slice(*metadata.value).each_key do |value| = render 'posts/new_array_value', value: value From 99310c56bbef6fe395e2a0ad39de556d21e47ce7 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 27 May 2024 16:44:23 -0300 Subject: [PATCH 053/161] fix: el valor es obligatorio --- app/views/posts/attributes/_new_array.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 71c409a2..38ba4eb9 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -45,7 +45,7 @@ - content_for :"#{id}_footer" do .input-group.w-auto.flex-grow-1.my-0 - %input.form-control{ form: form_id, name: 'value', type: 'text', placeholder: t('.add_new') } + %input.form-control{ form: form_id, name: 'value', type: 'text', placeholder: t('.add_new'), required: true } .input-group-append = render 'bootstrap/btn', content: t('.add', layout: ''), form: 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' From 9c8ec08e7371776ccce05d5548827386e3743ad3 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 13:15:45 -0300 Subject: [PATCH 054/161] fix: brakeman --- app/controllers/posts_controller.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index bf030ee7..f38ccff3 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/#{params.require(:attribute)}_value", layout: false + render render_path_from_attribute, layout: false else headers['HX-Retarget'] = "##{params.require(:form)}" headers['HX-Reswap'] = 'outerHTML' @@ -240,4 +240,13 @@ class PostsController < ApplicationController headers['HX-Trigger'] = triggers.to_json if triggers.present? end + + # @return [String] + def render_path_from_attribute + case params.require(:attribute) + when 'new_has_many' then 'posts/new_has_many_value' + when 'new_belongs_to' then 'posts/new_belongs_to_value' + else 'nothing' + end + end end From 40047c722c17ba3a48408d70d45e0d4c04feebfd Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 16:27:39 -0300 Subject: [PATCH 055/161] =?UTF-8?q?feat:=20relaci=C3=B3n=20de=20muchos=20a?= =?UTF-8?q?=20muchos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/posts_controller.rb | 1 + .../metadata_new_has_and_belongs_to_many.rb | 4 + .../_new_has_and_belongs_to_many.haml | 6 + .../_new_has_and_belongs_to_many.haml | 104 ++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 app/models/metadata_new_has_and_belongs_to_many.rb create mode 100644 app/views/posts/attribute_ro/_new_has_and_belongs_to_many.haml create mode 100644 app/views/posts/attributes/_new_has_and_belongs_to_many.haml diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index f38ccff3..6d199cd7 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -246,6 +246,7 @@ class PostsController < ApplicationController case params.require(: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' else 'nothing' end end diff --git a/app/models/metadata_new_has_and_belongs_to_many.rb b/app/models/metadata_new_has_and_belongs_to_many.rb new file mode 100644 index 00000000..44102ec0 --- /dev/null +++ b/app/models/metadata_new_has_and_belongs_to_many.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Nueva interfaz para relaciones muchos a muchos +class MetadataNewHasAndBelongsToMany < MetadataHasAndBelongsToMany; end diff --git a/app/views/posts/attribute_ro/_new_has_and_belongs_to_many.haml b/app/views/posts/attribute_ro/_new_has_and_belongs_to_many.haml new file mode 100644 index 00000000..d6b51a7a --- /dev/null +++ b/app/views/posts/attribute_ro/_new_has_and_belongs_to_many.haml @@ -0,0 +1,6 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + %ul{ dir: dir, lang: locale } + - metadata.has_many.each do |p| + %li= link_to p.title.value, site_post_path(site, p.id) 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 new file mode 100644 index 00000000..143ad2d1 --- /dev/null +++ b/app/views/posts/attributes/_new_has_and_belongs_to_many.haml @@ -0,0 +1,104 @@ +-# + Genera un listado de checkboxes entre los que se puede elegir 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) } } + %template{ data: { target: 'array.placeholder' } } + .col.p-3{ 'aria-hidden': 'true' } + %span.placeholder.w-100 + + .form-group + = hidden_field_tag name, '' + .d-flex.align-items-center.justify-content-between + = label_tag id, post_label_t(attribute, post: post) + = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + -# 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.no-gutters.placeholder-glow{ data: { target: 'array.current' } } + - metadata.values.slice(*metadata.value).each do |value| + = render 'posts/new_array_value', value: value + + = 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 + + -# + 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' From 067991e72b83c77d3fc63b14eae87377524dce61 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 17:55:21 -0300 Subject: [PATCH 056/161] fix: mostrar los valores anteriores al editar --- app/views/posts/attributes/_new_belongs_to.haml | 5 +++-- .../posts/attributes/_new_has_and_belongs_to_many.haml | 6 ++++-- app/views/posts/attributes/_new_has_many.haml | 6 ++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index afbc8bdd..0a2a4c5b 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -41,8 +41,9 @@ 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.no-gutters.placeholder-glow{ data: { target: 'array.current' } } - - metadata.values.slice(*metadata.value).each do |value| - = render 'posts/new_array_value', value: value + -# @todo issue-7537 + - if !metadata.empty? && (indexed_post = site.indexed_posts.find_by(post_id: metadata.value)) + = render 'posts/new_related_post', post: indexed_post = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do - content_for :"#{id}_header" do 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 143ad2d1..0b53f0ae 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 @@ -40,8 +40,10 @@ 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.no-gutters.placeholder-glow{ data: { target: 'array.current' } } - - metadata.values.slice(*metadata.value).each do |value| - = render 'posts/new_array_value', value: value + -# @todo issue-7537 + - metadata.value.each do |uuid| + - if (indexed_post = site.indexed_posts.find_by(post_id: uuid)) + = render 'posts/new_related_post', post: indexed_post = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do - content_for :"#{id}_header" do diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 143ad2d1..0b53f0ae 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -40,8 +40,10 @@ 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.no-gutters.placeholder-glow{ data: { target: 'array.current' } } - - metadata.values.slice(*metadata.value).each do |value| - = render 'posts/new_array_value', value: value + -# @todo issue-7537 + - metadata.value.each do |uuid| + - if (indexed_post = site.indexed_posts.find_by(post_id: uuid)) + = render 'posts/new_related_post', post: indexed_post = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do - content_for :"#{id}_header" do From a6e46d112fb1d4a9c415ad60bba6138b47764b2b Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 17:56:27 -0300 Subject: [PATCH 057/161] =?UTF-8?q?fix:=20un=20poco=20de=20separaci=C3=B3n?= =?UTF-8?q?=20entre=20columnas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/posts/_new_related_post.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/_new_related_post.haml b/app/views/posts/_new_related_post.haml index 8425ad3e..79bf33a2 100644 --- a/app/views/posts/_new_related_post.haml +++ b/app/views/posts/_new_related_post.haml @@ -6,7 +6,7 @@ description = post.post.image.value['description'] end -.col.mb-3 +.col.mb-3.p-1 = render('bootstrap/card', image: image, description: description, title: post.title, class: 'h-100') do - if post.post.attribute?(:description) %p.card-text= post.post.description.value From 9648bc135de314064cd37732c58a4412a6f4ad41 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 18:18:29 -0300 Subject: [PATCH 058/161] feat: predefined value --- app/models/metadata_new_predefined_value.rb | 4 ++ .../attribute_ro/_new_predefined_value.haml | 3 + .../attributes/_new_predefined_value.haml | 61 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 app/models/metadata_new_predefined_value.rb create mode 100644 app/views/posts/attribute_ro/_new_predefined_value.haml create mode 100644 app/views/posts/attributes/_new_predefined_value.haml diff --git a/app/models/metadata_new_predefined_value.rb b/app/models/metadata_new_predefined_value.rb new file mode 100644 index 00000000..ff839759 --- /dev/null +++ b/app/models/metadata_new_predefined_value.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Nueva interfaz +class MetadataNewPredefinedValue < MetadataPredefinedValue; end diff --git a/app/views/posts/attribute_ro/_new_predefined_value.haml b/app/views/posts/attribute_ro/_new_predefined_value.haml new file mode 100644 index 00000000..67642e2c --- /dev/null +++ b/app/views/posts/attribute_ro/_new_predefined_value.haml @@ -0,0 +1,3 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td{ dir: dir, lang: locale }= metadata.value diff --git a/app/views/posts/attributes/_new_predefined_value.haml b/app/views/posts/attributes/_new_predefined_value.haml new file mode 100644 index 00000000..9975cd11 --- /dev/null +++ b/app/views/posts/attributes/_new_predefined_value.haml @@ -0,0 +1,61 @@ +-# + 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_array_value_path(site) } } + %template{ data: { target: 'array.placeholder' } } + .col.p-3{ 'aria-hidden': 'true' } + %span.placeholder.w-100 + + .form-group + = hidden_field_tag name, '' + .d-flex.align-items-center.justify-content-between + = label_tag id, post_label_t(attribute, post: post) + = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + + -# 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.no-gutters.placeholder-glow{ data: { target: 'array.current' } } + - unless metadata.empty? + = render 'posts/new_array_value', metadata.values[metadata.value] + + = 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, 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' + + - content_for :"#{id}_footer" do + = 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' From 74049a9cb490f22da719009dfc51cc9cf5092fcf Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 18:21:40 -0300 Subject: [PATCH 059/161] fix: mostrar el valor legible por humanes --- app/views/posts/attribute_ro/_new_predefined_value.haml | 2 +- app/views/posts/attribute_ro/_predefined_value.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/posts/attribute_ro/_new_predefined_value.haml b/app/views/posts/attribute_ro/_new_predefined_value.haml index 67642e2c..fd594975 100644 --- a/app/views/posts/attribute_ro/_new_predefined_value.haml +++ b/app/views/posts/attribute_ro/_new_predefined_value.haml @@ -1,3 +1,3 @@ %tr{ id: attribute } %th= post_label_t(attribute, post: post) - %td{ dir: dir, lang: locale }= metadata.value + %td{ dir: dir, lang: locale }= metadata.values.invert[metadata.value] diff --git a/app/views/posts/attribute_ro/_predefined_value.haml b/app/views/posts/attribute_ro/_predefined_value.haml index 67642e2c..fd594975 100644 --- a/app/views/posts/attribute_ro/_predefined_value.haml +++ b/app/views/posts/attribute_ro/_predefined_value.haml @@ -1,3 +1,3 @@ %tr{ id: attribute } %th= post_label_t(attribute, post: post) - %td{ dir: dir, lang: locale }= metadata.value + %td{ dir: dir, lang: locale }= metadata.values.invert[metadata.value] From f58b3fb46493ed3d9bad9f74ba23d5f093331d98 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 18:26:10 -0300 Subject: [PATCH 060/161] =?UTF-8?q?fix:=20etiquetas=20por=20defecto=20para?= =?UTF-8?q?=20el=20t=C3=ADtulo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/locales/en.yml | 2 ++ config/locales/es.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 8fbfd536..b432f11b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -725,6 +725,8 @@ en: remove: "Remove" attributes: add: Add + title: + label: "Title" lang: label: Language date: diff --git a/config/locales/es.yml b/config/locales/es.yml index 52ade5b2..7ffe4f90 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -733,6 +733,8 @@ es: remove: "Eliminar" attributes: add: Agregar + title: + label: "Título" lang: label: Idioma date: From 25efe4d51d166d676100f4a29628d799a97b61b1 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 18:34:28 -0300 Subject: [PATCH 061/161] fix: dry --- app/views/posts/attribute_ro/_new_predefined_value.haml | 2 +- app/views/posts/attribute_ro/_predefined_value.haml | 2 +- app/views/posts/attributes/_new_predefined_value.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/posts/attribute_ro/_new_predefined_value.haml b/app/views/posts/attribute_ro/_new_predefined_value.haml index fd594975..d44eef69 100644 --- a/app/views/posts/attribute_ro/_new_predefined_value.haml +++ b/app/views/posts/attribute_ro/_new_predefined_value.haml @@ -1,3 +1,3 @@ %tr{ id: attribute } %th= post_label_t(attribute, post: post) - %td{ dir: dir, lang: locale }= metadata.values.invert[metadata.value] + %td{ dir: dir, lang: locale }= metadata.to_s diff --git a/app/views/posts/attribute_ro/_predefined_value.haml b/app/views/posts/attribute_ro/_predefined_value.haml index fd594975..d44eef69 100644 --- a/app/views/posts/attribute_ro/_predefined_value.haml +++ b/app/views/posts/attribute_ro/_predefined_value.haml @@ -1,3 +1,3 @@ %tr{ id: attribute } %th= post_label_t(attribute, post: post) - %td{ dir: dir, lang: locale }= metadata.values.invert[metadata.value] + %td{ dir: dir, lang: locale }= metadata.to_s diff --git a/app/views/posts/attributes/_new_predefined_value.haml b/app/views/posts/attributes/_new_predefined_value.haml index 9975cd11..42e309c3 100644 --- a/app/views/posts/attributes/_new_predefined_value.haml +++ b/app/views/posts/attributes/_new_predefined_value.haml @@ -42,7 +42,7 @@ o activamos los ítemes según estén incluidos en esa lista o no. .row.row-cols-3.row-cols-md-4.no-gutters.placeholder-glow{ data: { target: 'array.current' } } - unless metadata.empty? - = render 'posts/new_array_value', metadata.values[metadata.value] + = render 'posts/new_array_value', value: metadata.to_s = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do - content_for :"#{id}_header" do From 1947c49fb061f051ad5b323339a4bb780363419b Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 18:36:22 -0300 Subject: [PATCH 062/161] fix: distinguir los valores --- app/views/posts/_new_array_value.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/posts/_new_array_value.haml b/app/views/posts/_new_array_value.haml index 75c5bf4d..00cf293f 100644 --- a/app/views/posts/_new_array_value.haml +++ b/app/views/posts/_new_array_value.haml @@ -1,2 +1,3 @@ .col - %p= value + %p + %strong= value From b21f93641bdc9ba39aba32b4016e60ec9b1e08fa Mon Sep 17 00:00:00 2001 From: f Date: Tue, 28 May 2024 18:37:33 -0300 Subject: [PATCH 063/161] fix: usar clases --- app/assets/stylesheets/application.scss | 4 ---- app/views/sites/_form.haml | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 7a082ae8..8fe16152 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -322,10 +322,6 @@ svg { .designs { .design { margin-top: 1rem; - - .custom-control-label { - font-weight: bold; - } } } diff --git a/app/views/sites/_form.haml b/app/views/sites/_form.haml index ec2712bf..258f28d3 100644 --- a/app/views/sites/_form.haml +++ b/app/views/sites/_form.haml @@ -64,7 +64,7 @@ disabled: design.disabled, required: true, class: 'custom-control-input' = f.label "design_id_#{design.id}", design.name, - class: 'custom-control-label' + class: 'custom-control-label font-weight-bold' .flex-fill = sanitize_markdown design.description, tags: %w[p a strong em] @@ -93,7 +93,7 @@ = f.radio_button :licencia_id, licencia.id, checked: licencia.id == site.licencia_id, required: true, class: 'custom-control-input' - = f.label "licencia_id_#{licencia.id}", class: 'custom-control-label' do + = f.label "licencia_id_#{licencia.id}", class: 'custom-control-label font-weight-bold' do = licencia.name = sanitize_markdown licencia.description, tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6] From 9d87da2e5436bebb812129bcef82ac41db13a354 Mon Sep 17 00:00:00 2001 From: fauno Date: Wed, 29 May 2024 19:40:17 +0000 Subject: [PATCH 064/161] ci: test [skip ci] From 50a4823dad3e1c93a2e1b34373584cbe92376941 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 31 May 2024 10:06:44 -0300 Subject: [PATCH 065/161] =?UTF-8?q?fix:=20arreglar=20migraci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/migrate/20240227134845_create_fediblocks.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20240227134845_create_fediblocks.rb b/db/migrate/20240227134845_create_fediblocks.rb index 03f65f7c..1a61ccba 100644 --- a/db/migrate/20240227134845_create_fediblocks.rb +++ b/db/migrate/20240227134845_create_fediblocks.rb @@ -12,7 +12,7 @@ class CreateFediblocks < ActiveRecord::Migration[6.1] t.string :url, null: false t.string :download_url, null: false t.string :format, null: false - t.jsonb :instances, default: [] + t.jsonb :hostnames, default: [] end YAML.safe_load(File.read('db/seeds/activity_pub/fediblocks.yml')).each do |fediblock| From 78fd8c933a35fcbf839ab783bcc892d4062b57fd Mon Sep 17 00:00:00 2001 From: f Date: Fri, 31 May 2024 10:06:44 -0300 Subject: [PATCH 066/161] =?UTF-8?q?fix:=20arreglar=20migraci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/migrate/20240227134845_create_fediblocks.rb | 2 +- ...0228171335_rename_fediblock_instances_to_hostnames.rb | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 db/migrate/20240228171335_rename_fediblock_instances_to_hostnames.rb diff --git a/db/migrate/20240227134845_create_fediblocks.rb b/db/migrate/20240227134845_create_fediblocks.rb index 03f65f7c..1a61ccba 100644 --- a/db/migrate/20240227134845_create_fediblocks.rb +++ b/db/migrate/20240227134845_create_fediblocks.rb @@ -12,7 +12,7 @@ class CreateFediblocks < ActiveRecord::Migration[6.1] t.string :url, null: false t.string :download_url, null: false t.string :format, null: false - t.jsonb :instances, default: [] + t.jsonb :hostnames, default: [] end YAML.safe_load(File.read('db/seeds/activity_pub/fediblocks.yml')).each do |fediblock| diff --git a/db/migrate/20240228171335_rename_fediblock_instances_to_hostnames.rb b/db/migrate/20240228171335_rename_fediblock_instances_to_hostnames.rb deleted file mode 100644 index bad343f2..00000000 --- a/db/migrate/20240228171335_rename_fediblock_instances_to_hostnames.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# Cambia el nombre de la columna para que podamos obtener todas las -# instancias de un fediblock -class RenameFediblockInstancesToHostnames < ActiveRecord::Migration[6.1] - def change - rename_column :activity_pub_fediblocks, :instances, :hostnames - end -end From 12dfd1e9e71519fd124b767085563bf76d3a659e Mon Sep 17 00:00:00 2001 From: f Date: Fri, 31 May 2024 10:16:04 -0300 Subject: [PATCH 067/161] fix: oliphant blocklist --- db/seeds/activity_pub/fediblocks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/seeds/activity_pub/fediblocks.yml b/db/seeds/activity_pub/fediblocks.yml index c977f9bf..35fe38cd 100644 --- a/db/seeds/activity_pub/fediblocks.yml +++ b/db/seeds/activity_pub/fediblocks.yml @@ -6,7 +6,7 @@ id: "9046789a-5de8-4b16-beed-796060f8f3cc" - title: "Oliphant Tier 0" url: "https://writer.oliphant.social/oliphant/the-oliphant-social-blocklist" - download_url: "https://codeberg.org/oliphant/blocklists/raw/branch/main/blocklists/mastodon/tier0.csv" + download_url: "https://codeberg.org/oliphant/blocklists/raw/branch/main/blocklists/mastodon/seirdy-tier0.csv" format: "mastodon" id: "fc1efcb8-7e68-4a76-ae9e-0c447752b12b" - title: "The Bad Space (90%)" From 43308e28112292d0df35798f83e7dce80864c2a5 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 3 Jun 2024 17:45:23 -0300 Subject: [PATCH 068/161] 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 069/161] 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 070/161] =?UTF-8?q?fix:=20m=C3=A1s=20espacio=20para=20los?= =?UTF-8?q?=20modales?= 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 071/161] =?UTF-8?q?fix:=20no=20establecer=20ids=20vac?= =?UTF-8?q?=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 072/161] =?UTF-8?q?docs:=20documentar=20los=20par=C3=A1met?= =?UTF-8?q?ros=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 073/161] 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 074/161] 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 From 41ad7d806e5448c5297c6ac10e222ef00728d040 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 4 Jun 2024 17:07:05 -0300 Subject: [PATCH 075/161] fix: correr htmx junto a turbolinks --- app/javascript/packs/application.js | 2 +- package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index c523b0f0..dc87b1c3 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -41,5 +41,5 @@ Rails.start() Turbolinks.start() ActiveStorage.start() -window.htmx = require('htmx.org/dist/htmx.js') +window.htmx = require("@suttyweb/htmx.org/dist/htmx.js"); window.htmx.config.selfRequestsOnly = true; diff --git a/package.json b/package.json index 088316bc..73cfd589 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@rails/ujs": "^6.1.3-1", "@rails/webpacker": "5.4.4", "@suttyweb/editor": "^0.1.29", + "@suttyweb/htmx.org": "^1.9.13", "babel-loader": "^8.2.2", "bs-custom-file-input": "^1.3.4", "chart.js": "^3.5.1", @@ -22,7 +23,6 @@ "commonmark": "^0.29.0", "fork-awesome": "^1.1.7", "fork-ts-checker-webpack-plugin": "^6.1.0", - "htmx.org": "^1.9.11", "input-map": "git+https://0xacab.org/sutty/input-map.git", "input-tag": "git+https://0xacab.org/sutty/input-tag.git", "leaflet": "^1.7.1", diff --git a/yarn.lock b/yarn.lock index fc6ae7cb..d181bebe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1984,6 +1984,11 @@ linkifyjs "^4.1.1" prosemirror-svelte-nodeview "^1.0.2" +"@suttyweb/htmx.org@^1.9.13": + version "1.9.13" + resolved "https://registry.yarnpkg.com/@suttyweb/htmx.org/-/htmx.org-1.9.13.tgz#bc67a5e2947d7cc125649b829610da8ee61f21af" + integrity sha512-/2x3AGXT2CFOmp8Nf59XY2brah5wSo4YvcctYA2zD4BByNft4XE0rUk4VM7TjPxR1LOzg6VK1crZmdiy3JyzxQ== + "@types/caseless@*": version "0.12.2" resolved "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz" @@ -4575,11 +4580,6 @@ html-entities@^1.3.1: resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz" integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== -htmx.org@^1.9.11: - version "1.9.11" - resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-1.9.11.tgz#00192041ee682d6ca7146d0fbd901169ffe72d87" - integrity sha512-WlVuICn8dfNOOgYmdYzYG8zSnP3++AdHkMHooQAzGZObWpVXYathpz/I37ycF4zikR6YduzfCvEcxk20JkIUsw== - http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" From 3983f17ba225680cb02d531bcbd9b94cc3dd1c36 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 4 Jun 2024 17:07:43 -0300 Subject: [PATCH 076/161] fix: no cambiar el post si lo asignamos antes --- app/services/post_service.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 93daaa45..3045bd9c 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -7,8 +7,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do # # @return Post def create - self.post = site.posts(lang: locale) - .build(layout: layout) + self.post ||= site.posts(lang: locale).build(layout: layout) post.usuaries << usuarie post.draft.value = true if site.invitade? usuarie post.assign_attributes(post_params) From 74ed69d52c0c33f3fe11968d0c3c11ca3fbd82a8 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 4 Jun 2024 17:08:31 -0300 Subject: [PATCH 077/161] feat: poder actualizar el post si sabemos el uuid --- app/controllers/posts_controller.rb | 2 +- app/services/post_service.rb | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 282a784a..8b103422 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -117,7 +117,7 @@ class PostsController < ApplicationController service = PostService.new(site: site, usuarie: current_usuarie, params: params) - @post = service.create + @post = service.create_or_update if post.persisted? site.touch diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 3045bd9c..bb49c5bd 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -3,6 +3,26 @@ # Este servicio se encarga de crear artículos y guardarlos en git, # asignándoselos a une usuarie PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do + + # Si estamos pasando el UUID con los parámetros, el post quizás + # existe. + # + # @return [Post] + def create_or_update + uuid = params.require(base).permit(:uuid).values.first + + binding.pry + + if uuid.blank? + create + elsif (indexed_post = site.indexed_posts.find_by(post_id: uuid)).present? + self.post = indexed_post.post + update + else + create + end + end + # Crea un artículo nuevo # # @return Post From 5a6ee375b1e52a36b28acc3c00d13f38bf3fd1fc Mon Sep 17 00:00:00 2001 From: f Date: Tue, 4 Jun 2024 17:09:04 -0300 Subject: [PATCH 078/161] feat: soportar htmx al actualizar posts --- app/controllers/posts_controller.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 8b103422..0f3c24d5 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -165,7 +165,24 @@ class PostsController < ApplicationController if service.update.persisted? site.touch forget_content + end + if htmx? + if post.persisted? + swap_modals + + @value = post.title.value + @uuid = post.uuid.value + @name = params.require(:name) + + render render_path_from_attribute, layout: false + else + headers['HX-Retarget'] = "##{params.require(:form)}" + headers['HX-Reswap'] = 'outerHTML' + + 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) else render 'posts/edit' From 0e83b09bf01a9903feb9629b5986e29219f7155c Mon Sep 17 00:00:00 2001 From: f Date: Tue, 4 Jun 2024 17:11:32 -0300 Subject: [PATCH 079/161] fix: new_has_one --- app/views/posts/attributes/_new_has_one.haml | 6 ++++-- config/locales/en.yml | 3 +++ config/locales/es.yml | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/views/posts/attributes/_new_has_one.haml b/app/views/posts/attributes/_new_has_one.haml index 5a614990..4235b9b3 100644 --- a/app/views/posts/attributes/_new_has_one.haml +++ b/app/views/posts/attributes/_new_has_one.haml @@ -38,7 +38,8 @@ datos y recibir su UUID en respuesta. - content_for :post_form do %div{ id: modal_id, data: { controller: 'modal' }} - - if layout.is_a?(String) + -# Si hay un solo layout o el post asociado ya existía + - if layout.is_a?(String) || metadata.has_one.present? = 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 @@ -47,7 +48,8 @@ - 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' + = render 'bootstrap/btn', content: t('.close'), action: 'modal#hide', class: 'm-0 mt-1 mr-1' - else + -# @todo Implementar selección de layout para cargar el formulario correcto Nada diff --git a/config/locales/en.yml b/config/locales/en.yml index b432f11b..c9d988a4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -756,6 +756,8 @@ en: empty: "(Empty)" draft: label: Draft + new_has_one: + edit: "Edit" reorder: submit: 'Save order' select: 'Select this post' @@ -944,6 +946,7 @@ en: add: "Add %{layout}" add_new: "Add new option" cancel: "Cancel" + close: "Close" edit: "Edit" filter: "Start typing to filter..." save: "Save" diff --git a/config/locales/es.yml b/config/locales/es.yml index 7ffe4f90..8d3d7f74 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -764,6 +764,8 @@ es: empty: "(Vacío)" draft: label: Borrador + new_has_one: + edit: "Editar" reorder: submit: 'Guardar orden' select: 'Seleccionar este artículo' @@ -952,6 +954,7 @@ es: add: "Agregar %{layout}" add_new: "Agregar nueva opción" cancel: "Cancelar" + close: "Cerrar" edit: "Editar" filter: "Empezá a escribir para filtrar..." save: "Guardar" From a2275d8654c59b5e9e85fe3fb834990bb0915163 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 4 Jun 2024 17:12:18 -0300 Subject: [PATCH 080/161] fix: traducciones --- config/locales/en.yml | 10 ++++++++++ config/locales/es.yml | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index c9d988a4..3eb0cc24 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -756,8 +756,18 @@ en: empty: "(Empty)" draft: label: Draft + new_has_many: + edit: "Edit" + new_has_and_belongs_to_many: + edit: "Edit" + new_predefined_array: + edit: "Edit" + new_array: + edit: "Edit" new_has_one: edit: "Edit" + new_belongs_to: + edit: "Edit" reorder: submit: 'Save order' select: 'Select this post' diff --git a/config/locales/es.yml b/config/locales/es.yml index 8d3d7f74..d69a88b7 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -764,8 +764,18 @@ es: empty: "(Vacío)" draft: label: Borrador + new_has_many: + edit: "Editar" + new_has_and_belongs_to_many: + edit: "Editar" + new_predefined_array: + edit: "Editar" + new_array: + edit: "Editar" new_has_one: edit: "Editar" + new_belongs_to: + edit: "Editar" reorder: submit: 'Guardar orden' select: 'Seleccionar este artículo' From 295c4f57ef4e053866037e7bdf188a0cc74bade0 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 5 Jun 2024 18:00:55 -0300 Subject: [PATCH 081/161] feat: cancelar con escape --- app/javascript/controllers/array_controller.js | 8 +++++++- app/javascript/controllers/modal_controller.js | 6 ++++++ app/views/bootstrap/_modal.haml | 4 +++- app/views/posts/attributes/_new_array.haml | 2 +- app/views/posts/attributes/_new_belongs_to.haml | 2 +- .../posts/attributes/_new_has_and_belongs_to_many.haml | 2 +- app/views/posts/attributes/_new_has_many.haml | 2 +- app/views/posts/attributes/_new_predefined_array.haml | 2 +- app/views/posts/attributes/_new_predefined_value.haml | 2 +- 9 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/javascript/controllers/array_controller.js b/app/javascript/controllers/array_controller.js index 19d00ccd..db768ef3 100644 --- a/app/javascript/controllers/array_controller.js +++ b/app/javascript/controllers/array_controller.js @@ -76,10 +76,16 @@ export default class extends Controller { return this.inputFrom(itemTarget)?.checked || false; } + cancelWithEscape(event) { + if (event?.key !== "Escape") return; + + this.cancel(); + } + /* * Al cancelar, se vuelve al estado original de la lista */ - cancel(event) { + cancel(event = undefined) { for (const itemTarget of this.itemTargets) { const input = this.inputFrom(itemTarget); diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js index 0f8deeca..2ffdedaa 100644 --- a/app/javascript/controllers/modal_controller.js +++ b/app/javascript/controllers/modal_controller.js @@ -56,6 +56,12 @@ export default class extends Controller { }, 1); } + hideWithEscape(event) { + if (event?.key !== "Escape") return; + + this.hide(); + } + hide(event = undefined) { event?.preventDefault(); const modalId = event?.detail?.id; diff --git a/app/views/bootstrap/_modal.haml b/app/views/bootstrap/_modal.haml index 3efd1525..8624f9cd 100644 --- a/app/views/bootstrap/_modal.haml +++ b/app/views/bootstrap/_modal.haml @@ -21,12 +21,14 @@ :ruby local_assigns[:hide_actions] ||= [] local_assigns[:hide_actions] << 'click->modal#hide' + local_assigns[:keydown_actions] ||= [] + local_assigns[:keydown_actions] << 'keydown->modal#hideWithEscape' local_assigns[:modal_content_attributes] ||= {} -# XXX: Necesario para poder generar todas las demás = yield -.modal.fade{ tabindex: -1, aria: { hidden: 'true' }, data: { target: 'modal.modal' } } +.modal.fade{ tabindex: -1, aria: { hidden: 'true' }, data: { target: 'modal.modal', action: local_assigns[:keydown_actions].join(' ') } } .modal-backdrop.fade{ data: { target: 'modal.backdrop', action: local_assigns[:hide_actions].join(' ') } } .modal-dialog.modal-dialog-scrollable.modal-dialog-centered.modal-lg .modal-content{ **local_assigns[:modal_content_attributes] } diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 1ad12b8e..da4a93c7 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -30,7 +30,7 @@ - metadata.value.sort_by(&:remove_diacritics).each do |value| = render 'posts/new_array_value', value: value - = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do + = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'], keydown_actions: %w[keydown->array#cancelWithEscape] do - content_for :"#{id}_header" do .form-group.flex-grow-1.mb-0 = label_tag id, post_label_t(attribute, post: post) diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index a4867276..5c8844a5 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -45,7 +45,7 @@ - if !metadata.empty? && (indexed_post = site.indexed_posts.find_by(post_id: metadata.value)) = render 'posts/new_related_post', post: indexed_post - = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do + = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'], keydown_actions: %w[keydown->array#cancelWithEscape] do - content_for :"#{id}_header" do .form-group.flex-grow-1.mb-0 = label_tag id, post_label_t(attribute, post: post) 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 243178d9..8b615173 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 @@ -45,7 +45,7 @@ - if (indexed_post = site.indexed_posts.find_by(post_id: uuid)) = render 'posts/new_related_post', post: indexed_post - = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do + = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'], keydown_actions: %w[keydown->array#cancelWithEscape] do - content_for :"#{id}_header" do .form-group.flex-grow-1.mb-0 = label_tag id, post_label_t(attribute, post: post) diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index f8312c20..d211d864 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -45,7 +45,7 @@ - if (indexed_post = site.indexed_posts.find_by(post_id: uuid)) = render 'posts/new_related_post', post: indexed_post - = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do + = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'], keydown_actions: %w[keydown->array#cancelWithEscape] do - content_for :"#{id}_header" do .form-group.flex-grow-1.mb-0 = label_tag id, post_label_t(attribute, post: post) diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index 2adf44c1..c67b2ea9 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -33,7 +33,7 @@ - metadata.values.slice(*metadata.value).each_key do |value| = render 'posts/new_array_value', value: value - = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do + = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'], keydown_actions: %w[keydown->array#cancelWithEscape] do - content_for :"#{id}_header" do .form-group.flex-grow-1.mb-0 = label_tag id, post_label_t(attribute, post: post) diff --git a/app/views/posts/attributes/_new_predefined_value.haml b/app/views/posts/attributes/_new_predefined_value.haml index c592b20a..7f418472 100644 --- a/app/views/posts/attributes/_new_predefined_value.haml +++ b/app/views/posts/attributes/_new_predefined_value.haml @@ -44,7 +44,7 @@ - unless metadata.empty? = render 'posts/new_array_value', value: metadata.to_s - = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'] do + = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'], keydown_actions: %w[keydown->array#cancelWithEscape] do - content_for :"#{id}_header" do .form-group.flex-grow-1.mb-0 = label_tag id, post_label_t(attribute, post: post) From 706f0162a53d6c72b723be79fe36b5c672aaf9b5 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 5 Jun 2024 18:01:44 -0300 Subject: [PATCH 082/161] feat: pasar el foco al modal al abrir y volver al cerrar --- app/javascript/controllers/modal_controller.js | 12 +++++++++++- app/views/posts/attributes/_new_has_one.haml | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js index 2ffdedaa..1d89c826 100644 --- a/app/javascript/controllers/modal_controller.js +++ b/app/javascript/controllers/modal_controller.js @@ -26,7 +26,7 @@ export default class extends Controller { if (!event.target?.dataset?.modalShowValue) return; - window.dispatchEvent(new CustomEvent("modal:show", { detail: { id: event.target.dataset.modalShowValue } })); + window.dispatchEvent(new CustomEvent("modal:show", { detail: { id: event.target.dataset.modalShowValue, previousFocus: event.target.id } })); } /* @@ -50,9 +50,17 @@ export default class extends Controller { window.document.body.classList.add("modal-open"); + if (event?.detail?.previousFocus) { + this.previousFocus = window.document.getElementById(event.detail.previousFocus); + } else { + this.previousFocus = event?.target; + } + setTimeout(() => { this.modalTarget.classList.add("show"); this.backdropTarget.classList.add("show"); + + this.modalTarget.focus(); }, 1); } @@ -75,6 +83,8 @@ export default class extends Controller { this.modalTarget.removeAttribute("role"); this.modalTarget.removeAttribute("aria-modal"); + this.previousFocus?.focus(); + setTimeout(() => { this.modalTarget.style.display = ""; this.backdropTarget.style.display = ""; diff --git a/app/views/posts/attributes/_new_has_one.haml b/app/views/posts/attributes/_new_has_one.haml index 4235b9b3..58e098b6 100644 --- a/app/views/posts/attributes/_new_has_one.haml +++ b/app/views/posts/attributes/_new_has_one.haml @@ -25,7 +25,7 @@ = 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 } + = render 'bootstrap/btn', content: t('.edit'), action: 'modal#showAnother', data: { 'modal-show-value': modal_id }, id: random_id -# Aquí se reemplaza por la tarjeta y el UUID luego de guardar .row.row-cols-1.no-gutters.placeholder-glow{ id: target_id } From 9edbe9ecf16acf66298864eb59c397bbaade9ce4 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 5 Jun 2024 18:02:14 -0300 Subject: [PATCH 083/161] fix: pry perdido --- app/services/post_service.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/services/post_service.rb b/app/services/post_service.rb index bb49c5bd..5f27211d 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -11,8 +11,6 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do def create_or_update uuid = params.require(base).permit(:uuid).values.first - binding.pry - if uuid.blank? create elsif (indexed_post = site.indexed_posts.find_by(post_id: uuid)).present? From 74d462867ad443bb197b8f955334155dc77fdf51 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 13 Jun 2024 11:39:47 -0300 Subject: [PATCH 084/161] fix: arreglos en modo oscuro #16489 --- app/assets/stylesheets/application.scss | 7 +++++++ app/assets/stylesheets/dark.scss | 1 + 2 files changed, 8 insertions(+) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 8fe16152..4b659972 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -21,6 +21,11 @@ $form-feedback-invalid-color: $magenta; $form-feedback-icon-valid-color: $black; $component-active-bg: $magenta; $zindex-modal-backdrop: 0; +$modal-content-bg: var(--background); +$card-bg: var(--background); +$card-border-color: var(--card-border-color); +$input-bg: var(--background); +$input-color: var(--foreground); $spacers: ( 2-plus: 0.75rem @@ -61,6 +66,7 @@ $sizes: ( --foreground: #{$black}; --background: #{$white}; --color: #{$magenta}; + --card-border-color: #{rgba($black, .125)}; } @media (prefers-color-scheme: dark) { @@ -68,6 +74,7 @@ $sizes: ( --foreground: #{$white}; --background: #{$black}; --color: #{$cyan}; + --card-border-color: #{rgba($white, .125)}; } .btn-secondary { diff --git a/app/assets/stylesheets/dark.scss b/app/assets/stylesheets/dark.scss index f7f3a09d..a5a1f837 100644 --- a/app/assets/stylesheets/dark.scss +++ b/app/assets/stylesheets/dark.scss @@ -6,6 +6,7 @@ $cyan: #13fefe; --foreground: #{$white}; --background: #{$black}; --color: #{$cyan}; + --card-border-color: #{rgba($white, .125)}; } .btn { From ad5a553fa19fc61364df3aa9c01c0cba43185e83 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 17 Jun 2024 11:08:27 -0300 Subject: [PATCH 085/161] =?UTF-8?q?fix:=20arreglar=20el=20bot=C3=B3n=20de?= =?UTF-8?q?=20editar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/posts/_new_related_post.haml | 2 +- config/locales/en.yml | 2 ++ config/locales/es.yml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/posts/_new_related_post.haml b/app/views/posts/_new_related_post.haml index 79bf33a2..046080c9 100644 --- a/app/views/posts/_new_related_post.haml +++ b/app/views/posts/_new_related_post.haml @@ -11,4 +11,4 @@ - if post.post.attribute?(:description) %p.card-text= post.post.description.value - = link_to 'Editar', post.path, class: 'btn btn-secondary' + = link_to t('.edit'), edit_site_post_path(post.site, post.path), class: 'btn btn-secondary' diff --git a/config/locales/en.yml b/config/locales/en.yml index 3eb0cc24..457b7927 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -960,3 +960,5 @@ en: edit: "Edit" filter: "Start typing to filter..." save: "Save" + card: + edit: "Edit" diff --git a/config/locales/es.yml b/config/locales/es.yml index d69a88b7..d149dee8 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -968,3 +968,5 @@ es: edit: "Editar" filter: "Empezá a escribir para filtrar..." save: "Guardar" + card: + edit: "Editar" From cb45d0aa6c5d198927f924923249c85de096bb7f Mon Sep 17 00:00:00 2001 From: f Date: Mon, 17 Jun 2024 11:10:27 -0300 Subject: [PATCH 086/161] fix: distinguir mejor los valores elegidos --- app/views/posts/_new_array_value.haml | 4 +--- app/views/posts/attributes/_new_array.haml | 2 +- app/views/posts/attributes/_new_predefined_array.haml | 2 +- app/views/posts/attributes/_new_predefined_value.haml | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/views/posts/_new_array_value.haml b/app/views/posts/_new_array_value.haml index 00cf293f..73bea5dc 100644 --- a/app/views/posts/_new_array_value.haml +++ b/app/views/posts/_new_array_value.haml @@ -1,3 +1 @@ -.col - %p - %strong= value +%li= value diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index da4a93c7..d0d659dd 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -26,7 +26,7 @@ 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-1.row-cols-md-2.no-gutters.placeholder-glow{ data: { target: 'array.current' } } + %ul.placeholder-glow{ data: { target: 'array.current' } } - metadata.value.sort_by(&:remove_diacritics).each do |value| = render 'posts/new_array_value', value: value diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index c67b2ea9..ca0cdaf2 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -29,7 +29,7 @@ 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-1.row-cols-md-2.no-gutters.placeholder-glow{ data: { target: 'array.current' } } + %ul.placeholder-glow{ data: { target: 'array.current' } } - metadata.values.slice(*metadata.value).each_key do |value| = render 'posts/new_array_value', value: value diff --git a/app/views/posts/attributes/_new_predefined_value.haml b/app/views/posts/attributes/_new_predefined_value.haml index 7f418472..ae04fd0b 100644 --- a/app/views/posts/attributes/_new_predefined_value.haml +++ b/app/views/posts/attributes/_new_predefined_value.haml @@ -40,7 +40,7 @@ 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.no-gutters.placeholder-glow{ data: { target: 'array.current' } } + %ul.placeholder-glow{ data: { target: 'array.current' } } - unless metadata.empty? = render 'posts/new_array_value', value: metadata.to_s From e35fa8746436fdd62caa3b10c3314a4de8cd260d Mon Sep 17 00:00:00 2001 From: f Date: Tue, 18 Jun 2024 10:45:57 -0300 Subject: [PATCH 087/161] fix: ya no es necesario estimar visitas #16542 --- app/controllers/stats_controller.rb | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index c2c7bc58..bc5bb962 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -59,9 +59,6 @@ class StatsController < ApplicationController .order('sum(value) desc') .sum(:value) .transform_values(&:to_i) - .transform_values do |v| - v * nodes - end end end end @@ -73,9 +70,6 @@ class StatsController < ApplicationController stats = rollup_scope.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series| series.each do |serie| serie[:name] = serie.dig(:dimensions, 'host') - serie[:data].transform_values! do |value| - value * nodes - end end end @@ -99,9 +93,6 @@ class StatsController < ApplicationController stats = rollup_scope.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series| series.each do |serie| serie[:name] = serie[:dimensions].slice('host', 'uri').values.join.sub('/index.html', '/') - serie[:data].transform_values! do |value| - value * nodes - end end end @@ -197,21 +188,6 @@ class StatsController < ApplicationController end end - # Obtiene la cantidad de nodos de Sutty, para poder calcular la - # cantidad de visitas. - # - # Como repartimos las visitas por nodo rotando las IPs en el - # nameserver y los resolvedores de DNS eligen un nameserver - # aleatoriamente, la cantidad de visitas se reparte - # equitativamente. - # - # XXX: Remover cuando podamos centralizar los AccessLog - # - # @return [Integer] - def nodes - @nodes ||= ENV.fetch('NODES', 1).to_i - end - def period @period ||= begin p = params.permit(:period_start, :period_end) From 11e2353c2fdebcbddb7a18ad02342852cd84ba11 Mon Sep 17 00:00:00 2001 From: fauno Date: Tue, 18 Jun 2024 15:14:57 +0000 Subject: [PATCH 088/161] ci: test [skip ci] From af2f97e56e74c1a945a37ed72a6ff92c9d1af8b0 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 18 Jun 2024 15:36:13 -0300 Subject: [PATCH 089/161] fix: avisar antes de salir si hubo cambios en el formulario --- .../controllers/unsaved_changes_controller.js | 45 +++++++++++++++++++ app/views/posts/_form.haml | 2 +- config/locales/en.yml | 1 + config/locales/es.yml | 1 + 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 app/javascript/controllers/unsaved_changes_controller.js diff --git a/app/javascript/controllers/unsaved_changes_controller.js b/app/javascript/controllers/unsaved_changes_controller.js new file mode 100644 index 00000000..04b80f56 --- /dev/null +++ b/app/javascript/controllers/unsaved_changes_controller.js @@ -0,0 +1,45 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + connect() { + this.originalFormData = new FormData(this.element); + this.originalFormDataSerialized = this.serializeFormData(this.originalFormData); + this.submitting = false; + } + + submit(event) { + this.submitting = true; + } + + unsaved(event) { + if (this.submitting) return; + if (!this.hasChanged()) return; + + this.submitting = false; + + event.preventDefault(); + + event.returnValue = true; + } + + unsavedTurbolinks(event) { + if (this.submitting) return; + if (!this.hasChanged()) return; + + this.submitting = false; + + if (window.confirm(this.element.dataset.unsavedChangesConfirmValue)) return; + + event.preventDefault(); + } + + serializeFormData(formData) { + formData.delete("authenticity_token"); + + return (new URLSearchParams(formData)).toString();; + } + + hasChanged() { + return (this.originalFormDataSerialized !== this.serializeFormData(new FormData(this.element))); + } +} diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index 8c006274..8a86d203 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -33,7 +33,7 @@ - dir = t("locales.#{@locale}.dir") -# Comienza el formulario -= form_tag url, method: method, class: "form post #{extra_class}", multipart: true do += form_tag url, method: method, class: "form post #{extra_class}", multipart: true, data: { controller: 'unsaved-changes', action: 'unsaved-changes#submit beforeunload@window->unsaved-changes#unsaved turbolinks:before-visit@window->unsaved-changes#unsavedTurbolinks', 'unsaved-changes-confirm-value': t('.confirm') } do -# Botones de guardado = render 'posts/submit', site: site, post: post diff --git a/config/locales/en.yml b/config/locales/en.yml index 457b7927..94a6968a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -814,6 +814,7 @@ en: destroy: Delete confirm_destroy: Are you sure? form: + confirm: "You have unsaved changes and changing pages may lose them, continue anyway?" errors: title: There are some errors on the form help: Please, verify that all values are correct. diff --git a/config/locales/es.yml b/config/locales/es.yml index d149dee8..fcaa6658 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -822,6 +822,7 @@ es: destroy: Borrar confirm_destroy: ¿Estás segure? form: + confirm: "Tenés cambios sin guardar y cambiar de página podría perderlos, ¿querés continuar de todas formas?" errors: title: Hay errores en el formulario help: Por favor, verifica que todos los valores sean correctos. From db07eefb92bb2e116e9ea37f54fac4d7c4e2acfb Mon Sep 17 00:00:00 2001 From: f Date: Tue, 18 Jun 2024 16:06:59 -0300 Subject: [PATCH 090/161] feat: poder pasar data a un checkbox --- app/views/bootstrap/_custom_checkbox.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/bootstrap/_custom_checkbox.haml b/app/views/bootstrap/_custom_checkbox.haml index ae35caf5..73910a6f 100644 --- a/app/views/bootstrap/_custom_checkbox.haml +++ b/app/views/bootstrap/_custom_checkbox.haml @@ -1,6 +1,6 @@ :ruby help_id = "#{id}_help" - checkbox_attributes = local_assigns.slice(:id, :type, :name, :value, :required, :checked) + checkbox_attributes = local_assigns.slice(:id, :type, :name, :value, :required, :checked, :data) checkbox_attributes[:type] ||= 'checkbox' .custom-control{ class: "custom-#{checkbox_attributes[:type]}" } From 5fc6118957ffb67d6adac462072a760ca7826e04 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 18 Jun 2024 16:07:22 -0300 Subject: [PATCH 091/161] feat: poder marcar un new_array como obligatorio --- .../required_checkbox_controller.js | 24 +++++++++++++++++++ app/views/posts/attributes/_new_array.haml | 14 +++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 app/javascript/controllers/required_checkbox_controller.js diff --git a/app/javascript/controllers/required_checkbox_controller.js b/app/javascript/controllers/required_checkbox_controller.js new file mode 100644 index 00000000..9754b506 --- /dev/null +++ b/app/javascript/controllers/required_checkbox_controller.js @@ -0,0 +1,24 @@ +import { Controller } from "stimulus"; + +/* + * Para poder indicar que al menos uno del grupo de checkboxes es + * obligatorio, marcamos uno como `required` (que es el que mostraría el + * error) y se lo quitamos cuando detectamos que alguno cambió. + */ +export default class extends Controller { + static targets = ["required", "checkbox"]; + + connect() { + } + + /* + * El grupo deja de ser obligatorio cuando al menos uno está activo. + */ + change(event = undefined) { + if (event.target.checked) { + this.requiredTarget.required = false; + } else { + this.requiredTarget.required = !Array.from(this.checkboxTargets).some(x => x.checked); + } + } +} diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index d0d659dd..4293ded1 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -5,13 +5,19 @@ name = "#{base}[#{attribute}][]" 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) } } +%div{ data: { controller: "modal array enter#{' required-checkbox' if metadata.required}", 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } %template{ data: { target: 'array.placeholder' } } .col.mb-3{ 'aria-hidden': 'true' } %span.placeholder.w-100 - .form-group - = hidden_field_tag name, '' + .form-group.mb-0 + -# + Si la lista es obligatoria, al menos uno de los ítems tiene que + estar activado. Logramos esto con un checkbox oculto que se marca + como obligatorio al validar el formulario. + - if metadata.required + %input.form-control{ type: 'checkbox', name: name, data: { target: 'required-checkbox.required' }, required: metadata.value.empty? } + .invalid-feedback Requerido! .d-flex.align-items-center.justify-content-between = label_tag id, post_label_t(attribute, post: post) = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' @@ -41,7 +47,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: random_id, 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, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } - content_for :"#{id}_footer" do .input-group.w-auto.flex-grow-1.my-0 From 4e473dd2cbb591d7c7049f766b8a824eedc6b839 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 13:12:54 -0300 Subject: [PATCH 092/161] fix: validar con stimulus --- .../controllers/form_validation_controller.js | 29 ++++++++++++++++ app/javascript/etc/index.js | 1 - app/javascript/etc/validation.js | 34 ------------------- app/views/posts/_form.haml | 2 +- 4 files changed, 30 insertions(+), 36 deletions(-) create mode 100644 app/javascript/controllers/form_validation_controller.js delete mode 100644 app/javascript/etc/validation.js diff --git a/app/javascript/controllers/form_validation_controller.js b/app/javascript/controllers/form_validation_controller.js new file mode 100644 index 00000000..fe65934e --- /dev/null +++ b/app/javascript/controllers/form_validation_controller.js @@ -0,0 +1,29 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + connect() { + this.element.setAttribute("novalidate", true); + + for (const input of this.element.elements) { + if (input.type === "button" || input.type === "submit") continue; + + if (input.dataset.action) { + input.dataset.action = `${input.dataset.action} htmx:validation:validate->form-validation#submit`; + } else { + input.dataset.action = "htmx:validation:validate->form-validation#submit"; + } + } + } + + submit(event = undefined) { + event?.preventDefault(); + event?.stopPropagation(); + + if (this.element.reportValidity()) { + this.element.classList.remove("was-validated"); + this.element.submit(); + } else { + this.element.classList.add("was-validated"); + } + } +} diff --git a/app/javascript/etc/index.js b/app/javascript/etc/index.js index 3a1ef75c..641d8085 100644 --- a/app/javascript/etc/index.js +++ b/app/javascript/etc/index.js @@ -4,6 +4,5 @@ import './input-tag' import './prosemirror' import './timezone' import './turbolinks-anchors' -import './validation' import './new_editor' import './htmx_abort' diff --git a/app/javascript/etc/validation.js b/app/javascript/etc/validation.js deleted file mode 100644 index 5a48148f..00000000 --- a/app/javascript/etc/validation.js +++ /dev/null @@ -1,34 +0,0 @@ -document.addEventListener('turbolinks:load', () => { - // Al enviar el formulario del artículo, aplicar la validación - // localmente y actualizar los comentarios para lectores de pantalla. - document.querySelectorAll('form').forEach(form => { - form.addEventListener('submit', event => { - const invalid_help = form.querySelectorAll('.invalid-help') - const sending_help = form.querySelectorAll('.sending-help') - - invalid_help.forEach(i => i.classList.add('d-none')) - sending_help.forEach(i => i.classList.add('d-none')) - - form.querySelectorAll('[aria-invalid="true"]').forEach(aria => { - aria.setAttribute('aria-invalid', false) - aria.setAttribute('aria-describedby', aria.parentElement.querySelector('.feedback').id) - }) - - if (form.checkValidity() === false) { - event.preventDefault() - event.stopPropagation() - - invalid_help.forEach(i => i.classList.remove('d-none')) - - form.querySelectorAll(':invalid').forEach(invalid => { - invalid.setAttribute('aria-invalid', true) - invalid.setAttribute('aria-describedby', invalid.parentElement.querySelector('.invalid-feedback').id) - }) - } else { - sending_help.forEach(i => i.classList.remove('d-none')) - } - - form.classList.add('was-validated') - }) - }) -}) diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index 8a86d203..aa8f3d1d 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -33,7 +33,7 @@ - dir = t("locales.#{@locale}.dir") -# Comienza el formulario -= form_tag url, method: method, class: "form post #{extra_class}", multipart: true, data: { controller: 'unsaved-changes', action: 'unsaved-changes#submit beforeunload@window->unsaved-changes#unsaved turbolinks:before-visit@window->unsaved-changes#unsavedTurbolinks', 'unsaved-changes-confirm-value': t('.confirm') } do += form_tag url, method: method, class: "form post #{extra_class}", multipart: true, data: { controller: 'unsaved-changes form-validation', action: 'unsaved-changes#submit form-validation#submit beforeunload@window->unsaved-changes#unsaved turbolinks:before-visit@window->unsaved-changes#unsavedTurbolinks', 'unsaved-changes-confirm-value': t('.confirm') } do -# Botones de guardado = render 'posts/submit', site: site, post: post From 4cdfbb7b2f48ccef20eca1cc2a683ff8a4217709 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 13:13:09 -0300 Subject: [PATCH 093/161] =?UTF-8?q?fix:=20validaci=C3=B3n=20en=20modo=20os?= =?UTF-8?q?curo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/application.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 4b659972..ce0b0924 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -95,6 +95,16 @@ $sizes: ( box-shadow: 0 0 0 0.2rem $cyan; } } + + @include form-validation-state("valid", $cyan, url("data:image/svg+xml,")); + + .custom-checkbox { + .custom-control-input:checked ~ .custom-control-label { + &::after { + background-image: url("data:image/svg+xml,"); + } + } + } } // TODO: Encontrar la forma de generar esto desde los locales de Rails From 2ec752ee1406769eaddb03f9aa569ec4e6c0a84f Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 13:13:30 -0300 Subject: [PATCH 094/161] =?UTF-8?q?feat:=20validaci=C3=B3n=20de=20new=5Far?= =?UTF-8?q?ray?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/required_checkbox_controller.js | 15 +++++++++++++++ app/views/posts/attributes/_new_array.haml | 9 +++++---- config/locales/en.yml | 1 + config/locales/es.yml | 1 + 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/javascript/controllers/required_checkbox_controller.js b/app/javascript/controllers/required_checkbox_controller.js index 9754b506..a24ce881 100644 --- a/app/javascript/controllers/required_checkbox_controller.js +++ b/app/javascript/controllers/required_checkbox_controller.js @@ -20,5 +20,20 @@ export default class extends Controller { } else { this.requiredTarget.required = !Array.from(this.checkboxTargets).some(x => x.checked); } + + for (const checkbox of this.checkboxTargets) { + if (checkbox === event.target) continue; + + checkbox.required = !event.target.checked; + } + } + + /* + * Si el checkbox es considerado + */ + invalid(event) { + for (const checkbox of this.checkboxTargets) { + checkbox.required = true; + } } } diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 4293ded1..5778f619 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -15,11 +15,12 @@ Si la lista es obligatoria, al menos uno de los ítems tiene que estar activado. Logramos esto con un checkbox oculto que se marca como obligatorio al validar el formulario. - - if metadata.required - %input.form-control{ type: 'checkbox', name: name, data: { target: 'required-checkbox.required' }, required: metadata.value.empty? } - .invalid-feedback Requerido! .d-flex.align-items-center.justify-content-between - = label_tag id, post_label_t(attribute, post: post) + %div + = label_tag id, post_label_t(attribute, post: post), class: 'mb-0' + - if metadata.required + %input.form-control.d-none{ type: 'checkbox', name: name, data: { target: 'required-checkbox.required', action: 'invalid->required-checkbox#invalid' }, required: metadata.value.empty? } + .invalid-feedback.mt-0= t('.required') = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' -# Mostramos la lista de valores actuales. diff --git a/config/locales/en.yml b/config/locales/en.yml index 94a6968a..6b8562d1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -764,6 +764,7 @@ en: edit: "Edit" new_array: edit: "Edit" + required: "Please select at least one option." new_has_one: edit: "Edit" new_belongs_to: diff --git a/config/locales/es.yml b/config/locales/es.yml index fcaa6658..d1c61fd2 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -772,6 +772,7 @@ es: edit: "Editar" new_array: edit: "Editar" + required: "Seleccioná al menos una opción." new_has_one: edit: "Editar" new_belongs_to: From 084cf69f93f6feafd9e42ac13c87d49bd4eda3df Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 13:20:18 -0300 Subject: [PATCH 095/161] fix: mostrar la ayuda del campo --- app/views/posts/attributes/_new_array.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 5778f619..86a8f275 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -40,7 +40,8 @@ = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'], keydown_actions: %w[keydown->array#cancelWithEscape] do - content_for :"#{id}_header" do .form-group.flex-grow-1.mb-0 - = label_tag id, post_label_t(attribute, post: post) + = label_tag id, post_label_t(attribute, post: post), class: 'mb-0' + %small.feedback.form-text.text-muted.mt-0.mb-1= post_help_t(metadata.name, post: post) %input.form-control{ data: { target: 'array.search', action: 'input->array#search keydown->enter#prevent' }, type: 'search', placeholder: t('.filter') } - content_for :"#{id}_body" do From 654fff0ab9c641a507e28269a15c51e46c61c7b5 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 14:28:53 -0300 Subject: [PATCH 096/161] fix: volver a mostrar los mensajes al guardar y validar --- .../controllers/form_validation_controller.js | 23 +++++++++++++++++++ app/views/bootstrap/_alert.haml | 2 +- app/views/posts/_submit.haml | 17 ++++++++------ config/locales/en.yml | 4 ++-- config/locales/es.yml | 4 ++-- 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/app/javascript/controllers/form_validation_controller.js b/app/javascript/controllers/form_validation_controller.js index fe65934e..c9817e27 100644 --- a/app/javascript/controllers/form_validation_controller.js +++ b/app/javascript/controllers/form_validation_controller.js @@ -1,6 +1,8 @@ import { Controller } from "stimulus"; export default class extends Controller { + static targets = ["invalid", "submitting"]; + connect() { this.element.setAttribute("novalidate", true); @@ -22,8 +24,29 @@ export default class extends Controller { if (this.element.reportValidity()) { this.element.classList.remove("was-validated"); this.element.submit(); + + this.show(this.submittingTargets); + this.hide(this.invalidTargets); } else { this.element.classList.add("was-validated"); + this.hide(this.submittingTargets); + this.show(this.invalidTargets); + } + } + + show(elements) { + for (const element of elements) { + element.classList.remove("d-none"); + + setTimeout(() => element.classList.add("show"), 1); + } + } + + hide(elements) { + for (const element of elements) { + element.classList.remove("show"); + + setTimeout(() => element.classList.add("d-none"), 2000); } } } diff --git a/app/views/bootstrap/_alert.haml b/app/views/bootstrap/_alert.haml index 85bcbe84..a14064ce 100644 --- a/app/views/bootstrap/_alert.haml +++ b/app/views/bootstrap/_alert.haml @@ -1,2 +1,2 @@ -.alert.alert-primary.mx-auto.content.max-w-md-70ch{ role: 'alert', class: local_assigns[:class] } +.alert.alert-primary.mx-auto.content.max-w-md-70ch{ role: 'alert', **local_assigns } = yield diff --git a/app/views/posts/_submit.haml b/app/views/posts/_submit.haml index c6c0a68a..c3064c53 100644 --- a/app/views/posts/_submit.haml +++ b/app/views/posts/_submit.haml @@ -1,8 +1,11 @@ - invalid_help = site.config.fetch('invalid_help', t('.invalid_help')) -- sending_help = site.config.fetch('sending_help', t('.sending_help')) -.form-group - = submit_tag t('.save'), class: 'btn btn-secondary submit-post' - = render 'bootstrap/alert', class: 'invalid-help d-none' do - = invalid_help - = render 'bootstrap/alert', class: 'sending-help d-none' do - = sending_help +- submitting_help = site.config.fetch('submitting_help', t('.submitting_help')) + +.d-flex.flex-column.flex-md-row.align-items-start.mb-3 + %div + = submit_tag t('.save'), class: 'btn btn-secondary submit-post' + .d-flex.flex-column.position-relative + = render 'bootstrap/alert', class: 'm-0 d-none fade', data: { target: 'form-validation.invalid' } do + = invalid_help + = render 'bootstrap/alert', class: 'm-0 d-none fade position-absolute top-0 left-0', data: { target: 'form-validation.submitting' } do + = submitting_help diff --git a/config/locales/en.yml b/config/locales/en.yml index 6b8562d1..3815f5de 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -719,8 +719,8 @@ en: submit: save: 'Save' save_draft: 'Save as draft' - invalid_help: 'Some fields need attention! Please search for the fields marked as invalid.' - sending_help: Saving, please wait... + invalid_help: "Some fields need attention! Please search for the fields marked as not valid." + submitting_help: "Saving changes, please wait..." new_array: remove: "Remove" attributes: diff --git a/config/locales/es.yml b/config/locales/es.yml index d1c61fd2..7b981b1d 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -727,8 +727,8 @@ es: submit: save: 'Guardar' save_draft: 'Guardar como borrador' - invalid_help: '¡Te faltan completar algunos campos! Busca los que estén marcados como inválidos' - sending_help: Guardando, por favor espera... + invalid_help: "¡Te falta completar algunos campos! Busca los que estén marcados como no válidos." + submitting_help: "Guardando, por favor espera..." new_array: remove: "Eliminar" attributes: From 5106cdd414b05d1e81689e3fb8efb5fda78b4d01 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 18:16:49 -0300 Subject: [PATCH 097/161] =?UTF-8?q?fix:=20m=C3=A1s=20arreglos=20tema=20osc?= =?UTF-8?q?uro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/application.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index ce0b0924..32530ce5 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -26,6 +26,10 @@ $card-bg: var(--background); $card-border-color: var(--card-border-color); $input-bg: var(--background); $input-color: var(--foreground); +$btn-bg-color: var(--btn-bg-color); +$btn-color: var(--btn-color); +$input-group-addon-bg: var(--btn-bg-color); +$custom-file-color: var(--btn-color); $spacers: ( 2-plus: 0.75rem From 6f30727a7b7d0026c24c64d28e59294316f96b03 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 18:16:58 -0300 Subject: [PATCH 098/161] fix: traducir campo de imagen --- app/assets/stylesheets/application.scss | 14 ++++++++------ app/views/posts/attributes/_file.haml | 1 + app/views/posts/attributes/_image.haml | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 32530ce5..d8fe8c9f 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,6 +11,14 @@ $colors: ( "magenta": $magenta ); +// TODO: Encontrar la forma de generar esto desde los locales de Rails +$custom-file-text: ( + en: "Browse", + es: "Buscar archivo", + pt: "Buscar arquivo", + pt-BR: "Buscar arquivo" +); + // Redefinir variables de Bootstrap $primary: $magenta; $secondary: $black; @@ -111,12 +119,6 @@ $sizes: ( } } -// TODO: Encontrar la forma de generar esto desde los locales de Rails -$custom-file-text: ( - en: 'Browse', - es: 'Buscar archivo' -); - @font-face { font-family: 'Saira'; font-style: normal; diff --git a/app/views/posts/attributes/_file.haml b/app/views/posts/attributes/_file.haml index 20c27399..007f6b9f 100644 --- a/app/views/posts/attributes/_file.haml +++ b/app/views/posts/attributes/_file.haml @@ -26,6 +26,7 @@ = file_field(*field_name_for(base, attribute, :path), **field_options(attribute, metadata, required: (metadata.required && !metadata.path?)), class: "custom-file-input #{invalid(post, attribute)}", + lang: locale, data: { target: 'file-preview.input', action: 'file-preview#update' }) = label_tag "#{base}_#{attribute}_path", post_label_t(attribute, :path, post: post), class: 'custom-file-label' diff --git a/app/views/posts/attributes/_image.haml b/app/views/posts/attributes/_image.haml index 241a78e8..03d9d15f 100644 --- a/app/views/posts/attributes/_image.haml +++ b/app/views/posts/attributes/_image.haml @@ -23,6 +23,7 @@ **field_options(attribute, metadata, required: (metadata.required && !metadata.path?)), class: "custom-file-input #{invalid(post, attribute)}", accept: ActiveStorage.web_image_content_types.join(','), + lang: locale, data: { target: 'file-preview.input', action: 'file-preview#update' }) = label_tag "#{base}_#{attribute}_path", post_label_t(attribute, :path, post: post), class: 'custom-file-label' From e2af1f215ac389e6ae87e1d4f3c5546992adfc8f Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 18:19:07 -0300 Subject: [PATCH 099/161] feat: las notificaciones se manejan con eventos --- .../controllers/form_validation_controller.js | 37 +++++++--------- .../controllers/notification_controller.js | 43 +++++++++++++++++++ app/views/posts/_form.haml | 14 ++++-- app/views/posts/_submit.haml | 12 +----- app/views/posts/_validation.haml | 16 +++++++ config/locales/en.yml | 5 ++- config/locales/es.yml | 7 ++- 7 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 app/javascript/controllers/notification_controller.js create mode 100644 app/views/posts/_validation.haml diff --git a/app/javascript/controllers/form_validation_controller.js b/app/javascript/controllers/form_validation_controller.js index c9817e27..85e7bb86 100644 --- a/app/javascript/controllers/form_validation_controller.js +++ b/app/javascript/controllers/form_validation_controller.js @@ -3,6 +3,16 @@ import { Controller } from "stimulus"; export default class extends Controller { static targets = ["invalid", "submitting"]; + // @todo Stimulus >1 + get submittingIdValue() { + return this.element.dataset?.formValidationSubmittingIdValue; + } + + // @todo Stimulus >1 + get invalidIdValue() { + return this.element.dataset?.formValidationInvalidIdValue; + } + connect() { this.element.setAttribute("novalidate", true); @@ -19,34 +29,19 @@ export default class extends Controller { submit(event = undefined) { event?.preventDefault(); - event?.stopPropagation(); if (this.element.reportValidity()) { this.element.classList.remove("was-validated"); - this.element.submit(); - this.show(this.submittingTargets); - this.hide(this.invalidTargets); + if (!this.element.hasAttribute("hx-post")) this.element.submit(); + + window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.submittingIdValue } })); } else { + event?.stopPropagation(); + this.element.classList.add("was-validated"); - this.hide(this.submittingTargets); - this.show(this.invalidTargets); - } - } - show(elements) { - for (const element of elements) { - element.classList.remove("d-none"); - - setTimeout(() => element.classList.add("show"), 1); - } - } - - hide(elements) { - for (const element of elements) { - element.classList.remove("show"); - - setTimeout(() => element.classList.add("d-none"), 2000); + window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.invalidIdValue } })); } } } diff --git a/app/javascript/controllers/notification_controller.js b/app/javascript/controllers/notification_controller.js new file mode 100644 index 00000000..7fbe3b5a --- /dev/null +++ b/app/javascript/controllers/notification_controller.js @@ -0,0 +1,43 @@ +import { Controller } from "stimulus"; + +/* + * Solo se puede mostrar una notificación a la vez + */ +export default class extends Controller { + // @todo Stimulus >1 + get showClasses() { + return (this.element.dataset?.notificationShowClass || "").split(" ").filter(x => x); + } + + // @todo Stimulus >1 + get hideClasses() { + return (this.element.dataset?.notificationHideClass || "").split(" ").filter(x => x); + } + + /* + * Al recibir el evento de mostrar, si no está dirigido al elemento + * actual, se oculta. + */ + show(event = undefined) { + if (event?.detail?.id !== this.element.id) { + this.hide({ detail: { id: this.element.id } }); + return; + } + + this.element.classList.remove("d-none"); + + setTimeout(() => { + this.element.classList.remove(...this.hideClasses); + this.element.classList.add(...this.showClasses); + }, 1); + } + + hide(event = undefined) { + if (event?.detail?.id !== this.element.id) return; + + this.element.classList.remove(...this.showClasses); + this.element.classList.add(...this.hideClasses); + + setTimeout(() => this.element.classList.add("d-none"), 150); + } +} diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index aa8f3d1d..3e09cb72 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -31,11 +31,19 @@ end - dir = t("locales.#{@locale}.dir") +- submitting_id = random_id +- invalid_id = random_id +- data = {} +- data[:controller] = 'unsaved-changes form-validation' +- data[:action] = 'unsaved-changes#submit form-validation#submit beforeunload@window->unsaved-changes#unsaved turbolinks:before-visit@window->unsaved-changes#unsavedTurbolinks' +- data[:'unsaved-changes-confirm-value'] = t('.confirm') +- data[:'form-validation-submitting-id-value'] = submitting_id +- data[:'form-validation-invalid-id-value'] = invalid_id -# Comienza el formulario -= form_tag url, method: method, class: "form post #{extra_class}", multipart: true, data: { controller: 'unsaved-changes form-validation', action: 'unsaved-changes#submit form-validation#submit beforeunload@window->unsaved-changes#unsaved turbolinks:before-visit@window->unsaved-changes#unsavedTurbolinks', 'unsaved-changes-confirm-value': t('.confirm') } do += form_tag url, method: method, class: "form post #{extra_class}", multipart: true, data: data do -# Botones de guardado - = render 'posts/submit', site: site, post: post + = render 'posts/submit', site: site, post: post, invalid: invalid_id, submitting: submitting_id = hidden_field_tag 'post[layout]', post.layout.name @@ -43,7 +51,7 @@ = render 'posts/attributes', site: site, post: post, dir: dir, base: 'post', locale: @locale -# Botones de guardado - = render 'posts/submit', site: site, post: post + = render 'posts/submit', site: site, post: post, invalid: invalid_id, submitting: submitting_id -# Formularios usados por los modales = yield(:post_form) diff --git a/app/views/posts/_submit.haml b/app/views/posts/_submit.haml index c3064c53..41d6f420 100644 --- a/app/views/posts/_submit.haml +++ b/app/views/posts/_submit.haml @@ -1,11 +1,3 @@ -- invalid_help = site.config.fetch('invalid_help', t('.invalid_help')) -- submitting_help = site.config.fetch('submitting_help', t('.submitting_help')) - .d-flex.flex-column.flex-md-row.align-items-start.mb-3 - %div - = submit_tag t('.save'), class: 'btn btn-secondary submit-post' - .d-flex.flex-column.position-relative - = render 'bootstrap/alert', class: 'm-0 d-none fade', data: { target: 'form-validation.invalid' } do - = invalid_help - = render 'bootstrap/alert', class: 'm-0 d-none fade position-absolute top-0 left-0', data: { target: 'form-validation.submitting' } do - = submitting_help + %div= submit_tag t('.save'), class: 'btn btn-secondary submit-post' + = render 'posts/validation', site: site, submitting: { id: submitting }, invalid: { id: invalid } diff --git a/app/views/posts/_validation.haml b/app/views/posts/_validation.haml new file mode 100644 index 00000000..c28a743a --- /dev/null +++ b/app/views/posts/_validation.haml @@ -0,0 +1,16 @@ +- invalid = site.config.fetch('invalid', t('.invalid')) +- submitting = site.config.fetch('submitting', t('.submitting')) +- %i[invalid submitting].each do |key| + - local_assigns[key] ||= {} + - local_assigns[key][:data] ||= {} + - local_assigns[key][:data][:target] ||= "form-validation.#{key}" + - local_assigns[key][:data][:action] ||= 'notification:show@window->notification#show' + - local_assigns[key][:data][:controller] ||= 'notification' + - local_assigns[key][:data][:'notification-hide-class'] ||= 'hide' + - local_assigns[key][:data][:'notification-show-class'] ||= 'show' + +.d-flex.flex-column + = render 'bootstrap/alert', class: 'm-0 d-none fade', **local_assigns[:invalid] do + = invalid + = render 'bootstrap/alert', class: 'm-0 d-none fade', **local_assigns[:submitting] do + = submitting diff --git a/config/locales/en.yml b/config/locales/en.yml index 3815f5de..c0ad36d2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -719,8 +719,9 @@ en: submit: save: 'Save' save_draft: 'Save as draft' - invalid_help: "Some fields need attention! Please search for the fields marked as not valid." - submitting_help: "Saving changes, please wait..." + validation: + invalid: "Some fields need attention! Please search for the fields marked as not valid." + submitting: "Saving changes, please wait..." new_array: remove: "Remove" attributes: diff --git a/config/locales/es.yml b/config/locales/es.yml index 7b981b1d..1fd397e4 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -727,8 +727,9 @@ es: submit: save: 'Guardar' save_draft: 'Guardar como borrador' - invalid_help: "¡Te falta completar algunos campos! Busca los que estén marcados como no válidos." - submitting_help: "Guardando, por favor espera..." + validation: + invalid: "¡Te falta completar algunos campos! Busca los que estén marcados como no válidos." + submitting: "Guardando, por favor espera..." new_array: remove: "Eliminar" attributes: @@ -770,6 +771,8 @@ es: edit: "Editar" new_predefined_array: edit: "Editar" + new_predefined_value: + edit: "Editar" new_array: edit: "Editar" required: "Seleccioná al menos una opción." From 798bb992dc41423b0241510d4614fb39ae7a609f Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 18:20:05 -0300 Subject: [PATCH 100/161] feat: validar formularios htmx --- app/views/posts/_htmx_form.haml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index f4f4a845..7a23445b 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -27,7 +27,12 @@ multipart: true, class: 'form post ', 'hx-swap': params.require(:swap), - 'hx-target': "##{params.require(:target)}" + 'hx-target': "##{params.require(:target)}", + 'hx-validate': true, + data: { + controller: 'form-validation', + action: 'form-validation#submit' + } } if post.new? From 34aa8822f29254e21bf316f092fe578a665367b0 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 19 Jun 2024 18:21:44 -0300 Subject: [PATCH 101/161] feat: avisar que se guardaron los cambios --- app/controllers/posts_controller.rb | 8 ++++++-- app/views/posts/_htmx_form.haml | 6 +++++- app/views/posts/attributes/_new_has_one.haml | 8 +++++++- config/locales/en.yml | 2 ++ config/locales/es.yml | 2 ++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 0f3c24d5..70ba2e54 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -128,7 +128,9 @@ class PostsController < ApplicationController # condiciones. if htmx? if post.persisted? - swap_modals + triggers = { 'notification:show' => { 'id' => params.permit(:saved).values.first } } + + swap_modals(triggers) @value = post.title.value @uuid = post.uuid.value @@ -169,7 +171,9 @@ class PostsController < ApplicationController if htmx? if post.persisted? - swap_modals + triggers = { 'notification:show' => params.permit(:saved).values.first } + + swap_modals(triggers) @value = post.title.value @uuid = post.uuid.value diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index 7a23445b..1a8c0597 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -31,7 +31,9 @@ 'hx-validate': true, data: { controller: 'form-validation', - action: 'form-validation#submit' + action: 'form-validation#submit', + 'form-validation-submitting-id-value': params.permit(:submitting).values.first, + 'form-validation-invalid-id-value': params.permit(:invalid).values.first, } } @@ -78,6 +80,8 @@ %input{ type: 'hidden', name: 'swap', value: params.require(:swap) } - if params[:inverse].present? %input{ type: 'hidden', name: 'inverse', value: params.require(:inverse) } + - if params[:saved].present? + %input{ type: 'hidden', name: 'saved', value: params.require(:saved) } = hidden_field_tag "#{base}[layout]", post.layout.name diff --git a/app/views/posts/attributes/_new_has_one.haml b/app/views/posts/attributes/_new_has_one.haml index 58e098b6..87ef0440 100644 --- a/app/views/posts/attributes/_new_has_one.haml +++ b/app/views/posts/attributes/_new_has_one.haml @@ -19,6 +19,9 @@ post_form_loaded_id = random_id value_list_id = random_id layout = metadata.filter[:layout] + invalid_id = random_id + submitting_id = random_id + saved_id = random_id %div{ data: { controller: 'modal' }} .form-group @@ -43,10 +46,13 @@ = 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' } + .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, invalid: invalid_id, submitting: submitting_id, saved: saved_id), 'hx-trigger': 'load' } %span.placeholder.w-100.h-100 - content_for :"#{id}_footer" do + = render 'posts/validation', site: site, invalid: { id: invalid_id }, submitting: { id: submitting_id } + = render 'bootstrap/alert', class: 'm-0 d-none fade', id: saved_id, data: { controller: 'notification', action: 'notification:show@window->notification#show', 'notification-hide-class': 'hide', 'notification-show-class': 'show' } do + = t('.saved') = render 'bootstrap/btn', form: form_id, content: t('.save'), type: 'submit', class: 'm-0 mt-1 mr-1' = render 'bootstrap/btn', content: t('.close'), action: 'modal#hide', class: 'm-0 mt-1 mr-1' diff --git a/config/locales/en.yml b/config/locales/en.yml index c0ad36d2..52f002e6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -965,3 +965,5 @@ en: save: "Save" card: edit: "Edit" + alert: + saved: "Changes were saved!" diff --git a/config/locales/es.yml b/config/locales/es.yml index 1fd397e4..8fe09758 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -975,3 +975,5 @@ es: save: "Guardar" card: edit: "Editar" + alert: + saved: "¡Cambios guardados!" From 19e8933388e48b7d7b23ced66eebe646b765efe2 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 20 Jun 2024 16:15:33 -0300 Subject: [PATCH 102/161] feat: campos obligatorios --- app/views/posts/_required_checkbox.haml | 19 +++++++++++++++++++ app/views/posts/attributes/_new_array.haml | 8 ++++---- .../posts/attributes/_new_belongs_to.haml | 8 ++++++-- .../_new_has_and_belongs_to_many.haml | 11 ++++++++--- app/views/posts/attributes/_new_has_many.haml | 11 ++++++++--- .../attributes/_new_predefined_array.haml | 10 +++++++--- .../attributes/_new_predefined_value.haml | 8 ++++++-- 7 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 app/views/posts/_required_checkbox.haml diff --git a/app/views/posts/_required_checkbox.haml b/app/views/posts/_required_checkbox.haml new file mode 100644 index 00000000..22f186a6 --- /dev/null +++ b/app/views/posts/_required_checkbox.haml @@ -0,0 +1,19 @@ +-# + Para el controlador required-checkbox necesitamos un checkbox oculto + que es obligatorio según si alguno de los checkboxes reales está + seleccionado o no. Al ser obligatorio, va a tener feedback de + validación. Sin embargo, como está oculto, no podemos mostrar el + mensaje de validación nativo del navegador. + + @param :required [Boolean] + @param :name [String,Symbol] + @param :initial [Boolean] + @param :feedback [String] + @param :type [String] + +- if required + - local_assigns[:feedback] ||= t('.required') + - local_assigns[:type] ||= 'checkbox' + + %input.form-control.d-none{ type: local_assigns[:type], name: name, data: { target: 'required-checkbox.required', action: 'invalid->required-checkbox#invalid' }, required: initial } + .invalid-feedback.mt-0= local_assigns[:feedback] diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index 86a8f275..e73560c8 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -4,8 +4,10 @@ id = id_for(base, attribute) name = "#{base}[#{attribute}][]" form_id = random_id + controllers = %w[modal array enter] + controllers << 'required-checkbox' if metadata.required -%div{ data: { controller: "modal array enter#{' required-checkbox' if metadata.required}", 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } +%div{ data: { controller: controllers.join(' '), 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } %template{ data: { target: 'array.placeholder' } } .col.mb-3{ 'aria-hidden': 'true' } %span.placeholder.w-100 @@ -18,9 +20,7 @@ .d-flex.align-items-center.justify-content-between %div = label_tag id, post_label_t(attribute, post: post), class: 'mb-0' - - if metadata.required - %input.form-control.d-none{ type: 'checkbox', name: name, data: { target: 'required-checkbox.required', action: 'invalid->required-checkbox#invalid' }, required: metadata.value.empty? } - .invalid-feedback.mt-0= t('.required') + = render 'posts/required_checkbox', required: metadata.required, name: name, initial: metadata.empty? = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' -# Mostramos la lista de valores actuales. diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index 5c8844a5..c39176e0 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -17,8 +17,10 @@ post_modal_id = random_id post_form_loaded_id = random_id value_list_id = random_id + controllers = %w[modal array] + controllers << 'required-checkbox' if metadata.required -%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) } } +%div{ id: modal_id, data: { controller: controllers.join(' '), 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_related_post_path(site) } } %template{ data: { target: 'array.placeholder' } } .col.p-3{ 'aria-hidden': 'true' } %span.placeholder.w-100 @@ -26,7 +28,9 @@ .form-group = hidden_field_tag name, '' .d-flex.align-items-center.justify-content-between - = label_tag id, post_label_t(attribute, post: post) + %div + = label_tag id, post_label_t(attribute, post: post), class: 'mb-0' + = render 'posts/required_checkbox', required: metadata.required, name: name, initial: metadata.empty?, type: 'radio' = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' -# Mostramos la lista de valores actuales. 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 8b615173..4ca444cf 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 @@ -17,8 +17,10 @@ post_modal_id = random_id post_form_loaded_id = random_id value_list_id = random_id + controllers = %w[modal array] + controllers << 'required-checkbox' if metadata.required -%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) } } +%div{ id: modal_id, data: { controller: controllers.join(' '), 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_related_post_path(site) } } %template{ data: { target: 'array.placeholder' } } .col.p-3{ 'aria-hidden': 'true' } %span.placeholder.w-100 @@ -26,8 +28,11 @@ .form-group = hidden_field_tag name, '' .d-flex.align-items-center.justify-content-between - = label_tag id, post_label_t(attribute, post: post) + %div + = label_tag id, post_label_t(attribute, post: post) + = render 'posts/required_checkbox', required: metadata.required, name: name, initial: metadata.empty? = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + -# Mostramos la lista de valores actuales. Al aceptar el modal, se vacía el listado y se completa en base a @@ -55,7 +60,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: random_id, 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, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } -# 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 d211d864..476f2c2f 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -17,8 +17,10 @@ post_modal_id = random_id post_form_loaded_id = random_id value_list_id = random_id + controllers = %w[modal array] + controllers << 'required-checkbox' if metadata.required -%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) } } +%div{ id: modal_id, data: { controller: controllers.join(' '), 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_related_post_path(site) } } %template{ data: { target: 'array.placeholder' } } .col.p-3{ 'aria-hidden': 'true' } %span.placeholder.w-100 @@ -26,8 +28,11 @@ .form-group = hidden_field_tag name, '' .d-flex.align-items-center.justify-content-between - = label_tag id, post_label_t(attribute, post: post) + %div + = label_tag id, post_label_t(attribute, post: post) + = render 'posts/required_checkbox', required: metadata.required, name: name, initial: metadata.empty? = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' + -# Mostramos la lista de valores actuales. Al aceptar el modal, se vacía el listado y se completa en base a @@ -55,7 +60,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: random_id, 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, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } -# 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 ca0cdaf2..d5f2110e 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -6,8 +6,10 @@ id = id_for(base, attribute) name = "#{base}[#{attribute}][]" form_id = random_id + controllers = %w[modal array] + controllers << 'required-checkbox' if metadata.required -%div{ data: { controller: 'modal array', 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } +%div{ data: { controller: controllers.join(' '), 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } %template{ data: { target: 'array.placeholder' } } .col.mb-3{ 'aria-hidden': 'true' } %span.placeholder.w-100 @@ -15,7 +17,9 @@ .form-group = hidden_field_tag name, '' .d-flex.align-items-center.justify-content-between - = label_tag id, post_label_t(attribute, post: post) + %div + = label_tag id, post_label_t(attribute, post: post) + = render 'posts/required_checkbox', required: metadata.required, name: name, initial: metadata.empty? = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' -# Mostramos la lista de valores actuales. @@ -44,7 +48,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: random_id, 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, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } - 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 ae04fd0b..dad716b0 100644 --- a/app/views/posts/attributes/_new_predefined_value.haml +++ b/app/views/posts/attributes/_new_predefined_value.haml @@ -17,8 +17,10 @@ post_modal_id = random_id post_form_loaded_id = random_id value_list_id = random_id + controllers = %w[modal array] + controllers << 'required-checkbox' if metadata.required -%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) } } +%div{ id: modal_id, data: { controller: controllers.join(' '), 'array-original-value': metadata.value.to_json, 'array-new-array-value': site_posts_new_array_value_path(site) } } %template{ data: { target: 'array.placeholder' } } .col.p-3{ 'aria-hidden': 'true' } %span.placeholder.w-100 @@ -26,7 +28,9 @@ .form-group = hidden_field_tag name, '' .d-flex.align-items-center.justify-content-between - = label_tag id, post_label_t(attribute, post: post) + %div + = label_tag id, post_label_t(attribute, post: post) + = render 'posts/required_checkbox', required: metadata.required, name: name, initial: metadata.empty?, type: 'radio' = render 'bootstrap/btn', content: t('.edit'), action: 'modal#show' -# Mostramos la lista de valores actuales. From 270684485c43f614f2c4dd87bc14b421882598f3 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 20 Jun 2024 16:19:40 -0300 Subject: [PATCH 103/161] fix: no ordenar los valores --- app/views/posts/attributes/_new_array.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index e73560c8..b8607627 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -34,7 +34,7 @@ Para poder cancelar, mantenemos el estado original y desactivamos o activamos los ítemes según estén incluidos en esa lista o no. %ul.placeholder-glow{ data: { target: 'array.current' } } - - metadata.value.sort_by(&:remove_diacritics).each do |value| + - metadata.value.each do |value| = render 'posts/new_array_value', value: value = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'], keydown_actions: %w[keydown->array#cancelWithEscape] do @@ -46,8 +46,8 @@ - content_for :"#{id}_body" do .form-group.mb-0{ id: "#{id}_body" } - -# Eliminamos las tildes para poder buscar independientemente de cómo se escriba - - metadata.values.sort_by(&:remove_diacritics).each do |value| + -# Eliminamos las tildes para poder buscar independientemente de cómo se escriba. + - metadata.values.each do |value| .mb-2{ data: { target: 'array.item', 'searchable-value': value.remove_diacritics.downcase, value: value } } = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: value, checked: metadata.value.include?(value), content: value, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } From be59478c02b4101988da2392e2c3b0b7d04d792a Mon Sep 17 00:00:00 2001 From: f Date: Mon, 24 Jun 2024 17:31:29 -0300 Subject: [PATCH 104/161] =?UTF-8?q?fix:=20no=20enviar=20el=20formulario=20?= =?UTF-8?q?si=20tiene=20alg=C3=BAn=20atributo=20de=20htmx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/controllers/form_validation_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/controllers/form_validation_controller.js b/app/javascript/controllers/form_validation_controller.js index 85e7bb86..862ca9f3 100644 --- a/app/javascript/controllers/form_validation_controller.js +++ b/app/javascript/controllers/form_validation_controller.js @@ -33,7 +33,7 @@ export default class extends Controller { if (this.element.reportValidity()) { this.element.classList.remove("was-validated"); - if (!this.element.hasAttribute("hx-post")) this.element.submit(); + if (!this.element.getAttributeNames().some(x => x.startsWith("hx-"))) this.element.submit(); window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.submittingIdValue } })); } else { From ce3ab24dc09c73d3aa096bd724e27f4d1e255dab Mon Sep 17 00:00:00 2001 From: f Date: Mon, 24 Jun 2024 17:47:08 -0300 Subject: [PATCH 105/161] =?UTF-8?q?fixup!=20fix:=20no=20enviar=20el=20form?= =?UTF-8?q?ulario=20si=20tiene=20alg=C3=BAn=20atributo=20de=20htmx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/controllers/form_validation_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/controllers/form_validation_controller.js b/app/javascript/controllers/form_validation_controller.js index 862ca9f3..8756e272 100644 --- a/app/javascript/controllers/form_validation_controller.js +++ b/app/javascript/controllers/form_validation_controller.js @@ -33,7 +33,7 @@ export default class extends Controller { if (this.element.reportValidity()) { this.element.classList.remove("was-validated"); - if (!this.element.getAttributeNames().some(x => x.startsWith("hx-"))) this.element.submit(); + if (this.element.getAttributeNames().some(x => x.startsWith("hx-"))) this.element.submit(); window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.submittingIdValue } })); } else { From 99e965d4da09d108e0cb1c2d48f78135d2202248 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 25 Jun 2024 14:24:12 -0300 Subject: [PATCH 106/161] =?UTF-8?q?fixup!=20fixup!=20fix:=20no=20enviar=20?= =?UTF-8?q?el=20formulario=20si=20tiene=20alg=C3=BAn=20atributo=20de=20htm?= =?UTF-8?q?x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/controllers/form_validation_controller.js | 8 +++++++- app/javascript/packs/application.js | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/javascript/controllers/form_validation_controller.js b/app/javascript/controllers/form_validation_controller.js index 8756e272..dd1a98ed 100644 --- a/app/javascript/controllers/form_validation_controller.js +++ b/app/javascript/controllers/form_validation_controller.js @@ -28,12 +28,16 @@ export default class extends Controller { } submit(event = undefined) { + if (this.submitting) return; + + this.submitting = true; + event?.preventDefault(); if (this.element.reportValidity()) { this.element.classList.remove("was-validated"); - if (this.element.getAttributeNames().some(x => x.startsWith("hx-"))) this.element.submit(); + if (!this.element.getAttributeNames().some(x => x.startsWith("hx-"))) this.element.submit(); window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.submittingIdValue } })); } else { @@ -43,5 +47,7 @@ export default class extends Controller { window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.invalidIdValue } })); } + + this.submitting = false; } } diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index dc87b1c3..a0f18024 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -41,5 +41,5 @@ Rails.start() Turbolinks.start() ActiveStorage.start() -window.htmx = require("@suttyweb/htmx.org/dist/htmx.js"); +window.htmx = require("@suttyweb/htmx.org/dist/htmx.cjs.js"); window.htmx.config.selfRequestsOnly = true; diff --git a/package.json b/package.json index 73cfd589..c5642236 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@rails/ujs": "^6.1.3-1", "@rails/webpacker": "5.4.4", "@suttyweb/editor": "^0.1.29", - "@suttyweb/htmx.org": "^1.9.13", + "@suttyweb/htmx.org": "2.0.0", "babel-loader": "^8.2.2", "bs-custom-file-input": "^1.3.4", "chart.js": "^3.5.1", diff --git a/yarn.lock b/yarn.lock index d181bebe..45ceae0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1984,10 +1984,10 @@ linkifyjs "^4.1.1" prosemirror-svelte-nodeview "^1.0.2" -"@suttyweb/htmx.org@^1.9.13": - version "1.9.13" - resolved "https://registry.yarnpkg.com/@suttyweb/htmx.org/-/htmx.org-1.9.13.tgz#bc67a5e2947d7cc125649b829610da8ee61f21af" - integrity sha512-/2x3AGXT2CFOmp8Nf59XY2brah5wSo4YvcctYA2zD4BByNft4XE0rUk4VM7TjPxR1LOzg6VK1crZmdiy3JyzxQ== +"@suttyweb/htmx.org@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@suttyweb/htmx.org/-/htmx.org-2.0.0.tgz#44435d834d143ae9b60daa454f68f8e72e6ebd7f" + integrity sha512-EJk9s8judGLIZ6c9N779z91WHPIfAkwkVY5QF7WH2ZT2Kt03k/hAoy7P4NjYreFIQcIo8d+TU/CIhViCmB4c0Q== "@types/caseless@*": version "0.12.2" From c6a11d3ccde62eabe041bc90bf0bbda0b103ab15 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 25 Jun 2024 16:59:15 -0300 Subject: [PATCH 107/161] fix: auto-organizar las columnas --- app/views/posts/attributes/_new_belongs_to.haml | 2 +- app/views/posts/attributes/_new_has_and_belongs_to_many.haml | 2 +- app/views/posts/attributes/_new_has_many.haml | 2 +- app/views/posts/attributes/_new_has_one.haml | 2 +- app/views/posts/attributes/_new_predefined_value.haml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index c39176e0..b083e55e 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -44,7 +44,7 @@ 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.no-gutters.placeholder-glow{ data: { target: 'array.current' } } + .row.no-gutters.placeholder-glow{ data: { target: 'array.current' } } -# @todo issue-7537 - if !metadata.empty? && (indexed_post = site.indexed_posts.find_by(post_id: metadata.value)) = render 'posts/new_related_post', post: indexed_post 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 4ca444cf..1142c3b9 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 @@ -44,7 +44,7 @@ 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.no-gutters.placeholder-glow{ data: { target: 'array.current' } } + .row.no-gutters.placeholder-glow{ data: { target: 'array.current' } } -# @todo issue-7537 - metadata.value.each do |uuid| - if (indexed_post = site.indexed_posts.find_by(post_id: uuid)) diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 476f2c2f..9910e078 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -44,7 +44,7 @@ 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.no-gutters.placeholder-glow{ data: { target: 'array.current' } } + .row.no-gutters.placeholder-glow{ data: { target: 'array.current' } } -# @todo issue-7537 - metadata.value.each do |uuid| - if (indexed_post = site.indexed_posts.find_by(post_id: uuid)) diff --git a/app/views/posts/attributes/_new_has_one.haml b/app/views/posts/attributes/_new_has_one.haml index 87ef0440..21b7b511 100644 --- a/app/views/posts/attributes/_new_has_one.haml +++ b/app/views/posts/attributes/_new_has_one.haml @@ -31,7 +31,7 @@ = render 'bootstrap/btn', content: t('.edit'), action: 'modal#showAnother', data: { 'modal-show-value': modal_id }, id: random_id -# Aquí se reemplaza por la tarjeta y el UUID luego de guardar - .row.row-cols-1.no-gutters.placeholder-glow{ id: target_id } + .row.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 diff --git a/app/views/posts/attributes/_new_predefined_value.haml b/app/views/posts/attributes/_new_predefined_value.haml index dad716b0..5158169d 100644 --- a/app/views/posts/attributes/_new_predefined_value.haml +++ b/app/views/posts/attributes/_new_predefined_value.haml @@ -44,7 +44,7 @@ Para poder cancelar, mantenemos el estado original y desactivamos o activamos los ítemes según estén incluidos en esa lista o no. - %ul.placeholder-glow{ data: { target: 'array.current' } } + %ul.list-unstyled.px-3.font-weight-bold.placeholder-glow{ data: { target: 'array.current' } } - unless metadata.empty? = render 'posts/new_array_value', value: metadata.to_s From 63bd017491fee20825bf1c60705e6f45b57c6987 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 25 Jun 2024 16:59:37 -0300 Subject: [PATCH 108/161] fix: ocultar los posts con layout oculto --- app/views/posts/index.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml index 3de30aa3..ad019c3f 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -99,6 +99,8 @@ - dir = @site.data.dig(params[:locale], 'dir') - size = @posts.size - @posts.each_with_index do |post, i| + -# @todo issue-7537 + - next if post.layout.hidden? -# TODO: Solo les usuaries cachean porque tenemos que separar les botones por permisos. From 2a6fafcb9c50a26a41103852b03eed8a867b7e48 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 25 Jun 2024 17:06:36 -0300 Subject: [PATCH 109/161] fixup! fix: ocultar los posts con layout oculto --- app/views/posts/index.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml index ad019c3f..14c27ec9 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -100,7 +100,7 @@ - size = @posts.size - @posts.each_with_index do |post, i| -# @todo issue-7537 - - next if post.layout.hidden? + - next if @site.layouts[post.layout].hidden? -# TODO: Solo les usuaries cachean porque tenemos que separar les botones por permisos. From f23e451793f7e89c436aa653011f4f91e6fece0c Mon Sep 17 00:00:00 2001 From: f Date: Wed, 26 Jun 2024 14:02:44 -0300 Subject: [PATCH 110/161] fix: poder abrir modales --- app/views/posts/attributes/_new_has_one.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_new_has_one.haml b/app/views/posts/attributes/_new_has_one.haml index 21b7b511..1d0bb380 100644 --- a/app/views/posts/attributes/_new_has_one.haml +++ b/app/views/posts/attributes/_new_has_one.haml @@ -28,7 +28,7 @@ = 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 }, id: random_id + = render 'bootstrap/btn', content: t('.edit'), data: { action: 'modal#showAnother', 'modal-show-value': modal_id }, id: random_id -# Aquí se reemplaza por la tarjeta y el UUID luego de guardar .row.no-gutters.placeholder-glow{ id: target_id } From d1274dbfd4311a7e615b1f7332c10e6f4948ccb9 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 26 Jun 2024 14:47:30 -0300 Subject: [PATCH 111/161] fix: permitir que la imagen ocupe todo lo necesario --- app/views/bootstrap/_card.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/bootstrap/_card.haml b/app/views/bootstrap/_card.haml index e7e1f108..87e9691a 100644 --- a/app/views/bootstrap/_card.haml +++ b/app/views/bootstrap/_card.haml @@ -1,7 +1,6 @@ .card{ **local_assigns.except(:image, :description) } - if local_assigns[:image] - = render 'bootstrap/responsive' do - = image_tag url_for(local_assigns[:image]), alt: local_assigns[:description], class: 'img-fluid' + = image_tag url_for(local_assigns[:image]), alt: local_assigns[:description], class: 'img-fluid' .card-body .card-title= title From 53ae9323934e6da8937cb2461af3fa8c385f1bce Mon Sep 17 00:00:00 2001 From: f Date: Wed, 26 Jun 2024 18:50:20 -0300 Subject: [PATCH 112/161] fix: borde del modal --- app/assets/stylesheets/application.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d8fe8c9f..350c4aaa 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -30,6 +30,7 @@ $form-feedback-icon-valid-color: $black; $component-active-bg: $magenta; $zindex-modal-backdrop: 0; $modal-content-bg: var(--background); +$modal-content-border-color: var(--modal-content-border-color); $card-bg: var(--background); $card-border-color: var(--card-border-color); $input-bg: var(--background); @@ -79,6 +80,7 @@ $sizes: ( --background: #{$white}; --color: #{$magenta}; --card-border-color: #{rgba($black, .125)}; + --modal-content-border-color: rgba(#{$black}, .2); } @media (prefers-color-scheme: dark) { @@ -87,6 +89,7 @@ $sizes: ( --background: #{$black}; --color: #{$cyan}; --card-border-color: #{rgba($white, .125)}; + --modal-content-border-color: #{rgba($white, .2)}; } .btn-secondary { From 6063f8dc6b012ec03f4b534eab86b480b9672a4e Mon Sep 17 00:00:00 2001 From: maki Date: Mon, 1 Jul 2024 15:47:36 -0300 Subject: [PATCH 113/161] fix: idioma por defecto #15068 --- app/helpers/application_helper.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7d1f9c0b..baef3b05 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -156,8 +156,8 @@ module ApplicationHelper private def post_t(*attribute, post:, type:) - post.layout.metadata.dig(*attribute, type.to_s, I18n.locale.to_s) || - post.layout.metadata.dig(*attribute, type.to_s, I18n.default_locale.to_s) || - I18n.t("posts.attributes.#{attribute.join('.')}.#{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 || + I18n.t("posts.attributes.#{attribute.join('.')}.#{type}").presence end end From 91e9eab97574896d640d35b4fd74ef10e15915ab Mon Sep 17 00:00:00 2001 From: f Date: Tue, 2 Jul 2024 12:58:24 -0300 Subject: [PATCH 114/161] =?UTF-8?q?fix:=20la=20descripci=C3=B3n=20del=20ar?= =?UTF-8?q?chivo=20es=20parte=20del=20t=C3=ADtulo=20#16679?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/metadata_file.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/models/metadata_file.rb b/app/models/metadata_file.rb index 61aea897..fb5082f4 100644 --- a/app/models/metadata_file.rb +++ b/app/models/metadata_file.rb @@ -18,6 +18,18 @@ class MetadataFile < MetadataTemplate # XXX: Esto ayuda a deserializar en {Site#everything_of} def values; end + # Usar la descripción + def titleize? + true + end + + # Devolver la descripción + # + # @return [String] + def to_s + value['description'].to_s + end + def validate super From bd3df6c1863fcec19c98e79070360e7bd49588bc Mon Sep 17 00:00:00 2001 From: f Date: Tue, 2 Jul 2024 13:01:13 -0300 Subject: [PATCH 115/161] fix: new_predefined_array con valores legibles por humanes --- app/models/metadata_predefined_array.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/models/metadata_predefined_array.rb b/app/models/metadata_predefined_array.rb index b8e5050e..566a491b 100644 --- a/app/models/metadata_predefined_array.rb +++ b/app/models/metadata_predefined_array.rb @@ -7,4 +7,13 @@ class MetadataPredefinedArray < MetadataArray [v[I18n.locale.to_s], k] end&.to_h end + + # Devolver los valores legibles por humanes + # + # @todo Debería devolver los valores en el idioma del post, no de le + # usuarie + # @return [String] + def to_s + values.invert.select { |x, k| value.include?(x) }.values.join(', ') + end end From 100df4b157fdb7b8f0b00651d679d284a65440be Mon Sep 17 00:00:00 2001 From: f Date: Tue, 2 Jul 2024 13:01:27 -0300 Subject: [PATCH 116/161] fix: el titulo siempre se actualiza #16669 --- app/models/metadata_title.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/metadata_title.rb b/app/models/metadata_title.rb index 729a71fd..6d6b1bde 100644 --- a/app/models/metadata_title.rb +++ b/app/models/metadata_title.rb @@ -7,6 +7,11 @@ class MetadataTitle < MetadataString false end + # Siempre recalcular el título + def value + self[:value] = default_value + end + # Obtener todos los valores de texto del artículo y generar un título # en base a eso. # From a344c16f4d61366ea2dd44487bb6bfba77feb75a Mon Sep 17 00:00:00 2001 From: f Date: Tue, 2 Jul 2024 13:01:58 -0300 Subject: [PATCH 117/161] fix: cargar los valores actuales de new_predefined_array #16678 --- app/views/posts/attributes/_new_predefined_array.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index d5f2110e..3b251bcb 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -34,7 +34,7 @@ Para poder cancelar, mantenemos el estado original y desactivamos o activamos los ítemes según estén incluidos en esa lista o no. %ul.placeholder-glow{ data: { target: 'array.current' } } - - metadata.values.slice(*metadata.value).each_key do |value| + - metadata.values.invert.slice(*metadata.value).each_value do |value| = render 'posts/new_array_value', value: value = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' }, hide_actions: ['array#cancel'], keydown_actions: %w[keydown->array#cancelWithEscape] do From 3a2005e54ac5916f767b840f3436adc225bd9e7a Mon Sep 17 00:00:00 2001 From: f Date: Tue, 2 Jul 2024 13:02:32 -0300 Subject: [PATCH 118/161] fix: no romper relaciones! --- app/views/posts/_htmx_form.haml | 8 +++++--- app/views/posts/attributes/_new_belongs_to.haml | 1 + .../posts/attributes/_new_has_and_belongs_to_many.haml | 1 + app/views/posts/attributes/_new_has_many.haml | 2 ++ app/views/posts/attributes/_new_has_one.haml | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index 1a8c0597..f6704695 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -85,11 +85,13 @@ = hidden_field_tag "#{base}[layout]", post.layout.name - -# Dibuja cada atributo + -# Dibuja cada atributo, excepto algunos = render 'posts/attributes', site: site, post: post, dir: dir, base: base, locale: locale, except: except - -# Enviamos valores vacíos para los atributos ocultos + -# + 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: "" } + %input{ type: 'hidden', name: "#{base}[#{attr}]", value: params[attr].presence } = yield(:post_form) diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index b083e55e..a187e9ea 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -100,6 +100,7 @@ %input{ type: 'hidden', name: 'attribute', value: metadata.type } - if metadata.inverse? %input{ type: 'hidden', name: 'inverse', value: metadata.inverse } + %input{ type: 'hidden', name: metadata.inverse, value: post.uuid.value } %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 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 1142c3b9..5dd1039b 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 @@ -101,6 +101,7 @@ %input{ type: 'hidden', name: 'attribute', value: metadata.type } - if metadata.inverse? %input{ type: 'hidden', name: 'inverse', value: metadata.inverse } + %input{ type: 'hidden', name: metadata.inverse, value: post.uuid.value } %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 diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 9910e078..805636da 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -99,8 +99,10 @@ %input{ type: 'hidden', name: 'name', value: name } %input{ type: 'hidden', name: 'form', value: form_id } %input{ type: 'hidden', name: 'attribute', value: metadata.type } + -# @todo Forma genérica de arrastrar valores desde un formulario al siguiente - if metadata.inverse? %input{ type: 'hidden', name: 'inverse', value: metadata.inverse } + %input{ type: 'hidden', name: metadata.inverse, value: post.uuid.value } %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 diff --git a/app/views/posts/attributes/_new_has_one.haml b/app/views/posts/attributes/_new_has_one.haml index 1d0bb380..c082da6d 100644 --- a/app/views/posts/attributes/_new_has_one.haml +++ b/app/views/posts/attributes/_new_has_one.haml @@ -46,7 +46,7 @@ = 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, invalid: invalid_id, submitting: submitting_id, saved: saved_id), 'hx-trigger': 'load' } + .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, invalid: invalid_id, submitting: submitting_id, saved: saved_id, inverse: metadata.inverse, metadata.inverse => post.uuid.value), 'hx-trigger': 'load' } %span.placeholder.w-100.h-100 - content_for :"#{id}_footer" do From 58cd2f514a176d5130e52729f1cd386f795b3816 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 2 Jul 2024 17:06:18 -0300 Subject: [PATCH 119/161] fix: poder cancelar el modal #16711 --- app/views/posts/attributes/_new_predefined_array.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index 3b251bcb..80a88098 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -47,7 +47,7 @@ .form-group.mb-0{ id: "#{id}_body" } -# 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 } } + .mb-2{ data: { target: 'array.item', 'searchable-value': value.remove_diacritics.downcase, value: key } } = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: key, checked: metadata.value.include?(key), content: value, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } - content_for :"#{id}_footer" do From 338af3f0fecc8c8ced47445c26bbee0a6e82dd6e Mon Sep 17 00:00:00 2001 From: f Date: Thu, 4 Jul 2024 15:02:17 -0300 Subject: [PATCH 120/161] feat: array.item reutilizable #16718 --- .../controllers/array_controller.js | 2 +- app/views/posts/attributes/_new_array.haml | 2 +- .../posts/attributes/_new_belongs_to.haml | 2 +- .../_new_has_and_belongs_to_many.haml | 2 +- app/views/posts/attributes/_new_has_many.haml | 2 +- .../attributes/_new_predefined_array.haml | 2 +- .../attributes/_new_predefined_value.haml | 2 +- app/views/posts/new_array.haml | 2 +- app/views/posts/new_belongs_to_value.haml | 2 +- app/views/posts/new_has_many_value.haml | 2 +- app/views/targets/array/_item.haml | 21 +++++++++++++++++++ config/locales/en.yml | 6 ++++-- config/locales/es.yml | 6 ++++-- 13 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 app/views/targets/array/_item.haml diff --git a/app/javascript/controllers/array_controller.js b/app/javascript/controllers/array_controller.js index db768ef3..fb74a673 100644 --- a/app/javascript/controllers/array_controller.js +++ b/app/javascript/controllers/array_controller.js @@ -108,7 +108,7 @@ export default class extends Controller { if (!this.isChecked(itemTarget)) continue; this.originalValue.push(itemTarget.dataset.value); - this.newArrayValueURL.searchParams.set("value", itemTarget.dataset.value); + this.newArrayValueURL.searchParams.set("value", itemTarget.dataset?.humanValue || itemTarget.dataset?.value); const placeholder = this.placeholderTarget.content.firstElementChild.cloneNode(true); diff --git a/app/views/posts/attributes/_new_array.haml b/app/views/posts/attributes/_new_array.haml index b8607627..989d734b 100644 --- a/app/views/posts/attributes/_new_array.haml +++ b/app/views/posts/attributes/_new_array.haml @@ -48,7 +48,7 @@ .form-group.mb-0{ id: "#{id}_body" } -# Eliminamos las tildes para poder buscar independientemente de cómo se escriba. - metadata.values.each do |value| - .mb-2{ data: { target: 'array.item', 'searchable-value': value.remove_diacritics.downcase, value: value } } + = render 'targets/array/item', value: value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: value, checked: metadata.value.include?(value), content: value, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } - content_for :"#{id}_footer" do diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index a187e9ea..20b40140 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -58,7 +58,7 @@ - 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 'targets/array/item', value: uuid, 'human-value': value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: uuid, checked: metadata.value.include?(uuid), content: value, type: 'radio' -# 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 5dd1039b..d70d9eb4 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 @@ -59,7 +59,7 @@ - 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 'targets/array/item', value: uuid, 'human-value': value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: uuid, checked: metadata.value.include?(uuid), content: value, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } -# diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 805636da..601a7e38 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -59,7 +59,7 @@ - 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 'targets/array/item', value: uuid, 'human-value': value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: uuid, checked: metadata.value.include?(uuid), content: value, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } -# diff --git a/app/views/posts/attributes/_new_predefined_array.haml b/app/views/posts/attributes/_new_predefined_array.haml index 80a88098..feececc5 100644 --- a/app/views/posts/attributes/_new_predefined_array.haml +++ b/app/views/posts/attributes/_new_predefined_array.haml @@ -47,7 +47,7 @@ .form-group.mb-0{ id: "#{id}_body" } -# 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: key } } + = render 'targets/array/item', class: 'mb-2', value: key, 'human-value': value do = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: key, checked: metadata.value.include?(key), content: value, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } - content_for :"#{id}_footer" do diff --git a/app/views/posts/attributes/_new_predefined_value.haml b/app/views/posts/attributes/_new_predefined_value.haml index 5158169d..ba52baf1 100644 --- a/app/views/posts/attributes/_new_predefined_value.haml +++ b/app/views/posts/attributes/_new_predefined_value.haml @@ -57,7 +57,7 @@ - content_for :"#{id}_body" do .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 'targets/array/item', value: value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: key, checked: (metadata.value == key), content: value, type: 'radio' - content_for :"#{id}_footer" do diff --git a/app/views/posts/new_array.haml b/app/views/posts/new_array.haml index 77d64b55..9d6e62ea 100644 --- a/app/views/posts/new_array.haml +++ b/app/views/posts/new_array.haml @@ -1,6 +1,6 @@ - item_id = random_id -.mb-2{ id: item_id, data: { target: 'array.item', 'searchable-value': @value.remove_diacritics.downcase, value: @value } } += render 'targets/array/item', value: @value, class: 'mb-2', id: item_id do .d-flex.flex-row.flex-wrap .flex-grow-1 = render 'bootstrap/custom_checkbox', name: @name, id: random_id, value: @value, checked: true, content: @value diff --git a/app/views/posts/new_belongs_to_value.haml b/app/views/posts/new_belongs_to_value.haml index d1f9c3f9..cc66f680 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 'targets/array/item', value: @uuid, 'human-value': @value, class: 'mb-2' do = 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 b9846475..8479f3e4 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 'targets/array/item', value: @uuid, 'human-value': @value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: @name, id: random_id, value: @uuid, checked: true, content: @value diff --git a/app/views/targets/array/_item.haml b/app/views/targets/array/_item.haml new file mode 100644 index 00000000..f89a42e5 --- /dev/null +++ b/app/views/targets/array/_item.haml @@ -0,0 +1,21 @@ +-# + Un item de un array. + + Además de los valores por defecto, se pueden pasar otros atributos + para el div del ítem. + + @param :value [String] El valor (requerido) + @param :human-value [String] El valor legible por humanes (opcional) + @param :searchable-value [String] El valor para usar en el filtro (opcional) + +:ruby + local_assigns[:'human-value'] ||= value + local_assigns[:'searchable-value'] ||= local_assigns[:'human-value'].remove_diacritics.downcase + local_assigns.delete(:value) + + data = local_assigns.delete(:data) + data ||= {} + data[:'human-value'] = local_assigns.delete(:'human-value') + data[:'searchable-value'] = local_assigns.delete(:'searchable-value') + +%div{ **local_assigns, data: { target: 'array.item', value: value, **data } }= yield diff --git a/config/locales/en.yml b/config/locales/en.yml index 52f002e6..ce5a9e36 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -722,8 +722,6 @@ en: validation: invalid: "Some fields need attention! Please search for the fields marked as not valid." submitting: "Saving changes, please wait..." - new_array: - remove: "Remove" attributes: add: Add title: @@ -967,3 +965,7 @@ en: edit: "Edit" alert: saved: "Changes were saved!" + targets: + array: + item: + remove: "Remove" diff --git a/config/locales/es.yml b/config/locales/es.yml index 8fe09758..416c3e1a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -730,8 +730,6 @@ es: validation: invalid: "¡Te falta completar algunos campos! Busca los que estén marcados como no válidos." submitting: "Guardando, por favor espera..." - new_array: - remove: "Eliminar" attributes: add: Agregar title: @@ -977,3 +975,7 @@ es: edit: "Editar" alert: saved: "¡Cambios guardados!" + targets: + array: + item: + remove: "Eliminar" From e9e55945d93984454cd577bca8c08737895f83c8 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 4 Jul 2024 15:05:58 -0300 Subject: [PATCH 121/161] fix: editar el post de has_one desde su modal #16665 --- app/views/posts/_new_has_one.haml | 2 +- app/views/posts/_new_related_post.haml | 6 ++++-- app/views/posts/attributes/_new_has_one.haml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/views/posts/_new_has_one.haml b/app/views/posts/_new_has_one.haml index 54a370cd..f5286670 100644 --- a/app/views/posts/_new_has_one.haml +++ b/app/views/posts/_new_has_one.haml @@ -1,2 +1,2 @@ -= render 'posts/new_related_post', post: post += render 'posts/new_related_post', post: post, modal_id: modal_id %input{ type: 'hidden', name: name, value: value } diff --git a/app/views/posts/_new_related_post.haml b/app/views/posts/_new_related_post.haml index 046080c9..9e342a6c 100644 --- a/app/views/posts/_new_related_post.haml +++ b/app/views/posts/_new_related_post.haml @@ -1,4 +1,6 @@ :ruby + local_assigns[:modal_id] ||= 'generic_modal' + image = nil description = nil @@ -6,9 +8,9 @@ description = post.post.image.value['description'] end -.col.mb-3.p-1 +.col.mb-3.p-1{ data: { controller: 'modal' } } = render('bootstrap/card', image: image, description: description, title: post.title, class: 'h-100') do - if post.post.attribute?(:description) %p.card-text= post.post.description.value - = link_to t('.edit'), edit_site_post_path(post.site, post.path), class: 'btn btn-secondary' + = render 'bootstrap/btn', content: t('.edit'), data: { action: 'modal#showAnother', 'modal-show-value': local_assigns[:modal_id] }, id: random_id diff --git a/app/views/posts/attributes/_new_has_one.haml b/app/views/posts/attributes/_new_has_one.haml index c082da6d..09cc9b7f 100644 --- a/app/views/posts/attributes/_new_has_one.haml +++ b/app/views/posts/attributes/_new_has_one.haml @@ -34,7 +34,7 @@ .row.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 + = render 'posts/new_has_one', post: indexed_post, name: name, value: metadata.value, modal_id: modal_id -# El modal se genera por fuera del formulario, para poder enviar los From 434e1311f20633880b36ef41f2abaa79f433e0ef Mon Sep 17 00:00:00 2001 From: f Date: Thu, 4 Jul 2024 16:51:08 -0300 Subject: [PATCH 122/161] fix: htmx usa value para el evento #16714 --- app/javascript/controllers/form_validation_controller.js | 4 ++-- app/javascript/controllers/notification_controller.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/javascript/controllers/form_validation_controller.js b/app/javascript/controllers/form_validation_controller.js index dd1a98ed..b1c0a2f8 100644 --- a/app/javascript/controllers/form_validation_controller.js +++ b/app/javascript/controllers/form_validation_controller.js @@ -39,13 +39,13 @@ export default class extends Controller { if (!this.element.getAttributeNames().some(x => x.startsWith("hx-"))) this.element.submit(); - window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.submittingIdValue } })); + window.dispatchEvent(new CustomEvent("notification:show", { detail: { value: this.submittingIdValue } })); } else { event?.stopPropagation(); this.element.classList.add("was-validated"); - window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.invalidIdValue } })); + window.dispatchEvent(new CustomEvent("notification:show", { detail: { value: this.invalidIdValue } })); } this.submitting = false; diff --git a/app/javascript/controllers/notification_controller.js b/app/javascript/controllers/notification_controller.js index 7fbe3b5a..89ae085d 100644 --- a/app/javascript/controllers/notification_controller.js +++ b/app/javascript/controllers/notification_controller.js @@ -19,8 +19,8 @@ export default class extends Controller { * actual, se oculta. */ show(event = undefined) { - if (event?.detail?.id !== this.element.id) { - this.hide({ detail: { id: this.element.id } }); + if (event?.detail?.value !== this.element.id) { + this.hide({ detail: { value: this.element.id } }); return; } @@ -33,7 +33,7 @@ export default class extends Controller { } hide(event = undefined) { - if (event?.detail?.id !== this.element.id) return; + if (event?.detail?.value !== this.element.id) return; this.element.classList.remove(...this.showClasses); this.element.classList.add(...this.hideClasses); From 3efd3827a6fdb6f19e3cd5dd93ad3aff33ec21d0 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 4 Jul 2024 16:51:40 -0300 Subject: [PATCH 123/161] =?UTF-8?q?fix:=20ser=20espec=C3=ADficxs=20con=20l?= =?UTF-8?q?o=20que=20va=20a=20pasar=20en=20el=20bot=C3=B3n=20#16712?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/locales/en.yml | 2 +- config/locales/es.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index ce5a9e36..ee3a23e7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -957,7 +957,7 @@ en: add: "Add %{layout}" add_new: "Add new option" cancel: "Cancel" - close: "Close" + close: "Close without saving" edit: "Edit" filter: "Start typing to filter..." save: "Save" diff --git a/config/locales/es.yml b/config/locales/es.yml index 416c3e1a..336dc503 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -967,7 +967,7 @@ es: add: "Agregar %{layout}" add_new: "Agregar nueva opción" cancel: "Cancelar" - close: "Cerrar" + close: "Cerrar sin guardar" edit: "Editar" filter: "Empezá a escribir para filtrar..." save: "Guardar" From 01363c701803e9b789135c7bdca68891684f2269 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 5 Jul 2024 10:10:18 -0300 Subject: [PATCH 124/161] fixup! fix: editar el post de has_one desde su modal #16665 --- app/views/posts/new_has_one_value.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/new_has_one_value.haml b/app/views/posts/new_has_one_value.haml index 9f2b660a..8407f981 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 += render 'posts/new_has_one', post: @post.to_index, name: params.require(:name), value: @uuid, modal_id: params.require(:saved) From 76cf8ea9405b2442a0c6a0dcd4f3697cd09c7d60 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 5 Jul 2024 10:32:36 -0300 Subject: [PATCH 125/161] =?UTF-8?q?fix:=20poder=20volver=20a=20abrir=20el?= =?UTF-8?q?=20mismo=20modal=20despu=C3=A9s=20de=20guardar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/posts/new_has_one_value.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/new_has_one_value.haml b/app/views/posts/new_has_one_value.haml index 8407f981..6eece5dc 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(:saved) += render 'posts/new_has_one', post: @post.to_index, name: params.require(:name), value: @uuid, modal_id: params.require(:show) From 9960f12863b7adb065e7b75aeac4a60f1130524e Mon Sep 17 00:00:00 2001 From: f Date: Fri, 5 Jul 2024 10:38:48 -0300 Subject: [PATCH 126/161] fix: traducciones faltantes --- config/locales/en.yml | 3 ++- config/locales/es.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index ee3a23e7..ec5b34a9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -763,11 +763,12 @@ en: edit: "Edit" new_array: edit: "Edit" - required: "Please select at least one option." new_has_one: edit: "Edit" new_belongs_to: edit: "Edit" + required_checkbox: + required: "Please select at least one option." reorder: submit: 'Save order' select: 'Select this post' diff --git a/config/locales/es.yml b/config/locales/es.yml index 336dc503..61c645a0 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -773,11 +773,12 @@ es: edit: "Editar" new_array: edit: "Editar" - required: "Seleccioná al menos una opción." new_has_one: edit: "Editar" new_belongs_to: edit: "Editar" + required_checkbox: + required: "Seleccioná al menos una opción." reorder: submit: 'Guardar orden' select: 'Seleccionar este artículo' From d793d4fe54ea8687ddcbcc9bc9289d0522252f66 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 5 Jul 2024 11:56:48 -0300 Subject: [PATCH 127/161] fix: poder mandar uuids de relaciones! --- app/javascript/controllers/array_controller.js | 2 +- app/views/posts/attributes/_new_belongs_to.haml | 2 +- app/views/posts/attributes/_new_has_and_belongs_to_many.haml | 2 +- app/views/posts/attributes/_new_has_many.haml | 2 +- app/views/targets/array/_item.haml | 3 +++ 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/javascript/controllers/array_controller.js b/app/javascript/controllers/array_controller.js index fb74a673..fea690d2 100644 --- a/app/javascript/controllers/array_controller.js +++ b/app/javascript/controllers/array_controller.js @@ -108,7 +108,7 @@ export default class extends Controller { if (!this.isChecked(itemTarget)) continue; this.originalValue.push(itemTarget.dataset.value); - this.newArrayValueURL.searchParams.set("value", itemTarget.dataset?.humanValue || itemTarget.dataset?.value); + this.newArrayValueURL.searchParams.set("value", itemTarget.dataset?.sendValue || itemTarget.dataset?.value); const placeholder = this.placeholderTarget.content.firstElementChild.cloneNode(true); diff --git a/app/views/posts/attributes/_new_belongs_to.haml b/app/views/posts/attributes/_new_belongs_to.haml index 20b40140..d23301d9 100644 --- a/app/views/posts/attributes/_new_belongs_to.haml +++ b/app/views/posts/attributes/_new_belongs_to.haml @@ -58,7 +58,7 @@ - content_for :"#{id}_body" do .form-group.mb-0{ id: value_list_id } - metadata.values.each_pair do |value, uuid| - = render 'targets/array/item', value: uuid, 'human-value': value, class: 'mb-2' do + = render 'targets/array/item', value: uuid, 'send-value': uuid, 'human-value': value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: uuid, checked: metadata.value.include?(uuid), content: value, type: 'radio' -# 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 d70d9eb4..bf5e45e9 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 @@ -59,7 +59,7 @@ - content_for :"#{id}_body" do .form-group.mb-0{ id: value_list_id } - metadata.values.each_pair do |value, uuid| - = render 'targets/array/item', value: uuid, 'human-value': value, class: 'mb-2' do + = render 'targets/array/item', value: uuid, 'send-value': uuid, 'human-value': value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: uuid, checked: metadata.value.include?(uuid), content: value, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } -# diff --git a/app/views/posts/attributes/_new_has_many.haml b/app/views/posts/attributes/_new_has_many.haml index 601a7e38..39d4538d 100644 --- a/app/views/posts/attributes/_new_has_many.haml +++ b/app/views/posts/attributes/_new_has_many.haml @@ -59,7 +59,7 @@ - content_for :"#{id}_body" do .form-group.mb-0{ id: value_list_id } - metadata.values.each_pair do |value, uuid| - = render 'targets/array/item', value: uuid, 'human-value': value, class: 'mb-2' do + = render 'targets/array/item', value: uuid, 'send-value': uuid, 'human-value': value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: name, id: random_id, value: uuid, checked: metadata.value.include?(uuid), content: value, data: { action: 'required-checkbox#change', target: 'required-checkbox.checkbox' } -# diff --git a/app/views/targets/array/_item.haml b/app/views/targets/array/_item.haml index f89a42e5..ead9f4e2 100644 --- a/app/views/targets/array/_item.haml +++ b/app/views/targets/array/_item.haml @@ -6,16 +6,19 @@ @param :value [String] El valor (requerido) @param :human-value [String] El valor legible por humanes (opcional) + @param :send-value [String] El valor que se envía al controlador (opcional) @param :searchable-value [String] El valor para usar en el filtro (opcional) :ruby local_assigns[:'human-value'] ||= value + local_assigns[:'send-value'] ||= local_assigns[:'human-value'] local_assigns[:'searchable-value'] ||= local_assigns[:'human-value'].remove_diacritics.downcase local_assigns.delete(:value) data = local_assigns.delete(:data) data ||= {} data[:'human-value'] = local_assigns.delete(:'human-value') + data[:'send-value'] = local_assigns.delete(:'send-value') data[:'searchable-value'] = local_assigns.delete(:'searchable-value') %div{ **local_assigns, data: { target: 'array.item', value: value, **data } }= yield From d596ae57143097cbed16357d27fcd35b63ca6774 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 5 Jul 2024 16:39:07 -0300 Subject: [PATCH 128/161] fixup! fix: poder mandar uuids de relaciones! --- app/views/posts/new_belongs_to_value.haml | 2 +- app/views/posts/new_has_many_value.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/posts/new_belongs_to_value.haml b/app/views/posts/new_belongs_to_value.haml index cc66f680..c7917b7d 100644 --- a/app/views/posts/new_belongs_to_value.haml +++ b/app/views/posts/new_belongs_to_value.haml @@ -1,2 +1,2 @@ -= render 'targets/array/item', value: @uuid, 'human-value': @value, class: 'mb-2' do += render 'targets/array/item', value: @uuid, 'send-value': @uuid, 'human-value': @value, class: 'mb-2' do = 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 8479f3e4..a4c87fc9 100644 --- a/app/views/posts/new_has_many_value.haml +++ b/app/views/posts/new_has_many_value.haml @@ -1,2 +1,2 @@ -= render 'targets/array/item', value: @uuid, 'human-value': @value, class: 'mb-2' do += render 'targets/array/item', value: @uuid, 'send-value': @uuid, 'human-value': @value, class: 'mb-2' do = render 'bootstrap/custom_checkbox', name: @name, id: random_id, value: @uuid, checked: true, content: @value From f1968a0b49d7cbc13551c2963693445dabb5fca1 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 10 Jul 2024 16:48:57 -0300 Subject: [PATCH 129/161] =?UTF-8?q?fix:=20permitir=20que=20el=20idioma=20p?= =?UTF-8?q?or=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 130/161] 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 131/161] =?UTF-8?q?feat:=20poder=20cancelar=20cuando=20se?= =?UTF-8?q?=20est=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 132/161] 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 133/161] 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