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::model::*;
21use crate::session::Session;
22use crate::*;
23
24#[derive(Properties, PartialEq, PerspectiveProperties!, Clone)]
25pub struct ExpressionEditorProps {
26    pub on_save: Callback<()>,
27    pub on_validate: Callback<bool>,
28    pub on_input: Callback<Rc<String>>,
29    pub alias: Option<String>,
30    pub disabled: bool,
31
32    #[prop_or_default]
33    pub reset_count: u8,
34
35    // State
36    pub session: Session,
37}
38
39#[derive(Debug)]
40pub enum ExpressionEditorMsg {
41    SetExpr(Rc<String>),
42    ValidateComplete(Option<ExprValidationError>),
43}
44
45/// Expression editor component `CodeEditor` and a button toolbar.
46pub struct ExpressionEditor {
47    expr: Rc<String>,
48    error: Option<ExprValidationError>,
49    oninput: Callback<Rc<String>>,
50}
51
52impl Component for ExpressionEditor {
53    type Message = ExpressionEditorMsg;
54    type Properties = ExpressionEditorProps;
55
56    fn create(ctx: &Context<Self>) -> Self {
57        let oninput = ctx.link().callback(ExpressionEditorMsg::SetExpr);
58        let expr = initial_expr(&ctx.props().session, &ctx.props().alias);
59        ctx.link()
60            .send_message(Self::Message::SetExpr(expr.clone()));
61
62        Self {
63            error: None,
64            expr,
65            oninput,
66        }
67    }
68
69    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
70        match msg {
71            ExpressionEditorMsg::SetExpr(val) => {
72                ctx.props().on_input.emit(val.clone());
73                self.expr = val.clone();
74                clone!(ctx.props().session);
75                ctx.link().send_future(async move {
76                    match session.validate_expr(&val).await {
77                        Ok(x) => ExpressionEditorMsg::ValidateComplete(x),
78                        Err(err) => {
79                            web_sys::console::error_1(&format!("{err:?}").into());
80                            ExpressionEditorMsg::ValidateComplete(None)
81                        },
82                    }
83                });
84
85                true
86            },
87            ExpressionEditorMsg::ValidateComplete(err) => {
88                self.error = err;
89                if self.error.is_none() {
90                    maybe!({
91                        let alias = ctx.props().alias.as_ref()?;
92                        let session = ctx.props().session();
93                        let old = session.metadata().get_expression_by_alias(alias)?;
94                        let is_edited = *self.expr != old;
95                        session
96                            .metadata_mut()
97                            .set_edit_by_alias(alias, self.expr.to_string());
98
99                        Some(is_edited)
100                    });
101
102                    ctx.props().on_validate.emit(true);
103                } else {
104                    ctx.props().on_validate.emit(false);
105                }
106                true
107            },
108        }
109    }
110
111    fn view(&self, ctx: &Context<Self>) -> Html {
112        let disabled_class = ctx.props().disabled.then_some("disabled");
113        clone!(ctx.props().disabled);
114        html! {
115            <>
116                <LocalStyle href={css!("expression-editor")} />
117                <label class="item_title">{ "Expression" }</label>
118                <div id="editor-container" class={disabled_class}>
119                    <CodeEditor
120                        autofocus=true
121                        expr={&self.expr}
122                        autosuggest=true
123                        error={self.error.clone().map(|x| x.into())}
124                        {disabled}
125                        oninput={self.oninput.clone()}
126                        onsave={ctx.props().on_save.clone()}
127                    />
128                    <div id="psp-expression-editor-meta">
129                        <div class="error">
130                            { &self.error.clone().map(|e| e.error_message).unwrap_or_default() }
131                        </div>
132                    </div>
133                </div>
134            </>
135        }
136    }
137
138    fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
139        if ctx.props().alias != old_props.alias || ctx.props().reset_count != old_props.reset_count
140        {
141            ctx.link()
142                .send_message(ExpressionEditorMsg::SetExpr(initial_expr(
143                    &ctx.props().session,
144                    &ctx.props().alias,
145                )));
146            false
147        } else {
148            true
149        }
150    }
151}
152
153fn initial_expr(session: &Session, alias: &Option<String>) -> Rc<String> {
154    alias
155        .as_ref()
156        .and_then(|alias| session.metadata().get_expression_by_alias(alias))
157        .unwrap_or_default()
158        .into()
159}