[MM-36459] Drag and drop for dropdown (#1651)
* [MM-36459] Drag and drop for dropdown * CircleCI build * Drag and drop feedback from UX * PR feedback * PR feedback
This commit is contained in:
@@ -4,10 +4,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
|
||||
|
||||
import {Team} from 'types/config';
|
||||
|
||||
import {CLOSE_TEAMS_DROPDOWN, REQUEST_TEAMS_DROPDOWN_INFO, SEND_DROPDOWN_MENU_SIZE, SHOW_NEW_SERVER_MODAL, SWITCH_SERVER, UPDATE_TEAMS_DROPDOWN} from 'common/communication';
|
||||
import {CLOSE_TEAMS_DROPDOWN, REQUEST_TEAMS_DROPDOWN_INFO, SEND_DROPDOWN_MENU_SIZE, SHOW_NEW_SERVER_MODAL, SWITCH_SERVER, UPDATE_TEAMS, UPDATE_TEAMS_DROPDOWN} from 'common/communication';
|
||||
|
||||
import './css/dropdown.scss';
|
||||
import './css/compass-icons.css';
|
||||
@@ -20,24 +21,39 @@ type State = {
|
||||
unreads?: Map<string, boolean>;
|
||||
mentions?: Map<string, number>;
|
||||
expired?: Map<string, boolean>;
|
||||
hasGPOTeams?: boolean;
|
||||
isAnyDragging: boolean;
|
||||
}
|
||||
|
||||
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
||||
if (style?.transform) {
|
||||
const axisLockY = `translate(0px${style.transform.slice(style.transform.indexOf(','), style.transform.length)}`;
|
||||
return {
|
||||
...style,
|
||||
transform: axisLockY,
|
||||
};
|
||||
}
|
||||
return style;
|
||||
}
|
||||
class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||
constructor(props: Record<string, never>) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state = {
|
||||
isAnyDragging: false,
|
||||
};
|
||||
|
||||
window.addEventListener('message', this.handleMessageEvent);
|
||||
}
|
||||
|
||||
handleMessageEvent = (event: MessageEvent) => {
|
||||
if (event.data.type === UPDATE_TEAMS_DROPDOWN) {
|
||||
const {teams, activeTeam, darkMode, unreads, mentions, expired} = event.data.data;
|
||||
const {teams, activeTeam, darkMode, hasGPOTeams, unreads, mentions, expired} = event.data.data;
|
||||
this.setState({
|
||||
teams,
|
||||
orderedTeams: teams.concat().sort((a: Team, b: Team) => a.order - b.order),
|
||||
activeTeam,
|
||||
darkMode,
|
||||
hasGPOTeams,
|
||||
unreads,
|
||||
mentions,
|
||||
expired,
|
||||
@@ -53,8 +69,10 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||
}
|
||||
|
||||
closeMenu = () => {
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
window.postMessage({type: CLOSE_TEAMS_DROPDOWN}, window.location.href);
|
||||
if (!this.state.isAnyDragging) {
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
window.postMessage({type: CLOSE_TEAMS_DROPDOWN}, window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
preventPropogation = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
@@ -70,6 +88,39 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||
return team.name === this.state.activeTeam;
|
||||
}
|
||||
|
||||
onDragStart = () => {
|
||||
this.setState({isAnyDragging: true});
|
||||
}
|
||||
|
||||
onDragEnd = (result: DropResult) => {
|
||||
const removedIndex = result.source.index;
|
||||
const addedIndex = result.destination?.index;
|
||||
if (addedIndex === undefined || removedIndex === addedIndex) {
|
||||
this.setState({isAnyDragging: false});
|
||||
return;
|
||||
}
|
||||
if (!this.state.teams) {
|
||||
throw new Error('No config');
|
||||
}
|
||||
const teams = this.state.teams.concat();
|
||||
const tabOrder = teams.map((team, index) => {
|
||||
return {
|
||||
index,
|
||||
order: team.order,
|
||||
};
|
||||
}).sort((a, b) => (a.order - b.order));
|
||||
|
||||
const team = tabOrder.splice(removedIndex, 1);
|
||||
const newOrder = addedIndex < this.state.teams.length ? addedIndex : this.state.teams.length - 1;
|
||||
tabOrder.splice(newOrder, 0, team[0]);
|
||||
|
||||
tabOrder.forEach((t, order) => {
|
||||
teams[t.index].order = order;
|
||||
});
|
||||
this.setState({teams, orderedTeams: teams.concat().sort((a: Team, b: Team) => a.order - b.order), isAnyDragging: false});
|
||||
window.postMessage({type: UPDATE_TEAMS, data: teams}, window.location.href);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.postMessage({type: REQUEST_TEAMS_DROPDOWN_INFO}, window.location.href);
|
||||
window.addEventListener('click', this.closeMenu);
|
||||
@@ -83,6 +134,12 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||
window.removeEventListener('click', this.closeMenu);
|
||||
}
|
||||
|
||||
handleClickOnDragHandle = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (this.state.isAnyDragging) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
@@ -95,61 +152,106 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||
<span>{'Servers'}</span>
|
||||
</div>
|
||||
<hr className='TeamDropdown__divider'/>
|
||||
{this.state.orderedTeams?.map((team, index) => {
|
||||
const sessionExpired = this.state.expired?.get(team.name);
|
||||
const hasUnreads = this.state.unreads?.get(team.name);
|
||||
const mentionCount = this.state.mentions?.get(team.name);
|
||||
<DragDropContext
|
||||
onDragStart={this.onDragStart}
|
||||
onDragEnd={this.onDragEnd}
|
||||
>
|
||||
<Droppable
|
||||
isDropDisabled={this.state.hasGPOTeams}
|
||||
droppableId='TeamDropdown__droppable'
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
className='TeamDropdown__droppable'
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{this.state.orderedTeams?.map((team, orderedIndex) => {
|
||||
const index = this.state.teams?.indexOf(team);
|
||||
|
||||
let badgeDiv: React.ReactNode;
|
||||
if (sessionExpired) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-expired'>
|
||||
<i className='icon-alert-circle-outline'/>
|
||||
</div>
|
||||
);
|
||||
} else if (mentionCount && mentionCount > 0) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-count'>
|
||||
<span>{mentionCount > 99 ? '99+' : mentionCount}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (hasUnreads) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-dot'/>
|
||||
);
|
||||
}
|
||||
const sessionExpired = this.state.expired?.get(team.name);
|
||||
const hasUnreads = this.state.unreads?.get(team.name);
|
||||
const mentionCount = this.state.mentions?.get(team.name);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={'TeamDropdown__button'}
|
||||
onClick={this.selectServer(team)}
|
||||
key={index}
|
||||
>
|
||||
{this.isActiveTeam(team) ? <i className='icon-check'/> : <i className='icon-server-variant'/>}
|
||||
<span>{team.name}</span>
|
||||
<div className='TeamDropdown__indicators'>
|
||||
<button
|
||||
className='TeamDropdown__button-edit'
|
||||
disabled={true}
|
||||
>
|
||||
<i className='icon-pencil-outline'/>
|
||||
</button>
|
||||
<button
|
||||
className='TeamDropdown__button-remove'
|
||||
disabled={true}
|
||||
>
|
||||
<i className='icon-trash-can-outline'/>
|
||||
</button>
|
||||
{badgeDiv && <div className='TeamDropdown__badge'>
|
||||
{badgeDiv}
|
||||
</div>}
|
||||
let badgeDiv: React.ReactNode;
|
||||
if (sessionExpired) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-expired'>
|
||||
<i className='icon-alert-circle-outline'/>
|
||||
</div>
|
||||
);
|
||||
} else if (mentionCount && mentionCount > 0) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-count'>
|
||||
<span>{mentionCount > 99 ? '99+' : mentionCount}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (hasUnreads) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-dot'/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={index}
|
||||
draggableId={`TeamDropdown__draggable-${index}`}
|
||||
index={orderedIndex}
|
||||
disableInteractiveElementBlocking={true}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<button
|
||||
className={classNames('TeamDropdown__button', {
|
||||
dragging: snapshot.isDragging,
|
||||
anyDragging: this.state.isAnyDragging,
|
||||
active: this.isActiveTeam(team),
|
||||
})}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
onClick={this.selectServer(team)}
|
||||
style={getStyle(provided.draggableProps.style)}
|
||||
>
|
||||
<div
|
||||
className={classNames('TeamDropdown__draggable-handle', {
|
||||
dragging: snapshot.isDragging,
|
||||
})}
|
||||
{...provided.dragHandleProps}
|
||||
onClick={this.handleClickOnDragHandle}
|
||||
>
|
||||
<i className='icon-drag-vertical'/>
|
||||
{this.isActiveTeam(team) ? <i className='icon-check'/> : <i className='icon-server-variant'/>}
|
||||
<span>{team.name}</span>
|
||||
</div>
|
||||
<div className='TeamDropdown__indicators'>
|
||||
<button
|
||||
className='TeamDropdown__button-edit'
|
||||
disabled={true}
|
||||
>
|
||||
<i className='icon-pencil-outline'/>
|
||||
</button>
|
||||
<button
|
||||
className='TeamDropdown__button-remove'
|
||||
disabled={true}
|
||||
>
|
||||
<i className='icon-trash-can-outline'/>
|
||||
</button>
|
||||
{badgeDiv && <div className='TeamDropdown__badge'>
|
||||
{badgeDiv}
|
||||
</div>}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<hr className='TeamDropdown__divider'/>
|
||||
<button
|
||||
className='TeamDropdown__button'
|
||||
className='TeamDropdown__button addServer'
|
||||
onClick={this.addServer}
|
||||
>
|
||||
<i className='icon-plus'/>
|
||||
|
Reference in New Issue
Block a user