Compare commits

..

6 Commits

Author SHA1 Message Date
6ddfee0b4e chore(main): release 0.13.0 (#661) 2025-09-21 20:50:47 -06:00
Igor Monadical
47716f6e5d feat: room form edit with enter (#662)
* room form edit with enter

* mobile form enter do nothing

* restore overwritten older change

---------

Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
2025-09-19 15:14:40 -04:00
0abcebfc94 fix: invalid cleanup call (#660) 2025-09-18 10:02:30 -06:00
Igor Monadical
2b723da08b rooms-page-calendar-ics-room-name-fix (#659)
Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
2025-09-17 20:02:17 -04:00
6566e04300 chore(main): release 0.12.1 (#658) 2025-09-17 17:17:22 -06:00
870e860517 fix: production blocked because having existing meeting with room_id null (#657) 2025-09-17 17:09:54 -06:00
5 changed files with 455 additions and 425 deletions

View File

@@ -1,5 +1,24 @@
# Changelog # Changelog
## [0.13.0](https://github.com/Monadical-SAS/reflector/compare/v0.12.1...v0.13.0) (2025-09-19)
### Features
* room form edit with enter ([#662](https://github.com/Monadical-SAS/reflector/issues/662)) ([47716f6](https://github.com/Monadical-SAS/reflector/commit/47716f6e5ddee952609d2fa0ffabdfa865286796))
### Bug Fixes
* invalid cleanup call ([#660](https://github.com/Monadical-SAS/reflector/issues/660)) ([0abcebf](https://github.com/Monadical-SAS/reflector/commit/0abcebfc9491f87f605f21faa3e53996fafedd9a))
## [0.12.1](https://github.com/Monadical-SAS/reflector/compare/v0.12.0...v0.12.1) (2025-09-17)
### Bug Fixes
* production blocked because having existing meeting with room_id null ([#657](https://github.com/Monadical-SAS/reflector/issues/657)) ([870e860](https://github.com/Monadical-SAS/reflector/commit/870e8605171a27155a9cbee215eeccb9a8d6c0a2))
## [0.12.0](https://github.com/Monadical-SAS/reflector/compare/v0.11.0...v0.12.0) (2025-09-17) ## [0.12.0](https://github.com/Monadical-SAS/reflector/compare/v0.11.0...v0.12.0) (2025-09-17)

View File

@@ -8,7 +8,6 @@ Create Date: 2025-09-10 10:47:06.006819
from typing import Sequence, Union from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
@@ -21,7 +20,6 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("meeting", schema=None) as batch_op: with op.batch_alter_table("meeting", schema=None) as batch_op:
batch_op.alter_column("room_id", existing_type=sa.VARCHAR(), nullable=False)
batch_op.create_foreign_key( batch_op.create_foreign_key(
None, "room", ["room_id"], ["id"], ondelete="CASCADE" None, "room", ["room_id"], ["id"], ondelete="CASCADE"
) )
@@ -33,6 +31,5 @@ def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("meeting", schema=None) as batch_op: with op.batch_alter_table("meeting", schema=None) as batch_op:
batch_op.drop_constraint("meeting_room_id_fkey", type_="foreignkey") batch_op.drop_constraint("meeting_room_id_fkey", type_="foreignkey")
batch_op.alter_column("room_id", existing_type=sa.VARCHAR(), nullable=True)
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@@ -5,7 +5,6 @@ Deletes old anonymous transcripts and their associated meetings/recordings.
Transcripts are the main entry point - any associated data is also removed. Transcripts are the main entry point - any associated data is also removed.
""" """
import asyncio
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import TypedDict from typing import TypedDict
@@ -152,5 +151,5 @@ async def cleanup_old_public_data(
retry_kwargs={"max_retries": 3, "countdown": 300}, retry_kwargs={"max_retries": 3, "countdown": 300},
) )
@asynctask @asynctask
def cleanup_old_public_data_task(days: int | None = None): async def cleanup_old_public_data_task(days: int | None = None):
asyncio.run(cleanup_old_public_data(days=days)) await cleanup_old_public_data(days=days)

View File

@@ -27,7 +27,7 @@ import {
} from "../../../lib/utils"; } from "../../../lib/utils";
interface ICSSettingsProps { interface ICSSettingsProps {
roomName: NonEmptyString; roomName: NonEmptyString | null;
icsUrl?: string; icsUrl?: string;
icsEnabled?: boolean; icsEnabled?: boolean;
icsFetchInterval?: number; icsFetchInterval?: number;
@@ -85,7 +85,7 @@ export default function ICSSettings({
const handleCopyRoomUrl = async () => { const handleCopyRoomUrl = async () => {
try { try {
await navigator.clipboard.writeText( await navigator.clipboard.writeText(
roomAbsoluteUrl(assertExistsAndNonEmptyString(roomName)), roomAbsoluteUrl(assertExists(roomName)),
); );
setJustCopied(true); setJustCopied(true);
@@ -123,7 +123,7 @@ export default function ICSSettings({
const handleRoomUrlClick = () => { const handleRoomUrlClick = () => {
if (roomUrlInputRef.current) { if (roomUrlInputRef.current) {
roomUrlInputRef.current.select(); roomUrlInputRef.current.select();
handleCopyRoomUrl(); handleCopyRoomUrl().then(() => {});
} }
}; };
@@ -196,30 +196,32 @@ export default function ICSSettings({
To enable Reflector to recognize your calendar events as meetings, To enable Reflector to recognize your calendar events as meetings,
add this URL as the location in your calendar events add this URL as the location in your calendar events
</Field.HelperText> </Field.HelperText>
<HStack gap={0} position="relative" width="100%"> {roomName ? (
<Input <HStack gap={0} position="relative" width="100%">
ref={roomUrlInputRef} <Input
value={roomAbsoluteUrl(parseNonEmptyString(roomName))} ref={roomUrlInputRef}
readOnly value={roomAbsoluteUrl(parseNonEmptyString(roomName))}
onClick={handleRoomUrlClick} readOnly
cursor="pointer" onClick={handleRoomUrlClick}
bg="gray.100" cursor="pointer"
_hover={{ bg: "gray.200" }} bg="gray.100"
_focus={{ bg: "gray.200" }} _hover={{ bg: "gray.200" }}
pr="90px" _focus={{ bg: "gray.200" }}
width="100%" pr="90px"
/> width="100%"
<HStack position="absolute" right="4px" gap={1} zIndex={1}> />
<IconButton <HStack position="absolute" right="4px" gap={1} zIndex={1}>
aria-label="Copy room URL" <IconButton
onClick={handleCopyRoomUrl} aria-label="Copy room URL"
variant="ghost" onClick={handleCopyRoomUrl}
size="sm" variant="ghost"
> size="sm"
{justCopied ? <LuCheck /> : <LuCopy />} >
</IconButton> {justCopied ? <LuCheck /> : <LuCopy />}
</IconButton>
</HStack>
</HStack> </HStack>
</HStack> ) : null}
</Field.Root> </Field.Root>
<Field.Root> <Field.Root>

View File

@@ -309,7 +309,7 @@ export default function RoomsList() {
setRoomInput(null); setRoomInput(null);
setIsEditing(false); setIsEditing(false);
setEditRoomId(""); setEditRoomId(null);
setNameError(""); setNameError("");
refetch(); refetch();
onClose(); onClose();
@@ -449,415 +449,428 @@ export default function RoomsList() {
</Dialog.CloseTrigger> </Dialog.CloseTrigger>
</Dialog.Header> </Dialog.Header>
<Dialog.Body> <Dialog.Body>
<Tabs.Root defaultValue="general"> <form
<Tabs.List> id="room-form"
<Tabs.Trigger value="general">General</Tabs.Trigger> onSubmit={(e) => {
<Tabs.Trigger value="calendar">Calendar</Tabs.Trigger> e.preventDefault();
<Tabs.Trigger value="share">Share</Tabs.Trigger> handleSaveRoom();
<Tabs.Trigger value="webhook">WebHook</Tabs.Trigger> }}
</Tabs.List> >
<Tabs.Root defaultValue="general">
<Tabs.List>
<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}> <Tabs.Content value="general" pt={6}>
<Field.Root> <Field.Root>
<Field.Label>Room name</Field.Label> <Field.Label>Room name</Field.Label>
<Input <Input
name="name" name="name"
placeholder="room-name" placeholder="room-name"
value={room.name} value={room.name}
onChange={handleRoomChange} onChange={handleRoomChange}
/> enterKeyHint="next"
<Field.HelperText> />
No spaces or special characters allowed <Field.HelperText>
</Field.HelperText> No spaces or special characters allowed
{nameError && ( </Field.HelperText>
<Field.ErrorText>{nameError}</Field.ErrorText> {nameError && (
)} <Field.ErrorText>{nameError}</Field.ErrorText>
</Field.Root> )}
</Field.Root>
<Field.Root mt={4}> <Field.Root mt={4}>
<Checkbox.Root <Checkbox.Root
name="isLocked" name="isLocked"
checked={room.isLocked} checked={room.isLocked}
onCheckedChange={(e) => { onCheckedChange={(e) => {
const syntheticEvent = { const syntheticEvent = {
target: { target: {
name: "isLocked", name: "isLocked",
type: "checkbox", type: "checkbox",
checked: e.checked, checked: e.checked,
}, },
}; };
handleRoomChange(syntheticEvent); handleRoomChange(syntheticEvent);
}} }}
> >
<Checkbox.HiddenInput /> <Checkbox.HiddenInput />
<Checkbox.Control> <Checkbox.Control>
<Checkbox.Indicator /> <Checkbox.Indicator />
</Checkbox.Control> </Checkbox.Control>
<Checkbox.Label>Locked room</Checkbox.Label> <Checkbox.Label>Locked room</Checkbox.Label>
</Checkbox.Root> </Checkbox.Root>
</Field.Root> </Field.Root>
<Field.Root mt={4}>
<Field.Label>Room size</Field.Label>
<Select.Root
value={[room.roomMode]}
onValueChange={(e) =>
setRoomInput({ ...room, roomMode: e.value[0] })
}
collection={roomModeCollection}
>
<Select.HiddenSelect />
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select room size" />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
</Select.IndicatorGroup>
</Select.Control>
<Select.Positioner>
<Select.Content>
{roomModeOptions.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>Recording type</Field.Label>
<Select.Root
value={[room.recordingType]}
onValueChange={(e) =>
setRoomInput({
...room,
recordingType: e.value[0],
recordingTrigger:
e.value[0] !== "cloud"
? "none"
: room.recordingTrigger,
})
}
collection={recordingTypeCollection}
>
<Select.HiddenSelect />
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select recording type" />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
</Select.IndicatorGroup>
</Select.Control>
<Select.Positioner>
<Select.Content>
{recordingTypeOptions.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>Cloud recording start trigger</Field.Label>
<Select.Root
value={[room.recordingTrigger]}
onValueChange={(e) =>
setRoomInput({
...room,
recordingTrigger: e.value[0],
})
}
collection={recordingTriggerCollection}
disabled={room.recordingType !== "cloud"}
>
<Select.HiddenSelect />
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select trigger" />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
</Select.IndicatorGroup>
</Select.Control>
<Select.Positioner>
<Select.Content>
{recordingTriggerOptions.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.Root mt={4}>
<Field.Label>Room size</Field.Label> <Checkbox.Root
<Select.Root name="isShared"
value={[room.roomMode]} checked={room.isShared}
onValueChange={(e) => onCheckedChange={(e) => {
setRoomInput({ ...room, roomMode: e.value[0] }) const syntheticEvent = {
} target: {
collection={roomModeCollection} name: "isShared",
> type: "checkbox",
<Select.HiddenSelect /> checked: e.checked,
<Select.Control> },
<Select.Trigger> };
<Select.ValueText placeholder="Select room size" /> handleRoomChange(syntheticEvent);
</Select.Trigger> }}
<Select.IndicatorGroup> >
<Select.Indicator /> <Checkbox.HiddenInput />
</Select.IndicatorGroup> <Checkbox.Control>
</Select.Control> <Checkbox.Indicator />
<Select.Positioner> </Checkbox.Control>
<Select.Content> <Checkbox.Label>Shared room</Checkbox.Label>
{roomModeOptions.map((option) => ( </Checkbox.Root>
<Select.Item key={option.value} item={option}> </Field.Root>
{option.label} </Tabs.Content>
<Select.ItemIndicator />
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Select.Root>
</Field.Root>
<Field.Root mt={4}> <Tabs.Content value="share" pt={6}>
<Field.Label>Recording type</Field.Label> <Field.Root>
<Select.Root <Checkbox.Root
value={[room.recordingType]} name="zulipAutoPost"
onValueChange={(e) => checked={room.zulipAutoPost}
setRoomInput({ onCheckedChange={(e) => {
...room, const syntheticEvent = {
recordingType: e.value[0], target: {
recordingTrigger: name: "zulipAutoPost",
e.value[0] !== "cloud" type: "checkbox",
? "none" checked: e.checked,
: room.recordingTrigger, },
}) };
} handleRoomChange(syntheticEvent);
collection={recordingTypeCollection} }}
> >
<Select.HiddenSelect /> <Checkbox.HiddenInput />
<Select.Control> <Checkbox.Control>
<Select.Trigger> <Checkbox.Indicator />
<Select.ValueText placeholder="Select recording type" /> </Checkbox.Control>
</Select.Trigger> <Checkbox.Label>
<Select.IndicatorGroup> Automatically post transcription to Zulip
<Select.Indicator /> </Checkbox.Label>
</Select.IndicatorGroup> </Checkbox.Root>
</Select.Control> </Field.Root>
<Select.Positioner> <Field.Root mt={4}>
<Select.Content> <Field.Label>Zulip stream</Field.Label>
{recordingTypeOptions.map((option) => ( <Select.Root
<Select.Item key={option.value} item={option}> value={room.zulipStream ? [room.zulipStream] : []}
{option.label} onValueChange={(e) =>
<Select.ItemIndicator /> setRoomInput({
</Select.Item> ...room,
))} zulipStream: e.value[0],
</Select.Content> zulipTopic: "",
</Select.Positioner> })
</Select.Root> }
</Field.Root> 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>
<Field.Root mt={4}> <Tabs.Content value="webhook" pt={6}>
<Field.Label>Cloud recording start trigger</Field.Label> <Field.Root>
<Select.Root <Field.Label>Webhook URL</Field.Label>
value={[room.recordingTrigger]} <Input
onValueChange={(e) => name="webhookUrl"
setRoomInput({ ...room, recordingTrigger: e.value[0] }) placeholder="https://example.com/webhook"
} value={room.webhookUrl}
collection={recordingTriggerCollection} onChange={handleRoomChange}
disabled={room.recordingType !== "cloud"} enterKeyHint="next"
> />
<Select.HiddenSelect /> <Field.HelperText>
<Select.Control> Optional: URL to receive notifications when transcripts
<Select.Trigger> are ready
<Select.ValueText placeholder="Select trigger" /> </Field.HelperText>
</Select.Trigger> </Field.Root>
<Select.IndicatorGroup>
<Select.Indicator />
</Select.IndicatorGroup>
</Select.Control>
<Select.Positioner>
<Select.Content>
{recordingTriggerOptions.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}> {room.webhookUrl && (
<Checkbox.Root <>
name="isShared" <Field.Root mt={4}>
checked={room.isShared} <Field.Label>Webhook Secret</Field.Label>
onCheckedChange={(e) => { <Flex gap={2}>
const syntheticEvent = { <Input
target: { name="webhookSecret"
name: "isShared", type={showWebhookSecret ? "text" : "password"}
type: "checkbox", value={room.webhookSecret}
checked: e.checked, onChange={handleRoomChange}
}, placeholder={
}; isEditing && room.webhookSecret
handleRoomChange(syntheticEvent); ? "••••••••"
}} : "Leave empty to auto-generate"
>
<Checkbox.HiddenInput />
<Checkbox.Control>
<Checkbox.Indicator />
</Checkbox.Control>
<Checkbox.Label>Shared room</Checkbox.Label>
</Checkbox.Root>
</Field.Root>
</Tabs.Content>
<Tabs.Content value="calendar" pt={6}>
<ICSSettings
roomName={parseNonEmptyString(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"
type="url"
placeholder="https://example.com/webhook"
value={room.webhookUrl}
onChange={handleRoomChange}
/>
<Field.HelperText>
Optional: URL to receive notifications when transcripts
are ready
</Field.HelperText>
</Field.Root>
{room.webhookUrl && (
<>
<Field.Root mt={4}>
<Field.Label>Webhook Secret</Field.Label>
<Flex gap={2}>
<Input
name="webhookSecret"
type={showWebhookSecret ? "text" : "password"}
value={room.webhookSecret}
onChange={handleRoomChange}
placeholder={
isEditing && room.webhookSecret
? "••••••••"
: "Leave empty to auto-generate"
}
flex="1"
/>
{isEditing && room.webhookSecret && (
<IconButton
size="sm"
variant="ghost"
aria-label={
showWebhookSecret
? "Hide secret"
: "Show secret"
} }
onClick={() => flex="1"
setShowWebhookSecret(!showWebhookSecret) />
} {isEditing && room.webhookSecret && (
> <IconButton
{showWebhookSecret ? <LuEyeOff /> : <LuEye />} size="sm"
</IconButton> variant="ghost"
)} aria-label={
</Flex> showWebhookSecret
<Field.HelperText> ? "Hide secret"
Used for HMAC signature verification (auto-generated : "Show secret"
if left empty) }
</Field.HelperText> onClick={() =>
</Field.Root> setShowWebhookSecret(!showWebhookSecret)
}
{isEditing && (
<>
<Flex
mt={2}
gap={2}
alignItems="flex-start"
direction="column"
>
<Button
size="sm"
variant="outline"
onClick={handleTestWebhook}
disabled={testingWebhook || !room.webhookUrl}
>
{testingWebhook ? (
<>
<Spinner size="xs" mr={2} />
Testing...
</>
) : (
"Test Webhook"
)}
</Button>
{webhookTestResult && (
<div
style={{
fontSize: "14px",
wordBreak: "break-word",
maxWidth: "100%",
padding: "8px",
borderRadius: "4px",
backgroundColor: webhookTestResult.startsWith(
SUCCESS_EMOJI,
)
? "#f0fdf4"
: "#fef2f2",
border: `1px solid ${webhookTestResult.startsWith(SUCCESS_EMOJI) ? "#86efac" : "#fca5a5"}`,
}}
> >
{webhookTestResult} {showWebhookSecret ? <LuEyeOff /> : <LuEye />}
</div> </IconButton>
)} )}
</Flex> </Flex>
</> <Field.HelperText>
)} Used for HMAC signature verification (auto-generated
</> if left empty)
)} </Field.HelperText>
</Tabs.Content> </Field.Root>
</Tabs.Root>
{isEditing && (
<>
<Flex
mt={2}
gap={2}
alignItems="flex-start"
direction="column"
>
<Button
size="sm"
variant="outline"
onClick={handleTestWebhook}
disabled={testingWebhook || !room.webhookUrl}
>
{testingWebhook ? (
<>
<Spinner size="xs" mr={2} />
Testing...
</>
) : (
"Test Webhook"
)}
</Button>
{webhookTestResult && (
<div
style={{
fontSize: "14px",
wordBreak: "break-word",
maxWidth: "100%",
padding: "8px",
borderRadius: "4px",
backgroundColor:
webhookTestResult.startsWith(
SUCCESS_EMOJI,
)
? "#f0fdf4"
: "#fef2f2",
border: `1px solid ${webhookTestResult.startsWith(SUCCESS_EMOJI) ? "#86efac" : "#fca5a5"}`,
}}
>
{webhookTestResult}
</div>
)}
</Flex>
</>
)}
</>
)}
</Tabs.Content>
<Tabs.Content value="calendar" pt={6}>
<Field.Root>
<ICSSettings
roomName={
room.name ? parseNonEmptyString(room.name) : null
}
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}
/>
</Field.Root>
</Tabs.Content>
</Tabs.Root>
</form>
</Dialog.Body> </Dialog.Body>
<Dialog.Footer> <Dialog.Footer>
<Button variant="ghost" onClick={handleCloseDialog}> <Button variant="ghost" onClick={handleCloseDialog}>
Cancel Cancel
</Button> </Button>
<Button <Button
type="submit"
colorPalette="primary" colorPalette="primary"
onClick={handleSaveRoom} form="room-form"
disabled={ disabled={
!room.name || (room.zulipAutoPost && !room.zulipTopic) !room.name || (room.zulipAutoPost && !room.zulipTopic)
} }