Refactor forms to use separate item forms with Cancel/Submit pattern

Create separate item form components:
- Created epg_source_item_form.rs for EPG sources
- Created alias_item_form.rs for input aliases
- Created trakt_list_item_form.rs for Trakt lists

Refactor parent forms:
- input_form.rs now uses EpgSourceItemForm and AliasItemForm
- output_xtream_form.rs now uses TraktListItemForm
- Add-buttons toggle form visibility instead of inline inputs
- Each item form has Cancel/Submit buttons
- Removed inline form reducers, moved to item forms

UI improvements:
- Consistent form pattern across all list items
- Better UX with explicit Cancel/Submit workflow
- Cleaner code without dyn_into DOM manipulation

i18n updates:
- Added EPG_SOURCE_URL, EPG_SOURCES, ADD_EPG_SOURCE labels
- Added ALIAS_NAME, ADD_ALIAS labels
- Added all Trakt labels (TRAKT_USER, TRAKT_LIST_SLUG, etc.)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
moe
2025-12-01 15:36:38 +01:00
co-authored by Claude
parent af87249c01
commit cd523c6ad2
7 changed files with 446 additions and 159 deletions
+15
View File
@@ -245,6 +245,16 @@
"DEVICE_TYPE": "Device Type",
"DEVICE_UDN": "Device UDN",
"TUNER_COUNT": "Tuner Count",
"TRAKT_USER": "Trakt User",
"TRAKT_LIST_SLUG": "Trakt List Slug",
"TRAKT_CATEGORY_NAME": "Trakt Category Name",
"TRAKT_CONTENT_TYPE": "Trakt Content Type",
"TRAKT_FUZZY_MATCH_THRESHOLD": "Trakt Fuzzy Match Threshold (%)",
"TRAKT_API_KEY": "Trakt API Key",
"TRAKT_API_VERSION": "Trakt API Version",
"TRAKT_API_URL": "Trakt API URL",
"TRAKT_LISTS": "Trakt Lists",
"ADD_TRAKT_LIST": "Add Trakt List",
"SOURCE": "Source",
"PRIORITY": "Priority",
"LOGO_OVERRIDE": "logo Override",
@@ -274,8 +284,13 @@
"WATCH": "Watch",
"INPUT_TYPE": "Input Type",
"EPG": "Epg",
"EPG_SOURCE_URL": "EPG Source URL",
"EPG_SOURCES": "EPG Sources",
"ADD_EPG_SOURCE": "Add EPG Source",
"PERSIST": "Persist",
"ALIASES": "Aliases",
"ALIAS_NAME": "Alias Name",
"ADD_ALIAS": "Add Alias",
"MAX_CON": "Max Con.",
"MAX_CONNECTIONS": "Max Connections",
"METHOD": "Method",
@@ -0,0 +1,106 @@
use crate::app::components::{Card, TextButton};
use crate::{edit_field_date, edit_field_number_i16, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer};
use shared::model::ConfigInputAliasDto;
use yew::{function_component, html, use_reducer, Callback, Html, Properties, UseReducerHandle};
use yew_i18n::use_translation;
const LABEL_ALIAS_NAME: &str = "LABEL.ALIAS_NAME";
const LABEL_URL: &str = "LABEL.URL";
const LABEL_USERNAME: &str = "LABEL.USERNAME";
const LABEL_PASSWORD: &str = "LABEL.PASSWORD";
const LABEL_PRIORITY: &str = "LABEL.PRIORITY";
const LABEL_MAX_CONNECTIONS: &str = "LABEL.MAX_CONNECTIONS";
const LABEL_EXP_DATE: &str = "LABEL.EXP_DATE";
generate_form_reducer!(
state: AliasFormState { form: ConfigInputAliasDto },
action_name: AliasFormAction,
fields {
Id => id: u16,
Name => name: String,
Url => url: String,
Username => username: Option<String>,
Password => password: Option<String>,
Priority => priority: i16,
MaxConnections => max_connections: u16,
ExpDate => exp_date: Option<i64>,
}
);
#[derive(Properties, PartialEq, Clone)]
pub struct AliasItemFormProps {
pub on_submit: Callback<ConfigInputAliasDto>,
pub on_cancel: Callback<()>,
#[prop_or_default]
pub initial: Option<ConfigInputAliasDto>,
#[prop_or(0)]
pub next_id: u16,
}
#[function_component]
pub fn AliasItemForm(props: &AliasItemFormProps) -> Html {
let translate = use_translation();
let form_state: UseReducerHandle<AliasFormState> = use_reducer(|| {
AliasFormState {
form: props.initial.clone().unwrap_or_else(|| ConfigInputAliasDto {
id: props.next_id,
name: String::new(),
url: String::new(),
username: None,
password: None,
priority: 0,
max_connections: 1,
exp_date: None,
}),
modified: false,
}
});
let handle_submit = {
let form_state = form_state.clone();
let on_submit = props.on_submit.clone();
Callback::from(move |_| {
let data = form_state.data().clone();
if !data.name.trim().is_empty() && !data.url.trim().is_empty() {
on_submit.emit(data);
}
})
};
let handle_cancel = {
let on_cancel = props.on_cancel.clone();
Callback::from(move |_| {
on_cancel.emit(());
})
};
html! {
<Card class="tp__config-view__card tp__item-form">
{ edit_field_text!(form_state, translate.t(LABEL_ALIAS_NAME), name, AliasFormAction::Name) }
{ edit_field_text!(form_state, translate.t(LABEL_URL), url, AliasFormAction::Url) }
{ edit_field_text_option!(form_state, translate.t(LABEL_USERNAME), username, AliasFormAction::Username) }
{ edit_field_text_option!(form_state, translate.t(LABEL_PASSWORD), password, AliasFormAction::Password, true) }
{ edit_field_number_i16!(form_state, translate.t(LABEL_PRIORITY), priority, AliasFormAction::Priority) }
{ edit_field_number_u16!(form_state, translate.t(LABEL_MAX_CONNECTIONS), max_connections, AliasFormAction::MaxConnections) }
{ edit_field_date!(form_state, translate.t(LABEL_EXP_DATE), exp_date, AliasFormAction::ExpDate) }
<div class="tp__item-form__buttons">
<TextButton
class="primary"
name="submit_alias"
icon="Accept"
title={translate.t("LABEL.SUBMIT")}
onclick={handle_submit}
/>
<TextButton
class="secondary"
name="cancel_alias"
icon="Cancel"
title={translate.t("LABEL.CANCEL")}
onclick={handle_cancel}
/>
</div>
</Card>
}
}
@@ -0,0 +1,82 @@
use crate::app::components::{Card, TextButton};
use crate::{edit_field_text, generate_form_reducer};
use shared::model::EpgSourceDto;
use yew::{function_component, html, use_reducer, Callback, Html, Properties, UseReducerHandle};
use yew_i18n::use_translation;
const LABEL_EPG_SOURCE_URL: &str = "LABEL.EPG_SOURCE_URL";
generate_form_reducer!(
state: EpgSourceFormState { form: EpgSourceDto },
action_name: EpgSourceFormAction,
fields {
Url => url: String,
Priority => priority: i16,
LogoOverride => logo_override: bool,
}
);
#[derive(Properties, PartialEq, Clone)]
pub struct EpgSourceItemFormProps {
pub on_submit: Callback<EpgSourceDto>,
pub on_cancel: Callback<()>,
#[prop_or_default]
pub initial: Option<EpgSourceDto>,
}
#[function_component]
pub fn EpgSourceItemForm(props: &EpgSourceItemFormProps) -> Html {
let translate = use_translation();
let form_state: UseReducerHandle<EpgSourceFormState> = use_reducer(|| {
EpgSourceFormState {
form: props.initial.clone().unwrap_or_else(|| EpgSourceDto {
url: String::new(),
priority: 0,
logo_override: false,
}),
modified: false,
}
});
let handle_submit = {
let form_state = form_state.clone();
let on_submit = props.on_submit.clone();
Callback::from(move |_| {
let data = form_state.data().clone();
if !data.url.trim().is_empty() {
on_submit.emit(data);
}
})
};
let handle_cancel = {
let on_cancel = props.on_cancel.clone();
Callback::from(move |_| {
on_cancel.emit(());
})
};
html! {
<Card class="tp__config-view__card tp__item-form">
{ edit_field_text!(form_state, translate.t(LABEL_EPG_SOURCE_URL), url, EpgSourceFormAction::Url) }
<div class="tp__item-form__buttons">
<TextButton
class="primary"
name="submit_epg_source"
icon="Accept"
title={translate.t("LABEL.SUBMIT")}
onclick={handle_submit}
/>
<TextButton
class="secondary"
name="cancel_epg_source"
icon="Cancel"
title={translate.t("LABEL.CANCEL")}
onclick={handle_cancel}
/>
</div>
</Card>
}
}
@@ -169,6 +169,10 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
let aliases_state = use_state(|| Vec::<ConfigInputAliasDto>::new());
let headers_state = use_state(|| HashMap::<String, String>::new());
// State for showing item forms
let show_epg_form_state = use_state(|| false);
let show_alias_form_state = use_state(|| false);
let staged_input_types = use_memo(staged_input_state.form.input_type, |input_type| {
let default_it = input_type;
[
@@ -339,6 +343,8 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
let headers = headers_state.clone();
let epg_sources = epg_sources_state.clone();
let aliases = aliases_state.clone();
let show_epg_form = show_epg_form_state.clone();
let show_alias_form = show_alias_form_state.clone();
html! {
<Card class="tp__config-view__card">
@@ -360,14 +366,17 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
// EPG Sources Section
{ config_field_child!(translate.t(LABEL_EPG_SOURCES), {
let epg_sources_add = epg_sources.clone();
let epg_sources_list = epg_sources.clone();
let epg_sources_add = epg_sources.clone();
let show_epg_form_toggle = show_epg_form.clone();
let show_epg_form_check = show_epg_form.clone();
html! {
<div class="tp__form-list">
<div class="tp__form-list__items">
{
for (*epg_sources).iter().enumerate().map(|(idx, source)| {
let epg_sources_remove = epg_sources.clone();
for (*epg_sources_list).iter().enumerate().map(|(idx, source)| {
let epg_sources_remove = epg_sources_list.clone();
html! {
<div class="tp__form-list__item" key={idx}>
<div class="tp__form-list__item-content">
@@ -388,50 +397,48 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
})
}
</div>
<div class="tp__form-list__add">
<input
type="text"
placeholder={translate.t(LABEL_EPG_SOURCE_URL)}
id="epg-source-input"
/>
<button
onclick={Callback::from(move |_| {
if let Some(input) = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id("epg-source-input"))
.and_then(|e| e.dyn_into::<web_sys::HtmlInputElement>().ok())
{
let url = input.value().trim().to_string();
if !url.is_empty() {
let mut sources = (*epg_sources_add).clone();
sources.push(EpgSourceDto {
url,
priority: 0,
logo_override: false,
});
epg_sources_add.set(sources);
input.set_value("");
}
}
if *show_epg_form_check {
<EpgSourceItemForm
on_submit={Callback::from(move |source: EpgSourceDto| {
let mut sources = (*epg_sources_add).clone();
sources.push(source);
epg_sources_add.set(sources);
show_epg_form.set(false);
})}
>
{"+"}
</button>
</div>
on_cancel={Callback::from(move |_| {
show_epg_form.set(false);
})}
/>
} else {
<TextButton
class="primary"
name="add_epg_source"
icon="Add"
title={translate.t(LABEL_ADD_EPG_SOURCE)}
onclick={Callback::from(move |_| {
show_epg_form_toggle.set(true);
})}
/>
}
</div>
}
})}
// Aliases Section
{ config_field_child!(translate.t(LABEL_ALIASES), {
let aliases_add = aliases.clone();
let aliases_list = aliases.clone();
let aliases_add = aliases.clone();
let show_alias_form_toggle = show_alias_form.clone();
let show_alias_form_check = show_alias_form.clone();
let next_id = (*aliases_list).iter().map(|a| a.id).max().unwrap_or(0) + 1;
html! {
<div class="tp__form-list">
<div class="tp__form-list__items">
{
for (*aliases).iter().enumerate().map(|(idx, alias)| {
let aliases_remove = aliases.clone();
for (*aliases_list).iter().enumerate().map(|(idx, alias)| {
let aliases_remove = aliases_list.clone();
html! {
<div class="tp__form-list__item" key={idx}>
<div class="tp__form-list__item-content">
@@ -452,48 +459,31 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
})
}
</div>
<div class="tp__form-list__add-alias">
<input type="text" placeholder={translate.t(LABEL_ALIAS_NAME)} id="alias-name-input" />
<input type="text" placeholder={translate.t(LABEL_URL)} id="alias-url-input" />
<button
onclick={Callback::from(move |_| {
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
let name_input = document
.get_element_by_id("alias-name-input")
.and_then(|e| e.dyn_into::<web_sys::HtmlInputElement>().ok());
let url_input = document
.get_element_by_id("alias-url-input")
.and_then(|e| e.dyn_into::<web_sys::HtmlInputElement>().ok());
if let (Some(name_el), Some(url_el)) = (name_input, url_input) {
let name = name_el.value().trim().to_string();
let url = url_el.value().trim().to_string();
if !name.is_empty() && !url.is_empty() {
let mut items = (*aliases_add).clone();
let new_id = items.iter().map(|a| a.id).max().unwrap_or(0) + 1;
items.push(ConfigInputAliasDto {
id: new_id,
name,
url,
username: None,
password: None,
priority: 0,
max_connections: 1,
exp_date: None,
});
aliases_add.set(items);
name_el.set_value("");
url_el.set_value("");
}
}
}
}
if *show_alias_form_check {
<AliasItemForm
next_id={next_id}
on_submit={Callback::from(move |alias: ConfigInputAliasDto| {
let mut items = (*aliases_add).clone();
items.push(alias);
aliases_add.set(items);
show_alias_form.set(false);
})}
>
{"+"}
</button>
</div>
on_cancel={Callback::from(move |_| {
show_alias_form.set(false);
})}
/>
} else {
<TextButton
class="primary"
name="add_alias"
icon="Add"
title={translate.t(LABEL_ADD_ALIAS)}
onclick={Callback::from(move |_| {
show_alias_form_toggle.set(true);
})}
/>
}
</div>
}
})}
@@ -12,6 +12,9 @@ mod output_m3u_form;
mod output_strm_form;
mod output_hdhomerun_form;
mod layout;
mod epg_source_item_form;
mod alias_item_form;
mod trakt_list_item_form;
pub(crate) use editor_view::*;
pub(crate) use editor_model::*;
@@ -25,4 +28,7 @@ pub(crate) use block_view::*;
pub(crate) use output_xtream_form::*;
pub(crate) use output_m3u_form::*;
pub(crate) use output_strm_form::*;
pub(crate) use output_hdhomerun_form::*;
pub(crate) use output_hdhomerun_form::*;
pub(crate) use epg_source_item_form::*;
pub(crate) use alias_item_form::*;
pub(crate) use trakt_list_item_form::*;
@@ -52,18 +52,6 @@ generate_form_reducer!(
}
);
generate_form_reducer!(
state: TraktListConfigFormState { form: TraktListConfigDto },
action_name: TraktListConfigFormAction,
fields {
User => user: String,
ListSlug => list_slug: String,
CategoryName => category_name: String,
ContentType => content_type: TraktContentType,
FuzzyMatchThreshold => fuzzy_match_threshold: u8,
}
);
generate_form_reducer!(
state: XtreamTargetOutputFormState { form: XtreamTargetOutputDto },
action_name: XtreamTargetOutputFormAction,
@@ -105,12 +93,8 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
// State for Trakt lists
let trakt_lists_state = use_state(|| Vec::<TraktListConfigDto>::new());
// State for adding new Trakt list
let trakt_list_add_state: UseReducerHandle<TraktListConfigFormState> =
use_reducer(|| TraktListConfigFormState {
form: TraktListConfigDto::default(),
modified: false,
});
// State for showing trakt list form
let show_trakt_list_form_state = use_state(|| false);
let view_visible = use_state(|| OutputFormPage::Main.to_string());
@@ -164,29 +148,7 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
let render_trakt = || {
let trakt_lists = trakt_lists_state.clone();
let trakt_api = trakt_api_state.clone();
let trakt_list_add = trakt_list_add_state.clone();
// Create content type options
let content_type_options = use_memo(trakt_list_add.form.content_type, |content_type| {
let default_ct = content_type;
vec![
DropDownOption {
id: "vod".to_string(),
label: html! { "Vod" },
selected: default_ct == &TraktContentType::Vod,
},
DropDownOption {
id: "series".to_string(),
label: html! { "Series" },
selected: default_ct == &TraktContentType::Series,
},
DropDownOption {
id: "both".to_string(),
label: html! { "Both" },
selected: default_ct == &TraktContentType::Both,
},
]
});
let show_trakt_list_form = show_trakt_list_form_state.clone();
html! {
<Card class="tp__config-view__card">
@@ -200,13 +162,17 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
// Trakt Lists
{ config_field_child!(translate.t(LABEL_TRAKT_LISTS), {
let trakt_lists_list = trakt_lists.clone();
let trakt_lists_add = trakt_lists.clone();
let show_trakt_list_form_toggle = show_trakt_list_form.clone();
let show_trakt_list_form_check = show_trakt_list_form.clone();
html! {
<div class="tp__form-list">
<div class="tp__form-list__items">
{
for (*trakt_lists).iter().enumerate().map(|(idx, list)| {
let trakt_lists_remove = trakt_lists.clone();
for (*trakt_lists_list).iter().enumerate().map(|(idx, list)| {
let trakt_lists_remove = trakt_lists_list.clone();
let content_type_str = match list.content_type {
TraktContentType::Vod => "Vod",
TraktContentType::Series => "Series",
@@ -243,47 +209,30 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
})
}
</div>
<div class="tp__form-list__add-trakt">
{ edit_field_text!(trakt_list_add, translate.t(LABEL_TRAKT_USER), user, TraktListConfigFormAction::User) }
{ edit_field_text!(trakt_list_add, translate.t(LABEL_TRAKT_LIST_SLUG), list_slug, TraktListConfigFormAction::ListSlug) }
{ edit_field_text!(trakt_list_add, translate.t(LABEL_TRAKT_CATEGORY_NAME), category_name, TraktListConfigFormAction::CategoryName) }
{ config_field_child!(translate.t(LABEL_TRAKT_CONTENT_TYPE), {
let trakt_list_add_ct = trakt_list_add.clone();
html! {
<Select
name={"trakt_content_type"}
multi_select={false}
on_select={Callback::from(move |(_, selections):(String, DropDownSelection)| {
match selections {
DropDownSelection::Single(option) => {
if let Ok(ct) = option.parse::<TraktContentType>() {
trakt_list_add_ct.dispatch(TraktListConfigFormAction::ContentType(ct));
}
}
_ => {}
}
})}
options={content_type_options.clone()}
/>
}
})}
{ edit_field_number_u8!(trakt_list_add, translate.t(LABEL_TRAKT_FUZZY_MATCH_THRESHOLD), fuzzy_match_threshold, TraktListConfigFormAction::FuzzyMatchThreshold) }
<button
class="tp__form-list__add-button"
onclick={Callback::from(move |_| {
let list_data = trakt_list_add.data();
if !list_data.user.is_empty() && !list_data.list_slug.is_empty() && !list_data.category_name.is_empty() {
let mut items = (*trakt_lists_add).clone();
items.push(list_data.clone());
trakt_lists_add.set(items);
// Reset form
trakt_list_add.dispatch(TraktListConfigFormAction::SetAll(TraktListConfigDto::default()));
}
if *show_trakt_list_form_check {
<TraktListItemForm
on_submit={Callback::from(move |list: TraktListConfigDto| {
let mut items = (*trakt_lists_add).clone();
items.push(list);
trakt_lists_add.set(items);
show_trakt_list_form.set(false);
})}
>
{translate.t(LABEL_ADD_TRAKT_LIST)}
</button>
</div>
on_cancel={Callback::from(move |_| {
show_trakt_list_form.set(false);
})}
/>
} else {
<TextButton
class="primary"
name="add_trakt_list"
icon="Add"
title={translate.t(LABEL_ADD_TRAKT_LIST)}
onclick={Callback::from(move |_| {
show_trakt_list_form_toggle.set(true);
})}
/>
}
</div>
}
})}
@@ -0,0 +1,139 @@
use crate::app::components::select::Select;
use crate::app::components::{Card, DropDownOption, DropDownSelection, TextButton};
use crate::{config_field_child, edit_field_number_u8, edit_field_text, generate_form_reducer};
use shared::model::{TraktContentType, TraktListConfigDto};
use yew::{function_component, html, use_memo, use_reducer, Callback, Html, Properties, UseReducerHandle};
use yew_i18n::use_translation;
const LABEL_TRAKT_USER: &str = "LABEL.TRAKT_USER";
const LABEL_TRAKT_LIST_SLUG: &str = "LABEL.TRAKT_LIST_SLUG";
const LABEL_TRAKT_CATEGORY_NAME: &str = "LABEL.TRAKT_CATEGORY_NAME";
const LABEL_TRAKT_CONTENT_TYPE: &str = "LABEL.TRAKT_CONTENT_TYPE";
const LABEL_TRAKT_FUZZY_MATCH_THRESHOLD: &str = "LABEL.TRAKT_FUZZY_MATCH_THRESHOLD";
generate_form_reducer!(
state: TraktListFormState { form: TraktListConfigDto },
action_name: TraktListFormAction,
fields {
User => user: String,
ListSlug => list_slug: String,
CategoryName => category_name: String,
ContentType => content_type: TraktContentType,
FuzzyMatchThreshold => fuzzy_match_threshold: u8,
}
);
#[derive(Properties, PartialEq, Clone)]
pub struct TraktListItemFormProps {
pub on_submit: Callback<TraktListConfigDto>,
pub on_cancel: Callback<()>,
#[prop_or_default]
pub initial: Option<TraktListConfigDto>,
}
#[function_component]
pub fn TraktListItemForm(props: &TraktListItemFormProps) -> Html {
let translate = use_translation();
let form_state: UseReducerHandle<TraktListFormState> = use_reducer(|| {
TraktListFormState {
form: props.initial.clone().unwrap_or_else(|| TraktListConfigDto {
user: String::new(),
list_slug: String::new(),
category_name: String::new(),
content_type: TraktContentType::Both,
fuzzy_match_threshold: 80,
}),
modified: false,
}
});
let content_type_options = use_memo(form_state.form.content_type, |content_type| {
let default_ct = content_type;
vec![
DropDownOption {
id: "vod".to_string(),
label: html! { "Vod" },
selected: default_ct == &TraktContentType::Vod,
},
DropDownOption {
id: "series".to_string(),
label: html! { "Series" },
selected: default_ct == &TraktContentType::Series,
},
DropDownOption {
id: "both".to_string(),
label: html! { "Both" },
selected: default_ct == &TraktContentType::Both,
},
]
});
let handle_submit = {
let form_state = form_state.clone();
let on_submit = props.on_submit.clone();
Callback::from(move |_| {
let data = form_state.data().clone();
if !data.user.trim().is_empty()
&& !data.list_slug.trim().is_empty()
&& !data.category_name.trim().is_empty() {
on_submit.emit(data);
}
})
};
let handle_cancel = {
let on_cancel = props.on_cancel.clone();
Callback::from(move |_| {
on_cancel.emit(());
})
};
html! {
<Card class="tp__config-view__card tp__item-form">
{ edit_field_text!(form_state, translate.t(LABEL_TRAKT_USER), user, TraktListFormAction::User) }
{ edit_field_text!(form_state, translate.t(LABEL_TRAKT_LIST_SLUG), list_slug, TraktListFormAction::ListSlug) }
{ edit_field_text!(form_state, translate.t(LABEL_TRAKT_CATEGORY_NAME), category_name, TraktListFormAction::CategoryName) }
{ config_field_child!(translate.t(LABEL_TRAKT_CONTENT_TYPE), {
let form_state_ct = form_state.clone();
html! {
<Select
name={"trakt_content_type"}
multi_select={false}
on_select={Callback::from(move |(_, selections):(String, DropDownSelection)| {
match selections {
DropDownSelection::Single(option) => {
if let Ok(ct) = option.parse::<TraktContentType>() {
form_state_ct.dispatch(TraktListFormAction::ContentType(ct));
}
}
_ => {}
}
})}
options={content_type_options.clone()}
/>
}
})}
{ edit_field_number_u8!(form_state, translate.t(LABEL_TRAKT_FUZZY_MATCH_THRESHOLD), fuzzy_match_threshold, TraktListFormAction::FuzzyMatchThreshold) }
<div class="tp__item-form__buttons">
<TextButton
class="primary"
name="submit_trakt_list"
icon="Accept"
title={translate.t("LABEL.SUBMIT")}
onclick={handle_submit}
/>
<TextButton
class="secondary"
name="cancel_trakt_list"
icon="Cancel"
title={translate.t("LABEL.CANCEL")}
onclick={handle_cancel}
/>
</div>
</Card>
}
}