feat: Refine filters and dropdown UI for task table

Update task table filters and dropdowns, including due-date options, layout tweaks, and style polish.
Keep panel style refinement intact so layout stays stable.
This commit is contained in:
Ignacio Perez
2025-12-27 16:00:38 -03:00
parent 45d1036fa7
commit 29a23017f2
8 changed files with 566 additions and 56 deletions
+2 -2
View File
@@ -20,9 +20,9 @@ impl gpui::Render for App {
fn render(
&mut self,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let theme = _cx.theme();
let theme = cx.theme();
let content = div()
.flex()
+100 -20
View File
@@ -3,17 +3,31 @@ use std::sync::Arc;
use gpui::prelude::*;
use crate::components::button::Button;
use crate::components::label::Label;
use crate::theme::ActiveTheme;
#[derive(Clone, Debug)]
pub struct DropdownItem {
pub label: gpui::SharedString,
pub value: gpui::SharedString,
}
impl DropdownItem {
pub fn new(label: impl Into<gpui::SharedString>) -> Self {
let label = label.into();
Self {
value: label.clone(),
label,
}
}
pub fn with_value(
label: impl Into<gpui::SharedString>,
value: impl Into<gpui::SharedString>,
) -> Self {
Self {
label: label.into(),
value: value.into(),
}
}
}
@@ -27,6 +41,7 @@ pub struct Dropdown {
disabled: bool,
loading: bool,
placeholder: gpui::SharedString,
label_prefix: Option<gpui::SharedString>,
on_select: Option<Arc<dyn Fn(usize, &DropdownItem, &mut gpui::Context<Self>) + Send + Sync>>,
}
@@ -41,6 +56,7 @@ impl Dropdown {
disabled: false,
loading: false,
placeholder: "Seleccionar".into(),
label_prefix: None,
on_select: None,
}
}
@@ -68,6 +84,11 @@ impl Dropdown {
self
}
pub fn label_prefix(mut self, prefix: impl Into<gpui::SharedString>) -> Self {
self.label_prefix = Some(prefix.into());
self
}
pub fn selected_index(mut self, index: usize) -> Self {
self.selected_index = Some(index);
self
@@ -108,6 +129,23 @@ impl Dropdown {
.and_then(|index| self.items.get(index).map(|item| item.label.clone()))
}
pub fn set_selected_index(&mut self, index: usize, cx: &mut gpui::Context<Self>) {
if index < self.items.len() {
self.selected_index = Some(index);
cx.notify();
}
}
pub fn set_items(&mut self, items: Vec<DropdownItem>, cx: &mut gpui::Context<Self>) {
self.items = items;
if let Some(selected) = self.selected_index {
if selected >= self.items.len() {
self.selected_index = None;
}
}
cx.notify();
}
fn toggle_open(&mut self, cx: &mut gpui::Context<Self>) {
if self.disabled || self.loading || self.items.is_empty() {
return;
@@ -168,15 +206,21 @@ impl Dropdown {
let is_selected = self.selected_index == Some(index);
let mut row = gpui::div()
.id(index)
.w_full()
.px_2()
.py_1()
.text_sm()
.whitespace_nowrap()
.bg(theme.background)
.text_color(if is_selected {
theme.selection_foreground
} else {
theme.foreground
})
.when(is_selected, |el| el.bg(theme.selection))
.hover(|s: gpui::StyleRefinement| s.bg(theme.selection))
.when(!is_disabled, |el| {
el.hover(|s: gpui::StyleRefinement| s.bg(theme.selection))
})
.child(item.label.clone());
if is_disabled {
@@ -198,13 +242,17 @@ impl Dropdown {
.absolute()
.top_full()
.left_0()
.right_0()
.min_w(gpui::rems(12.0))
.min_w_full()
.mt_1()
.occlude()
.p_1()
.border_1()
.border_color(theme.border)
.bg(theme.panel)
.bg(theme.background)
.rounded_md()
.overflow_hidden()
.shadow_lg()
.children(items)
.into_any_element()
}
@@ -216,28 +264,60 @@ impl gpui::Render for Dropdown {
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let theme = cx.theme();
let is_disabled = self.disabled || self.loading;
let label = self
let disabled = is_disabled || self.items.is_empty();
let mut label = self
.selected_label()
.unwrap_or_else(|| self.placeholder.clone());
let mut trigger = if let Some(button) = self.button.clone() {
let mut button = button;
button.on_click = None;
button.label = Some(label.clone());
button.disabled(is_disabled).loading(self.loading)
} else {
Button::label(self.id.clone(), label)
.disabled(is_disabled)
.loading(self.loading)
};
if is_disabled || self.items.is_empty() {
trigger = trigger.disabled(true);
if let Some(prefix) = &self.label_prefix {
label = format!("{}: {}", prefix, label).into();
}
let mut trigger_wrap = gpui::div().child(trigger);
if !is_disabled && !self.items.is_empty() {
let arrow = "";
let trigger: gpui::AnyElement = if let Some(button) = self.button.clone() {
let mut button = button;
button.on_click = None;
button.label = Some(format!("{} {}", label, arrow).into());
button
.disabled(disabled)
.loading(self.loading)
.bg(theme.background)
.border_color(theme.border)
.text_color(theme.foreground)
.into_any_element()
} else {
let mut trigger = gpui::div()
.flex()
.items_center()
.gap_2()
.px_3()
.py_2()
.rounded_md()
.border_1()
.border_color(theme.border)
.bg(theme.background)
.text_sm()
.text_color(theme.foreground)
.child(Label::new(label.clone()))
.child(Label::new(arrow).text_color(theme.muted));
if disabled {
trigger = trigger.text_color(theme.muted).cursor_not_allowed();
} else {
trigger = trigger
.cursor_pointer()
.hover(|s: gpui::StyleRefinement| s.bg(theme.selection));
}
trigger.into_any_element()
};
let mut trigger_wrap = gpui::div()
.min_w(gpui::rems(12.0))
.child(trigger);
if !disabled {
trigger_wrap = trigger_wrap.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(Self::handle_trigger_mouse_down),
+2 -1
View File
@@ -7,6 +7,7 @@ use gpui::{
use std::sync::Arc;
use crate::components::icon::{Icon, IconSize};
use crate::components::label::Label;
use crate::theme::ActiveTheme;
fn darken(color: gpui::Rgba, amount: f32) -> gpui::Rgba {
@@ -264,7 +265,7 @@ impl RenderOnce for Button {
}
if let Some(label) = self.label {
base = base.child(label);
base = base.child(Label::new(label).text_color(fg));
}
if self.loading {
+7 -2
View File
@@ -78,7 +78,8 @@ impl gpui::RenderOnce for Panel {
let children: Vec<gpui::AnyElement> = self.content.drain(..).collect();
gpui::div()
let mut base = gpui::div()
.relative()
.size_full()
.bg(theme.panel)
.border(gpui::px(self.border))
@@ -87,6 +88,10 @@ impl gpui::RenderOnce for Panel {
.p(gpui::px(self.padding))
.overflow_hidden()
.children(header)
.children(children)
.children(children);
base.style().refine(&self.style);
base
}
}
+73 -10
View File
@@ -1,5 +1,7 @@
use std::collections::HashSet;
use chrono::NaiveDate;
#[derive(Debug, Clone, Default)]
pub struct FilterState {
pub selected_project: Option<String>,
@@ -45,6 +47,17 @@ impl StatusFilter {
Self::Deleted,
]
}
pub fn from_index(index: usize) -> Self {
Self::all_variants().get(index).copied().unwrap_or_default()
}
pub fn to_index(&self) -> usize {
Self::all_variants()
.iter()
.position(|v| v == self)
.unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -76,6 +89,17 @@ impl PriorityFilter {
pub fn all_variants() -> &'static [Self] {
&[Self::All, Self::High, Self::Medium, Self::Low, Self::None]
}
pub fn from_index(index: usize) -> Self {
Self::all_variants().get(index).copied().unwrap_or_default()
}
pub fn to_index(&self) -> usize {
Self::all_variants()
.iter()
.position(|v| v == self)
.unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -85,6 +109,7 @@ pub enum DueFilter {
Today,
ThisWeek,
NoDate,
OnDate(NaiveDate),
}
impl Default for DueFilter {
@@ -94,16 +119,6 @@ impl Default for DueFilter {
}
impl DueFilter {
pub fn as_str(&self) -> &'static str {
match self {
Self::All => "All",
Self::Overdue => "Overdue",
Self::Today => "Today",
Self::ThisWeek => "This Week",
Self::NoDate => "No Date",
}
}
pub fn all_variants() -> &'static [Self] {
&[
Self::All,
@@ -113,6 +128,54 @@ impl DueFilter {
Self::NoDate,
]
}
pub fn from_index(index: usize) -> Self {
Self::all_variants().get(index).copied().unwrap_or_default()
}
pub fn to_index(&self) -> usize {
Self::all_variants()
.iter()
.position(|v| v == self)
.unwrap_or(0)
}
pub fn label(&self) -> String {
match self {
Self::All => "All".to_string(),
Self::Overdue => "Overdue".to_string(),
Self::Today => "Today".to_string(),
Self::ThisWeek => "This Week".to_string(),
Self::NoDate => "No Date".to_string(),
Self::OnDate(date) => date.format("%d-%m-%Y").to_string(),
}
}
pub fn value_key(&self) -> String {
match self {
Self::All => "all".to_string(),
Self::Overdue => "overdue".to_string(),
Self::Today => "today".to_string(),
Self::ThisWeek => "this_week".to_string(),
Self::NoDate => "none".to_string(),
Self::OnDate(date) => format!("date:{}", date.format("%Y-%m-%d")),
}
}
pub fn from_value(value: &str) -> Option<Self> {
match value {
"all" => Some(Self::All),
"overdue" => Some(Self::Overdue),
"today" => Some(Self::Today),
"this_week" => Some(Self::ThisWeek),
"none" => Some(Self::NoDate),
_ => value.strip_prefix("date:").and_then(|date| {
NaiveDate::parse_from_str(date, "%Y-%m-%d")
.ok()
.map(Self::OnDate)
}),
}
}
}
impl FilterState {
+8 -1
View File
@@ -1,6 +1,6 @@
use std::collections::HashSet;
use chrono::{DateTime, Utc};
use chrono::{DateTime, NaiveDate, Utc};
use super::model::{Task, TaskPriority, TaskStatus};
use crate::models::{DueFilter, FilterState, PriorityFilter, StatusFilter};
@@ -20,6 +20,7 @@ pub enum DueDateFilter {
NoDate,
Before(DateTime<Utc>),
After(DateTime<Utc>),
OnDate(NaiveDate),
}
#[derive(Debug, Clone, Default)]
@@ -112,6 +113,7 @@ impl From<&FilterState> for TaskFilter {
DueFilter::Today => Some(DueDateFilter::Today),
DueFilter::ThisWeek => Some(DueDateFilter::ThisWeek),
DueFilter::NoDate => Some(DueDateFilter::NoDate),
DueFilter::OnDate(date) => Some(DueDateFilter::OnDate(date)),
};
if !state.search_text.is_empty() {
@@ -228,6 +230,11 @@ impl TaskFilter {
return false;
}
}
DueDateFilter::OnDate(date) => {
if !task.due.map(|d| d.date_naive() == *date).unwrap_or(false) {
return false;
}
}
}
}
+61 -2
View File
@@ -71,6 +71,22 @@ impl Sidebar {
cx.notify();
}
fn handle_clear_tags(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.filter_state.update(cx, |filter, cx| {
filter.active_tags.clear();
cx.notify();
});
cx.notify();
}
fn handle_clear_project(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.filter_state.update(cx, |filter, cx| {
filter.selected_project = None;
cx.notify();
});
cx.notify();
}
fn render_projects(&self, cx: &mut Context<Self>) -> Vec<Div> {
let theme = cx.theme();
let filter = self.filter_state.read(cx);
@@ -259,6 +275,9 @@ impl Render for Sidebar {
let theme = cx.theme().clone();
let projects = self.render_projects(cx);
let tags = self.render_tags(cx);
let filter = self.filter_state.read(cx);
let has_project = filter.selected_project.is_some();
let has_tags = !filter.active_tags.is_empty();
Panel::new("Sidebar").border(1.0).padding(0.0).child(
div()
@@ -276,6 +295,9 @@ impl Render for Sidebar {
.child(
div()
.flex_shrink_0()
.flex()
.items_center()
.justify_between()
.px_3()
.py_2()
.border_b_1()
@@ -285,7 +307,24 @@ impl Render for Sidebar {
.text_sm()
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme.foreground),
),
)
.when(has_project, |this| {
this.child(
div()
.id("clear-project")
.text_xs()
.text_color(theme.muted)
.cursor_pointer()
.hover(|s| s.text_color(theme.accent))
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(|view, _, window, cx| {
view.handle_clear_project(window, cx);
}),
)
.child("Clear"),
)
}),
)
.child(
div()
@@ -308,6 +347,9 @@ impl Render for Sidebar {
.child(
div()
.flex_shrink_0()
.flex()
.items_center()
.justify_between()
.px_3()
.py_2()
.border_b_1()
@@ -317,7 +359,24 @@ impl Render for Sidebar {
.text_sm()
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme.foreground),
),
)
.when(has_tags, |this| {
this.child(
div()
.id("clear-tags")
.text_xs()
.text_color(theme.muted)
.cursor_pointer()
.hover(|s| s.text_color(theme.accent))
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(|view, _, window, cx| {
view.handle_clear_tags(window, cx);
}),
)
.child("Clear"),
)
}),
)
.child(
div()
+313 -18
View File
@@ -1,10 +1,16 @@
use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::sync::Arc;
use gpui::prelude::*;
use crate::{
components,
models::FilterState,
components::{
self,
button::{Dropdown, DropdownItem},
input::Input,
},
models::{DueFilter, FilterState, PriorityFilter, StatusFilter},
task::{self, TaskFilter, TaskService},
theme::{self, ActiveTheme},
};
@@ -216,14 +222,93 @@ pub struct TaskTable {
selected_page_idx: Option<usize>,
selected_global_idx: Option<usize>,
need_reload: bool,
filter_bar_height: gpui::Pixels,
search_input: gpui::Entity<Input>,
status_dropdown: gpui::Entity<Dropdown>,
priority_dropdown: gpui::Entity<Dropdown>,
due_dropdown: gpui::Entity<Dropdown>,
}
impl TaskTable {
pub fn new(
id: impl Into<gpui::ElementId>,
filter_state: gpui::Entity<FilterState>,
_cx: &mut gpui::Context<Self>,
cx: &mut gpui::Context<Self>,
) -> Self {
let search_input = {
let filter_state = filter_state.clone();
cx.new(|cx| {
Input::new("filter-search", cx, "Search...").with_on_change(Arc::new(
move |value: &str, cx: &mut gpui::Context<Input>| {
cx.update_entity(&filter_state, |filter, cx| {
filter.search_text = value.to_string();
cx.notify();
});
},
))
})
};
let status_items = StatusFilter::all_variants()
.iter()
.map(|status| DropdownItem::new(status.as_str()))
.collect::<Vec<_>>();
let status_dropdown = {
let filter_state = filter_state.clone();
cx.new(|_cx| {
Dropdown::new("filter-status")
.items(status_items)
.label_prefix("Status")
.selected_index(StatusFilter::default().to_index())
.on_select(Arc::new(move |index, _item, cx| {
let selected = StatusFilter::from_index(index);
cx.update_entity(&filter_state, |filter, cx| {
filter.status_filter = selected;
cx.notify();
});
}))
})
};
let priority_items = PriorityFilter::all_variants()
.iter()
.map(|priority| DropdownItem::new(priority.as_str()))
.collect::<Vec<_>>();
let priority_dropdown = {
let filter_state = filter_state.clone();
cx.new(|_cx| {
Dropdown::new("filter-priority")
.items(priority_items)
.label_prefix("Priority")
.selected_index(PriorityFilter::default().to_index())
.on_select(Arc::new(move |index, _item, cx| {
let selected = PriorityFilter::from_index(index);
cx.update_entity(&filter_state, |filter, cx| {
filter.priority_filter = selected;
cx.notify();
});
}))
})
};
let due_dropdown = {
let filter_state = filter_state.clone();
cx.new(|_cx| {
Dropdown::new("filter-due")
.items(vec![DropdownItem::with_value("All", "all")])
.label_prefix("Due")
.selected_index(0)
.on_select(Arc::new(move |_index, item, cx| {
let selected =
DueFilter::from_value(item.value.as_ref()).unwrap_or(DueFilter::All);
cx.update_entity(&filter_state, |filter, cx| {
filter.due_filter = selected;
cx.notify();
});
}))
})
};
Self {
id: id.into(),
filter_state,
@@ -234,6 +319,11 @@ impl TaskTable {
selected_page_idx: None,
selected_global_idx: None,
need_reload: true,
filter_bar_height: gpui::px(52.0),
search_input,
status_dropdown,
priority_dropdown,
due_dropdown,
}
}
@@ -297,14 +387,17 @@ impl TaskTable {
pub fn reload_tasks(&mut self, task_service: &mut TaskService, cx: &mut gpui::Context<Self>) {
let filter_state = self.filter_state.read(cx).clone();
let task_filter = TaskFilter::from(&filter_state);
let all_tasks = task_service.get_all_tasks().unwrap_or_else(|e| {
log::error!("[TaskTable] Failed to load tasks: {}", e);
vec![]
});
let filtered_tasks = task_service
.get_filtered_tasks(&task_filter)
.unwrap_or_else(|e| {
log::error!("[TaskTable] Failed to load filtered tasks: {}", e);
vec![]
});
let task_filter = TaskFilter::from(&filter_state);
let mut due_filter = task_filter.clone();
due_filter.due_filter = None;
let filtered_tasks = task_filter.apply(&all_tasks);
let due_tasks = due_filter.apply(&all_tasks);
self.cached_tasks = filtered_tasks;
self.apply_sort();
@@ -314,6 +407,7 @@ impl TaskTable {
self.selected_page_idx = None;
self.recalculate_rows();
self.sync_filter_dropdowns(&due_tasks, &filter_state, cx);
self.need_reload = false;
@@ -324,6 +418,121 @@ impl TaskTable {
self.cached_rows = self.cached_tasks.iter().map(TaskRow::from).collect();
}
fn sync_filter_dropdowns(
&mut self,
due_tasks: &[task::Task],
filter_state: &FilterState,
cx: &mut gpui::Context<Self>,
) {
let status_index = filter_state.status_filter.to_index();
self.status_dropdown.update(cx, |dropdown, cx| {
dropdown.set_selected_index(status_index, cx);
});
let priority_index = filter_state.priority_filter.to_index();
self.priority_dropdown.update(cx, |dropdown, cx| {
dropdown.set_selected_index(priority_index, cx);
});
let mut due_items = Self::build_due_items(due_tasks);
let selected_key = filter_state.due_filter.value_key();
let mut selected_index = due_items
.iter()
.position(|item| item.value.as_ref() == selected_key);
if selected_index.is_none() && filter_state.due_filter != DueFilter::All {
if let Some(item) = Self::due_item_from_filter(&filter_state.due_filter) {
due_items.push(item);
selected_index = Some(due_items.len() - 1);
}
}
self.due_dropdown.update(cx, |dropdown, cx| {
dropdown.set_items(due_items, cx);
if let Some(index) = selected_index {
dropdown.set_selected_index(index, cx);
}
});
}
fn build_due_items(tasks: &[task::Task]) -> Vec<DropdownItem> {
let now = chrono::Utc::now();
let today = now.date_naive();
let week_end = now + chrono::Duration::days(7);
let mut dates = BTreeSet::new();
let mut has_no_date = false;
let mut has_overdue = false;
let mut has_today = false;
let mut has_this_week = false;
for task in tasks {
match task.due {
None => {
has_no_date = true;
}
Some(due) => {
let date = due.date_naive();
dates.insert(date);
if due < now {
has_overdue = true;
}
if date == today {
has_today = true;
}
if due >= now && due <= week_end {
has_this_week = true;
}
}
}
}
let mut items = vec![DropdownItem::with_value("All", "all")];
if has_no_date {
items.push(DropdownItem::with_value("No Date", "none"));
}
if has_overdue {
items.push(DropdownItem::with_value("Overdue", "overdue"));
}
if has_today {
items.push(DropdownItem::with_value("Today", "today"));
}
if has_this_week {
items.push(DropdownItem::with_value("This Week", "this_week"));
}
for date in dates {
if date == today {
continue;
}
let label = Self::format_due_label(date);
let value = format!("date:{}", date.format("%Y-%m-%d"));
items.push(DropdownItem::with_value(label, value));
}
items
}
fn due_item_from_filter(filter: &DueFilter) -> Option<DropdownItem> {
match filter {
DueFilter::All => None,
DueFilter::OnDate(date) => Some(DropdownItem::with_value(
Self::format_due_label(*date),
filter.value_key(),
)),
_ => Some(DropdownItem::with_value(filter.label(), filter.value_key())),
}
}
fn format_due_label(date: chrono::NaiveDate) -> String {
let today = chrono::Utc::now().date_naive();
if date == today {
"Today".to_string()
} else {
date.format("%d-%m-%Y").to_string()
}
}
fn priority_color(&self, row: &TaskRow, cx: &gpui::Context<Self>) -> theme::Color {
let theme = cx.theme();
@@ -376,6 +585,74 @@ impl TaskTable {
cx.notify();
}
fn handle_clear_filters(&mut self, cx: &mut gpui::Context<Self>) {
self.search_input.update(cx, |input, cx| {
input.clear(cx);
});
self.filter_state.update(cx, |filter, cx| {
filter.clear();
cx.notify();
});
}
fn render_filter_bar(&self, cx: &gpui::Context<Self>) -> impl gpui::IntoElement {
let theme = cx.theme();
let filter = self.filter_state.read(cx);
let has_filters = filter.has_active_filters();
let view = cx.entity().clone();
let bar = gpui::div()
.id("filter-bar")
.flex()
.flex_wrap()
.gap_2()
.items_center()
.px_4()
.py_2()
.bg(theme.panel)
.child(
gpui::div()
.w(gpui::rems(14.0))
.child(self.search_input.clone()),
)
.child(self.status_dropdown.clone())
.child(self.priority_dropdown.clone())
.child(self.due_dropdown.clone())
.when(has_filters, |this| {
this.child(
gpui::div()
.id("clear-all-filters")
.px_2()
.py_1()
.rounded_md()
.text_sm()
.text_color(theme.error)
.cursor_pointer()
.hover(|s| s.bg(theme.selection))
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(|table, _, _, cx| {
table.handle_clear_filters(cx);
}),
)
.child("✕ Clear"),
)
});
gpui::div().child(bar).on_children_prepainted(move |bounds, _, cx| {
let Some(bounds) = bounds.first() else {
return;
};
let height = bounds.size.height;
cx.update_entity(&view, |table, cx| {
if table.filter_bar_height != height {
table.filter_bar_height = height;
cx.notify();
}
});
})
}
fn render_header_column(
&self,
column: SortColumn,
@@ -424,9 +701,10 @@ impl TaskTable {
.items_center()
.gap_2()
.px_4()
.py_1()
.border_b_1()
.py_2()
.border_t_1()
.border_color(theme.border)
.border_b_1()
.bg(theme.panel)
.text_sm()
.font_weight(gpui::FontWeight::MEDIUM)
@@ -632,12 +910,12 @@ impl TaskTable {
impl gpui::Render for TaskTable {
fn render(
&mut self,
window: &mut gpui::Window,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let theme = cx.theme();
let panel = components::panel::Panel::new(self.id.clone());
let panel = components::panel::Panel::new(self.id.clone()).flex().flex_col();
if self.need_reload {
return panel
@@ -656,13 +934,19 @@ impl gpui::Render for TaskTable {
.map(|(index, row)| self.render_row(index, row, cx))
.collect();
panel
let filter_bar = self.render_filter_bar(cx);
let header = self.render_header(cx);
let footer = self.render_footer(cx);
let body = gpui::div()
.flex()
.flex_col()
.size_full()
.flex_1()
.min_h_0()
.overflow_hidden()
.bg(theme.background)
.child(self.render_header(cx))
.child(gpui::div().h(self.filter_bar_height))
.child(header)
.child(
gpui::div()
.id("task-table-content")
@@ -671,6 +955,17 @@ impl gpui::Render for TaskTable {
.overflow_y_scroll()
.child(gpui::div().flex().flex_col().children(rows)),
)
.child(self.render_footer(cx))
.child(footer);
panel
.child(body)
.child(
gpui::div()
.absolute()
.top_0()
.left_0()
.right_0()
.child(filter_bar),
)
}
}