mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-01 05:22:28 +02:00
edit user form
This commit is contained in:
@@ -12,4 +12,12 @@
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__option-item {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
</>
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user