User edit form

This commit is contained in:
euzu
2025-08-21 11:41:06 +02:00
parent 9df150468c
commit 8a34673033
9 changed files with 216 additions and 53 deletions
+23 -17
View File
@@ -189,7 +189,7 @@
"RENAME_SETTINGS": "Rename Settings",
"PATTERN": "Pattern",
"NEW_NAME": "New Name",
"SKIP": "Skip",
"SKIP": "Skip",
"LIVE_STREAM_USE_PREFIX": "Live use prefix",
"LIVE_STREAM_WITHOUT_EXTENSION": "Live w/o ext.",
"ALIAS": "Alias",
@@ -201,12 +201,12 @@
"COUNTER": "Counter",
"MAPPING_PATH": "Mapping dir",
"USER_CONFIG_DIR": "User Config dir",
"USER_ACCESS_CONTROL": "User Access Control",
"USER_ACCESS_CONTROL": "User Access Control",
"SLEEP_TIMER_MINS": "Sleep Timer (mins)",
"CONNECT_TIMEOUT_SECS": "Connect Timeout (secs)",
"CONNECT_TIMEOUT_SECS": "Connect Timeout (secs)",
"CONFIG_HOT_RELOAD": "Config Hot Reload",
"CUSTOM_STREAM_RESPONSE_PATH": "Custom Stream Response dir",
"REVERSE_PROXY" : "Reverse Proxy",
"CUSTOM_STREAM_RESPONSE_PATH": "Custom Stream Response dir",
"REVERSE_PROXY": "Reverse Proxy",
"WEB_SEARCH": "Web Search",
"USER_UI_ENABLED": "User-UI Enabled",
"RESOURCE_REWRITE_DISABLED": "Resource Rewrite Disabled",
@@ -220,13 +220,13 @@
"THROTTLE_KBPS": "Throttle kbps",
"FRIENDLY_NAME": "Friendly Name",
"MANUFACTURER": "Manufacturer",
"MODEL_NAME": "Model Name",
"MODEL_NAME": "Model Name",
"MODEL_NUMBER": "Model Number",
"FIRMWARE_NAME": "Firmware Name",
"FIRMWARE_NAME": "Firmware Name",
"FIRMWARE_VERSION": "Firmware Version",
"DEVICE_TYPE": "Device Type",
"DEVICE_TYPE": "Device Type",
"DEVICE_UDN": "Device UDN",
"TUNER_COUNT": "Tuner Count",
"TUNER_COUNT": "Tuner Count",
"SOURCE": "Source",
"PRIORITY": "Priority",
"LOGO_OVERRIDE": "logo Override",
@@ -238,12 +238,12 @@
"BEST_MATCH_THRESHOLD": "Best Match Threshold",
"NORMALIZE_REGEX": "Normalize Regex",
"NAME_PREFIX": "Name Prefix",
"NAME_PREFIX_SEPARATOR":"Name Prefix Separator",
"NAME_PREFIX_SEPARATOR": "Name Prefix Separator",
"STRIP": "Strip",
"COPY_LINK_TULIPROX_VIRTUAL_ID": "Copy Virtual Id",
"COPY_LINK_TULIPROX_WEBPLAYER_URL": "Copy Webplayer Url",
"COPY_LINK_PROVIDER_URL": "Copy Provider Url",
"CONTENT_SECURITY_POLICY" : "Content Security Policy",
"CONTENT_SECURITY_POLICY": "Content Security Policy",
"CUSTOM_ATTRIBUTES": "Custom Attributes",
"EMPTY": "",
"OPTIONS": "Options",
@@ -260,12 +260,18 @@
"ALIASES": "Aliases",
"MAX_CONNECTIONS": "Max Connections",
"METHOD": "Method",
"EPG_TIMESHIFT": "Epg Timeshift",
"CREATED_AT": "Created at",
"EXP_DATE": "Exp. date",
"UI_ENABLED": "Ui",
"EPG_TIMESHIFT": "Epg Timeshift",
"CREATED_AT": "Created at",
"EXP_DATE": "Exp. date",
"UI_ENABLED": "Ui",
"COMMENT": "Comment",
"STAGED": "Staged"
"STAGED": "Staged",
"USER_STATUS_ACTIVE": "Active",
"USER_STATUS_EXPIRED": "Expired",
"USER_STATUS_BANNED": "Banned",
"USER_STATUS_TRIAL": "Trial",
"USER_STATUS_DISABLED": "Disabled",
"USER_STATUS_PENDING": "Pending"
},
"TITLE": {
"USER_BOUQUET_EDITOR": "User group editor"
@@ -276,7 +282,7 @@
"INVALID_FILETYPE": "Invalid filetype",
"TARGET_NOT_EXISTS": "Target does not exist",
"CONFIRM_DELETE": "You really want to delete?",
"CONFIG_CHANGED": "Configuration on server changed.",
"CONFIG_CHANGED": "Configuration on server changed.",
"DOWNLOAD": {
"SUCCESS": "Successfully downloaded",
"FAIL": "Failed to download!",
+1
View File
@@ -66,3 +66,4 @@
@forward "components/config/config_view";
@forward "components/tabset";
@forward "components/form";
@forward "components/select";
+45
View File
@@ -0,0 +1,45 @@
.tp__select {
display: flex;
flex-flow: column;
overflow: hidden;
box-sizing: border-box;
gap: 6px;
&-wrapper {
fill: var(--menu-icon-color);
display: flex;
flex-flow: row nowrap;
align-items: center;
overflow: hidden;
box-sizing: border-box;
padding-left: var(--padding-mini);
border: 1px solid var(--input-border-color);
border-radius: var(--border-radius);
background-color: var(--input-background-color);
&:focus-within {
border-color: var(--input-focus-border-color);
}
.svg-icon {
height: 2rem;
width: 2rem;
}
.tp__icon-button {
.svg-icon {
height: 1.5rem;
width: 1.5rem;
}
}
}
label {
padding-left: 2px;
color: var(--text-color);
}
&__selected {
display: flex;
flex-flow: row nowrap;
flex: 1 1 auto;
}
}
+27 -27
View File
@@ -105,33 +105,6 @@ macro_rules! config_field_empty {
};
}
#[macro_export]
macro_rules! edit_field_text {
($instance:expr, $label:expr, $field:ident) => {
$crate::edit_field_text!(@inner $instance, $label, $field, false)
};
($instance:expr, $label:expr, $field:ident, $hidden:expr) => {
$crate::edit_field_text!(@inner $instance, $label, $field, $hidden)
};
(@inner $instance:expr, $label:expr, $field:ident, $hidden:expr) => {{
let instance = $instance.clone();
html! {
<div class="tp__config-field tp__config-field__text">
<$crate::app::components::input::Input
label={$label}
hidden={$hidden}
name={stringify!($field)}
autocomplete={true}
value={instance.borrow().$field.clone()}
ontext={Callback::from(move |value: String| {
instance.borrow_mut().$field = value;
})}
/>
</div>
}
}};
}
#[macro_export]
macro_rules! edit_field_text_option {
($instance:expr, $label:expr, $field:ident) => {
@@ -163,6 +136,33 @@ macro_rules! edit_field_text_option {
}};
}
#[macro_export]
macro_rules! edit_field_text {
($instance:expr, $label:expr, $field:ident) => {
$crate::edit_field_text!(@inner $instance, $label, $field, false)
};
($instance:expr, $label:expr, $field:ident, $hidden:expr) => {
$crate::edit_field_text!(@inner $instance, $label, $field, $hidden)
};
(@inner $instance:expr, $label:expr, $field:ident, $hidden:expr) => {{
let instance = $instance.clone();
html! {
<div class="tp__config-field tp__config-field__text">
<$crate::app::components::input::Input
label={$label}
hidden={$hidden}
name={stringify!($field)}
autocomplete={true}
value={instance.borrow().$field.clone()}
ontext={Callback::from(move |value: String| {
instance.borrow_mut().$field = value;
})}
/>
</div>
}
}};
}
//<Input label={translate.t("LABEL.PASSWORD")} input_ref={password_ref} input_type="password" name="password" autocomplete={false} onkeydown={handle_key_down.clone()}/>
#[macro_export]
@@ -10,9 +10,9 @@ use crate::html_if;
#[derive(Clone, PartialEq, Debug)]
pub struct DropDownOption {
id: String,
label: String,
selected: bool,
pub(crate) id: String,
pub(crate) label: String,
pub(crate) selected: bool,
}
impl DropDownOption {
@@ -31,7 +31,6 @@ pub struct DropDownIconButtonProps {
pub options: Rc<Vec<DropDownOption>>,
#[prop_or_default]
pub multi_select: bool,
}
#[function_component]
+1
View File
@@ -35,6 +35,7 @@ mod accordion_panel;
mod csv_table;
mod config;
mod tabset;
mod select;
// pub use self::input::*;
// pub use self::menu_item::*;
// pub use self::popup_menu::*;
+69
View File
@@ -0,0 +1,69 @@
use std::rc::Rc;
use log::warn;
use yew::prelude::*;
use crate::app::components::{DropDownIconButton, DropDownOption, Tag, TagList};
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct SelectProps {
pub name: String,
#[prop_or_default]
pub icon: Option<String>,
pub onselect: Callback<(String, Vec<String>)>,
#[prop_or_default]
pub class: String,
pub options: Rc<Vec<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 {
// 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 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>>>();
set_selected_options.set(selections);
});
}
let handle_options_click = {
let onselect = props.onselect.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));
})
};
html! {
<div class={classes!("tp__select", props.class.clone())}>
<div class="tp__select-wrapper">
<div class="tp__select__selected">
<TagList tags={(*selected_options).clone()} />
</div>
<DropDownIconButton
multi_select={props.multi_select}
options={props.options.clone()}
name={props.name.clone()}
icon={props.icon.as_ref().map_or_else(|| "Popup".to_owned(), |i|i.to_string())}
onselect={handle_options_click} />
</div>
</div>
}
}
+8
View File
@@ -32,6 +32,14 @@ pub fn tag_list(props: &TagListProps) -> Html {
let tag_state = use_state(|| tags.clone());
let new_tag = use_state(String::default);
{
let tag_state = tag_state.clone();
use_effect_with(tags.clone(), move |tags| {
tag_state.set(tags.clone());
|| ()
});
}
let on_remove = {
let tag_state = tag_state.clone();
let on_change = on_change.clone();
@@ -3,10 +3,11 @@ use std::rc::Rc;
use log::warn;
use yew::prelude::*;
use yew_i18n::use_translation;
use shared::model::ProxyUserCredentialsDto;
use shared::model::{ProxyUserCredentialsDto, ProxyUserStatus};
use crate::app::TargetUser;
use crate::{edit_field_bool, edit_field_text, edit_field_text_option};
use crate::app::components::TextButton;
use crate::app::components::select::Select;
use crate::app::components::{convert_bool_to_chip_style, Chip, DropDownOption, TextButton};
#[derive(Properties, PartialEq, Clone)]
pub struct ProxyUserCredentialsFormProps {
@@ -16,18 +17,52 @@ pub struct ProxyUserCredentialsFormProps {
#[function_component]
pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
let translate = use_translation();
let form_state = use_memo(props.user.clone(), |user| Rc::new(RefCell::new(user.as_ref().map_or_else(|| ProxyUserCredentialsDto::default(), |usr| usr.credentials.as_ref().clone()))));
let form_state = use_memo(props.user.clone(),
|user| RefCell::new(user.as_ref()
.map_or_else(|| ProxyUserCredentialsDto::default(), |usr| usr.credentials.as_ref().clone())));
let proxy_user_status = use_memo(props.user.clone(), |user|
vec![
ProxyUserStatus::Active,
ProxyUserStatus::Expired,
ProxyUserStatus::Banned,
ProxyUserStatus::Trial,
ProxyUserStatus::Disabled,
ProxyUserStatus::Pending,
].iter().map(|s| DropDownOption {
id: s.to_string(),
label: format!("LABEL.USER_STATUS_{}", s.to_string().to_uppercase()),
selected: user.as_ref().is_some_and(|user| user.credentials.status.as_ref() == Some(s)),
}).collect::<Vec<DropDownOption>>()
);
let handle_save_user = {
let user = form_state.clone();
let user = form_state.clone();
Callback::from(move |_| {
warn!("{:?}", user.borrow());
})
};
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>
<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(", "));
})}
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) }
@@ -40,7 +75,6 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
// pub created_at: Option<i64>,
// pub exp_date: Option<i64>,
// pub max_connections: u32,
// pub status: Option<ProxyUserStatus>,
{ edit_field_text_option!(*form_state, translate.t("LABEL.COMMENT"), comment) }