5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2025-02-28 22:01:50 +00:00

Merge branch 'production.panel.sutty.nl' of 0xacab.org:sutty/sutty into production.panel.sutty.nl

This commit is contained in:
Sutty 2024-07-11 19:21:29 +00:00
commit 6a3bdb7054
38 changed files with 353 additions and 108 deletions

View file

@ -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

View file

@ -2,6 +2,8 @@
# Controlador para artículos
class PostsController < ApplicationController
include StrongParamsHelper
before_action :authenticate_usuarie!
before_action :service_for_direct_upload, only: %i[new edit]
@ -17,9 +19,9 @@ class PostsController < ApplicationController
# @todo Mover a tu propio scope
def new_array
@value = params.require(:value).strip
@name = params.require(:name).strip
id = params.require(:id).strip
@value = pluck_param(:value)
@name = pluck_param(:name)
id = pluck_param(:id)
headers['HX-Trigger-After-Swap'] = 'htmx:resetForm'
@ -27,13 +29,13 @@ class PostsController < ApplicationController
end
def new_array_value
@value = params.require(:value).strip
@value = pluck_param(:value)
render layout: false
end
def new_related_post
@uuid = params.require(:value).strip
@uuid = pluck_param(:value)
@indexed_post = site.indexed_posts.find_by!(post_id: @uuid)
@ -41,7 +43,7 @@ class PostsController < ApplicationController
end
def new_has_one
@uuid = params.require(:value).strip
@uuid = pluck_param(:value)
@indexed_post = site.indexed_posts.find_by!(post_id: @uuid)
@ -51,7 +53,7 @@ class PostsController < ApplicationController
# El formulario de un Post, si pasamos el UUID, estamos editando, sino
# estamos creando.
def form
uuid = params.permit(:uuid).try(:[], :uuid).presence
uuid = pluck_param(:uuid, optional: true)
locale
@post =
@ -59,7 +61,31 @@ class PostsController < ApplicationController
site.indexed_posts.find_by!(post_id: uuid).post
else
# @todo Usar la base de datos
site.posts(lang: locale).build(layout: params.require(:layout))
site.posts(lang: locale).build(layout: pluck_param(:layout))
end
swap_modals
render layout: false
end
# Genera un modal completo
#
# @todo recibir el atributo anterior
# @param :uuid [String] UUID del post (opcional)
# @param :layout [String] El layout a cargar (opcional)
def modal
uuid = pluck_param(:uuid, optional: true)
locale
# @todo hacer que si el uuid no existe se genera un post, para poder
# pasar el uuid sabiendolo
@post =
if uuid.present?
site.indexed_posts.find_by!(post_id: uuid).post
else
# @todo Usar la base de datos
site.posts(lang: locale).build(layout: pluck_param(:layout))
end
swap_modals
@ -107,7 +133,7 @@ class PostsController < ApplicationController
def new
authorize Post
@post = site.posts(lang: locale).build(layout: params[:layout])
@post = site.posts(lang: locale).build(layout: pluck_param(:layout))
breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), ''
end
@ -128,17 +154,17 @@ class PostsController < ApplicationController
# condiciones.
if htmx?
if post.persisted?
triggers = { 'notification:show' => { 'id' => params.permit(:saved).values.first } }
triggers = { 'notification:show' => { 'id' => pluck_param(:saved, optional: true) } }
swap_modals(triggers)
@value = post.title.value
@uuid = post.uuid.value
@name = params.require(:name)
@name = pluck_param(:name)
render render_path_from_attribute, layout: false
else
headers['HX-Retarget'] = "##{params.require(:form)}"
headers['HX-Retarget'] = "##{pluck_param(:form)}"
headers['HX-Reswap'] = 'outerHTML'
render 'posts/form', layout: false, post: post, site: site, **params.permit(:form, :base, :dir, :locale)
@ -156,6 +182,16 @@ class PostsController < ApplicationController
breadcrumb 'posts.edit', ''
end
# Este endpoint se encarga de actualizar el post. Si el post se edita
# desde el formulario principal, re-renderizamos el formulario si hay
# errores o enviamos a otro lado al guardar.
#
# Si los datos llegaron por HTMX, hay que regenerar el formulario
# y reemplazarlo en su modal (?) o responder con su tarjeta para
# reemplazarla donde sea que esté.
#
# @todo la re-renderización del formulario no es necesaria si tenemos
# validación client-side.
def update
authorize post
@ -171,15 +207,26 @@ class PostsController < ApplicationController
if htmx?
if post.persisted?
triggers = { 'notification:show' => params.permit(:saved).values.first }
triggers = { 'notification:show' => pluck_param(:saved, optional: true) }
swap_modals(triggers)
@value = post.title.value
@uuid = post.uuid.value
@name = params.require(:name)
render render_path_from_attribute, layout: false
if (result_id = pluck_param(:result_id, optional: true))
headers['HX-Retarget'] = "##{result_id}"
headers['HX-Reswap'] = 'outerHTML'
@indexed_post = site.indexed_posts.find_by_post_id(post.uuid.value)
render 'posts/new_related_post', layout: false
# @todo Confirmar que esta ruta no esté transitada
else
@name = pluck_param(:name)
render render_path_from_attribute, layout: false
end
else
headers['HX-Retarget'] = "##{params.require(:form)}"
headers['HX-Reswap'] = 'outerHTML'
@ -272,7 +319,7 @@ class PostsController < ApplicationController
# @return [String]
def render_path_from_attribute
case params.require(:attribute)
case pluck_param(:attribute)
when 'new_has_many' then 'posts/new_has_many_value'
when 'new_belongs_to' then 'posts/new_belongs_to_value'
when 'new_has_and_belongs_to_many' then 'posts/new_has_many_value'

View file

@ -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

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
# Métodos reutilizables para trabajar con StrongParams
module StrongParamsHelper
# Obtiene el valor de un param
#
# @todo No hay una forma mejor de hacer esto?
# @param param [Symbol]
# @param :optional [Bool]
# @return [nil,String]
def pluck_param(param, optional: false)
if optional
params.permit(param).values.first.presence
else
params.require(param).presence
end
end
end

View file

@ -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");

View file

@ -0,0 +1,107 @@
import { Controller } from "stimulus";
/*
* Un controlador que imita a HTMX
*/
export default class extends Controller {
connect() {
// @todo Convertir en <template>
this.placeholder = "<span class=\"placeholder w-100\" aria-hidden=\"true\"></span>";
}
/*
* Obtiene la URL y elimina la acción.
*
* @param event [Event]
*/
getUrlOnce(event) {
this.getUrl(event);
event.target.dataset.action = event.target.dataset.action.replace("htmx#getUrlOnce", "").trim();
}
/*
* Lanza el evento que va a descargar la URL y agregarse en algún
* lado.
*
* @param event [Event]
*/
getUrl(event) {
// @todo Stimulus >1
const value = event.target.dataset.htmxGetUrlParam;
if (value) {
window.dispatchEvent(new CustomEvent("htmx:getUrl", { detail: { value } }));
} else {
console.error("Missing data-htmx-get-url-param attribute on element", event.target);
}
}
/*
* Realiza una petición.
*
* @param url [String]
* @return [Promise<Response>]
*/
async request(url) {
const headers = new Headers();
const signal = window.abortController?.signal;
headers.set("HX-Request", "true");
return fetch(url, { headers, signal });
}
/*
* Obtiene la URL enviada por el evento y reemplaza el contenido del
* elemento.
*/
async swap(event) {
const response = await this.request(event.detail.value);
if (response.ok) {
this.element.innerHTML = this.placeholder;
this.element.innerHTML = await response.text();
this.triggerEvents(response.headers);
window.htmx.process(this.element);
} else {
console.error(response);
}
}
/*
* Agrega el resultado de la descarga al final del elemento.
*/
async beforeend(event) {
const response = await this.request(event.detail.value);
if (response.ok) {
this.element.insertAdjacentHTML("beforeend", this.placeholder);
this.element.lastElementChild.outerHTML = await response.text();
this.triggerEvents(response.headers);
// @todo Asume que cada endpoint solo devuelve un elemento por vez
window.htmx.process(this.element.lastElementChild);
} else {
console.error(response);
}
}
/*
* Lanza los eventos que vienen con la respuesta.
*/
triggerEvents(headers) {
if (!headers.has("HX-Trigger")) return;
const events = JSON.parse(headers.get("HX-Trigger"));
setTimeout(() => {
for (const event in events) {
const detail = events[event];
window.dispatchEvent(new CustomEvent(event, { detail }));
}
}, 1);
}
}

View file

@ -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),

View file

@ -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

View file

@ -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' } }

View file

@ -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),
}
}
@ -47,41 +47,23 @@
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: 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 +74,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)

View file

@ -1,16 +1,31 @@
:ruby
local_assigns[:modal_id] ||= 'generic_modal'
image = nil
description = nil
card_id = random_id
if post.post.attribute?(:image) && (image = post.post.image.static_file)
description = post.post.image.value['description']
end
.col.mb-3.p-1{ data: { controller: 'modal' } }
.col.mb-3.p-1{ id: card_id, 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
= render 'bootstrap/btn', content: t('.edit'), data: { action: 'modal#showAnother', 'modal-show-value': local_assigns[:modal_id] }, id: random_id
-#
Si pasamos el ID del modal, asumimos que hay uno que ya existe y
lo llamamos. Sino, tenemos que abrir el modal genérico y cargarle
el formulario vía "HTMX".
- if local_assigns[:modal_id].present?
= render 'bootstrap/btn', content: t('.edit'), data: { action: 'modal#showAnother', 'modal-show-value': local_assigns[:modal_id] }, id: random_id
- else
- form_params = {}
- form_params[:layout] = post.layout
- form_params[:uuid] = post.post_id
- form_params[:modal_id] = form_params[:show] = modal_id = random_id
-# Asociar un modal con una tarjeta
- form_params[:result_id] = card_id
-# @todo Poder indicar en qué elemento queremos asociar lo descargado
= render 'bootstrap/btn', content: t('.edit'), data: { controller: 'htmx', action: 'modal#showAnother htmx#getUrlOnce', 'modal-show-value': modal_id, 'htmx-get-url-param': site_posts_modal_path(post.site, **form_params) }, id: random_id

View file

@ -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)

View file

@ -0,0 +1,55 @@
-#
Genera un modal completo con el formulario del post y sus botones de
guardado.
Se comporta como "HTMX".
:ruby
post = @post
site = post.site
locale = @locale
base = random_id
dir = t("locales.#{locale}.dir")
modal_id = pluck_param(:modal_id)
result_id = pluck_param(:result_id)
form_id = random_id
except = %i[date]
options = {
id: form_id,
multipart: true,
class: 'form post'
}
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
%div{ id: modal_id, data: { controller: 'modal' }}
= render 'bootstrap/modal', id: modal_id, modal_content_attributes: { class: 'h-100' } do
- content_for :"#{modal_id}_body" do
= form_tag url, **options do
= hidden_field_tag 'base', base
= hidden_field_tag 'result_id', result_id
= hidden_field_tag 'modal_id', modal_id
= hidden_field_tag "#{base}[layout]", post.layout.name
= render 'errors', post: post
= render 'posts/attributes', site: site, post: post, dir: dir, base: base, locale: locale, except: except
-# @todo Volver obligatorios?
- except.each do |attr|
%input{ type: 'hidden', name: "#{base}[#{attr}]", value: pluck_param(attr, optional: true) }
- content_for :"#{modal_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'
= yield(:post_form)

View file

@ -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

View file

@ -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)

View file

@ -1 +1 @@
= render 'posts/new_related_post', post: @indexed_post
= render 'posts/new_related_post', post: @indexed_post, modal_id: pluck_param(:modal_id, optional: true)

View file

@ -1 +1 @@
= render 'sites/build', site: @site, class: params.permit(:class)[:class]
= render 'sites/build', site: @site, class: pluck_param(:class, optional: true)

View file

@ -105,6 +105,7 @@ Rails.application.routes.draw do
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'
get :'posts/modal', to: 'posts#modal'
resources :posts do
get 'p/:page', action: :index, on: :collection

BIN
public/packs/js/application-bc27e0de6050d2dd9396.js (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/packs/js/application-bc27e0de6050d2dd9396.js.br (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/packs/js/application-bc27e0de6050d2dd9396.js.gz (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/packs/js/application-bc27e0de6050d2dd9396.js.map (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/packs/js/application-bc27e0de6050d2dd9396.js.map.br (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/packs/js/application-bc27e0de6050d2dd9396.js.map.gz (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/packs/manifest.json (Stored with Git LFS)

Binary file not shown.

BIN
public/packs/manifest.json.br (Stored with Git LFS)

Binary file not shown.

BIN
public/packs/manifest.json.gz (Stored with Git LFS)

Binary file not shown.