Skip to content

Commit

Permalink
[MI-3831]: Link channels modal (#40)
Browse files Browse the repository at this point in the history
* [MI-3831]: Link channels modal

* [MI-3831]: Fix lint errors

* [MI-3831]: Update naming

* [MI-3833]: Link channel modal integration (#43)

* [MI-3833]: Link Channel Modal integration

* [MI-3833]: Add constants

* [MI-3833]: Update constants

* [MI-3837]: Integrate webhooks and enhancements (#47)

* [MI-3837]: Integrate webhooks and enhancements

* [MI-3837]: Remove yarn lock

* [MI-3834] Add unlink button on hover state on linked channel list (#45)

* [MI-3834] Add unlink button on hover state on linked channel list

* [MI-3834] Update style and props of unlink button

* [MI-3836] Integrate API to unlink channels and websocket event (#46)

* [MI-3836] Integrate API to unlink channels and websocket event

* [MI-3836] Fix issue of alert not visible

* [MI-3836] Update dialog component

* [MI-3836] Minor review fixes and revert some changes

---------

Co-authored-by: Ayush Thakur <[email protected]>

* [MI-3831]: Minor refactoring

* [MI-3831]: Update UI-Lib version

---------

Co-authored-by: Ayush Thakur <[email protected]>
  • Loading branch information
SaurabhSharma-884 and ayusht2810 authored Dec 15, 2023
1 parent 7b03ae6 commit 7d082b6
Show file tree
Hide file tree
Showing 41 changed files with 951 additions and 219 deletions.
1 change: 0 additions & 1 deletion server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ func NewAPI(p *Plugin, store store.Store) *API {
router.HandleFunc("/connect", api.handleAuthRequired(api.connect)).Methods(http.MethodGet)
router.HandleFunc("/disconnect", api.handleAuthRequired(api.checkUserConnected(api.disconnect))).Methods(http.MethodGet)
router.HandleFunc("/linked-channels", api.handleAuthRequired(api.getLinkedChannels)).Methods(http.MethodGet)
router.HandleFunc("/link-channels", api.handleAuthRequired(api.checkUserConnected(api.linkChannels))).Methods(http.MethodPost)
router.HandleFunc("/oauth-redirect", api.oauthRedirectHandler).Methods(http.MethodGet)
router.HandleFunc("/connected-users", api.getConnectedUsers).Methods(http.MethodGet)
router.HandleFunc("/connected-users/download", api.getConnectedUsersFile).Methods(http.MethodGet)
Expand Down
2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"webpack-cli": "5.0.1"
},
"dependencies": {
"@brightscout/mattermost-ui-library": "2.3.3",
"@brightscout/mattermost-ui-library": "2.3.4",
"react-infinite-scroll-component": "6.1.0",
"@reduxjs/toolkit": "1.8.2",
"core-js": "3.29.1",
Expand Down
43 changes: 12 additions & 31 deletions webapp/src/components/Dialog/Dialog.component.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,16 @@
import React from 'react';
import {useDispatch} from 'react-redux';

import {Dialog as MMDialog, LinearProgress, DialogProps} from '@brightscout/mattermost-ui-library';

import usePluginApi from 'hooks/usePluginApi';
import {getDialogState} from 'selectors';
import {closeDialog} from 'reducers/dialog';

export const Dialog = ({onCloseHandler, onSubmitHandler}: Pick<DialogProps, 'onCloseHandler' | 'onSubmitHandler'>) => {
const dispatch = useDispatch();
const {state} = usePluginApi();
const {show, title, description, destructive, primaryButtonText, secondaryButtonText, isLoading} = getDialogState(state);

const handleClose = () => dispatch(closeDialog());

return (
<MMDialog
description={description}
destructive={destructive}
show={show}
primaryButtonText={primaryButtonText}
secondaryButtonText={secondaryButtonText}
onCloseHandler={() => {
handleClose();
onCloseHandler();
}}
onSubmitHandler={onSubmitHandler}
className='disconnect-dialog'
title={title}
>
{isLoading && <LinearProgress/>}
</MMDialog>
);
};
export const Dialog = ({
children,
isLoading = false,
...rest
}: DialogProps & {isLoading?: boolean, children: React.ReactNode}) => (
<MMDialog
{...rest}
>
{isLoading && <LinearProgress className='absolute w-full left-0 top-62'/>}
{children}
</MMDialog>
);
17 changes: 16 additions & 1 deletion webapp/src/components/Icon/Icon.map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,6 @@ export const IconMap : Record<IconName, JSX.Element> = {
width='18'
height='18'
transform='translate(0.5 0.5)'
fill='white'
/>
<path
d='M8.6504 10.3596C8.7752 10.4748 8.8376 10.6188 8.8376 10.7916C8.8376 10.9548 8.7752 11.094 8.6504 11.2092C8.5352 11.3244 8.3912 11.382 8.2184 11.382C8.0552 11.382 7.916 11.3244 7.8008 11.2092C7.4168 10.8252 7.1576 10.3788 7.0232 9.87005C6.8888 9.35165 6.8888 8.83325 7.0232 8.31485C7.1576 7.79645 7.4168 7.34525 7.8008 6.96125L9.932 4.84445C10.316 4.46045 10.7624 4.20125 11.2712 4.06685C11.7896 3.93245 12.3032 3.93245 12.812 4.06685C13.3304 4.20125 13.7816 4.46045 14.1656 4.84445C14.5496 5.22845 14.8088 5.67965 14.9432 6.19805C15.0776 6.70685 15.0776 7.22045 14.9432 7.73885C14.8088 8.24765 14.5496 8.69405 14.1656 9.07805L13.2728 9.97085C13.2824 9.48125 13.2008 9.00125 13.028 8.53085L13.316 8.24285C13.5464 8.01245 13.7 7.74365 13.7768 7.43645C13.8632 7.12925 13.8632 6.82205 13.7768 6.51485C13.7 6.19805 13.5464 5.92445 13.316 5.69405C13.0856 5.46365 12.812 5.31005 12.4952 5.23325C12.188 5.14685 11.8808 5.14685 11.5736 5.23325C11.2664 5.31005 10.9976 5.46365 10.7672 5.69405L8.6504 7.81085C8.42 8.04125 8.2616 8.31485 8.1752 8.63165C8.0984 8.93885 8.0984 9.24605 8.1752 9.55325C8.2616 9.86045 8.42 10.1292 8.6504 10.3596ZM10.3496 7.81085C10.4648 7.69565 10.604 7.63805 10.7672 7.63805C10.94 7.63805 11.084 7.69565 11.1992 7.81085C11.5832 8.19485 11.8424 8.64605 11.9768 9.16445C12.1112 9.67325 12.1112 10.1868 11.9768 10.7052C11.8424 11.2236 11.5832 11.6748 11.1992 12.0588L9.068 14.1756C8.684 14.5596 8.2328 14.8188 7.7144 14.9532C7.2056 15.0876 6.692 15.0876 6.1736 14.9532C5.6648 14.8188 5.2184 14.5596 4.8344 14.1756C4.4504 13.7916 4.1912 13.3452 4.0568 12.8364C3.9224 12.318 3.9224 11.8044 4.0568 11.2956C4.1912 10.7772 4.4504 10.326 4.8344 9.94205L5.7272 9.03485C5.7272 9.56285 5.8088 10.0524 5.972 10.5036L5.684 10.7772C5.4536 11.0076 5.2952 11.2764 5.2088 11.5836C5.132 11.8908 5.132 12.2028 5.2088 12.5196C5.2952 12.8268 5.4536 13.0956 5.684 13.326C5.9144 13.5564 6.1832 13.7148 6.4904 13.8012C6.7976 13.878 7.1048 13.878 7.412 13.8012C7.7288 13.7148 8.0024 13.5564 8.2328 13.326L10.3496 11.2092C10.58 10.9788 10.7336 10.71 10.8104 10.4028C10.8968 10.086 10.8968 9.77405 10.8104 9.46685C10.7336 9.15965 10.58 8.89085 10.3496 8.66045C10.2248 8.54525 10.1624 8.40605 10.1624 8.24285C10.1624 8.07005 10.2248 7.92605 10.3496 7.81085Z'
Expand Down Expand Up @@ -462,4 +461,20 @@ export const IconMap : Record<IconName, JSX.Element> = {
/>
</svg>
),
mattermost: (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M15.4248 10.1717C15.4248 10.1717 15.4559 11.5363 14.5096 12.4634C13.5632 13.3904 12.4008 13.3057 11.6446 13.0497C10.8884 12.7936 9.91359 12.1549 9.72489 10.8437C9.53632 9.53238 10.3898 8.46713 10.3898 8.46713L12.2498 6.15412L13.3331 4.83309L14.2629 3.68188C14.2629 3.68188 14.6896 3.11029 14.8113 2.99232C14.8354 2.96895 14.8601 2.95361 14.8843 2.9418L14.902 2.93279L14.9051 2.93157C14.9563 2.90954 15.0152 2.90479 15.072 2.92403C15.1277 2.9429 15.1708 2.98112 15.198 3.02824L15.2038 3.03737L15.2088 3.04784C15.222 3.07292 15.2331 3.10153 15.2383 3.13683C15.2632 3.30447 15.255 4.01777 15.255 4.01777L15.2943 5.49695L15.3524 7.2044L15.4248 10.1717ZM17.8325 3.90867C21.2725 6.41232 22.8502 10.9559 21.4157 15.2055C19.6501 20.4353 13.9909 23.2398 8.77554 21.4694C3.56021 19.6989 0.763636 14.0241 2.52911 8.79424C3.96607 4.53772 7.98216 1.8881 12.239 2.00363L10.8745 3.62026C8.34917 4.07814 6.16873 5.8075 5.31172 8.34622C4.0366 12.1233 6.17382 16.2617 10.0854 17.5895C13.9968 18.9173 18.2015 16.9318 19.4766 13.1547C20.3308 10.6244 19.6535 7.93217 17.9375 6.03115L17.8325 3.90867Z'
fill='#1E325C'
/>
</svg>
),
};
2 changes: 1 addition & 1 deletion webapp/src/components/Icon/Icon.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type IconName = 'user' | 'message' | 'connectAccount' | 'warning' | 'close' | 'globe' | 'msTeams' | 'link' | 'noChannels' | 'tick' | 'lock'
export type IconName = 'user' | 'message' | 'connectAccount' | 'warning' | 'close' | 'globe' | 'msTeams' | 'link' | 'noChannels' | 'tick' | 'lock' | 'mattermost'

export type IconProps = {
iconName: IconName;
Expand Down
130 changes: 130 additions & 0 deletions webapp/src/components/LinkChannelModal/LinkChannelModal.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, {useState} from 'react';

import {LinearProgress, Modal} from '@brightscout/mattermost-ui-library';

import {useDispatch} from 'react-redux';

import usePluginApi from 'hooks/usePluginApi';

import {getCurrentTeam, getLinkModalState} from 'selectors';

import {Dialog} from 'components/Dialog';
import {pluginApiServiceConfigs} from 'constants/apiService.constant';
import useApiRequestCompletionState from 'hooks/useApiRequestCompletionState';
import {hideLinkModal, preserveState, resetState, setLinkModalLoading, showLinkModal} from 'reducers/linkModal';
import useAlert from 'hooks/useAlert';

import {refetch} from 'reducers/refetchState';

import {SearchMSChannels} from './SearchMSChannels';
import {SearchMSTeams} from './SearchMSTeams';
import {SearchMMChannels} from './SearchMMChannels';

export const LinkChannelModal = () => {
const dispatch = useDispatch();
const showAlert = useAlert();
const {state, makeApiRequestWithCompletionStatus} = usePluginApi();
const {show = false, isLoading} = getLinkModalState(state);
const {currentTeamId} = getCurrentTeam(state);

// Show retry dialog component
const [showRetryDialog, setShowRetryDialog] = useState(false);

const [mmChannel, setMMChannel] = useState<MMTeamOrChannel | null>(null);
const [msTeam, setMSTeam] = useState<MSTeamOrChannel | null>(null);
const [msChannel, setMSChannel] = useState<MSTeamOrChannel | null>(null);
const [linkChannelsPayload, setLinkChannelsPayload] = useState<LinkChannelsPayload | null>(null);

const handleModalClose = (preserve?: boolean) => {
if (!preserve) {
setMMChannel(null);
setMSTeam(null);
setMSChannel(null);
}
dispatch(resetState());
dispatch(hideLinkModal());
};

const handleChannelLinking = () => {
const payload: LinkChannelsPayload = {
mattermostTeamID: currentTeamId || '',
mattermostChannelID: mmChannel?.id || '',
msTeamsTeamID: msTeam?.ID || '',
msTeamsChannelID: msChannel?.ID || '',
};
setLinkChannelsPayload(payload);
makeApiRequestWithCompletionStatus(pluginApiServiceConfigs.linkChannels.apiServiceName, payload);
dispatch(setLinkModalLoading(true));
};

useApiRequestCompletionState({
serviceName: pluginApiServiceConfigs.linkChannels.apiServiceName,
payload: linkChannelsPayload as LinkChannelsPayload,
handleSuccess: () => {
dispatch(setLinkModalLoading(false));
handleModalClose();
dispatch(refetch());
showAlert({
message: 'Successfully linked channels',
severity: 'success',
});
},
handleError: () => {
dispatch(setLinkModalLoading(false));
handleModalClose(true);
setShowRetryDialog(true);
},
});

return (
<>
<Modal
show={show}
className='msteams-sync-modal'
title='Link a channel'
subtitle='Link a channel in Mattermost with a channel in Microsoft Teams'
primaryActionText='Link Channels'
secondaryActionText='Cancel'
onFooterCloseHandler={handleModalClose}
onHeaderCloseHandler={handleModalClose}
isPrimaryButtonDisabled={!mmChannel || !msChannel || !msTeam}
onSubmitHandler={handleChannelLinking}
backdrop={true}
>
{isLoading && <LinearProgress className='fixed w-full left-0 top-100'/>}
<SearchMMChannels
setChannel={setMMChannel}
teamId={currentTeamId}
/>
<hr className='w-full my-32'/>
<SearchMSTeams setMSTeam={setMSTeam}/>
<SearchMSChannels
setChannel={setMSChannel}
teamId={msTeam?.ID}
/>
</Modal>
<Dialog
show={showRetryDialog}
destructive={true}
primaryButtonText='Try Again'
secondaryButtonText='Cancel'
title='Unable to link channels'
onSubmitHandler={() => {
dispatch(preserveState({
mmChannel: mmChannel?.displayName ?? '',
msChannel: msChannel?.DisplayName ?? '',
msTeam: msTeam?.DisplayName ?? '',
}));
setShowRetryDialog(false);
dispatch(showLinkModal());
}}
onCloseHandler={() => {
setShowRetryDialog(false);
dispatch(resetState());
}}
>
{'We were not able to link the selected channels. Please try again.'}
</Dialog>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, {useCallback, useEffect, useState} from 'react';

import {Client4} from 'mattermost-redux/client';
import {channels as MMChannelTypes} from 'mattermost-redux/types';
import {General as MMConstants} from 'mattermost-redux/constants';

import {ListItemType, MMSearch} from '@brightscout/mattermost-ui-library';

import {useDispatch} from 'react-redux';

import utils from 'utils';

import {Icon} from 'components/Icon';

import {debounceFunctionTimeLimitInMilliseconds} from 'constants/common.constants';

import {setLinkModalLoading} from 'reducers/linkModal';

import usePluginApi from 'hooks/usePluginApi';
import {getCurrentTeam, getLinkModalState} from 'selectors';

import {SearchMMChannelProps} from './SearchMMChannels.types';

export const SearchMMChannels = ({
setChannel,
teamId,
}: SearchMMChannelProps) => {
const dispatch = useDispatch();
const {state} = usePluginApi();
const [searchTerm, setSearchTerm] = useState<string>('');
const {teams} = getCurrentTeam(state);
const {mmChannel} = getLinkModalState(state);

const [searchSuggestions, setSearchSuggestions] = useState<ListItemType[]>([]);
const [suggestionsLoading, setSuggestionsLoading] = useState<boolean>(false);

useEffect(() => {
if (!mmChannel) {
handleClearInput();
}
setSearchTerm(mmChannel);
}, [teamId]);

const searchChannels = ({searchFor}: {searchFor?: string}) => {
if (searchFor) {
setSuggestionsLoading(true);
dispatch(setLinkModalLoading(true));
Client4.searchAllChannels(searchFor).
then((channels) => {
const suggestions:ListItemType[] = [];
for (const channel of channels as MMChannelTypes.Channel[]) {
suggestions.push({
label: channel.display_name,
value: channel.id,
secondaryLabel: teams[channel.team_id].display_name,
icon: channel.type === MMConstants.PRIVATE_CHANNEL ? 'Lock' : 'Globe',
});
}
setSearchSuggestions(suggestions);
}).finally(() => {
setSuggestionsLoading(false);
dispatch(setLinkModalLoading(false));
});
}
};

const debouncedSearchChannels = useCallback(utils.debounce(searchChannels, debounceFunctionTimeLimitInMilliseconds), [searchChannels]);

const handleSearch = (val: string) => {
if (!val) {
setSearchSuggestions([]);
setChannel(null);
}
setSearchTerm(val);
debouncedSearchChannels({searchFor: val});
};

const handleChannelSelect = (_: any, option: ListItemType) => {
setChannel({
id: option.value,
displayName: option.label as string,
});
setSearchTerm(option.label as string);
};

const handleClearInput = () => {
setSearchTerm('');
setSearchSuggestions([]);
setChannel(null);
};

return (
<div className='d-flex flex-column gap-24'>
<div className='d-flex gap-8 align-items-center'>
<Icon iconName='mattermost'/>
<h5 className='my-0 lh-20 wt-600'>{'Select a Mattermost channel'}</h5>
</div>
<MMSearch
autoFocus={true}
fullWidth={true}
label='Search Mattermost channels'
items={searchSuggestions}
secondaryLabelPosition='inline'
onSelect={handleChannelSelect}
searchValue={searchTerm}
setSearchValue={handleSearch}
onClearInput={handleClearInput}
optionsLoading={suggestionsLoading}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type SearchMMChannelProps = {
setChannel: React.Dispatch<React.SetStateAction<MMTeamOrChannel | null>>;
teamId: string | null,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {SearchMMChannels} from './SearchMMChannels.component';
Loading

0 comments on commit 7d082b6

Please sign in to comment.