mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 12:49:06 +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
|
||||
|
||||
|
||||
class SyncResult(TypedDict, total=False):
|
||||
class SyncResultBase(TypedDict):
|
||||
status: str # "success", "unchanged", "error", "skipped"
|
||||
|
||||
|
||||
class SyncResult(SyncResultBase, total=False):
|
||||
hash: str | None
|
||||
events_found: int
|
||||
total_events: int
|
||||
@@ -73,41 +76,41 @@ class ICSFetchService:
|
||||
window_end = now + timedelta(hours=24)
|
||||
|
||||
for component in calendar.walk():
|
||||
if component.name == "VEVENT":
|
||||
# Skip cancelled events
|
||||
status = component.get("STATUS", "").upper()
|
||||
if status == "CANCELLED":
|
||||
continue
|
||||
if component.name != "VEVENT":
|
||||
continue
|
||||
|
||||
# Count total non-cancelled events in the time window
|
||||
event_data = self._parse_event(component)
|
||||
if (
|
||||
event_data
|
||||
and window_start <= event_data["start_time"] <= window_end
|
||||
):
|
||||
total_events += 1
|
||||
status = component.get("STATUS", "").upper()
|
||||
if status == "CANCELLED":
|
||||
continue
|
||||
|
||||
# Check if event matches this room
|
||||
if self._event_matches_room(component, room_name, room_url):
|
||||
events.append(event_data)
|
||||
# Count total non-cancelled events in the time window
|
||||
event_data = self._parse_event(component)
|
||||
print(room_url, event_data)
|
||||
if event_data and window_start <= event_data["start_time"] <= window_end:
|
||||
total_events += 1
|
||||
|
||||
# Check if event matches this room
|
||||
if self._event_matches_room(component, room_name, room_url):
|
||||
events.append(event_data)
|
||||
|
||||
return events, total_events
|
||||
|
||||
def _event_matches_room(self, event: Event, room_name: str, room_url: str) -> bool:
|
||||
print("_____", room_url)
|
||||
location = str(event.get("LOCATION", ""))
|
||||
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 = [
|
||||
room_url, # Full URL with protocol
|
||||
room_url.replace("https://", ""), # Without https protocol
|
||||
room_url.replace("http://", ""), # Without http protocol
|
||||
room_url,
|
||||
]
|
||||
|
||||
# Check location and description for patterns
|
||||
text_to_check = f"{location} {description}".lower()
|
||||
|
||||
for pattern in patterns:
|
||||
print(text_to_check, pattern.lower())
|
||||
if pattern.lower() in text_to_check:
|
||||
return True
|
||||
|
||||
@@ -225,7 +228,7 @@ class ICSSyncService:
|
||||
logger.info(f"No changes in ICS for room {room.id}")
|
||||
# Still parse to get event count
|
||||
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(
|
||||
calendar, room.name, room_url
|
||||
)
|
||||
@@ -243,7 +246,7 @@ class ICSSyncService:
|
||||
calendar = self.fetch_service.parse_ics(ics_content)
|
||||
|
||||
# Build room URL
|
||||
room_url = f"{settings.BASE_URL}/room/{room.name}"
|
||||
room_url = f"{settings.UI_BASE_URL}/{room.name}"
|
||||
|
||||
# Extract matching 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")
|
||||
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)
|
||||
event.add("dtstart", now + timedelta(hours=1))
|
||||
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():
|
||||
service = ICSFetchService()
|
||||
room_name = "test-room"
|
||||
room_url = "https://example.com/room/test-room"
|
||||
room_url = "https://example.com/test-room"
|
||||
|
||||
# Create test event
|
||||
event = Event()
|
||||
@@ -21,12 +21,12 @@ async def test_ics_fetch_service_event_matching():
|
||||
event.add("summary", "Test Meeting")
|
||||
|
||||
# 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
|
||||
|
||||
# Test matching with URL without protocol
|
||||
event["location"] = "example.com/room/test-room"
|
||||
assert service._event_matches_room(event, room_name, room_url) is True
|
||||
# Test non-matching with URL without protocol (exact matching only now)
|
||||
event["location"] = "example.com/test-room"
|
||||
assert service._event_matches_room(event, room_name, room_url) is False
|
||||
|
||||
# Test matching in description
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
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("summary", "Team Standup")
|
||||
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)
|
||||
event.add("dtstart", now)
|
||||
@@ -73,7 +73,7 @@ async def test_ics_fetch_service_parse_event():
|
||||
assert result["ics_uid"] == "test-456"
|
||||
assert result["title"] == "Team Standup"
|
||||
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
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ async def test_ics_fetch_service_parse_event():
|
||||
async def test_ics_fetch_service_extract_room_events():
|
||||
service = ICSFetchService()
|
||||
room_name = "meeting"
|
||||
room_url = "https://example.com/room/meeting"
|
||||
room_url = "https://example.com/meeting"
|
||||
|
||||
# Create calendar with multiple events
|
||||
cal = Calendar()
|
||||
@@ -100,7 +100,7 @@ async def test_ics_fetch_service_extract_room_events():
|
||||
event2 = Event()
|
||||
event2.add("uid", "no-match")
|
||||
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("dtend", now + timedelta(hours=5))
|
||||
cal.add_component(event2)
|
||||
@@ -125,9 +125,10 @@ async def test_ics_fetch_service_extract_room_events():
|
||||
cal.add_component(event4)
|
||||
|
||||
# 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 total_events == 3 # 3 events in time window (excluding cancelled)
|
||||
assert events[0]["ics_uid"] == "match-1"
|
||||
assert events[1]["ics_uid"] == "match-2"
|
||||
|
||||
@@ -155,10 +156,10 @@ async def test_ics_sync_service_sync_room_calendar():
|
||||
event = Event()
|
||||
event.add("uid", "sync-event-1")
|
||||
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
|
||||
|
||||
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)
|
||||
event.add("dtstart", now + timedelta(hours=1))
|
||||
event.add("dtend", now + timedelta(hours=2))
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
Spinner,
|
||||
Box,
|
||||
} from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import { FaSync, FaCheckCircle, FaExclamationCircle } from "react-icons/fa";
|
||||
import { useState, useEffect } from "react";
|
||||
import { LuRefreshCw } from "react-icons/lu";
|
||||
import { FaCheckCircle, FaExclamationCircle } from "react-icons/fa";
|
||||
import { useRoomIcsSync, useRoomIcsStatus } from "../../../lib/apiHooks";
|
||||
|
||||
interface ICSSettingsProps {
|
||||
@@ -26,6 +27,7 @@ interface ICSSettingsProps {
|
||||
icsLastEtag?: string;
|
||||
onChange: (settings: Partial<ICSSettingsData>) => void;
|
||||
isOwner?: boolean;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
export interface ICSSettingsData {
|
||||
@@ -52,12 +54,18 @@ export default function ICSSettings({
|
||||
icsLastEtag,
|
||||
onChange,
|
||||
isOwner = true,
|
||||
isEditing = false,
|
||||
}: ICSSettingsProps) {
|
||||
const [syncStatus, setSyncStatus] = useState<
|
||||
"idle" | "syncing" | "success" | "error"
|
||||
>("idle");
|
||||
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
|
||||
const syncMutation = useRoomIcsSync();
|
||||
@@ -67,46 +75,21 @@ export default function ICSSettings({
|
||||
items: fetchIntervalOptions,
|
||||
});
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!icsUrl || !roomName) return;
|
||||
|
||||
setSyncStatus("syncing");
|
||||
setTestResult("");
|
||||
|
||||
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) {
|
||||
setSyncStatus("error");
|
||||
setTestResult(err.body?.detail || "Failed to test ICS connection");
|
||||
// Clear sync results when dialog closes
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
setSyncStatus("idle");
|
||||
setSyncResult(null);
|
||||
setSyncMessage("");
|
||||
}
|
||||
};
|
||||
}, [isEditing]);
|
||||
|
||||
const handleManualSync = async () => {
|
||||
if (!roomName) return;
|
||||
const handleForceSync = async () => {
|
||||
if (!roomName || !isEditing) return;
|
||||
|
||||
// Clear previous results
|
||||
setSyncStatus("syncing");
|
||||
setSyncResult(null);
|
||||
setSyncMessage("");
|
||||
|
||||
try {
|
||||
@@ -116,26 +99,22 @@ export default function ICSSettings({
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === "success") {
|
||||
if (result.status === "success" || result.status === "unchanged") {
|
||||
setSyncStatus("success");
|
||||
setSyncMessage(
|
||||
`Sync complete! Found ${result.events_found} events, ` +
|
||||
`created ${result.events_created}, updated ${result.events_updated}.`,
|
||||
);
|
||||
setSyncResult({
|
||||
eventsFound: result.events_found || 0,
|
||||
totalEvents: result.total_events || 0,
|
||||
eventsCreated: result.events_created || 0,
|
||||
eventsUpdated: result.events_updated || 0,
|
||||
});
|
||||
} else {
|
||||
setSyncStatus("error");
|
||||
setSyncMessage(result.error || "Sync failed");
|
||||
}
|
||||
} catch (err: any) {
|
||||
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) {
|
||||
@@ -198,46 +177,48 @@ export default function ICSSettings({
|
||||
</Field.HelperText>
|
||||
</Field.Root>
|
||||
|
||||
{icsUrl && (
|
||||
{icsUrl && isEditing && roomName && (
|
||||
<HStack gap={3}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
onClick={handleForceSync}
|
||||
disabled={syncStatus === "syncing"}
|
||||
>
|
||||
{syncStatus === "syncing" && <Spinner size="sm" />}
|
||||
Test Connection
|
||||
{syncStatus === "syncing" ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<LuRefreshCw />
|
||||
)}
|
||||
Force Sync
|
||||
</Button>
|
||||
|
||||
{roomName && icsLastSync && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleManualSync}
|
||||
disabled={syncStatus === "syncing"}
|
||||
>
|
||||
<FaSync />
|
||||
Sync Now
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{testResult && (
|
||||
{syncResult && syncStatus === "success" && (
|
||||
<Box
|
||||
p={3}
|
||||
borderRadius="md"
|
||||
bg={syncStatus === "success" ? "green.50" : "red.50"}
|
||||
bg="green.50"
|
||||
borderLeft="4px solid"
|
||||
borderColor={syncStatus === "success" ? "green.400" : "red.400"}
|
||||
borderColor="green.400"
|
||||
>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={syncStatus === "success" ? "green.800" : "red.800"}
|
||||
>
|
||||
{testResult}
|
||||
</Text>
|
||||
<VStack gap={1} align="stretch">
|
||||
<Text fontSize="sm" color="green.800" fontWeight="medium">
|
||||
Sync completed
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -448,6 +448,7 @@ export default function RoomsList() {
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="calendar">Calendar</Tabs.Trigger>
|
||||
<Tabs.Trigger value="share">Share</Tabs.Trigger>
|
||||
<Tabs.Trigger value="webhook">Webhook</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="general" pt={6}>
|
||||
@@ -613,9 +614,134 @@ export default function RoomsList() {
|
||||
<Checkbox.Label>Shared room</Checkbox.Label>
|
||||
</Checkbox.Root>
|
||||
</Field.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
{/* Webhook Configuration Section */}
|
||||
<Field.Root mt={8}>
|
||||
<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}
|
||||
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>
|
||||
<Input
|
||||
name="webhookUrl"
|
||||
@@ -718,129 +844,6 @@ export default function RoomsList() {
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer>
|
||||
|
||||
Reference in New Issue
Block a user