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:
Bendt
2025-12-19 10:51:15 -05:00
parent a82f001918
commit 3c45e2a154
4 changed files with 379 additions and 1 deletions

View File

@@ -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."""