Skip to content

Commit

Permalink
[MI-3833]: Link channel modal integration (#43)
Browse files Browse the repository at this point in the history
* [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]>
  • Loading branch information
SaurabhSharma-884 and ayusht2810 authored Dec 15, 2023
1 parent 606c6d5 commit 4d5aad8
Show file tree
Hide file tree
Showing 29 changed files with 506 additions and 268 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
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>
);
1 change: 0 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
147 changes: 111 additions & 36 deletions webapp/src/components/LinkChannelModal/LinkChannelModal.component.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,130 @@
import React, {useEffect, useState} from 'react';
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 {SearchMMChannels} from './SearchMMChannels';
import {SearchMSTeams} from './SearchMSTeams';
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 = ({onClose}: {onClose: () => void}) => {
const {state} = usePluginApi();
export const LinkChannelModal = () => {
const dispatch = useDispatch();
const showAlert = useAlert();
const {state, makeApiRequestWithCompletionStatus} = usePluginApi();
const {show = false, isLoading} = getLinkModalState(state);
const currentTeam = getCurrentTeam(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 = () => {
setMMChannel(null);
setMSTeam(null);
setMSChannel(null);
onClose();
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}
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={() => {
// TODO: handle channel linking
}}
>
{isLoading && <LinearProgress className='fixed w-full left-0 top-100'/>}
<SearchMMChannels
setChannel={setMMChannel}
teamId={currentTeam}
/>
<hr className='w-full my-32'/>
<SearchMSTeams setMSTeam={setMSTeam}/>
<SearchMSChannels
setChannel={setMSChannel}
teamId={msTeam?.ID}
/>
</Modal>
<>
<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
@@ -1,6 +1,8 @@
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';

Expand All @@ -10,37 +12,48 @@ import utils from 'utils';

import {Icon} from 'components/Icon';

import {debounceFunctionTimeLimit} from 'constants/common.constants';
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(() => {
handleClearInput();
if (!mmChannel) {
handleClearInput();
}
setSearchTerm(mmChannel);
}, [teamId]);

const searchChannels = ({searchFor}: {searchFor?: string}) => {
if (searchFor && teamId) {
if (searchFor) {
setSuggestionsLoading(true);
dispatch(setLinkModalLoading(true));
Client4.autocompleteChannelsForSearch(teamId, searchFor).
Client4.searchAllChannels(searchFor).
then((channels) => {
const suggestions = [];
for (const channel of 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);
Expand All @@ -53,7 +66,7 @@ export const SearchMMChannels = ({
}
};

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

const handleSearch = (val: string) => {
if (!val) {
Expand Down Expand Up @@ -89,6 +102,7 @@ export const SearchMMChannels = ({
fullWidth={true}
label='Search Mattermost channels'
items={searchSuggestions}
secondaryLabelPosition='inline'
onSelect={handleChannelSelect}
searchValue={searchTerm}
setSearchValue={handleSearch}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@ import {useDispatch} from 'react-redux';

import usePluginApi from 'hooks/usePluginApi';
import utils from 'utils';
import {debounceFunctionTimeLimit, defaultPage, defaultPerPage} from 'constants/common.constants';
import {debounceFunctionTimeLimitInMilliseconds, defaultPage, defaultPerPage} from 'constants/common.constants';
import {pluginApiServiceConfigs} from 'constants/apiService.constant';
import useApiRequestCompletionState from 'hooks/useApiRequestCompletionState';

import {setLinkModalLoading} from 'reducers/linkModal';

import {Icon} from 'components/Icon';

import {getLinkModalState} from 'selectors';

import {SearchMSChannelProps} from './SearchMSChannels.types';

export const SearchMSChannels = ({setChannel, teamId}: SearchMSChannelProps) => {
const dispatch = useDispatch();
const {makeApiRequestWithCompletionStatus, getApiState} = usePluginApi();
const {makeApiRequestWithCompletionStatus, getApiState, state} = usePluginApi();
const {msChannel} = getLinkModalState(state);
const [searchTerm, setSearchTerm] = useState<string>('');
const [searchChannelsPayload, setSearchChannelsPayload] = useState<SearchMSChannelsParams | null>(null);
const [searchSuggestions, setSearchSuggestions] = useState<ListItemType[]>([]);

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

const searchChannels = ({searchFor}: {searchFor?: string}) => {
Expand All @@ -39,7 +47,7 @@ export const SearchMSChannels = ({setChannel, teamId}: SearchMSChannelProps) =>
}
};

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

const handleSearch = (val: string) => {
if (!val) {
Expand Down Expand Up @@ -75,6 +83,10 @@ export const SearchMSChannels = ({setChannel, teamId}: SearchMSChannelProps) =>
suggestions.push({
label: channel.DisplayName,
value: channel.ID,
secondaryLabel: channel.ID,

// TODO: Replace with msteams icon
icon: 'Pin',
});
}
setSearchSuggestions(suggestions);
Expand All @@ -83,8 +95,6 @@ export const SearchMSChannels = ({setChannel, teamId}: SearchMSChannelProps) =>
},
handleError: () => {
dispatch(setLinkModalLoading(false));

// TODO: Handle this error
},
});

Expand All @@ -98,6 +108,7 @@ export const SearchMSChannels = ({setChannel, teamId}: SearchMSChannelProps) =>
searchValue={searchTerm}
setSearchValue={handleSearch}
onClearInput={handleClearInput}
secondaryLabelPosition='inline'
optionsLoading={searchSuggestionsLoading}
disabled={!teamId}
/>
Expand Down
Loading

0 comments on commit 4d5aad8

Please sign in to comment.