From be2f67bb7bd710d69b51501d212a188f7cb146e7 Mon Sep 17 00:00:00 2001 From: Bendt Date: Fri, 19 Dec 2025 11:24:15 -0500 Subject: [PATCH] Fix TUI bugs: folder selection, filter stability, UI consistency - Mail: Fix folder/account selector not triggering reload (use direct fetch instead of reactive reload_needed flag) - Tasks: Store all_projects/all_tags on mount so filters don't change when filtering; add OR search for multiple tags - Sync: Use rounded borders and border_title for sidebar/activity log - Calendar: Remove padding from mini-calendar, add rounded border and border_title to invites panel --- src/calendar/widgets/InvitesPanel.py | 5 +++ src/calendar/widgets/MonthCalendar.py | 2 +- src/cli/sync_dashboard.py | 28 +++++------- src/mail/app.py | 8 +++- src/tasks/app.py | 65 ++++++++++++++++++++------- 5 files changed, 71 insertions(+), 37 deletions(-) diff --git a/src/calendar/widgets/InvitesPanel.py b/src/calendar/widgets/InvitesPanel.py index d9844b0..4149b1b 100644 --- a/src/calendar/widgets/InvitesPanel.py +++ b/src/calendar/widgets/InvitesPanel.py @@ -38,6 +38,7 @@ class InvitesPanel(Widget): height: auto; min-height: 3; padding: 0 1; + border: round $primary; } """ @@ -71,6 +72,10 @@ class InvitesPanel(Widget): if invites: self.invites = invites + def on_mount(self) -> None: + """Set border title on mount.""" + self.border_title = "Invites" + def _get_theme_color(self, color_name: str) -> str: """Get a color from the current theme.""" try: diff --git a/src/calendar/widgets/MonthCalendar.py b/src/calendar/widgets/MonthCalendar.py index 82f42de..ee9907e 100644 --- a/src/calendar/widgets/MonthCalendar.py +++ b/src/calendar/widgets/MonthCalendar.py @@ -62,7 +62,7 @@ class MonthCalendar(Widget): MonthCalendar { width: 24; height: auto; - padding: 0 1; + padding: 0; } """ diff --git a/src/cli/sync_dashboard.py b/src/cli/sync_dashboard.py index ec6a59a..b258ad0 100644 --- a/src/cli/sync_dashboard.py +++ b/src/cli/sync_dashboard.py @@ -216,16 +216,10 @@ class SyncDashboard(App): .sidebar { width: 30; height: 100%; - border: solid $primary; + border: round $primary; padding: 0; } - .sidebar-title { - text-style: bold; - padding: 1; - background: $primary-darken-2; - } - .countdown-container { height: 5; padding: 0 1; @@ -269,15 +263,10 @@ class SyncDashboard(App): .log-container { height: 1fr; - border: solid $primary; + border: round $primary; padding: 0; } - .log-title { - padding: 0 1; - background: $primary-darken-2; - } - ListView { height: 1fr; } @@ -338,8 +327,7 @@ class SyncDashboard(App): with Horizontal(classes="dashboard"): # Sidebar with task list - with Vertical(classes="sidebar"): - yield Static("Tasks", classes="sidebar-title") + with Vertical(classes="sidebar", id="tasks-sidebar"): yield ListView( # Stage 1: Sync local changes to server TaskListItem( @@ -416,8 +404,7 @@ class SyncDashboard(App): yield Static("0%", id="progress-percent") # Log for selected task - with Vertical(classes="log-container"): - yield Static("Activity Log", classes="log-title") + with Vertical(classes="log-container", id="log-container"): yield Log(id="task-log") yield Footer() @@ -427,6 +414,13 @@ class SyncDashboard(App): # Set theme from shared config self.theme = get_theme_name() + # Set border titles + try: + self.query_one("#tasks-sidebar").border_title = "Tasks" + self.query_one("#log-container").border_title = "Activity Log" + except Exception: + pass + # Store references to task items task_list = self.query_one("#task-list", ListView) for item in task_list.children: diff --git a/src/mail/app.py b/src/mail/app.py index ebbb3f0..9d63387 100644 --- a/src/mail/app.py +++ b/src/mail/app.py @@ -353,7 +353,9 @@ class EmailViewerApp(App): self.current_message_id = 0 self.current_message_index = 0 self.selected_messages.clear() - self.reload_needed = True + self.search_query = "" # Clear search when switching folders + # Directly fetch instead of relying on reload_needed watcher + self.fetch_envelopes() except Exception as e: logging.error(f"Error selecting folder: {e}") @@ -372,9 +374,11 @@ class EmailViewerApp(App): self.current_message_id = 0 self.current_message_index = 0 self.selected_messages.clear() + self.search_query = "" # Clear search when switching accounts # Refresh folders for new account self.fetch_folders() - self.reload_needed = True + # Directly fetch instead of relying on reload_needed watcher + self.fetch_envelopes() except Exception as e: logging.error(f"Error selecting account: {e}") diff --git a/src/tasks/app.py b/src/tasks/app.py index 99692be..fd85aeb 100644 --- a/src/tasks/app.py +++ b/src/tasks/app.py @@ -197,6 +197,8 @@ class TasksApp(App): self.tasks = [] self.projects = [] self.tags = [] + self.all_projects = [] # Stable list of all projects (not filtered) + self.all_tags = [] # Stable list of all tags (not filtered) self.current_project_filter = None self.current_tag_filters = [] self.current_sort_column = "priority" @@ -261,7 +263,10 @@ class TasksApp(App): height = max(10, min(90, height)) notes_pane.styles.height = f"{height}%" - # Load tasks (this will also update the sidebar) + # Load ALL projects and tags once for stable sidebar + self._load_all_filters() + + # Load tasks (filtered by current filters) self.load_tasks() def _setup_columns(self, table: DataTable, columns: list[str]) -> None: @@ -375,32 +380,58 @@ class TasksApp(App): if not self.backend: return - # Get tasks with current filters - self.tasks = self.backend.get_tasks( - project=self.current_project_filter, - tags=self.current_tag_filters if self.current_tag_filters else None, - ) + # Get ALL tasks first (unfiltered) + all_tasks = self.backend.get_tasks() + + # Apply client-side filtering for OR logic + self.tasks = self._filter_tasks(all_tasks) # Sort tasks self._sort_tasks() - # Also load projects and tags for filtering - self.projects = self.backend.get_projects() - self.tags = self.backend.get_tags() - - # Update sidebar with available filters - self._update_sidebar() - # Update table self._update_table() + def _load_all_filters(self) -> None: + """Load all projects and tags once for stable sidebar.""" + if not self.backend: + return + + self.all_projects = self.backend.get_projects() + self.all_tags = self.backend.get_tags() + + # Update sidebar with stable filter options + self._update_sidebar() + + def _filter_tasks(self, tasks: list[Task]) -> list[Task]: + """Filter tasks by current project and tag filters using OR logic. + + - If project filter is set, only show tasks from that project + - If tag filters are set, show tasks that have ANY of the selected tags (OR) + """ + filtered = tasks + + # Filter by project (single project filter is AND) + if self.current_project_filter: + filtered = [t for t in filtered if t.project == self.current_project_filter] + + # Filter by tags using OR logic - show tasks with ANY of the selected tags + if self.current_tag_filters: + filtered = [ + t + for t in filtered + if any(tag in t.tags for tag in self.current_tag_filters) + ] + + return filtered + def _update_sidebar(self) -> None: - """Update the filter sidebar with current projects and tags.""" + """Update the filter sidebar with all available projects and tags.""" try: sidebar = self.query_one("#sidebar", FilterSidebar) - # Convert projects to (name, count) tuples - project_data = [(p.name, p.task_count) for p in self.projects if p.name] - sidebar.update_filters(projects=project_data, tags=self.tags) + # Use stable all_projects/all_tags, not filtered ones + project_data = [(p.name, p.task_count) for p in self.all_projects if p.name] + sidebar.update_filters(projects=project_data, tags=self.all_tags) except Exception: pass # Sidebar may not be mounted yet