Skip to content

Commit 11a06a2

Browse files
committed
Invalid drag/drop UX
1 parent ccf9c2c commit 11a06a2

File tree

17 files changed

+287
-99
lines changed

17 files changed

+287
-99
lines changed

packages/perspective-jupyterlab/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"@finos/perspective-esbuild-plugin": "^2.7.0",
4949
"@finos/perspective-test": "^2.7.0",
5050
"@jupyterlab/builder": "^3.4.0",
51-
"@prospective.co/procss": "^0.1.14",
51+
"@prospective.co/procss": "^0.1.15",
5252
"cpy": "^9.0.1"
5353
},
5454
"jupyterlab": {

packages/perspective-viewer-d3fc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,6 @@
5656
"devDependencies": {
5757
"@finos/perspective-esbuild-plugin": "^2.7.0",
5858
"@finos/perspective-test": "^2.7.0",
59-
"@prospective.co/procss": "^0.1.14"
59+
"@prospective.co/procss": "^0.1.15"
6060
}
6161
}

packages/perspective-workspace/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"lodash": "^4.17.4"
4242
},
4343
"devDependencies": {
44-
"@prospective.co/procss": "^0.1.14",
44+
"@prospective.co/procss": "^0.1.15",
4545
"@finos/perspective-esbuild-plugin": "^2.7.0",
4646
"@finos/perspective-test": "^2.7.0"
4747
}

rust/perspective-viewer/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/perspective-viewer/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ strip = true
4949

5050
[build-dependencies]
5151
serde_json = { version = "1.0.59", features = ["raw_value"] }
52-
procss = { version = "0.1.14" }
52+
procss = { version = "0.1.15" }
5353
glob = "0.3.0"
5454
anyhow = "1.0.66"
5555

rust/perspective-viewer/src/less/empty-column.less

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,41 @@
1111
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

1313
:host {
14+
--invalid-column-pattern: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' preserveAspectRatio='none' viewBox='0 0 100 24'><path d='M100 0 L0 24 ' stroke='black' stroke-width='1'/><path d='M0 0 L100 24 ' stroke='black' stroke-width='1'/></svg>");
1415
.column-empty {
1516
width: 100%;
1617
}
1718

19+
#top_panel .column-invalid.pivot-column,
20+
.column-invalid.pivot-column {
21+
.column-invalid-input {
22+
mask-image: var(--invalid-column-pattern);
23+
-webkit-mask-image: var(--invalid-column-pattern);
24+
background-color: var(--icon--color);
25+
mask-size: cover;
26+
-webkit-mask-size: cover;
27+
width: 100%;
28+
height: 22px;
29+
background-repeat: no-repeat;
30+
background-position: center center;
31+
background-size: 100% 100%, auto;
32+
}
33+
34+
position: relative;
35+
box-sizing: border-box;
36+
width: calc(100% - 7px);
37+
background-color: #8b868045;
38+
border: 1px solid var(--icon--color);
39+
border-radius: 2px;
40+
margin-right: 6px;
41+
margin-bottom: 4px;
42+
min-height: 22px;
43+
width: calc(100% - 7px);
44+
outline: none;
45+
}
46+
1847
.column-empty-input {
1948
position: relative;
20-
width: calc(100% - 27px);
21-
min-height: 28px;
2249
display: flex;
2350
align-items: stretch;
2451
cursor: auto;

rust/perspective-viewer/src/rust/components/column_selector.rs

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ mod empty_column;
1818
mod expression_toolbar;
1919
mod filter_column;
2020
mod inactive_column;
21+
mod invalid_column;
2122
mod pivot_column;
2223
mod sort_column;
2324

2425
use std::iter::*;
2526
use std::rc::Rc;
2627

2728
pub use empty_column::*;
29+
pub use invalid_column::*;
2830
use web_sys::*;
2931
use yew::prelude::*;
3032

@@ -161,43 +163,28 @@ impl Component for ColumnSelector {
161163
self.named_row_count = named.unwrap_or_default();
162164
true
163165
},
164-
HoverActiveIndex(Some(to_index)) => {
165-
let min_cols = ctx.props().renderer.metadata().min;
166-
let config = ctx.props().session.get_view_config();
167-
let is_to_empty = !config
168-
.columns
169-
.get(to_index)
170-
.map(|x| x.is_some())
171-
.unwrap_or_default();
172-
173-
let from_index = ctx
174-
.props()
175-
.dragdrop
176-
.get_drag_column()
177-
.and_then(|x| config.columns.iter().position(|z| z.as_ref() == Some(&x)));
178-
179-
if min_cols
180-
.and_then(|x| from_index.map(|from_index| from_index < x))
181-
.unwrap_or_default()
182-
&& is_to_empty
183-
|| from_index
184-
.map(|from_index| {
185-
from_index == config.columns.len() - 1 && to_index > from_index
186-
})
187-
.unwrap_or_default()
188-
{
189-
ctx.props().dragdrop.notify_drag_leave(DragTarget::Active);
190-
true
191-
} else {
192-
ctx.props()
193-
.dragdrop
194-
.notify_drag_enter(DragTarget::Active, to_index)
195-
}
196-
},
166+
HoverActiveIndex(Some(to_index)) => ctx
167+
.props()
168+
.dragdrop
169+
.notify_drag_enter(DragTarget::Active, to_index),
197170
HoverActiveIndex(_) => {
198171
ctx.props().dragdrop.notify_drag_leave(DragTarget::Active);
199172
true
200173
},
174+
Drop((column, DragTarget::Active, DragEffect::Move(DragTarget::Active), index)) => {
175+
if !ctx.props().is_invalid_columns_column(&column, index) {
176+
let update = ctx.props().session.create_drag_drop_update(
177+
column,
178+
index,
179+
DragTarget::Active,
180+
DragEffect::Move(DragTarget::Active),
181+
&ctx.props().renderer.metadata(),
182+
);
183+
184+
ApiFuture::spawn(ctx.props().update_and_render(update));
185+
}
186+
true
187+
},
201188
Drop((column, DragTarget::Active, effect, index)) => {
202189
let update = ctx.props().session.create_drag_drop_update(
203190
column,
@@ -273,26 +260,29 @@ impl Component for ColumnSelector {
273260
.get_name()
274261
.map(|x| x.to_owned())
275262
.unwrap_or_else(|| format!("__auto_{}__", idx));
263+
276264
let column_dropdown = self.column_dropdown.clone();
277265
let is_editing = matches!(
278266
&ctx.props().selected_column,
279267
Some(ColumnLocator::Plain(x)) | Some(ColumnLocator::Expr(Some(x))) if x == &key
280268
);
269+
270+
let on_open_expr_panel = &ctx.props().on_open_expr_panel;
281271
html_nested! {
282272
<ScrollPanelItem key={ key } { size_hint }>
283273
<ActiveColumn
274+
{ column_dropdown }
284275
{ idx }
285-
{ name }
286276
{ is_aggregated }
287277
{ is_editing }
288-
{ column_dropdown }
278+
{ name }
279+
{ on_open_expr_panel }
289280
dragdrop={ &ctx.props().dragdrop }
290281
session={ &ctx.props().session }
291282
renderer={ &ctx.props().renderer }
292283
ondragenter={ ondragenter }
293284
ondragend={ &ondragend }
294-
onselect={ &onselect }
295-
on_open_expr_panel={ &ctx.props().on_open_expr_panel } />
285+
onselect={ &onselect }/>
296286
</ScrollPanelItem>
297287
}
298288
})

rust/perspective-viewer/src/rust/components/column_selector/active_column.rs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use yew::prelude::*;
1818
use super::aggregate_selector::*;
1919
use super::expression_toolbar::*;
2020
use super::InPlaceColumn;
21-
use crate::components::column_selector::EmptyColumn;
21+
use crate::components::column_selector::{EmptyColumn, InvalidColumn};
2222
use crate::components::viewer::ColumnLocator;
2323
use crate::config::*;
2424
use crate::custom_elements::ColumnDropDownElement;
@@ -30,6 +30,12 @@ use crate::session::*;
3030
use crate::utils::ApiFuture;
3131
use crate::*;
3232

33+
enum ColumnState {
34+
Empty,
35+
Invalid,
36+
Named(String),
37+
}
38+
3339
#[derive(Properties, Clone)]
3440
pub struct ActiveColumnProps {
3541
pub idx: usize,
@@ -56,10 +62,11 @@ impl PartialEq for ActiveColumnProps {
5662

5763
impl ActiveColumnProps {
5864
fn get_name(&self) -> Option<String> {
59-
match &self.name {
60-
ActiveColumnState::DragOver(_) => Some(self.dragdrop.get_drag_column().unwrap()),
61-
ActiveColumnState::Column(_, name) => Some(name.to_owned()),
62-
ActiveColumnState::Required(_) => None,
65+
match &self.name.state {
66+
ActiveColumnStateData::DragOver => Some(self.dragdrop.get_drag_column().unwrap()),
67+
ActiveColumnStateData::Column(name) => Some(name.to_owned()),
68+
ActiveColumnStateData::Required => None,
69+
ActiveColumnStateData::Invalid => None,
6370
}
6471
}
6572

@@ -237,18 +244,31 @@ impl Component for ActiveColumn {
237244
}
238245

239246
let name = match &ctx.props().name {
240-
ActiveColumnState::DragOver(label) => {
247+
ActiveColumnState {
248+
label,
249+
state: ActiveColumnStateData::DragOver,
250+
} => {
241251
classes.push("dragover");
242252
outer_classes.push("dragover-container");
243253
classes.push("empty-named");
244254

245255
(
246256
label.clone(),
247-
Some(ctx.props().dragdrop.get_drag_column().unwrap()),
257+
ColumnState::Named(ctx.props().dragdrop.get_drag_column().unwrap()),
248258
)
249259
},
250-
ActiveColumnState::Column(label, name) => (label.clone(), Some(name.to_owned())),
251-
ActiveColumnState::Required(label) => (label.clone(), None),
260+
ActiveColumnState {
261+
label,
262+
state: ActiveColumnStateData::Column(name),
263+
} => (label.clone(), ColumnState::Named(name.to_owned())),
264+
ActiveColumnState {
265+
label,
266+
state: ActiveColumnStateData::Required,
267+
} => (label.clone(), ColumnState::Empty),
268+
ActiveColumnState {
269+
label,
270+
state: ActiveColumnStateData::Invalid,
271+
} => (label.clone(), ColumnState::Invalid),
252272
};
253273

254274
let ondragenter = ctx.props().ondragenter.reform(move |event: DragEvent| {
@@ -263,7 +283,7 @@ impl Component for ActiveColumn {
263283

264284
let col_type = self.column_type;
265285
match (name, col_type) {
266-
((label, None), _) => {
286+
((label, ColumnState::Empty), _) => {
267287
classes.push("empty-named");
268288
let column_dropdown = ctx.props().column_dropdown.clone();
269289
let on_select = ctx.link().callback(ActiveColumnMsg::New);
@@ -284,11 +304,24 @@ impl Component for ActiveColumn {
284304
data-index={ ctx.props().idx.to_string() }
285305
ondragenter={ ondragenter.clone() }>
286306

287-
<EmptyColumn { column_dropdown } { exclude } { on_select } />
307+
<EmptyColumn { column_dropdown } { exclude } { on_select }/>
308+
</div>
309+
}
310+
},
311+
((label, ColumnState::Invalid), _) => {
312+
classes.push("empty-named");
313+
html! {
314+
<div
315+
class={ outer_classes }
316+
data-label={ label }
317+
data-index={ ctx.props().idx.to_string() }
318+
ondragenter={ ondragenter.clone() }>
319+
320+
<InvalidColumn />
288321
</div>
289322
}
290323
},
291-
((label, Some(name)), Some(col_type)) => {
324+
((label, ColumnState::Named(name)), Some(col_type)) => {
292325
let remove_column = if self.is_required {
293326
None
294327
} else {

rust/perspective-viewer/src/rust/components/column_selector/empty_column.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ pub struct EmptyColumn {
2828
#[derive(Clone, Debug)]
2929
pub enum InPlaceColumn {
3030
Column(String),
31-
// ExpressionAlias(String),
3231
Expression(Expression<'static>),
3332
}
3433

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 yew::prelude::*;
14+
15+
use crate::components::style::LocalStyle;
16+
use crate::css;
17+
18+
#[derive(Default)]
19+
pub struct InvalidColumn {}
20+
21+
impl Component for InvalidColumn {
22+
type Message = ();
23+
type Properties = ();
24+
25+
fn create(_ctx: &Context<Self>) -> Self {
26+
Self::default()
27+
}
28+
29+
fn view(&self, _ctx: &Context<Self>) -> Html {
30+
html! {
31+
<div class="pivot-column column-empty column-invalid">
32+
<LocalStyle href={ css!("empty-column") } />
33+
<div class="column-invalid-input"></div>
34+
</div>
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)