[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:
Devin Binnie
2021-07-14 16:29:35 -04:00
committed by GitHub
parent 26ca8ccefc
commit e71c4ff9f0
13 changed files with 252 additions and 78 deletions

View File

@@ -446,7 +446,7 @@ workflows:
branches: branches:
only: only:
- /^release-\d+(\.\d+){1,2}(-rc.*)?/ - /^release-\d+(\.\d+){1,2}(-rc.*)?/
- pull/1647 - pull/1651
- store_artifacts: - store_artifacts:
# for master/PR builds # for master/PR builds

View File

@@ -58,6 +58,13 @@ export const updateUnreads = (serverName: string, unreads: boolean) => {
emitMentions(serverName); emitMentions(serverName);
}; };
export const updateBadge = () => {
const expired = anyExpired();
const mentions = totalMentions();
const unreads = anyUnreads();
emitBadge(expired, mentions, unreads);
};
export const getUnreads = (serverName: string) => { export const getUnreads = (serverName: string) => {
return status.unreads.get(serverName) || false; return status.unreads.get(serverName) || false;
}; };

View File

@@ -64,7 +64,7 @@ function showBadge(sessionExpired: boolean, mentionCount: number, showUnreadBadg
export function setUnreadBadgeSetting(showUnreadBadge: boolean) { export function setUnreadBadgeSetting(showUnreadBadge: boolean) {
showUnreadBadgeSetting = showUnreadBadge; showUnreadBadgeSetting = showUnreadBadge;
AppState.emitStatus(); AppState.updateBadge();
} }
export function setupBadge() { export function setupBadge() {

View File

@@ -13,6 +13,7 @@ import {
SWITCH_SERVER, SWITCH_SERVER,
CLOSE_TEAMS_DROPDOWN, CLOSE_TEAMS_DROPDOWN,
SHOW_NEW_SERVER_MODAL, SHOW_NEW_SERVER_MODAL,
UPDATE_TEAMS,
} from 'common/communication'; } from 'common/communication';
console.log('preloaded for the dropdown!'); console.log('preloaded for the dropdown!');
@@ -34,12 +35,15 @@ window.addEventListener('message', async (event) => {
case CLOSE_TEAMS_DROPDOWN: case CLOSE_TEAMS_DROPDOWN:
ipcRenderer.send(CLOSE_TEAMS_DROPDOWN); ipcRenderer.send(CLOSE_TEAMS_DROPDOWN);
break; break;
case UPDATE_TEAMS:
ipcRenderer.invoke(UPDATE_TEAMS, event.data.data);
break;
default: default:
console.log(`got a message: ${event}`); console.log(`got a message: ${event}`);
console.log(event); console.log(event);
} }
}); });
ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (event, teams, activeTeam, darkMode, expired, mentions, unreads) => { ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (event, teams, activeTeam, darkMode, hasGPOTeams, expired, mentions, unreads) => {
window.postMessage({type: UPDATE_TEAMS_DROPDOWN, data: {teams, activeTeam, darkMode, expired, mentions, unreads}}, window.location.href); window.postMessage({type: UPDATE_TEAMS_DROPDOWN, data: {teams, activeTeam, darkMode, hasGPOTeams, expired, mentions, unreads}}, window.location.href);
}); });

View File

@@ -25,6 +25,7 @@ export default class TeamDropdownView {
teams: Team[]; teams: Team[];
activeTeam?: string; activeTeam?: string;
darkMode: boolean; darkMode: boolean;
hasGPOTeams?: boolean;
unreads?: Map<string, boolean>; unreads?: Map<string, boolean>;
mentions?: Map<string, number>; mentions?: Map<string, number>;
expired?: Map<string, boolean>; expired?: Map<string, boolean>;
@@ -57,6 +58,7 @@ export default class TeamDropdownView {
updateConfig = (event: IpcMainEvent, config: CombinedConfig) => { updateConfig = (event: IpcMainEvent, config: CombinedConfig) => {
this.teams = config.teams; this.teams = config.teams;
this.darkMode = config.darkMode; this.darkMode = config.darkMode;
this.hasGPOTeams = config.registryTeams && config.registryTeams.length > 0;
this.updateDropdown(); this.updateDropdown();
} }
@@ -73,7 +75,7 @@ export default class TeamDropdownView {
} }
updateDropdown = () => { updateDropdown = () => {
this.view.webContents.send(UPDATE_TEAMS_DROPDOWN, this.teams, this.activeTeam, this.darkMode, this.expired, this.mentions, this.unreads); this.view.webContents.send(UPDATE_TEAMS_DROPDOWN, this.teams, this.activeTeam, this.darkMode, this.hasGPOTeams, this.expired, this.mentions, this.unreads);
} }
handleOpen = () => { handleOpen = () => {

View File

@@ -368,6 +368,8 @@
<glyph glyph-name="credit-card-outline" unicode="&#xf019b;" d="M833 517h-666v83h666m0-500h-666v250h666m0 333h-666c-47 0-84-37-84-83v-500a83 83 0 0 1 84-83h666a83 83 0 0 1 84 83v500c0 46-38 83-84 83z" horiz-adv-x="1000" /> <glyph glyph-name="credit-card-outline" unicode="&#xf019b;" d="M833 517h-666v83h666m0-500h-666v250h666m0 333h-666c-47 0-84-37-84-83v-500a83 83 0 0 1 84-83h666a83 83 0 0 1 84 83v500c0 46-38 83-84 83z" horiz-adv-x="1000" />
<glyph glyph-name="drag-vertical" unicode="&#xf01dd;" d="M375 725h83v-83h-83v83m167 0h83v-83h-83v83m-167-167h83v-83h-83v83m167 0h83v-83h-83v83m-167-166h83v-84h-83v84m167 0h83v-84h-83v84m-167-167h83v-83h-83v83m167 0h83v-83h-83v83m-167-167h83v-83h-83v83m167 0h83v-83h-83v83z" horiz-adv-x="1000" />
<glyph glyph-name="view-grid-outline" unicode="&#xf0204;" d="M125 392h333v333h-333m83-83h167v-167h-167m334-500h333v333h-333m83-83h167v-167h-167m-500-83h333v333h-333m83-83h167v-167h-167m334 667v-333h333v333m-83-250h-167v167h167z" horiz-adv-x="1000" /> <glyph glyph-name="view-grid-outline" unicode="&#xf0204;" d="M125 392h333v333h-333m83-83h167v-167h-167m334-500h333v333h-333m83-83h167v-167h-167m-500-83h333v333h-333m83-83h167v-167h-167m334 667v-333h333v333m-83-250h-167v167h167z" horiz-adv-x="1000" />
<glyph glyph-name="exit-to-app" unicode="&#xf0206;" d="M792 725h-584c-46 0-83-37-83-83v-167h83v167h584v-584h-584v167h-83v-167c0-46 37-83 83-83h584c46 0 83 37 83 83v584c0 46-37 83-83 83m-372-524l59-59 209 208-209 208-59-58 108-108h-403v-84h403l-108-107z" horiz-adv-x="1000" /> <glyph glyph-name="exit-to-app" unicode="&#xf0206;" d="M792 725h-584c-46 0-83-37-83-83v-167h83v167h584v-584h-584v167h-83v-167c0-46 37-83 83-83h584c46 0 83 37 83 83v584c0 46-37 83-83 83m-372-524l59-59 209 208-209 208-59-58 108-108h-403v-84h403l-108-107z" horiz-adv-x="1000" />
@@ -398,6 +400,8 @@
<glyph glyph-name="minus-circle-outline" unicode="&#xf0377;" d="M500 17c-184 0-333 149-333 333 0 184 149 333 333 333 184 0 333-149 333-333 0-184-149-333-333-333m0 750a417 417 0 0 1-417-417 417 417 0 0 1 417-417 417 417 0 0 1 417 417 417 417 0 0 1-417 417m-208-459h416v84h-416" horiz-adv-x="1000" /> <glyph glyph-name="minus-circle-outline" unicode="&#xf0377;" d="M500 17c-184 0-333 149-333 333 0 184 149 333 333 333 184 0 333-149 333-333 0-184-149-333-333-333m0 750a417 417 0 0 1-417-417 417 417 0 0 1 417-417 417 417 0 0 1 417 417 417 417 0 0 1-417 417m-208-459h416v84h-416" horiz-adv-x="1000" />
<glyph glyph-name="open-in-new" unicode="&#xf03cc;" d="M583 725v-83h150l-410-410 59-59 410 410v-150h83v292m-83-667h-584v584h292v83h-292c-46 0-83-37-83-83v-584a83 83 0 0 1 83-83h584a83 83 0 0 1 83 83v292h-83v-292z" horiz-adv-x="1000" />
<glyph glyph-name="pause" unicode="&#xf03e4;" d="M583 58h167v584h-167m-333-584h167v584h-167v-584z" horiz-adv-x="1000" /> <glyph glyph-name="pause" unicode="&#xf03e4;" d="M583 58h167v584h-167m-333-584h167v584h-167v-584z" horiz-adv-x="1000" />
<glyph glyph-name="play" unicode="&#xf040a;" d="M333 636v-583l459 291-459 292z" horiz-adv-x="1000" /> <glyph glyph-name="play" unicode="&#xf040a;" d="M333 636v-583l459 291-459 292z" horiz-adv-x="1000" />
@@ -422,6 +426,8 @@
<glyph glyph-name="infinity" unicode="&#xf06e4;" d="M775 574c124 0 225-99 225-224 0-123-101-224-225-224-60 0-117 24-159 66l-116 102-118-104c-40-41-97-64-157-64-124 0-225 101-225 224 0 123 101 224 225 224 60 0 117-23 159-66l116-102 118 104c40 41 97 64 157 64m-450-324l113 100-111 98c-29 28-64 43-102 43-78 0-142-63-142-141 0-78 64-141 142-141 38 0 73 15 100 41m350 200l-112-100 110-98c29-28 65-43 102-43 78 0 142 63 142 141 0 78-64 141-142 141-38 0-73-15-100-41z" horiz-adv-x="1000" /> <glyph glyph-name="infinity" unicode="&#xf06e4;" d="M775 574c124 0 225-99 225-224 0-123-101-224-225-224-60 0-117 24-159 66l-116 102-118-104c-40-41-97-64-157-64-124 0-225 101-225 224 0 123 101 224 225 224 60 0 117-23 159-66l116-102 118 104c40 41 97 64 157 64m-450-324l113 100-111 98c-29 28-64 43-102 43-78 0-142-63-142-141 0-78 64-141 142-141 38 0 73 15 100 41m350 200l-112-100 110-98c29-28 65-43 102-43 78 0 142 63 142 141 0 78-64 141-142 141-38 0-73-15-100-41z" horiz-adv-x="1000" />
<glyph glyph-name="plus-box-outline" unicode="&#xf0704;" d="M792 58v584h-584v-584h584m0 667a83 83 0 0 0 83-83v-584a83 83 0 0 0-83-83h-584a83 83 0 0 0-83 83v584c0 46 38 83 83 83h584m-334-167h84v-166h166v-84h-166v-166h-84v166h-166v84h166v166z" horiz-adv-x="1000" />
<glyph glyph-name="arrow-right-bold-outline" unicode="&#xf09c2;" d="M458 183h-333v334h333v250l417-417-417-417v250m84 375v-125h-334v-166h334v-125l208 208-208 208z" horiz-adv-x="1000" /> <glyph glyph-name="arrow-right-bold-outline" unicode="&#xf09c2;" d="M458 183h-333v334h333v250l417-417-417-417v250m84 375v-125h-334v-166h334v-125l208 208-208 208z" horiz-adv-x="1000" />
<glyph glyph-name="trash-can-outline" unicode="&#xf0a7a;" d="M375 725v-42h-208v-83h41v-542a83 83 0 0 1 84-83h416a83 83 0 0 1 84 83v542h41v83h-208v42h-250m-83-125h416v-542h-416v542m83-83v-375h83v375h-83m167 0v-375h83v375h-83z" horiz-adv-x="1000" /> <glyph glyph-name="trash-can-outline" unicode="&#xf0a7a;" d="M375 725v-42h-208v-83h41v-542a83 83 0 0 1 84-83h416a83 83 0 0 1 84 83v542h41v83h-208v42h-250m-83-125h416v-542h-416v542m83-83v-375h83v375h-83m167 0v-375h83v375h-83z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,13 +1,12 @@
@charset "UTF-8"; @charset "UTF-8";
@font-face {
@font-face {
font-family: 'compass-icons'; font-family: 'compass-icons';
src: url('../assets/fonts/compass-icons/compass-icons.eot?45182295'); src: url('../assets/fonts/compass-icons/compass-icons.eot?81398164');
src: url('../assets/fonts/compass-icons/compass-icons.eot?45182295#iefix') format('embedded-opentype'), src: url('../assets/fonts/compass-icons/compass-icons.eot?81398164#iefix') format('embedded-opentype'),
url('../assets/fonts/compass-icons/compass-icons.woff2?45182295') format('woff2'), url('../assets/fonts/compass-icons/compass-icons.woff2?81398164') format('woff2'),
url('../assets/fonts/compass-icons/compass-icons.woff?45182295') format('woff'), url('../assets/fonts/compass-icons/compass-icons.woff?81398164') format('woff'),
url('../assets/fonts/compass-icons/compass-icons.ttf?45182295') format('truetype'), url('../assets/fonts/compass-icons/compass-icons.ttf?81398164') format('truetype'),
url('../assets/fonts/compass-icons/compass-icons.svg?45182295#compass-icons') format('svg'); url('../assets/fonts/compass-icons/compass-icons.svg?81398164#compass-icons') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@@ -17,11 +16,10 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'compass-icons'; font-family: 'compass-icons';
src: url('../fonts/compass-icons/compass-icons.svg?45182295#compass-icons') format('svg'); src: url('../font/compass-icons.svg?81398164#compass-icons') format('svg');
} }
} }
*/ */
[class^="icon-"]:before, [class*=" icon-"]:before { [class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "compass-icons"; font-family: "compass-icons";
font-style: normal; font-style: normal;
@@ -238,6 +236,7 @@
.icon-content-copy:before { content: '󰆏'; } /* '\f018f' */ .icon-content-copy:before { content: '󰆏'; } /* '\f018f' */
.icon-send-outline:before { content: '󰆐'; } /* '\f0190' */ .icon-send-outline:before { content: '󰆐'; } /* '\f0190' */
.icon-credit-card-outline:before { content: '󰆛'; } /* '\f019b' */ .icon-credit-card-outline:before { content: '󰆛'; } /* '\f019b' */
.icon-drag-vertical:before { content: '󰇝'; } /* '\f01dd' */
.icon-view-grid-outline:before { content: '󰈄'; } /* '\f0204' */ .icon-view-grid-outline:before { content: '󰈄'; } /* '\f0204' */
.icon-exit-to-app:before { content: '󰈆'; } /* '\f0206' */ .icon-exit-to-app:before { content: '󰈆'; } /* '\f0206' */
.icon-file-outline:before { content: '󰈤'; } /* '\f0224' */ .icon-file-outline:before { content: '󰈤'; } /* '\f0224' */
@@ -253,6 +252,7 @@
.icon-message-text-outline:before { content: '󰍪'; } /* '\f036a' */ .icon-message-text-outline:before { content: '󰍪'; } /* '\f036a' */
.icon-minus:before { content: '󰍴'; } /* '\f0374' */ .icon-minus:before { content: '󰍴'; } /* '\f0374' */
.icon-minus-circle-outline:before { content: '󰍷'; } /* '\f0377' */ .icon-minus-circle-outline:before { content: '󰍷'; } /* '\f0377' */
.icon-open-in-new:before { content: '󰏌'; } /* '\f03cc' */
.icon-pause:before { content: '󰏤'; } /* '\f03e4' */ .icon-pause:before { content: '󰏤'; } /* '\f03e4' */
.icon-play:before { content: '󰐊'; } /* '\f040a' */ .icon-play:before { content: '󰐊'; } /* '\f040a' */
.icon-radiobox-blank:before { content: '󰐽'; } /* '\f043d' */ .icon-radiobox-blank:before { content: '󰐽'; } /* '\f043d' */
@@ -265,6 +265,7 @@
.icon-update:before { content: '󰚰'; } /* '\f06b0' */ .icon-update:before { content: '󰚰'; } /* '\f06b0' */
.icon-eye-off-outline:before { content: '󰛑'; } /* '\f06d1' */ .icon-eye-off-outline:before { content: '󰛑'; } /* '\f06d1' */
.icon-infinity:before { content: '󰛤'; } /* '\f06e4' */ .icon-infinity:before { content: '󰛤'; } /* '\f06e4' */
.icon-plus-box-outline:before { content: '󰜄'; } /* '\f0704' */
.icon-arrow-right-bold-outline:before { content: '󰧂'; } /* '\f09c2' */ .icon-arrow-right-bold-outline:before { content: '󰧂'; } /* '\f09c2' */
.icon-trash-can-outline:before { content: '󰩺'; } /* '\f0a7a' */ .icon-trash-can-outline:before { content: '󰩺'; } /* '\f0a7a' */
.icon-account-minus-outline:before { content: '󰫬'; } /* '\f0aec' */ .icon-account-minus-outline:before { content: '󰫬'; } /* '\f0aec' */

View File

@@ -24,6 +24,15 @@ body {
min-width: 354px; min-width: 354px;
} }
.TeamDropdown__droppable {
width: 100%;
> button {
border: none;
background: transparent;
}
}
.TeamDropdown__header { .TeamDropdown__header {
padding: 6px 20px; padding: 6px 20px;
@@ -45,19 +54,23 @@ body {
.TeamDropdown__button { .TeamDropdown__button {
background-color: transparent; background-color: transparent;
border: none; border: none;
padding: 8px 18px; padding: 8px 18px 8px 7px;
display: flex; display: flex;
width: 100%; width: 100%;
align-items: center; align-items: center;
font-family: Open Sans; font-family: Open Sans;
&:hover { &:not(.anyDragging):hover {
background-color: rgba(61, 60, 64, 0.08); background-color: rgba(61, 60, 64, 0.08);
.TeamDropdown__button-edit, .TeamDropdown__button-remove { .TeamDropdown__button-edit, .TeamDropdown__button-remove {
opacity: 1; opacity: 1;
pointer-events: all; pointer-events: all;
} }
.TeamDropdown__draggable-handle > i.icon-drag-vertical {
opacity: 1;
}
} }
&:focus, &:focus-within { &:focus, &:focus-within {
@@ -68,6 +81,15 @@ body {
opacity: 1; opacity: 1;
pointer-events: all; pointer-events: all;
} }
.TeamDropdown__draggable-handle > i.icon-drag-vertical {
opacity: 1;
}
}
&.dragging .TeamDropdown__draggable-handle {
opacity: 1;
pointer-events: all;
} }
i { i {
@@ -76,17 +98,43 @@ body {
color: rgba(61, 60, 64, 0.56); color: rgba(61, 60, 64, 0.56);
} }
> i.icon-check { &.active i.icon-check {
color: #166de0; color: #166de0;
} }
> span { > .TeamDropdown__draggable-handle > span, &.addServer > span {
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
color: #3D3C40; color: #3D3C40;
margin-left: 12px; margin-left: 12px;
white-space: nowrap; white-space: nowrap;
} }
&.addServer {
padding-left: 24px;
> i.icon-plus::before {
margin: 0;
}
}
}
.TeamDropdown__draggable-handle {
cursor: pointer !important;
&.dragging {
cursor: grabbing !important;
}
> i.icon-drag-vertical {
opacity: 0;
&::before {
margin: 0;
color: rgba(61, 60, 64, 0.32);
font-size: 14px;
}
}
} }
.TeamDropdown__badge { .TeamDropdown__badge {
@@ -171,7 +219,7 @@ body {
} }
.TeamDropdown__button { .TeamDropdown__button {
&:hover { &:not(.anyDragging):hover {
background-color: rgba(221, 221, 221, 0.08); background-color: rgba(221, 221, 221, 0.08);
} }
@@ -179,11 +227,11 @@ body {
background-color: rgba(1, 119, 231, 0.08); background-color: rgba(1, 119, 231, 0.08);
} }
> i.icon-server-variant, i.icon-plus { &:not(.active) i, i.icon-drag-vertical {
color: rgba(221, 221, 221, 0.56); color: rgba(221, 221, 221, 0.56);
} }
> span { > .TeamDropdown__draggable-handle > span, &.addServer > span {
color: #DDD; color: #DDD;
} }
} }
@@ -195,4 +243,8 @@ body {
.TeamDropdown__button-edit > i { .TeamDropdown__button-edit > i {
color: rgba(221, 221, 221, 0.56); color: rgba(221, 221, 221, 0.56);
} }
.TeamDropdown__draggable-handle > i.icon-drag-vertical::before {
color:rgba(221, 221, 221, 0.32);
}
} }

View File

@@ -4,10 +4,11 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
import {Team} from 'types/config'; 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/dropdown.scss';
import './css/compass-icons.css'; import './css/compass-icons.css';
@@ -20,24 +21,39 @@ type State = {
unreads?: Map<string, boolean>; unreads?: Map<string, boolean>;
mentions?: Map<string, number>; mentions?: Map<string, number>;
expired?: Map<string, boolean>; 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> { class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
constructor(props: Record<string, never>) { constructor(props: Record<string, never>) {
super(props); super(props);
this.state = {}; this.state = {
isAnyDragging: false,
};
window.addEventListener('message', this.handleMessageEvent); window.addEventListener('message', this.handleMessageEvent);
} }
handleMessageEvent = (event: MessageEvent) => { handleMessageEvent = (event: MessageEvent) => {
if (event.data.type === UPDATE_TEAMS_DROPDOWN) { 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({ this.setState({
teams, teams,
orderedTeams: teams.concat().sort((a: Team, b: Team) => a.order - b.order), orderedTeams: teams.concat().sort((a: Team, b: Team) => a.order - b.order),
activeTeam, activeTeam,
darkMode, darkMode,
hasGPOTeams,
unreads, unreads,
mentions, mentions,
expired, expired,
@@ -53,8 +69,10 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
} }
closeMenu = () => { closeMenu = () => {
(document.activeElement as HTMLElement).blur(); if (!this.state.isAnyDragging) {
window.postMessage({type: CLOSE_TEAMS_DROPDOWN}, window.location.href); (document.activeElement as HTMLElement).blur();
window.postMessage({type: CLOSE_TEAMS_DROPDOWN}, window.location.href);
}
} }
preventPropogation = (event: React.MouseEvent<HTMLDivElement>) => { preventPropogation = (event: React.MouseEvent<HTMLDivElement>) => {
@@ -70,6 +88,39 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
return team.name === this.state.activeTeam; 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() { componentDidMount() {
window.postMessage({type: REQUEST_TEAMS_DROPDOWN_INFO}, window.location.href); window.postMessage({type: REQUEST_TEAMS_DROPDOWN_INFO}, window.location.href);
window.addEventListener('click', this.closeMenu); window.addEventListener('click', this.closeMenu);
@@ -83,6 +134,12 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
window.removeEventListener('click', this.closeMenu); window.removeEventListener('click', this.closeMenu);
} }
handleClickOnDragHandle = (event: React.MouseEvent<HTMLDivElement>) => {
if (this.state.isAnyDragging) {
event.stopPropagation();
}
}
render() { render() {
return ( return (
<div <div
@@ -95,61 +152,106 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
<span>{'Servers'}</span> <span>{'Servers'}</span>
</div> </div>
<hr className='TeamDropdown__divider'/> <hr className='TeamDropdown__divider'/>
{this.state.orderedTeams?.map((team, index) => { <DragDropContext
const sessionExpired = this.state.expired?.get(team.name); onDragStart={this.onDragStart}
const hasUnreads = this.state.unreads?.get(team.name); onDragEnd={this.onDragEnd}
const mentionCount = this.state.mentions?.get(team.name); >
<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; const sessionExpired = this.state.expired?.get(team.name);
if (sessionExpired) { const hasUnreads = this.state.unreads?.get(team.name);
badgeDiv = ( const mentionCount = this.state.mentions?.get(team.name);
<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 ( let badgeDiv: React.ReactNode;
<button if (sessionExpired) {
className={'TeamDropdown__button'} badgeDiv = (
onClick={this.selectServer(team)} <div className='TeamDropdown__badge-expired'>
key={index} <i className='icon-alert-circle-outline'/>
> </div>
{this.isActiveTeam(team) ? <i className='icon-check'/> : <i className='icon-server-variant'/>} );
<span>{team.name}</span> } else if (mentionCount && mentionCount > 0) {
<div className='TeamDropdown__indicators'> badgeDiv = (
<button <div className='TeamDropdown__badge-count'>
className='TeamDropdown__button-edit' <span>{mentionCount > 99 ? '99+' : mentionCount}</span>
disabled={true} </div>
> );
<i className='icon-pencil-outline'/> } else if (hasUnreads) {
</button> badgeDiv = (
<button <div className='TeamDropdown__badge-dot'/>
className='TeamDropdown__button-remove' );
disabled={true} }
>
<i className='icon-trash-can-outline'/> return (
</button> <Draggable
{badgeDiv && <div className='TeamDropdown__badge'> key={index}
{badgeDiv} draggableId={`TeamDropdown__draggable-${index}`}
</div>} 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> </div>
</button> )}
); </Droppable>
})} </DragDropContext>
<hr className='TeamDropdown__divider'/> <hr className='TeamDropdown__divider'/>
<button <button
className='TeamDropdown__button' className='TeamDropdown__button addServer'
onClick={this.addServer} onClick={this.addServer}
> >
<i className='icon-plus'/> <i className='icon-plus'/>