mirror of https://github.com/sualko/cloud_bbb
feat: move room settings to dialog
parent
bcc483b918
commit
b0edd567f0
|
@ -163,3 +163,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bbb-form-element {
|
||||||
|
min-width: 300px;
|
||||||
|
margin: 2em 0;
|
||||||
|
|
||||||
|
input:not([type="checkbox"]) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
}
|
|
@ -72,7 +72,7 @@ const App: React.FC<Props> = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRoom(room: Room) {
|
function updateRoom(room: Room) {
|
||||||
api.updateRoom(room).then(updatedRoom => {
|
return api.updateRoom(room).then(updatedRoom => {
|
||||||
setRooms(rooms.map(room => {
|
setRooms(rooms.map(room => {
|
||||||
if (room.id === updatedRoom.id) {
|
if (room.id === updatedRoom.id) {
|
||||||
return updatedRoom;
|
return updatedRoom;
|
||||||
|
@ -101,9 +101,6 @@ const App: React.FC<Props> = () => {
|
||||||
<th onClick={() => onOrderBy('name')}>
|
<th onClick={() => onOrderBy('name')}>
|
||||||
{t('bbb', 'Name')} <SortArrow name='name' value={orderBy} direction={sortOrder} />
|
{t('bbb', 'Name')} <SortArrow name='name' value={orderBy} direction={sortOrder} />
|
||||||
</th>
|
</th>
|
||||||
<th onClick={() => onOrderBy('welcome')}>
|
|
||||||
{t('bbb', 'Welcome')} <SortArrow name='welcome' value={orderBy} direction={sortOrder} />
|
|
||||||
</th>
|
|
||||||
<th onClick={() => onOrderBy('maxParticipants')}>
|
<th onClick={() => onOrderBy('maxParticipants')}>
|
||||||
{t('bbb', 'Max')} <SortArrow name='maxParticipants' value={orderBy} direction={sortOrder} />
|
{t('bbb', 'Max')} <SortArrow name='maxParticipants' value={orderBy} direction={sortOrder} />
|
||||||
</th>
|
</th>
|
||||||
|
@ -114,6 +111,7 @@ const App: React.FC<Props> = () => {
|
||||||
{t('bbb', 'Recordings')}
|
{t('bbb', 'Recordings')}
|
||||||
</th>
|
</th>
|
||||||
<th />
|
<th />
|
||||||
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dialog: React.FC<Props> = ({open, title, children, onClose = () => undefined}) => {
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="oc-dialog-dim"> </div>
|
||||||
|
<div className="oc-dialog" tabIndex={-1} role="dialog" style={{display:'inline-block', position: 'fixed'}}>
|
||||||
|
<h2 className="oc-dialog-title">{title}</h2>
|
||||||
|
<a className="oc-dialog-close" onClick={ev => {ev.preventDefault(); onClose();}}></a>
|
||||||
|
|
||||||
|
<div className="oc-dialog-content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dialog;
|
|
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditRoomDialog: React.FC<Props> = ({ room, updateProperty }) => {
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
function formElement(label: string, field: string, type: 'text' | 'number' = 'text') {
|
||||||
|
return (
|
||||||
|
<div className="bbb-form-element">
|
||||||
|
<label htmlFor={`bbb-${field}`}>
|
||||||
|
<h3>{t('bbb', label)}</h3>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<SubmitInput initialValue={room[field]} type={type} name={field} onSubmitValue={value => updateProperty(field, value)} />
|
||||||
|
{descriptions[field] && <em>{descriptions[field]}</em>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a className="icon icon-edit icon-visible"
|
||||||
|
onClick={ev => { ev.preventDefault(), setOpen(true); }}
|
||||||
|
title={t('bbb', 'Edit')} />
|
||||||
|
|
||||||
|
<Dialog open={open} onClose={() => setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}>
|
||||||
|
{formElement('Name', 'name')}
|
||||||
|
{formElement('Welcome', 'welcome')}
|
||||||
|
{formElement('Participant limit', 'maxParticipants', 'number')}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<input id={`bbb-record-${room.id}`}
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox"
|
||||||
|
checked={room.record}
|
||||||
|
onChange={(event) => updateProperty('record', event.target.checked)} />
|
||||||
|
<label htmlFor={`bbb-record-${room.id}`}>{t('bbb', 'Recording')}</label>
|
||||||
|
</div>
|
||||||
|
<em>{descriptions.recording}</em>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditRoomDialog;
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React, {useState} from 'react';
|
||||||
|
import { SubmitInput } from './SubmitInput';
|
||||||
|
|
||||||
|
type EditableValueProps = {
|
||||||
|
value: string;
|
||||||
|
setValue: (key: string, value: string | number) => Promise<void>;
|
||||||
|
field: string;
|
||||||
|
type: 'text' | 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: currentValue, type }) => {
|
||||||
|
const [active, setActive] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const submit = (value: string | number) => {
|
||||||
|
if (value === currentValue) {
|
||||||
|
setActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(field, value).then(() => {
|
||||||
|
setActive(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
return <SubmitInput
|
||||||
|
name={field}
|
||||||
|
autoFocus={true}
|
||||||
|
onSubmitValue={(value) => submit(type === 'number' ? parseInt(value) : value)}
|
||||||
|
onClick={event => event.stopPropagation()}
|
||||||
|
initialValue={currentValue}
|
||||||
|
type={type}
|
||||||
|
focus={true}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
setActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <a className="action-rename" onClick={onClick}>{currentValue}</a>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditableValue;
|
|
@ -1,24 +1,16 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
import { SubmitInput } from './SubmitInput';
|
import { api, Recording, Room } from './Api';
|
||||||
import { Room, Recording, api } from './Api';
|
import EditRoomDialog from './EditRoomDialog';
|
||||||
import RecordingRow from './RecordingRow';
|
import RecordingRow from './RecordingRow';
|
||||||
|
import EditableValue from './EditableValue';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
room: Room;
|
room: Room;
|
||||||
updateRoom: (room: Room) => void;
|
updateRoom: (room: Room) => Promise<void>;
|
||||||
deleteRoom: (id: number) => void;
|
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 = {
|
type RecordingsNumberProps = {
|
||||||
recordings: null | Recording[];
|
recordings: null | Recording[];
|
||||||
showRecordings: boolean;
|
showRecordings: boolean;
|
||||||
|
@ -41,28 +33,9 @@ const RecordingsNumber: React.FC<RecordingsNumberProps> = ({ recordings, showRec
|
||||||
return <span>0</span>;
|
return <span>0</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditableValue: React.FC<EditableValueProps> = ({ setValue, setActive, active, field, value, type }) => {
|
|
||||||
if (active === field) {
|
|
||||||
return <SubmitInput
|
|
||||||
autoFocus={true}
|
|
||||||
onSubmitValue={(value) => setValue(field, type === 'number' ? parseInt(value) : value)}
|
|
||||||
onClick={event => event.stopPropagation()}
|
|
||||||
initialValue={value}
|
|
||||||
type={type}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClick(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
setActive(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <a className="action-rename" onClick={onClick}>{value}</a>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RoomRow: React.FC<Props> = (props) => {
|
const RoomRow: React.FC<Props> = (props) => {
|
||||||
const [activeEdit, setActiveEdit] = useState('');
|
|
||||||
const [recordings, setRecordings] = useState<Recording[] | null>(null);
|
const [recordings, setRecordings] = useState<Recording[] | null>(null);
|
||||||
const [showRecordings, setShowRecordings] = useState<boolean>(false);
|
const [showRecordings, setShowRecordings] = useState<boolean>(false);
|
||||||
const room = props.room;
|
const room = props.room;
|
||||||
|
@ -83,12 +56,10 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
}, [areRecordingsLoaded]);
|
}, [areRecordingsLoaded]);
|
||||||
|
|
||||||
function updateRoom(key: string, value: string | boolean | number) {
|
function updateRoom(key: string, value: string | boolean | number) {
|
||||||
props.updateRoom({
|
return props.updateRoom({
|
||||||
...props.room,
|
...props.room,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
});
|
});
|
||||||
|
|
||||||
setActiveEdit('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteRow(ev: MouseEvent) {
|
function deleteRow(ev: MouseEvent) {
|
||||||
|
@ -184,7 +155,7 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(field: string, type: 'text' | 'number' = 'text') {
|
function edit(field: string, type: 'text' | 'number' = 'text') {
|
||||||
return <EditableValue field={field} value={room[field]} active={activeEdit} setActive={setActiveEdit} setValue={updateRoom} type={type} />;
|
return <EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -204,9 +175,6 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
<td className="name">
|
<td className="name">
|
||||||
{edit('name')}
|
{edit('name')}
|
||||||
</td>
|
</td>
|
||||||
<td className="welcome">
|
|
||||||
{edit('welcome')}
|
|
||||||
</td>
|
|
||||||
<td className="max-participants">
|
<td className="max-participants">
|
||||||
{edit('maxParticipants', 'number')}
|
{edit('maxParticipants', 'number')}
|
||||||
</td>
|
</td>
|
||||||
|
@ -215,6 +183,9 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
<label htmlFor={`bbb-record-${room.id}`}></label>
|
<label htmlFor={`bbb-record-${room.id}`}></label>
|
||||||
</td>
|
</td>
|
||||||
<td><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
|
<td><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
|
||||||
|
<td className="edit icon-col">
|
||||||
|
<EditRoomDialog room={props.room} updateProperty={updateRoom} />
|
||||||
|
</td>
|
||||||
<td className="remove icon-col">
|
<td className="remove icon-col">
|
||||||
<a className="icon icon-delete icon-visible"
|
<a className="icon icon-delete icon-visible"
|
||||||
onClick={deleteRow as any}
|
onClick={deleteRow as any}
|
||||||
|
|
|
@ -7,7 +7,9 @@ import {
|
||||||
export interface SubmitInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
export interface SubmitInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
type?: 'text' | 'number';
|
type?: 'text' | 'number';
|
||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
|
name: string;
|
||||||
onSubmitValue: (value: string) => void;
|
onSubmitValue: (value: string) => void;
|
||||||
|
focus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubmitInputState {
|
export interface SubmitInputState {
|
||||||
|
@ -21,7 +23,7 @@ export class SubmitInput extends Component<SubmitInputProps, SubmitInputState> {
|
||||||
|
|
||||||
constructor(props: SubmitInputProps) {
|
constructor(props: SubmitInputProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state.value = props.initialValue || '';
|
this.state.value = props.initialValue ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = (event: SyntheticEvent<any>) => {
|
onSubmit = (event: SyntheticEvent<any>) => {
|
||||||
|
@ -33,7 +35,12 @@ export class SubmitInput extends Component<SubmitInputProps, SubmitInputState> {
|
||||||
return <form onSubmit={this.onSubmit}>
|
return <form onSubmit={this.onSubmit}>
|
||||||
<input value={this.state.value}
|
<input value={this.state.value}
|
||||||
type={this.props.type}
|
type={this.props.type}
|
||||||
onChange={event => 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}
|
||||||
|
/>
|
||||||
</form>;
|
</form>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue