From 2bc4dc3f813799a8a3d7cc914f8e4c4a3f657c6e Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Mon, 28 Oct 2024 12:57:36 +0100 Subject: [PATCH] Add linking team functionality --- docker-compose.yml | 4 +- server/plugin.go | 78 +++++++++--- webapp/src/api.ts | 21 ++++ .../src/components/link_team_modal/index.tsx | 21 ++++ .../link_team_modal/link_team_modal.tsx | 119 ++++++++++++++++++ webapp/src/components/sidepanel/index.tsx | 48 +++++-- webapp/src/index.tsx | 10 +- webapp/src/reducers.ts | 9 ++ webapp/src/selectors.ts | 2 + 9 files changed, 286 insertions(+), 26 deletions(-) create mode 100644 webapp/src/components/link_team_modal/index.tsx create mode 100644 webapp/src/components/link_team_modal/link_team_modal.tsx diff --git a/docker-compose.yml b/docker-compose.yml index 3521b8a..08ceb47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,8 +33,9 @@ services: ports: - 8065:8065 - 8443:8443 + volumes: + - plugin-data:/var/mattermost/data/plugins # Having the socket on a mounted volume does not work on arm MacOS - #volumes: #- ./tmp:/var/mattermost/tmp:rw environment: - TZ=UTC @@ -49,3 +50,4 @@ services: - "host.docker.internal:host-gateway" volumes: postgres-data: {} + plugin-data: {} diff --git a/server/plugin.go b/server/plugin.go index 4c43648..93d6772 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -77,13 +77,9 @@ func (p *Plugin) authenticated(handler HTTPHandlerFuncWithContext) http.HandlerF } func (p *Plugin) notify(w http.ResponseWriter, r *http.Request) { + teamId := r.PathValue("teamId") userId, err := p.API.KVGet(botUserID) - channel, err2 := p.API.KVGet("notifications") - if err != nil || err2 != nil { - fmt.Println("Error getting bot user id or notifications channel", err, err2) - return - } - fmt.Println("User ID", userId, channel) + fmt.Println("GEORG teamId", teamId) var props map[string]interface{} err3 := getJson(r.Body, &props) @@ -91,13 +87,20 @@ func (p *Plugin) notify(w http.ResponseWriter, r *http.Request) { fmt.Println("GEORG err3", err3) return } - fmt.Println("GEORG Props", props) - _, err = p.API.CreatePost(&model.Post{ - ChannelId: "f3hzc15q63f75meazr8h4ok5ca", //string(channel), - Props: props, - UserId: string(userId), - }) - fmt.Println("GEORG post err", err) + channels, err2 := p.getChannels(teamId) + fmt.Println("GEORG channels", channels) + if err2 != nil { + fmt.Println("GEORG err2", err2) + return + } + for _, channel := range channels { + _, err = p.API.CreatePost(&model.Post{ + ChannelId: channel, + Props: props, + UserId: string(userId), + }) + fmt.Println("GEORG post err", err) + } } func (p *Plugin) query(c *Context, w http.ResponseWriter, r *http.Request) { @@ -155,9 +158,56 @@ func (p *Plugin) query(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(responseBody) } +func (p *Plugin) linkedTeams(c *Context, w http.ResponseWriter, r *http.Request) { + channelId := r.PathValue("channelId") + teams, err := p.getTeams(channelId) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error": "Could not read linked teams"}`)) + return + } + + body, err := json.Marshal(teams) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error": "Marshal error"}`)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(body) +} + +func (p *Plugin) linkTeam(c *Context, w http.ResponseWriter, r *http.Request) { + channelId := r.PathValue("channelId") + teamId := r.PathValue("teamId") + err := p.linkTeamToChannel(channelId, teamId) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + msg := fmt.Sprintf(`{"error": "Error linking team to channel", "originalError": "%v"}`, err) + w.Write([]byte(msg)) + return + } +} + +func (p *Plugin) unlinkTeam(c *Context, w http.ResponseWriter, r *http.Request) { + channelId := r.PathValue("channelId") + teamId := r.PathValue("teamId") + err := p.linkTeamToChannel(channelId, teamId) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error": "Error unlinking team from channel"}`)) + return + } +} + func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { mux := http.NewServeMux() - mux.HandleFunc("POST /notify", p.notify) + mux.HandleFunc("POST /notify/{teamId}", p.notify) mux.HandleFunc("POST /query/{query}", p.authenticated(p.query)) + mux.HandleFunc("/linkedTeams/{channelId}", p.authenticated(p.linkedTeams)) + mux.HandleFunc("POST /linkTeam/{channelId}/{teamId}", p.authenticated(p.linkTeam)) + mux.HandleFunc("POST /unlinkTeam/{channelId}/{teamId}", p.authenticated(p.unlinkTeam)) mux.ServeHTTP(w, r) } diff --git a/webapp/src/api.ts b/webapp/src/api.ts index 47b5ee3..0760ed1 100644 --- a/webapp/src/api.ts +++ b/webapp/src/api.ts @@ -180,6 +180,24 @@ export const api = createApi({ body: variables, }), }), + linkedTeams: builder.query({ + query: ({channelId}) => ({ + url: `/linkedTeams/${channelId}`, + method: 'GET', + }), + }), + linkTeam: builder.mutation({ + query: ({channelId, teamId}) => ({ + url: `/linkTeam/${channelId}/${teamId}`, + method: 'POST', + }), + }), + unlinkTeam: builder.mutation({ + query: ({channelId, teamId}) => ({ + url: `/unlinkTeam/${channelId}/${teamId}`, + method: 'POST', + }), + }), }), }) @@ -197,4 +215,7 @@ export const { useStartSprintPokerMutation, useGetActiveMeetingsQuery, useCreateReflectionMutation, + useLinkedTeamsQuery, + useLinkTeamMutation, + useUnlinkTeamMutation, } = api diff --git a/webapp/src/components/link_team_modal/index.tsx b/webapp/src/components/link_team_modal/index.tsx new file mode 100644 index 0000000..e093b5a --- /dev/null +++ b/webapp/src/components/link_team_modal/index.tsx @@ -0,0 +1,21 @@ +import React, {lazy, Suspense} from 'react' +import {useSelector} from 'react-redux' + +import {isLinkTeamModalVisible} from '../../selectors' + +const LinkTeamModal = lazy(() => import(/* webpackChunkName: 'LinkTeamModal' */ './link_team_modal')) + +const LinkTeamModalRoot = () => { + const isVisible = useSelector(isLinkTeamModalVisible) + if (!isVisible) { + return null + } + + return ( + + + + ) +} + +export default LinkTeamModalRoot diff --git a/webapp/src/components/link_team_modal/link_team_modal.tsx b/webapp/src/components/link_team_modal/link_team_modal.tsx new file mode 100644 index 0000000..ea54a2d --- /dev/null +++ b/webapp/src/components/link_team_modal/link_team_modal.tsx @@ -0,0 +1,119 @@ +import React, {useEffect, useMemo} from 'react' +import {Modal} from 'react-bootstrap' +import {useDispatch, useSelector} from 'react-redux' + +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common' + +import {isError, useGetTemplatesQuery, useLinkedTeamsQuery, useLinkTeamMutation} from '../../api' +import {closeLinkTeamModal} from '../../reducers' +import {getAssetsUrl, isLinkTeamModalVisible} from '../../selectors' + +const LinkTeamModal = () => { + const isVisible = useSelector(isLinkTeamModalVisible) + const {data: teamData, refetch} = useGetTemplatesQuery() + useEffect(() => { + if (isVisible) { + refetch() + } + }, [isVisible, refetch]) + + const [linkTeam] = useLinkTeamMutation() + const channelId = useSelector(getCurrentChannelId) + const {data: linkedTeamIds} = useLinkedTeamsQuery({channelId}) + + const {teams} = teamData ?? {} + const [selectedTeam, setSelectedTeam] = React.useState[number]>() + + useEffect(() => { + if (!selectedTeam && teams && teams.length > 0) { + setSelectedTeam(teams[0]) + } + }, [teams, selectedTeam]) + const dispatch = useDispatch() + + const onChangeTeam = (teamId: string) => { + setSelectedTeam(teams?.find((team) => team.id === teamId)) + } + + const handleClose = () => { + dispatch(closeLinkTeamModal()) + } + + const handleLink = async () => { + if (!selectedTeam) { + return + } + const res = await linkTeam({channelId, teamId: selectedTeam.id}) + + if (isError(res)) { + console.error('Failed to link team', res.error) + return + } + handleClose() + } + + const assetsPath = useSelector(getAssetsUrl) + + if (!isVisible) { + return null + } + + return ( + + + + + {'Link a Parabol Team to this Channel'} + + + + {teamData && (<> +
+ +
+ +
+
+ )} +
+ + + + +
+ ) +} + +export default LinkTeamModal diff --git a/webapp/src/components/sidepanel/index.tsx b/webapp/src/components/sidepanel/index.tsx index 00d9250..52a4364 100644 --- a/webapp/src/components/sidepanel/index.tsx +++ b/webapp/src/components/sidepanel/index.tsx @@ -1,18 +1,52 @@ import React from 'react' -import {useSelector} from 'react-redux' +import {useDispatch, useSelector} from 'react-redux' import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common' -import {useGetActiveMeetingsQuery} from '../../api' +import {useGetActiveMeetingsQuery, useLinkedTeamsQuery} from '../../api' +import {openLinkTeamModal} from '../../reducers' const SidePanelRoot = () => { - const {data, isLoading} = useGetActiveMeetingsQuery() - const channel = useSelector(getCurrentChannelId) + const {data: meetings, isLoading} = useGetActiveMeetingsQuery() + const channelId = useSelector(getCurrentChannelId) + const {data: teams} = useLinkedTeamsQuery({channelId}) + const dispatch = useDispatch() + + const [selectedTab, setSelectedTab] = React.useState('linked-teams') + + const handleLink = () => { + dispatch(openLinkTeamModal()) + console.log('Link Team') + } + return (
-

SidePanelRoot

-

Channel: {channel}

- {data?.map((meeting) => ( +
+ +
+ +
+ Foo +
+ +

Linked Parabol Teams

+ + {teams} +

Channel: {channelId}

+ {meetings?.map((meeting) => (

{meeting.name}

diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index 01df1db..a8652b0 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -6,10 +6,10 @@ import {setupListeners} from '@reduxjs/toolkit/query' import {GlobalState} from 'mattermost-redux/types/store' import manifest from '@/manifest' - import {PluginRegistry} from '@/types/mattermost-webapp' import StartActivityModal from './components/start_activity' +import LinkTeamModal from './components/link_team_modal' import rootReducer, {openPushPostAsReflection, openStartActivityModal} from './reducers' import {getAssetsUrl} from './selectors' import {api} from './api' @@ -28,6 +28,7 @@ export default class Plugin { // @see https://developers.mattermost.com/extend/plugins/webapp/reference/ registry.registerRootComponent(StartActivityModal) + registry.registerRootComponent(LinkTeamModal) registry.registerWebSocketEventHandler(`custom_${manifest.id}_open_start_activity_modal`, (message) => { store.dispatch(openStartActivityModal()) }) @@ -40,15 +41,16 @@ export default class Plugin { width={24} height={24} src={`${getAssetsUrl(store.getState())}/parabol.png`} - />Parabol + /> Parabol
, ) registry.registerChannelHeaderButtonAction( , // In the future we want to toggle the side panel - //() => store.dispatch(toggleRHSPlugin), - () => store.dispatch(openStartActivityModal()), + () => store.dispatch(toggleRHSPlugin), + + //() => store.dispatch(openStartActivityModal()), 'Start a Parabol Activity', ) diff --git a/webapp/src/reducers.ts b/webapp/src/reducers.ts index 15d36b5..6791568 100644 --- a/webapp/src/reducers.ts +++ b/webapp/src/reducers.ts @@ -7,6 +7,7 @@ const localSlice = createSlice({ initialState: { isStartActivityModalVisible: false, pushPostAsReflection: null as string | null, + isLinkTeamModalVisible: false, }, reducers: { openStartActivityModal: (state) => { @@ -21,6 +22,12 @@ const localSlice = createSlice({ closePushPostAsReflection: (state) => { state.pushPostAsReflection = null }, + openLinkTeamModal: (state) => { + state.isLinkTeamModalVisible = true + }, + closeLinkTeamModal: (state) => { + state.isLinkTeamModalVisible = false + }, }, }) @@ -29,6 +36,8 @@ export const { closeStartActivityModal, openPushPostAsReflection, closePushPostAsReflection, + openLinkTeamModal, + closeLinkTeamModal, } = localSlice.actions export type PluginState = ReturnType & ReturnType diff --git a/webapp/src/selectors.ts b/webapp/src/selectors.ts index 9baf842..8180816 100644 --- a/webapp/src/selectors.ts +++ b/webapp/src/selectors.ts @@ -55,6 +55,8 @@ export const getPluginState = (state: GlobalState) => ((state as any)[`plugins-$ export const isStartActivityModalVisible = (state: GlobalState) => getPluginState(state).isStartActivityModalVisible +export const isLinkTeamModalVisible = (state: GlobalState) => getPluginState(state).isLinkTeamModalVisible + export const pushPostAsReflection = (state: GlobalState) => getPluginState(state).pushPostAsReflection //export const get