From d2aa7e7db7b14fb2e80df8e50b270fbbd4131a46 Mon Sep 17 00:00:00 2001 From: euzu Date: Wed, 3 Dec 2025 18:07:40 +0100 Subject: [PATCH 1/9] Filter Input and Filter editor started --- frontend/public/config.json | 2 +- frontend/scss/app/_component.scss | 1 + frontend/scss/app/components/_filter.scss | 1 + .../scss/app/components/_filter_input.scss | 27 +++++++ frontend/scss/app/components/_input.scss | 2 +- .../app/components/filter/filter_editor.rs | 0 .../src/app/components/filter/filter_input.rs | 80 +++++++++++++++++++ frontend/src/app/components/filter/mod.rs | 5 ++ frontend/src/app/components/mod.rs | 7 +- .../source_editor/output_m3u_form.rs | 13 ++- .../source_editor/output_strm_form.rs | 11 ++- .../source_editor/output_xtream_form.rs | 11 ++- .../components/source_editor/target_form.rs | 11 ++- frontend/src/model/web_config.rs | 5 +- 14 files changed, 161 insertions(+), 15 deletions(-) create mode 100644 frontend/scss/app/components/_filter_input.scss create mode 100644 frontend/src/app/components/filter/filter_editor.rs create mode 100644 frontend/src/app/components/filter/filter_input.rs create mode 100644 frontend/src/app/components/filter/mod.rs diff --git a/frontend/public/config.json b/frontend/public/config.json index 2a055c157..e7bfcc794 100644 --- a/frontend/public/config.json +++ b/frontend/public/config.json @@ -7,7 +7,7 @@ "authUrl": "/auth/" }, "discord": "https://discord.gg/gkzCmWw9Tf", - "documentation": "https://github.com/euzu/tuliprox/blob/master/README.md", + "documentation": "https://euzu.github.io/tuliprox-docs/", "wsUrl" : "/ws", "protocolVersion": 1 } \ No newline at end of file diff --git a/frontend/scss/app/_component.scss b/frontend/scss/app/_component.scss index 7367da42a..3ad18a9e5 100644 --- a/frontend/scss/app/_component.scss +++ b/frontend/scss/app/_component.scss @@ -81,3 +81,4 @@ @forward "components/source_editor/input_form"; @forward "components/cluster_flags"; @forward "components/titled_card"; +@forward "components/filter_input"; diff --git a/frontend/scss/app/components/_filter.scss b/frontend/scss/app/components/_filter.scss index 729b0976e..3b0b6da03 100644 --- a/frontend/scss/app/components/_filter.scss +++ b/frontend/scss/app/components/_filter.scss @@ -61,6 +61,7 @@ .bracket-3 { color: var(--code-bracket-3-color); } } .tp__filter__inline { + overflow: hidden; .tp__filter__code { padding: 0; margin-block: 0; diff --git a/frontend/scss/app/components/_filter_input.scss b/frontend/scss/app/components/_filter_input.scss new file mode 100644 index 000000000..716b623e2 --- /dev/null +++ b/frontend/scss/app/components/_filter_input.scss @@ -0,0 +1,27 @@ +.tp__filter-input { + display: flex; + cursor: pointer; + fill: var(--modest-text-color); + + .tp__input-wrapper { + height: 2.5rem; + padding: 0 var(--padding-mini); + justify-content: space-between; + } + + &:hover { + fill: var(--text-color); + cursor: pointer; + filter: brightness(1.2); + } + + &__preview { + flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-height: 2rem; + pointer-events: none; + } + +} \ No newline at end of file diff --git a/frontend/scss/app/components/_input.scss b/frontend/scss/app/components/_input.scss index 799cba9ed..d2d96a021 100644 --- a/frontend/scss/app/components/_input.scss +++ b/frontend/scss/app/components/_input.scss @@ -3,7 +3,7 @@ flex-flow: column; overflow: hidden; box-sizing: border-box; - gap: 6px; + gap: var(--gap-small); .tp__input-wrapper { display: flex; flex-flow: row nowrap; diff --git a/frontend/src/app/components/filter/filter_editor.rs b/frontend/src/app/components/filter/filter_editor.rs new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/filter/filter_input.rs b/frontend/src/app/components/filter/filter_input.rs new file mode 100644 index 000000000..39f92193c --- /dev/null +++ b/frontend/src/app/components/filter/filter_input.rs @@ -0,0 +1,80 @@ +use crate::app::components::{AppIcon, FilterView}; +use crate::services::DialogService; +use yew::platform::spawn_local; +use yew::prelude::*; +use shared::foundation::filter::get_filter; +use crate::app::{ConfigContext}; + +#[derive(Properties, Clone, PartialEq, Debug)] +pub struct FilterInputProps { + #[prop_or_default] + pub icon: String, + #[prop_or_default] + pub filter: Option, + #[prop_or_default] + pub on_change: Callback>, +} + +#[function_component] +pub fn FilterInput(props: &FilterInputProps) -> Html { + let config_ctx = use_context::().expect("Config context not found"); + let dialog = use_context::().expect("Dialog service not found"); + + let filter_state = use_state(|| None); + let parsed_filter_state = use_state(|| None); + let templates_state = use_state(|| None); + + { + let templates = templates_state.clone(); + let cfg_templates = config_ctx.config.as_ref().and_then(|c| c.sources.templates.clone()); + use_effect_with(cfg_templates, move |templ| { + templates.set(templ.clone()); + }); + } + + { + let filter = filter_state.clone(); + let parsed_filter = parsed_filter_state.clone(); + let templates = templates_state.clone(); + use_effect_with(props.filter.clone(), move |flt| { + filter.set(flt.clone()); + let parsed = if let Some(new_fltr) = flt.as_ref() { + get_filter(new_fltr, (*templates).as_ref()).ok() + } else { + None + }; + parsed_filter.set(parsed); + }); + } + + let handle_click = { + let dialog = dialog.clone(); + let current_filter = filter_state.clone(); + Callback::from(move |e: MouseEvent| { + e.prevent_default(); + let dlg = dialog.clone(); + spawn_local(async move { + //let filter_view = html!{}; + //let _result = dlg.content(filter_view, None).await; + }); + }) + }; + + html! { +
+
+ + { + match (*parsed_filter_state).as_ref() { + None => html! {}, + Some(preview) => html! { + + } + } + } + + +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/filter/mod.rs b/frontend/src/app/components/filter/mod.rs new file mode 100644 index 000000000..dceb737ba --- /dev/null +++ b/frontend/src/app/components/filter/mod.rs @@ -0,0 +1,5 @@ +mod filter_editor; +mod filter_input; + +pub use filter_editor::*; +pub use filter_input::*; diff --git a/frontend/src/app/components/mod.rs b/frontend/src/app/components/mod.rs index 29b37babf..7920a8648 100644 --- a/frontend/src/app/components/mod.rs +++ b/frontend/src/app/components/mod.rs @@ -50,9 +50,12 @@ mod floating_background; mod source_editor; mod cluster_flags_input; mod title_card; +mod filter; // pub use self::input::*; // pub use self::menu_item::*; // pub use self::popup_menu::*; +//pub use self::number_input::*; +//pub use self::date_input::*; pub(crate) use self::cluster_flags_input::*; pub(crate) use self::toggle_switch::*; @@ -94,6 +97,4 @@ pub(crate) use self::loading_screen::*; pub(crate) use self::websocket_status::*; pub(crate) use self::source_editor::*; pub(crate) use self::title_card::*; - -//pub use self::number_input::*; -//pub use self::date_input::*; +pub(crate) use self::filter::*; diff --git a/frontend/src/app/components/source_editor/output_m3u_form.rs b/frontend/src/app/components/source_editor/output_m3u_form.rs index a86b0ac58..c27ad2e5f 100644 --- a/frontend/src/app/components/source_editor/output_m3u_form.rs +++ b/frontend/src/app/components/source_editor/output_m3u_form.rs @@ -1,6 +1,6 @@ use crate::app::components::config::HasFormData; -use crate::app::components::{BlockId, BlockInstance, Card, EditMode, SourceEditorContext, TextButton}; -use crate::{edit_field_bool, edit_field_text_option, generate_form_reducer}; +use crate::app::components::{BlockId, BlockInstance, Card, EditMode, FilterInput, SourceEditorContext, TextButton}; +use crate::{config_field_child, edit_field_bool, edit_field_text_option, generate_form_reducer}; use shared::model::{M3uTargetOutputDto, TargetOutputDto}; use std::rc::Rc; use yew::{function_component, html, use_context, use_effect_with, use_reducer, Callback, Html, Properties, UseReducerHandle}; @@ -54,12 +54,19 @@ pub fn M3uTargetOutputView(props: &M3uTargetOutputViewProps) -> Html { } let render_output = || { + let output_form_state_1 = output_form_state.clone(); html! { { edit_field_text_option!(output_form_state, translate.t(LABEL_FILENAME), filename, M3uTargetOutputFormAction::Filename) } { edit_field_bool!(output_form_state, translate.t(LABEL_INCLUDE_TYPE_IN_URL), include_type_in_url, M3uTargetOutputFormAction::IncludeTypeInUrl) } { edit_field_bool!(output_form_state, translate.t(LABEL_MASK_REDIRECT_URL), mask_redirect_url, M3uTargetOutputFormAction::MaskRedirectUrl) } - { edit_field_text_option!(output_form_state, translate.t(LABEL_FILTER), filter, M3uTargetOutputFormAction::Filter) } + { config_field_child!(translate.t(LABEL_FILTER), { + html! { + + } + })} } }; diff --git a/frontend/src/app/components/source_editor/output_strm_form.rs b/frontend/src/app/components/source_editor/output_strm_form.rs index 258078322..17f7d0c4d 100644 --- a/frontend/src/app/components/source_editor/output_strm_form.rs +++ b/frontend/src/app/components/source_editor/output_strm_form.rs @@ -1,6 +1,6 @@ use crate::app::components::config::HasFormData; use crate::app::components::select::Select; -use crate::app::components::{BlockId, BlockInstance, Card, DropDownOption, DropDownSelection, EditMode, Panel, SourceEditorContext, TextButton}; +use crate::app::components::{BlockId, BlockInstance, Card, DropDownOption, DropDownSelection, EditMode, FilterInput, Panel, SourceEditorContext, TextButton}; use crate::{config_field_child, edit_field_bool, edit_field_list_option, edit_field_text, edit_field_text_option, generate_form_reducer}; use shared::model::{StrmExportStyle, StrmTargetOutputDto, TargetOutputDto}; use std::fmt::Display; @@ -106,6 +106,7 @@ pub fn StrmTargetOutputView(props: &StrmTargetOutputViewProps) -> Html { let render_main = || { let output_form_state_1 = output_form_state.clone(); + let output_form_state_2 = output_form_state.clone(); html! { { edit_field_text!(output_form_state, translate.t(LABEL_DIRECTORY), directory, StrmTargetOutputFormAction::Directory) } @@ -134,7 +135,13 @@ pub fn StrmTargetOutputView(props: &StrmTargetOutputViewProps) -> Html { /> } })} - { edit_field_text_option!(output_form_state, translate.t(LABEL_FILTER), filter, StrmTargetOutputFormAction::Filter) } + { config_field_child!(translate.t(LABEL_FILTER), { + html! { + + } + })} } }; diff --git a/frontend/src/app/components/source_editor/output_xtream_form.rs b/frontend/src/app/components/source_editor/output_xtream_form.rs index a60e697df..9ff661416 100644 --- a/frontend/src/app/components/source_editor/output_xtream_form.rs +++ b/frontend/src/app/components/source_editor/output_xtream_form.rs @@ -1,5 +1,5 @@ use crate::app::components::config::HasFormData; -use crate::app::components::{BlockId, BlockInstance, Card, EditMode, IconButton, Panel, SourceEditorContext, TextButton, TraktListItemForm, TitledCard}; +use crate::app::components::{BlockId, BlockInstance, Card, EditMode, IconButton, Panel, SourceEditorContext, TextButton, TraktListItemForm, TitledCard, FilterInput}; use crate::{config_field_child, edit_field_bool, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer}; use shared::model::{TargetOutputDto, TraktApiConfigDto, TraktConfigDto, TraktContentType, TraktListConfigDto, XtreamTargetOutputDto}; use std::fmt::Display; @@ -165,6 +165,7 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html { }; let render_output = || { + let output_form_state_1 = output_form_state.clone(); html! { @@ -186,7 +187,13 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html { { edit_field_number_u16!(output_form_state, translate.t(LABEL_SERIES), resolve_series_delay, XtreamTargetOutputFormAction::ResolveSeriesDelay) } - { edit_field_text_option!(output_form_state, translate.t(LABEL_FILTER), filter, XtreamTargetOutputFormAction::Filter) } + { config_field_child!(translate.t(LABEL_FILTER), { + html! { + + } + })} } }; diff --git a/frontend/src/app/components/source_editor/target_form.rs b/frontend/src/app/components/source_editor/target_form.rs index af03f6465..2bd679a7d 100644 --- a/frontend/src/app/components/source_editor/target_form.rs +++ b/frontend/src/app/components/source_editor/target_form.rs @@ -1,6 +1,6 @@ use crate::app::components::config::HasFormData; use crate::app::components::select::Select; -use crate::app::components::{BlockId, BlockInstance, Card, ClusterFlagsInput, DropDownOption, DropDownSelection, EditMode, Panel, SourceEditorContext, TextButton}; +use crate::app::components::{BlockId, BlockInstance, Card, ClusterFlagsInput, DropDownOption, DropDownSelection, EditMode, FilterInput, Panel, SourceEditorContext, TextButton}; use crate::{config_field_child, edit_field_bool, edit_field_list_option, edit_field_text, generate_form_reducer}; use shared::model::{ClusterFlags, ConfigTargetDto, ConfigTargetOptions, ProcessingOrder}; use std::fmt::Display; @@ -160,6 +160,7 @@ pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html { let render_target = || { let target_form_state_1 = target_form_state.clone(); + let target_form_state_2 = target_form_state.clone(); html! {
@@ -167,7 +168,13 @@ pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html { { edit_field_bool!(target_form_state, translate.t(LABEL_USE_MEMORY_CACHE), use_memory_cache, ConfigTargetFormAction::UseMemoryCache) }
{ edit_field_text!(target_form_state, translate.t(LABEL_NAME), name, ConfigTargetFormAction::Name) } - { edit_field_text!(target_form_state, translate.t(LABEL_FILTER), filter, ConfigTargetFormAction::Filter) } + { config_field_child!(translate.t(LABEL_FILTER), { + html! { + | { + target_form_state_2.dispatch(ConfigTargetFormAction::Filter(new_filter.unwrap_or_else(String::new))); + })} /> + } + })} { config_field_child!(translate.t(LABEL_PROCESSING_ORDER), { html! { diff --git a/frontend/src/model/web_config.rs b/frontend/src/model/web_config.rs index 0eb6476fc..db519e842 100644 --- a/frontend/src/model/web_config.rs +++ b/frontend/src/model/web_config.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; +pub fn default_documentation_url() -> String { String::from("https://euzu.github.io/tuliprox-docs/") } + + #[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug, Default)] pub struct ApiConfig { #[serde(alias = "apiUrl")] @@ -24,7 +27,7 @@ pub struct WebConfig { pub discord: String, #[serde(default)] pub github: String, - #[serde(default)] + #[serde(default = "default_documentation_url")] pub documentation: String, #[serde(alias = "wsUrl")] pub ws_url: String, From bd911e38a149d6eac780785b31b121ac2a7a2eef Mon Sep 17 00:00:00 2001 From: euzu Date: Wed, 3 Dec 2025 18:51:42 +0100 Subject: [PATCH 2/9] Cluster flags input has now 2 modes --- .../src/app/components/cluster_flags_input.rs | 16 ++++++++++++++-- .../src/app/components/filter/filter_input.rs | 5 +++-- .../source_editor/output_xtream_form.rs | 2 +- .../app/components/source_editor/target_form.rs | 3 ++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/cluster_flags_input.rs b/frontend/src/app/components/cluster_flags_input.rs index b19e0c2b1..342dcfee1 100644 --- a/frontend/src/app/components/cluster_flags_input.rs +++ b/frontend/src/app/components/cluster_flags_input.rs @@ -2,6 +2,13 @@ use shared::model::{ClusterFlags}; use yew::prelude::*; use yew_i18n::use_translation; +#[derive(Clone, Copy, PartialEq, Debug, Default)] +pub enum ClusterFlagsInputMode { + #[default] + NoneIsAll, + NoneIsNone, +} + #[derive(Properties, Clone, PartialEq, Debug)] pub struct ClusterFlagsInputProps { pub name: String, @@ -9,6 +16,8 @@ pub struct ClusterFlagsInputProps { pub value: Option, #[prop_or_default] pub on_change: Callback<(String, Option)>, + #[prop_or_default] + pub mode: ClusterFlagsInputMode, } #[function_component] @@ -18,8 +27,11 @@ pub fn ClusterFlagsInput(props: &ClusterFlagsInputProps) -> Html { let flags = use_state(|| props.value.unwrap_or_else(ClusterFlags::all)); { let set_flags = flags.clone(); - use_effect_with(props.value, move |val| { - set_flags.set((*val).unwrap_or_else(ClusterFlags::all)); + use_effect_with((props.value, props.mode), move |(val, cmode)| { + set_flags.set((*val).unwrap_or_else(|| match cmode { + ClusterFlagsInputMode::NoneIsAll => ClusterFlags::all(), + ClusterFlagsInputMode::NoneIsNone => ClusterFlags::empty(), + })); }); } diff --git a/frontend/src/app/components/filter/filter_input.rs b/frontend/src/app/components/filter/filter_input.rs index 39f92193c..79505ff96 100644 --- a/frontend/src/app/components/filter/filter_input.rs +++ b/frontend/src/app/components/filter/filter_input.rs @@ -52,10 +52,11 @@ pub fn FilterInput(props: &FilterInputProps) -> Html { let current_filter = filter_state.clone(); Callback::from(move |e: MouseEvent| { e.prevent_default(); + let current_filter = (*current_filter).clone(); let dlg = dialog.clone(); spawn_local(async move { - //let filter_view = html!{}; - //let _result = dlg.content(filter_view, None).await; + let filter_view = html!{
{current_filter}
}; + let _result = dlg.content(filter_view, None).await; }); }) }; diff --git a/frontend/src/app/components/source_editor/output_xtream_form.rs b/frontend/src/app/components/source_editor/output_xtream_form.rs index 9ff661416..dc3e4cf2f 100644 --- a/frontend/src/app/components/source_editor/output_xtream_form.rs +++ b/frontend/src/app/components/source_editor/output_xtream_form.rs @@ -1,6 +1,6 @@ use crate::app::components::config::HasFormData; use crate::app::components::{BlockId, BlockInstance, Card, EditMode, IconButton, Panel, SourceEditorContext, TextButton, TraktListItemForm, TitledCard, FilterInput}; -use crate::{config_field_child, edit_field_bool, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer}; +use crate::{config_field_child, edit_field_bool, edit_field_number_u16, edit_field_text, generate_form_reducer}; use shared::model::{TargetOutputDto, TraktApiConfigDto, TraktConfigDto, TraktContentType, TraktListConfigDto, XtreamTargetOutputDto}; use std::fmt::Display; use std::rc::Rc; diff --git a/frontend/src/app/components/source_editor/target_form.rs b/frontend/src/app/components/source_editor/target_form.rs index 2bd679a7d..3e6f47f8b 100644 --- a/frontend/src/app/components/source_editor/target_form.rs +++ b/frontend/src/app/components/source_editor/target_form.rs @@ -1,6 +1,6 @@ use crate::app::components::config::HasFormData; use crate::app::components::select::Select; -use crate::app::components::{BlockId, BlockInstance, Card, ClusterFlagsInput, DropDownOption, DropDownSelection, EditMode, FilterInput, Panel, SourceEditorContext, TextButton}; +use crate::app::components::{BlockId, BlockInstance, Card, ClusterFlagsInput, ClusterFlagsInputMode, DropDownOption, DropDownSelection, EditMode, FilterInput, Panel, SourceEditorContext, TextButton}; use crate::{config_field_child, edit_field_bool, edit_field_list_option, edit_field_text, generate_form_reducer}; use shared::model::{ClusterFlags, ConfigTargetDto, ConfigTargetOptions, ProcessingOrder}; use std::fmt::Display; @@ -149,6 +149,7 @@ pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html { )| { target_options_state_1.dispatch(ConfigTargetOptionsFormAction::ForceRedirect(flags)); })} From b1903096b7d3ca0a86081fb5c4b8e7b5174d904d Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 4 Dec 2025 16:28:03 +0100 Subject: [PATCH 3/9] Worked on Filter Input and Filter editor --- frontend/public/assets/i18n/en.json | 3 +- frontend/scss/app/_component.scss | 1 + .../scss/app/components/_collapse_panel.scss | 1 + .../scss/app/components/_filter_editor.scss | 85 ++++++++++++ frontend/scss/app/components/_input.scss | 1 + frontend/src/app/components/confirm_dialog.rs | 4 +- frontend/src/app/components/content_dialog.rs | 4 +- frontend/src/app/components/custom_dialog.rs | 7 +- .../app/components/filter/filter_editor.rs | 123 ++++++++++++++++++ .../src/app/components/filter/filter_input.rs | 46 ++++++- frontend/src/app/components/reveal_content.rs | 2 +- frontend/src/provider/dialog_provider.rs | 1 + frontend/src/services/dialog_service.rs | 4 +- shared/src/model/config/pattern_template.rs | 18 +++ 14 files changed, 288 insertions(+), 12 deletions(-) create mode 100644 frontend/scss/app/components/_filter_editor.scss diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index a52e93860..9e922d53b 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -343,7 +343,8 @@ "EPG_LOGO_OVERRIDE": "Logo Override", "API_CONFIGURATION" : "API Configuration", "HEADER_NAME": "Header Name", - "HEADER_VALUE": "Header Value" + "HEADER_VALUE": "Header Value", + "TEMPLATES": "Templates" }, "TITLE": { "USER_BOUQUET_EDITOR": "Playlist Category Selection" diff --git a/frontend/scss/app/_component.scss b/frontend/scss/app/_component.scss index 3ad18a9e5..bf2dc35d8 100644 --- a/frontend/scss/app/_component.scss +++ b/frontend/scss/app/_component.scss @@ -82,3 +82,4 @@ @forward "components/cluster_flags"; @forward "components/titled_card"; @forward "components/filter_input"; +@forward "components/filter_editor"; diff --git a/frontend/scss/app/components/_collapse_panel.scss b/frontend/scss/app/components/_collapse_panel.scss index fa7b86645..87ec82be9 100644 --- a/frontend/scss/app/components/_collapse_panel.scss +++ b/frontend/scss/app/components/_collapse_panel.scss @@ -13,6 +13,7 @@ cursor: pointer; box-sizing: border-box; width: 100%; + padding: var(--padding-small); &:hover { color: var(--menu-button-hover-color); diff --git a/frontend/scss/app/components/_filter_editor.scss b/frontend/scss/app/components/_filter_editor.scss new file mode 100644 index 000000000..d6580658e --- /dev/null +++ b/frontend/scss/app/components/_filter_editor.scss @@ -0,0 +1,85 @@ +.tp__filter-editor { + display: flex; + flex-flow: column; + overflow: hidden; + background-color: transparent; + box-sizing: border-box; + gap: var(--gap-default); + + &__templates { + display: flex; + flex-flow: column; + background-color: transparent; + box-sizing: border-box; + padding: var(--padding-default); + gap: var(--gap-default); + max-height: 400px; + overflow: auto; + + &-container { + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + } + + &-content { + display: grid; + grid-template-columns: minmax(max-content, 100px) 1fr; + background-color: transparent; + box-sizing: border-box; + gap: var(--gap-default); + + } + + &-template { + display: flex; + flex-flow: row nowrap; + overflow: hidden; + box-sizing: border-box; + gap: var(--gap-default); + + &-name { + font-weight: bold; + color: var(--code-reserved-color); + &:after { + content: ':'; + } + } + + &-value { + color: var(--modest-text-color); + } + } + } + + &__editor { + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--code-editor-background-color); + color: var(--code-editor-color); + + &-input { + height: 20rem; + width: 100rem; + } + } + + &__preview { + display: flex; + flex-flow: column; + border-radius: var(--border-radius); + min-height: 3rem; + max-height: 12rem; + } +} + +.tp__filter-editor-valid { + .tp__filter-editor__preview { + border: 1px solid var(--ok-color); + } +} + +.tp__filter-editor-invalid { + .tp__filter-editor__preview { + border: 1px solid var(--attention-color); + } +} \ No newline at end of file diff --git a/frontend/scss/app/components/_input.scss b/frontend/scss/app/components/_input.scss index d2d96a021..7cc02f8e2 100644 --- a/frontend/scss/app/components/_input.scss +++ b/frontend/scss/app/components/_input.scss @@ -43,6 +43,7 @@ } } +textarea, input { background-clip: content-box; box-sizing: border-box; diff --git a/frontend/src/app/components/confirm_dialog.rs b/frontend/src/app/components/confirm_dialog.rs index c0bb2d7c5..c778acc51 100644 --- a/frontend/src/app/components/confirm_dialog.rs +++ b/frontend/src/app/components/confirm_dialog.rs @@ -10,6 +10,8 @@ pub struct ConfirmDialogProps { pub ok_caption: String, pub cancel_caption: String, pub on_confirm: Callback, + #[prop_or(true)] + pub close_on_backdrop_click: bool, } #[function_component] @@ -45,7 +47,7 @@ pub fn ConfirmDialog(props: &ConfirmDialogProps) -> Html { open={*is_open} class="tp__confirm-dialog" modal=true - close_on_backdrop_click=true + close_on_backdrop_click={props.close_on_backdrop_click} on_close={Some(on_close)} >

{ &props.title }

diff --git a/frontend/src/app/components/content_dialog.rs b/frontend/src/app/components/content_dialog.rs index e26d67980..57fdf3324 100644 --- a/frontend/src/app/components/content_dialog.rs +++ b/frontend/src/app/components/content_dialog.rs @@ -9,6 +9,8 @@ pub struct ContentDialogProps { pub content: Html, pub actions: DialogActions, pub on_confirm: Callback, + #[prop_or(true)] + pub close_on_backdrop_click: bool, } #[function_component] @@ -68,7 +70,7 @@ pub fn ContentDialog(props: &ContentDialogProps) -> Html { open={*is_open} class="tp__content-dialog" modal=true - close_on_backdrop_click=true + close_on_backdrop_click={props.close_on_backdrop_click} on_close={on_close} > { props.content.clone() } diff --git a/frontend/src/app/components/custom_dialog.rs b/frontend/src/app/components/custom_dialog.rs index c7c49b1f2..def0805f8 100644 --- a/frontend/src/app/components/custom_dialog.rs +++ b/frontend/src/app/components/custom_dialog.rs @@ -1,3 +1,4 @@ +use log::debug; use yew::prelude::*; use web_sys::MouseEvent; @@ -26,12 +27,14 @@ pub fn CustomDialog(props: &CustomDialogProps) -> Html { || () }); } - + + debug!(" props close {}", props.close_on_backdrop_click); + // Handle backdrop click let on_backdrop_click = { let on_close = props.on_close.clone(); let close_on_backdrop = props.close_on_backdrop_click; - + Callback::from(move |_e: MouseEvent| { if close_on_backdrop { if let Some(on_close) = &on_close { diff --git a/frontend/src/app/components/filter/filter_editor.rs b/frontend/src/app/components/filter/filter_editor.rs index e69de29bb..8353e981a 100644 --- a/frontend/src/app/components/filter/filter_editor.rs +++ b/frontend/src/app/components/filter/filter_editor.rs @@ -0,0 +1,123 @@ +use web_sys::InputEvent; +use crate::app::ConfigContext; +use shared::model::PatternTemplate; +use yew::{classes, function_component, html, use_context, use_effect_with, use_state, Callback, Html, Properties, TargetCast}; +use yew_i18n::use_translation; +use shared::foundation::filter::{get_filter}; +use crate::app::components::{CollapsePanel, FilterView}; + +#[derive(Properties, Clone, PartialEq, Debug)] +pub struct FilterEditorProps { + #[prop_or_default] + pub filter: Option, + #[prop_or_default] + pub on_filter_change: Callback>, + pub on_templates_change: Callback>>, +} + +#[function_component] +pub fn FilterEditor(props: &FilterEditorProps) -> Html { + let config_ctx = use_context::().expect("Config context not found"); + let translate = use_translation(); + + let templates_state = use_state(|| None); + let filter_state = use_state(|| None); + let parsed_filter_state = use_state(|| None); + let valid_filter_state = use_state(|| true); + + { + let templates = templates_state.clone(); + let cfg_templates = config_ctx.config.as_ref().and_then(|c| c.sources.templates.clone()); + use_effect_with(cfg_templates, move |templ| { + templates.set(templ.clone()); + }); + } + + { + let templates = templates_state.clone(); + let cfg_templates = config_ctx.config.as_ref().and_then(|c| c.sources.templates.clone()); + use_effect_with(cfg_templates, move |templ| { + templates.set(templ.clone()); + }); + } + + { + let filter = filter_state.clone(); + use_effect_with(props.filter.clone(), move |flt| { + filter.set(flt.clone()); + }); + } + + { + let filter = filter_state.clone(); + let parsed_filter = parsed_filter_state.clone(); + let templates = templates_state.clone(); + let valid_filter = valid_filter_state.clone(); + use_effect_with(filter.clone(), move |flt| { + let parsed = if let Some(new_fltr) = flt.as_ref() { + match get_filter(new_fltr, (*templates).as_ref()) { + Ok(fltr) => { + valid_filter.set(true); + Some(fltr) + } + Err(_) => { + valid_filter.set(false); + None + } + } + } else { + valid_filter.set(true); + None + }; + parsed_filter.set(parsed); + }); + } + + let handle_filter_input = { + let filter = filter_state.clone(); + Callback::from(move |event: InputEvent| { + if let Some(input) = event.target_dyn_into::() { + let value = input.value(); + if value.is_empty() { + filter.set(None); + } else { + filter.set(Some(value)); + } + } + }) + }; + + html! { +
+ +
+
+ { if let Some(templ_vec) = &*templates_state { + html! { + for templ_vec.iter().map(|templ| html! { + <> +
+ { &templ.name } +
+
+ { templ.value.to_string() } +
+ + }) + } + } else { + html! {} + } + } +
+
+
+
+