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::code_editor::CodeEditor;
19use crate::session::{Session, SessionMetadataRc};
20use crate::tasks::{ExprValidation, validate_expression};
21
22#[derive(Properties, PartialEq, Clone)]
23pub struct ExpressionEditorProps {
24    pub on_save: Callback<()>,
25    pub on_validate: Callback<bool>,
26    pub on_input: Callback<Rc<String>>,
27    pub alias: Option<String>,
28    pub disabled: bool,
29
30    /// The saved expression text this editor edits against.
31    pub initial_expr: Rc<String>,
32
33    #[prop_or_default]
34    pub reset_count: u8,
35
36    /// Session metadata snapshot — threaded from `SessionProps`.
37    pub metadata: SessionMetadataRc,
38
39    /// Selected theme name, threaded for PortalModal consumers.
40    #[prop_or_default]
41    pub selected_theme: Option<String>,
42
43    // State
44    pub session: Session,
45}
46
47#[derive(Debug)]
48pub enum ExpressionEditorMsg {
49    SetExpr(Rc<String>),
50    ValidateComplete(ExprValidation),
51}
52
53/// Expression editor component `CodeEditor` and a button toolbar.
54pub struct ExpressionEditor {
55    expr: Rc<String>,
56    error: Option<ExprValidationError>,
57    oninput: Callback<Rc<String>>,
58
59    /// Monotonically increasing request id used to drop stale
60    /// validation results when the user types faster than the engine
61    /// can validate.
62    validation_req_id: u64,
63
64    /// The id of the most recently dispatched validation; the result
65    /// is only applied when its echoed id matches.
66    last_dispatched_req_id: u64,
67}
68
69impl Component for ExpressionEditor {
70    type Message = ExpressionEditorMsg;
71    type Properties = ExpressionEditorProps;
72
73    fn create(ctx: &Context<Self>) -> Self {
74        let oninput = ctx.link().callback(ExpressionEditorMsg::SetExpr);
75        let expr = ctx.props().initial_expr.clone();
76        ctx.link()
77            .send_message(Self::Message::SetExpr(expr.clone()));
78
79        Self {
80            error: None,
81            expr,
82            oninput,
83            validation_req_id: 0,
84            last_dispatched_req_id: 0,
85        }
86    }
87
88    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
89        match msg {
90            ExpressionEditorMsg::SetExpr(val) => {
91                ctx.props().on_input.emit(val.clone());
92                self.expr = val.clone();
93                self.validation_req_id += 1;
94                self.last_dispatched_req_id = self.validation_req_id;
95                let cb = ctx.link().callback(ExpressionEditorMsg::ValidateComplete);
96                validate_expression(
97                    &ctx.props().session,
98                    cb,
99                    self.validation_req_id,
100                    (*val).clone(),
101                );
102                true
103            },
104            ExpressionEditorMsg::ValidateComplete(result) => {
105                if result.req_id != self.last_dispatched_req_id {
106                    // Stale result from a superseded request — ignore.
107                    return false;
108                }
109                self.error = result.error;
110                if self.error.is_none() {
111                    let _: Option<bool> = try {
112                        let alias = ctx.props().alias.as_ref()?;
113                        let session = &ctx.props().session;
114                        let old = ctx.props().metadata.get_expression_by_alias(alias)?;
115                        let is_edited = *self.expr != old;
116                        session
117                            .metadata_mut()
118                            .set_edit_by_alias(alias, self.expr.to_string());
119
120                        is_edited
121                    };
122
123                    ctx.props().on_validate.emit(true);
124                } else {
125                    ctx.props().on_validate.emit(false);
126                }
127                true
128            },
129        }
130    }
131
132    fn view(&self, ctx: &Context<Self>) -> Html {
133        let disabled_class = ctx.props().disabled.then_some("disabled");
134        clone!(ctx.props().disabled);
135        html! {
136            <>
137                <label class="item_title">{ "Expression" }</label>
138                <div id="editor-container" class={disabled_class}>
139                    <CodeEditor
140                        autofocus=true
141                        expr={&self.expr}
142                        autosuggest=true
143                        error={self.error.clone().map(|x| x.into())}
144                        {disabled}
145                        oninput={self.oninput.clone()}
146                        onsave={ctx.props().on_save.clone()}
147                        theme={ctx.props().selected_theme.clone().unwrap_or_default()}
148                    />
149                    <div id="psp-expression-editor-meta">
150                        <div class="error">
151                            { &self.error.clone().map(|e| e.error_message).unwrap_or_default() }
152                        </div>
153                    </div>
154                </div>
155            </>
156        }
157    }
158
159    fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
160        if ctx.props().alias != old_props.alias
161            || ctx.props().reset_count != old_props.reset_count
162            || ctx.props().initial_expr != old_props.initial_expr
163        {
164            ctx.link().send_message(ExpressionEditorMsg::SetExpr(
165                ctx.props().initial_expr.clone(),
166            ));
167            false
168        } else {
169            true
170        }
171    }
172}