Skip to main content

perspective_viewer/components/
expression_editor.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::rc::Rc;
14
15use perspective_client::{ExprValidationError, clone};
16use yew::prelude::*;
17
18use super::form::code_editor::*;
19use super::style::LocalStyle;
20use crate::session::{Session, SessionMetadata, SessionMetadataRc};
21use crate::*;
22
23#[derive(Properties, PartialEq, Clone)]
24pub struct ExpressionEditorProps {
25    pub on_save: Callback<()>,
26    pub on_validate: Callback<bool>,
27    pub on_input: Callback<Rc<String>>,
28    pub alias: Option<String>,
29    pub disabled: bool,
30
31    #[prop_or_default]
32    pub reset_count: u8,
33
34    /// Session metadata snapshot — threaded from `SessionProps`.
35    pub metadata: SessionMetadataRc,
36
37    /// Selected theme name, threaded for PortalModal consumers.
38    #[prop_or_default]
39    pub selected_theme: Option<String>,
40
41    // State
42    pub session: Session,
43}
44
45#[derive(Debug)]
46pub enum ExpressionEditorMsg {
47    SetExpr(Rc<String>),
48    ValidateComplete(Option<ExprValidationError>),
49}
50
51/// Expression editor component `CodeEditor` and a button toolbar.
52pub struct ExpressionEditor {
53    expr: Rc<String>,
54    error: Option<ExprValidationError>,
55    oninput: Callback<Rc<String>>,
56}
57
58impl Component for ExpressionEditor {
59    type Message = ExpressionEditorMsg;
60    type Properties = ExpressionEditorProps;
61
62    fn create(ctx: &Context<Self>) -> Self {
63        let oninput = ctx.link().callback(ExpressionEditorMsg::SetExpr);
64        let expr = initial_expr(&ctx.props().metadata, &ctx.props().alias);
65        ctx.link()
66            .send_message(Self::Message::SetExpr(expr.clone()));
67
68        Self {
69            error: None,
70            expr,
71            oninput,
72        }
73    }
74
75    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
76        match msg {
77            ExpressionEditorMsg::SetExpr(val) => {
78                ctx.props().on_input.emit(val.clone());
79                self.expr = val.clone();
80                clone!(ctx.props().session);
81                ctx.link().send_future(async move {
82                    match session.validate_expr(&val).await {
83                        Ok(x) => ExpressionEditorMsg::ValidateComplete(x),
84                        Err(err) => {
85                            web_sys::console::error_1(&format!("{err:?}").into());
86                            ExpressionEditorMsg::ValidateComplete(None)
87                        },
88                    }
89                });
90
91                true
92            },
93            ExpressionEditorMsg::ValidateComplete(err) => {
94                self.error = err;
95                if self.error.is_none() {
96                    maybe!({
97                        let alias = ctx.props().alias.as_ref()?;
98                        let session = &ctx.props().session;
99                        let old = ctx.props().metadata.get_expression_by_alias(alias)?;
100                        let is_edited = *self.expr != old;
101                        session
102                            .metadata_mut()
103                            .set_edit_by_alias(alias, self.expr.to_string());
104
105                        Some(is_edited)
106                    });
107
108                    ctx.props().on_validate.emit(true);
109                } else {
110                    ctx.props().on_validate.emit(false);
111                }
112                true
113            },
114        }
115    }
116
117    fn view(&self, ctx: &Context<Self>) -> Html {
118        let disabled_class = ctx.props().disabled.then_some("disabled");
119        clone!(ctx.props().disabled);
120        html! {
121            <>
122                <LocalStyle href={css!("expression-editor")} />
123                <label class="item_title">{ "Expression" }</label>
124                <div id="editor-container" class={disabled_class}>
125                    <CodeEditor
126                        autofocus=true
127                        expr={&self.expr}
128                        autosuggest=true
129                        error={self.error.clone().map(|x| x.into())}
130                        {disabled}
131                        oninput={self.oninput.clone()}
132                        onsave={ctx.props().on_save.clone()}
133                        theme={ctx.props().selected_theme.clone().unwrap_or_default()}
134                    />
135                    <div id="psp-expression-editor-meta">
136                        <div class="error">
137                            { &self.error.clone().map(|e| e.error_message).unwrap_or_default() }
138                        </div>
139                    </div>
140                </div>
141            </>
142        }
143    }
144
145    fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
146        if ctx.props().alias != old_props.alias
147            || ctx.props().reset_count != old_props.reset_count
148            || (ctx.props().alias.is_some() && ctx.props().metadata != old_props.metadata)
149        {
150            ctx.link()
151                .send_message(ExpressionEditorMsg::SetExpr(initial_expr(
152                    &ctx.props().metadata,
153                    &ctx.props().alias,
154                )));
155            false
156        } else {
157            true
158        }
159    }
160}
161
162fn initial_expr(metadata: &SessionMetadata, alias: &Option<String>) -> Rc<String> {
163    alias
164        .as_ref()
165        .and_then(|alias| metadata.get_expression_by_alias(alias))
166        .unwrap_or_default()
167        .into()
168}