✨ (auth): Añade idioma y tema preferidos al usuario
- CurrentUser pasa de enum a struct con `id` opcional (anónimo = None) y preferencias de idioma, zona horaria y tema validadas en `with_*()`. - `RequestLocale` tiene en cuenta el idioma preferido del usuario, y Context usa su tema si lo tiene (`default_theme()` en otro caso). - Nuevos componentes `form::SelectLanguage`, `form::SelectTheme` y `form::SelectTimezone`, con `Timezone::supported_by_region()` para listar y validar las zonas IANA ofrecidas.
This commit is contained in:
parent
35f2b66161
commit
70744a51a5
25 changed files with 1162 additions and 190 deletions
|
|
@ -18,6 +18,15 @@ pub mod radio;
|
|||
|
||||
pub mod select;
|
||||
|
||||
mod select_language;
|
||||
pub use select_language::SelectLanguage;
|
||||
|
||||
mod select_theme;
|
||||
pub use select_theme::SelectTheme;
|
||||
|
||||
mod select_timezone;
|
||||
pub use select_timezone::SelectTimezone;
|
||||
|
||||
pub mod input;
|
||||
|
||||
mod number;
|
||||
|
|
|
|||
130
src/base/component/form/select_language.rs
Normal file
130
src/base/component/form/select_language.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente para **elegir un idioma** de la lista de idiomas soportados por PageTop.
|
||||
///
|
||||
/// Ofrece un elemento por cada idioma de [`Locale::supported_languages()`], con su identificador
|
||||
/// como valor (p. ej. `"es-ES"`) y su nombre traducido como etiqueta, ordenados por ese nombre en
|
||||
/// el idioma de la página. Se renderiza como cualquier [`form::select::Field`].
|
||||
///
|
||||
/// La primera opción, con valor vacío, depende de si el campo es obligatorio:
|
||||
///
|
||||
/// - Si no lo es (por defecto), siempre se incluye y propone usar el idioma del sitio. Quedarse sin
|
||||
/// idioma propio es válido y significa usar el predeterminado de la aplicación. Se selecciona
|
||||
/// cuando el valor elegido no corresponde a ningún idioma de la lista.
|
||||
/// - Si lo es ([`with_required(true)`](Self::with_required)), sólo se incluye cuando el valor
|
||||
/// seleccionado no corresponde a ningún idioma de la lista, y pide elegir uno; así el navegador
|
||||
/// no deja enviar el formulario sin elegir un idioma. El valor recibido debe validarse
|
||||
/// igualmente en el servidor.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let language = form::SelectLanguage::new()
|
||||
/// .with_name("language")
|
||||
/// .with_label(Lc::n("Language"))
|
||||
/// .with_selected("es-ES");
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct SelectLanguage {
|
||||
/// Devuelve la lista de selección interna con la configuración común (nombre, etiqueta, ayuda,
|
||||
/// propiedades...), todavía sin opciones; éstas se añaden al renderizar.
|
||||
field: form::select::Field,
|
||||
/// Devuelve el identificador del idioma seleccionado.
|
||||
selected: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for SelectLanguage {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.field().id()
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let mut field = self.field().clone();
|
||||
let mut languages = Locale::supported_languages();
|
||||
languages.sort_by_cached_key(|(_, name)| name.collation_key(&*cx));
|
||||
|
||||
let selected = Locale::resolve(self.selected()).as_option();
|
||||
let known = selected.is_some();
|
||||
if !field.required() {
|
||||
let default_langid = Locale::default_langid();
|
||||
let default_name = languages
|
||||
.iter()
|
||||
.find(|(langid, _)| *langid == default_langid)
|
||||
.and_then(|(_, name)| name.lookup(cx))
|
||||
.unwrap_or_else(|| default_langid.to_string());
|
||||
let label = Lc::l("select_language_site_default").with_arg("language", default_name);
|
||||
field.alter_item(form::select::Item::new("", label).with_selected(!known));
|
||||
} else if !known {
|
||||
let label = Lc::l("select_language_placeholder");
|
||||
field.alter_item(form::select::Item::new("", label).with_selected(true));
|
||||
}
|
||||
|
||||
for (langid, name) in languages {
|
||||
let item = form::select::Item::new(langid.to_string(), name);
|
||||
field.alter_item(item.with_selected(selected == Some(langid)));
|
||||
}
|
||||
Ok(field.render(cx).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[builder_impl]
|
||||
impl SelectLanguage {
|
||||
// **< SelectLanguage BUILDER >*****************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.field.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
|
||||
self.field.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo.
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.field.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la etiqueta del campo.
|
||||
pub fn with_label(mut self, label: Lc) -> Self {
|
||||
self.field.alter_label(label);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el texto de ayuda del campo.
|
||||
pub fn with_help_text(mut self, help_text: Lc) -> Self {
|
||||
self.field.alter_help_text(help_text);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio, lo que cambia su primera opción (ver
|
||||
/// [`SelectLanguage`]).
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.field.alter_required(required);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo está deshabilitado.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.field.alter_disabled(disabled);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el identificador del idioma seleccionado (p. ej. `"es-ES"`). Se resuelve con
|
||||
/// [`Locale::resolve()`], así que también acepta alias o variantes (`"es"`, `"es-es"`...) del
|
||||
/// mismo idioma.
|
||||
pub fn with_selected(mut self, selected: impl Into<String>) -> Self {
|
||||
self.selected = selected.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
140
src/base/component/form/select_theme.rs
Normal file
140
src/base/component/form/select_theme.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente para **elegir un tema** de los temas habilitados en la aplicación.
|
||||
///
|
||||
/// Ofrece un elemento por cada tema de [`enabled_themes()`], con su nombre corto como valor (p. ej.
|
||||
/// `"Bootsier"`) y su nombre traducido como etiqueta, ordenados por ese nombre en el idioma de la
|
||||
/// página. Se renderiza como cualquier [`form::select::Field`].
|
||||
///
|
||||
/// Si sólo hay un tema habilitado, la lista se muestra deshabilitada con ese tema seleccionado (un
|
||||
/// campo deshabilitado no se envía con el formulario). Si hay varios, la primera opción, con valor
|
||||
/// vacío, depende de si el campo es obligatorio:
|
||||
///
|
||||
/// - Si no lo es (por defecto), siempre se incluye y propone usar el tema del sitio. Quedarse sin
|
||||
/// tema propio es válido y significa usar el predeterminado de la aplicación. Se selecciona
|
||||
/// cuando el valor elegido no corresponde a ningún tema de la lista (sin distinguir mayúsculas y
|
||||
/// minúsculas).
|
||||
/// - Si lo es ([`with_required(true)`](Self::with_required)), sólo se incluye cuando el valor
|
||||
/// seleccionado no corresponde a ningún tema de la lista, y pide elegir uno; así el navegador no
|
||||
/// deja enviar el formulario sin elegir un tema. El valor recibido debe validarse igualmente en
|
||||
/// el servidor.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let theme = form::SelectTheme::new()
|
||||
/// .with_name("theme")
|
||||
/// .with_label(Lc::n("Theme"))
|
||||
/// .with_selected("Basic");
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct SelectTheme {
|
||||
/// Devuelve la lista de selección interna con la configuración común (nombre, etiqueta, ayuda,
|
||||
/// propiedades...), todavía sin opciones; éstas se añaden al renderizar.
|
||||
field: form::select::Field,
|
||||
/// Devuelve el nombre corto del tema seleccionado.
|
||||
selected: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for SelectTheme {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.field().id()
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let mut field = self.field().clone();
|
||||
let mut themes = enabled_themes();
|
||||
|
||||
if let [theme] = themes[..] {
|
||||
field.alter_disabled(true);
|
||||
field.alter_item(
|
||||
form::select::Item::new(theme.short_name(), theme.name()).with_selected(true),
|
||||
);
|
||||
return Ok(field.render(cx).await);
|
||||
}
|
||||
|
||||
themes.sort_by_cached_key(|theme| theme.name().collation_key(&*cx));
|
||||
let is_selected =
|
||||
|theme: ThemeRef| theme.short_name().eq_ignore_ascii_case(self.selected());
|
||||
|
||||
let known = themes.iter().any(|theme| is_selected(*theme));
|
||||
if !field.required() {
|
||||
let default_theme = default_theme();
|
||||
let default_name = default_theme
|
||||
.name()
|
||||
.lookup(cx)
|
||||
.unwrap_or_else(|| default_theme.short_name().to_owned());
|
||||
let label = Lc::l("select_theme_site_default").with_arg("theme", default_name);
|
||||
field.alter_item(form::select::Item::new("", label).with_selected(!known));
|
||||
} else if !known {
|
||||
let label = Lc::l("select_theme_placeholder");
|
||||
field.alter_item(form::select::Item::new("", label).with_selected(true));
|
||||
}
|
||||
|
||||
for theme in themes {
|
||||
let item = form::select::Item::new(theme.short_name(), theme.name());
|
||||
field.alter_item(item.with_selected(is_selected(theme)));
|
||||
}
|
||||
Ok(field.render(cx).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[builder_impl]
|
||||
impl SelectTheme {
|
||||
// **< SelectTheme BUILDER >********************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.field.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
|
||||
self.field.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo.
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.field.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la etiqueta del campo.
|
||||
pub fn with_label(mut self, label: Lc) -> Self {
|
||||
self.field.alter_label(label);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el texto de ayuda del campo.
|
||||
pub fn with_help_text(mut self, help_text: Lc) -> Self {
|
||||
self.field.alter_help_text(help_text);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio, lo que cambia su primera opción (ver [`SelectTheme`]).
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.field.alter_required(required);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo está deshabilitado. Con un solo tema habilitado lo está siempre.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.field.alter_disabled(disabled);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre corto del tema seleccionado (p. ej. `"Bootsier"`). Vacío, o uno que no
|
||||
/// esté habilitado, selecciona la primera opción (ver [`SelectTheme`]).
|
||||
pub fn with_selected(mut self, selected: impl Into<String>) -> Self {
|
||||
self.selected = selected.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
142
src/base/component/form/select_timezone.rs
Normal file
142
src/base/component/form/select_timezone.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente para crear una **lista de selección de zonas horarias** IANA.
|
||||
///
|
||||
/// Ofrece las zonas horarias de [`Timezone::supported_by_region()`] agrupadas por región, con el
|
||||
/// nombre de la región traducido (*"Europa"*, *"América"*...) y el nombre IANA completo como valor
|
||||
/// y como etiqueta (p. ej. `"Europe/Madrid"`). No admite opciones libres. Se renderiza como
|
||||
/// cualquier [`form::select::Field`].
|
||||
///
|
||||
/// La primera opción, con valor vacío, depende de si el campo es obligatorio:
|
||||
///
|
||||
/// - Si no lo es (por defecto), siempre se incluye y propone usar la zona horaria del sitio.
|
||||
/// Quedarse sin zona propia es válido y significa usar la predeterminada de la aplicación. Se
|
||||
/// selecciona cuando el valor elegido no corresponde a ninguna zona de la lista.
|
||||
/// - Si lo es ([`with_required(true)`](Self::with_required)), sólo se incluye cuando el valor
|
||||
/// seleccionado no corresponde a ninguna zona de la lista, y pide elegir una; así el navegador no
|
||||
/// deja enviar el formulario sin elegir una zona horaria. El valor recibido debe validarse
|
||||
/// igualmente en el servidor.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let timezone = form::SelectTimezone::new()
|
||||
/// .with_name("timezone")
|
||||
/// .with_label(Lc::n("Time zone"))
|
||||
/// .with_selected("Europe/Madrid");
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct SelectTimezone {
|
||||
/// Devuelve la lista de selección interna con la configuración común (nombre, etiqueta, ayuda,
|
||||
/// propiedades...), todavía sin opciones; éstas se añaden al renderizar.
|
||||
field: form::select::Field,
|
||||
/// Devuelve el nombre IANA de la zona horaria seleccionada.
|
||||
selected: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for SelectTimezone {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.field().id()
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let mut field = self.field().clone();
|
||||
let regions = Timezone::supported_by_region();
|
||||
let known = regions
|
||||
.iter()
|
||||
.any(|(_, names)| names.contains(&self.selected()));
|
||||
if !field.required() {
|
||||
let label = Lc::l("select_timezone_site_default")
|
||||
.with_arg("timezone", Timezone::default_tz().name());
|
||||
field.alter_item(form::select::Item::new("", label).with_selected(!known));
|
||||
} else if !known {
|
||||
let label = Lc::l("select_timezone_placeholder");
|
||||
field.alter_item(form::select::Item::new("", label).with_selected(true));
|
||||
}
|
||||
for (region, names) in regions {
|
||||
let mut group = form::select::Group::new(match *region {
|
||||
"Africa" => Lc::l("timezone_region_africa"),
|
||||
"America" => Lc::l("timezone_region_america"),
|
||||
"Antarctica" => Lc::l("timezone_region_antarctica"),
|
||||
"Arctic" => Lc::l("timezone_region_arctic"),
|
||||
"Asia" => Lc::l("timezone_region_asia"),
|
||||
"Atlantic" => Lc::l("timezone_region_atlantic"),
|
||||
"Australia" => Lc::l("timezone_region_australia"),
|
||||
"Etc" => Lc::l("timezone_region_etc"),
|
||||
"Europe" => Lc::l("timezone_region_europe"),
|
||||
"Indian" => Lc::l("timezone_region_indian"),
|
||||
"Pacific" => Lc::l("timezone_region_pacific"),
|
||||
_ => Lc::n(*region),
|
||||
});
|
||||
for name in names {
|
||||
let selected = *name == self.selected();
|
||||
group.alter_item(
|
||||
form::select::Item::new(*name, Lc::n(*name)).with_selected(selected),
|
||||
);
|
||||
}
|
||||
field.alter_group(group);
|
||||
}
|
||||
Ok(field.render(cx).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[builder_impl]
|
||||
impl SelectTimezone {
|
||||
// **< SelectTimezone BUILDER >*****************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.field.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
|
||||
self.field.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo.
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.field.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la etiqueta del campo.
|
||||
pub fn with_label(mut self, label: Lc) -> Self {
|
||||
self.field.alter_label(label);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el texto de ayuda del campo.
|
||||
pub fn with_help_text(mut self, help_text: Lc) -> Self {
|
||||
self.field.alter_help_text(help_text);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio, lo que cambia su primera opción (ver
|
||||
/// [`SelectTimezone`]).
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.field.alter_required(required);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo está deshabilitado.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.field.alter_disabled(disabled);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre IANA de la zona horaria seleccionada (p. ej. `"Europe/Madrid"`). Vacía,
|
||||
/// o una que no esté en la lista, selecciona la primera opción (ver [`SelectTimezone`]).
|
||||
pub fn with_selected(mut self, selected: impl Into<String>) -> Self {
|
||||
self.selected = selected.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue