mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 20:59:05 +00:00
feat: improve ICS calendar sync UX and fix room URL matching
- Replace "Test Connection" button with "Force Sync" button (Edit Room only) - Show detailed sync results: total events downloaded vs room matches - Remove emoticons and auto-hide timeout for cleaner UX - Fix room URL matching to use UI_BASE_URL instead of BASE_URL - Replace FaSync icon with LuRefreshCw for consistency - Clear sync results when dialog closes or Force Sync pressed - Update tests to reflect UI_BASE_URL change and exact URL matching 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -36,8 +36,11 @@ class SyncStats(TypedDict):
|
|||||||
events_deleted: int
|
events_deleted: int
|
||||||
|
|
||||||
|
|
||||||
class SyncResult(TypedDict, total=False):
|
class SyncResultBase(TypedDict):
|
||||||
status: str # "success", "unchanged", "error", "skipped"
|
status: str # "success", "unchanged", "error", "skipped"
|
||||||
|
|
||||||
|
|
||||||
|
class SyncResult(SyncResultBase, total=False):
|
||||||
hash: str | None
|
hash: str | None
|
||||||
events_found: int
|
events_found: int
|
||||||
total_events: int
|
total_events: int
|
||||||
@@ -73,18 +76,17 @@ class ICSFetchService:
|
|||||||
window_end = now + timedelta(hours=24)
|
window_end = now + timedelta(hours=24)
|
||||||
|
|
||||||
for component in calendar.walk():
|
for component in calendar.walk():
|
||||||
if component.name == "VEVENT":
|
if component.name != "VEVENT":
|
||||||
# Skip cancelled events
|
continue
|
||||||
|
|
||||||
status = component.get("STATUS", "").upper()
|
status = component.get("STATUS", "").upper()
|
||||||
if status == "CANCELLED":
|
if status == "CANCELLED":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Count total non-cancelled events in the time window
|
# Count total non-cancelled events in the time window
|
||||||
event_data = self._parse_event(component)
|
event_data = self._parse_event(component)
|
||||||
if (
|
print(room_url, event_data)
|
||||||
event_data
|
if event_data and window_start <= event_data["start_time"] <= window_end:
|
||||||
and window_start <= event_data["start_time"] <= window_end
|
|
||||||
):
|
|
||||||
total_events += 1
|
total_events += 1
|
||||||
|
|
||||||
# Check if event matches this room
|
# Check if event matches this room
|
||||||
@@ -94,20 +96,21 @@ class ICSFetchService:
|
|||||||
return events, total_events
|
return events, total_events
|
||||||
|
|
||||||
def _event_matches_room(self, event: Event, room_name: str, room_url: str) -> bool:
|
def _event_matches_room(self, event: Event, room_name: str, room_url: str) -> bool:
|
||||||
|
print("_____", room_url)
|
||||||
location = str(event.get("LOCATION", ""))
|
location = str(event.get("LOCATION", ""))
|
||||||
description = str(event.get("DESCRIPTION", ""))
|
description = str(event.get("DESCRIPTION", ""))
|
||||||
|
|
||||||
# Only match full room URL (with or without protocol)
|
# Only match full room URL
|
||||||
|
# XXX leaved here as a patterns, to later be extended with tinyurl or such too
|
||||||
patterns = [
|
patterns = [
|
||||||
room_url, # Full URL with protocol
|
room_url,
|
||||||
room_url.replace("https://", ""), # Without https protocol
|
|
||||||
room_url.replace("http://", ""), # Without http protocol
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Check location and description for patterns
|
# Check location and description for patterns
|
||||||
text_to_check = f"{location} {description}".lower()
|
text_to_check = f"{location} {description}".lower()
|
||||||
|
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
|
print(text_to_check, pattern.lower())
|
||||||
if pattern.lower() in text_to_check:
|
if pattern.lower() in text_to_check:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -225,7 +228,7 @@ class ICSSyncService:
|
|||||||
logger.info(f"No changes in ICS for room {room.id}")
|
logger.info(f"No changes in ICS for room {room.id}")
|
||||||
# Still parse to get event count
|
# Still parse to get event count
|
||||||
calendar = self.fetch_service.parse_ics(ics_content)
|
calendar = self.fetch_service.parse_ics(ics_content)
|
||||||
room_url = f"{settings.BASE_URL}/room/{room.name}"
|
room_url = f"{settings.UI_BASE_URL}/{room.name}"
|
||||||
events, total_events = self.fetch_service.extract_room_events(
|
events, total_events = self.fetch_service.extract_room_events(
|
||||||
calendar, room.name, room_url
|
calendar, room.name, room_url
|
||||||
)
|
)
|
||||||
@@ -243,7 +246,7 @@ class ICSSyncService:
|
|||||||
calendar = self.fetch_service.parse_ics(ics_content)
|
calendar = self.fetch_service.parse_ics(ics_content)
|
||||||
|
|
||||||
# Build room URL
|
# Build room URL
|
||||||
room_url = f"{settings.BASE_URL}/room/{room.name}"
|
room_url = f"{settings.UI_BASE_URL}/{room.name}"
|
||||||
|
|
||||||
# Extract matching events
|
# Extract matching events
|
||||||
events, total_events = self.fetch_service.extract_room_events(
|
events, total_events = self.fetch_service.extract_room_events(
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async def test_sync_room_ics_task():
|
|||||||
event.add("summary", "Task Test Meeting")
|
event.add("summary", "Task Test Meeting")
|
||||||
from reflector.settings import settings
|
from reflector.settings import settings
|
||||||
|
|
||||||
event.add("location", f"{settings.BASE_URL}/room/{room.name}")
|
event.add("location", f"{settings.UI_BASE_URL}/{room.name}")
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
event.add("dtstart", now + timedelta(hours=1))
|
event.add("dtstart", now + timedelta(hours=1))
|
||||||
event.add("dtend", now + timedelta(hours=2))
|
event.add("dtend", now + timedelta(hours=2))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from reflector.services.ics_sync import ICSFetchService, ICSSyncService
|
|||||||
async def test_ics_fetch_service_event_matching():
|
async def test_ics_fetch_service_event_matching():
|
||||||
service = ICSFetchService()
|
service = ICSFetchService()
|
||||||
room_name = "test-room"
|
room_name = "test-room"
|
||||||
room_url = "https://example.com/room/test-room"
|
room_url = "https://example.com/test-room"
|
||||||
|
|
||||||
# Create test event
|
# Create test event
|
||||||
event = Event()
|
event = Event()
|
||||||
@@ -21,12 +21,12 @@ async def test_ics_fetch_service_event_matching():
|
|||||||
event.add("summary", "Test Meeting")
|
event.add("summary", "Test Meeting")
|
||||||
|
|
||||||
# Test matching with full URL in location
|
# Test matching with full URL in location
|
||||||
event.add("location", "https://example.com/room/test-room")
|
event.add("location", "https://example.com/test-room")
|
||||||
assert service._event_matches_room(event, room_name, room_url) is True
|
assert service._event_matches_room(event, room_name, room_url) is True
|
||||||
|
|
||||||
# Test matching with URL without protocol
|
# Test non-matching with URL without protocol (exact matching only now)
|
||||||
event["location"] = "example.com/room/test-room"
|
event["location"] = "example.com/test-room"
|
||||||
assert service._event_matches_room(event, room_name, room_url) is True
|
assert service._event_matches_room(event, room_name, room_url) is False
|
||||||
|
|
||||||
# Test matching in description
|
# Test matching in description
|
||||||
event["location"] = "Conference Room A"
|
event["location"] = "Conference Room A"
|
||||||
@@ -39,7 +39,7 @@ async def test_ics_fetch_service_event_matching():
|
|||||||
assert service._event_matches_room(event, room_name, room_url) is False
|
assert service._event_matches_room(event, room_name, room_url) is False
|
||||||
|
|
||||||
# Test partial paths should NOT match anymore
|
# Test partial paths should NOT match anymore
|
||||||
event["location"] = "/room/test-room"
|
event["location"] = "/test-room"
|
||||||
assert service._event_matches_room(event, room_name, room_url) is False
|
assert service._event_matches_room(event, room_name, room_url) is False
|
||||||
|
|
||||||
event["location"] = f"Room: {room_name}"
|
event["location"] = f"Room: {room_name}"
|
||||||
@@ -55,7 +55,7 @@ async def test_ics_fetch_service_parse_event():
|
|||||||
event.add("uid", "test-456")
|
event.add("uid", "test-456")
|
||||||
event.add("summary", "Team Standup")
|
event.add("summary", "Team Standup")
|
||||||
event.add("description", "Daily team sync")
|
event.add("description", "Daily team sync")
|
||||||
event.add("location", "https://example.com/room/standup")
|
event.add("location", "https://example.com/standup")
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
event.add("dtstart", now)
|
event.add("dtstart", now)
|
||||||
@@ -73,7 +73,7 @@ async def test_ics_fetch_service_parse_event():
|
|||||||
assert result["ics_uid"] == "test-456"
|
assert result["ics_uid"] == "test-456"
|
||||||
assert result["title"] == "Team Standup"
|
assert result["title"] == "Team Standup"
|
||||||
assert result["description"] == "Daily team sync"
|
assert result["description"] == "Daily team sync"
|
||||||
assert result["location"] == "https://example.com/room/standup"
|
assert result["location"] == "https://example.com/standup"
|
||||||
assert len(result["attendees"]) == 3 # 2 attendees + 1 organizer
|
assert len(result["attendees"]) == 3 # 2 attendees + 1 organizer
|
||||||
|
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ async def test_ics_fetch_service_parse_event():
|
|||||||
async def test_ics_fetch_service_extract_room_events():
|
async def test_ics_fetch_service_extract_room_events():
|
||||||
service = ICSFetchService()
|
service = ICSFetchService()
|
||||||
room_name = "meeting"
|
room_name = "meeting"
|
||||||
room_url = "https://example.com/room/meeting"
|
room_url = "https://example.com/meeting"
|
||||||
|
|
||||||
# Create calendar with multiple events
|
# Create calendar with multiple events
|
||||||
cal = Calendar()
|
cal = Calendar()
|
||||||
@@ -100,7 +100,7 @@ async def test_ics_fetch_service_extract_room_events():
|
|||||||
event2 = Event()
|
event2 = Event()
|
||||||
event2.add("uid", "no-match")
|
event2.add("uid", "no-match")
|
||||||
event2.add("summary", "Other Meeting")
|
event2.add("summary", "Other Meeting")
|
||||||
event2.add("location", "https://example.com/room/other")
|
event2.add("location", "https://example.com/other")
|
||||||
event2.add("dtstart", now + timedelta(hours=4))
|
event2.add("dtstart", now + timedelta(hours=4))
|
||||||
event2.add("dtend", now + timedelta(hours=5))
|
event2.add("dtend", now + timedelta(hours=5))
|
||||||
cal.add_component(event2)
|
cal.add_component(event2)
|
||||||
@@ -125,9 +125,10 @@ async def test_ics_fetch_service_extract_room_events():
|
|||||||
cal.add_component(event4)
|
cal.add_component(event4)
|
||||||
|
|
||||||
# Extract events
|
# Extract events
|
||||||
events = service.extract_room_events(cal, room_name, room_url)
|
events, total_events = service.extract_room_events(cal, room_name, room_url)
|
||||||
|
|
||||||
assert len(events) == 2
|
assert len(events) == 2
|
||||||
|
assert total_events == 3 # 3 events in time window (excluding cancelled)
|
||||||
assert events[0]["ics_uid"] == "match-1"
|
assert events[0]["ics_uid"] == "match-1"
|
||||||
assert events[1]["ics_uid"] == "match-2"
|
assert events[1]["ics_uid"] == "match-2"
|
||||||
|
|
||||||
@@ -155,10 +156,10 @@ async def test_ics_sync_service_sync_room_calendar():
|
|||||||
event = Event()
|
event = Event()
|
||||||
event.add("uid", "sync-event-1")
|
event.add("uid", "sync-event-1")
|
||||||
event.add("summary", "Sync Test Meeting")
|
event.add("summary", "Sync Test Meeting")
|
||||||
# Use the actual BASE_URL from settings
|
# Use the actual UI_BASE_URL from settings
|
||||||
from reflector.settings import settings
|
from reflector.settings import settings
|
||||||
|
|
||||||
event.add("location", f"{settings.BASE_URL}/room/{room.name}")
|
event.add("location", f"{settings.UI_BASE_URL}/{room.name}")
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
event.add("dtstart", now + timedelta(hours=1))
|
event.add("dtstart", now + timedelta(hours=1))
|
||||||
event.add("dtend", now + timedelta(hours=2))
|
event.add("dtend", now + timedelta(hours=2))
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
Box,
|
Box,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { FaSync, FaCheckCircle, FaExclamationCircle } from "react-icons/fa";
|
import { LuRefreshCw } from "react-icons/lu";
|
||||||
|
import { FaCheckCircle, FaExclamationCircle } from "react-icons/fa";
|
||||||
import { useRoomIcsSync, useRoomIcsStatus } from "../../../lib/apiHooks";
|
import { useRoomIcsSync, useRoomIcsStatus } from "../../../lib/apiHooks";
|
||||||
|
|
||||||
interface ICSSettingsProps {
|
interface ICSSettingsProps {
|
||||||
@@ -26,6 +27,7 @@ interface ICSSettingsProps {
|
|||||||
icsLastEtag?: string;
|
icsLastEtag?: string;
|
||||||
onChange: (settings: Partial<ICSSettingsData>) => void;
|
onChange: (settings: Partial<ICSSettingsData>) => void;
|
||||||
isOwner?: boolean;
|
isOwner?: boolean;
|
||||||
|
isEditing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICSSettingsData {
|
export interface ICSSettingsData {
|
||||||
@@ -52,12 +54,18 @@ export default function ICSSettings({
|
|||||||
icsLastEtag,
|
icsLastEtag,
|
||||||
onChange,
|
onChange,
|
||||||
isOwner = true,
|
isOwner = true,
|
||||||
|
isEditing = false,
|
||||||
}: ICSSettingsProps) {
|
}: ICSSettingsProps) {
|
||||||
const [syncStatus, setSyncStatus] = useState<
|
const [syncStatus, setSyncStatus] = useState<
|
||||||
"idle" | "syncing" | "success" | "error"
|
"idle" | "syncing" | "success" | "error"
|
||||||
>("idle");
|
>("idle");
|
||||||
const [syncMessage, setSyncMessage] = useState<string>("");
|
const [syncMessage, setSyncMessage] = useState<string>("");
|
||||||
const [testResult, setTestResult] = useState<string>("");
|
const [syncResult, setSyncResult] = useState<{
|
||||||
|
eventsFound: number;
|
||||||
|
totalEvents: number;
|
||||||
|
eventsCreated: number;
|
||||||
|
eventsUpdated: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// React Query hooks
|
// React Query hooks
|
||||||
const syncMutation = useRoomIcsSync();
|
const syncMutation = useRoomIcsSync();
|
||||||
@@ -67,46 +75,21 @@ export default function ICSSettings({
|
|||||||
items: fetchIntervalOptions,
|
items: fetchIntervalOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTestConnection = async () => {
|
// Clear sync results when dialog closes
|
||||||
if (!icsUrl || !roomName) return;
|
useEffect(() => {
|
||||||
|
if (!isEditing) {
|
||||||
setSyncStatus("syncing");
|
setSyncStatus("idle");
|
||||||
setTestResult("");
|
setSyncResult(null);
|
||||||
|
setSyncMessage("");
|
||||||
try {
|
|
||||||
// First notify parent to update the room with the ICS URL
|
|
||||||
onChange({
|
|
||||||
ics_url: icsUrl,
|
|
||||||
ics_enabled: true,
|
|
||||||
ics_fetch_interval: icsFetchInterval,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then trigger a sync
|
|
||||||
const result = await syncMutation.mutateAsync({
|
|
||||||
params: {
|
|
||||||
path: { room_name: roomName },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status === "success") {
|
|
||||||
setSyncStatus("success");
|
|
||||||
setTestResult(
|
|
||||||
`Successfully synced! Found ${result.events_found} events.`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSyncStatus("error");
|
|
||||||
setTestResult(result.error || "Sync failed");
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
}, [isEditing]);
|
||||||
setSyncStatus("error");
|
|
||||||
setTestResult(err.body?.detail || "Failed to test ICS connection");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleManualSync = async () => {
|
const handleForceSync = async () => {
|
||||||
if (!roomName) return;
|
if (!roomName || !isEditing) return;
|
||||||
|
|
||||||
|
// Clear previous results
|
||||||
setSyncStatus("syncing");
|
setSyncStatus("syncing");
|
||||||
|
setSyncResult(null);
|
||||||
setSyncMessage("");
|
setSyncMessage("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -116,26 +99,22 @@ export default function ICSSettings({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.status === "success") {
|
if (result.status === "success" || result.status === "unchanged") {
|
||||||
setSyncStatus("success");
|
setSyncStatus("success");
|
||||||
setSyncMessage(
|
setSyncResult({
|
||||||
`Sync complete! Found ${result.events_found} events, ` +
|
eventsFound: result.events_found || 0,
|
||||||
`created ${result.events_created}, updated ${result.events_updated}.`,
|
totalEvents: result.total_events || 0,
|
||||||
);
|
eventsCreated: result.events_created || 0,
|
||||||
|
eventsUpdated: result.events_updated || 0,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setSyncStatus("error");
|
setSyncStatus("error");
|
||||||
setSyncMessage(result.error || "Sync failed");
|
setSyncMessage(result.error || "Sync failed");
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setSyncStatus("error");
|
setSyncStatus("error");
|
||||||
setSyncMessage(err.body?.detail || "Failed to sync calendar");
|
setSyncMessage(err.body?.detail || "Failed to force sync calendar");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear status after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
setSyncStatus("idle");
|
|
||||||
setSyncMessage("");
|
|
||||||
}, 5000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isOwner) {
|
if (!isOwner) {
|
||||||
@@ -198,46 +177,48 @@ export default function ICSSettings({
|
|||||||
</Field.HelperText>
|
</Field.HelperText>
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
|
|
||||||
{icsUrl && (
|
{icsUrl && isEditing && roomName && (
|
||||||
<HStack gap={3}>
|
<HStack gap={3}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleTestConnection}
|
onClick={handleForceSync}
|
||||||
disabled={syncStatus === "syncing"}
|
disabled={syncStatus === "syncing"}
|
||||||
>
|
>
|
||||||
{syncStatus === "syncing" && <Spinner size="sm" />}
|
{syncStatus === "syncing" ? (
|
||||||
Test Connection
|
<Spinner size="sm" />
|
||||||
</Button>
|
) : (
|
||||||
|
<LuRefreshCw />
|
||||||
{roomName && icsLastSync && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleManualSync}
|
|
||||||
disabled={syncStatus === "syncing"}
|
|
||||||
>
|
|
||||||
<FaSync />
|
|
||||||
Sync Now
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
Force Sync
|
||||||
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{testResult && (
|
{syncResult && syncStatus === "success" && (
|
||||||
<Box
|
<Box
|
||||||
p={3}
|
p={3}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
bg={syncStatus === "success" ? "green.50" : "red.50"}
|
bg="green.50"
|
||||||
borderLeft="4px solid"
|
borderLeft="4px solid"
|
||||||
borderColor={syncStatus === "success" ? "green.400" : "red.400"}
|
borderColor="green.400"
|
||||||
>
|
>
|
||||||
<Text
|
<VStack gap={1} align="stretch">
|
||||||
fontSize="sm"
|
<Text fontSize="sm" color="green.800" fontWeight="medium">
|
||||||
color={syncStatus === "success" ? "green.800" : "red.800"}
|
Sync completed
|
||||||
>
|
|
||||||
{testResult}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text fontSize="sm" color="green.700">
|
||||||
|
{syncResult.totalEvents} events downloaded,{" "}
|
||||||
|
{syncResult.eventsFound} match this room
|
||||||
|
</Text>
|
||||||
|
{(syncResult.eventsCreated > 0 ||
|
||||||
|
syncResult.eventsUpdated > 0) && (
|
||||||
|
<Text fontSize="sm" color="green.700">
|
||||||
|
{syncResult.eventsCreated} created,{" "}
|
||||||
|
{syncResult.eventsUpdated} updated
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ export default function RoomsList() {
|
|||||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="calendar">Calendar</Tabs.Trigger>
|
<Tabs.Trigger value="calendar">Calendar</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="share">Share</Tabs.Trigger>
|
<Tabs.Trigger value="share">Share</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="webhook">Webhook</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<Tabs.Content value="general" pt={6}>
|
<Tabs.Content value="general" pt={6}>
|
||||||
@@ -613,9 +614,134 @@ export default function RoomsList() {
|
|||||||
<Checkbox.Label>Shared room</Checkbox.Label>
|
<Checkbox.Label>Shared room</Checkbox.Label>
|
||||||
</Checkbox.Root>
|
</Checkbox.Root>
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
{/* Webhook Configuration Section */}
|
<Tabs.Content value="calendar" pt={6}>
|
||||||
<Field.Root mt={8}>
|
<ICSSettings
|
||||||
|
roomId={editRoomId ?? undefined}
|
||||||
|
roomName={room.name}
|
||||||
|
icsUrl={room.icsUrl}
|
||||||
|
icsEnabled={room.icsEnabled}
|
||||||
|
icsFetchInterval={room.icsFetchInterval}
|
||||||
|
onChange={(settings) => {
|
||||||
|
setRoomInput({
|
||||||
|
...room,
|
||||||
|
icsUrl:
|
||||||
|
settings.ics_url !== undefined
|
||||||
|
? settings.ics_url
|
||||||
|
: room.icsUrl,
|
||||||
|
icsEnabled:
|
||||||
|
settings.ics_enabled !== undefined
|
||||||
|
? settings.ics_enabled
|
||||||
|
: room.icsEnabled,
|
||||||
|
icsFetchInterval:
|
||||||
|
settings.ics_fetch_interval !== undefined
|
||||||
|
? settings.ics_fetch_interval
|
||||||
|
: room.icsFetchInterval,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isOwner={true}
|
||||||
|
isEditing={isEditing}
|
||||||
|
/>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="share" pt={6}>
|
||||||
|
<Field.Root>
|
||||||
|
<Checkbox.Root
|
||||||
|
name="zulipAutoPost"
|
||||||
|
checked={room.zulipAutoPost}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: {
|
||||||
|
name: "zulipAutoPost",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: e.checked,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
handleRoomChange(syntheticEvent);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox.HiddenInput />
|
||||||
|
<Checkbox.Control>
|
||||||
|
<Checkbox.Indicator />
|
||||||
|
</Checkbox.Control>
|
||||||
|
<Checkbox.Label>
|
||||||
|
Automatically post transcription to Zulip
|
||||||
|
</Checkbox.Label>
|
||||||
|
</Checkbox.Root>
|
||||||
|
</Field.Root>
|
||||||
|
|
||||||
|
<Field.Root mt={4}>
|
||||||
|
<Field.Label>Zulip stream</Field.Label>
|
||||||
|
<Select.Root
|
||||||
|
value={room.zulipStream ? [room.zulipStream] : []}
|
||||||
|
onValueChange={(e) =>
|
||||||
|
setRoomInput({
|
||||||
|
...room,
|
||||||
|
zulipStream: e.value[0],
|
||||||
|
zulipTopic: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
collection={streamCollection}
|
||||||
|
disabled={!room.zulipAutoPost}
|
||||||
|
>
|
||||||
|
<Select.HiddenSelect />
|
||||||
|
<Select.Control>
|
||||||
|
<Select.Trigger>
|
||||||
|
<Select.ValueText placeholder="Select stream" />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.IndicatorGroup>
|
||||||
|
<Select.Indicator />
|
||||||
|
</Select.IndicatorGroup>
|
||||||
|
</Select.Control>
|
||||||
|
<Select.Positioner>
|
||||||
|
<Select.Content>
|
||||||
|
{streamOptions.map((option) => (
|
||||||
|
<Select.Item key={option.value} item={option}>
|
||||||
|
{option.label}
|
||||||
|
<Select.ItemIndicator />
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Positioner>
|
||||||
|
</Select.Root>
|
||||||
|
</Field.Root>
|
||||||
|
|
||||||
|
<Field.Root mt={4}>
|
||||||
|
<Field.Label>Zulip topic</Field.Label>
|
||||||
|
<Select.Root
|
||||||
|
value={room.zulipTopic ? [room.zulipTopic] : []}
|
||||||
|
onValueChange={(e) =>
|
||||||
|
setRoomInput({ ...room, zulipTopic: e.value[0] })
|
||||||
|
}
|
||||||
|
collection={topicCollection}
|
||||||
|
disabled={!room.zulipAutoPost}
|
||||||
|
>
|
||||||
|
<Select.HiddenSelect />
|
||||||
|
<Select.Control>
|
||||||
|
<Select.Trigger>
|
||||||
|
<Select.ValueText placeholder="Select topic" />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.IndicatorGroup>
|
||||||
|
<Select.Indicator />
|
||||||
|
</Select.IndicatorGroup>
|
||||||
|
</Select.Control>
|
||||||
|
<Select.Positioner>
|
||||||
|
<Select.Content>
|
||||||
|
{topicOptions.map((option) => (
|
||||||
|
<Select.Item key={option.value} item={option}>
|
||||||
|
{option.label}
|
||||||
|
<Select.ItemIndicator />
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Positioner>
|
||||||
|
</Select.Root>
|
||||||
|
</Field.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="webhook" pt={6}>
|
||||||
|
<Field.Root>
|
||||||
<Field.Label>Webhook URL</Field.Label>
|
<Field.Label>Webhook URL</Field.Label>
|
||||||
<Input
|
<Input
|
||||||
name="webhookUrl"
|
name="webhookUrl"
|
||||||
@@ -718,129 +844,6 @@ export default function RoomsList() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
||||||
<Tabs.Content value="calendar" pt={6}>
|
|
||||||
<ICSSettings
|
|
||||||
roomId={editRoomId ?? undefined}
|
|
||||||
roomName={room.name}
|
|
||||||
icsUrl={room.icsUrl}
|
|
||||||
icsEnabled={room.icsEnabled}
|
|
||||||
icsFetchInterval={room.icsFetchInterval}
|
|
||||||
onChange={(settings) => {
|
|
||||||
setRoomInput({
|
|
||||||
...room,
|
|
||||||
icsUrl:
|
|
||||||
settings.ics_url !== undefined
|
|
||||||
? settings.ics_url
|
|
||||||
: room.icsUrl,
|
|
||||||
icsEnabled:
|
|
||||||
settings.ics_enabled !== undefined
|
|
||||||
? settings.ics_enabled
|
|
||||||
: room.icsEnabled,
|
|
||||||
icsFetchInterval:
|
|
||||||
settings.ics_fetch_interval !== undefined
|
|
||||||
? settings.ics_fetch_interval
|
|
||||||
: room.icsFetchInterval,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
isOwner={true}
|
|
||||||
/>
|
|
||||||
</Tabs.Content>
|
|
||||||
|
|
||||||
<Tabs.Content value="share" pt={6}>
|
|
||||||
<Field.Root>
|
|
||||||
<Checkbox.Root
|
|
||||||
name="zulipAutoPost"
|
|
||||||
checked={room.zulipAutoPost}
|
|
||||||
onCheckedChange={(e) => {
|
|
||||||
const syntheticEvent = {
|
|
||||||
target: {
|
|
||||||
name: "zulipAutoPost",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: e.checked,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
handleRoomChange(syntheticEvent);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox.HiddenInput />
|
|
||||||
<Checkbox.Control>
|
|
||||||
<Checkbox.Indicator />
|
|
||||||
</Checkbox.Control>
|
|
||||||
<Checkbox.Label>
|
|
||||||
Automatically post transcription to Zulip
|
|
||||||
</Checkbox.Label>
|
|
||||||
</Checkbox.Root>
|
|
||||||
</Field.Root>
|
|
||||||
|
|
||||||
<Field.Root mt={4}>
|
|
||||||
<Field.Label>Zulip stream</Field.Label>
|
|
||||||
<Select.Root
|
|
||||||
value={room.zulipStream ? [room.zulipStream] : []}
|
|
||||||
onValueChange={(e) =>
|
|
||||||
setRoomInput({
|
|
||||||
...room,
|
|
||||||
zulipStream: e.value[0],
|
|
||||||
zulipTopic: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
collection={streamCollection}
|
|
||||||
disabled={!room.zulipAutoPost}
|
|
||||||
>
|
|
||||||
<Select.HiddenSelect />
|
|
||||||
<Select.Control>
|
|
||||||
<Select.Trigger>
|
|
||||||
<Select.ValueText placeholder="Select stream" />
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.IndicatorGroup>
|
|
||||||
<Select.Indicator />
|
|
||||||
</Select.IndicatorGroup>
|
|
||||||
</Select.Control>
|
|
||||||
<Select.Positioner>
|
|
||||||
<Select.Content>
|
|
||||||
{streamOptions.map((option) => (
|
|
||||||
<Select.Item key={option.value} item={option}>
|
|
||||||
{option.label}
|
|
||||||
<Select.ItemIndicator />
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Positioner>
|
|
||||||
</Select.Root>
|
|
||||||
</Field.Root>
|
|
||||||
|
|
||||||
<Field.Root mt={4}>
|
|
||||||
<Field.Label>Zulip topic</Field.Label>
|
|
||||||
<Select.Root
|
|
||||||
value={room.zulipTopic ? [room.zulipTopic] : []}
|
|
||||||
onValueChange={(e) =>
|
|
||||||
setRoomInput({ ...room, zulipTopic: e.value[0] })
|
|
||||||
}
|
|
||||||
collection={topicCollection}
|
|
||||||
disabled={!room.zulipAutoPost}
|
|
||||||
>
|
|
||||||
<Select.HiddenSelect />
|
|
||||||
<Select.Control>
|
|
||||||
<Select.Trigger>
|
|
||||||
<Select.ValueText placeholder="Select topic" />
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.IndicatorGroup>
|
|
||||||
<Select.Indicator />
|
|
||||||
</Select.IndicatorGroup>
|
|
||||||
</Select.Control>
|
|
||||||
<Select.Positioner>
|
|
||||||
<Select.Content>
|
|
||||||
{topicOptions.map((option) => (
|
|
||||||
<Select.Item key={option.value} item={option}>
|
|
||||||
{option.label}
|
|
||||||
<Select.ItemIndicator />
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Positioner>
|
|
||||||
</Select.Root>
|
|
||||||
</Field.Root>
|
|
||||||
</Tabs.Content>
|
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
</Dialog.Body>
|
</Dialog.Body>
|
||||||
<Dialog.Footer>
|
<Dialog.Footer>
|
||||||
|
|||||||
Reference in New Issue
Block a user