T14b Tables: resize, reorder, visibility, pinning, expansion, grouping, inline edit, toolbar, filter and sort builders (review pending: codex 401)
This commit is contained in:
17 files changed
+2292
-420
No files matched your search
@@ -1,14 +1,18 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::SegmentedControl,
|
||||
data_display::Tone,
|
||||
forms::{SearchInput, TextInput},
|
||||
tables::{Aggregate, Cell, Column, DataTable, HeatmapTable, Row, Table},
|
||||
theme::{ActiveTheme, ControlSize, Density, Radius},
|
||||
forms::TextInput,
|
||||
tables::{
|
||||
Aggregate, Cell, Column, DataTable, FilterBuilder, FilterRule, HeatmapTable, Row,
|
||||
SortBuilder, SortKey, Table, TableToolbar, Test, to_csv,
|
||||
},
|
||||
theme::{ActiveTheme, Density, Radius},
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px, rems};
|
||||
use gpui::{
|
||||
App, ClipboardItem, IntoElement, ParentElement, SharedString, Styled, Window, div, px, rems,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
@@ -56,7 +60,7 @@ fn noise(seed: u64) -> impl FnMut() -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn order_rows() -> Vec<Row> {
|
||||
pub(super) fn order_rows() -> Vec<Row> {
|
||||
let mut next = noise(7);
|
||||
(0..24)
|
||||
.map(|ix| {
|
||||
@@ -82,7 +86,7 @@ fn order_rows() -> Vec<Row> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn order_columns() -> [Column; 7] {
|
||||
pub(super) fn order_columns() -> [Column; 7] {
|
||||
[
|
||||
Column::new("owner", "Owner"),
|
||||
Column::new("status", "Status").width(rems(6.)),
|
||||
@@ -110,58 +114,125 @@ pub fn orders(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let search = window.use_keyed_state("orders-search", cx, TextInput::new);
|
||||
let query = search.read(cx).text().to_string();
|
||||
let picked = keep("orders-picked", Vec::<SharedString>::new, window, cx);
|
||||
let density = keep(
|
||||
"orders-density",
|
||||
|| SharedString::from("standard"),
|
||||
let spacing = keep("orders-density", || Density::Standard, window, cx);
|
||||
let hidden = keep("orders-hidden", Vec::<SharedString>::new, window, cx);
|
||||
let panel = keep("orders-panel", || false, window, cx);
|
||||
let rules = keep(
|
||||
"orders-rules",
|
||||
|| {
|
||||
(
|
||||
vec![FilterRule {
|
||||
column: "status".into(),
|
||||
test: Test::Contains,
|
||||
value: SharedString::default(),
|
||||
}],
|
||||
false,
|
||||
)
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let (chosen, store, dense, pick) = (
|
||||
picked.read(cx).clone(),
|
||||
picked.clone(),
|
||||
density.read(cx).clone(),
|
||||
density.clone(),
|
||||
);
|
||||
let spacing = match dense.as_ref() {
|
||||
"compact" => Density::Compact,
|
||||
"comfortable" => Density::Comfortable,
|
||||
_ => Density::Standard,
|
||||
};
|
||||
section(
|
||||
"DataTable / CellRenderer / RowSelection / TableDensity / AggregationFooter / ConditionalFormatting",
|
||||
"Press a header to sort, rising then falling. Search keeps rows that hold the text; boxes select, Shift takes a range. Cells draw people, tags, sparklines and progress; growth is tinted by where it sits; the footer sums and averages what the search keeps.",
|
||||
let sorts = keep(
|
||||
"orders-sorts",
|
||||
|| {
|
||||
vec![SortKey {
|
||||
column: "region".into(),
|
||||
rising: true,
|
||||
}]
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
);
|
||||
let copied = keep("orders-copied", || None::<usize>, window, cx);
|
||||
let (chosen, dense, gone, open, (now_rules, any), now_sorts, told) = (
|
||||
picked.read(cx).clone(),
|
||||
*spacing.read(cx),
|
||||
hidden.read(cx).clone(),
|
||||
*panel.read(cx),
|
||||
rules.read(cx).clone(),
|
||||
sorts.read(cx).clone(),
|
||||
*copied.read(cx),
|
||||
);
|
||||
let titles: Vec<(SharedString, SharedString)> = order_columns()
|
||||
.iter()
|
||||
.map(|column| (column.key().clone(), column.title().clone()))
|
||||
.collect();
|
||||
let theme = cx.theme();
|
||||
let toolbar = TableToolbar::new("orders-tools")
|
||||
.search(&search)
|
||||
.filter(now_rules.len(), {
|
||||
let panel = panel.clone();
|
||||
move |_, cx| set(&panel, !open, cx)
|
||||
})
|
||||
.columns(titles.clone(), gone.clone(), {
|
||||
let hidden = hidden.clone();
|
||||
move |keys, _, cx| set(&hidden, keys.to_vec(), cx)
|
||||
})
|
||||
.density(dense, {
|
||||
let spacing = spacing.clone();
|
||||
move |density, _, cx| set(&spacing, density, cx)
|
||||
})
|
||||
.export({
|
||||
let copied = copied.clone();
|
||||
move |_, cx| {
|
||||
let rows = order_rows();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(to_csv(&order_columns(), &rows)));
|
||||
set(&copied, Some(rows.len()), cx)
|
||||
}
|
||||
});
|
||||
let builders = open.then(|| {
|
||||
let (store_rules, store_sorts) = (rules.clone(), sorts.clone());
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w(px(900.))
|
||||
.pb_3()
|
||||
.child(div().w(px(260.)).child(SearchInput::new("orders-find", &search).size(ControlSize::Sm)))
|
||||
.gap_8()
|
||||
.p_4()
|
||||
.mb_3()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.bg(theme.colors.sunken)
|
||||
.child(
|
||||
SegmentedControl::new("orders-density", dense)
|
||||
.segment("compact", "Compact", None)
|
||||
.segment("standard", "Standard", None)
|
||||
.segment("comfortable", "Comfortable", None)
|
||||
.size(ControlSize::Sm)
|
||||
.on_change(move |value, _, cx| set(&pick, value.clone(), cx)),
|
||||
),
|
||||
div().flex_1().child(
|
||||
FilterBuilder::new("orders-filter", titles.clone())
|
||||
.rules(now_rules.clone(), any)
|
||||
.on_change(move |rules, any, _, cx| {
|
||||
set(&store_rules, (rules.to_vec(), any), cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().w(px(380.)).child(
|
||||
SortBuilder::new("orders-sort", titles.clone())
|
||||
.keys(now_sorts.clone())
|
||||
.on_change(move |keys, _, cx| set(&store_sorts, keys.to_vec(), cx)),
|
||||
),
|
||||
)
|
||||
});
|
||||
let store = picked.clone();
|
||||
section(
|
||||
"DataTable / TableToolbar / FilterBuilder / SortBuilder / ColumnVisibility / CellRenderer / RowSelection / TableDensity / AggregationFooter / ConditionalFormatting",
|
||||
"Press a header to sort, Shift to add it to the sort. The toolbar searches, opens the filter and sort builders, hides columns, sets the density and copies the rows as CSV. Boxes select; Shift takes a range. Growth is tinted by where it sits; the footer sums and averages what the filters keep.",
|
||||
cx,
|
||||
)
|
||||
.child(div().pb_3().child(probe("orders-tools", div().w(px(900.)).child(toolbar))))
|
||||
.children(builders.map(|panel| div().w(px(900.)).child(panel)))
|
||||
.child(probe(
|
||||
"orders",
|
||||
div().w(px(900.)).child(
|
||||
DataTable::new("orders", order_columns())
|
||||
.rows(order_rows())
|
||||
.query(query)
|
||||
.filters(now_rules, any)
|
||||
.sorts(now_sorts)
|
||||
.hidden(gone)
|
||||
.paged(8)
|
||||
.density(spacing)
|
||||
.density(dense)
|
||||
.selected(chosen.clone())
|
||||
.on_select(move |keys, _, cx| set(&store, keys.to_vec(), cx)),
|
||||
),
|
||||
))
|
||||
.child(Caption::new(format!("{} selected", chosen.len())))
|
||||
.child(Caption::new(match told {
|
||||
Some(rows) => format!("{} selected. Copied {rows} rows as CSV.", chosen.len()),
|
||||
None => format!("{} selected", chosen.len()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn long(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
use ely_gpui_component::{
|
||||
data_display::{DescriptionList, Tone},
|
||||
tables::{Cell, Column, DataTable, Row},
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px, rems};
|
||||
|
||||
use super::core::{order_columns, order_rows};
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, section, set},
|
||||
};
|
||||
|
||||
const MONTHS: [&str; 12] = [
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||
];
|
||||
|
||||
pub fn wide(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let columns = std::iter::once(Column::new("team", "Team").width(rems(10.)).pinned()).chain(
|
||||
MONTHS
|
||||
.iter()
|
||||
.map(|month| Column::new(*month, *month).width(rems(5.5)).end()),
|
||||
);
|
||||
let rows = ["Design", "Platform", "Apps", "Growth", "Support"]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, team)| {
|
||||
Row::new(
|
||||
*team,
|
||||
std::iter::once(Cell::from(*team)).chain(
|
||||
(0..12).map(|month| Cell::Number(((ix * 7 + month * 13) % 40 + 10) as f64)),
|
||||
),
|
||||
)
|
||||
});
|
||||
section(
|
||||
"ColumnPinning / ColumnResizer / ColumnReorder",
|
||||
"Team stays put while the months scroll sideways. Drag a header's right edge to resize it; drag a header onto another to move it there.",
|
||||
cx,
|
||||
)
|
||||
.child(div().w(px(620.)).child(DataTable::new("wide", columns).rows(rows.collect::<Vec<_>>())))
|
||||
}
|
||||
|
||||
/// Tasks the demo edits in place: key, name, owner, state.
|
||||
fn tasks() -> Vec<(SharedString, SharedString, &'static str, &'static str)> {
|
||||
[
|
||||
("t1", "Draft the launch notes", "Ada Lovelace", "Doing"),
|
||||
("t2", "Review the type scale", "Grace Hopper", "Done"),
|
||||
("t3", "Record the demo", "Alan Turing", "Next"),
|
||||
]
|
||||
.map(|(key, name, owner, state)| (key.into(), name.into(), owner, state))
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
pub fn rows(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let kept = keep("tasks", tasks, window, cx);
|
||||
let (now, store) = (kept.read(cx).clone(), kept.clone());
|
||||
let rows: Vec<Row> = now
|
||||
.iter()
|
||||
.map(|(key, name, owner, state)| {
|
||||
let tone = match *state {
|
||||
"Done" => Tone::Success,
|
||||
"Doing" => Tone::Info,
|
||||
_ => Tone::Neutral,
|
||||
};
|
||||
Row::new(
|
||||
key.clone(),
|
||||
[
|
||||
Cell::Text(name.clone()),
|
||||
Cell::Person((*owner).into()),
|
||||
Cell::Tag((*state).into(), tone),
|
||||
],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let about = now.clone();
|
||||
section(
|
||||
"RowExpansion / InlineRowEdit",
|
||||
"The chevron opens a row's detail beneath it. Double press a task's name to edit it; Enter keeps the change, Escape drops it.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"tasks",
|
||||
div().w(px(640.)).child(
|
||||
DataTable::new(
|
||||
"tasks",
|
||||
[Column::new("name", "Task").editable(), Column::new("owner", "Owner").width(rems(12.)), Column::new("state", "State").width(rems(6.))],
|
||||
)
|
||||
.rows(rows)
|
||||
.detail(move |key, _, _| {
|
||||
let (_, name, owner, state) = about.iter().find(|task| task.0 == *key).expect("a listed task").clone();
|
||||
DescriptionList::new()
|
||||
.item("Task", name)
|
||||
.item("Owner", owner)
|
||||
.item("State", state)
|
||||
.into_any_element()
|
||||
})
|
||||
.on_edit(move |key, _, text, _, cx| {
|
||||
let mut next = store.read(cx).clone();
|
||||
if let Some(task) = next.iter_mut().find(|task| task.0 == *key) {
|
||||
task.1 = text.clone();
|
||||
}
|
||||
set(&store, next, cx)
|
||||
}),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn grouped(cx: &mut App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"RowGrouping",
|
||||
"Orders gathered by region, each group with its count; a press on a group folds it.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"grouped",
|
||||
div().w(px(900.)).child(
|
||||
DataTable::new("grouped", order_columns())
|
||||
.rows(order_rows().into_iter().take(9).collect::<Vec<_>>())
|
||||
.group_by("region"),
|
||||
),
|
||||
))
|
||||
.child(Caption::new(
|
||||
"Groups keep the order their first rows come in; sort to reorder them.",
|
||||
))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod core;
|
||||
mod features;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
|
||||
@@ -26,6 +27,22 @@ const SCRIPT: &[Step] = &[
|
||||
Step::UpAt("orders", 18.0, 126.0),
|
||||
Step::Wait(200),
|
||||
Step::Shot("selected"),
|
||||
Step::DownAt("orders-tools", 300.0, 14.0),
|
||||
Step::UpAt("orders-tools", 300.0, 14.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("builders"),
|
||||
Step::Hover("tasks"),
|
||||
Step::Wait(300),
|
||||
Step::DownAt("tasks", 14.0, 54.0),
|
||||
Step::UpAt("tasks", 14.0, 54.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("expanded"),
|
||||
Step::Hover("grouped"),
|
||||
Step::Wait(300),
|
||||
Step::DownAt("grouped", 60.0, 54.0),
|
||||
Step::UpAt("grouped", 60.0, 54.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("folded"),
|
||||
Step::Rest,
|
||||
];
|
||||
|
||||
@@ -33,6 +50,9 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(core::plain(cx))
|
||||
.child(core::orders(window, cx))
|
||||
.child(features::wide(cx))
|
||||
.child(features::rows(window, cx))
|
||||
.child(features::grouped(cx))
|
||||
.child(core::long(window, cx))
|
||||
.child(core::heatmap(cx))
|
||||
.into_any_element()
|
||||
|
||||
Reference in new issue
Block a user