edit user form

This commit is contained in:
euzu
2025-08-21 17:18:44 +02:00
parent 8a34673033
commit 64ae45ea36
8 changed files with 142 additions and 62 deletions
@@ -12,4 +12,12 @@
height: 1rem;
}
}
&__option {
cursor: pointer;
}
&__option-item {
pointer-events: none;
}
}
+6 -1
View File
@@ -16,6 +16,8 @@
border: 1px solid var(--input-border-color);
border-radius: var(--border-radius);
background-color: var(--input-background-color);
cursor: pointer;
&:focus-within {
border-color: var(--input-focus-border-color);
}
@@ -39,7 +41,10 @@
&__selected {
display: flex;
flex-flow: row nowrap;
flex-flow: row wrap;
align-items: center;
flex: 1 1 auto;
gap: var(--gap-default);
pointer-events: none;
}
}
@@ -1,9 +1,8 @@
use std::collections::HashSet;
use std::rc::Rc;
use web_sys::MouseEvent;
use yew::{classes, function_component, html, use_node_ref, use_state, Callback, Html, Properties, TargetCast};
use yew::{classes, function_component, html, use_state, Callback, Html, NodeRef, Properties};
use yew_hooks::use_set;
use yew_i18n::use_translation;
use crate::app::components::{AppIcon, IconButton};
use crate::app::components::popup_menu::PopupMenu;
use crate::html_if;
@@ -11,13 +10,13 @@ use crate::html_if;
#[derive(Clone, PartialEq, Debug)]
pub struct DropDownOption {
pub(crate) id: String,
pub(crate) label: String,
pub(crate) label: Html,
pub(crate) selected: bool,
}
impl DropDownOption {
pub fn new(id: &str, label: &str, selected: bool) -> Self {
Self { id: id.to_owned(), label: label.to_owned(), selected }
pub fn new(id: &str, label: Html, selected: bool) -> Self {
Self { id: id.to_owned(), label, selected }
}
}
@@ -28,18 +27,19 @@ pub struct DropDownIconButtonProps {
pub onselect: Callback<(String, Vec<String>)>,
#[prop_or_default]
pub class: String,
pub options: Rc<Vec<DropDownOption>>,
pub options: Vec<Rc<DropDownOption>>,
#[prop_or_default]
pub multi_select: bool,
#[prop_or_default]
pub button_ref: Option<NodeRef>,
}
#[function_component]
pub fn DropDownIconButton(props: &DropDownIconButtonProps) -> Html {
let translate = use_translation();
let button_ref = use_node_ref();
let button_ref = props.button_ref.clone().unwrap_or_default();
let popup_anchor_ref = use_state(|| None::<web_sys::Element>);
let popup_is_open = use_state(|| false);
let selections = use_set(props.options.as_ref().iter().filter(|x| x.selected).map(|x|x.id.clone()).collect::<HashSet<String>>());
let selections = use_set(props.options.iter().filter(|x| x.selected).map(|x|x.id.clone()).collect::<HashSet<String>>());
let handle_popup_close = {
let set_is_open = popup_is_open.clone();
@@ -49,11 +49,13 @@ pub fn DropDownIconButton(props: &DropDownIconButtonProps) -> Html {
};
let handle_click = {
let button_ref = button_ref.clone();
let set_anchor_ref = popup_anchor_ref.clone();
let set_is_open = popup_is_open.clone();
Callback::from(move |(_name, event): (String, MouseEvent)| {
if let Some(target) = event.target_dyn_into::<web_sys::Element>() {
set_anchor_ref.set(Some(target));
event.prevent_default();
if let Some(button) = button_ref.cast::<web_sys::Element>() {
set_anchor_ref.set(Some(button));
set_is_open.set(true);
}
})
@@ -64,7 +66,7 @@ pub fn DropDownIconButton(props: &DropDownIconButtonProps) -> Html {
let name = props.name.clone();
let selections = selections.clone();
let onselect = props.onselect.clone();
let close_popup = handle_popup_close.clone();
let set_is_open = popup_is_open.clone();
Callback::from(move |(id, e): (String, MouseEvent)| {
e.prevent_default();
if selections.current().contains(&id) {
@@ -79,7 +81,7 @@ pub fn DropDownIconButton(props: &DropDownIconButtonProps) -> Html {
};
onselect.emit((name.clone(), selected_options));
if !multi_select {
close_popup.emit(());
set_is_open.set(false);
}
})
};
@@ -109,11 +111,11 @@ pub fn DropDownIconButton(props: &DropDownIconButtonProps) -> Html {
}
})
}
<label>{ translate.t(&o.label) }</label>
<span class={"tp__dropdown-icon-button__option-item"}>{o.label.clone()}</span>
</div>
}})
}
</PopupMenu>
</>
</>
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ pub struct SearchProps {
#[prop_or_default]
pub class: String,
#[prop_or_default]
pub options: Option<Rc<Vec<DropDownOption>>>,
pub options: Option<Vec<Rc<DropDownOption>>>,
pub onsearch: Option<Callback<SearchRequest>>,
#[prop_or(3)]
pub min_length: usize,
+29 -25
View File
@@ -1,63 +1,67 @@
use std::rc::Rc;
use log::warn;
use yew::prelude::*;
use crate::app::components::{DropDownIconButton, DropDownOption, Tag, TagList};
use crate::app::components::{DropDownIconButton, DropDownOption};
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct SelectProps {
pub name: String,
#[prop_or_default]
pub icon: Option<String>,
pub onselect: Callback<(String, Vec<String>)>,
pub onselect: Callback<(String, Vec<Rc<DropDownOption>>)>,
#[prop_or_default]
pub class: String,
pub options: Rc<Vec<DropDownOption>>,
pub options: Vec<Rc<DropDownOption>>,
#[prop_or_default]
pub multi_select: bool,
}
fn create_tag_from_str(s: &str) -> Rc<Tag> {
Rc::new(Tag { label: s.to_owned(), class: Some("active".to_string()) })
}
fn create_tag_from_option(o: &DropDownOption) -> Rc<Tag> {
create_tag_from_str(o.label.as_str())
}
#[function_component]
pub fn Select(props: &SelectProps) -> Html {
let button_ref = use_node_ref();
// TODO render for selections !!!
let selected_options = use_state(|| props.options.as_ref().iter().filter(|o| o.selected)
.map(create_tag_from_option)
.collect::<Vec<Rc<Tag>>>());
let selected_options = use_state(|| props.options.iter().filter(|o| o.selected)
.map(|o| o.label.clone()).collect::<Vec<Html>>());
{
let set_selected_options = selected_options.clone();
use_effect_with(props.options.clone(), move |options| {
let selections = options.as_ref().iter().filter(|o| o.selected)
.map(create_tag_from_option)
.collect::<Vec<Rc<Tag>>>();
use_effect_with(props.options.clone(), move |options: &Vec<Rc<DropDownOption>>| {
let selections = options.iter().filter(|o| o.selected)
.map(|o| o.label.clone()).collect::<Vec<Html>>();
set_selected_options.set(selections);
});
}
let handle_options_click = {
let onselect = props.onselect.clone();
let options = props.options.clone();
let set_selections = selected_options.clone();
Callback::from(move |(name, selections): (String, Vec<String>)| {
set_selections.set(selections.iter().map(|s|s.as_str()).map(create_tag_from_str).collect::<Vec<Rc<Tag>>>());
onselect.emit((name, selections));
let selected_options: Vec<Rc<DropDownOption>> = options.iter().filter(|&o| selections.contains(&o.id)).cloned().collect();
set_selections.set(selected_options.iter().map(|o| o.label.clone()).collect());
onselect.emit((name, selected_options));
})
};
let handle_click_button = {
let button_ref = button_ref.clone();
Callback::from(move |event: MouseEvent| {
if let Some(target) = event.target_dyn_into::<web_sys::Element>() {
if target.class_name().contains("tp__select-wrapper") {
if let Some(button) = button_ref.cast::<web_sys::HtmlInputElement>() {
button.click();
}
}
}
})
};
html! {
<div class={classes!("tp__select", props.class.clone())}>
<div class="tp__select-wrapper">
<div class="tp__select-wrapper" onclick={handle_click_button}>
<div class="tp__select__selected">
<TagList tags={(*selected_options).clone()} />
{(*selected_options).clone()}
</div>
<DropDownIconButton
button_ref={button_ref}
multi_select={props.multi_select}
options={props.options.clone()}
name={props.name.clone()}
+26 -1
View File
@@ -2,11 +2,36 @@ use yew::prelude::*;
use yew_i18n::use_translation;
use crate::app::components::{UserlistContext, UserlistPage, TextButton, Card};
use crate::app::components::userlist::proxy_user_credentials_form::ProxyUserCredentialsForm;
use crate::app::{ConfigContext, PlaylistContext};
#[function_component]
pub fn UserEdit() -> Html {
let translate = use_translation();
let userlist_ctx = use_context::<UserlistContext>().expect("Userlist context not found");
let playlist_ctx = use_context::<PlaylistContext>().expect("Playlist context not found");
let config_ctx = use_context::<ConfigContext>().expect("Config context not found");
let targets = use_memo(playlist_ctx.clone(), |playlist_ctx| {
match playlist_ctx.sources.as_ref() {
None => vec![],
Some(sources) => sources.iter().map(|(_, t)| t)
.flatten()
.cloned()
.collect()
}
});
let server = use_memo(config_ctx.clone(), |config_ctx| {
match config_ctx.config.as_ref() {
None => vec![],
Some(app_config) => {
match app_config.api_proxy.as_ref() {
None => vec![],
Some(api_proxy) => api_proxy.server.iter().cloned().collect()
}
}
}
});
let handle_back = {
let userlist_ctx = userlist_ctx.clone();
@@ -26,7 +51,7 @@ pub fn UserEdit() -> Html {
</div>
<div class="tp__userlist-edit__body tp__list-create__body">
<Card>
<ProxyUserCredentialsForm user={(*userlist_ctx.selected_user).clone()}/>
<ProxyUserCredentialsForm server={server.clone()} targets={targets.clone()} user={(*userlist_ctx.selected_user).clone()}/>
</Card>
</div>
</div>
+6 -5
View File
@@ -8,11 +8,12 @@ use crate::app::components::userlist::user_table::UserTable;
pub fn UserlistList() -> Html {
let translate = use_translation();
let userlist_ctx = use_context::<UserlistContext>().expect("Userlist context not found");
let search_fields = use_memo((), |_| Rc::new(vec![
DropDownOption::new("username", "LABEL.NAME", true),
DropDownOption::new("playlist", "LABEL.PLAYLIST", false),
DropDownOption::new("server", "LABEL.SERVER", false),
]));
let search_fields = use_memo((), |_| vec![
Rc::new(DropDownOption::new("username", html! { translate.t("LABEL.NAME") }, true)),
Rc::new(DropDownOption::new("playlist", html! { translate.t("LABEL.PLAYLIST") }, false)),
Rc::new(DropDownOption::new("server", html! { translate.t("LABEL.SERVER") }, false)),
]);
let handle_create = {
let userlist_ctx = userlist_ctx.clone();
@@ -3,15 +3,17 @@ use std::rc::Rc;
use log::warn;
use yew::prelude::*;
use yew_i18n::use_translation;
use shared::model::{ProxyUserCredentialsDto, ProxyUserStatus};
use shared::model::{ApiProxyServerInfoDto, ConfigTargetDto, ProxyUserCredentialsDto, ProxyUserStatus};
use crate::app::TargetUser;
use crate::{edit_field_bool, edit_field_text, edit_field_text_option};
use crate::app::components::select::Select;
use crate::app::components::{convert_bool_to_chip_style, Chip, DropDownOption, TextButton};
use crate::app::components::{DropDownOption, TextButton, UserStatus};
#[derive(Properties, PartialEq, Clone)]
pub struct ProxyUserCredentialsFormProps {
pub user: Option<Rc<TargetUser>>,
pub targets: Rc<Vec<Rc<ConfigTargetDto>>>,
pub server: Rc<Vec<ApiProxyServerInfoDto>>,
}
#[function_component]
@@ -21,6 +23,22 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|user| RefCell::new(user.as_ref()
.map_or_else(|| ProxyUserCredentialsDto::default(), |usr| usr.credentials.as_ref().clone())));
let targets = use_memo((props.targets.clone(), props.user.clone()), |(targets, user)|
targets.iter().map(|t| Rc::new(DropDownOption {
id: t.name.to_string(),
label: html! { t.name.clone() },
selected: user.as_ref().is_some_and(|user| user.target == t.name),
})).collect::<Vec<Rc<DropDownOption>>>()
);
let server = use_memo((props.server.clone(), props.user.clone()), |(server, user)|
server.iter().map(|s| Rc::new(DropDownOption {
id: s.name.to_string(),
label: html! { s.name.clone() },
selected: user.as_ref().is_some_and(|user| user.credentials.server.as_ref() == Some(&s.name)),
})).collect::<Vec<Rc<DropDownOption>>>()
);
let proxy_user_status = use_memo(props.user.clone(), |user|
vec![
ProxyUserStatus::Active,
@@ -29,11 +47,11 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
ProxyUserStatus::Trial,
ProxyUserStatus::Disabled,
ProxyUserStatus::Pending,
].iter().map(|s| DropDownOption {
].iter().map(|s| Rc::new(DropDownOption {
id: s.to_string(),
label: format!("LABEL.USER_STATUS_{}", s.to_string().to_uppercase()),
label: html! { <UserStatus status={Some(s.clone())} /> },
selected: user.as_ref().is_some_and(|user| user.credentials.status.as_ref() == Some(s)),
}).collect::<Vec<DropDownOption>>()
})).collect::<Vec<Rc<DropDownOption>>>()
);
let handle_save_user = {
@@ -43,39 +61,56 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
})
};
let user_active = props.user.as_ref().is_some_and(|u| u.credentials.is_active());
html! {
<div class="tp__proxy-user-credentials-form tp__form-page">
<div class="tp__proxy-user-credentials-form__body tp__form-page__body">
<div class="tp__config-field tp__config-field__bool">
<Chip class={ convert_bool_to_chip_style(user_active) }
label={if user_active {translate.t("LABEL.ENABLED")} else { translate.t("LABEL.DISABLED")} }
<div class="tp__config-field tp__config-field__text">
<label>{translate.t("LABEL.PLAYLIST")}</label>
<Select name="target"
multi_select={false}
onselect={Callback::from(move |(_name, selections):(String, Vec<Rc<DropDownOption>>)| {
warn!("{}", selections.iter().map(|o| o.id.to_string()).collect::<Vec<String>>().join(", "));
})}
options={(*targets).clone()}
/>
</div>
<div class="tp__config-field tp__config-field__text">
<label>{translate.t("LABEL.STATUS")}</label>
<Select name="status"
multi_select={false}
onselect={Callback::from(move |(_name, selections):(String, Vec<String>)| {
warn!("{}", selections.join(", "));
onselect={Callback::from(move |(_name, selections):(String, Vec<Rc<DropDownOption>>)| {
warn!("{}", selections.iter().map(|o| o.id.to_string()).collect::<Vec<String>>().join(", "));
})}
options={proxy_user_status.clone()}
options={(*proxy_user_status).clone()}
/>
</div>
{ edit_field_text!(*form_state, translate.t("LABEL.USERNAME"), username) }
{ edit_field_text!(*form_state, translate.t("LABEL.PASSWORD"), password, true) }
{ edit_field_bool!(*form_state, translate.t("LABEL.USER_UI_ENABLED"), ui_enabled) }
{ edit_field_text_option!(*form_state, translate.t("LABEL.TOKEN"), token, true) }
<label>{ translate.t("LABEL.PROXY") }</label>
<span>{"TODO"}</span>
<div class="tp__config-field tp__config-field__text">
<label>{translate.t("LABEL.SERVER")}</label>
<Select name="server"
multi_select={false}
onselect={Callback::from(move |(_name, selections):(String, Vec<Rc<DropDownOption>>)| {
warn!("{}", selections.iter().map(|o| o.id.to_string()).collect::<Vec<String>>().join(", "));
})}
options={(*server).clone()}
/>
</div>
// pub server: Option<String>,
// pub epg_timeshift: Option<String>,
// pub created_at: Option<i64>,
// pub exp_date: Option<i64>,
// pub max_connections: u32,
{ edit_field_bool!(*form_state, translate.t("LABEL.USER_UI_ENABLED"), ui_enabled) }
{ edit_field_text_option!(*form_state, translate.t("LABEL.COMMENT"), comment) }
</div>