Skip to content

Commit

Permalink
Add linking team functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Dschoordsch committed Oct 28, 2024
1 parent 0fe2e38 commit 2bc4dc3
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 26 deletions.
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -49,3 +50,4 @@ services:
- "host.docker.internal:host-gateway"
volumes:
postgres-data: {}
plugin-data: {}
78 changes: 64 additions & 14 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,30 @@ 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)
if err3 != nil {
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) {
Expand Down Expand Up @@ -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)
}
21 changes: 21 additions & 0 deletions webapp/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,24 @@ export const api = createApi({
body: variables,
}),
}),
linkedTeams: builder.query<string[], {channelId: string}>({
query: ({channelId}) => ({
url: `/linkedTeams/${channelId}`,
method: 'GET',
}),
}),
linkTeam: builder.mutation<void, {channelId: string, teamId: string}>({
query: ({channelId, teamId}) => ({
url: `/linkTeam/${channelId}/${teamId}`,
method: 'POST',
}),
}),
unlinkTeam: builder.mutation<void, {channelId: string, teamId: string}>({
query: ({channelId, teamId}) => ({
url: `/unlinkTeam/${channelId}/${teamId}`,
method: 'POST',
}),
}),
}),
})

Expand All @@ -197,4 +215,7 @@ export const {
useStartSprintPokerMutation,
useGetActiveMeetingsQuery,
useCreateReflectionMutation,
useLinkedTeamsQuery,
useLinkTeamMutation,
useUnlinkTeamMutation,
} = api
21 changes: 21 additions & 0 deletions webapp/src/components/link_team_modal/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Suspense fallback={null}>
<LinkTeamModal/>
</Suspense>
)
}

export default LinkTeamModalRoot
119 changes: 119 additions & 0 deletions webapp/src/components/link_team_modal/link_team_modal.tsx
Original file line number Diff line number Diff line change
@@ -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<NonNullable<typeof teams>[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 (
<Modal
dialogClassName='modal--scroll'
show={true}
onHide={handleClose}
onExited={handleClose}
bsSize='large'
backdrop='static'
>
<Modal.Header closeButton={true}>
<Modal.Title>
<img
width={36}
height={36}
src={`${assetsPath}/parabol.png`}
/>
{'Link a Parabol Team to this Channel'}
</Modal.Title>
</Modal.Header>
<Modal.Body>
{teamData && (<>
<div className='form-group'>
<label
className='control-label'
htmlFor='team'
>Choose Parabol Team<span className='error-text'> *</span></label>
<div className='input-wrapper'>
<select
className='form-control'
id='team'
value={selectedTeam?.id}
onChange={(e) => onChangeTeam(e.target.value)}
>
{teams?.map((team) => (
<option
key={team.id}
value={team.id}
>{team.name}</option>
))}
</select>
</div>
</div>
</>)}
</Modal.Body>
<Modal.Footer>
<button
className='btn btn-tertiary cancel-button'
onClick={handleClose}
>Cancel</button>
<button
className='btn btn-primary save-button'
onClick={handleLink}
>Link Team</button>
</Modal.Footer>
</Modal>
)
}

export default LinkTeamModal
48 changes: 41 additions & 7 deletions webapp/src/components/sidepanel/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<h1>SidePanelRoot</h1>
<h2>Channel: {channel}</h2>
{data?.map((meeting) => (
<div className='form-group'>
<label
className='control-label'
htmlFor='team'
>Choose Parabol Team<span className='error-text'> *</span></label>
<div className='input-wrapper'>
<select
className='form-control'
id='team'
value={selectedTab}
onChange={(e) => setSelectedTab(e.target.value)}
>
<option
key='linked-teams'
value='linked-teams'
>Linked Parabol Teams</option>
</select>
</div>
Foo
</div>

<h2>Linked Parabol Teams</h2>
<button onClick={handleLink}>Add Team</button>
{teams}
<h2>Channel: {channelId}</h2>
{meetings?.map((meeting) => (
<div key={meeting.id}>
<h2>{meeting.name}</h2>
</div>
Expand Down
10 changes: 6 additions & 4 deletions webapp/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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())
})
Expand All @@ -40,15 +41,16 @@ export default class Plugin {
width={24}
height={24}
src={`${getAssetsUrl(store.getState())}/parabol.png`}
/>Parabol
/> Parabol
</div>,
)
registry.registerChannelHeaderButtonAction(
<img src={`${getAssetsUrl(store.getState())}/parabol.png`}/>,

// 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',
)

Expand Down
Loading

0 comments on commit 2bc4dc3

Please sign in to comment.