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>
This commit is contained in:
Igor Monadical
2025-09-19 15:14:40 -04:00
committed by GitHub
parent 0abcebfc94
commit 47716f6e5d

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={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}
/>
</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)
} }