@perspective-dev/viewer 5.5.0 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -3
- package/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +3 -3
- package/dist/css/blueprint.css +1 -1
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/eggplant.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/ar.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/ledger.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/nord.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/css/velvet.css +1 -1
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/column-format.js +2 -0
- package/dist/esm/column-format.js.map +7 -0
- package/dist/esm/extensions.d.ts +3 -13
- package/dist/esm/perspective-viewer.d.ts +1 -1
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +3 -3
- package/dist/esm/plugin.d.ts +4 -1
- package/dist/esm/select-detail.d.ts +15 -0
- package/dist/esm/select-detail.js +2 -0
- package/dist/esm/select-detail.js.map +7 -0
- package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
- package/dist/esm/ts-rs/TableRef.d.ts +9 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
- package/dist/wasm/perspective-viewer.d.ts +126 -35
- package/dist/wasm/perspective-viewer.js +320 -185
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
- package/package.json +33 -20
- package/src/css/column-selector.css +205 -50
- package/src/css/ui/containers/context-menu.css +12 -1
- package/src/css/ui/containers/dropdown-menu-items.css +29 -0
- package/src/css/ui/containers/scroll-panel.css +4 -1
- package/src/rust/agent/client/protocol.rs +0 -89
- package/src/rust/agent/markdown/mod.rs +0 -238
- package/src/rust/agent/preamble.md +1 -0
- package/src/rust/agent/tools.rs +2 -16
- package/src/rust/components/column_selector/active_column.rs +15 -5
- package/src/rust/components/column_selector/add_expression_button.rs +9 -26
- package/src/rust/components/column_selector/config_selector.rs +34 -6
- package/src/rust/components/column_selector/derived_control.rs +229 -0
- package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
- package/src/rust/components/column_selector/filter_column.rs +0 -2
- package/src/rust/components/column_selector/inactive_column.rs +4 -5
- package/src/rust/components/column_selector/pivot_column.rs +21 -1
- package/src/rust/components/column_selector/sort_column.rs +4 -3
- package/src/rust/components/column_selector.rs +21 -1
- package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
- package/src/rust/components/column_settings_sidebar.rs +25 -19
- package/src/rust/components/debug_panel.rs +38 -29
- package/src/rust/components/dragdrop_list.rs +8 -9
- package/src/rust/components/export_dropdown.rs +6 -0
- package/src/rust/components/main_panel/msg.rs +6 -4
- package/src/rust/components/main_panel/reconcile.rs +6 -1
- package/src/rust/components/main_panel/render.rs +70 -51
- package/src/rust/components/main_panel/update.rs +39 -1
- package/src/rust/components/main_panel.rs +97 -59
- package/src/rust/components/new_panel_menu.rs +69 -1
- package/src/rust/components/panel_menu.rs +63 -12
- package/src/rust/components/plugin_tab.rs +3 -2
- package/src/rust/components/render_warning.rs +2 -1
- package/src/rust/components/settings_panel.rs +4 -4
- package/src/rust/components/status_bar.rs +80 -21
- package/src/rust/components/status_indicator.rs +8 -1
- package/src/rust/components/viewer/filters.rs +15 -3
- package/src/rust/components/viewer/msg.rs +8 -0
- package/src/rust/components/viewer/panels.rs +44 -4
- package/src/rust/components/viewer/render.rs +1 -0
- package/src/rust/components/viewer/settings.rs +0 -22
- package/src/rust/components/viewer/snapshots.rs +2 -2
- package/src/rust/components/viewer/wiring.rs +18 -1
- package/src/rust/components/viewer.rs +47 -27
- package/src/rust/components/window_editor.rs +5 -4
- package/src/rust/config/column_config_schema.rs +38 -377
- package/src/rust/config/export_method.rs +24 -0
- package/src/rust/config/number_string_format.rs +0 -94
- package/src/rust/config/options.rs +3 -7
- package/src/rust/config/plugin_static_config.rs +0 -42
- package/src/rust/config/viewer_config.rs +47 -17
- package/src/rust/config/workspace_config.rs +158 -128
- package/src/rust/custom_elements/viewer.rs +189 -129
- package/src/rust/custom_events.rs +33 -18
- package/src/rust/derived.rs +266 -0
- package/src/rust/exprtk/tokenize.rs +0 -54
- package/src/rust/js/plugin.rs +1 -1
- package/src/rust/lib.rs +3 -0
- package/src/rust/presentation.rs +23 -4
- package/src/rust/queries/column_locator.rs +55 -48
- package/src/rust/queries/get_viewer_config.rs +1 -1
- package/src/rust/queries/plugin_column_styles.rs +22 -23
- package/src/rust/renderer/dispatch.rs +2 -0
- package/src/rust/renderer/plugin_config.rs +652 -487
- package/src/rust/renderer/props.rs +4 -0
- package/src/rust/renderer/registry.rs +2 -6
- package/src/rust/renderer/state.rs +205 -0
- package/src/rust/renderer.rs +201 -59
- package/src/rust/session/metadata.rs +35 -19
- package/src/rust/session/op_queue.rs +430 -0
- package/src/rust/session/panel_state.rs +327 -0
- package/src/rust/session/view_subscription.rs +42 -3
- package/src/rust/session.rs +931 -419
- package/src/rust/tasks/apply_global_filters.rs +49 -35
- package/src/rust/tasks/auto_pause.rs +22 -12
- package/src/rust/tasks/copy_export.rs +72 -0
- package/src/rust/tasks/create_panel.rs +46 -21
- package/src/rust/tasks/derive.rs +312 -0
- package/src/rust/tasks/mod.rs +6 -2
- package/src/rust/tasks/pipeline.rs +152 -89
- package/src/rust/tasks/reset_all.rs +31 -20
- package/src/rust/tasks/restore_derived.rs +200 -0
- package/src/rust/tasks/restore_panel.rs +153 -141
- package/src/rust/tasks/send_column_config.rs +56 -29
- package/src/rust/tasks/send_plugin_config.rs +52 -23
- package/src/rust/tasks/set_edit_mode.rs +33 -25
- package/src/rust/tasks/sync_update_panels.rs +40 -13
- package/src/rust/tasks/table_lifecycle.rs +70 -34
- package/src/rust/tasks/transactional_restore.rs +387 -0
- package/src/rust/tasks/update_theme.rs +45 -6
- package/src/rust/ui/containers/scroll_panel.rs +17 -10
- package/src/rust/ui/containers/split_panel.rs +0 -41
- package/src/rust/ui/context_menu.rs +157 -18
- package/src/rust/ui/font_loader.rs +23 -35
- package/src/rust/ui/form/mirrored_textarea.rs +0 -12
- package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
- package/src/rust/ui/form/palette_selector.rs +0 -51
- package/src/rust/ui/portal.rs +4 -10
- package/src/rust/utils/browser/mod.rs +2 -3
- package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
- package/src/rust/utils/browser/request_animation_frame.rs +27 -8
- package/src/rust/utils/css.rs +0 -360
- package/src/rust/utils/{tests/mod.rs → in_flight.rs} +62 -2
- package/src/rust/utils/local_fonts.rs +0 -36
- package/src/rust/utils/mod.rs +4 -3
- package/src/rust/utils/modal_position.rs +15 -21
- package/src/rust/utils/pending_effects.rs +6 -30
- package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
- package/src/rust/utils/pubsub.rs +0 -1
- package/src/rust/workspace.rs +187 -140
- package/src/svg/derived.svg +6 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/ar.css +10 -0
- package/src/themes/intl/de.css +10 -0
- package/src/themes/intl/es.css +10 -0
- package/src/themes/intl/fr.css +10 -0
- package/src/themes/intl/ja.css +10 -0
- package/src/themes/intl/pt.css +10 -0
- package/src/themes/intl/zh.css +10 -0
- package/src/themes/intl.css +10 -0
- package/src/themes/monokai.css +1 -1
- package/src/themes/vaporwave.css +1 -1
- package/src/ts/extensions.ts +3 -36
- package/src/ts/perspective-viewer.ts +1 -1
- package/src/ts/plugin.ts +4 -1
- package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
- package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
- package/src/ts/ts-rs/ExportMethod.ts +1 -1
- package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
- package/src/ts/ts-rs/RestoreOptions.ts +3 -7
- package/src/ts/ts-rs/TableRef.ts +9 -0
- package/src/ts/ts-rs/ViewerConfig.ts +7 -6
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
- package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
- package/dist/esm/perspective-viewer.inline.d.ts +0 -3
- package/dist/esm/perspective-viewer.inline.js +0 -21
- package/dist/esm/perspective-viewer.inline.js.map +0 -7
- package/src/rust/tasks/restore_and_render.rs +0 -107
- package/src/rust/utils/browser/tests/debounce.rs +0 -163
- package/src/rust/utils/tests/pubsub.rs +0 -95
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
|
|
13
|
+
use perspective_client::config::ViewConfig;
|
|
14
|
+
use perspective_js::utils::ApiFuture;
|
|
15
|
+
use yew::prelude::*;
|
|
16
|
+
|
|
17
|
+
// use crate::derived::suggest_name;
|
|
18
|
+
use crate::presentation::Presentation;
|
|
19
|
+
// use crate::queries::fetch_hosted_tables;
|
|
20
|
+
use crate::renderer::Renderer;
|
|
21
|
+
use crate::session::{Session, SessionMetadataRc};
|
|
22
|
+
use crate::tasks::release_derived;
|
|
23
|
+
// use crate::tasks::{can_derive, create_derived, release_derived};
|
|
24
|
+
use crate::utils::{AddListener, PtrEqRc, Subscription, left_click, read_intl_string};
|
|
25
|
+
use crate::workspace::Workspace;
|
|
26
|
+
|
|
27
|
+
#[derive(Clone, Properties)]
|
|
28
|
+
pub struct DerivedControlProps {
|
|
29
|
+
pub session: Session,
|
|
30
|
+
pub renderer: Renderer,
|
|
31
|
+
pub presentation: Presentation,
|
|
32
|
+
pub workspace: Workspace,
|
|
33
|
+
pub metadata: SessionMetadataRc,
|
|
34
|
+
pub view_config: PtrEqRc<ViewConfig>,
|
|
35
|
+
|
|
36
|
+
/// The `Table` this panel is bound to, threaded as a value prop so the
|
|
37
|
+
/// chain re-renders when the binding changes.
|
|
38
|
+
pub table: Option<String>,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl PartialEq for DerivedControlProps {
|
|
42
|
+
fn eq(&self, other: &Self) -> bool {
|
|
43
|
+
self.table == other.table
|
|
44
|
+
&& self.metadata == other.metadata
|
|
45
|
+
&& self.view_config == other.view_config
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub enum DerivedControlMsg {
|
|
50
|
+
// Create,
|
|
51
|
+
Release(bool),
|
|
52
|
+
|
|
53
|
+
/// Some panel's table binding changed — re-render so the consumer
|
|
54
|
+
/// count ([`Workspace::derived_consumers`]) gating refreshes.
|
|
55
|
+
Refresh,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub struct DerivedControl {
|
|
59
|
+
/// Keeps the [`Workspace::bindings_changed`] listener attached for this
|
|
60
|
+
/// component's lifetime.
|
|
61
|
+
_bindings_sub: Subscription,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl Component for DerivedControl {
|
|
65
|
+
type Message = DerivedControlMsg;
|
|
66
|
+
type Properties = DerivedControlProps;
|
|
67
|
+
|
|
68
|
+
fn create(ctx: &Context<Self>) -> Self {
|
|
69
|
+
let link = ctx.link().clone();
|
|
70
|
+
let _bindings_sub = ctx
|
|
71
|
+
.props()
|
|
72
|
+
.workspace
|
|
73
|
+
.bindings_changed()
|
|
74
|
+
.add_listener(move |_| link.send_message(DerivedControlMsg::Refresh));
|
|
75
|
+
|
|
76
|
+
Self { _bindings_sub }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
80
|
+
match msg {
|
|
81
|
+
// DerivedControlMsg::Create => {
|
|
82
|
+
// let props = ctx.props().clone();
|
|
83
|
+
// ApiFuture::spawn(async move {
|
|
84
|
+
// let name = suggest_name(
|
|
85
|
+
// &fetch_hosted_tables(&props.workspace)
|
|
86
|
+
// .await
|
|
87
|
+
// .into_iter()
|
|
88
|
+
// .flat_map(|(_, tables)| tables)
|
|
89
|
+
// .collect(),
|
|
90
|
+
// &props.view_config,
|
|
91
|
+
// &props.table,
|
|
92
|
+
// );
|
|
93
|
+
|
|
94
|
+
// create_derived(
|
|
95
|
+
// &props.session,
|
|
96
|
+
// &props.renderer,
|
|
97
|
+
// &props.presentation,
|
|
98
|
+
// &props.workspace,
|
|
99
|
+
// name,
|
|
100
|
+
// )
|
|
101
|
+
// .await
|
|
102
|
+
// });
|
|
103
|
+
|
|
104
|
+
// false
|
|
105
|
+
// },
|
|
106
|
+
DerivedControlMsg::Release(delete) => {
|
|
107
|
+
let props = ctx.props().clone();
|
|
108
|
+
let effect = props.workspace.effects().guard();
|
|
109
|
+
ApiFuture::spawn(async move {
|
|
110
|
+
let _effect = effect;
|
|
111
|
+
release_derived(
|
|
112
|
+
&props.session,
|
|
113
|
+
&props.renderer,
|
|
114
|
+
&props.presentation,
|
|
115
|
+
&props.workspace,
|
|
116
|
+
delete,
|
|
117
|
+
)
|
|
118
|
+
.await
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
false
|
|
122
|
+
},
|
|
123
|
+
DerivedControlMsg::Refresh => true,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
128
|
+
let props = ctx.props();
|
|
129
|
+
let workspace = &props.workspace;
|
|
130
|
+
let bound = props.table.clone().unwrap_or_default();
|
|
131
|
+
let is_derived = workspace.is_derived(&bound);
|
|
132
|
+
// let is_pivoted =
|
|
133
|
+
// !props.view_config.group_by.is_empty() ||
|
|
134
|
+
// !props.view_config.split_by.is_empty();
|
|
135
|
+
|
|
136
|
+
// let _can_create = is_pivoted && can_derive(&props.session);
|
|
137
|
+
// if !is_derived && !can_create {
|
|
138
|
+
// return html! {};
|
|
139
|
+
// }
|
|
140
|
+
|
|
141
|
+
if !is_derived {
|
|
142
|
+
return html! {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let is_snapshot = !props.metadata.get_features().is_some_and(|x| x.on_update);
|
|
146
|
+
let mut chain = workspace.derived_ancestors(&bound);
|
|
147
|
+
chain.reverse();
|
|
148
|
+
if is_derived {
|
|
149
|
+
chain.push(bound.clone());
|
|
150
|
+
chain.remove(0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let sole_consumer = workspace.derived_consumers(&bound) == 1;
|
|
154
|
+
let release = left_click(
|
|
155
|
+
ctx.link()
|
|
156
|
+
.callback(|event: MouseEvent| DerivedControlMsg::Release(event.shift_key())),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// let create = ctx.link().callback(|_| DerivedControlMsg::Create);
|
|
160
|
+
// let last = chain.len().saturating_sub(1);
|
|
161
|
+
let viewer = props.presentation.viewer_elem();
|
|
162
|
+
let intl = |slug: &str, fallback: &str| {
|
|
163
|
+
read_intl_string(viewer, slug).unwrap_or_else(|| fallback.to_owned())
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// let _create_title = intl("derive-table-title", "Derive a new table
|
|
167
|
+
// from this view");
|
|
168
|
+
let snapshot_title = intl(
|
|
169
|
+
"derived-snapshot-title",
|
|
170
|
+
"This data source does not stream - this table's data is fixed at the point it was \
|
|
171
|
+
derived.",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
let release_title = if sole_consumer {
|
|
175
|
+
intl(
|
|
176
|
+
"derived-release-delete-title",
|
|
177
|
+
"Back to the parent table - shift-click to delete this one",
|
|
178
|
+
)
|
|
179
|
+
} else {
|
|
180
|
+
intl("derived-release-title", "Back to the parent table")
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
let last = chain.len().saturating_sub(1);
|
|
184
|
+
let derived_tables = chain.iter().enumerate().map(|(idx, name)| {
|
|
185
|
+
let is_own = is_derived && idx == last;
|
|
186
|
+
// let consumers = workspace.derived_consumers(name);
|
|
187
|
+
let mut close_class = classes!("derived_release");
|
|
188
|
+
if sole_consumer {
|
|
189
|
+
close_class.push("shift-alt-icon");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let mut level_class = classes!("derived_level");
|
|
193
|
+
if is_own {
|
|
194
|
+
level_class.push("is-own");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
html! {
|
|
198
|
+
<div class={level_class}>
|
|
199
|
+
<div class="derived_label_wrapper">
|
|
200
|
+
<span class="derived_icon" />
|
|
201
|
+
<span class="derived_label">{ name }</span>
|
|
202
|
+
</div>
|
|
203
|
+
// if consumers > 1 { <span class="derived_consumers">{ consumers }</span> }
|
|
204
|
+
if is_snapshot {
|
|
205
|
+
<span class="derived_snapshot" title={snapshot_title.clone()} />
|
|
206
|
+
}
|
|
207
|
+
if is_own {
|
|
208
|
+
<span
|
|
209
|
+
class={close_class}
|
|
210
|
+
title={release_title.clone()}
|
|
211
|
+
onmousedown={release.clone()}
|
|
212
|
+
/>
|
|
213
|
+
} else {
|
|
214
|
+
<span class="derived_release disabled" />
|
|
215
|
+
}
|
|
216
|
+
</div>
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
html! {
|
|
221
|
+
<div id="derived_stack">
|
|
222
|
+
// if can_create {
|
|
223
|
+
// <div class="derived_create" title={create_title} onmousedown={create} />
|
|
224
|
+
// }
|
|
225
|
+
{ for derived_tables }
|
|
226
|
+
</div>
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
use yew::prelude::*;
|
|
14
14
|
|
|
15
15
|
use super::ColumnSettingsTarget;
|
|
16
|
+
use crate::utils::left_click;
|
|
16
17
|
|
|
17
18
|
#[derive(PartialEq, Clone, Properties)]
|
|
18
19
|
pub struct ExprEditButtonProps {
|
|
@@ -41,10 +42,10 @@ pub struct ExprEditButtonProps {
|
|
|
41
42
|
/// when pressed, it opens up the expression editor side-panel.
|
|
42
43
|
#[function_component]
|
|
43
44
|
pub fn ExprEditButton(p: &ExprEditButtonProps) -> Html {
|
|
44
|
-
let onmousedown = yew::use_callback(p.clone(), |_, p| {
|
|
45
|
+
let onmousedown = left_click(yew::use_callback(p.clone(), |_, p| {
|
|
45
46
|
p.on_open_expr_panel
|
|
46
47
|
.emit(ColumnSettingsTarget::Column(p.name.clone()))
|
|
47
|
-
});
|
|
48
|
+
}));
|
|
48
49
|
|
|
49
50
|
let class = if p.is_disabled {
|
|
50
51
|
"expression-edit-button disabled"
|
|
@@ -410,8 +410,6 @@ impl FilterColumnProps {
|
|
|
410
410
|
self.metadata.get_column_table_type(filter.column())
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
// Get the string value, suitable for the `value` field of a `FilterColumns`'s
|
|
414
|
-
// `<input>`.
|
|
415
413
|
fn get_filter_input(&self) -> Option<String> {
|
|
416
414
|
let filter_type = self.get_current_filter_type()?;
|
|
417
415
|
match (&filter_type, &self.filter.term()) {
|
|
@@ -132,9 +132,10 @@ impl Component for InactiveColumn {
|
|
|
132
132
|
.get_column_table_type(&ctx.props().name)
|
|
133
133
|
.unwrap_or(ColumnType::String);
|
|
134
134
|
|
|
135
|
-
let add_column =
|
|
136
|
-
.link()
|
|
137
|
-
|
|
135
|
+
let add_column =
|
|
136
|
+
left_click(ctx.link().callback(|event: MouseEvent| {
|
|
137
|
+
InactiveColumnMsg::ActivateColumn(event.shift_key())
|
|
138
|
+
}));
|
|
138
139
|
|
|
139
140
|
let ondragend = ctx.props().ondragend.reform(|_| {});
|
|
140
141
|
let ondragstart = ctx.link().callback({
|
|
@@ -210,8 +211,6 @@ impl InactiveColumnProps {
|
|
|
210
211
|
let mut columns = self.view_config.columns.clone();
|
|
211
212
|
let max_cols = self.renderer.metadata().config_column_names.len();
|
|
212
213
|
|
|
213
|
-
// Don't treat `None` at the end of the column list as columns, we'll refill
|
|
214
|
-
// these later
|
|
215
214
|
if let Some(last_filled) = columns.iter().rposition(|x| !x.is_none()) {
|
|
216
215
|
columns.truncate(last_filled + 1);
|
|
217
216
|
|
|
@@ -14,9 +14,10 @@ use perspective_client::config::ColumnType;
|
|
|
14
14
|
use web_sys::*;
|
|
15
15
|
use yew::prelude::*;
|
|
16
16
|
|
|
17
|
+
use super::expr_edit_button::*;
|
|
17
18
|
use crate::components::dragdrop_list::*;
|
|
18
19
|
use crate::components::type_icon::TypeIcon;
|
|
19
|
-
use crate::presentation::Presentation;
|
|
20
|
+
use crate::presentation::{ColumnSettingsTarget, Presentation};
|
|
20
21
|
use crate::session::*;
|
|
21
22
|
use crate::utils::*;
|
|
22
23
|
|
|
@@ -35,6 +36,14 @@ pub struct PivotColumnProps {
|
|
|
35
36
|
#[prop_or_default]
|
|
36
37
|
pub metadata: Option<SessionMetadataRc>,
|
|
37
38
|
|
|
39
|
+
/// Opens this column's settings, when the host offers them.
|
|
40
|
+
#[prop_or_default]
|
|
41
|
+
pub on_open_expr_panel: Option<Callback<ColumnSettingsTarget>>,
|
|
42
|
+
|
|
43
|
+
/// Is this column's settings panel open?
|
|
44
|
+
#[prop_or_default]
|
|
45
|
+
pub is_editing: bool,
|
|
46
|
+
|
|
38
47
|
// State
|
|
39
48
|
#[prop_or_default]
|
|
40
49
|
pub opt_session: Option<Session>,
|
|
@@ -46,6 +55,8 @@ impl PartialEq for PivotColumnProps {
|
|
|
46
55
|
self.column == other.column
|
|
47
56
|
&& self.action == other.action
|
|
48
57
|
&& self.metadata == other.metadata
|
|
58
|
+
&& self.is_editing == other.is_editing
|
|
59
|
+
&& self.on_open_expr_panel.is_some() == other.on_open_expr_panel.is_some()
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -103,6 +114,15 @@ impl Component for PivotColumn {
|
|
|
103
114
|
<span class="drag-handle icon" />
|
|
104
115
|
<TypeIcon ty={col_type} />
|
|
105
116
|
<span class="column_name">{ ctx.props().column.clone() }</span>
|
|
117
|
+
if let Some(on_open_expr_panel) = &ctx.props().on_open_expr_panel {
|
|
118
|
+
<ExprEditButton
|
|
119
|
+
name={ctx.props().column.clone()}
|
|
120
|
+
{on_open_expr_panel}
|
|
121
|
+
is_expression=false
|
|
122
|
+
is_disabled=false
|
|
123
|
+
is_editing={ctx.props().is_editing}
|
|
124
|
+
/>
|
|
125
|
+
}
|
|
106
126
|
</div>
|
|
107
127
|
</div>
|
|
108
128
|
}
|
|
@@ -96,9 +96,10 @@ impl Component for SortColumn {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
99
|
-
let onclick =
|
|
100
|
-
.link()
|
|
101
|
-
|
|
99
|
+
let onclick = left_click(
|
|
100
|
+
ctx.link()
|
|
101
|
+
.callback(|event: MouseEvent| SortColumnMsg::SortDirClick(event.shift_key())),
|
|
102
|
+
);
|
|
102
103
|
|
|
103
104
|
let dragstart = Callback::from({
|
|
104
105
|
let event_name = ctx.props().sort.0.to_owned();
|
|
@@ -15,6 +15,7 @@ mod add_expression_button;
|
|
|
15
15
|
mod aggregate_selector;
|
|
16
16
|
mod column_selector_column_row;
|
|
17
17
|
mod config_selector;
|
|
18
|
+
mod derived_control;
|
|
18
19
|
mod empty_column;
|
|
19
20
|
mod expr_edit_button;
|
|
20
21
|
mod filter_column;
|
|
@@ -37,6 +38,7 @@ use yew::prelude::*;
|
|
|
37
38
|
use self::active_column::*;
|
|
38
39
|
use self::add_expression_button::AddExpressionButton;
|
|
39
40
|
use self::config_selector::ConfigSelector;
|
|
41
|
+
use self::derived_control::DerivedControl;
|
|
40
42
|
use self::inactive_column::*;
|
|
41
43
|
use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
|
|
42
44
|
use crate::config::PluginStaticConfig;
|
|
@@ -50,6 +52,7 @@ use crate::session::*;
|
|
|
50
52
|
use crate::tasks::apply_and_render;
|
|
51
53
|
use crate::ui::{Orientation, ScrollPanel, ScrollPanelItem, SplitPanel};
|
|
52
54
|
use crate::utils::*;
|
|
55
|
+
use crate::workspace::Workspace;
|
|
53
56
|
|
|
54
57
|
#[derive(Properties)]
|
|
55
58
|
pub struct ColumnSelectorProps {
|
|
@@ -83,6 +86,7 @@ pub struct ColumnSelectorProps {
|
|
|
83
86
|
// State
|
|
84
87
|
pub session: Session,
|
|
85
88
|
pub renderer: Renderer,
|
|
89
|
+
pub workspace: Workspace,
|
|
86
90
|
pub presentation: Presentation,
|
|
87
91
|
|
|
88
92
|
/// Fires when this component is resized via the UI.
|
|
@@ -391,10 +395,26 @@ impl Component for ColumnSelector {
|
|
|
391
395
|
.unwrap_or_default(),
|
|
392
396
|
);
|
|
393
397
|
|
|
398
|
+
let derived_control = html_nested! {
|
|
399
|
+
<ScrollPanelItem key="derived_control" size_hint=0_f64>
|
|
400
|
+
<DerivedControl
|
|
401
|
+
session={ctx.props().session.clone()}
|
|
402
|
+
renderer={ctx.props().renderer.clone()}
|
|
403
|
+
presentation={ctx.props().presentation.clone()}
|
|
404
|
+
workspace={ctx.props().workspace.clone()}
|
|
405
|
+
metadata={ctx.props().metadata.clone()}
|
|
406
|
+
view_config={ctx.props().view_config.clone()}
|
|
407
|
+
table={ctx.props().session.get_table().map(|x| x.get_name().to_owned())}
|
|
408
|
+
/>
|
|
409
|
+
</ScrollPanelItem>
|
|
410
|
+
};
|
|
411
|
+
|
|
394
412
|
let config_selector = html_nested! {
|
|
395
413
|
<ScrollPanelItem key="config_selector" {size_hint}>
|
|
396
414
|
<ConfigSelector
|
|
397
415
|
onselect={onselect.clone()}
|
|
416
|
+
on_open_expr_panel={ctx.props().on_open_expr_panel.clone()}
|
|
417
|
+
selected_column={ctx.props().selected_column.clone()}
|
|
398
418
|
ondragenter={ctx.link().callback(|()| Redraw)}
|
|
399
419
|
view_config={ctx.props().view_config.clone()}
|
|
400
420
|
drag_column={ctx.props().drag_column.clone()}
|
|
@@ -570,7 +590,7 @@ impl Component for ColumnSelector {
|
|
|
570
590
|
drop={ondrop}
|
|
571
591
|
on_resize={&ctx.props().on_resize}
|
|
572
592
|
on_dimensions_reset={&self.on_reset}
|
|
573
|
-
children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
|
|
593
|
+
children={std::iter::once(derived_control).chain(std::iter::once(config_selector)).chain(active_columns).collect::<Vec<_>>()}
|
|
574
594
|
/>
|
|
575
595
|
</div>
|
|
576
596
|
}];
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
use yew::{Callback, Html, Properties, function_component, html};
|
|
14
14
|
|
|
15
|
+
use crate::utils::left_click;
|
|
16
|
+
|
|
15
17
|
#[derive(Properties, PartialEq, Clone)]
|
|
16
18
|
pub struct SaveSettingsProps {
|
|
17
19
|
pub save_enabled: bool,
|
|
@@ -26,9 +28,9 @@ pub struct SaveSettingsProps {
|
|
|
26
28
|
|
|
27
29
|
#[function_component(SaveSettings)]
|
|
28
30
|
pub fn save_settings(props: &SaveSettingsProps) -> Html {
|
|
29
|
-
let reset = props.on_reset.reform(|_| ());
|
|
30
|
-
let save = props.on_save.reform(|_| ());
|
|
31
|
-
let delete = props.on_delete.reform(|_| ());
|
|
31
|
+
let reset = left_click(props.on_reset.reform(|_| ()));
|
|
32
|
+
let save = left_click(props.on_save.reform(|_| ()));
|
|
33
|
+
let delete = left_click(props.on_delete.reform(|_| ()));
|
|
32
34
|
html! {
|
|
33
35
|
<div id="save-settings-wrapper">
|
|
34
36
|
if props.show_danger_zone {
|
|
@@ -34,6 +34,7 @@ use crate::components::expression_editor::ExpressionEditorProps;
|
|
|
34
34
|
use crate::components::type_icon::TypeIconType;
|
|
35
35
|
use crate::components::window_editor::WindowEditorProps;
|
|
36
36
|
use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
|
|
37
|
+
use crate::queries::{has_column_config, is_pivot_only_column};
|
|
37
38
|
use crate::renderer::Renderer;
|
|
38
39
|
use crate::session::{Session, SessionMetadataRc};
|
|
39
40
|
use crate::tasks::{
|
|
@@ -218,7 +219,9 @@ impl Component for ColumnSettingsPanel {
|
|
|
218
219
|
|
|
219
220
|
fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
|
|
220
221
|
let next = Initials::of(ctx.props());
|
|
221
|
-
if ctx.props().selected_column != old_props.selected_column
|
|
222
|
+
if ctx.props().selected_column != old_props.selected_column
|
|
223
|
+
|| next != Initials::of(old_props)
|
|
224
|
+
{
|
|
222
225
|
self.reset_to(ctx, next);
|
|
223
226
|
} else {
|
|
224
227
|
self.refresh_derived(ctx);
|
|
@@ -541,33 +544,36 @@ impl ColumnSettingsPanel {
|
|
|
541
544
|
self.save_enabled = changed && valid;
|
|
542
545
|
}
|
|
543
546
|
|
|
544
|
-
fn initials(&self) -> Initials {
|
|
545
|
-
Initials {
|
|
546
|
-
column_name: self.column_name.clone(),
|
|
547
|
-
expr: self.initial_expr_value.clone(),
|
|
548
|
-
header: self.initial_header_value.clone(),
|
|
549
|
-
window: self.initial_window_value.clone(),
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
547
|
fn refresh_derived(&mut self, ctx: &yew::prelude::Context<Self>) {
|
|
554
|
-
|
|
548
|
+
let view_ty = ctx
|
|
555
549
|
.props()
|
|
556
550
|
.metadata
|
|
557
551
|
.locator_view_type(&ctx.props().selected_column);
|
|
558
552
|
|
|
553
|
+
self.maybe_ty = match ctx.props().selected_column.name() {
|
|
554
|
+
Some(name) if is_pivot_only_column(name, &ctx.props().view_config) => {
|
|
555
|
+
ctx.props().metadata.get_column_table_type(name).or(view_ty)
|
|
556
|
+
},
|
|
557
|
+
_ => view_ty,
|
|
558
|
+
};
|
|
559
|
+
|
|
559
560
|
self.tabs = {
|
|
560
561
|
let mut tabs = vec![];
|
|
561
562
|
let is_new_expr = ctx.props().selected_column.is_new_expr();
|
|
563
|
+
let selected_name = ctx
|
|
564
|
+
.props()
|
|
565
|
+
.selected_column
|
|
566
|
+
.name()
|
|
567
|
+
.map(|x| x.to_string())
|
|
568
|
+
.unwrap_or_default();
|
|
569
|
+
|
|
562
570
|
let show_styles = !is_new_expr
|
|
563
|
-
&&
|
|
564
|
-
|
|
565
|
-
ctx.props()
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
.unwrap_or_default(),
|
|
570
|
-
));
|
|
571
|
+
&& has_column_config(
|
|
572
|
+
&selected_name,
|
|
573
|
+
&ctx.props().view_config,
|
|
574
|
+
&ctx.props().metadata,
|
|
575
|
+
&ctx.props().renderer,
|
|
576
|
+
);
|
|
571
577
|
|
|
572
578
|
if !is_new_expr && show_styles {
|
|
573
579
|
tabs.push(ColumnSettingsTab::Style);
|
|
@@ -22,13 +22,12 @@ use crate::components::code_editor::CodeEditor;
|
|
|
22
22
|
use crate::config::*;
|
|
23
23
|
use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard};
|
|
24
24
|
use crate::presentation::*;
|
|
25
|
-
use crate::queries::fetch_hosted_tables;
|
|
26
25
|
use crate::renderer::*;
|
|
27
26
|
use crate::session::*;
|
|
28
27
|
use crate::utils::*;
|
|
29
28
|
use crate::workspace::Workspace;
|
|
30
29
|
|
|
31
|
-
#[derive(Clone,
|
|
30
|
+
#[derive(Clone, Properties)]
|
|
32
31
|
pub struct DebugPanelProps {
|
|
33
32
|
pub presentation: Presentation,
|
|
34
33
|
pub renderer: Renderer,
|
|
@@ -47,9 +46,16 @@ pub struct DebugPanelProps {
|
|
|
47
46
|
pub on_auto_width: Callback<f64>,
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
impl PartialEq for DebugPanelProps {
|
|
50
|
+
fn eq(&self, rhs: &Self) -> bool {
|
|
51
|
+
self.initial_width == rhs.initial_width
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
/// Whether the editor holds text the panel has not successfully applied, read
|
|
51
|
-
/// synchronously by the config-change listeners
|
|
52
|
-
/// `
|
|
56
|
+
/// synchronously by the config-change listeners and again when a
|
|
57
|
+
/// [`DebugPanelProps::set_text`] read lands, so neither a failed apply's own
|
|
58
|
+
/// `view_config_changed` nor an in-flight config read discards it.
|
|
53
59
|
type Dirty = Rc<Cell<bool>>;
|
|
54
60
|
|
|
55
61
|
#[function_component(DebugPanel)]
|
|
@@ -72,10 +78,12 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
72
78
|
}
|
|
73
79
|
});
|
|
74
80
|
|
|
75
|
-
use_effect_with((
|
|
81
|
+
use_effect_with((), {
|
|
76
82
|
clone!(error, modified, dirty);
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
let state = props.clone();
|
|
84
|
+
let text = expr.setter();
|
|
85
|
+
move |()| {
|
|
86
|
+
state.set_text(text.clone(), dirty.clone());
|
|
79
87
|
error.set(None);
|
|
80
88
|
let sub1 = state
|
|
81
89
|
.renderer
|
|
@@ -154,7 +162,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
154
162
|
clone!(modified, dirty);
|
|
155
163
|
move |_, (text, error, props)| {
|
|
156
164
|
dirty.set(false);
|
|
157
|
-
props.set_text(text.clone());
|
|
165
|
+
props.set_text(text.clone(), dirty.clone());
|
|
158
166
|
error.set(None);
|
|
159
167
|
modified.set(false);
|
|
160
168
|
}
|
|
@@ -214,7 +222,9 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
impl DebugPanelProps {
|
|
217
|
-
|
|
225
|
+
/// Asynchronously read this panel's config into the editor, unless the
|
|
226
|
+
/// editor has unapplied text by the time the read lands.
|
|
227
|
+
fn set_text(&self, setter: UseStateSetter<Rc<String>>, dirty: Dirty) {
|
|
218
228
|
let props = self.clone();
|
|
219
229
|
ApiFuture::spawn(async move {
|
|
220
230
|
let config = crate::queries::get_viewer_config(
|
|
@@ -222,12 +232,16 @@ impl DebugPanelProps {
|
|
|
222
232
|
&props.renderer,
|
|
223
233
|
&props.presentation,
|
|
224
234
|
)
|
|
225
|
-
.await
|
|
235
|
+
.await?
|
|
236
|
+
.map_table(|name| props.workspace.inline_table_ref(&name));
|
|
226
237
|
let json = JsValue::from_serde_ext(&config)?;
|
|
227
238
|
let js_string =
|
|
228
239
|
js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?;
|
|
229
240
|
|
|
230
|
-
|
|
241
|
+
if !dirty.get() {
|
|
242
|
+
setter.set(Rc::new(js_string.as_string().unwrap()));
|
|
243
|
+
}
|
|
244
|
+
|
|
231
245
|
Ok(())
|
|
232
246
|
});
|
|
233
247
|
}
|
|
@@ -246,7 +260,7 @@ impl DebugPanelProps {
|
|
|
246
260
|
}
|
|
247
261
|
|
|
248
262
|
error.set(None);
|
|
249
|
-
props.set_text(text.clone());
|
|
263
|
+
props.set_text(text.clone(), dirty.clone());
|
|
250
264
|
modified.set(false);
|
|
251
265
|
}
|
|
252
266
|
}
|
|
@@ -283,21 +297,6 @@ impl DebugPanelProps {
|
|
|
283
297
|
},
|
|
284
298
|
};
|
|
285
299
|
|
|
286
|
-
if let OptionalUpdate::Update(name) = &config.table {
|
|
287
|
-
let hosted = fetch_hosted_tables(&props.workspace).await;
|
|
288
|
-
if !hosted
|
|
289
|
-
.iter()
|
|
290
|
-
.any(|(_, names)| names.iter().any(|n| n == name))
|
|
291
|
-
{
|
|
292
|
-
fail(
|
|
293
|
-
format!("Unknown table \"{name}\""),
|
|
294
|
-
locate_key(&source, "table"),
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
return Ok(());
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
300
|
let active = props.workspace.active_renderer().as_ref() == Some(&props.renderer);
|
|
302
301
|
let result = crate::tasks::restore_panel(
|
|
303
302
|
&props.session,
|
|
@@ -308,6 +307,7 @@ impl DebugPanelProps {
|
|
|
308
307
|
config,
|
|
309
308
|
crate::tasks::RestoreErrors::Suppress,
|
|
310
309
|
MissingTable::Error,
|
|
310
|
+
None,
|
|
311
311
|
)
|
|
312
312
|
.await;
|
|
313
313
|
|
|
@@ -316,9 +316,18 @@ impl DebugPanelProps {
|
|
|
316
316
|
dirty.set(false);
|
|
317
317
|
error.set(None);
|
|
318
318
|
modified.set(false);
|
|
319
|
-
props.set_text(text);
|
|
319
|
+
props.set_text(text, dirty.clone());
|
|
320
|
+
},
|
|
321
|
+
Err(e) => {
|
|
322
|
+
let message = format!("{e}");
|
|
323
|
+
let position = if message.starts_with("Unknown table") {
|
|
324
|
+
locate_key(&source, "table")
|
|
325
|
+
} else {
|
|
326
|
+
(0, 0)
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
fail(message, position)
|
|
320
330
|
},
|
|
321
|
-
Err(e) => fail(format!("{e}"), (0, 0)),
|
|
322
331
|
}
|
|
323
332
|
|
|
324
333
|
Ok(())
|