feat & fix: Add status bar with sync button and fix filter bugs
Added a status bar at the bottom with a sync button that updates the task list from the local database. The button shows different states (idle, syncing, success, error) with colors and icons, and displays status messages. Fixed the layout shift bug when clearing filters by giving dropdowns a minimum width. Also fixed sidebar counts not updating when filters change - now the sidebar shows correct project and tag counts based on filtered tasks. Extracted reusable button and dropdown styles to keep the UI consistent across components.
This commit is contained in:
+101
-7
@@ -1,11 +1,13 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
|
|
||||||
use crate::models::{FilterState, ProjectTree};
|
use crate::models::{FilterState, ProjectTree};
|
||||||
use crate::task::{TaskOverview, TaskService};
|
use crate::task::{self, TaskFilter, TaskOverview, TaskService};
|
||||||
use crate::theme::ActiveTheme;
|
use crate::theme::ActiveTheme;
|
||||||
use crate::ui::{card_style, SECTION_GAP, ROOT_PADDING};
|
use crate::ui::{ROOT_PADDING, SECTION_GAP, card_style};
|
||||||
use crate::view::sidebar::{Sidebar, TagItem};
|
use crate::view::sidebar::{Sidebar, TagItem};
|
||||||
use crate::view::status_bar::StatusBar;
|
use crate::view::status_bar::{StatusBar, StatusBarEvent, SyncState};
|
||||||
use crate::view::task_table::TaskTable;
|
use crate::view::task_table::TaskTable;
|
||||||
use gpui::div;
|
use gpui::div;
|
||||||
|
|
||||||
@@ -61,11 +63,96 @@ impl gpui::Render for App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
fn build_sidebar_data(tasks: &[task::Task]) -> (Vec<(String, usize)>, Vec<TagItem>) {
|
||||||
|
let mut project_counts: HashMap<String, usize> = HashMap::new();
|
||||||
|
let mut tag_counts: HashMap<String, usize> = HashMap::new();
|
||||||
|
|
||||||
|
for task in tasks {
|
||||||
|
if let Some(project) = &task.project {
|
||||||
|
*project_counts.entry(project.clone()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in &task.tags {
|
||||||
|
*tag_counts.entry(tag.clone()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut projects: Vec<(String, usize)> = project_counts.into_iter().collect();
|
||||||
|
projects.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
|
||||||
|
|
||||||
|
let mut tags: Vec<(String, usize)> = tag_counts.into_iter().collect();
|
||||||
|
tags.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
|
||||||
|
|
||||||
|
let tag_items = tags
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, task_count)| TagItem { name, task_count })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(projects, tag_items)
|
||||||
|
}
|
||||||
|
|
||||||
fn reload_tasks(&mut self, cx: &mut gpui::Context<Self>) {
|
fn reload_tasks(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
let task_service = &mut self.task_service;
|
let all_tasks = self.task_service.get_all_tasks().unwrap_or_else(|e| {
|
||||||
self.task_table.update(cx, |table, cx| {
|
log::error!("[App] Failed to load tasks: {}", e);
|
||||||
table.reload_tasks(task_service, cx);
|
vec![]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let filter_state = self.filter_state.read(cx).clone();
|
||||||
|
let task_filter = TaskFilter::from(&filter_state);
|
||||||
|
let filtered_tasks = task_filter.apply(&all_tasks);
|
||||||
|
let (projects, tags) = Self::build_sidebar_data(&filtered_tasks);
|
||||||
|
|
||||||
|
let mut project_tree = ProjectTree::new();
|
||||||
|
project_tree.build_from_projects(&projects);
|
||||||
|
|
||||||
|
self.sidebar.update(cx, |sidebar, cx| {
|
||||||
|
sidebar.update_projects(project_tree, cx);
|
||||||
|
sidebar.update_tags(tags, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.task_table.update(cx, |table, cx| {
|
||||||
|
table.reload_tasks_from_all(all_tasks, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_sync(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.status_bar.update(cx, |bar, cx| {
|
||||||
|
bar.set_sync_state(SyncState::Syncing, cx);
|
||||||
|
bar.set_last_sync_message("Syncing...".to_string(), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
match self.task_service.get_all_tasks() {
|
||||||
|
Ok(all_tasks) => {
|
||||||
|
let filter_state = self.filter_state.read(cx).clone();
|
||||||
|
let task_filter = TaskFilter::from(&filter_state);
|
||||||
|
let filtered_tasks = task_filter.apply(&all_tasks);
|
||||||
|
let (projects, tags) = Self::build_sidebar_data(&filtered_tasks);
|
||||||
|
|
||||||
|
let mut project_tree = ProjectTree::new();
|
||||||
|
project_tree.build_from_projects(&projects);
|
||||||
|
|
||||||
|
self.sidebar.update(cx, |sidebar, cx| {
|
||||||
|
sidebar.update_projects(project_tree, cx);
|
||||||
|
sidebar.update_tags(tags, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.task_table.update(cx, |table, cx| {
|
||||||
|
table.reload_tasks_from_all(all_tasks, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.status_bar.update(cx, |bar, cx| {
|
||||||
|
bar.set_sync_state(SyncState::Success, cx);
|
||||||
|
bar.set_last_sync_message("Synced".to_string(), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("[App] Sync failed: {}", e);
|
||||||
|
self.status_bar.update(cx, |bar, cx| {
|
||||||
|
bar.set_sync_state(SyncState::Error, cx);
|
||||||
|
bar.set_last_sync_message(format!("Error: {}", e), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run() -> () {
|
pub fn run() -> () {
|
||||||
@@ -118,7 +205,7 @@ impl App {
|
|||||||
let app = App {
|
let app = App {
|
||||||
sidebar,
|
sidebar,
|
||||||
filter_state: filter_state.clone(),
|
filter_state: filter_state.clone(),
|
||||||
status_bar,
|
status_bar: status_bar.clone(),
|
||||||
task_table,
|
task_table,
|
||||||
task_service,
|
task_service,
|
||||||
};
|
};
|
||||||
@@ -128,6 +215,13 @@ impl App {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
cx.subscribe(&status_bar, |app, _bar, event, cx| match event {
|
||||||
|
StatusBarEvent::SyncRequested => {
|
||||||
|
app.handle_sync(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
app
|
app
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use gpui::prelude::*;
|
|||||||
use crate::components::button::Button;
|
use crate::components::button::Button;
|
||||||
use crate::components::label::Label;
|
use crate::components::label::Label;
|
||||||
use crate::theme::ActiveTheme;
|
use crate::theme::ActiveTheme;
|
||||||
|
use crate::ui::{clickable_control_style, disabled_control_style};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DropdownItem {
|
pub struct DropdownItem {
|
||||||
@@ -288,36 +289,20 @@ impl gpui::Render for Dropdown {
|
|||||||
.text_color(theme.foreground)
|
.text_color(theme.foreground)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
let mut trigger = gpui::div()
|
let base = 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)
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.child(Label::new(label.clone()))
|
.child(Label::new(label.clone()))
|
||||||
.child(Label::new(arrow).text_color(theme.muted));
|
.child(Label::new(arrow).text_color(theme.muted));
|
||||||
|
|
||||||
if disabled {
|
let trigger = if disabled {
|
||||||
trigger = trigger.text_color(theme.muted).cursor_not_allowed();
|
disabled_control_style(base, theme)
|
||||||
} else {
|
} else {
|
||||||
trigger = trigger
|
clickable_control_style(base, theme)
|
||||||
.cursor_pointer()
|
};
|
||||||
.hover(|s: gpui::StyleRefinement| s.bg(theme.hover));
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger.into_any_element()
|
trigger.into_any_element()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut trigger_wrap = gpui::div()
|
let mut trigger_wrap = gpui::div().flex_shrink_0().child(trigger);
|
||||||
.flex_shrink_0()
|
|
||||||
.child(trigger);
|
|
||||||
if !disabled {
|
if !disabled {
|
||||||
trigger_wrap = trigger_wrap.on_mouse_down(
|
trigger_wrap = trigger_wrap.on_mouse_down(
|
||||||
gpui::MouseButton::Left,
|
gpui::MouseButton::Left,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
|
|
||||||
use crate::theme::ActiveTheme;
|
|
||||||
|
|
||||||
#[derive(gpui::IntoElement)]
|
#[derive(gpui::IntoElement)]
|
||||||
pub struct Label {
|
pub struct Label {
|
||||||
text: gpui::SharedString,
|
text: gpui::SharedString,
|
||||||
|
|||||||
+6
-3
@@ -1,5 +1,3 @@
|
|||||||
use gpui::prelude::*;
|
|
||||||
|
|
||||||
pub type Color = gpui::Rgba;
|
pub type Color = gpui::Rgba;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -43,7 +41,12 @@ pub struct Theme {
|
|||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
pub fn alpha(c: Color, a: f32) -> Color {
|
pub fn alpha(c: Color, a: f32) -> Color {
|
||||||
gpui::Rgba { r: c.r, g: c.g, b: c.b, a: a.clamp(0.0, 1.0) }
|
gpui::Rgba {
|
||||||
|
r: c.r,
|
||||||
|
g: c.g,
|
||||||
|
b: c.b,
|
||||||
|
a: a.clamp(0.0, 1.0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dark() -> Self {
|
pub fn dark() -> Self {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{px, Pixels};
|
use gpui::{Pixels, px};
|
||||||
|
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
|
|
||||||
@@ -8,6 +8,8 @@ pub const CARD_PADDING: Pixels = px(8.0);
|
|||||||
pub const SECTION_GAP: Pixels = px(12.0);
|
pub const SECTION_GAP: Pixels = px(12.0);
|
||||||
pub const INSET_GAP: Pixels = px(8.0);
|
pub const INSET_GAP: Pixels = px(8.0);
|
||||||
pub const ROOT_PADDING: Pixels = px(12.0);
|
pub const ROOT_PADDING: Pixels = px(12.0);
|
||||||
|
pub const CONTROL_RADIUS: Pixels = px(6.0);
|
||||||
|
pub const CONTROL_BORDER: Pixels = px(1.0);
|
||||||
|
|
||||||
pub fn card_style(div: gpui::Div, theme: &Theme) -> gpui::Div {
|
pub fn card_style(div: gpui::Div, theme: &Theme) -> gpui::Div {
|
||||||
div.bg(theme.card)
|
div.bg(theme.card)
|
||||||
@@ -80,3 +82,53 @@ pub fn priority_badge(priority: &str, theme: &Theme) -> gpui::Div {
|
|||||||
.font_weight(gpui::FontWeight::MEDIUM)
|
.font_weight(gpui::FontWeight::MEDIUM)
|
||||||
.child(priority.to_string())
|
.child(priority.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn control_style(div: gpui::Div, theme: &Theme) -> gpui::Div {
|
||||||
|
div.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.px_3()
|
||||||
|
.py_2()
|
||||||
|
.rounded(CONTROL_RADIUS)
|
||||||
|
.border(CONTROL_BORDER)
|
||||||
|
.border_color(theme.border)
|
||||||
|
.bg(theme.background)
|
||||||
|
.text_sm()
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
.whitespace_nowrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clickable_control_style(div: gpui::Div, theme: &Theme) -> gpui::Div {
|
||||||
|
control_style(div, theme)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.hover))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disabled_control_style(div: gpui::Div, theme: &Theme) -> gpui::Div {
|
||||||
|
control_style(div, theme)
|
||||||
|
.text_color(theme.muted)
|
||||||
|
.cursor_not_allowed()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ghost_button_style(div: gpui::Div, theme: &Theme) -> gpui::Div {
|
||||||
|
div.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.rounded(CONTROL_RADIUS)
|
||||||
|
.text_sm()
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.hover))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_button_style(div: gpui::Div, theme: &Theme) -> gpui::Div {
|
||||||
|
div.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(theme.muted)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.text_color(theme.accent))
|
||||||
|
}
|
||||||
|
|||||||
+3
-3
@@ -103,7 +103,7 @@ impl Sidebar {
|
|||||||
.rounded_sm()
|
.rounded_sm()
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.when(is_all_selected, |this| this.bg(theme.selection))
|
.when(is_all_selected, |this| this.bg(theme.selection))
|
||||||
.when(!is_all_selected, |this| this.hover(|style| style.bg(theme.hover)))
|
.when(!is_all_selected, |this| this.hover(|s| s.bg(theme.hover)))
|
||||||
.on_mouse_down(
|
.on_mouse_down(
|
||||||
gpui::MouseButton::Left,
|
gpui::MouseButton::Left,
|
||||||
cx.listener(|view, _event, window, cx| {
|
cx.listener(|view, _event, window, cx| {
|
||||||
@@ -153,7 +153,7 @@ impl Sidebar {
|
|||||||
.rounded_sm()
|
.rounded_sm()
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.when(is_selected, |this| this.bg(theme.selection))
|
.when(is_selected, |this| this.bg(theme.selection))
|
||||||
.when(!is_selected, |this| this.hover(|style| style.bg(theme.hover)))
|
.when(!is_selected, |this| this.hover(|s| s.bg(theme.hover)))
|
||||||
.child(div().w(px(indent as f32)))
|
.child(div().w(px(indent as f32)))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -235,7 +235,7 @@ impl Sidebar {
|
|||||||
.rounded_sm()
|
.rounded_sm()
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.when(is_active, |this| this.bg(theme.selection))
|
.when(is_active, |this| this.bg(theme.selection))
|
||||||
.when(!is_active, |this| this.hover(|style| style.bg(theme.hover)))
|
.when(!is_active, |this| this.hover(|s| s.bg(theme.hover)))
|
||||||
.on_mouse_down(
|
.on_mouse_down(
|
||||||
gpui::MouseButton::Left,
|
gpui::MouseButton::Left,
|
||||||
cx.listener(move |view, _event, window, cx| {
|
cx.listener(move |view, _event, window, cx| {
|
||||||
|
|||||||
+112
-7
@@ -1,30 +1,135 @@
|
|||||||
use gpui::{Context, IntoElement, Render, Window, div, prelude::*, px};
|
use gpui::{Context, IntoElement, MouseButton, Render, Window, div, prelude::*, rems};
|
||||||
|
|
||||||
|
use crate::components::label::Label;
|
||||||
use crate::theme::ActiveTheme;
|
use crate::theme::ActiveTheme;
|
||||||
use crate::ui::{card_style, CARD_RADIUS};
|
use crate::ui::divider_v;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SyncState {
|
||||||
|
Idle,
|
||||||
|
Syncing,
|
||||||
|
Success,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SyncState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct StatusBar {
|
pub struct StatusBar {
|
||||||
// TODO: Add vim_mode, sync_state, last_sync, etc.
|
sync_state: SyncState,
|
||||||
|
last_sync_message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusBar {
|
impl StatusBar {
|
||||||
pub fn new(_cx: &mut Context<Self>) -> Self {
|
pub fn new(_cx: &mut Context<Self>) -> Self {
|
||||||
Self {}
|
Self {
|
||||||
|
sync_state: SyncState::default(),
|
||||||
|
last_sync_message: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sync_state(&mut self, state: SyncState, cx: &mut Context<Self>) {
|
||||||
|
self.sync_state = state;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_last_sync_message(&mut self, message: String, cx: &mut Context<Self>) {
|
||||||
|
self.last_sync_message = message;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_message(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.last_sync_message.clear();
|
||||||
|
self.sync_state = SyncState::Idle;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_icon(&self) -> &'static str {
|
||||||
|
match self.sync_state {
|
||||||
|
SyncState::Idle => "↻",
|
||||||
|
SyncState::Syncing => "◌",
|
||||||
|
SyncState::Success => "✓",
|
||||||
|
SyncState::Error => "✕",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for StatusBar {
|
impl Render for StatusBar {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let theme = cx.theme().clone();
|
let theme = cx.theme().clone();
|
||||||
|
let is_syncing = self.sync_state == SyncState::Syncing;
|
||||||
|
|
||||||
card_style(div(), &theme)
|
let sync_color = match self.sync_state {
|
||||||
|
SyncState::Success => theme.success,
|
||||||
|
SyncState::Error => theme.error,
|
||||||
|
SyncState::Syncing => theme.info,
|
||||||
|
SyncState::Idle => theme.muted,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sync_button = div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_1()
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.rounded_md()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(sync_color)
|
||||||
|
.when(!is_syncing, |d| {
|
||||||
|
d.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.hover))
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(|_this, _event, _window, cx| {
|
||||||
|
cx.emit(StatusBarEvent::SyncRequested);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(is_syncing, |d| d.cursor_not_allowed())
|
||||||
|
.child(Label::new(self.sync_icon()).text_color(sync_color))
|
||||||
|
.child(Label::new("Sync").text_color(sync_color));
|
||||||
|
|
||||||
|
let status_text = if !self.last_sync_message.is_empty() {
|
||||||
|
Label::new(self.last_sync_message.clone()).text_color(theme.muted)
|
||||||
|
} else {
|
||||||
|
Label::new("")
|
||||||
|
};
|
||||||
|
|
||||||
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(px(28.0))
|
.h(rems(2.0))
|
||||||
.px_3()
|
.px_3()
|
||||||
.py_1()
|
.py_1()
|
||||||
.rounded(CARD_RADIUS)
|
.bg(theme.panel)
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(theme.divider)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_3()
|
||||||
|
.text_xs()
|
||||||
|
.child(status_text),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(divider_v(&theme).h(rems(1.0)))
|
||||||
|
.child(sync_button),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum StatusBarEvent {
|
||||||
|
SyncRequested,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::EventEmitter<StatusBarEvent> for StatusBar {}
|
||||||
|
|||||||
+65
-46
@@ -386,13 +386,21 @@ impl TaskTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reload_tasks(&mut self, task_service: &mut TaskService, cx: &mut gpui::Context<Self>) {
|
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 all_tasks = task_service.get_all_tasks().unwrap_or_else(|e| {
|
let all_tasks = task_service.get_all_tasks().unwrap_or_else(|e| {
|
||||||
log::error!("[TaskTable] Failed to load tasks: {}", e);
|
log::error!("[TaskTable] Failed to load tasks: {}", e);
|
||||||
vec![]
|
vec![]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.reload_tasks_from_all(all_tasks, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reload_tasks_from_all(
|
||||||
|
&mut self,
|
||||||
|
all_tasks: Vec<task::Task>,
|
||||||
|
cx: &mut gpui::Context<Self>,
|
||||||
|
) {
|
||||||
|
let filter_state = self.filter_state.read(cx).clone();
|
||||||
|
|
||||||
let task_filter = TaskFilter::from(&filter_state);
|
let task_filter = TaskFilter::from(&filter_state);
|
||||||
let mut due_filter = task_filter.clone();
|
let mut due_filter = task_filter.clone();
|
||||||
due_filter.due_filter = None;
|
due_filter.due_filter = None;
|
||||||
@@ -602,6 +610,35 @@ impl TaskTable {
|
|||||||
let has_filters = filter.has_active_filters();
|
let has_filters = filter.has_active_filters();
|
||||||
let view = cx.entity().clone();
|
let view = cx.entity().clone();
|
||||||
|
|
||||||
|
let clear_button = {
|
||||||
|
let mut btn = gpui::div()
|
||||||
|
.id("clear-all-filters")
|
||||||
|
.flex_shrink_0()
|
||||||
|
.min_w(gpui::rems(5.5))
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.rounded_md()
|
||||||
|
.text_sm();
|
||||||
|
|
||||||
|
if has_filters {
|
||||||
|
btn = btn
|
||||||
|
.text_color(theme.error)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.hover))
|
||||||
|
.on_mouse_down(
|
||||||
|
gpui::MouseButton::Left,
|
||||||
|
cx.listener(|table, _, _, cx| {
|
||||||
|
table.handle_clear_filters(cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child("✕ Clear");
|
||||||
|
} else {
|
||||||
|
btn = btn.child(gpui::div().opacity(0.0).child("✕ Clear"));
|
||||||
|
}
|
||||||
|
|
||||||
|
btn
|
||||||
|
};
|
||||||
|
|
||||||
let bar = gpui::div()
|
let bar = gpui::div()
|
||||||
.id("filter-bar")
|
.id("filter-bar")
|
||||||
.flex()
|
.flex()
|
||||||
@@ -618,40 +655,22 @@ impl TaskTable {
|
|||||||
.child(self.status_dropdown.clone())
|
.child(self.status_dropdown.clone())
|
||||||
.child(self.priority_dropdown.clone())
|
.child(self.priority_dropdown.clone())
|
||||||
.child(self.due_dropdown.clone())
|
.child(self.due_dropdown.clone())
|
||||||
.when(has_filters, |this| {
|
.child(clear_button);
|
||||||
this.child(
|
|
||||||
gpui::div()
|
|
||||||
.id("clear-all-filters")
|
|
||||||
.flex_shrink_0()
|
|
||||||
.px_2()
|
|
||||||
.py_1()
|
|
||||||
.rounded_md()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(theme.error)
|
|
||||||
.cursor_pointer()
|
|
||||||
.hover(|s| s.bg(theme.hover))
|
|
||||||
.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| {
|
gpui::div()
|
||||||
let Some(bounds) = bounds.first() else {
|
.child(bar)
|
||||||
return;
|
.on_children_prepainted(move |bounds, _, cx| {
|
||||||
};
|
let Some(bounds) = bounds.first() else {
|
||||||
let height = bounds.size.height;
|
return;
|
||||||
cx.update_entity(&view, |table, cx| {
|
};
|
||||||
if table.filter_bar_height != height {
|
let height = bounds.size.height;
|
||||||
table.filter_bar_height = height;
|
cx.update_entity(&view, |table, cx| {
|
||||||
cx.notify();
|
if table.filter_bar_height != height {
|
||||||
}
|
table.filter_bar_height = height;
|
||||||
});
|
cx.notify();
|
||||||
})
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_header_column(
|
fn render_header_column(
|
||||||
@@ -917,7 +936,9 @@ impl gpui::Render for TaskTable {
|
|||||||
) -> impl gpui::IntoElement {
|
) -> impl gpui::IntoElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
|
|
||||||
let panel = components::panel::Panel::new(self.id.clone()).flex().flex_col();
|
let panel = components::panel::Panel::new(self.id.clone())
|
||||||
|
.flex()
|
||||||
|
.flex_col();
|
||||||
|
|
||||||
if self.need_reload {
|
if self.need_reload {
|
||||||
return panel
|
return panel
|
||||||
@@ -959,15 +980,13 @@ impl gpui::Render for TaskTable {
|
|||||||
)
|
)
|
||||||
.child(footer);
|
.child(footer);
|
||||||
|
|
||||||
panel
|
panel.child(body).child(
|
||||||
.child(body)
|
gpui::div()
|
||||||
.child(
|
.absolute()
|
||||||
gpui::div()
|
.top_0()
|
||||||
.absolute()
|
.left_0()
|
||||||
.top_0()
|
.right_0()
|
||||||
.left_0()
|
.child(filter_bar),
|
||||||
.right_0()
|
)
|
||||||
.child(filter_bar),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user