Add calendar invites panel to Calendar TUI sidebar
- Create InvitesPanel widget showing pending invites from Microsoft Graph - Add fetch_pending_invites() and respond_to_invite() API functions - Invites load asynchronously in background on app mount - Display invite subject, date/time, and organizer - Add 'i' keybinding to focus invites panel - Style: tentative invites shown in warning color
This commit is contained in:
@@ -19,6 +19,7 @@ from textual.reactive import reactive
|
||||
from src.calendar.backend import CalendarBackend, Event
|
||||
from src.calendar.widgets.WeekGrid import WeekGrid
|
||||
from src.calendar.widgets.MonthCalendar import MonthCalendar
|
||||
from src.calendar.widgets.InvitesPanel import InvitesPanel, CalendarInvite
|
||||
from src.calendar.widgets.AddEventForm import EventFormData
|
||||
from src.utils.shared_config import get_theme_name
|
||||
|
||||
@@ -72,6 +73,13 @@ class CalendarApp(App):
|
||||
#sidebar-calendar {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#sidebar-invites {
|
||||
height: auto;
|
||||
margin-top: 1;
|
||||
border-top: solid $surface-darken-1;
|
||||
padding-top: 1;
|
||||
}
|
||||
|
||||
#week-grid {
|
||||
height: 1fr;
|
||||
@@ -120,6 +128,7 @@ class CalendarApp(App):
|
||||
Binding("g", "goto_today", "Today", show=True),
|
||||
Binding("w", "toggle_weekends", "Weekends", show=True),
|
||||
Binding("s", "toggle_sidebar", "Sidebar", show=True),
|
||||
Binding("i", "focus_invites", "Invites", show=True),
|
||||
Binding("r", "refresh", "Refresh", show=True),
|
||||
Binding("enter", "view_event", "View", show=True),
|
||||
Binding("a", "add_event", "Add", show=True),
|
||||
@@ -132,9 +141,11 @@ class CalendarApp(App):
|
||||
|
||||
# Instance attributes
|
||||
backend: Optional[CalendarBackend]
|
||||
_invites: list[CalendarInvite]
|
||||
|
||||
def __init__(self, backend: Optional[CalendarBackend] = None):
|
||||
super().__init__()
|
||||
self._invites = []
|
||||
|
||||
if backend:
|
||||
self.backend = backend
|
||||
@@ -150,6 +161,7 @@ class CalendarApp(App):
|
||||
with Horizontal(id="main-content"):
|
||||
with Vertical(id="sidebar"):
|
||||
yield MonthCalendar(id="sidebar-calendar")
|
||||
yield InvitesPanel(id="sidebar-invites")
|
||||
yield WeekGrid(id="week-grid")
|
||||
yield Static(id="event-detail", classes="hidden")
|
||||
yield CalendarStatusBar(id="status-bar")
|
||||
@@ -165,10 +177,75 @@ class CalendarApp(App):
|
||||
# Sync sidebar calendar with current week
|
||||
self._sync_sidebar_calendar()
|
||||
|
||||
# Load invites in background
|
||||
self.run_worker(self._load_invites_async(), exclusive=True)
|
||||
|
||||
# Update status bar and title
|
||||
self._update_status()
|
||||
self._update_title()
|
||||
|
||||
async def _load_invites_async(self) -> None:
|
||||
"""Load pending calendar invites from Microsoft Graph."""
|
||||
try:
|
||||
from src.services.microsoft_graph.auth import get_access_token
|
||||
from src.services.microsoft_graph.calendar import fetch_pending_invites
|
||||
from dateutil import parser as date_parser
|
||||
|
||||
# Get auth token
|
||||
scopes = ["https://graph.microsoft.com/Calendars.Read"]
|
||||
_, headers = get_access_token(scopes)
|
||||
|
||||
# Fetch invites
|
||||
raw_invites = await fetch_pending_invites(headers, days_forward=30)
|
||||
|
||||
# Convert to CalendarInvite objects
|
||||
invites = []
|
||||
for inv in raw_invites:
|
||||
try:
|
||||
start_str = inv.get("start", {}).get("dateTime", "")
|
||||
end_str = inv.get("end", {}).get("dateTime", "")
|
||||
start_dt = (
|
||||
date_parser.parse(start_str) if start_str else datetime.now()
|
||||
)
|
||||
end_dt = date_parser.parse(end_str) if end_str else start_dt
|
||||
|
||||
organizer_data = inv.get("organizer", {}).get("emailAddress", {})
|
||||
organizer_name = organizer_data.get(
|
||||
"name", organizer_data.get("address", "Unknown")
|
||||
)
|
||||
|
||||
invite = CalendarInvite(
|
||||
id=inv.get("id", ""),
|
||||
subject=inv.get("subject", "No Subject"),
|
||||
organizer=organizer_name,
|
||||
start=start_dt,
|
||||
end=end_dt,
|
||||
location=inv.get("location", {}).get("displayName"),
|
||||
is_all_day=inv.get("isAllDay", False),
|
||||
response_status=inv.get("responseStatus", {}).get(
|
||||
"response", "notResponded"
|
||||
),
|
||||
)
|
||||
invites.append(invite)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse invite: {e}")
|
||||
|
||||
# Update the panel
|
||||
self._invites = invites
|
||||
self.call_from_thread(self._update_invites_panel)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load invites: {e}")
|
||||
# Silently fail - invites are optional
|
||||
|
||||
def _update_invites_panel(self) -> None:
|
||||
"""Update the invites panel with loaded invites."""
|
||||
try:
|
||||
panel = self.query_one("#sidebar-invites", InvitesPanel)
|
||||
panel.set_invites(self._invites)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _sync_sidebar_calendar(self) -> None:
|
||||
"""Sync the sidebar calendar with the main week grid."""
|
||||
try:
|
||||
@@ -443,6 +520,7 @@ Keybindings:
|
||||
g - Go to today
|
||||
w - Toggle weekends (5/7 days)
|
||||
s - Toggle sidebar
|
||||
i - Focus invites panel
|
||||
Enter - View event details
|
||||
a - Add new event
|
||||
r - Refresh
|
||||
@@ -450,6 +528,16 @@ Keybindings:
|
||||
"""
|
||||
self.notify(help_text.strip(), timeout=10)
|
||||
|
||||
def action_focus_invites(self) -> None:
|
||||
"""Focus on the invites panel and show invite count."""
|
||||
if not self.show_sidebar:
|
||||
self.action_toggle_sidebar()
|
||||
|
||||
if self._invites:
|
||||
self.notify(f"You have {len(self._invites)} pending invite(s)")
|
||||
else:
|
||||
self.notify("No pending invites")
|
||||
|
||||
|
||||
def run_app(backend: Optional[CalendarBackend] = None) -> None:
|
||||
"""Run the Calendar TUI application."""
|
||||
|
||||
Reference in New Issue
Block a user