From 7d082b6f1875a8bbbd6e229c999f379e02e64eef Mon Sep 17 00:00:00 2001 From: SaurabhSharma-884 Date: Fri, 15 Dec 2023 19:26:37 +0530 Subject: [PATCH] [MI-3831]: Link channels modal (#40) * [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 <100013900+ayusht2810@users.noreply.github.com> * [MI-3831]: Minor refactoring * [MI-3831]: Update UI-Lib version --------- Co-authored-by: Ayush Thakur <100013900+ayusht2810@users.noreply.github.com> --- server/api.go | 1 - webapp/package.json | 2 +- .../components/Dialog/Dialog.component.tsx | 43 ++--- webapp/src/components/Icon/Icon.map.tsx | 17 +- webapp/src/components/Icon/Icon.types.ts | 2 +- .../LinkChannelModal.component.tsx | 130 +++++++++++++++ .../SearchMMChannels.component.tsx | 112 +++++++++++++ .../SearchMMChannels.types.ts | 4 + .../SearchMMChannels/index.ts | 1 + .../SearchMSChannels.component.tsx | 115 +++++++++++++ .../SearchMSChannels.types.ts | 4 + .../SearchMSChannels/index.ts | 1 + .../SearchMSTeams/SearchMSTeams.component.tsx | 107 ++++++++++++ .../LinkChannelModal/SearchMSTeams/index.ts | 1 + .../src/components/LinkChannelModal/index.ts | 1 + .../LinkedChannelCard.component.tsx | 157 +++++++++++++----- .../LinkedChannelCard.styles.scss | 28 +++- .../LinkedChannelCard.types.ts | 2 +- webapp/src/components/index.ts | 1 + webapp/src/constants/apiService.constant.ts | 20 +++ webapp/src/constants/common.constants.ts | 4 +- webapp/src/constants/index.ts | 3 +- webapp/src/containers/Rhs/Rhs.container.tsx | 105 ++++++------ webapp/src/containers/Rhs/Rhs.styles.scss | 10 ++ webapp/src/hooks/useDialog.tsx | 32 ---- webapp/src/index.tsx | 11 +- webapp/src/reducers/dialog/index.ts | 37 ----- webapp/src/reducers/index.ts | 6 +- webapp/src/reducers/linkModal/index.ts | 43 +++++ webapp/src/reducers/refetchState/index.ts | 24 +++ webapp/src/selectors/index.tsx | 12 +- webapp/src/services/index.ts | 29 ++++ webapp/src/styles/_utils.scss | 4 + webapp/src/styles/_variables.scss | 2 +- webapp/src/types/common/index.d.ts | 12 ++ webapp/src/types/common/payload.d.ts | 13 +- webapp/src/types/common/service.d.ts | 4 +- webapp/src/types/common/store.d.ts | 15 +- webapp/src/utils/index.ts | 27 ++- webapp/src/websocket/index.ts | 20 +++ webapp/yarn.lock | 8 +- 41 files changed, 951 insertions(+), 219 deletions(-) create mode 100644 webapp/src/components/LinkChannelModal/LinkChannelModal.component.tsx create mode 100644 webapp/src/components/LinkChannelModal/SearchMMChannels/SearchMMChannels.component.tsx create mode 100644 webapp/src/components/LinkChannelModal/SearchMMChannels/SearchMMChannels.types.ts create mode 100644 webapp/src/components/LinkChannelModal/SearchMMChannels/index.ts create mode 100644 webapp/src/components/LinkChannelModal/SearchMSChannels/SearchMSChannels.component.tsx create mode 100644 webapp/src/components/LinkChannelModal/SearchMSChannels/SearchMSChannels.types.ts create mode 100644 webapp/src/components/LinkChannelModal/SearchMSChannels/index.ts create mode 100644 webapp/src/components/LinkChannelModal/SearchMSTeams/SearchMSTeams.component.tsx create mode 100644 webapp/src/components/LinkChannelModal/SearchMSTeams/index.ts create mode 100644 webapp/src/components/LinkChannelModal/index.ts delete mode 100644 webapp/src/hooks/useDialog.tsx delete mode 100644 webapp/src/reducers/dialog/index.ts create mode 100644 webapp/src/reducers/linkModal/index.ts create mode 100644 webapp/src/reducers/refetchState/index.ts diff --git a/server/api.go b/server/api.go index 1876bd1b9..7bb6593bf 100644 --- a/server/api.go +++ b/server/api.go @@ -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) diff --git a/webapp/package.json b/webapp/package.json index 21a82bec5..0961db885 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -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", diff --git a/webapp/src/components/Dialog/Dialog.component.tsx b/webapp/src/components/Dialog/Dialog.component.tsx index f20b935f0..b1a1e4141 100644 --- a/webapp/src/components/Dialog/Dialog.component.tsx +++ b/webapp/src/components/Dialog/Dialog.component.tsx @@ -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) => { - const dispatch = useDispatch(); - const {state} = usePluginApi(); - const {show, title, description, destructive, primaryButtonText, secondaryButtonText, isLoading} = getDialogState(state); - - const handleClose = () => dispatch(closeDialog()); - - return ( - { - handleClose(); - onCloseHandler(); - }} - onSubmitHandler={onSubmitHandler} - className='disconnect-dialog' - title={title} - > - {isLoading && } - - ); -}; +export const Dialog = ({ + children, + isLoading = false, + ...rest +}: DialogProps & {isLoading?: boolean, children: React.ReactNode}) => ( + + {isLoading && } + {children} + +); diff --git a/webapp/src/components/Icon/Icon.map.tsx b/webapp/src/components/Icon/Icon.map.tsx index c77a389f8..a59a8bee2 100644 --- a/webapp/src/components/Icon/Icon.map.tsx +++ b/webapp/src/components/Icon/Icon.map.tsx @@ -292,7 +292,6 @@ export const IconMap : Record = { width='18' height='18' transform='translate(0.5 0.5)' - fill='white' /> = { /> ), + mattermost: ( + + + + ), }; diff --git a/webapp/src/components/Icon/Icon.types.ts b/webapp/src/components/Icon/Icon.types.ts index f6407fdfb..8a97b2b59 100644 --- a/webapp/src/components/Icon/Icon.types.ts +++ b/webapp/src/components/Icon/Icon.types.ts @@ -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; diff --git a/webapp/src/components/LinkChannelModal/LinkChannelModal.component.tsx b/webapp/src/components/LinkChannelModal/LinkChannelModal.component.tsx new file mode 100644 index 000000000..c2fd0bf8e --- /dev/null +++ b/webapp/src/components/LinkChannelModal/LinkChannelModal.component.tsx @@ -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(null); + const [msTeam, setMSTeam] = useState(null); + const [msChannel, setMSChannel] = useState(null); + const [linkChannelsPayload, setLinkChannelsPayload] = useState(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 ( + <> + + {isLoading && } + +
+ + +
+ { + 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.'} + + + ); +}; diff --git a/webapp/src/components/LinkChannelModal/SearchMMChannels/SearchMMChannels.component.tsx b/webapp/src/components/LinkChannelModal/SearchMMChannels/SearchMMChannels.component.tsx new file mode 100644 index 000000000..b6aa8e76f --- /dev/null +++ b/webapp/src/components/LinkChannelModal/SearchMMChannels/SearchMMChannels.component.tsx @@ -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(''); + const {teams} = getCurrentTeam(state); + const {mmChannel} = getLinkModalState(state); + + const [searchSuggestions, setSearchSuggestions] = useState([]); + const [suggestionsLoading, setSuggestionsLoading] = useState(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 ( +
+
+ +
{'Select a Mattermost channel'}
+
+ +
+ ); +}; diff --git a/webapp/src/components/LinkChannelModal/SearchMMChannels/SearchMMChannels.types.ts b/webapp/src/components/LinkChannelModal/SearchMMChannels/SearchMMChannels.types.ts new file mode 100644 index 000000000..de7d1ef23 --- /dev/null +++ b/webapp/src/components/LinkChannelModal/SearchMMChannels/SearchMMChannels.types.ts @@ -0,0 +1,4 @@ +export type SearchMMChannelProps = { + setChannel: React.Dispatch>; + teamId: string | null, +} diff --git a/webapp/src/components/LinkChannelModal/SearchMMChannels/index.ts b/webapp/src/components/LinkChannelModal/SearchMMChannels/index.ts new file mode 100644 index 000000000..4c2534c95 --- /dev/null +++ b/webapp/src/components/LinkChannelModal/SearchMMChannels/index.ts @@ -0,0 +1 @@ +export {SearchMMChannels} from './SearchMMChannels.component'; diff --git a/webapp/src/components/LinkChannelModal/SearchMSChannels/SearchMSChannels.component.tsx b/webapp/src/components/LinkChannelModal/SearchMSChannels/SearchMSChannels.component.tsx new file mode 100644 index 000000000..1667287b6 --- /dev/null +++ b/webapp/src/components/LinkChannelModal/SearchMSChannels/SearchMSChannels.component.tsx @@ -0,0 +1,115 @@ +import React, {useCallback, useEffect, useState} from 'react'; + +import {ListItemType, MMSearch} from '@brightscout/mattermost-ui-library'; + +import {useDispatch} from 'react-redux'; + +import usePluginApi from 'hooks/usePluginApi'; +import utils from 'utils'; +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, state} = usePluginApi(); + const {msChannel} = getLinkModalState(state); + const [searchTerm, setSearchTerm] = useState(''); + const [searchChannelsPayload, setSearchChannelsPayload] = useState(null); + const [searchSuggestions, setSearchSuggestions] = useState([]); + + useEffect(() => { + if (!msChannel || !teamId) { + handleClearInput(); + } + setSearchTerm(msChannel); + }, [teamId]); + + const searchChannels = ({searchFor}: {searchFor?: string}) => { + if (searchFor && teamId) { + const payload = { + search: searchFor, + page: defaultPage, + per_page: defaultPerPage, + teamId, + }; + setSearchChannelsPayload(payload); + makeApiRequestWithCompletionStatus(pluginApiServiceConfigs.searchMSChannels.apiServiceName, payload); + dispatch(setLinkModalLoading(true)); + } + }; + + const debouncedSearchChannels = useCallback(utils.debounce(searchChannels, debounceFunctionTimeLimitInMilliseconds), [searchChannels]); + + const handleSearch = (val: string) => { + if (!val) { + setSearchSuggestions([]); + setChannel(null); + } + setSearchTerm(val); + debouncedSearchChannels({searchFor: val}); + }; + + const {data: searchedChannels, isLoading: searchSuggestionsLoading} = getApiState(pluginApiServiceConfigs.searchMSChannels.apiServiceName, searchChannelsPayload as SearchMSChannelsParams); + const handleChannelSelect = (_: any, option: ListItemType) => { + setChannel({ + ID: option.value, + DisplayName: option.label as string, + }); + setSearchTerm(option.label as string); + }; + + const handleClearInput = () => { + setSearchTerm(''); + setChannel(null); + setSearchSuggestions([]); + }; + + useApiRequestCompletionState({ + serviceName: pluginApiServiceConfigs.searchMSChannels.apiServiceName, + payload: searchChannelsPayload as SearchMSChannelsParams, + handleSuccess: () => { + if (searchedChannels) { + const suggestions: ListItemType[] = []; + for (const channel of searchedChannels as MSTeamsSearchResponse) { + suggestions.push({ + label: channel.DisplayName, + value: channel.ID, + secondaryLabel: channel.ID, + icon: , + }); + } + setSearchSuggestions(suggestions); + } + dispatch(setLinkModalLoading(false)); + }, + handleError: () => { + dispatch(setLinkModalLoading(false)); + }, + }); + + return ( +
+ +
+ ); +}; diff --git a/webapp/src/components/LinkChannelModal/SearchMSChannels/SearchMSChannels.types.ts b/webapp/src/components/LinkChannelModal/SearchMSChannels/SearchMSChannels.types.ts new file mode 100644 index 000000000..03a3310f2 --- /dev/null +++ b/webapp/src/components/LinkChannelModal/SearchMSChannels/SearchMSChannels.types.ts @@ -0,0 +1,4 @@ +export type SearchMSChannelProps = { + setChannel: React.Dispatch>; + teamId?: string | null, +} diff --git a/webapp/src/components/LinkChannelModal/SearchMSChannels/index.ts b/webapp/src/components/LinkChannelModal/SearchMSChannels/index.ts new file mode 100644 index 000000000..a07f7a3ef --- /dev/null +++ b/webapp/src/components/LinkChannelModal/SearchMSChannels/index.ts @@ -0,0 +1 @@ +export {SearchMSChannels} from './SearchMSChannels.component'; diff --git a/webapp/src/components/LinkChannelModal/SearchMSTeams/SearchMSTeams.component.tsx b/webapp/src/components/LinkChannelModal/SearchMSTeams/SearchMSTeams.component.tsx new file mode 100644 index 000000000..7c05ac89a --- /dev/null +++ b/webapp/src/components/LinkChannelModal/SearchMSTeams/SearchMSTeams.component.tsx @@ -0,0 +1,107 @@ +import React, {useCallback, useEffect, useState} from 'react'; + +import {ListItemType, MMSearch} from '@brightscout/mattermost-ui-library'; + +import {useDispatch} from 'react-redux'; + +import {Icon} from 'components/Icon'; +import {pluginApiServiceConfigs} from 'constants/apiService.constant'; +import {debounceFunctionTimeLimitInMilliseconds, defaultPage, defaultPerPage} from 'constants/common.constants'; +import useApiRequestCompletionState from 'hooks/useApiRequestCompletionState'; +import usePluginApi from 'hooks/usePluginApi'; +import utils from 'utils'; +import {setLinkModalLoading} from 'reducers/linkModal'; +import {getLinkModalState} from 'selectors'; + +export const SearchMSTeams = ({setMSTeam}: {setMSTeam: React.Dispatch>}) => { + const dispatch = useDispatch(); + const {makeApiRequestWithCompletionStatus, getApiState, state} = usePluginApi(); + const [searchTerm, setSearchTerm] = useState(''); + const {msTeam} = getLinkModalState(state); + const [searchTeamsPayload, setSearchTeamsPayload] = useState(null); + const [searchSuggestions, setSearchSuggestions] = useState([]); + + useEffect(() => { + setSearchTerm(msTeam); + }, []); + + const searchTeams = ({searchFor}: {searchFor?: string}) => { + if (searchFor) { + const payload = { + search: searchFor, + page: defaultPage, + per_page: defaultPerPage, + }; + setSearchTeamsPayload(payload); + makeApiRequestWithCompletionStatus(pluginApiServiceConfigs.searchMSTeams.apiServiceName, payload); + dispatch(setLinkModalLoading(true)); + } + }; + + const debouncedSearchTeams = useCallback(utils.debounce(searchTeams, debounceFunctionTimeLimitInMilliseconds), [searchTeams]); + + const handleSearch = (val: string) => { + if (!val) { + setSearchSuggestions([]); + setMSTeam(null); + } + setSearchTerm(val); + debouncedSearchTeams({searchFor: val}); + }; + + const handleTeamSelect = (_: any, option: ListItemType) => { + setMSTeam({ + ID: option.value, + DisplayName: option.label as string, + }); + setSearchTerm(option.label as string); + }; + + const handleClearInput = () => { + setSearchTerm(''); + setMSTeam(null); + setSearchSuggestions([]); + }; + + const {data: searchedTeams, isLoading: searchSuggestionsLoading} = getApiState(pluginApiServiceConfigs.searchMSTeams.apiServiceName, searchTeamsPayload as SearchParams); + useApiRequestCompletionState({ + serviceName: pluginApiServiceConfigs.searchMSTeams.apiServiceName, + payload: searchTeamsPayload as SearchParams, + handleSuccess: () => { + if (searchedTeams) { + const suggestions: ListItemType[] = []; + for (const team of searchedTeams as MSTeamsSearchResponse) { + suggestions.push({ + label: team.DisplayName, + value: team.ID, + icon: , + }); + } + setSearchSuggestions(suggestions); + } + dispatch(setLinkModalLoading(false)); + }, + handleError: () => { + dispatch(setLinkModalLoading(false)); + }, + }); + + return ( +
+
+ +
{'Select a Microsoft Teams channel'}
+
+ +
+ ); +}; diff --git a/webapp/src/components/LinkChannelModal/SearchMSTeams/index.ts b/webapp/src/components/LinkChannelModal/SearchMSTeams/index.ts new file mode 100644 index 000000000..4d5d9aeee --- /dev/null +++ b/webapp/src/components/LinkChannelModal/SearchMSTeams/index.ts @@ -0,0 +1 @@ +export {SearchMSTeams} from './SearchMSTeams.component'; diff --git a/webapp/src/components/LinkChannelModal/index.ts b/webapp/src/components/LinkChannelModal/index.ts new file mode 100644 index 000000000..f2203265c --- /dev/null +++ b/webapp/src/components/LinkChannelModal/index.ts @@ -0,0 +1 @@ +export {LinkChannelModal} from './LinkChannelModal.component'; diff --git a/webapp/src/components/LinkedChannelCard/LinkedChannelCard.component.tsx b/webapp/src/components/LinkedChannelCard/LinkedChannelCard.component.tsx index 59f0a4fa2..3d3a6a29b 100644 --- a/webapp/src/components/LinkedChannelCard/LinkedChannelCard.component.tsx +++ b/webapp/src/components/LinkedChannelCard/LinkedChannelCard.component.tsx @@ -1,51 +1,128 @@ -import React from 'react'; - -import {Tooltip} from '@brightscout/mattermost-ui-library'; +import React, {useState} from 'react'; +import {useDispatch} from 'react-redux'; +import {Button, Icon as UILibIcon, Tooltip} from '@brightscout/mattermost-ui-library'; import {General as MMConstants} from 'mattermost-redux/constants'; -import {Icon} from 'components/Icon'; +import {Icon, Dialog} from 'components'; +import {pluginApiServiceConfigs} from 'constants/apiService.constant'; +import usePluginApi from 'hooks/usePluginApi'; +import useApiRequestCompletionState from 'hooks/useApiRequestCompletionState'; +import useAlert from 'hooks/useAlert'; +import {refetch} from 'reducers/refetchState'; import {LinkedChannelCardProps} from './LinkedChannelCard.types'; import './LinkedChannelCard.styles.scss'; -export const LinkedChannelCard = ({msTeamsChannelName, msTeamsTeamName, mattermostChannelName, mattermostTeamName, mattermostChannelType}: LinkedChannelCardProps) => ( -
-
- -
-
-
- {mattermostChannelType === MMConstants.PRIVATE_CHANNEL ? : } - -
{mattermostChannelName}
-
- -
{mattermostTeamName}
-
+export const LinkedChannelCard = ({msTeamsChannelName, msTeamsTeamName, mattermostChannelName, mattermostTeamName, mattermostChannelType, mattermostChannelID}: LinkedChannelCardProps) => { + const [unlinkChannelParams, setUnlinkChannelParams] = useState(null); + + // Show unlink and retry dialog component + const [showUnlinkDialog, setShowUnlinkDialog] = useState(false); + const [showRetryDialog, setShowRetryDialog] = useState(false); + + const {makeApiRequestWithCompletionStatus, getApiState} = usePluginApi(); + const dispatch = useDispatch(); + const showAlert = useAlert(); + + const {isLoading: isUnlinkChannelsLoading} = getApiState(pluginApiServiceConfigs.unlinkChannel.apiServiceName, unlinkChannelParams as UnlinkChannelParams); + + const unlinkChannel = () => { + if (unlinkChannelParams?.channelId) { + makeApiRequestWithCompletionStatus(pluginApiServiceConfigs.unlinkChannel.apiServiceName, {channelId: unlinkChannelParams.channelId}); + } + }; + + useApiRequestCompletionState({ + serviceName: pluginApiServiceConfigs.unlinkChannel.apiServiceName, + payload: unlinkChannelParams as UnlinkChannelParams, + handleSuccess: () => { + setShowUnlinkDialog(false); + dispatch(refetch()); + showAlert({message: 'Successfully unlinked channels.', severity: 'success'}); + }, + handleError: () => { + setShowUnlinkDialog(false); + setShowRetryDialog(true); + }, + }); + + return ( +
+
+
-
- - -
{msTeamsChannelName}
-
- -
{msTeamsTeamName}
-
+
+
+ {mattermostChannelType === MMConstants.PRIVATE_CHANNEL ? : } + +
{mattermostChannelName}
+
+ +
{mattermostTeamName}
+
+
+
+ + +
{msTeamsChannelName}
+
+ +
{msTeamsTeamName}
+
+
+ + setShowUnlinkDialog(false)} + isLoading={isUnlinkChannelsLoading} + > + <>{'Are you sure you want to unlink the '}{mattermostChannelName}{' and '} {msTeamsChannelName}{' channels? Messages will no longer be synced.'} + + setShowRetryDialog(false)} + isLoading={isUnlinkChannelsLoading} + > + {'We were not able to unlink the selected channels. Please try again.'} +
-
-); + ); +}; diff --git a/webapp/src/components/LinkedChannelCard/LinkedChannelCard.styles.scss b/webapp/src/components/LinkedChannelCard/LinkedChannelCard.styles.scss index 643af210c..ed36b6e2b 100644 --- a/webapp/src/components/LinkedChannelCard/LinkedChannelCard.styles.scss +++ b/webapp/src/components/LinkedChannelCard/LinkedChannelCard.styles.scss @@ -2,6 +2,30 @@ .msteams-linked-channel { + &__unlink-icon { + width: 15%; + display: none; + + svg { + path,rect{ + color: var(--center-channel-color-56); + } + } + } + + &:hover { + background-color: var(--center-channel-color-08); + + .msteams-linked-channel__unlink-icon { + display: flex; + cursor: pointer; + + .mm-icon { + margin: 0 !important; + } + } + } + &__link-icon { &::before { @@ -28,14 +52,14 @@ } &__body { - width: 95%; + width: 80%; } &__body-values { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 45%; + max-width: 35%; line-height: inherit; } } diff --git a/webapp/src/components/LinkedChannelCard/LinkedChannelCard.types.ts b/webapp/src/components/LinkedChannelCard/LinkedChannelCard.types.ts index 7366df2a7..353bf168c 100644 --- a/webapp/src/components/LinkedChannelCard/LinkedChannelCard.types.ts +++ b/webapp/src/components/LinkedChannelCard/LinkedChannelCard.types.ts @@ -1,3 +1,3 @@ -export type LinkedChannelCardProps = Pick & { +export type LinkedChannelCardProps = Pick & { channelId: string } diff --git a/webapp/src/components/index.ts b/webapp/src/components/index.ts index 50c4ca6cb..0b80d0ad8 100644 --- a/webapp/src/components/index.ts +++ b/webapp/src/components/index.ts @@ -3,4 +3,5 @@ export * from './Icon'; export * from './WarningCard'; export * from './LinkedChannelCard'; export * from './Snackbar'; +export * from './LinkChannelModal'; export * from './Dialog'; diff --git a/webapp/src/constants/apiService.constant.ts b/webapp/src/constants/apiService.constant.ts index e341b450f..8bde1b16c 100644 --- a/webapp/src/constants/apiService.constant.ts +++ b/webapp/src/constants/apiService.constant.ts @@ -25,4 +25,24 @@ export const pluginApiServiceConfigs: Record = { success: 'success', @@ -18,3 +16,5 @@ export const alertSeverity: Record = { } as const; export const alertTimeout = 4000; + +export const debounceFunctionTimeLimitInMilliseconds = 300; diff --git a/webapp/src/constants/index.ts b/webapp/src/constants/index.ts index b2462dbe0..371c1889c 100644 --- a/webapp/src/constants/index.ts +++ b/webapp/src/constants/index.ts @@ -1,7 +1,7 @@ import {pluginApiServiceConfigs} from './apiService.constant'; import {channelListTitle, noMoreChannelsText} from './linkedChannels.constants'; import {iconUrl} from './illustrations.constants'; -import {pluginTitle, siteUrl, alertTimeout} from './common.constants'; +import {pluginTitle, siteUrl, alertTimeout, debounceFunctionTimeLimitInMilliseconds} from './common.constants'; export default { iconUrl, @@ -11,4 +11,5 @@ export default { channelListTitle, alertTimeout, noMoreChannelsText, + debounceFunctionTimeLimitInMilliseconds, }; diff --git a/webapp/src/containers/Rhs/Rhs.container.tsx b/webapp/src/containers/Rhs/Rhs.container.tsx index 15c5b7fb4..a0b71d70d 100644 --- a/webapp/src/containers/Rhs/Rhs.container.tsx +++ b/webapp/src/containers/Rhs/Rhs.container.tsx @@ -4,24 +4,26 @@ import {useDispatch} from 'react-redux'; import {Button, Input, Spinner} from '@brightscout/mattermost-ui-library'; -import {Icon, IconName, LinkedChannelCard, Snackbar, WarningCard} from 'components'; +import {Dialog, Icon, IconName, LinkChannelModal, LinkedChannelCard, Snackbar, WarningCard} from 'components'; import {pluginApiServiceConfigs} from 'constants/apiService.constant'; -import {debounceFunctionTimeLimit, defaultPage, defaultPerPage} from 'constants/common.constants'; +import {debounceFunctionTimeLimitInMilliseconds, defaultPage, defaultPerPage} from 'constants/common.constants'; import Constants from 'constants/connectAccount.constants'; import {channelListTitle, noMoreChannelsText} from 'constants/linkedChannels.constants'; import useApiRequestCompletionState from 'hooks/useApiRequestCompletionState'; import useAlert from 'hooks/useAlert'; -import useDialog from 'hooks/useDialog'; import usePluginApi from 'hooks/usePluginApi'; import usePreviousState from 'hooks/usePreviousState'; -import {getConnectedState, getIsRhsLoading, getSnackbarState} from 'selectors'; +import {getConnectedState, getIsRhsLoading, getLinkModalState, getRefetchState, getSnackbarState} from 'selectors'; import {setConnected} from 'reducers/connectedState'; +import {showLinkModal} from 'reducers/linkModal'; +import {resetRefetch} from 'reducers/refetchState'; import utils from 'utils'; import './Rhs.styles.scss'; export const Rhs = () => { const {makeApiRequestWithCompletionStatus, getApiState, state} = usePluginApi(); + const {Avatar} = window.Components; // state variables const [totalLinkedChannels, setTotalLinkedChannels] = useState([]); @@ -29,7 +31,7 @@ export const Rhs = () => { page: defaultPage, per_page: defaultPerPage, }); - const [getLinkedChannelsParams, setGetLinkedChannelsParams] = useState({...paginationQueryParams}); + const [getLinkedChannelsParams, setGetLinkedChannelsParams] = useState({...paginationQueryParams}); const {connected, msteamsUserId, username, isAlreadyConnected} = getConnectedState(state); const [searchLinkedChannelsText, setSearchLinkedChannelsText] = useState(''); const [firstRender, setFirstRender] = useState(true); @@ -67,18 +69,16 @@ export const Rhs = () => { ), [totalLinkedChannels]); // Show disconnect dialog component - const {DialogComponent, showDialog, hideDialog} = useDialog({ - onSubmitHandler: disconnectUser, - onCloseHandler: () => { - hideDialog(); - }, - }); + const [showDisconnectDialog, setShowDisconnectDialog] = useState(false); const {isRhsLoading} = getIsRhsLoading(state); const {isOpen} = getSnackbarState(state); + const {refetch} = getRefetchState(state); const {data} = getApiState(pluginApiServiceConfigs.whitelistUser.apiServiceName); + const {presentInWhitelist} = data as WhitelistUserResponse; - const {data: linkedChannels, isLoading} = getApiState(pluginApiServiceConfigs.getLinkedChannels.apiServiceName, getLinkedChannelsParams as SearchLinkedChannelParams); + const {data: linkedChannels, isLoading} = getApiState(pluginApiServiceConfigs.getLinkedChannels.apiServiceName, getLinkedChannelsParams as SearchParams); + const {isLoading: isUserDisconnecting} = getApiState(pluginApiServiceConfigs.disconnectUser.apiServiceName); // Handle searching of linked channels with debounce useEffect(() => { @@ -88,7 +88,7 @@ export const Rhs = () => { const timer = setTimeout(() => { resetStates(); - }, debounceFunctionTimeLimit); + }, debounceFunctionTimeLimitInMilliseconds); /* eslint-disable consistent-return */ return () => { @@ -98,7 +98,7 @@ export const Rhs = () => { // Make api call to get linked channels useEffect(() => { - const linkedChannelsParams: SearchLinkedChannelParams = {page: paginationQueryParams.page, per_page: paginationQueryParams.per_page}; + const linkedChannelsParams: SearchParams = {page: paginationQueryParams.page, per_page: paginationQueryParams.per_page}; if (searchLinkedChannelsText) { linkedChannelsParams.search = searchLinkedChannelsText; } @@ -118,7 +118,7 @@ export const Rhs = () => { // Update total linked channels after completion of the api to get linked channels useApiRequestCompletionState({ serviceName: pluginApiServiceConfigs.getLinkedChannels.apiServiceName, - payload: getLinkedChannelsParams as SearchLinkedChannelParams, + payload: getLinkedChannelsParams as SearchParams, handleSuccess: () => { if (linkedChannels) { setTotalLinkedChannels([...totalLinkedChannels, ...(linkedChannels as ChannelLinkData[])]); @@ -134,7 +134,7 @@ export const Rhs = () => { serviceName: pluginApiServiceConfigs.disconnectUser.apiServiceName, handleSuccess: () => { dispatch(setConnected({connected: false, username: '', msteamsUserId: '', isAlreadyConnected: false})); - hideDialog(); + setShowDisconnectDialog(false); showAlert({ message: 'Your account has been disconnected.', severity: 'default', @@ -145,10 +145,17 @@ export const Rhs = () => { message: 'Error occurred while disconnecting the user.', severity: 'error', }); - hideDialog(); + setShowDisconnectDialog(false); }, }); + useEffect(() => { + if (refetch) { + resetStates(); + dispatch(resetRefetch()); + } + }, [refetch]); + // Get different states of rhs const getRhsView = useCallback(() => { // Show spinner in the rhs during loading @@ -203,40 +210,16 @@ export const Rhs = () => {
{connected ? (
- {/* TODO: Refactor user Avatar */} -
- -
- +
{'Connected as '}{username}
-
) : (
@@ -255,16 +238,27 @@ export const Rhs = () => { {/* State when user is connected, but no linked channels are present. */} {!totalLinkedChannels.length && !isLoading && !searchLinkedChannelsText && !previousState?.searchLinkedChannelsText && (
- {<> - -

{'There are no linked channels yet'}

- } + +

{'There are no linked channels yet'}

+
)} {/* State when user is conected and linked channels are present. */} {((Boolean(totalLinkedChannels.length) || isLoading || searchLinkedChannelsText || previousState?.searchLinkedChannelsText) && !firstRender) && ( <> -

{channelListTitle}

+
+

{channelListTitle}

+ {connected && ( + + )} +
{ )} )} + setShowDisconnectDialog(false)} + > + {'Are you sure you want to disconnect your Microsoft Teams Account? You will no longer be able to send and receive messages to Microsoft Teams users from Mattermost.'} +
); - }, [connected, isRhsLoading, isLoading, totalLinkedChannels, firstRender, searchLinkedChannelsText]); + }, [connected, isRhsLoading, isLoading, totalLinkedChannels, firstRender, searchLinkedChannelsText, showDisconnectDialog]); return ( <> @@ -322,6 +328,7 @@ export const Rhs = () => { getRhsView() : 'MS Teams Sync plugin' } {isOpen && } + {} ); }; diff --git a/webapp/src/containers/Rhs/Rhs.styles.scss b/webapp/src/containers/Rhs/Rhs.styles.scss index f88f717eb..2034a7364 100644 --- a/webapp/src/containers/Rhs/Rhs.styles.scss +++ b/webapp/src/containers/Rhs/Rhs.styles.scss @@ -15,3 +15,13 @@ } } } + +.msteams-sync-modal { + .mm-autocomplete { + .mm-menuItem { + &__secondary-label { + text-align: end; + } + } + } +} diff --git a/webapp/src/hooks/useDialog.tsx b/webapp/src/hooks/useDialog.tsx deleted file mode 100644 index 03a37f259..000000000 --- a/webapp/src/hooks/useDialog.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, {useCallback} from 'react'; -import {useDispatch} from 'react-redux'; - -import {DialogProps} from '@brightscout/mattermost-ui-library'; - -import {showDialog as showDialogComponent, closeDialog} from 'reducers/dialog'; - -import {Dialog} from 'components'; -import {DialogState} from 'types/common/store.d'; - -const useDialog = ({onCloseHandler, onSubmitHandler}: Pick) => { - const dispatch = useDispatch(); - - const showDialog = (props: DialogState) => dispatch(showDialogComponent(props)); - - const hideDialog = () => dispatch(closeDialog()); - - const DialogComponent = useCallback(() => ( - - ), []); - - return { - DialogComponent, - showDialog, - hideDialog, - }; -}; - -export default useDialog; diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index 229315380..997979c0d 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -9,7 +9,7 @@ import EnforceConnectedAccountModal from 'components/enforceConnectedAccountModa import MSTeamsAppManifestSetting from 'components/appManifestSetting'; import ListConnectedUsers from 'components/getConnectedUsersSetting'; -import {RhsTitle} from 'components'; +import {LinkChannelModal, RhsTitle} from 'components'; import {Rhs} from 'containers'; @@ -17,7 +17,7 @@ import {pluginTitle} from 'constants/common.constants'; import {iconUrl} from 'constants/illustrations.constants'; -import {handleConnect, handleDisconnect} from 'websocket'; +import {handleConnect, handleDisconnect, handleLink, handleModalLink, handleUnlinkChannels} from 'websocket'; import manifest from './manifest'; @@ -31,6 +31,7 @@ export default class Plugin { public async initialize(registry: PluginRegistry, store: Store>>) { registry.registerReducer(reducer); registry.registerRootComponent(App); + registry.registerRootComponent(LinkChannelModal); // @see https://developers.mattermost.com/extend/plugins/webapp/reference/ this.enforceConnectedAccountId = registry.registerRootComponent(EnforceConnectedAccountModal); @@ -53,12 +54,16 @@ export default class Plugin { registry.registerWebSocketEventHandler(`custom_${manifest.id}_connect`, handleConnect(store)); registry.registerWebSocketEventHandler(`custom_${manifest.id}_disconnect`, handleDisconnect(store)); + registry.registerWebSocketEventHandler(`custom_${manifest.id}_unlink`, handleUnlinkChannels(store)); + registry.registerWebSocketEventHandler(`custom_${manifest.id}_link_channels`, handleModalLink(store)); + registry.registerWebSocketEventHandler(`custom_${manifest.id}_link`, handleLink(store)); } } declare global { interface Window { - registerPlugin(id: string, plugin: Plugin): void, + registerPlugin(id: string, plugin: Plugin): void; + Components: any; } } diff --git a/webapp/src/reducers/dialog/index.ts b/webapp/src/reducers/dialog/index.ts deleted file mode 100644 index a38ea4c88..000000000 --- a/webapp/src/reducers/dialog/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {PayloadAction, createSlice} from '@reduxjs/toolkit'; - -import {DialogState} from 'types/common/store.d'; - -const initialState: DialogState = { - description: '', - destructive: false, - show: false, - primaryButtonText: '', - secondaryButtonText: '', - isLoading: false, - title: '', -}; - -export const dialogSlice = createSlice({ - name: 'dialogSlice', - initialState, - reducers: { - showDialog: (state, {payload}: PayloadAction) => { - state.show = true; - state.description = payload.description; - state.destructive = payload.destructive; - state.isLoading = payload.isLoading; - state.primaryButtonText = payload.primaryButtonText; - state.secondaryButtonText = payload.secondaryButtonText; - state.title = payload.title; - }, - closeDialog: (state) => { - state.show = false; - }, - - }, -}); - -export const {showDialog, closeDialog} = dialogSlice.actions; - -export default dialogSlice.reducer; diff --git a/webapp/src/reducers/index.ts b/webapp/src/reducers/index.ts index d429bfb18..6597c8b1a 100644 --- a/webapp/src/reducers/index.ts +++ b/webapp/src/reducers/index.ts @@ -5,14 +5,16 @@ import {msTeamsPluginApi} from 'services'; import apiRequestCompletionSlice from 'reducers/apiRequest'; import connectedStateSlice from 'reducers/connectedState'; import snackbarSlice from 'reducers/snackbar'; -import dialogSlice from 'reducers/dialog'; import rhsLoadingSlice from 'reducers/spinner'; +import linkModalSlice from 'reducers/linkModal'; +import refetchSlice from 'reducers/refetchState'; export default combineReducers({ apiRequestCompletionSlice, connectedStateSlice, snackbarSlice, - dialogSlice, rhsLoadingSlice, + linkModalSlice, + refetchSlice, [msTeamsPluginApi.reducerPath]: msTeamsPluginApi.reducer, }); diff --git a/webapp/src/reducers/linkModal/index.ts b/webapp/src/reducers/linkModal/index.ts new file mode 100644 index 000000000..009ff50d6 --- /dev/null +++ b/webapp/src/reducers/linkModal/index.ts @@ -0,0 +1,43 @@ +import {PayloadAction, createSlice} from '@reduxjs/toolkit'; + +import {ModalState} from 'types/common/store.d'; + +const initialState: ModalState = { + show: false, + isLoading: false, + mmChannel: '', + msChannel: '', + msTeam: '', +}; + +export const linkModalSlice = createSlice({ + name: 'linkModalSlice', + initialState, + reducers: { + showLinkModal: (state) => { + state.show = true; + }, + hideLinkModal: (state) => { + state.show = false; + }, + setLinkModalLoading: (state, {payload}: PayloadAction) => { + state.isLoading = payload; + }, + preserveState: (state, {payload}: PayloadAction>) => { + state.mmChannel = payload.mmChannel; + state.msChannel = payload.msChannel; + state.msTeam = payload.msTeam; + }, + resetState: (state) => { + state.show = false; + state.isLoading = false; + state.mmChannel = ''; + state.msChannel = ''; + state.msTeam = ''; + }, + }, +}); + +export const {showLinkModal, hideLinkModal, setLinkModalLoading, preserveState, resetState} = linkModalSlice.actions; + +export default linkModalSlice.reducer; diff --git a/webapp/src/reducers/refetchState/index.ts b/webapp/src/reducers/refetchState/index.ts new file mode 100644 index 000000000..50cb6b74d --- /dev/null +++ b/webapp/src/reducers/refetchState/index.ts @@ -0,0 +1,24 @@ +import {createSlice} from '@reduxjs/toolkit'; + +import {RefetchState} from 'types/common/store.d'; + +const initialState: RefetchState = { + refetch: false, +}; + +export const refetchSlice = createSlice({ + name: 'refetch', + initialState, + reducers: { + refetch: (state) => { + state.refetch = true; + }, + resetRefetch: (state) => { + state.refetch = false; + }, + }, +}); + +export const {refetch, resetRefetch} = refetchSlice.actions; + +export default refetchSlice.reducer; diff --git a/webapp/src/selectors/index.tsx b/webapp/src/selectors/index.tsx index 95d07be12..db140084b 100644 --- a/webapp/src/selectors/index.tsx +++ b/webapp/src/selectors/index.tsx @@ -1,4 +1,6 @@ -import {ApiRequestCompletionState, ConnectedState, DialogState, ReduxState, SnackbarState} from 'types/common/store.d'; +import {TeamsState} from 'mattermost-redux/types/teams'; + +import {ApiRequestCompletionState, ConnectedState, ModalState, ReduxState, RefetchState, SnackbarState} from 'types/common/store.d'; const getPluginState = (state: ReduxState) => state['plugins-com.mattermost.msteams-sync']; @@ -8,6 +10,10 @@ export const getConnectedState = (state: ReduxState): ConnectedState => getPlugi export const getSnackbarState = (state: ReduxState): SnackbarState => getPluginState(state).snackbarSlice; -export const getDialogState = (state: ReduxState): DialogState => getPluginState(state).dialogSlice; - export const getIsRhsLoading = (state: ReduxState): {isRhsLoading: boolean} => getPluginState(state).rhsLoadingSlice; + +export const getCurrentTeam = (state: ReduxState): TeamsState => state.entities.teams; + +export const getLinkModalState = (state: ReduxState): ModalState => getPluginState(state).linkModalSlice; + +export const getRefetchState = (state: ReduxState): RefetchState => getPluginState(state).refetchSlice; diff --git a/webapp/src/services/index.ts b/webapp/src/services/index.ts index 47e2a0c9d..b3e361163 100644 --- a/webapp/src/services/index.ts +++ b/webapp/src/services/index.ts @@ -50,5 +50,34 @@ export const msTeamsPluginApi = createApi({ responseHandler: (res) => res.text(), }), }), + [pluginApiServiceConfigs.searchMSTeams.apiServiceName]: builder.query({ + query: (params) => ({ + url: pluginApiServiceConfigs.searchMSTeams.path, + method: pluginApiServiceConfigs.searchMSTeams.method, + params: {...params}, + }), + }), + [pluginApiServiceConfigs.searchMSChannels.apiServiceName]: builder.query({ + query: ({teamId, ...params}) => ({ + url: pluginApiServiceConfigs.searchMSChannels.path.replace('{team_id}', teamId), + method: pluginApiServiceConfigs.searchMSChannels.method, + params: {...params}, + }), + }), + [pluginApiServiceConfigs.linkChannels.apiServiceName]: builder.query({ + query: (payload) => ({ + url: pluginApiServiceConfigs.linkChannels.path, + method: pluginApiServiceConfigs.linkChannels.method, + body: payload, + responseHandler: (res) => res.text(), + }), + }), + [pluginApiServiceConfigs.unlinkChannel.apiServiceName]: builder.query({ + query: ({channelId}) => ({ + url: pluginApiServiceConfigs.unlinkChannel.path.replace('{channel_id}', channelId), + method: pluginApiServiceConfigs.unlinkChannel.method, + responseHandler: (res) => res.text(), + }), + }), }), }); diff --git a/webapp/src/styles/_utils.scss b/webapp/src/styles/_utils.scss index d2a6a2adc..d69ef8ef8 100644 --- a/webapp/src/styles/_utils.scss +++ b/webapp/src/styles/_utils.scss @@ -45,6 +45,10 @@ .m-#{$size} { margin: #{$size}px !important; } + + .mt-#{$size} { + margin-top: #{$size}px !important; + } } // margin block diff --git a/webapp/src/styles/_variables.scss b/webapp/src/styles/_variables.scss index 217e0c4f2..42d53b2cb 100644 --- a/webapp/src/styles/_variables.scss +++ b/webapp/src/styles/_variables.scss @@ -2,7 +2,7 @@ $display-styles: flex, none, block; // margins, paddings and gaps -$sizes: 0, 4, 6, 8, 10, 12, 16, 18, 24, 20, 30, 32, 40, auto; +$sizes: 0, 4, 6, 8, 10, 12, 16, 18, 24, 20, 30, 32, 40, 62, 100, auto; // font weights $font-weights: 300, 400, 500, 600, 700, 800; diff --git a/webapp/src/types/common/index.d.ts b/webapp/src/types/common/index.d.ts index 6d62d3bb7..ce72308cb 100644 --- a/webapp/src/types/common/index.d.ts +++ b/webapp/src/types/common/index.d.ts @@ -31,3 +31,15 @@ type ChannelLinkData = { mattermostChannelName: string, mattermostChannelType: string, } + +type MMTeamOrChannel = { + id: string, + displayName: string, +} + +type MSTeamOrChannel = { + ID: string, + DisplayName: string, +} + +type MSTeamsSearchResponse = MSTeamOrChannel[]; diff --git a/webapp/src/types/common/payload.d.ts b/webapp/src/types/common/payload.d.ts index 9610f1471..89e0d6457 100644 --- a/webapp/src/types/common/payload.d.ts +++ b/webapp/src/types/common/payload.d.ts @@ -7,6 +7,17 @@ type UnlinkChannelParams = { channelId: string; } -type SearchLinkedChannelParams = PaginationQueryParams & { +type SearchParams = PaginationQueryParams & { search?: string; } + +type SearchMSChannelsParams = SearchParams & { + teamId: string; +} + +type LinkChannelsPayload = { + mattermostTeamID: string, + mattermostChannelID: string, + msTeamsTeamID: string, + msTeamsChannelID: string, +} diff --git a/webapp/src/types/common/service.d.ts b/webapp/src/types/common/service.d.ts index 50313008f..b96016763 100644 --- a/webapp/src/types/common/service.d.ts +++ b/webapp/src/types/common/service.d.ts @@ -1,6 +1,6 @@ type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'; -type PluginApiServiceName = 'needsConnect' | 'connect' | 'whitelistUser' | 'getLinkedChannels' | 'disconnectUser' ; +type PluginApiServiceName = 'needsConnect' | 'connect' | 'whitelistUser' | 'getLinkedChannels' | 'disconnectUser' | 'searchMSTeams' | 'searchMSChannels' | 'linkChannels' | 'unlinkChannel'; type PluginApiService = { path: string, @@ -13,4 +13,4 @@ type APIError = { message: string, } -type APIRequestPayload = SearchLinkedChannelParams | void; +type APIRequestPayload = LinkChannelsPayload | SearchParams | UnlinkChannelParams | void; diff --git a/webapp/src/types/common/store.d.ts b/webapp/src/types/common/store.d.ts index 624e752a2..fc07350de 100644 --- a/webapp/src/types/common/store.d.ts +++ b/webapp/src/types/common/store.d.ts @@ -2,7 +2,8 @@ import {BaseQueryFn, FetchArgs, FetchBaseQueryError, FetchBaseQueryMeta} from '@ import {GlobalState} from 'mattermost-redux/types/store'; -import {DialogProps} from '@brightscout/mattermost-ui-library'; +import {DialogProps, ListItemType} from '@brightscout/mattermost-ui-library'; +import {ModalProps} from '@brightscout/mattermost-ui-library/build/components/Modal'; import {SnackbarColor} from 'components/Snackbar/Snackbar.types'; import {IconName} from 'components'; @@ -36,6 +37,14 @@ type SnackbarState = { type SnackbarActionPayload = Pick; -type DialogState = Pick & { - isLoading?: boolean +type ModalState = { + show?: boolean; + isLoading?: boolean; + mmChannel: string; + msTeam: string; + msChannel: string; } + +type RefetchState = { + refetch: boolean; +}; diff --git a/webapp/src/utils/index.ts b/webapp/src/utils/index.ts index 43b6cc53c..f9fc65a4e 100644 --- a/webapp/src/utils/index.ts +++ b/webapp/src/utils/index.ts @@ -16,4 +16,29 @@ const getAvatarUrl = (userId: string): string => { return `${getBaseUrls().pluginApiBaseUrl}/avatar/${userId}`; }; -export default {getBaseUrls, getIconUrl, getAvatarUrl}; +/** + * Uses closure functionality to implement debouncing + * @param {function} func Function on which debouncing is to be applied + * @param {number} limit The time limit for debouncing, the minimum pause in function calls required for the function to be actually called + * @returns {(args: Array) => void} a function with debouncing functionality applied on it + */ +const debounce: (func: (args: Record, type?: string) => void, limit: number) => (args: Record) => void = ( + func: (args: Record) => void, + limit: number, +): (args: Record) => void => { + let timer: NodeJS.Timeout; + + /** + * This is to use the functionality of closures so that timer isn't reinitialized once initialized + * @param {Array} args + * @returns {void} + */ + + // eslint-disable-next-line func-names + return function(args: Record): void { + clearTimeout(timer); + timer = setTimeout(() => func({...args}), limit); + }; +}; + +export default {getBaseUrls, getIconUrl, getAvatarUrl, debounce}; diff --git a/webapp/src/websocket/index.ts b/webapp/src/websocket/index.ts index d01d61d80..09d247f4d 100644 --- a/webapp/src/websocket/index.ts +++ b/webapp/src/websocket/index.ts @@ -3,6 +3,8 @@ import {Action, Store} from 'redux'; import {GlobalState} from 'mattermost-redux/types/store'; import {setConnected} from 'reducers/connectedState'; +import {showLinkModal} from 'reducers/linkModal'; +import {refetch} from 'reducers/refetchState'; export function handleConnect(store: Store>>) { return (msg: WebsocketEventParams) => { @@ -17,3 +19,21 @@ export function handleDisconnect(store: Store>>) { + return (_: WebsocketEventParams) => { + store.dispatch(refetch() as Action); + }; +} + +export function handleModalLink(store: Store>>) { + return (_: WebsocketEventParams) => { + store.dispatch(showLinkModal() as Action); + }; +} + +export function handleLink(store: Store>>) { + return (_: WebsocketEventParams) => { + store.dispatch(refetch() as Action); + }; +} diff --git a/webapp/yarn.lock b/webapp/yarn.lock index a3514c0e8..72cb6c96e 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -1054,10 +1054,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@brightscout/mattermost-ui-library@2.3.3": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@brightscout/mattermost-ui-library/-/mattermost-ui-library-2.3.3.tgz#894f8bc40b971529f691c93b52e7c207bf223234" - integrity sha512-elhZED6jH5XxeNgXpgfcCbIyHTKBNBpbFp0tUnB+p1c4kLfQq49uaGaK8JJ9SXdYzHPd9yi0K9HiB6ziMlwGFg== +"@brightscout/mattermost-ui-library@2.3.4": + version "2.3.4" + resolved "https://registry.yarnpkg.com/@brightscout/mattermost-ui-library/-/mattermost-ui-library-2.3.4.tgz#7895f6f96542de50b95bb1bd19c5db3bd38da436" + integrity sha512-G76Jy1L7+dz5lXq7iO54Bo4wf3wkqVQsFg9U+SiK5c97gnB/0306MncnjJCDD2BwpSOMzItsyQoOZ6y/oDjlBA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1"