diff --git a/ts/Manager/App.scss b/ts/Manager/App.scss index 78d5ea5..0d93b9f 100644 --- a/ts/Manager/App.scss +++ b/ts/Manager/App.scss @@ -163,3 +163,16 @@ } } } + +.bbb-form-element { + min-width: 300px; + margin: 2em 0; + + input:not([type="checkbox"]) { + width: 100%; + } + + em { + white-space: normal; + } +} \ No newline at end of file diff --git a/ts/Manager/App.tsx b/ts/Manager/App.tsx index 91966fa..da75e07 100644 --- a/ts/Manager/App.tsx +++ b/ts/Manager/App.tsx @@ -72,7 +72,7 @@ const App: React.FC = () => { } function updateRoom(room: Room) { - api.updateRoom(room).then(updatedRoom => { + return api.updateRoom(room).then(updatedRoom => { setRooms(rooms.map(room => { if (room.id === updatedRoom.id) { return updatedRoom; @@ -101,9 +101,6 @@ const App: React.FC = () => { onOrderBy('name')}> {t('bbb', 'Name')} - onOrderBy('welcome')}> - {t('bbb', 'Welcome')} - onOrderBy('maxParticipants')}> {t('bbb', 'Max')} @@ -114,6 +111,7 @@ const App: React.FC = () => { {t('bbb', 'Recordings')} + diff --git a/ts/Manager/Dialog.tsx b/ts/Manager/Dialog.tsx new file mode 100644 index 0000000..bb5bc4b --- /dev/null +++ b/ts/Manager/Dialog.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +type Props = { + open: boolean; + onClose?: () => void; + title: string; +} + +const Dialog: React.FC = ({open, title, children, onClose = () => undefined}) => { + + if (!open) { + return <>; + } + + return ( + <> +
+
+

{title}

+ {ev.preventDefault(); onClose();}}> + +
+ {children} +
+
+ + ); +}; + +export default Dialog; \ No newline at end of file diff --git a/ts/Manager/EditRoomDialog.tsx b/ts/Manager/EditRoomDialog.tsx new file mode 100644 index 0000000..c73ddf1 --- /dev/null +++ b/ts/Manager/EditRoomDialog.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import Dialog from './Dialog'; +import { Room } from './Api'; +import { SubmitInput } from './SubmitInput'; + +const descriptions: { [key: string]: string } = { + name: t('bbb', 'Descriptive name of this room.'), + welcome: t('bbb', 'This message is shown to all users in the chat area after they joined.'), + maxParticipants: t('bbb', 'Sets a limit on the number of participants for this room. Zero means there is no limit.'), + recording: t('bbb', 'If enabled, the moderator is able to start the recording.'), +}; + +type Props = { + room: Room; + updateProperty: (key: string, value: string | boolean | number) => Promise; +} + +const EditRoomDialog: React.FC = ({ room, updateProperty }) => { + const [open, setOpen] = useState(false); + + function formElement(label: string, field: string, type: 'text' | 'number' = 'text') { + return ( +
+ + + updateProperty(field, value)} /> + {descriptions[field] && {descriptions[field]}} +
+ ); + } + + return ( + <> + { ev.preventDefault(), setOpen(true); }} + title={t('bbb', 'Edit')} /> + + setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}> + {formElement('Name', 'name')} + {formElement('Welcome', 'welcome')} + {formElement('Participant limit', 'maxParticipants', 'number')} + +
+
+ updateProperty('record', event.target.checked)} /> + +
+ {descriptions.recording} +
+
+ + ); +}; + +export default EditRoomDialog; \ No newline at end of file diff --git a/ts/Manager/EditableValue.tsx b/ts/Manager/EditableValue.tsx new file mode 100644 index 0000000..0bcbb5d --- /dev/null +++ b/ts/Manager/EditableValue.tsx @@ -0,0 +1,46 @@ +import React, {useState} from 'react'; +import { SubmitInput } from './SubmitInput'; + +type EditableValueProps = { + value: string; + setValue: (key: string, value: string | number) => Promise; + field: string; + type: 'text' | 'number'; +} + +const EditableValue: React.FC = ({ setValue, field, value: currentValue, type }) => { + const [active, setActive] = useState(false); + + const submit = (value: string | number) => { + if (value === currentValue) { + setActive(false); + return; + } + + setValue(field, value).then(() => { + setActive(false); + }); + }; + + if (active) { + return submit(type === 'number' ? parseInt(value) : value)} + onClick={event => event.stopPropagation()} + initialValue={currentValue} + type={type} + focus={true} + />; + } + + function onClick(ev) { + ev.stopPropagation(); + + setActive(true); + } + + return
{currentValue}; +}; + +export default EditableValue; \ No newline at end of file diff --git a/ts/Manager/RoomRow.tsx b/ts/Manager/RoomRow.tsx index 5497b65..c3a78f6 100644 --- a/ts/Manager/RoomRow.tsx +++ b/ts/Manager/RoomRow.tsx @@ -1,24 +1,16 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { CopyToClipboard } from 'react-copy-to-clipboard'; -import { SubmitInput } from './SubmitInput'; -import { Room, Recording, api } from './Api'; +import { api, Recording, Room } from './Api'; +import EditRoomDialog from './EditRoomDialog'; import RecordingRow from './RecordingRow'; +import EditableValue from './EditableValue'; type Props = { room: Room; - updateRoom: (room: Room) => void; + updateRoom: (room: Room) => Promise; deleteRoom: (id: number) => void; } -type EditableValueProps = { - setValue: (key: string, value: string | number) => void; - setActive: (key: string) => void; - active: string; - field: string; - value: string; - type: 'text' | 'number'; -} - type RecordingsNumberProps = { recordings: null | Recording[]; showRecordings: boolean; @@ -41,28 +33,9 @@ const RecordingsNumber: React.FC = ({ recordings, showRec return 0; }; -const EditableValue: React.FC = ({ setValue, setActive, active, field, value, type }) => { - if (active === field) { - return setValue(field, type === 'number' ? parseInt(value) : value)} - onClick={event => event.stopPropagation()} - initialValue={value} - type={type} - />; - } - function onClick(ev) { - ev.stopPropagation(); - - setActive(field); - } - - return {value}; -}; const RoomRow: React.FC = (props) => { - const [activeEdit, setActiveEdit] = useState(''); const [recordings, setRecordings] = useState(null); const [showRecordings, setShowRecordings] = useState(false); const room = props.room; @@ -83,12 +56,10 @@ const RoomRow: React.FC = (props) => { }, [areRecordingsLoaded]); function updateRoom(key: string, value: string | boolean | number) { - props.updateRoom({ + return props.updateRoom({ ...props.room, [key]: value, }); - - setActiveEdit(''); } function deleteRow(ev: MouseEvent) { @@ -184,7 +155,7 @@ const RoomRow: React.FC = (props) => { } function edit(field: string, type: 'text' | 'number' = 'text') { - return ; + return ; } return ( @@ -204,9 +175,6 @@ const RoomRow: React.FC = (props) => { {edit('name')} - - {edit('welcome')} - {edit('maxParticipants', 'number')} @@ -215,6 +183,9 @@ const RoomRow: React.FC = (props) => { + + + { type?: 'text' | 'number'; initialValue?: string; + name: string; onSubmitValue: (value: string) => void; + focus?: boolean; } export interface SubmitInputState { @@ -21,7 +23,7 @@ export class SubmitInput extends Component { constructor(props: SubmitInputProps) { super(props); - this.state.value = props.initialValue || ''; + this.state.value = props.initialValue ?? ''; } onSubmit = (event: SyntheticEvent) => { @@ -33,7 +35,12 @@ export class SubmitInput extends Component { return
this.setState({value: event.currentTarget.value})}/> + id={`bbb-${this.props.name}`} + name={this.props.name} + onChange={event => this.setState({value: event.currentTarget.value})} + onBlur={() => this.props.onSubmitValue(this.state.value)} + autoFocus={this.props.focus} + />
; } }