Skip to content

Commit

Permalink
feat: Add Jira Server to Your Work (#9794)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dschoordsch authored May 30, 2024
1 parent 23d48c4 commit eec025e
Show file tree
Hide file tree
Showing 9 changed files with 410 additions and 4 deletions.
27 changes: 27 additions & 0 deletions packages/client/components/TeamPrompt/TeamPromptWorkDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import gcalLogo from '../../styles/theme/images/graphics/google-calendar.svg'
import SendClientSideEvent from '../../utils/SendClientSideEvent'
import GitHubSVG from '../GitHubSVG'
import JiraSVG from '../JiraSVG'
import JiraServerSVG from '../JiraServerSVG'
import ParabolLogoSVG from '../ParabolLogoSVG'
import Tab from '../Tab/Tab'
import Tabs from '../Tabs/Tabs'
import GCalIntegrationPanel from './WorkDrawer/GCalIntegrationPanel'
import GitHubIntegrationPanel from './WorkDrawer/GitHubIntegrationPanel'
import JiraIntegrationPanel from './WorkDrawer/JiraIntegrationPanel'
import JiraServerIntegrationPanel from './WorkDrawer/JiraServerIntegrationPanel'
import ParabolTasksPanel from './WorkDrawer/ParabolTasksPanel'

interface Props {
Expand All @@ -32,11 +34,26 @@ const TeamPromptWorkDrawer = (props: Props) => {
...GitHubIntegrationPanel_meeting
...JiraIntegrationPanel_meeting
...GCalIntegrationPanel_meeting
...JiraServerIntegrationPanel_meeting
viewerMeetingMember {
teamMember {
teamId
integrations {
jiraServer {
sharedProviders {
id
}
}
}
}
}
}
`,
meetingRef
)
const atmosphere = useAtmosphere()
const hasJiraServer =
!!meeting.viewerMeetingMember?.teamMember?.integrations.jiraServer?.sharedProviders?.length

useEffect(() => {
SendClientSideEvent(atmosphere, 'Your Work Drawer Impression', {
Expand All @@ -54,6 +71,16 @@ const TeamPromptWorkDrawer = (props: Props) => {
label: 'Parabol',
Component: ParabolTasksPanel
},
...(hasJiraServer
? [
{
icon: <JiraServerSVG />,
service: 'jiraServer',
label: 'Jira Server',
Component: JiraServerIntegrationPanel
}
]
: []),
{icon: <GitHubSVG />, service: 'github', label: 'GitHub', Component: GitHubIntegrationPanel},
{icon: <JiraSVG />, service: 'jira', label: 'Jira', Component: JiraIntegrationPanel},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import graphql from 'babel-plugin-relay/macro'
import React from 'react'
import {useFragment} from 'react-relay'
import {JiraServerIntegrationPanel_meeting$key} from '../../../__generated__/JiraServerIntegrationPanel_meeting.graphql'
import useAtmosphere from '../../../hooks/useAtmosphere'
import useMutationProps from '../../../hooks/useMutationProps'
import jiraServerSVG from '../../../styles/theme/images/graphics/jira-software-blue.svg'
import JiraServerClientManager from '../../../utils/JiraServerClientManager'
import SendClientSideEvent from '../../../utils/SendClientSideEvent'
import JiraServerIntegrationResultsRoot from './JiraServerIntegrationResultsRoot'

interface Props {
meetingRef: JiraServerIntegrationPanel_meeting$key
}

const JiraServerIntegrationPanel = (props: Props) => {
const {meetingRef} = props
const meeting = useFragment(
graphql`
fragment JiraServerIntegrationPanel_meeting on TeamPromptMeeting {
id
teamId
viewerMeetingMember {
teamMember {
teamId
integrations {
jiraServer {
auth {
id
isActive
}
sharedProviders {
id
}
}
}
}
}
}
`,
meetingRef
)

const teamMember = meeting.viewerMeetingMember?.teamMember
const integration = teamMember?.integrations.jiraServer
const providerId = integration?.sharedProviders?.[0]?.id
const isActive = !!integration?.auth?.isActive

const atmosphere = useAtmosphere()
const mutationProps = useMutationProps()
const {error, onError} = mutationProps

const authJiraServer = () => {
if (!teamMember || !providerId) {
return onError(new Error('Could not find integration provider'))
}
JiraServerClientManager.openOAuth(atmosphere, providerId, teamMember.teamId, mutationProps)

SendClientSideEvent(atmosphere, 'Your Work Drawer Integration Connected', {
teamId: meeting.teamId,
meetingId: meeting.id,
service: 'jira server'
})
}
if (!teamMember || !teamMember) {
return null
}

return (
<>
{isActive ? (
<JiraServerIntegrationResultsRoot teamId={teamMember.teamId} />
) : (
<div className='-mt-14 flex h-full flex-col items-center justify-center gap-2'>
<div className='h-10 w-10'>
<img className='h-10 w-10' src={jiraServerSVG} />
</div>
<b>Connect to Jira Server</b>
<div className='w-1/2 text-center text-sm'>
Connect to Jira Server to view your issues.
</div>
<button
className='mt-4 cursor-pointer rounded-full bg-sky-500 px-8 py-2 font-semibold text-white hover:bg-sky-600'
onClick={authJiraServer}
>
Connect
</button>
{error && <div className='text-tomato-500'>Error: {error.message}</div>}
</div>
)}
</>
)
}

export default JiraServerIntegrationPanel
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import graphql from 'babel-plugin-relay/macro'
import React from 'react'
import {PreloadedQuery, usePaginationFragment, usePreloadedQuery} from 'react-relay'
import {Link} from 'react-router-dom'
import halloweenRetrospectiveTemplate from '../../../../../static/images/illustrations/halloweenRetrospectiveTemplate.png'
import {JiraServerIntegrationResultsQuery} from '../../../__generated__/JiraServerIntegrationResultsQuery.graphql'
import {JiraServerIntegrationResultsSearchPaginationQuery} from '../../../__generated__/JiraServerIntegrationResultsSearchPaginationQuery.graphql'
import {JiraServerIntegrationResults_search$key} from '../../../__generated__/JiraServerIntegrationResults_search.graphql'
import useLoadNextOnScrollBottom from '../../../hooks/useLoadNextOnScrollBottom'
import Ellipsis from '../../Ellipsis/Ellipsis'
import JiraServerObjectCard from './JiraServerObjectCard'

interface Props {
queryRef: PreloadedQuery<JiraServerIntegrationResultsQuery>
teamId: string
}

const JiraServerIntegrationResults = (props: Props) => {
const {queryRef, teamId} = props
const query = usePreloadedQuery(
graphql`
query JiraServerIntegrationResultsQuery($teamId: ID!) {
...JiraServerIntegrationResults_search @arguments(teamId: $teamId)
}
`,
queryRef
)

const paginationRes = usePaginationFragment<
JiraServerIntegrationResultsSearchPaginationQuery,
JiraServerIntegrationResults_search$key
>(
graphql`
fragment JiraServerIntegrationResults_search on Query
@argumentDefinitions(
cursor: {type: "String"}
count: {type: "Int", defaultValue: 20}
teamId: {type: "ID!"}
)
@refetchable(queryName: "JiraServerIntegrationResultsSearchPaginationQuery") {
viewer {
teamMember(teamId: $teamId) {
integrations {
jiraServer {
issues(
first: $count
after: $cursor
isJQL: true
queryString: "assignee = currentUser() order by updated DESC"
) @connection(key: "JiraServerScopingSearchResults_issues") {
error {
message
}
edges {
node {
...JiraServerObjectCard_result
id
summary
url
issueKey
}
}
}
}
}
}
}
}
`,
query
)

const lastItem = useLoadNextOnScrollBottom(paginationRes, {}, 20)
const {data, hasNext} = paginationRes

const jira = data.viewer.teamMember?.integrations.jiraServer
const jiraResults = jira?.issues.edges.map((edge) => edge.node)
const error = jira?.issues.error ?? null

return (
<>
<div className='flex flex h-full flex-col gap-y-2 overflow-auto p-4'>
{jiraResults && jiraResults.length > 0 ? (
jiraResults?.map((result, idx) => {
if (!result) {
return null
}
return <JiraServerObjectCard key={idx} resultRef={result} />
})
) : (
<div className='-mt-14 flex h-full flex-col items-center justify-center'>
<img className='w-20' src={halloweenRetrospectiveTemplate} />
<div className='mt-7 w-2/3 text-center'>
{error?.message ? error.message : `Looks like you don’t have any issues to display.`}
</div>
<Link
to={`/team/${teamId}/integrations`}
className='mt-4 font-semibold text-sky-500 hover:text-sky-400'
>
Review your Jira Server configuration
</Link>
</div>
)}
{lastItem}
{hasNext && (
<div className='mx-auto mb-4 -mt-4 h-8 text-2xl' key={'loadingNext'}>
<Ellipsis />
</div>
)}
</div>
</>
)
}

export default JiraServerIntegrationResults
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, {Suspense} from 'react'
import {Loader} from '~/utils/relay/renderLoader'
import jiraIntegrationResultsQuery, {
JiraServerIntegrationResultsQuery
} from '../../../__generated__/JiraServerIntegrationResultsQuery.graphql'
import useQueryLoaderNow from '../../../hooks/useQueryLoaderNow'
import ErrorBoundary from '../../ErrorBoundary'
import JiraServerIntegrationResults from './JiraServerIntegrationResults'

interface Props {
teamId: string
}

const JiraServerIntegrationResultsRoot = (props: Props) => {
const {teamId} = props
const queryRef = useQueryLoaderNow<JiraServerIntegrationResultsQuery>(
jiraIntegrationResultsQuery,
{
teamId: teamId
}
)
return (
<ErrorBoundary>
<Suspense fallback={<Loader />}>
{queryRef && <JiraServerIntegrationResults queryRef={queryRef} teamId={teamId} />}
</Suspense>
</ErrorBoundary>
)
}

export default JiraServerIntegrationResultsRoot
Loading

0 comments on commit eec025e

Please sign in to comment.