diff --git a/frontend/scss/app/components/_drop_down_icon_button.scss b/frontend/scss/app/components/_drop_down_icon_button.scss index c321f2e95..3a45016ab 100644 --- a/frontend/scss/app/components/_drop_down_icon_button.scss +++ b/frontend/scss/app/components/_drop_down_icon_button.scss @@ -12,4 +12,12 @@ height: 1rem; } } + + &__option { + cursor: pointer; + } + + &__option-item { + pointer-events: none; + } } \ No newline at end of file diff --git a/frontend/scss/app/components/_select.scss b/frontend/scss/app/components/_select.scss index 5a87fd0f6..3f5e55768 100644 --- a/frontend/scss/app/components/_select.scss +++ b/frontend/scss/app/components/_select.scss @@ -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; } } diff --git a/frontend/src/app/components/drop_down_icon_button.rs b/frontend/src/app/components/drop_down_icon_button.rs index 21140eb32..59dcef8dd 100644 --- a/frontend/src/app/components/drop_down_icon_button.rs +++ b/frontend/src/app/components/drop_down_icon_button.rs @@ -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)>, #[prop_or_default] pub class: String, - pub options: Rc>, + pub options: Vec>, #[prop_or_default] pub multi_select: bool, + #[prop_or_default] + pub button_ref: Option, } #[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::); 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::>()); + let selections = use_set(props.options.iter().filter(|x| x.selected).map(|x|x.id.clone()).collect::>()); 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::() { - set_anchor_ref.set(Some(target)); + event.prevent_default(); + if let Some(button) = button_ref.cast::() { + 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 { } }) } - + {o.label.clone()} }}) } - + } } \ No newline at end of file diff --git a/frontend/src/app/components/search.rs b/frontend/src/app/components/search.rs index aa5ddf3bb..7ee00e72d 100644 --- a/frontend/src/app/components/search.rs +++ b/frontend/src/app/components/search.rs @@ -19,7 +19,7 @@ pub struct SearchProps { #[prop_or_default] pub class: String, #[prop_or_default] - pub options: Option>>, + pub options: Option>>, pub onsearch: Option>, #[prop_or(3)] pub min_length: usize, diff --git a/frontend/src/app/components/select.rs b/frontend/src/app/components/select.rs index 34a6b0ab7..717f0de62 100644 --- a/frontend/src/app/components/select.rs +++ b/frontend/src/app/components/select.rs @@ -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, - pub onselect: Callback<(String, Vec)>, + pub onselect: Callback<(String, Vec>)>, #[prop_or_default] pub class: String, - pub options: Rc>, + pub options: Vec>, #[prop_or_default] pub multi_select: bool, } -fn create_tag_from_str(s: &str) -> Rc { - Rc::new(Tag { label: s.to_owned(), class: Some("active".to_string()) }) -} - -fn create_tag_from_option(o: &DropDownOption) -> Rc { - 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::>>()); + let selected_options = use_state(|| props.options.iter().filter(|o| o.selected) + .map(|o| o.label.clone()).collect::>()); { 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::>>(); + use_effect_with(props.options.clone(), move |options: &Vec>| { + let selections = options.iter().filter(|o| o.selected) + .map(|o| o.label.clone()).collect::>(); 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)| { - set_selections.set(selections.iter().map(|s|s.as_str()).map(create_tag_from_str).collect::>>()); - onselect.emit((name, selections)); + let selected_options: Vec> = 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::() { + if target.class_name().contains("tp__select-wrapper") { + if let Some(button) = button_ref.cast::() { + button.click(); + } + } + } }) }; html! {
-
+
- + {(*selected_options).clone()}
Html { let translate = use_translation(); let userlist_ctx = use_context::().expect("Userlist context not found"); + let playlist_ctx = use_context::().expect("Playlist context not found"); + let config_ctx = use_context::().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 {
- +
diff --git a/frontend/src/app/components/userlist/list.rs b/frontend/src/app/components/userlist/list.rs index 336a48807..988e69707 100644 --- a/frontend/src/app/components/userlist/list.rs +++ b/frontend/src/app/components/userlist/list.rs @@ -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::().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(); diff --git a/frontend/src/app/components/userlist/proxy_user_credentials_form.rs b/frontend/src/app/components/userlist/proxy_user_credentials_form.rs index 674695016..c3ddcfddb 100644 --- a/frontend/src/app/components/userlist/proxy_user_credentials_form.rs +++ b/frontend/src/app/components/userlist/proxy_user_credentials_form.rs @@ -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>, + pub targets: Rc>>, + pub server: Rc>, } #[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::>>() + ); + + 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::>>() + ); + 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! { }, selected: user.as_ref().is_some_and(|user| user.credentials.status.as_ref() == Some(s)), - }).collect::>() + })).collect::>>() ); 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! {
-
- + + )| { - warn!("{}", selections.join(", ")); + onselect={Callback::from(move |(_name, selections):(String, Vec>)| { + warn!("{}", selections.iter().map(|o| o.id.to_string()).collect::>().join(", ")); })} - options={proxy_user_status.clone()} + options={(*proxy_user_status).clone()} />
{ 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) } {"TODO"} +
+ +