mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-24 11:26:23 +00:00
Merge branch 'issue-15068' into production.panel.sutty.nl
This commit is contained in:
commit
277b03eab1
21 changed files with 360 additions and 27 deletions
1
Gemfile
1
Gemfile
|
@ -87,6 +87,7 @@ gem 'after_commit_everywhere', '~> 1.0'
|
||||||
gem 'aasm'
|
gem 'aasm'
|
||||||
gem 'que-web'
|
gem 'que-web'
|
||||||
gem 'nanoid'
|
gem 'nanoid'
|
||||||
|
gem 'nice_partials'
|
||||||
|
|
||||||
# database
|
# database
|
||||||
gem 'hairtrigger'
|
gem 'hairtrigger'
|
||||||
|
|
|
@ -389,6 +389,8 @@ GEM
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.2.1)
|
net-ssh (7.2.1)
|
||||||
netaddr (2.0.6)
|
netaddr (2.0.6)
|
||||||
|
nice_partials (0.10.0)
|
||||||
|
actionview (>= 4.2.6)
|
||||||
nio4r (2.7.0-x86_64-linux-musl)
|
nio4r (2.7.0-x86_64-linux-musl)
|
||||||
nokogiri (1.16.4-x86_64-linux-musl)
|
nokogiri (1.16.4-x86_64-linux-musl)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
|
@ -685,6 +687,7 @@ DEPENDENCIES
|
||||||
mobility
|
mobility
|
||||||
nanoid
|
nanoid
|
||||||
net-ssh
|
net-ssh
|
||||||
|
nice_partials
|
||||||
nokogiri
|
nokogiri
|
||||||
pg
|
pg
|
||||||
pg_search
|
pg_search
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Forma de ingreso a Sutty
|
# Forma de ingreso a Sutty
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include ExceptionHandler
|
include ExceptionHandler if Rails.env.production?
|
||||||
include Pundit::Authorization
|
include Pundit::Authorization
|
||||||
|
|
||||||
protect_from_forgery with: :null_session, prepend: true
|
protect_from_forgery with: :null_session, prepend: true
|
||||||
|
@ -117,4 +117,9 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
session[:usuarie_return_to] = request.fullpath
|
session[:usuarie_return_to] = request.fullpath
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Detecta si una petición fue hecha por HTMX
|
||||||
|
def htmx?
|
||||||
|
request.headers.key? 'HX-Request'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,6 +32,33 @@ class PostsController < ApplicationController
|
||||||
render layout: false
|
render layout: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new_related_post
|
||||||
|
@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)
|
||||||
|
locale
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
swap_modals
|
||||||
|
|
||||||
|
render layout: false
|
||||||
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize Post
|
authorize Post
|
||||||
|
|
||||||
|
@ -87,7 +114,23 @@ class PostsController < ApplicationController
|
||||||
if @post.persisted?
|
if @post.persisted?
|
||||||
site.touch
|
site.touch
|
||||||
forget_content
|
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)
|
redirect_to site_post_path(@site, @post)
|
||||||
else
|
else
|
||||||
render 'posts/new'
|
render 'posts/new'
|
||||||
|
@ -185,4 +228,13 @@ class PostsController < ApplicationController
|
||||||
def service_for_direct_upload
|
def service_for_direct_upload
|
||||||
session[:service_name] = site.name.to_sym
|
session[:service_name] = site.name.to_sym
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -3,8 +3,30 @@ import { Controller } from "stimulus";
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["modal", "backdrop"];
|
static targets = ["modal", "backdrop"];
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Podemos enviar la orden de apertura como un click o como un
|
||||||
|
* CustomEvent incluyendo el id del modal como detail.
|
||||||
|
*/
|
||||||
show(event = undefined) {
|
show(event = undefined) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
const modalId = event?.detail?.id;
|
||||||
|
|
||||||
|
if (modalId && this.element.id !== modalId) return;
|
||||||
|
|
||||||
this.modalTarget.style.display = "block";
|
this.modalTarget.style.display = "block";
|
||||||
this.backdropTarget.style.display = "block";
|
this.backdropTarget.style.display = "block";
|
||||||
|
@ -22,6 +44,9 @@ export default class extends Controller {
|
||||||
|
|
||||||
hide(event = undefined) {
|
hide(event = undefined) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
const modalId = event?.detail?.id;
|
||||||
|
|
||||||
|
if (modalId && this.element.id !== modalId) return;
|
||||||
|
|
||||||
this.backdropTarget.classList.remove("show");
|
this.backdropTarget.classList.remove("show");
|
||||||
this.modalTarget.classList.remove("show");
|
this.modalTarget.classList.remove("show");
|
||||||
|
|
4
app/models/metadata_new_has_many.rb
Normal file
4
app/models/metadata_new_has_many.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Interfaz nueva para uno a muchos
|
||||||
|
class MetadataNewHasMany < MetadataHasMany; end
|
4
app/models/metadata_new_predefined_array.rb
Normal file
4
app/models/metadata_new_predefined_array.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Nueva interfaz para arrays predefinidos
|
||||||
|
class MetadataNewPredefinedArray < MetadataPredefinedArray; end
|
|
@ -26,6 +26,13 @@ class MetadataRelatedPosts < MetadataArray
|
||||||
posts.where(uuid: value).map(&:title).map(&:value)
|
posts.where(uuid: value).map(&:title).map(&:value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Encuentra el filtro
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def filter
|
||||||
|
layout.metadata.dig(name, 'filter')&.to_h&.symbolize_keys || {}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Obtiene todos los posts y opcionalmente los filtra
|
# 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})"
|
"#{post&.title&.value || post&.slug&.value} #{post&.date&.value.strftime('%F')} (#{post.layout.humanized_name})"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Encuentra el filtro
|
|
||||||
def filter
|
|
||||||
layout.metadata.dig(name, 'filter')&.to_h&.symbolize_keys || {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitize(uuid)
|
def sanitize(uuid)
|
||||||
super(uuid.map do |u|
|
super(uuid.map do |u|
|
||||||
u.to_s.gsub(/[^a-f0-9\-]/i, '')
|
u.to_s.gsub(/[^a-f0-9\-]/i, '')
|
||||||
|
|
|
@ -15,7 +15,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
post.draft.value = true if site.invitade? usuarie
|
post.draft.value = true if site.invitade? usuarie
|
||||||
post.assign_attributes(post_params)
|
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?
|
post.slug.value = p[:slug] if p[:slug].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
#
|
#
|
||||||
# { uuid => 2, uuid => 1, uuid => 0 }
|
# { uuid => 2, uuid => 1, uuid => 0 }
|
||||||
def reorder
|
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)
|
posts = site.posts(lang: locale).where(uuid: reorder.keys)
|
||||||
|
|
||||||
files = posts.map do |post|
|
files = posts.map do |post|
|
||||||
|
@ -110,6 +110,13 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
|
|
||||||
private
|
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
|
# Una lista de archivos a modificar
|
||||||
#
|
#
|
||||||
# @return [Set]
|
# @return [Set]
|
||||||
|
@ -131,7 +138,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
|
|
||||||
# Solo permitir cambiar estos atributos de cada articulo
|
# Solo permitir cambiar estos atributos de cada articulo
|
||||||
def post_params
|
def post_params
|
||||||
@post_params ||= params.require(:post).permit(post.params).to_h
|
@post_params ||= params.require(base).permit(post.params).to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
# Eliminar metadatos internos
|
# Eliminar metadatos internos
|
||||||
|
|
41
app/views/posts/_htmx_form.haml
Normal file
41
app/views/posts/_htmx_form.haml
Normal file
|
@ -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
|
2
app/views/posts/_new_related_post.haml
Normal file
2
app/views/posts/_new_related_post.haml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.col
|
||||||
|
%p= link_to post.title, post.path
|
6
app/views/posts/attribute_ro/_new_has_many.haml
Normal file
6
app/views/posts/attribute_ro/_new_has_many.haml
Normal file
|
@ -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)
|
5
app/views/posts/attribute_ro/_new_predefined_array.haml
Normal file
5
app/views/posts/attribute_ro/_new_predefined_array.haml
Normal file
|
@ -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
|
92
app/views/posts/attributes/_new_has_many.haml
Normal file
92
app/views/posts/attributes/_new_has_many.haml
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
-#
|
||||||
|
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}"
|
||||||
|
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-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: 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 }
|
||||||
|
%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 }
|
48
app/views/posts/attributes/_new_predefined_array.haml
Normal file
48
app/views/posts/attributes/_new_predefined_array.haml
Normal file
|
@ -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'
|
7
app/views/posts/form.haml
Normal file
7
app/views/posts/form.haml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
-#
|
||||||
|
El formulario sin ninguna decoración, para incluir dentro de otros
|
||||||
|
elementos.
|
||||||
|
|
||||||
|
@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)
|
2
app/views/posts/new_has_many_value.haml
Normal file
2
app/views/posts/new_has_many_value.haml
Normal file
|
@ -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
|
|
@ -4,7 +4,7 @@
|
||||||
%p.lead= t('.help')
|
%p.lead= t('.help')
|
||||||
- if policy(Site).new?
|
- if policy(Site).new?
|
||||||
= link_to t('sites.new.title'), new_site_path,
|
= link_to t('sites.new.title'), new_site_path,
|
||||||
class: 'btn btn-secondary'
|
class: 'btn btn-secondary'
|
||||||
|
|
||||||
%section.col
|
%section.col
|
||||||
- if @sites.empty?
|
- if @sites.empty?
|
||||||
|
@ -15,11 +15,14 @@
|
||||||
%tbody
|
%tbody
|
||||||
- @sites.each do |site|
|
- @sites.each do |site|
|
||||||
- next unless site.jekyll?
|
- next unless site.jekyll?
|
||||||
|
|
||||||
%tr
|
%tr
|
||||||
%td
|
%td
|
||||||
%h2
|
%h2
|
||||||
- if policy(site).show?
|
- if policy(site).show?
|
||||||
= link_to site.title, site_posts_path(site, locale: site.default_locale)
|
= link_to site.title,
|
||||||
|
site_posts_path(site,
|
||||||
|
locale: site.default_locale)
|
||||||
- else
|
- else
|
||||||
= site.title
|
= site.title
|
||||||
%p.lead= site.description
|
%p.lead= site.description
|
||||||
|
@ -28,27 +31,27 @@
|
||||||
= link_to t('.visit'), site.url, class: 'btn btn-secondary'
|
= link_to t('.visit'), site.url, class: 'btn btn-secondary'
|
||||||
- if current_usuarie.rol_for_site(site).temporal?
|
- if current_usuarie.rol_for_site(site).temporal?
|
||||||
= render 'components/btn_base',
|
= render 'components/btn_base',
|
||||||
text: t('sites.invitations.accept'),
|
text: t('sites.invitations.accept'),
|
||||||
path: site_usuaries_accept_invitation_path(site),
|
path: site_usuaries_accept_invitation_path(site),
|
||||||
title: t('help.sites.invitations.accept'),
|
title: t('help.sites.invitations.accept'),
|
||||||
class: 'btn-secondary'
|
class: 'btn-secondary'
|
||||||
= render 'components/btn_base',
|
= render 'components/btn_base',
|
||||||
text: t('sites.invitations.reject'),
|
text: t('sites.invitations.reject'),
|
||||||
path: site_usuaries_reject_invitation_path(site),
|
path: site_usuaries_reject_invitation_path(site),
|
||||||
title: t('help.sites.invitations.reject'),
|
title: t('help.sites.invitations.reject'),
|
||||||
class: 'btn-secondary'
|
class: 'btn-secondary'
|
||||||
- else
|
- else
|
||||||
- if policy(site).show?
|
- if policy(site).show?
|
||||||
= render 'layouts/btn_with_tooltip',
|
= render 'layouts/btn_with_tooltip',
|
||||||
tooltip: t('help.sites.edit_posts'),
|
tooltip: t('help.sites.edit_posts'),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
link: site_path(site),
|
link: site_path(site),
|
||||||
text: t('sites.posts')
|
text: t('sites.posts')
|
||||||
= render 'sites/build', site: site
|
= render 'sites/build', site: site
|
||||||
= render 'sites/moderation_queue', site: site
|
= render 'sites/moderation_queue', site: site
|
||||||
- if policy(SiteUsuarie.new(site, current_usuarie)).index?
|
- if policy(SiteUsuarie.new(site, current_usuarie)).index?
|
||||||
= render 'layouts/btn_with_tooltip',
|
= render 'layouts/btn_with_tooltip',
|
||||||
tooltip: t('usuaries.index.help.self'),
|
tooltip: t('usuaries.index.help.self'),
|
||||||
text: t('usuaries.index.title'),
|
text: t('usuaries.index.title'),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
link: site_usuaries_path(site)
|
link: site_usuaries_path(site)
|
||||||
|
|
|
@ -720,6 +720,17 @@ en:
|
||||||
new_array:
|
new_array:
|
||||||
remove: "Remove"
|
remove: "Remove"
|
||||||
attributes:
|
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:
|
new_array:
|
||||||
edit: "Edit"
|
edit: "Edit"
|
||||||
filter: "Start typing to filter..."
|
filter: "Start typing to filter..."
|
||||||
|
|
|
@ -728,6 +728,17 @@ es:
|
||||||
new_array:
|
new_array:
|
||||||
remove: "Eliminar"
|
remove: "Eliminar"
|
||||||
attributes:
|
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:
|
new_array:
|
||||||
edit: "Editar"
|
edit: "Editar"
|
||||||
filter: "Empezá a escribir para filtrar..."
|
filter: "Empezá a escribir para filtrar..."
|
||||||
|
|
|
@ -102,6 +102,8 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
get :'posts/new_array', to: 'posts#new_array'
|
get :'posts/new_array', to: 'posts#new_array'
|
||||||
get :'posts/new_array_value', to: 'posts#new_array_value'
|
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
|
resources :posts do
|
||||||
get 'p/:page', action: :index, on: :collection
|
get 'p/:page', action: :index, on: :collection
|
||||||
|
|
Loading…
Reference in a new issue