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