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:
Ignacio Perez
2025-12-27 12:40:49 -03:00
parent 3c10f8fdb8
commit d132e51da2
8 changed files with 774 additions and 43 deletions
+22 -11
View File
@@ -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,
}
})
+1 -4
View File
@@ -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
View File
@@ -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()
+38
View File
@@ -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()),
+17
View File
@@ -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
View File
@@ -1,2 +1,3 @@
pub mod sidebar;
pub mod status_bar;
pub mod task_table;
+3 -11
View File
@@ -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()
+686
View File
@@ -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))
}
}