feat: Add TaskTable with sorting and pagination
Add a new TaskTable component that displays tasks in a sortable, paginated table. Columns include ID, Description, Project, Due, Priority, and Status. Clicking column headers sorts the data (toggles between ascending and descending). Priority and status are color-coded based on theme colors. Update Panel component to accept an ID and simplify child rendering. Add warning, info, and priority colors (high/medium/low) to Theme. Add Into<usize> and Into<String> implementations for TaskPriority and TaskStatus to support sorting and display.
This commit is contained in:
+22
-11
@@ -5,12 +5,14 @@ use crate::task::{TaskOverview, TaskService};
|
||||
use crate::theme::ActiveTheme;
|
||||
use crate::view::sidebar::{Sidebar, TagItem};
|
||||
use crate::view::status_bar::StatusBar;
|
||||
use crate::view::task_table::TaskTable;
|
||||
use gpui::div;
|
||||
|
||||
pub(crate) struct App {
|
||||
sidebar: gpui::Entity<Sidebar>,
|
||||
filter_state: gpui::Entity<FilterState>,
|
||||
status_bar: gpui::Entity<StatusBar>,
|
||||
task_table: gpui::Entity<TaskTable>,
|
||||
task_service: TaskService,
|
||||
}
|
||||
|
||||
@@ -25,19 +27,21 @@ impl gpui::Render for App {
|
||||
let content = div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.child(div().w(gpui::px(250.)).h_full().child(self.sidebar.clone()))
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div()
|
||||
.w(gpui::px(250.))
|
||||
.h_full()
|
||||
.flex_shrink_0()
|
||||
.child(self.sidebar.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.text_color(theme.muted)
|
||||
.child("Main panel - TaskTable will go here"),
|
||||
),
|
||||
.min_w_0()
|
||||
.child(self.task_table.clone()),
|
||||
);
|
||||
|
||||
div()
|
||||
@@ -88,14 +92,21 @@ impl App {
|
||||
|
||||
let status_bar = cx.new(|cx| StatusBar::new(cx));
|
||||
|
||||
let sidebar = cx.new(|cx| {
|
||||
Sidebar::new(project_tree, tags, filter_state.clone(), cx)
|
||||
let sidebar =
|
||||
cx.new(|cx| Sidebar::new(project_tree, tags, filter_state.clone(), cx));
|
||||
|
||||
let task_table = cx
|
||||
.new(|cx| TaskTable::new("main-task-table", filter_state.clone(), cx));
|
||||
|
||||
task_table.update(cx, |table, cx| {
|
||||
table.reload_tasks(&mut task_service, cx);
|
||||
});
|
||||
|
||||
App {
|
||||
sidebar,
|
||||
filter_state,
|
||||
status_bar,
|
||||
task_table,
|
||||
task_service,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -24,12 +24,9 @@ impl gpui::Styled for Label {
|
||||
}
|
||||
|
||||
impl RenderOnce for Label {
|
||||
fn render(mut self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
let mut div = gpui::div()
|
||||
.line_height(gpui::rems(1.25))
|
||||
.text_color(theme.foreground)
|
||||
.child(gpui::StyledText::new(&self.text));
|
||||
|
||||
*div.style() = self.style;
|
||||
|
||||
+6
-17
@@ -4,6 +4,7 @@ use crate::theme::ActiveTheme;
|
||||
|
||||
#[derive(gpui::IntoElement)]
|
||||
pub struct Panel {
|
||||
id: gpui::ElementId,
|
||||
content: Vec<gpui::AnyElement>,
|
||||
title: Option<String>,
|
||||
border: f32,
|
||||
@@ -12,12 +13,13 @@ pub struct Panel {
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(id: impl Into<gpui::ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
content: Vec::new(),
|
||||
title: None,
|
||||
border: 1.0,
|
||||
padding: 8.0,
|
||||
border: 0.0,
|
||||
padding: 0.0,
|
||||
style: gpui::StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
@@ -74,20 +76,7 @@ impl gpui::RenderOnce for Panel {
|
||||
.into_any_element()
|
||||
});
|
||||
|
||||
let children: Vec<gpui::AnyElement> = self
|
||||
.content
|
||||
.drain(..)
|
||||
.enumerate()
|
||||
.map(|(ix, c)| {
|
||||
gpui::div()
|
||||
.id(ix)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
.child(c)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect();
|
||||
let children: Vec<gpui::AnyElement> = self.content.drain(..).collect();
|
||||
|
||||
let mut div = gpui::div()
|
||||
.size_full()
|
||||
|
||||
@@ -10,6 +10,17 @@ pub enum TaskPriority {
|
||||
None,
|
||||
}
|
||||
|
||||
impl Into<usize> for TaskPriority {
|
||||
fn into(self) -> usize {
|
||||
match self {
|
||||
TaskPriority::High => 0,
|
||||
TaskPriority::Medium => 1,
|
||||
TaskPriority::Low => 2,
|
||||
TaskPriority::None => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TaskPriority {
|
||||
fn default() -> Self {
|
||||
TaskPriority::None
|
||||
@@ -27,6 +38,17 @@ impl std::fmt::Display for TaskPriority {
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<String> for TaskPriority {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
TaskPriority::High => "High".to_string(),
|
||||
TaskPriority::Medium => "Medium".to_string(),
|
||||
TaskPriority::Low => "Low".to_string(),
|
||||
TaskPriority::None => "None".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TaskPriority {
|
||||
fn from(s: &str) -> Self {
|
||||
match s {
|
||||
@@ -59,6 +81,18 @@ impl Default for TaskStatus {
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<String> for TaskStatus {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
TaskStatus::Pending => "Pending".to_string(),
|
||||
TaskStatus::Completed => "Completed".to_string(),
|
||||
TaskStatus::Deleted => "Deleted".to_string(),
|
||||
TaskStatus::Unknown(reason) => format!("Unknown({})", reason),
|
||||
TaskStatus::Recurring => "Recurring".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TaskStatus {
|
||||
fn from(s: &str) -> Self {
|
||||
match s {
|
||||
@@ -119,6 +153,7 @@ impl From<taskchampion::Annotation> for TaskAnnotation {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Task {
|
||||
pub uuid: uuid::Uuid,
|
||||
pub id: Option<usize>,
|
||||
pub status: TaskStatus,
|
||||
pub description: String,
|
||||
pub project: Option<String>,
|
||||
@@ -138,6 +173,7 @@ pub struct Task {
|
||||
impl Task {
|
||||
pub fn new(
|
||||
uuid: uuid::Uuid,
|
||||
id: Option<usize>,
|
||||
status: TaskStatus,
|
||||
description: String,
|
||||
project: Option<String>,
|
||||
@@ -168,6 +204,7 @@ impl Task {
|
||||
dependencies,
|
||||
is_active,
|
||||
is_blocked,
|
||||
id,
|
||||
working_id,
|
||||
}
|
||||
}
|
||||
@@ -186,6 +223,7 @@ impl From<taskchampion::Task> for Task {
|
||||
fn from(task: taskchampion::Task) -> Self {
|
||||
Self {
|
||||
uuid: task.get_uuid(),
|
||||
id: task.get_value("ID").map(|value| value.parse().unwrap()),
|
||||
status: task.get_status().into(),
|
||||
description: task.get_description().to_string(),
|
||||
project: task.get_value("project").map(|v| v.to_string()),
|
||||
|
||||
@@ -10,10 +10,16 @@ pub struct Theme {
|
||||
pub border: Color,
|
||||
pub error: Color,
|
||||
pub success: Color,
|
||||
pub warning: Color,
|
||||
pub info: Color,
|
||||
pub selection: Color,
|
||||
pub selection_foreground: Color,
|
||||
pub text: Color,
|
||||
pub text_size: Option<gpui::Size<u32>>,
|
||||
|
||||
pub high: Color,
|
||||
pub medium: Color,
|
||||
pub low: Color,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
@@ -27,10 +33,16 @@ impl Theme {
|
||||
border: gpui::rgb(0x313244),
|
||||
error: gpui::rgb(0xF38BA8),
|
||||
success: gpui::rgb(0xA6E3A1),
|
||||
warning: gpui::rgb(0xF9E2AF),
|
||||
info: gpui::rgb(0x74C7EC),
|
||||
selection: gpui::rgb(0x45475A),
|
||||
selection_foreground: gpui::rgb(0xCDD6F4),
|
||||
text: gpui::rgb(0xCDD6F4),
|
||||
text_size: Some(gpui::Size::new(14, 14)),
|
||||
|
||||
high: gpui::rgb(0xF38BA8),
|
||||
medium: gpui::rgb(0xF9E2AF),
|
||||
low: gpui::rgb(0xA6E3A1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +56,15 @@ impl Theme {
|
||||
border: gpui::rgb(0xE0E0E0),
|
||||
error: gpui::rgb(0xFF4444),
|
||||
success: gpui::rgb(0x4CAF50),
|
||||
warning: gpui::rgb(0xFFA726),
|
||||
info: gpui::rgb(0x29B6F6),
|
||||
selection: gpui::rgb(0xD0D0D0),
|
||||
selection_foreground: gpui::rgb(0x333333),
|
||||
text: gpui::rgb(0x333333),
|
||||
text_size: Some(gpui::Size::new(14, 14)),
|
||||
high: gpui::rgb(0xF38BA8),
|
||||
medium: gpui::rgb(0xF9E2AF),
|
||||
low: gpui::rgb(0xA6E3A1),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod sidebar;
|
||||
pub mod status_bar;
|
||||
pub mod task_table;
|
||||
|
||||
+3
-11
@@ -1,8 +1,4 @@
|
||||
use crate::components::{
|
||||
icon::{Icon, IconName, IconSize},
|
||||
label::Label,
|
||||
panel::Panel,
|
||||
};
|
||||
use crate::components::{label::Label, panel::Panel};
|
||||
use crate::models::{FilterState, ProjectTree};
|
||||
use crate::theme::ActiveTheme;
|
||||
use gpui::{Context, Div, Entity, IntoElement, Window, div, prelude::*, px};
|
||||
@@ -284,7 +280,7 @@ impl Render for Sidebar {
|
||||
let projects = self.render_projects(cx);
|
||||
let tags = self.render_tags(cx);
|
||||
|
||||
Panel::new().border(1.0).padding(0.0).child(
|
||||
Panel::new("Sidebar").border(1.0).padding(0.0).child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -322,11 +318,7 @@ impl Render for Sidebar {
|
||||
.children(projects),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h_px()
|
||||
.bg(theme.border),
|
||||
)
|
||||
.child(div().h_px().bg(theme.border))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
|
||||
@@ -0,0 +1,686 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use gpui::prelude::*;
|
||||
|
||||
use crate::{
|
||||
components,
|
||||
models::FilterState,
|
||||
task::{self, TaskFilter, TaskService},
|
||||
theme::{self, ActiveTheme},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SortColumn {
|
||||
Id,
|
||||
Description,
|
||||
Project,
|
||||
Due,
|
||||
Priority,
|
||||
Status,
|
||||
}
|
||||
|
||||
impl SortColumn {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
SortColumn::Id => "ID",
|
||||
SortColumn::Description => "Description",
|
||||
SortColumn::Project => "Project",
|
||||
SortColumn::Due => "Due",
|
||||
SortColumn::Priority => "Priority",
|
||||
SortColumn::Status => "Status",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SortDirection {
|
||||
Asc,
|
||||
Desc,
|
||||
}
|
||||
|
||||
impl SortDirection {
|
||||
pub fn toggle(&self) -> Self {
|
||||
match self {
|
||||
SortDirection::Asc => SortDirection::Desc,
|
||||
SortDirection::Desc => SortDirection::Asc,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arrow(&self) -> &'static str {
|
||||
match self {
|
||||
SortDirection::Asc => "↑",
|
||||
SortDirection::Desc => "↓",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SortDirection {
|
||||
fn default() -> Self {
|
||||
Self::Desc
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SortState {
|
||||
pub column: SortColumn,
|
||||
pub direction: SortDirection,
|
||||
}
|
||||
|
||||
impl Default for SortState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
column: SortColumn::Priority,
|
||||
direction: SortDirection::Desc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PaginationState {
|
||||
current_page: usize,
|
||||
page_size: usize,
|
||||
total_items: usize,
|
||||
}
|
||||
|
||||
impl Default for PaginationState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current_page: 1,
|
||||
page_size: 20,
|
||||
total_items: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaginationState {
|
||||
pub fn new(current_page: usize, page_size: usize, total_items: usize) -> Self {
|
||||
Self {
|
||||
current_page,
|
||||
page_size,
|
||||
total_items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_items(&mut self, total_items: usize) {
|
||||
self.total_items = total_items;
|
||||
}
|
||||
|
||||
pub fn page_size(&mut self, page_size: usize) {
|
||||
self.page_size = page_size;
|
||||
}
|
||||
|
||||
pub fn current_page(&mut self, current_page: usize) {
|
||||
self.current_page = current_page;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn can_next(&self) -> bool {
|
||||
self.current_page < self.total_items / self.page_size + 1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn can_previous(&self) -> bool {
|
||||
self.current_page > 1
|
||||
}
|
||||
|
||||
pub fn next_page(&mut self) {
|
||||
if self.current_page < self.total_items / self.page_size + 1 {
|
||||
self.current_page += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_page(&mut self) {
|
||||
if self.current_page > 1 {
|
||||
self.current_page -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn first_item_index(&self) -> usize {
|
||||
(self.current_page - 1) * self.page_size
|
||||
}
|
||||
|
||||
pub fn last_item_index(&self) -> usize {
|
||||
(self.first_item_index() + self.page_size).min(self.total_items)
|
||||
}
|
||||
|
||||
pub fn last_item_display(&self) -> usize {
|
||||
self.last_item_index()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TaskRow {
|
||||
pub uuid: uuid::Uuid,
|
||||
pub id_display: String,
|
||||
pub description: String,
|
||||
pub project: String,
|
||||
pub due: String,
|
||||
pub priority: String,
|
||||
pub status: String,
|
||||
pub is_due_today: bool,
|
||||
pub is_overdue: bool,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
impl TaskRow {
|
||||
fn truncate(desc: &str, max_len: usize) -> String {
|
||||
if desc.len() <= max_len {
|
||||
desc.to_string()
|
||||
} else {
|
||||
format!("{}...", &desc[..max_len - 3])
|
||||
}
|
||||
}
|
||||
|
||||
fn format_date(due: &Option<chrono::DateTime<chrono::Utc>>, is_today: bool) -> String {
|
||||
match due {
|
||||
None => "-".to_string(),
|
||||
Some(dt) => {
|
||||
if is_today {
|
||||
"Today".to_string()
|
||||
} else {
|
||||
dt.format("%d-%m-%Y").to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&task::Task> for TaskRow {
|
||||
fn from(value: &task::Task) -> Self {
|
||||
let status = if value.is_active {
|
||||
"Active".to_string()
|
||||
} else {
|
||||
value.status.clone().into()
|
||||
};
|
||||
|
||||
Self {
|
||||
uuid: value.uuid,
|
||||
id_display: value.working_id.unwrap_or(0).to_string(),
|
||||
description: Self::truncate(&value.description, 50),
|
||||
project: value.project.clone().unwrap_or(String::new()),
|
||||
due: Self::format_date(&value.due, value.is_due_today()),
|
||||
priority: value.priority.into(),
|
||||
status,
|
||||
is_due_today: value.is_due_today(),
|
||||
is_overdue: value.is_overdue(),
|
||||
is_active: value.is_active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TaskTable {
|
||||
id: gpui::ElementId,
|
||||
filter_state: gpui::Entity<FilterState>,
|
||||
cached_tasks: Vec<task::Task>,
|
||||
cached_rows: Vec<TaskRow>,
|
||||
sort_state: SortState,
|
||||
pagination: PaginationState,
|
||||
selected_page_idx: Option<usize>,
|
||||
selected_global_idx: Option<usize>,
|
||||
need_reload: bool,
|
||||
}
|
||||
|
||||
impl TaskTable {
|
||||
pub fn new(
|
||||
id: impl Into<gpui::ElementId>,
|
||||
filter_state: gpui::Entity<FilterState>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&filter_state, |table, _filter, cx| {
|
||||
table.need_reload = true;
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
filter_state,
|
||||
cached_tasks: vec![],
|
||||
cached_rows: vec![],
|
||||
sort_state: SortState::default(),
|
||||
pagination: PaginationState::default(),
|
||||
selected_page_idx: None,
|
||||
selected_global_idx: None,
|
||||
need_reload: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_sort(&mut self, column: SortColumn, cx: &mut gpui::Context<Self>) {
|
||||
if self.sort_state.column == column {
|
||||
self.sort_state.direction = self.sort_state.direction.toggle();
|
||||
} else {
|
||||
self.sort_state.column = column;
|
||||
self.sort_state.direction = SortDirection::Desc;
|
||||
}
|
||||
self.apply_sort();
|
||||
self.recalculate_rows();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn apply_sort(&mut self) {
|
||||
let direction = self.sort_state.direction;
|
||||
let column = self.sort_state.column;
|
||||
|
||||
self.cached_tasks.sort_by(|a, b| {
|
||||
let ordering = match column {
|
||||
SortColumn::Id => a.working_id.unwrap_or(0).cmp(&b.working_id.unwrap_or(0)),
|
||||
SortColumn::Description => a.description.cmp(&b.description),
|
||||
SortColumn::Project => {
|
||||
let a_proj = a.project.as_deref().unwrap_or("");
|
||||
let b_proj = b.project.as_deref().unwrap_or("");
|
||||
a_proj.cmp(b_proj)
|
||||
}
|
||||
SortColumn::Due => match (&a.due, &b.due) {
|
||||
(Some(a_due), Some(b_due)) => a_due.cmp(b_due),
|
||||
(Some(_), None) => Ordering::Less,
|
||||
(None, Some(_)) => Ordering::Greater,
|
||||
(None, None) => Ordering::Equal,
|
||||
},
|
||||
SortColumn::Priority => {
|
||||
let a_order: usize = a.priority.into();
|
||||
let b_order: usize = b.priority.into();
|
||||
a_order.cmp(&b_order)
|
||||
}
|
||||
SortColumn::Status => {
|
||||
let a_status: String = a.status.clone().into();
|
||||
let b_status: String = b.status.clone().into();
|
||||
a_status.cmp(&b_status)
|
||||
}
|
||||
};
|
||||
|
||||
match direction {
|
||||
SortDirection::Asc => ordering,
|
||||
SortDirection::Desc => ordering.reverse(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn get_current_page_rows(&self) -> &[TaskRow] {
|
||||
let start = self.pagination.first_item_index();
|
||||
let end = self.pagination.last_item_index();
|
||||
|
||||
&self.cached_rows[start..end]
|
||||
}
|
||||
|
||||
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 filtered_tasks = task_service
|
||||
.get_filtered_tasks(&task_filter)
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("[TaskTable] Failed to load filtered tasks: {}", e);
|
||||
vec![]
|
||||
});
|
||||
|
||||
self.cached_tasks = filtered_tasks;
|
||||
self.apply_sort();
|
||||
self.pagination.total_items(self.cached_tasks.len());
|
||||
|
||||
if let Some(idx) = self.selected_global_idx {
|
||||
if idx >= self.cached_tasks.len() {
|
||||
self.selected_global_idx = None;
|
||||
self.selected_page_idx = None;
|
||||
}
|
||||
}
|
||||
|
||||
self.recalculate_rows();
|
||||
|
||||
self.need_reload = false;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn recalculate_rows(&mut self) {
|
||||
self.cached_rows = self.cached_tasks.iter().map(TaskRow::from).collect();
|
||||
}
|
||||
|
||||
fn priority_color(&self, row: &TaskRow, cx: &gpui::Context<Self>) -> theme::Color {
|
||||
let theme = cx.theme();
|
||||
|
||||
match row.priority.as_str() {
|
||||
"High" => theme.high,
|
||||
"Medium" => theme.medium,
|
||||
"Low" => theme.low,
|
||||
_ => theme.foreground,
|
||||
}
|
||||
}
|
||||
|
||||
fn due_color(&self, row: &TaskRow, cx: &gpui::Context<Self>) -> theme::Color {
|
||||
let theme = cx.theme();
|
||||
|
||||
if row.is_due_today {
|
||||
theme.accent
|
||||
} else if row.is_overdue {
|
||||
theme.error
|
||||
} else {
|
||||
theme.foreground
|
||||
}
|
||||
}
|
||||
|
||||
fn status_color(&self, row: &TaskRow, cx: &gpui::Context<Self>) -> theme::Color {
|
||||
let theme = cx.theme();
|
||||
|
||||
match row.status.as_str() {
|
||||
"Active" => theme.success,
|
||||
"Pending" => theme.warning,
|
||||
"Completed" => theme.muted,
|
||||
"Deleted" => theme.error,
|
||||
"Recurring" => theme.info,
|
||||
_ => theme.muted,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_row(&mut self, idx: usize, cx: &mut gpui::Context<Self>) {
|
||||
self.selected_page_idx = Some(idx);
|
||||
self.selected_global_idx = Some(self.pagination.first_item_index() + idx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn go_previous_page(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
self.pagination.previous_page();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn go_next_page(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
self.pagination.next_page();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_header_column(
|
||||
&self,
|
||||
column: SortColumn,
|
||||
id: &'static str,
|
||||
cx: &gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let theme = cx.theme();
|
||||
let is_sorted = self.sort_state.column == column;
|
||||
let arrow = if is_sorted {
|
||||
self.sort_state.direction.arrow()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
gpui::div()
|
||||
.id(id)
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.cursor_pointer()
|
||||
.hover(|s| s.text_color(theme.foreground))
|
||||
.on_mouse_down(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(move |table, _, _, cx| {
|
||||
table.set_sort(column, cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
components::label::Label::new(column.label()).text_color(if is_sorted {
|
||||
theme.accent
|
||||
} else {
|
||||
theme.muted
|
||||
}),
|
||||
)
|
||||
.when(!arrow.is_empty(), |div| {
|
||||
div.child(components::label::Label::new(arrow).text_color(theme.accent))
|
||||
})
|
||||
}
|
||||
|
||||
fn render_header(&self, cx: &gpui::Context<Self>) -> gpui::Div {
|
||||
let theme = cx.theme();
|
||||
|
||||
gpui::div()
|
||||
.flex()
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_4()
|
||||
.py_1()
|
||||
.border_b_1()
|
||||
.border_color(theme.border)
|
||||
.bg(theme.panel)
|
||||
.text_sm()
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.child(
|
||||
gpui::div()
|
||||
.min_w(gpui::rems(3.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(components::label::Label::new(" ").text_color(theme.muted))
|
||||
.child(self.render_header_column(SortColumn::Id, "header-id", cx)),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.flex_1()
|
||||
.min_w(gpui::rems(10.0))
|
||||
.child(self.render_header_column(SortColumn::Description, "header-desc", cx)),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.w(gpui::rems(10.0))
|
||||
.child(self.render_header_column(SortColumn::Project, "header-project", cx)),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.w(gpui::rems(7.0))
|
||||
.child(self.render_header_column(SortColumn::Due, "header-due", cx)),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.w(gpui::rems(5.0))
|
||||
.child(self.render_header_column(SortColumn::Priority, "header-priority", cx)),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.w(gpui::rems(6.0))
|
||||
.child(self.render_header_column(SortColumn::Status, "header-status", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_row(&self, idx: usize, row: &TaskRow, cx: &gpui::Context<Self>) -> gpui::Div {
|
||||
let theme = cx.theme();
|
||||
let selected = self.selected_page_idx == Some(idx);
|
||||
|
||||
gpui::div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_4()
|
||||
.py_1()
|
||||
.border_b_1()
|
||||
.border_color(theme.border)
|
||||
.text_color(theme.foreground)
|
||||
.when(selected, |d| {
|
||||
d.bg(theme.selection).text_color(theme.selection_foreground)
|
||||
})
|
||||
.when(!selected, |d| d.hover(|s| s.bg(theme.panel)))
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(move |table, _, _, cx| table.select_row(idx, cx)),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.min_w(gpui::rems(3.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
components::label::Label::new(if selected { ">" } else { " " })
|
||||
.text_color(theme.accent),
|
||||
)
|
||||
.child(components::label::Label::new(row.id_display.clone())),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.flex_1()
|
||||
.min_w(gpui::rems(10.0))
|
||||
.overflow_x_hidden()
|
||||
.child(
|
||||
components::label::Label::new(row.description.clone())
|
||||
.text_ellipsis()
|
||||
.whitespace_nowrap(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
gpui::div().w(gpui::rems(10.0)).overflow_x_hidden().child(
|
||||
components::label::Label::new(row.project.clone())
|
||||
.text_color(theme.muted)
|
||||
.text_ellipsis()
|
||||
.whitespace_nowrap(),
|
||||
),
|
||||
)
|
||||
.child(gpui::div().w(gpui::rems(7.0)).child(
|
||||
components::label::Label::new(row.due.clone()).text_color(self.due_color(row, cx)),
|
||||
))
|
||||
.child(
|
||||
gpui::div().w(gpui::rems(5.0)).child(
|
||||
components::label::Label::new(row.priority.clone())
|
||||
.text_color(self.priority_color(row, cx))
|
||||
.font_weight(gpui::FontWeight::BOLD),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
gpui::div().w(gpui::rems(6.0)).child(
|
||||
components::label::Label::new(row.status.clone())
|
||||
.text_color(self.status_color(row, cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_footer(&self, cx: &gpui::Context<Self>) -> gpui::Div {
|
||||
let theme = cx.theme();
|
||||
let can_prev = self.pagination.can_previous();
|
||||
let can_next = self.pagination.can_next();
|
||||
let pages = (self.pagination.total_items + self.pagination.page_size - 1)
|
||||
/ self.pagination.page_size.max(1);
|
||||
|
||||
gpui::div()
|
||||
.flex()
|
||||
.flex_shrink_0()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.border_t_1()
|
||||
.border_color(theme.border)
|
||||
.bg(theme.panel)
|
||||
.text_sm()
|
||||
.child(
|
||||
components::label::Label::new(format!(
|
||||
"Showing {}-{} of {}",
|
||||
self.pagination.first_item_index() + 1,
|
||||
self.pagination.last_item_display(),
|
||||
self.pagination.total_items
|
||||
))
|
||||
.text_color(theme.muted),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
components::label::Label::new(format!(
|
||||
"Page {} of {}",
|
||||
self.pagination.current_page,
|
||||
pages.max(1)
|
||||
))
|
||||
.text_color(theme.muted),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
gpui::div()
|
||||
.id("prev-btn")
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded_md()
|
||||
.text_color(if can_prev {
|
||||
theme.foreground
|
||||
} else {
|
||||
theme.muted
|
||||
})
|
||||
.when(can_prev, |d| {
|
||||
d.cursor_pointer().hover(|s| s.bg(theme.selection))
|
||||
})
|
||||
.when(!can_prev, |d| d.cursor_not_allowed())
|
||||
.on_mouse_down(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(|table, _, _, cx| table.go_previous_page(cx)),
|
||||
)
|
||||
.child(components::label::Label::new("← Prev")),
|
||||
)
|
||||
.child(
|
||||
gpui::div()
|
||||
.id("next-btn")
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded_md()
|
||||
.text_color(if can_next {
|
||||
theme.foreground
|
||||
} else {
|
||||
theme.muted
|
||||
})
|
||||
.when(can_next, |d| {
|
||||
d.cursor_pointer().hover(|s| s.bg(theme.selection))
|
||||
})
|
||||
.when(!can_next, |d| d.cursor_not_allowed())
|
||||
.on_mouse_down(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(|table, _, _, cx| table.go_next_page(cx)),
|
||||
)
|
||||
.child(components::label::Label::new("Next →")),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Render for TaskTable {
|
||||
fn render(
|
||||
&mut self,
|
||||
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());
|
||||
|
||||
if self.need_reload {
|
||||
return panel
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(components::label::Label::new("Loading...").text_color(theme.text));
|
||||
}
|
||||
|
||||
let current_page = self.get_current_page_rows();
|
||||
let rows: Vec<gpui::Div> = current_page
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, row)| self.render_row(index, row, cx))
|
||||
.collect();
|
||||
|
||||
panel
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.bg(theme.background)
|
||||
.child(self.render_header(cx))
|
||||
.child(
|
||||
gpui::div()
|
||||
.id("task-table-content")
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.overflow_y_scroll()
|
||||
.child(gpui::div().flex().flex_col().children(rows)),
|
||||
)
|
||||
.child(self.render_footer(cx))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user