@perspective-dev/viewer 5.0.0 → 5.2.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/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.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/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/monokai.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/docs/perspective-docs.json +1 -0
- package/dist/esm/column-format.d.ts +19 -28
- package/dist/esm/perspective-viewer.d.ts +2 -0
- package/dist/esm/perspective-viewer.inline.js +8 -8
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/providers.d.ts +79 -0
- package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
- package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
- package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
- package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
- package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
- package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
- package/dist/esm/ts-rs/Notation.d.ts +17 -0
- package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
- package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
- package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
- package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
- package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
- package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
- package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/Unit.d.ts +1 -0
- package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
- package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
- package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
- package/dist/wasm/perspective-viewer.d.ts +222 -26
- package/dist/wasm/perspective-viewer.js +378 -125
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
- package/package.json +2 -1
- package/src/css/_viewer_bundle.css +1 -0
- package/src/css/column-selector.css +9 -0
- package/src/css/column-settings-panel.css +67 -2
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +60 -4
- package/src/css/containers/context-menu.css +12 -6
- package/src/css/containers/tabs.css +2 -2
- package/src/css/form/code-editor.css +2 -3
- package/src/css/form/mirrored-textarea.css +81 -0
- package/src/css/viewer.css +336 -0
- package/src/rust/agent/client/mod.rs +19 -0
- package/src/rust/agent/client/protocol.rs +320 -0
- package/src/rust/agent/client/transport.rs +287 -0
- package/src/rust/agent/client/turn.rs +174 -0
- package/src/rust/agent/config.rs +159 -0
- package/src/rust/agent/docs.rs +293 -0
- package/src/rust/agent/markdown/block.rs +509 -0
- package/src/rust/agent/markdown/entity.rs +52 -0
- package/src/rust/agent/markdown/events.rs +71 -0
- package/src/rust/agent/markdown/inline.rs +497 -0
- package/src/rust/agent/markdown/mod.rs +466 -0
- package/src/rust/agent/mod.rs +23 -0
- package/src/rust/agent/preamble.md +16 -0
- package/src/rust/agent/runtime.rs +201 -0
- package/src/rust/agent/slot.rs +236 -0
- package/src/rust/agent/tools.rs +1110 -0
- package/src/rust/components/chat_panel.rs +289 -0
- package/src/rust/components/column_selector/active_column.rs +21 -27
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +83 -8
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/inactive_column.rs +8 -1
- package/src/rust/components/column_selector.rs +27 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +110 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +6 -0
- package/src/rust/components/editable_header.rs +2 -0
- package/src/rust/components/form/code_editor.rs +14 -23
- package/src/rust/components/form/debug.rs +8 -5
- package/src/rust/components/form/mirrored_textarea.rs +165 -0
- package/src/rust/components/form/mod.rs +1 -0
- package/src/rust/components/main_panel.rs +5 -0
- package/src/rust/components/mod.rs +3 -0
- package/src/rust/components/panel_menu.rs +7 -0
- package/src/rust/components/panel_tab.rs +5 -0
- package/src/rust/components/portal.rs +7 -0
- package/src/rust/components/settings_panel.rs +80 -2
- package/src/rust/components/viewer/msg.rs +2 -0
- package/src/rust/components/viewer/panels.rs +9 -14
- package/src/rust/components/viewer/render.rs +14 -0
- package/src/rust/components/viewer/settings.rs +33 -1
- package/src/rust/components/viewer.rs +2 -0
- package/src/rust/components/window_editor.rs +985 -0
- package/src/rust/config/datetime_column_style/custom.rs +17 -0
- package/src/rust/config/datetime_column_style/simple.rs +10 -0
- package/src/rust/config/number_string_format.rs +33 -0
- package/src/rust/config/options.rs +18 -0
- package/src/rust/config/plugin_static_config.rs +120 -5
- package/src/rust/config/viewer_config.rs +220 -1
- package/src/rust/config/workspace_config.rs +7 -6
- package/src/rust/custom_elements/viewer.rs +209 -18
- package/src/rust/custom_events.rs +29 -0
- package/src/rust/lib.rs +17 -1
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/presentation.rs +8 -0
- package/src/rust/queries/columns_iter_set.rs +7 -1
- package/src/rust/queries/get_viewer_config.rs +5 -1
- package/src/rust/renderer/dispatch.rs +85 -58
- package/src/rust/renderer.rs +34 -1
- package/src/rust/session/column_defaults_update.rs +18 -0
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +64 -9
- package/src/rust/session/replace_expression_update.rs +2 -0
- package/src/rust/session.rs +7 -5
- package/src/rust/tasks/create_panel.rs +18 -8
- package/src/rust/tasks/edit_window.rs +155 -0
- package/src/rust/tasks/mod.rs +2 -0
- package/src/rust/tasks/restore_panel.rs +34 -17
- package/src/rust/tasks/sync_update_panels.rs +7 -23
- package/src/rust/utils/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/debounce.rs +76 -27
- package/src/rust/workspace.rs +112 -20
- package/src/svg/pin-icon.svg +5 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/de.css +23 -0
- package/src/themes/intl/es.css +23 -0
- package/src/themes/intl/fr.css +23 -0
- package/src/themes/intl/ja.css +23 -0
- package/src/themes/intl/pt.css +23 -0
- package/src/themes/intl/zh.css +23 -0
- package/src/themes/intl.css +26 -1
- package/src/ts/column-format.ts +67 -46
- package/src/ts/perspective-viewer.ts +2 -0
- package/src/ts/providers.ts +96 -0
- package/src/ts/ts-rs/CompactDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyCode.ts +3 -0
- package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
- package/src/ts/ts-rs/CurrencySign.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
- package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
- package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
- package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
- package/src/ts/ts-rs/FormatMode.ts +3 -0
- package/src/ts/ts-rs/FormatUnit.ts +3 -0
- package/src/ts/ts-rs/Notation.ts +11 -0
- package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
- package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
- package/src/ts/ts-rs/RestoreOptions.ts +20 -0
- package/src/ts/ts-rs/RoundingMode.ts +3 -0
- package/src/ts/ts-rs/RoundingPriority.ts +3 -0
- package/src/ts/ts-rs/SignDisplay.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
- package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
- package/src/ts/ts-rs/StringColorMode.ts +3 -0
- package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
- package/src/ts/ts-rs/Unit.ts +3 -0
- package/src/ts/ts-rs/UnitDisplay.ts +3 -0
- package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
- package/src/ts/ts-rs/UseGrouping.ts +3 -0
- package/src/ts/ts-rs/ViewerConfig.ts +48 -1
- package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +47 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
|
@@ -10,15 +10,12 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
-
use futures::channel::oneshot::channel;
|
|
14
13
|
use perspective_client::clone;
|
|
15
14
|
use perspective_client::utils::PerspectiveResultExt;
|
|
16
15
|
|
|
17
|
-
use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg};
|
|
18
16
|
use crate::config::*;
|
|
19
17
|
use crate::presentation::Presentation;
|
|
20
18
|
use crate::renderer::Renderer;
|
|
21
|
-
use crate::root::Root;
|
|
22
19
|
use crate::session::{ResetOptions, Session};
|
|
23
20
|
use crate::tasks::*;
|
|
24
21
|
use crate::workspace::Workspace;
|
|
@@ -31,6 +28,20 @@ pub(crate) enum RestoreMode {
|
|
|
31
28
|
Fresh,
|
|
32
29
|
}
|
|
33
30
|
|
|
31
|
+
/// Where a failed restore's error goes. `Publish` (every user-facing path)
|
|
32
|
+
/// commits it to the session — the visible error UI and errored session
|
|
33
|
+
/// state. `Suppress` (the agent's `set_view_config` tool) returns it ONLY
|
|
34
|
+
/// to the caller: the model receives the error as a tool result and
|
|
35
|
+
/// self-corrects, so the transient failure is noise to the human user —
|
|
36
|
+
/// and the errored session state would otherwise BLOCK the corrected
|
|
37
|
+
/// retry, since error recovery below requires a `table` field the model's
|
|
38
|
+
/// patches don't carry.
|
|
39
|
+
#[derive(Clone, Copy)]
|
|
40
|
+
pub(crate) enum RestoreErrors {
|
|
41
|
+
Publish,
|
|
42
|
+
Suppress,
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
pub(crate) async fn bind_table_task(
|
|
35
46
|
session: &Session,
|
|
36
47
|
workspace: &Workspace,
|
|
@@ -57,9 +68,9 @@ pub(crate) async fn restore_panel(
|
|
|
57
68
|
renderer: &Renderer,
|
|
58
69
|
presentation: &Presentation,
|
|
59
70
|
workspace: &Workspace,
|
|
60
|
-
root: Option<&Root<PerspectiveViewer>>,
|
|
61
71
|
mode: RestoreMode,
|
|
62
72
|
mut update: ViewerConfigUpdate,
|
|
73
|
+
errors: RestoreErrors,
|
|
63
74
|
) -> ApiResult<()> {
|
|
64
75
|
let active = matches!(mode, RestoreMode::Existing { active: true });
|
|
65
76
|
let fresh = matches!(mode, RestoreMode::Fresh);
|
|
@@ -77,17 +88,17 @@ pub(crate) async fn restore_panel(
|
|
|
77
88
|
tracing::info!("Restoring {update}");
|
|
78
89
|
}
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
// NOTE: `update.settings` is deliberately NOT applied here. It is
|
|
92
|
+
// element-level chrome rather than panel state, so `restore()` — the
|
|
93
|
+
// only caller that can carry it — applies it before dispatching, and
|
|
94
|
+
// this pipeline stays per-panel. Applying it here reached only the
|
|
95
|
+
// `Existing { active: true }` mode, which is why a freshly created
|
|
96
|
+
// panel silently ignored it.
|
|
97
|
+
|
|
98
|
+
// Under `Suppress` the restore is TRANSACTIONAL, so snapshot the config
|
|
99
|
+
// it is about to overwrite — see the failure tail below for why.
|
|
100
|
+
let rollback =
|
|
101
|
+
matches!(errors, RestoreErrors::Suppress).then(|| session.get_view_config().clone());
|
|
91
102
|
|
|
92
103
|
let table_changed = !fresh
|
|
93
104
|
&& matches!(&update.table, OptionalUpdate::Update(name)
|
|
@@ -122,7 +133,6 @@ pub(crate) async fn restore_panel(
|
|
|
122
133
|
bind_table_task(&session, &workspace, name).await?;
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
receiver.await.unwrap_or_log();
|
|
126
136
|
Ok(())
|
|
127
137
|
}
|
|
128
138
|
},
|
|
@@ -130,7 +140,14 @@ pub(crate) async fn restore_panel(
|
|
|
130
140
|
.await;
|
|
131
141
|
|
|
132
142
|
if let Err(e) = &result {
|
|
133
|
-
|
|
143
|
+
match errors {
|
|
144
|
+
RestoreErrors::Publish => session.set_error(false, e.clone()).await?,
|
|
145
|
+
RestoreErrors::Suppress => {
|
|
146
|
+
if let Some(config) = rollback {
|
|
147
|
+
session.commit_view_config(config.into()).unwrap_or_log();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
}
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
result?;
|
|
@@ -55,20 +55,19 @@ pub fn sync_update_panels(
|
|
|
55
55
|
let mut fallback_fresh: Option<String> = None;
|
|
56
56
|
let mut active_fresh: Option<String> = None;
|
|
57
57
|
let mut contents = Vec::new();
|
|
58
|
+
// `panels` entries are `ViewerConfigInitial`s — `table` required by
|
|
59
|
+
// TYPE (a placed panel without a binding would be permanently blank),
|
|
60
|
+
// and no per-panel `settings` exists (element-level; carried by the
|
|
61
|
+
// top-level `active` field). An EMPTY `panels` map restores to the
|
|
62
|
+
// zero-panel empty stage — the former table-less fallback panel was
|
|
63
|
+
// exactly the blank-panel state this type exists to preclude.
|
|
58
64
|
for (saved_id, config) in panels {
|
|
59
|
-
if !matches!(config.settings, OptionalUpdate::Missing) {
|
|
60
|
-
#[rustfmt::skip]
|
|
61
|
-
tracing::warn!(
|
|
62
|
-
"`settings` on panel \"{saved_id}\" is ignored in a whole-element config; use the top-level `active` field"
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
65
|
let (fresh, session, renderer, config) = create_panel_model(
|
|
67
66
|
&this.elem,
|
|
68
67
|
&this.presentation,
|
|
69
68
|
&this.workspace,
|
|
70
69
|
None,
|
|
71
|
-
config,
|
|
70
|
+
config.into(),
|
|
72
71
|
None,
|
|
73
72
|
Placement::Placed,
|
|
74
73
|
);
|
|
@@ -82,21 +81,6 @@ pub fn sync_update_panels(
|
|
|
82
81
|
contents.push((fresh, session, renderer, config));
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
if contents.is_empty() {
|
|
86
|
-
let (fresh, session, renderer, config) = create_panel_model(
|
|
87
|
-
&this.elem,
|
|
88
|
-
&this.presentation,
|
|
89
|
-
&this.workspace,
|
|
90
|
-
None,
|
|
91
|
-
ViewerConfigUpdate::default(),
|
|
92
|
-
None,
|
|
93
|
-
Placement::Placed,
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
fallback_fresh = Some(fresh.as_str().to_owned());
|
|
97
|
-
contents.push((fresh, session, renderer, config));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
84
|
let mut eject_tasks = Vec::new();
|
|
101
85
|
if let Some(panel) = this.workspace.take_reserved() {
|
|
102
86
|
eject_tasks.push(eject_panel(panel));
|
|
@@ -17,6 +17,18 @@ pub enum DragTarget {
|
|
|
17
17
|
SplitBy,
|
|
18
18
|
Sort,
|
|
19
19
|
Filter,
|
|
20
|
+
WindowSource,
|
|
21
|
+
WindowOrderBy,
|
|
22
|
+
WindowPartitionBy,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl DragTarget {
|
|
26
|
+
pub fn is_staged(&self) -> bool {
|
|
27
|
+
matches!(
|
|
28
|
+
self,
|
|
29
|
+
Self::WindowSource | Self::WindowOrderBy | Self::WindowPartitionBy
|
|
30
|
+
)
|
|
31
|
+
}
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
@@ -86,6 +86,55 @@ pub async fn test_lock_seq() {
|
|
|
86
86
|
assert_eq!(cell.get(), 10);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/// A dedicated [`DebounceSlot`]'s task must RUN even when the default slot
|
|
90
|
+
/// has a parked runner — coalescing is scoped per slot. Pre-slot (one
|
|
91
|
+
/// shared parked flag), `t4` would resolve via `t2`'s settle without its
|
|
92
|
+
/// closure ever running: the resize-eaten-by-parked-update bug.
|
|
93
|
+
#[wasm_bindgen_test]
|
|
94
|
+
pub async fn test_slot_isolated_from_default_coalescing() {
|
|
95
|
+
let debounce_mutex = DebounceMutex::default();
|
|
96
|
+
let slot = debounce_mutex.slot();
|
|
97
|
+
let defaults: Rc<Cell<u32>> = Rc::new(Cell::new(0));
|
|
98
|
+
let slots: Rc<Cell<u32>> = Rc::new(Cell::new(0));
|
|
99
|
+
|
|
100
|
+
let t1 = debounce_mutex.lock(async {
|
|
101
|
+
set_timeout(10).await.unwrap();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let t2 = {
|
|
105
|
+
let defaults = defaults.clone();
|
|
106
|
+
debounce_mutex.debounce(async move {
|
|
107
|
+
defaults.set(defaults.get() + 1);
|
|
108
|
+
Ok(())
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
let t3 = {
|
|
113
|
+
let defaults = defaults.clone();
|
|
114
|
+
debounce_mutex.debounce(async move {
|
|
115
|
+
defaults.set(defaults.get() + 100);
|
|
116
|
+
Ok(())
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let t4 = {
|
|
121
|
+
let slots = slots.clone();
|
|
122
|
+
slot.debounce_with(|_| async move {
|
|
123
|
+
slots.set(slots.get() + 1);
|
|
124
|
+
Ok(())
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
let (_, r2, r3, r4) = futures::join!(t1, t2, t3, t4);
|
|
129
|
+
r2.unwrap();
|
|
130
|
+
r3.unwrap();
|
|
131
|
+
r4.unwrap();
|
|
132
|
+
|
|
133
|
+
// `t3` coalesced onto the parked `t2`; `t4` ran its own closure.
|
|
134
|
+
assert_eq!(defaults.get(), 1);
|
|
135
|
+
assert_eq!(slots.get(), 1);
|
|
136
|
+
}
|
|
137
|
+
|
|
89
138
|
#[wasm_bindgen_test]
|
|
90
139
|
pub async fn test_debounce_seq() {
|
|
91
140
|
let debounce_mutex = DebounceMutex::default();
|
|
@@ -33,16 +33,27 @@ pub struct RenderGuard {
|
|
|
33
33
|
_private: (),
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/// The per-slot debounce bookkeeping: one running + one parked evaluation,
|
|
37
|
+
/// with coalesced callers awaiting the parked runner's settle. Kept
|
|
38
|
+
/// per-[`DebounceSlot`] so coalescing is only expressible WITHIN a kind —
|
|
39
|
+
/// a caller can never be absorbed by a parked runner of a different kind
|
|
40
|
+
/// whose work does not subsume it (the resize-eaten-by-parked-update bug).
|
|
36
41
|
#[derive(Default)]
|
|
37
|
-
struct
|
|
42
|
+
struct SlotState {
|
|
38
43
|
id: Cell<u64>,
|
|
39
|
-
|
|
40
|
-
mutex: Mutex<u64>,
|
|
44
|
+
last: Cell<u64>,
|
|
41
45
|
parked: Cell<bool>,
|
|
42
46
|
settled_id: Cell<u64>,
|
|
43
47
|
on_settle: PubSub<()>,
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
#[derive(Default)]
|
|
51
|
+
struct DebounceMutexData {
|
|
52
|
+
held: Cell<bool>,
|
|
53
|
+
mutex: Mutex<()>,
|
|
54
|
+
default_slot: Rc<SlotState>,
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
/// Clears the `held` flag on drop, so cancellation of a locked task (its
|
|
47
58
|
/// future dropped mid-await) can't leave [`DebounceMutex::is_held`] stuck
|
|
48
59
|
/// `true`.
|
|
@@ -67,25 +78,25 @@ impl Drop for HeldFlag<'_> {
|
|
|
67
78
|
/// mid-run still frees the parked slot and releases its coalesced waiters,
|
|
68
79
|
/// so they can never be stranded awaiting a settle that no task will emit.
|
|
69
80
|
struct SettleGuard<'a> {
|
|
70
|
-
|
|
81
|
+
state: &'a SlotState,
|
|
71
82
|
next: u64,
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
impl<'a> SettleGuard<'a> {
|
|
75
|
-
fn park(
|
|
76
|
-
|
|
77
|
-
Self {
|
|
86
|
+
fn park(state: &'a SlotState, next: u64) -> Self {
|
|
87
|
+
state.parked.set(true);
|
|
88
|
+
Self { state, next }
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
impl Drop for SettleGuard<'_> {
|
|
82
93
|
fn drop(&mut self) {
|
|
83
|
-
self.
|
|
84
|
-
if self.
|
|
85
|
-
self.
|
|
94
|
+
self.state.parked.set(false);
|
|
95
|
+
if self.state.settled_id.get() < self.next {
|
|
96
|
+
self.state.settled_id.set(self.next);
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
self.
|
|
99
|
+
self.state.on_settle.emit(());
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
102
|
|
|
@@ -123,12 +134,11 @@ impl DebounceMutex {
|
|
|
123
134
|
F: FnOnce(RenderGuard) -> Fut,
|
|
124
135
|
Fut: Future<Output = T>,
|
|
125
136
|
{
|
|
126
|
-
let
|
|
127
|
-
let next = self.0.id.get();
|
|
137
|
+
let guard = self.0.mutex.lock().await;
|
|
128
138
|
let held = HeldFlag::set(&self.0.held);
|
|
129
139
|
let result = f(RenderGuard { _private: () }).await;
|
|
130
140
|
drop(held);
|
|
131
|
-
|
|
141
|
+
drop(guard);
|
|
132
142
|
result
|
|
133
143
|
}
|
|
134
144
|
|
|
@@ -144,23 +154,62 @@ impl DebounceMutex {
|
|
|
144
154
|
F: FnOnce(RenderGuard) -> Fut,
|
|
145
155
|
Fut: Future<Output = ApiResult<T>>,
|
|
146
156
|
{
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
DebounceSlot {
|
|
158
|
+
mutex: self.clone(),
|
|
159
|
+
state: self.0.default_slot.clone(),
|
|
160
|
+
}
|
|
161
|
+
.debounce_with(f)
|
|
162
|
+
.await
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// A NEW debounce slot over this mutex: its tasks serialize against
|
|
166
|
+
/// every other locked task, but coalesce only among themselves.
|
|
167
|
+
pub fn slot(&self) -> DebounceSlot {
|
|
168
|
+
DebounceSlot {
|
|
169
|
+
mutex: self.clone(),
|
|
170
|
+
state: Default::default(),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// One debounce KIND over a shared [`DebounceMutex`]. Execution is
|
|
176
|
+
/// exclusive across the whole mutex; coalescing (a caller resolving via a
|
|
177
|
+
/// parked runner instead of queueing) is scoped to the slot. A coalesced
|
|
178
|
+
/// caller's work must be subsumable by any same-slot runner that runs no
|
|
179
|
+
/// earlier than the call — which is why parameterized tasks sharing a slot
|
|
180
|
+
/// must read their parameters at RUN time (e.g. the [`Renderer`] geometry
|
|
181
|
+
/// command cell), never capture them at call time.
|
|
182
|
+
#[derive(Clone)]
|
|
183
|
+
pub struct DebounceSlot {
|
|
184
|
+
mutex: DebounceMutex,
|
|
185
|
+
state: Rc<SlotState>,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
impl DebounceSlot {
|
|
189
|
+
pub async fn debounce_with<T, F, Fut>(&self, f: F) -> ApiResult<T>
|
|
190
|
+
where
|
|
191
|
+
T: Default,
|
|
192
|
+
F: FnOnce(RenderGuard) -> Fut,
|
|
193
|
+
Fut: Future<Output = ApiResult<T>>,
|
|
194
|
+
{
|
|
195
|
+
let state = &self.state;
|
|
196
|
+
let next = state.id.get() + 1;
|
|
197
|
+
if state.parked.get() {
|
|
149
198
|
self.await_settled(next).await;
|
|
150
199
|
return Ok(T::default());
|
|
151
200
|
}
|
|
152
201
|
|
|
153
|
-
let settle = SettleGuard::park(
|
|
154
|
-
let
|
|
155
|
-
|
|
156
|
-
let result = if
|
|
157
|
-
let next =
|
|
158
|
-
|
|
159
|
-
let held = HeldFlag::set(&self.0.held);
|
|
202
|
+
let settle = SettleGuard::park(state, next);
|
|
203
|
+
let guard = self.mutex.0.mutex.lock().await;
|
|
204
|
+
state.parked.set(false);
|
|
205
|
+
let result = if state.last.get() < next {
|
|
206
|
+
let next = state.id.get() + 1;
|
|
207
|
+
state.id.set(next);
|
|
208
|
+
let held = HeldFlag::set(&self.mutex.0.held);
|
|
160
209
|
let result = f(RenderGuard { _private: () }).await;
|
|
161
210
|
drop(held);
|
|
162
211
|
if result.is_ok() {
|
|
163
|
-
|
|
212
|
+
state.last.set(next);
|
|
164
213
|
}
|
|
165
214
|
|
|
166
215
|
result
|
|
@@ -168,14 +217,14 @@ impl DebounceMutex {
|
|
|
168
217
|
Ok(T::default())
|
|
169
218
|
};
|
|
170
219
|
|
|
171
|
-
drop(
|
|
220
|
+
drop(guard);
|
|
172
221
|
drop(settle);
|
|
173
222
|
result
|
|
174
223
|
}
|
|
175
224
|
|
|
176
225
|
async fn await_settled(&self, next: u64) {
|
|
177
|
-
while self.
|
|
178
|
-
if self.
|
|
226
|
+
while self.state.settled_id.get() < next {
|
|
227
|
+
if self.state.on_settle.read_next().await.is_err() {
|
|
179
228
|
break;
|
|
180
229
|
}
|
|
181
230
|
}
|
package/src/rust/workspace.rs
CHANGED
|
@@ -21,7 +21,7 @@ use perspective_client::config::Filter;
|
|
|
21
21
|
|
|
22
22
|
use crate::renderer::Renderer;
|
|
23
23
|
use crate::session::Session;
|
|
24
|
-
use crate::utils::{EffectLedger, PubSub, Subscription};
|
|
24
|
+
use crate::utils::{EffectLedger, PubSub, Subscription, spawn_owned};
|
|
25
25
|
|
|
26
26
|
/// A unique identifier for a [`Panel`] within a [`Workspace`].
|
|
27
27
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
@@ -243,6 +243,22 @@ struct WorkspaceData {
|
|
|
243
243
|
/// In-flight effects (public mutators + scheduled internal flows),
|
|
244
244
|
/// drained by `flush()` — see [`EffectLedger`].
|
|
245
245
|
effects: EffectLedger,
|
|
246
|
+
|
|
247
|
+
/// The PLACED panel set changed since the last emit. Mutation sites set
|
|
248
|
+
/// this and emit NOTHING; the coalescing flush task owns delivery (see
|
|
249
|
+
/// [`Workspace::schedule_layout_flush`]).
|
|
250
|
+
layout_dirty: bool,
|
|
251
|
+
layout_changed: Rc<PubSub<Vec<PanelId>>>,
|
|
252
|
+
|
|
253
|
+
/// The active panel changed since the last emit. A separate channel from
|
|
254
|
+
/// `layout_dirty`: "which panel is selected" and "which panels exist" are
|
|
255
|
+
/// distinct facts, and one event may not mean both.
|
|
256
|
+
active_dirty: bool,
|
|
257
|
+
active_changed: Rc<PubSub<Option<PanelId>>>,
|
|
258
|
+
|
|
259
|
+
/// A flush task is already queued — the flag that makes N mutations
|
|
260
|
+
/// within one operation schedule ONE task rather than N.
|
|
261
|
+
flush_scheduled: bool,
|
|
246
262
|
}
|
|
247
263
|
|
|
248
264
|
impl Default for Workspace {
|
|
@@ -269,6 +285,11 @@ impl Workspace {
|
|
|
269
285
|
staged_changed: Rc::new(PubSub::default()),
|
|
270
286
|
reserved: None,
|
|
271
287
|
effects: EffectLedger::default(),
|
|
288
|
+
layout_dirty: false,
|
|
289
|
+
layout_changed: Rc::new(PubSub::default()),
|
|
290
|
+
active_dirty: false,
|
|
291
|
+
active_changed: Rc::new(PubSub::default()),
|
|
292
|
+
flush_scheduled: false,
|
|
272
293
|
})))
|
|
273
294
|
}
|
|
274
295
|
|
|
@@ -277,6 +298,62 @@ impl Workspace {
|
|
|
277
298
|
self.0.borrow().effects.clone()
|
|
278
299
|
}
|
|
279
300
|
|
|
301
|
+
pub fn layout_changed(&self) -> Rc<PubSub<Vec<PanelId>>> {
|
|
302
|
+
self.0.borrow().layout_changed.clone()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
pub fn active_changed(&self) -> Rc<PubSub<Option<PanelId>>> {
|
|
306
|
+
self.0.borrow().active_changed.clone()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// Queue the coalescing layout-event flush, if anything is dirty and no
|
|
310
|
+
/// flush is already pending.
|
|
311
|
+
fn schedule_layout_flush(&self) {
|
|
312
|
+
let schedule = {
|
|
313
|
+
let mut data = self.0.borrow_mut();
|
|
314
|
+
let dirty = data.layout_dirty || data.active_dirty;
|
|
315
|
+
let queued = data.flush_scheduled;
|
|
316
|
+
data.flush_scheduled |= dirty;
|
|
317
|
+
dirty && !queued
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
if !schedule {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let effects = self.effects();
|
|
325
|
+
let this = self.clone();
|
|
326
|
+
spawn_owned("workspace_layout_flush", async move {
|
|
327
|
+
effects.settle().await;
|
|
328
|
+
this.flush_layout_events();
|
|
329
|
+
Ok(())
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Emit whatever is dirty, outside any borrow.
|
|
334
|
+
fn flush_layout_events(&self) {
|
|
335
|
+
let (layout, active, panels, active_id, layout_pubsub, active_pubsub) = {
|
|
336
|
+
let mut data = self.0.borrow_mut();
|
|
337
|
+
data.flush_scheduled = false;
|
|
338
|
+
(
|
|
339
|
+
std::mem::take(&mut data.layout_dirty),
|
|
340
|
+
std::mem::take(&mut data.active_dirty),
|
|
341
|
+
data.panels.iter().map(|p| p.id.clone()).collect::<Vec<_>>(),
|
|
342
|
+
data.active.clone(),
|
|
343
|
+
data.layout_changed.clone(),
|
|
344
|
+
data.active_changed.clone(),
|
|
345
|
+
)
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
if layout {
|
|
349
|
+
layout_pubsub.emit(panels);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if active {
|
|
353
|
+
active_pubsub.emit(active_id);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
280
357
|
/// Stage a layout tree for `MainPanel` to apply at its next `rendered`
|
|
281
358
|
/// pass (see `WorkspaceData::pending_layout`).
|
|
282
359
|
pub fn set_pending_layout(&self, layout: crate::js::Layout) {
|
|
@@ -512,16 +589,22 @@ impl Workspace {
|
|
|
512
589
|
/// Append a [`Panel`]. When the element had zero panels, the inserted panel
|
|
513
590
|
/// becomes the active one (there is no other candidate).
|
|
514
591
|
pub fn insert_panel(&self, panel: Panel) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
data.active
|
|
592
|
+
{
|
|
593
|
+
let mut data = self.0.borrow_mut();
|
|
594
|
+
if data.active.is_none() {
|
|
595
|
+
data.active = Some(panel.id.clone());
|
|
596
|
+
data.active_dirty = true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
panel
|
|
600
|
+
.renderer
|
|
601
|
+
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
602
|
+
data.panels.push(panel);
|
|
603
|
+
data.layout_dirty = true;
|
|
604
|
+
Self::sync_solo_flags(&data);
|
|
518
605
|
}
|
|
519
606
|
|
|
520
|
-
|
|
521
|
-
.renderer
|
|
522
|
-
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
523
|
-
data.panels.push(panel);
|
|
524
|
-
Self::sync_solo_flags(&data);
|
|
607
|
+
self.schedule_layout_flush();
|
|
525
608
|
}
|
|
526
609
|
|
|
527
610
|
/// Hold `panel` in the reservation slot (see [`WorkspaceData::reserved`]):
|
|
@@ -585,8 +668,10 @@ impl Workspace {
|
|
|
585
668
|
|
|
586
669
|
if data.active.as_ref() == Some(id) {
|
|
587
670
|
data.active = None;
|
|
671
|
+
data.active_dirty = true;
|
|
588
672
|
}
|
|
589
673
|
|
|
674
|
+
data.layout_dirty |= removed.is_some();
|
|
590
675
|
Self::sync_solo_flags(&data);
|
|
591
676
|
(
|
|
592
677
|
removed,
|
|
@@ -605,25 +690,32 @@ impl Workspace {
|
|
|
605
690
|
staged_pubsub.emit(());
|
|
606
691
|
}
|
|
607
692
|
|
|
693
|
+
self.schedule_layout_flush();
|
|
608
694
|
removed
|
|
609
695
|
}
|
|
610
696
|
|
|
611
697
|
/// Set the active panel. Returns `false` (no-op) if `id` is not a known
|
|
612
698
|
/// panel.
|
|
613
699
|
pub fn set_active(&self, id: PanelId) -> bool {
|
|
614
|
-
let
|
|
615
|
-
|
|
616
|
-
data.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
700
|
+
let known = {
|
|
701
|
+
let mut data = self.0.borrow_mut();
|
|
702
|
+
if data.panels.iter().any(|p| p.id == id) {
|
|
703
|
+
data.active_dirty |= data.active.as_ref() != Some(&id);
|
|
704
|
+
data.active = Some(id);
|
|
705
|
+
for panel in data.panels.iter() {
|
|
706
|
+
panel
|
|
707
|
+
.renderer
|
|
708
|
+
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
true
|
|
712
|
+
} else {
|
|
713
|
+
false
|
|
621
714
|
}
|
|
715
|
+
};
|
|
622
716
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
false
|
|
626
|
-
}
|
|
717
|
+
self.schedule_layout_flush();
|
|
718
|
+
known
|
|
627
719
|
}
|
|
628
720
|
|
|
629
721
|
/// The default [`Client`], if one has been loaded.
|
package/src/themes/icons.css
CHANGED
|
@@ -52,6 +52,7 @@ perspective-viewer-tab {
|
|
|
52
52
|
--psp-icon--sort-none--mask-image: url("../svg/sort-none-icon.svg");
|
|
53
53
|
--psp-icon--add-expression--mask-image: url("../svg/expression.svg");
|
|
54
54
|
--psp-icon--close--mask-image: url("../svg/close-icon.svg");
|
|
55
|
+
--psp-icon--pin--mask-image: url("../svg/pin-icon.svg");
|
|
55
56
|
--psp-label--inactive-column-selector--content: url("../svg/checkbox-unchecked-icon.svg");
|
|
56
57
|
--psp-label--active-column-selector--content: url("../svg/checkbox-checked-icon.svg");
|
|
57
58
|
--psp-icon--select-arrow-light--mask-image: url("../svg/dropdown-selector-light.svg");
|
package/src/themes/intl/de.css
CHANGED
|
@@ -108,9 +108,32 @@ perspective-dropdown {
|
|
|
108
108
|
/* Tabs */
|
|
109
109
|
--psp-label--style-tab--content: "Stil";
|
|
110
110
|
--psp-label--attributes-tab--content: "Attribute";
|
|
111
|
+
--psp-label--window-tab--content: "Fenster";
|
|
112
|
+
--psp-label--window-source--content: "Spalte";
|
|
113
|
+
--psp-label--window-order-by--content: "Sortieren nach";
|
|
114
|
+
--psp-label--window-partition-by--content: "Partitionieren nach";
|
|
115
|
+
--psp-label--window-frame--content: "Rahmen";
|
|
116
|
+
--psp-label--window-offset--content: "Versatz";
|
|
117
|
+
--psp-label--window-alpha--content: "Alpha";
|
|
111
118
|
--psp-label--query-tab--content: "Abfrage";
|
|
112
119
|
--psp-label--plugin-tab--content: "Plugin";
|
|
113
120
|
--psp-label--debug-tab--content: "JSON";
|
|
121
|
+
--psp-label--chat-tab--content: "Chat";
|
|
122
|
+
|
|
123
|
+
/* Chat panel */
|
|
124
|
+
--psp-label--chat-reasoning--content: "Denkprozess";
|
|
125
|
+
--psp-label--chat-send--content: "Senden";
|
|
126
|
+
--psp-label--chat-stop--content: "Stopp";
|
|
127
|
+
--psp-label--chat-placeholder--content: "Frage zu Ihren Daten…";
|
|
128
|
+
|
|
129
|
+
/* Plugin-declared pivot roles (PluginStaticConfig) */
|
|
130
|
+
--psp-label--role--x-axis--content: "X-Achse";
|
|
131
|
+
--psp-label--role--y-axis--content: "Y-Achse";
|
|
132
|
+
--psp-label--role--series--content: "Serien";
|
|
133
|
+
--psp-label--role--hierarchy--content: "Hierarchie";
|
|
134
|
+
--psp-label--role--facets--content: "Facetten";
|
|
135
|
+
--psp-label--role--row-groups--content: "Zeilengruppen";
|
|
136
|
+
--psp-label--role--column-groups--content: "Spaltengruppen";
|
|
114
137
|
|
|
115
138
|
/* Plugin settings */
|
|
116
139
|
--psp-label--edit-mode--content: "Bearbeitungsmodus";
|
package/src/themes/intl/es.css
CHANGED
|
@@ -108,9 +108,32 @@ perspective-dropdown {
|
|
|
108
108
|
/* Tabs */
|
|
109
109
|
--psp-label--style-tab--content: "Estilo";
|
|
110
110
|
--psp-label--attributes-tab--content: "Atributos";
|
|
111
|
+
--psp-label--window-tab--content: "Ventana";
|
|
112
|
+
--psp-label--window-source--content: "Columna";
|
|
113
|
+
--psp-label--window-order-by--content: "Ordenar por";
|
|
114
|
+
--psp-label--window-partition-by--content: "Particionar por";
|
|
115
|
+
--psp-label--window-frame--content: "Marco";
|
|
116
|
+
--psp-label--window-offset--content: "Desplazamiento";
|
|
117
|
+
--psp-label--window-alpha--content: "Alfa";
|
|
111
118
|
--psp-label--query-tab--content: "Consulta";
|
|
112
119
|
--psp-label--plugin-tab--content: "Plugin";
|
|
113
120
|
--psp-label--debug-tab--content: "JSON";
|
|
121
|
+
--psp-label--chat-tab--content: "Chat";
|
|
122
|
+
|
|
123
|
+
/* Chat panel */
|
|
124
|
+
--psp-label--chat-reasoning--content: "Razonamiento";
|
|
125
|
+
--psp-label--chat-send--content: "Enviar";
|
|
126
|
+
--psp-label--chat-stop--content: "Detener";
|
|
127
|
+
--psp-label--chat-placeholder--content: "Pregunta sobre tus datos…";
|
|
128
|
+
|
|
129
|
+
/* Plugin-declared pivot roles (PluginStaticConfig) */
|
|
130
|
+
--psp-label--role--x-axis--content: "Eje X";
|
|
131
|
+
--psp-label--role--y-axis--content: "Eje Y";
|
|
132
|
+
--psp-label--role--series--content: "Series";
|
|
133
|
+
--psp-label--role--hierarchy--content: "Jerarquía";
|
|
134
|
+
--psp-label--role--facets--content: "Facetas";
|
|
135
|
+
--psp-label--role--row-groups--content: "Grupos de filas";
|
|
136
|
+
--psp-label--role--column-groups--content: "Grupos de columnas";
|
|
114
137
|
|
|
115
138
|
/* Plugin settings */
|
|
116
139
|
--psp-label--edit-mode--content: "Modo de edición";
|