wip
This commit is contained in:
@@ -106,6 +106,7 @@ class TasksApp(App):
|
||||
Binding("x", "delete_task", "Delete", show=False),
|
||||
Binding("p", "filter_project", "Project", show=True),
|
||||
Binding("t", "filter_tag", "Tag", show=True),
|
||||
Binding("o", "sort_tasks", "Sort", show=True),
|
||||
Binding("c", "clear_filters", "Clear", show=True),
|
||||
Binding("r", "refresh", "Refresh", show=True),
|
||||
Binding("y", "sync", "Sync", show=True),
|
||||
@@ -119,6 +120,8 @@ class TasksApp(App):
|
||||
tags: list[str]
|
||||
current_project_filter: Optional[str]
|
||||
current_tag_filters: list[str]
|
||||
current_sort_column: str
|
||||
current_sort_ascending: bool
|
||||
backend: Optional[TaskBackend]
|
||||
config: Optional[TasksAppConfig]
|
||||
|
||||
@@ -130,6 +133,8 @@ class TasksApp(App):
|
||||
self.tags = []
|
||||
self.current_project_filter = None
|
||||
self.current_tag_filters = []
|
||||
self.current_sort_column = "priority"
|
||||
self.current_sort_ascending = True
|
||||
self.config = get_config()
|
||||
|
||||
if backend:
|
||||
@@ -241,6 +246,9 @@ class TasksApp(App):
|
||||
tags=self.current_tag_filters if self.current_tag_filters else None,
|
||||
)
|
||||
|
||||
# Sort tasks
|
||||
self._sort_tasks()
|
||||
|
||||
# Also load projects and tags for filtering
|
||||
self.projects = self.backend.get_projects()
|
||||
self.tags = self.backend.get_tags()
|
||||
@@ -248,6 +256,31 @@ class TasksApp(App):
|
||||
# Update table
|
||||
self._update_table()
|
||||
|
||||
def _sort_tasks(self) -> None:
|
||||
"""Sort tasks based on current sort settings."""
|
||||
|
||||
def get_sort_key(task: Task):
|
||||
col = self.current_sort_column
|
||||
if col == "priority":
|
||||
# P0 should come first (lowest value), so use enum value index
|
||||
priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
|
||||
return priority_order.get(task.priority.value, 99)
|
||||
elif col == "project":
|
||||
return task.project or ""
|
||||
elif col == "summary":
|
||||
return task.summary.lower()
|
||||
elif col == "due":
|
||||
# Tasks without due date go to the end
|
||||
if task.due is None:
|
||||
return "9999-99-99"
|
||||
return task.due.isoformat()
|
||||
elif col == "status":
|
||||
return task.status.value
|
||||
else:
|
||||
return ""
|
||||
|
||||
self.tasks.sort(key=get_sort_key, reverse=not self.current_sort_ascending)
|
||||
|
||||
def _update_table(self) -> None:
|
||||
"""Update the task table with current tasks."""
|
||||
table = self.query_one("#task-table", DataTable)
|
||||
@@ -420,6 +453,24 @@ class TasksApp(App):
|
||||
handle_tag_selection,
|
||||
)
|
||||
|
||||
def action_sort_tasks(self) -> None:
|
||||
"""Open sort dialog."""
|
||||
from .screens.FilterScreens import SortScreen, SortConfig
|
||||
|
||||
def handle_sort_selection(config: SortConfig | None) -> None:
|
||||
if config is not None:
|
||||
self.current_sort_column = config.column
|
||||
self.current_sort_ascending = config.ascending
|
||||
self._sort_tasks()
|
||||
self._update_table()
|
||||
direction = "asc" if config.ascending else "desc"
|
||||
self.notify(f"Sorted by {config.column} ({direction})")
|
||||
|
||||
self.push_screen(
|
||||
SortScreen(self.current_sort_column, self.current_sort_ascending),
|
||||
handle_sort_selection,
|
||||
)
|
||||
|
||||
def action_clear_filters(self) -> None:
|
||||
"""Clear all filters."""
|
||||
self.current_project_filter = None
|
||||
@@ -457,6 +508,7 @@ Keybindings:
|
||||
x - Delete task
|
||||
p - Filter by project
|
||||
t - Filter by tag
|
||||
o - Sort tasks
|
||||
c - Clear filters
|
||||
r - Refresh
|
||||
y - Sync with remote
|
||||
|
||||
@@ -5,9 +5,9 @@ from typing import Optional
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Container, Vertical
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Label, SelectionList, Button
|
||||
from textual.widgets import Label, SelectionList, Button, RadioButton, RadioSet
|
||||
from textual.widgets.selection_list import Selection
|
||||
|
||||
|
||||
@@ -21,37 +21,39 @@ class ProjectFilterScreen(ModalScreen[Optional[str]]):
|
||||
|
||||
DEFAULT_CSS = """
|
||||
ProjectFilterScreen {
|
||||
align: center middle;
|
||||
align: center bottom;
|
||||
}
|
||||
|
||||
ProjectFilterScreen #filter-container {
|
||||
width: 50;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 80%;
|
||||
max-height: 12;
|
||||
background: $surface;
|
||||
border: thick $primary;
|
||||
border-top: thick $primary;
|
||||
padding: 1 2;
|
||||
layout: horizontal;
|
||||
}
|
||||
|
||||
ProjectFilterScreen #filter-title {
|
||||
text-style: bold;
|
||||
width: 1fr;
|
||||
width: auto;
|
||||
height: 1;
|
||||
text-align: center;
|
||||
margin-bottom: 1;
|
||||
margin-right: 2;
|
||||
padding-top: 1;
|
||||
}
|
||||
|
||||
ProjectFilterScreen SelectionList {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
max-height: 15;
|
||||
margin-bottom: 1;
|
||||
max-height: 8;
|
||||
}
|
||||
|
||||
ProjectFilterScreen #filter-buttons {
|
||||
width: 1fr;
|
||||
width: auto;
|
||||
height: auto;
|
||||
layout: horizontal;
|
||||
align: center middle;
|
||||
margin-top: 1;
|
||||
margin-left: 2;
|
||||
}
|
||||
|
||||
ProjectFilterScreen Button {
|
||||
@@ -76,8 +78,8 @@ class ProjectFilterScreen(ModalScreen[Optional[str]]):
|
||||
self._current_filter = current_filter
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="filter-container"):
|
||||
yield Label("Select Project", id="filter-title")
|
||||
with Horizontal(id="filter-container"):
|
||||
yield Label("Project:", id="filter-title")
|
||||
|
||||
selections = [
|
||||
Selection(
|
||||
@@ -90,7 +92,7 @@ class ProjectFilterScreen(ModalScreen[Optional[str]]):
|
||||
|
||||
yield SelectionList[str](*selections, id="project-list")
|
||||
|
||||
with Container(id="filter-buttons"):
|
||||
with Horizontal(id="filter-buttons"):
|
||||
yield Button("Cancel", id="cancel", variant="default")
|
||||
yield Button("Clear", id="clear", variant="warning")
|
||||
yield Button("Apply", id="apply", variant="primary")
|
||||
@@ -133,37 +135,39 @@ class TagFilterScreen(ModalScreen[list[str]]):
|
||||
|
||||
DEFAULT_CSS = """
|
||||
TagFilterScreen {
|
||||
align: center middle;
|
||||
align: center bottom;
|
||||
}
|
||||
|
||||
TagFilterScreen #filter-container {
|
||||
width: 50;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 80%;
|
||||
max-height: 12;
|
||||
background: $surface;
|
||||
border: thick $primary;
|
||||
border-top: thick $primary;
|
||||
padding: 1 2;
|
||||
layout: horizontal;
|
||||
}
|
||||
|
||||
TagFilterScreen #filter-title {
|
||||
text-style: bold;
|
||||
width: 1fr;
|
||||
width: auto;
|
||||
height: 1;
|
||||
text-align: center;
|
||||
margin-bottom: 1;
|
||||
margin-right: 2;
|
||||
padding-top: 1;
|
||||
}
|
||||
|
||||
TagFilterScreen SelectionList {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
max-height: 15;
|
||||
margin-bottom: 1;
|
||||
max-height: 8;
|
||||
}
|
||||
|
||||
TagFilterScreen #filter-buttons {
|
||||
width: 1fr;
|
||||
width: auto;
|
||||
height: auto;
|
||||
layout: horizontal;
|
||||
align: center middle;
|
||||
margin-top: 1;
|
||||
margin-left: 2;
|
||||
}
|
||||
|
||||
TagFilterScreen Button {
|
||||
@@ -188,8 +192,8 @@ class TagFilterScreen(ModalScreen[list[str]]):
|
||||
self._current_filters = current_filters
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="filter-container"):
|
||||
yield Label("Select Tags (multi-select)", id="filter-title")
|
||||
with Horizontal(id="filter-container"):
|
||||
yield Label("Tags:", id="filter-title")
|
||||
|
||||
selections = [
|
||||
Selection(
|
||||
@@ -202,7 +206,7 @@ class TagFilterScreen(ModalScreen[list[str]]):
|
||||
|
||||
yield SelectionList[str](*selections, id="tag-list")
|
||||
|
||||
with Container(id="filter-buttons"):
|
||||
with Horizontal(id="filter-buttons"):
|
||||
yield Button("Cancel", id="cancel", variant="default")
|
||||
yield Button("Clear", id="clear", variant="warning")
|
||||
yield Button("Apply", id="apply", variant="primary")
|
||||
@@ -230,3 +234,157 @@ class TagFilterScreen(ModalScreen[list[str]]):
|
||||
|
||||
def action_select(self) -> None:
|
||||
self.handle_apply()
|
||||
|
||||
|
||||
class SortConfig:
|
||||
"""Configuration for sort settings."""
|
||||
|
||||
def __init__(self, column: str = "priority", ascending: bool = True):
|
||||
self.column = column
|
||||
self.ascending = ascending
|
||||
|
||||
|
||||
class SortScreen(ModalScreen[Optional[SortConfig]]):
|
||||
"""Modal screen for selecting sort column and direction."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
Binding("enter", "select", "Select"),
|
||||
]
|
||||
|
||||
DEFAULT_CSS = """
|
||||
SortScreen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
SortScreen #sort-container {
|
||||
width: 40;
|
||||
height: auto;
|
||||
background: $surface;
|
||||
border: thick $primary;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
SortScreen #sort-title {
|
||||
text-style: bold;
|
||||
width: 100%;
|
||||
height: 1;
|
||||
text-align: center;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
SortScreen .section-label {
|
||||
margin-top: 1;
|
||||
margin-bottom: 0;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
SortScreen RadioSet {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
SortScreen RadioButton {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
height: 1;
|
||||
}
|
||||
|
||||
SortScreen #sort-buttons {
|
||||
width: 100%;
|
||||
height: 3;
|
||||
align: center middle;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
SortScreen Button {
|
||||
margin: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
# Available columns for sorting
|
||||
SORT_COLUMNS = [
|
||||
("priority", "Priority"),
|
||||
("project", "Project"),
|
||||
("summary", "Summary"),
|
||||
("due", "Due"),
|
||||
("status", "Status"),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
current_column: str = "priority",
|
||||
current_ascending: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the sort screen.
|
||||
|
||||
Args:
|
||||
current_column: Currently selected sort column
|
||||
current_ascending: Current sort direction (True=ascending)
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self._current_column = current_column
|
||||
self._current_ascending = current_ascending
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="sort-container"):
|
||||
yield Label("Sort Tasks", id="sort-title")
|
||||
|
||||
# Column selection
|
||||
yield Label("Sort by:", classes="section-label")
|
||||
with RadioSet(id="column-set"):
|
||||
for key, display in self.SORT_COLUMNS:
|
||||
yield RadioButton(
|
||||
display, value=key == self._current_column, id=f"col-{key}"
|
||||
)
|
||||
|
||||
# Direction selection
|
||||
yield Label("Direction:", classes="section-label")
|
||||
with RadioSet(id="direction-set"):
|
||||
yield RadioButton(
|
||||
"Ascending", value=self._current_ascending, id="dir-asc"
|
||||
)
|
||||
yield RadioButton(
|
||||
"Descending", value=not self._current_ascending, id="dir-desc"
|
||||
)
|
||||
|
||||
with Horizontal(id="sort-buttons"):
|
||||
yield Button("Cancel", id="cancel", variant="default")
|
||||
yield Button("Apply", id="apply", variant="primary")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Focus the column radio set."""
|
||||
self.query_one("#column-set", RadioSet).focus()
|
||||
|
||||
@on(Button.Pressed, "#apply")
|
||||
def handle_apply(self) -> None:
|
||||
column_set = self.query_one("#column-set", RadioSet)
|
||||
direction_set = self.query_one("#direction-set", RadioSet)
|
||||
|
||||
# Get selected column from pressed button id
|
||||
column = self._current_column
|
||||
if column_set.pressed_button and column_set.pressed_button.id:
|
||||
# Extract column key from id like "col-priority"
|
||||
column = column_set.pressed_button.id.replace("col-", "")
|
||||
|
||||
# Get direction
|
||||
ascending = self._current_ascending
|
||||
if direction_set.pressed_button and direction_set.pressed_button.id:
|
||||
ascending = direction_set.pressed_button.id == "dir-asc"
|
||||
|
||||
self.dismiss(SortConfig(column=column, ascending=ascending))
|
||||
|
||||
@on(Button.Pressed, "#cancel")
|
||||
def handle_cancel(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
def action_select(self) -> None:
|
||||
self.handle_apply()
|
||||
|
||||
Reference in New Issue
Block a user