Merge pull request #6404 from Tainan404/issue-6261

Add mobile version to sort user on create breakout rooms
This commit is contained in:
Anton Georgiev 2018-12-19 13:40:07 -05:00 committed by GitHub
commit 85d2e162c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 419 additions and 37 deletions

View File

@ -2,10 +2,13 @@ import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'lodash';
import cx from 'classnames';
import browser from 'browser-detect';
import Button from '/imports/ui/components/button/component';
import { Session } from 'meteor/session';
import Modal from '/imports/ui/components/modal/fullscreen/component';
import { withModalMounter } from '/imports/ui/components/modal/service';
import HoldButton from '/imports/ui/components/presentation/presentation-toolbar/zoom-tool/holdButton/component';
import SortList from './sort-user-list/component';
import { styles } from './styles';
import Icon from '../../icon/component';
@ -58,6 +61,22 @@ const intlMessages = defineMessages({
id: 'app.createBreakoutRoom.notAssigned',
description: 'Not assigned label',
},
breakoutRoomLabel: {
id: 'app.createBreakoutRoom.breakoutRoomLabel',
description: 'breakout room label',
},
addParticipantLabel: {
id: 'app.createBreakoutRoom.addParticipantLabel',
description: 'add Participant label',
},
nextLabel: {
id: 'app.createBreakoutRoom.nextLabel',
description: 'Next label',
},
backLabel: {
id: 'app.audio.backLabel',
description: 'Back label',
},
});
const MIN_BREAKOUT_ROOMS = 2;
const MAX_BREAKOUT_ROOMS = 8;
@ -78,6 +97,11 @@ class BreakoutRoom extends Component {
this.renderRoomsGrid = this.renderRoomsGrid.bind(this);
this.renderBreakoutForm = this.renderBreakoutForm.bind(this);
this.renderFreeJoinCheck = this.renderFreeJoinCheck.bind(this);
this.renderRoomSortList = this.renderRoomSortList.bind(this);
this.renderDesktop = this.renderDesktop.bind(this);
this.renderMobile = this.renderMobile.bind(this);
this.renderButtonSetLevel = this.renderButtonSetLevel.bind(this);
this.renderSelectUserScreen = this.renderSelectUserScreen.bind(this);
this.handleDismiss = this.handleDismiss.bind(this);
this.state = {
@ -86,6 +110,8 @@ class BreakoutRoom extends Component {
users: [],
durationTime: 1,
freeJoin: false,
formFillLevel: 1,
roomSelected: 0,
preventClosing: true,
valid: true,
};
@ -108,8 +134,12 @@ class BreakoutRoom extends Component {
meetingName,
intl,
} = this.props;
const {
users,
freeJoin,
} = this.state;
if (this.state.users.length === this.getUserByRoom(0).length) {
if (users.length === this.getUserByRoom(0).length) {
this.setState({ valid: false });
return;
}
@ -121,11 +151,11 @@ class BreakoutRoom extends Component {
0: meetingName,
1: value,
}),
freeJoin: this.state.freeJoin,
freeJoin,
sequence: value,
}));
createBreakoutRoom(rooms, durationTime, this.state.freeJoin);
createBreakoutRoom(rooms, durationTime, freeJoin);
Session.set('isUserListOpen', true);
}
@ -147,7 +177,8 @@ class BreakoutRoom extends Component {
}
getUserByRoom(room) {
return this.state.users.filter(user => user.room === room);
const { users } = this.state;
return users.filter(user => user.room === room);
}
handleDismiss() {
@ -176,11 +207,13 @@ class BreakoutRoom extends Component {
}
increaseDurationTime() {
this.setState({ durationTime: (1 * this.state.durationTime) + 1 });
const { durationTime } = this.state;
this.setState({ durationTime: (1 * durationTime) + 1 });
}
decreaseDurationTime() {
const number = ((1 * this.state.durationTime) - 1);
const { durationTime } = this.state;
const number = ((1 * durationTime) - 1);
this.setState({ durationTime: number < 1 ? 1 : number });
}
@ -194,7 +227,10 @@ class BreakoutRoom extends Component {
renderRoomsGrid() {
const { intl } = this.props;
const {
valid,
numberOfRooms,
} = this.state;
const allowDrop = (ev) => {
ev.preventDefault();
};
@ -208,32 +244,33 @@ class BreakoutRoom extends Component {
return (
<div className={styles.boxContainer}>
<label htmlFor="BreakoutRoom" className={!this.state.valid ? styles.changeToWarn : null}>
<label htmlFor="BreakoutRoom" className={!valid ? styles.changeToWarn : null}>
<p
className={styles.freeJoinLabel}
>
{intl.formatMessage(intlMessages.notAssigned, { 0: this.getUserByRoom(0).length })}
</p>
<div className={styles.breakoutBox} onDrop={drop(0)} onDragOver={allowDrop} >
<div className={styles.breakoutBox} onDrop={drop(0)} onDragOver={allowDrop}>
{this.renderUserItemByRoom(0)}
</div>
<span className={this.state.valid ? styles.dontShow : styles.leastOneWarn} >
<span className={valid ? styles.dontShow : styles.leastOneWarn}>
{intl.formatMessage(intlMessages.leastOneWarnBreakout)}
</span>
</label>
{
_.range(1, this.state.numberOfRooms + 1).map(value =>
(
<label htmlFor="BreakoutRoom" key={`room-${value}`}>
<p
className={styles.freeJoinLabel}
>
{intl.formatMessage(intlMessages.roomLabel, { 0: (value) })}
</p>
<div className={styles.breakoutBox} onDrop={drop(value)} onDragOver={allowDrop}>
{this.renderUserItemByRoom(value)}
</div>
</label>))
_.range(1, numberOfRooms + 1).map(value => (
<label htmlFor="BreakoutRoom" key={`room-${value}`}>
<p
id="BreakoutRoom"
className={styles.freeJoinLabel}
>
{intl.formatMessage(intlMessages.roomLabel, { 0: (value) })}
</p>
<div className={styles.breakoutBox} onDrop={drop(value)} onDragOver={allowDrop}>
{this.renderUserItemByRoom(value)}
</div>
</label>
))
}
</div>
);
@ -241,15 +278,19 @@ class BreakoutRoom extends Component {
renderBreakoutForm() {
const { intl } = this.props;
const {
numberOfRooms,
durationTime,
} = this.state;
return (
<div className={styles.breakoutSettings}>
<label htmlFor="numberOfRooms">
<p className={styles.labelText}>{intl.formatMessage(intlMessages.numberOfRooms)}</p>
<select
id="numberOfRooms"
name="numberOfRooms"
className={styles.inputRooms}
value={this.state.numberOfRooms}
value={numberOfRooms}
onChange={this.changeNumberOfRooms}
>
{
@ -257,14 +298,14 @@ class BreakoutRoom extends Component {
}
</select>
</label>
<label htmlFor="breakoutRoomTime" >
<label htmlFor="breakoutRoomTime">
<p className={styles.labelText}>{intl.formatMessage(intlMessages.duration)}</p>
<div className={styles.durationArea}>
<input
type="number"
className={styles.duration}
min={MIN_BREAKOUT_ROOMS}
value={this.state.durationTime}
value={durationTime}
onChange={this.changeDurationTime}
/>
<span>
@ -272,7 +313,7 @@ class BreakoutRoom extends Component {
key="decrease-breakout-time"
exec={this.decreaseDurationTime}
minBound={MIN_BREAKOUT_ROOMS}
value={this.state.durationTime}
value={durationTime}
>
<Icon
className={styles.iconsColor}
@ -297,15 +338,32 @@ class BreakoutRoom extends Component {
);
}
renderSelectUserScreen() {
const {
users,
roomSelected,
} = this.state;
return (
<SortList
confirm={() => this.setState({ formFillLevel: 2 })}
users={users}
room={roomSelected}
onCheck={this.changeUserRoom}
onUncheck={userId => this.changeUserRoom(userId, 0)}
/>
);
}
renderFreeJoinCheck() {
const { intl } = this.props;
const { freeJoin } = this.state;
return (
<label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel}>
<input
type="checkbox"
className={styles.freeJoinCheckbox}
onChange={this.setFreeJoin}
checked={this.state.freeJoin}
checked={freeJoin}
/>
{intl.formatMessage(intlMessages.freeJoinLabel)}
</label>
@ -313,16 +371,21 @@ class BreakoutRoom extends Component {
}
renderUserItemByRoom(room) {
const {
valid,
seletedId,
} = this.state;
const dragStart = (ev) => {
ev.dataTransfer.setData('text', ev.target.id);
this.setState({ seletedId: ev.target.id });
if (!this.state.valid) {
if (!valid) {
this.setState({ valid: true });
}
};
const dragEnd = (ev) => {
const dragEnd = () => {
this.setState({ seletedId: '' });
};
@ -333,8 +396,8 @@ class BreakoutRoom extends Component {
key={user.userId}
className={cx(
styles.roomUserItem,
this.state.seletedId === user.userId ? styles.selectedItem : null,
)
seletedId === user.userId ? styles.selectedItem : null,
)
}
draggable
onDragStart={dragStart}
@ -344,8 +407,79 @@ class BreakoutRoom extends Component {
</p>));
}
renderRoomSortList() {
const { intl } = this.props;
const { numberOfRooms } = this.state;
const onClick = roomNumber => this.setState({ formFillLevel: 3, roomSelected: roomNumber });
return (
<div className={styles.listContainer}>
<span>
{
new Array(numberOfRooms).fill(1).map((room, idx) => (
<div className={styles.roomItem}>
<h2 className={styles.itemTitle}>
{intl.formatMessage(intlMessages.breakoutRoomLabel, { 0: idx + 1 })}
</h2>
<Button
className={styles.itemButton}
label={intl.formatMessage(intlMessages.addParticipantLabel)}
size="lg"
ghost
color="primary"
onClick={() => onClick(idx + 1)}
/>
</div>
))
}
</span>
{this.renderButtonSetLevel(1, intl.formatMessage(intlMessages.backLabel))}
</div>
);
}
renderDesktop() {
return [
this.renderBreakoutForm(),
this.renderFreeJoinCheck(),
this.renderRoomsGrid(),
];
}
renderMobile() {
const { intl } = this.props;
const { formFillLevel } = this.state;
if (formFillLevel === 2) {
return this.renderRoomSortList();
}
if (formFillLevel === 3) {
return this.renderSelectUserScreen();
}
return [
this.renderBreakoutForm(),
this.renderFreeJoinCheck(),
this.renderButtonSetLevel(2, intl.formatMessage(intlMessages.nextLabel)),
];
}
renderButtonSetLevel(level, label) {
return (
<Button
color="primary"
size="lg"
label={label}
onClick={() => this.setState({ formFillLevel: level })}
/>
);
}
render() {
const { intl } = this.props;
const { preventClosing } = this.state;
const BROWSER_RESULTS = browser();
const isMobileBrowser = BROWSER_RESULTS.mobile || BROWSER_RESULTS.os.includes('Android');
return (
<Modal
@ -360,17 +494,15 @@ class BreakoutRoom extends Component {
callback: this.handleDismiss,
label: intl.formatMessage(intlMessages.dismissLabel),
}}
preventClosing={this.state.preventClosing}
preventClosing={preventClosing}
>
<div className={styles.content}>
<p className={styles.subTitle}>
{intl.formatMessage(intlMessages.breakoutRoomDesc)}
</p>
{this.renderBreakoutForm()}
{this.renderFreeJoinCheck()}
{this.renderRoomsGrid()}
{isMobileBrowser ? this.renderMobile() : this.renderDesktop()}
</div>
</Modal >
</Modal>
);
}
}

View File

@ -0,0 +1,124 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import { styles } from '../styles';
const propTypes = {
confirm: PropTypes.func.isRequired,
users: PropTypes.arrayOf(PropTypes.object).isRequired,
room: PropTypes.number.isRequired,
onCheck: PropTypes.func,
onUncheck: PropTypes.func,
};
const defaultProps = {
onCheck: () => {},
onUncheck: () => {},
};
const intlMessages = defineMessages({
breakoutRoomLabel: {
id: 'app.createBreakoutRoom.breakoutRoomLabel',
description: 'breakout room label',
},
doneLabel: {
id: 'app.createBreakoutRoom.doneLabel',
description: 'done label',
},
});
class SortUsers extends Component {
constructor(props) {
super(props);
this.setUsers = this.setUsers.bind(this);
this.renderUserItem = this.renderUserItem.bind(this);
this.onChage = this.onChage.bind(this);
this.state = {
users: [],
};
}
componentDidMount() {
const { users } = this.props;
this.setUsers(users);
}
onChage(userId, room) {
const {
onCheck,
onUncheck,
} = this.props;
return (ev) => {
const check = ev.target.checked;
if (check) {
return onCheck(userId, room);
}
return onUncheck(userId, room);
};
}
setUsers(users) {
this.setState({ users: users.sort((a, b) => a.room - b.room) });
}
renderUserItem() {
const { room } = this.props;
const { users } = this.state;
return users
.map((user, idx) => (
<div id={user.userId} className={styles.selectUserContainer} key={`breakout-user-${user.userId}`}>
<span className={styles.round}>
<input
type="checkbox"
id={`itemId${idx}`}
defaultChecked={user.room === room}
onChange={this.onChage(user.userId, room)}
/>
<label htmlFor={`itemId${idx}`}>
<input
type="checkbox"
id={`itemId${idx}`}
defaultChecked={user.room === room}
onChange={this.onChage(user.userId, room)}
/>
</label>
</span>
<span className={styles.textName}>
{user.userName}
{user.room && !(user.room === room) ? `\t[${user.room}]` : ''}
</span>
</div>));
}
render() {
const {
intl,
room,
confirm,
} = this.props;
return (
<div className={styles.selectUserScreen}>
<header className={styles.header}>
<h2 className={styles.title}>
{intl.formatMessage(intlMessages.breakoutRoomLabel, { 0: room })}
</h2>
<Button
className={styles.buttonAdd}
size="md"
label={intl.formatMessage(intlMessages.doneLabel)}
color="primary"
onClick={confirm}
/>
</header>
{this.renderUserItem()}
</div>
);
}
}
SortUsers.propTypes = propTypes;
SortUsers.defaultProps = defaultProps;
export default injectIntl(SortUsers);

View File

@ -154,6 +154,127 @@ input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-i
color: var(--color-white)
}
/* mobile */
.listContainer {
display: flex;
justify-content: flex-start;
flex-direction: column;
}
.itemTitle {
color: var(--color-blue-light);
margin: 0;
}
.roomItem {
margin: 1rem 0 1rem 0;
}
.itemButton {
padding: 0;
outline: none !important;
span {
color: var(--color-blue-light);
}
}
.selectUserScreenContainer {
position: fixed;
z-index: 1002;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, .85);
}
.selectUserScreen {
position: fixed;
display: block;
height: 100vh;
width: 100%;
background-color: var(--color-white);
z-index: 1002;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.header {
display: flex;
padding: var(--line-height-computed) 0;
border-bottom: var(--border-size) solid var(--color-gray-lighter);
margin: 0 1rem 0 1rem;
}
.title {
@extend %text-elipsis;
align-content: flex-end;
flex: 1;
margin: 0;
font-weight: 400;
}
.buttonAdd {
flex: 0 1 35%;
}
.selectUserContainer {
margin: 1.5rem 1rem;
}
.textName {
@extend %text-elipsis;
margin-left: 1.5rem;
}
.round {
position: relative;
& label {
margin-top: -10px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 50%;
cursor: pointer;
height: 28px;
left: 0;
position: absolute;
top: 0;
width: 28px;
}
& label:after {
border: 2px solid #fff;
border-top: none;
border-right: none;
content: "";
height: 6px;
left: 7px;
opacity: 0;
position: absolute;
top: 8px;
transform: rotate(-45deg);
width: 12px;
}
& input[type="checkbox"] {
visibility: hidden;
}
& input[type="checkbox"]:checked + label {
background-color: #66bb6a;
border-color: #66bb6a;
}
& input[type="checkbox"]:checked + label:after {
opacity: 1;
}
}
.dontShow {
display: none;
}

View File

@ -8,6 +8,7 @@
outline: none;
@include mq($small-only) {
width: 100%;
height: 100%;
}
}

View File

@ -458,6 +458,7 @@
"app.videoDock.webcamUnfocusLabel": "Unfocus",
"app.videoDock.webcamUnfocusDesc": "Unfocus the selected webcam",
"app.createBreakoutRoom.title": "Breakout Rooms",
"app.createBreakoutRoom.breakoutRoomLabel": "Breakout Rooms {0}",
"app.createBreakoutRoom.generatingURL": "Generating URL",
"app.createBreakoutRoom.generatedURL": "Generated",
"app.createBreakoutRoom.duration": "Duration {0}",
@ -472,6 +473,9 @@
"app.createBreakoutRoom.randomlyAssign": "Randomly Assign",
"app.createBreakoutRoom.endAllBreakouts": "End All Breakout Rooms",
"app.createBreakoutRoom.roomName": "{0} (Room - {1})",
"app.createBreakoutRoom.doneLabel": "Done",
"app.createBreakoutRoom.nextLabel": "Next",
"app.createBreakoutRoom.addParticipantLabel": "+ Add participant",
"app.createBreakoutRoom.freeJoin": "Allow users to choose a breakout room to join",
"app.createBreakoutRoom.leastOneWarnBreakout": "You must place at least one user in a breakout room.",
"app.createBreakoutRoom.modalDesc": "Complete the steps below to create rooms in your session, To add participants to a room."