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:
+313
-18
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user