From 3683e88abaddf761a6b34d57088f669b9886049e Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Wed, 29 May 2024 14:17:30 +0200 Subject: [PATCH 1/3] feat: Add Jira Server to Your Work --- .../TeamPrompt/TeamPromptWorkDrawer.tsx | 9 ++ .../WorkDrawer/JiraServerIntegrationPanel.tsx | 87 +++++++++++++ .../JiraServerIntegrationResults.tsx | 115 +++++++++++++++++ .../JiraServerIntegrationResultsRoot.tsx | 31 +++++ .../WorkDrawer/JiraServerObjectCard.tsx | 122 ++++++++++++++++++ .../graphql/private/typeDefs/_legacy.graphql | 6 + .../graphql/types/JiraServerIntegration.ts | 10 +- .../server/graphql/types/JiraServerIssue.ts | 8 ++ .../jiraServer/JiraServerRestManager.ts | 2 + 9 files changed, 386 insertions(+), 4 deletions(-) create mode 100644 packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx create mode 100644 packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResults.tsx create mode 100644 packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResultsRoot.tsx create mode 100644 packages/client/components/TeamPrompt/WorkDrawer/JiraServerObjectCard.tsx diff --git a/packages/client/components/TeamPrompt/TeamPromptWorkDrawer.tsx b/packages/client/components/TeamPrompt/TeamPromptWorkDrawer.tsx index 9f51d4e28cd..223b6194a62 100644 --- a/packages/client/components/TeamPrompt/TeamPromptWorkDrawer.tsx +++ b/packages/client/components/TeamPrompt/TeamPromptWorkDrawer.tsx @@ -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 { @@ -32,6 +34,7 @@ const TeamPromptWorkDrawer = (props: Props) => { ...GitHubIntegrationPanel_meeting ...JiraIntegrationPanel_meeting ...GCalIntegrationPanel_meeting + ...JiraServerIntegrationPanel_meeting } `, meetingRef @@ -54,6 +57,12 @@ const TeamPromptWorkDrawer = (props: Props) => { label: 'Parabol', Component: ParabolTasksPanel }, + { + icon: , + service: 'jiraServer', + label: 'Jira Server', + Component: JiraServerIntegrationPanel + }, {icon: , service: 'github', label: 'GitHub', Component: GitHubIntegrationPanel}, {icon: , service: 'jira', label: 'Jira', Component: JiraIntegrationPanel}, { diff --git a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx new file mode 100644 index 00000000000..aa62c2d22a6 --- /dev/null +++ b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx @@ -0,0 +1,87 @@ +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 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 { + id + providerId + } + } + } + } + } + `, + meetingRef + ) + + const teamMember = meeting.viewerMeetingMember?.teamMember + const integration = teamMember?.integrations.jiraServer + const providerId = integration?.providerId + const isActive = !!integration?.id + + 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 ? ( + + ) : ( +
+
{/* */}
+ Connect to Jira Server +
+ Connect to Jira Server to view your issues. +
+ + {error &&
Error: {error.message}
} +
+ )} + + ) +} + +export default JiraServerIntegrationPanel diff --git a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResults.tsx b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResults.tsx new file mode 100644 index 00000000000..558513beeda --- /dev/null +++ b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResults.tsx @@ -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 + 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 ( + <> +
+ {jiraResults && jiraResults.length > 0 ? ( + jiraResults?.map((result, idx) => { + if (!result) { + return null + } + return + }) + ) : ( +
+ +
+ {error?.message ? error.message : `Looks like you don’t have any issues to display.`} +
+ + Review your JiraServer configuration + +
+ )} + {lastItem} + {hasNext && ( +
+ +
+ )} +
+ + ) +} + +export default JiraServerIntegrationResults diff --git a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResultsRoot.tsx b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResultsRoot.tsx new file mode 100644 index 00000000000..c58e4170e3d --- /dev/null +++ b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResultsRoot.tsx @@ -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( + jiraIntegrationResultsQuery, + { + teamId: teamId + } + ) + return ( + + }> + {queryRef && } + + + ) +} + +export default JiraServerIntegrationResultsRoot diff --git a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerObjectCard.tsx b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerObjectCard.tsx new file mode 100644 index 00000000000..94d963674c6 --- /dev/null +++ b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerObjectCard.tsx @@ -0,0 +1,122 @@ +import {Link} from '@mui/icons-material' +import graphql from 'babel-plugin-relay/macro' +import React from 'react' +import CopyToClipboard from 'react-copy-to-clipboard' +import {useFragment} from 'react-relay' +import {JiraServerObjectCard_result$key} from '../../../__generated__/JiraServerObjectCard_result.graphql' +import useAtmosphere from '../../../hooks/useAtmosphere' +import {MenuPosition} from '../../../hooks/useCoords' +import useTooltip from '../../../hooks/useTooltip' +import jiraSVG from '../../../styles/theme/images/graphics/jira.svg' +import SendClientSideEvent from '../../../utils/SendClientSideEvent' +import relativeDate from '../../../utils/date/relativeDate' +import {mergeRefs} from '../../../utils/react/mergeRefs' + +interface Props { + resultRef: JiraServerObjectCard_result$key +} + +const JiraServerObjectCard = (props: Props) => { + const {resultRef} = props + + const result = useFragment( + graphql` + fragment JiraServerObjectCard_result on JiraServerIssue { + id + summary + url + issueKey + projectKey + projectName + updatedAt + } + `, + resultRef + ) + + const atmosphere = useAtmosphere() + + const {tooltipPortal, openTooltip, closeTooltip, originRef} = useTooltip( + MenuPosition.UPPER_CENTER + ) + + const { + tooltipPortal: copiedTooltipPortal, + openTooltip: openCopiedTooltip, + closeTooltip: closeCopiedTooltip, + originRef: copiedTooltipRef + } = useTooltip(MenuPosition.LOWER_CENTER) + + const trackLinkClick = () => { + SendClientSideEvent(atmosphere, 'Your Work Drawer Card Link Clicked', { + service: 'jira' + }) + } + + const trackCopy = () => { + SendClientSideEvent(atmosphere, 'Your Work Drawer Card Copied', { + service: 'jira' + }) + } + + const handleCopy = () => { + openCopiedTooltip() + trackCopy() + setTimeout(() => { + closeCopiedTooltip() + }, 2000) + } + + const {summary, url, issueKey, projectName, updatedAt} = result + + return ( +
+
+ {/**/} + + {issueKey} + +
Updated {relativeDate(updatedAt)}
+
+ +
+
+
+ +
+
{projectName}
+
+ +
+ +
+
+ {tooltipPortal('Copy link')} + {copiedTooltipPortal('Copied!')} +
+
+ ) +} + +export default JiraServerObjectCard diff --git a/packages/server/graphql/private/typeDefs/_legacy.graphql b/packages/server/graphql/private/typeDefs/_legacy.graphql index 0aeca980ef5..4bea1166dcc 100644 --- a/packages/server/graphql/private/typeDefs/_legacy.graphql +++ b/packages/server/graphql/private/typeDefs/_legacy.graphql @@ -1096,6 +1096,7 @@ type JiraServerIssue implements TaskIntegration { id: ID! issueKey: ID! projectKey: ID! + projectName: String! """ The parabol teamId this issue was fetched for @@ -1122,6 +1123,11 @@ type JiraServerIssue implements TaskIntegration { The description converted into raw HTML """ descriptionHTML: String! + + """ + The timestamp the issue was last updated + """ + updatedAt: DateTime! } """ diff --git a/packages/server/graphql/types/JiraServerIntegration.ts b/packages/server/graphql/types/JiraServerIntegration.ts index 2f4f3968b74..4a2d6a5bf67 100644 --- a/packages/server/graphql/types/JiraServerIntegration.ts +++ b/packages/server/graphql/types/JiraServerIntegration.ts @@ -92,7 +92,7 @@ const JiraServerIntegration = new GraphQLObjectType<{teamId: string; userId: str }, after: { type: GraphQLString, - defaultValue: '0' + defaultValue: '-1' }, queryString: { type: GraphQLString, @@ -152,6 +152,7 @@ const JiraServerIntegration = new GraphQLObjectType<{teamId: string; userId: str maxResults, startAt ) + console.log('GEORG issueRes', JSON.stringify(issueRes, null, 2)) if (issueRes instanceof Error) { return connectionFromTasks([], first, { @@ -162,21 +163,22 @@ const JiraServerIntegration = new GraphQLObjectType<{teamId: string; userId: str const {issues} = issueRes const mappedIssues = issues.map((issue) => { - const {project, issuetype, summary, description} = issue.fields + const {project, issuetype, summary, description, updated} = issue.fields return { ...issue, userId, teamId, providerId: provider.id, issueKey: issue.key, + description: description ?? '', descriptionHTML: issue.renderedFields.description, projectId: project.id, projectKey: project.key, + projectName: project.name, issueType: issuetype.id, summary, - description, service: 'jiraServer' as const, - updatedAt: new Date() + updatedAt: new Date(updated) } }) diff --git a/packages/server/graphql/types/JiraServerIssue.ts b/packages/server/graphql/types/JiraServerIssue.ts index 6d4a583c68a..3047f751c28 100644 --- a/packages/server/graphql/types/JiraServerIssue.ts +++ b/packages/server/graphql/types/JiraServerIssue.ts @@ -3,6 +3,7 @@ import JiraServerIssueId from '~/shared/gqlIds/JiraServerIssueId' import {JiraServerIssue as JiraServerRestIssue} from '../../dataloader/jiraServerLoaders' import connectionDefinitions from '../connectionDefinitions' import {GQLContext} from '../graphql' +import GraphQLISO8601Type from './GraphQLISO8601Type' import StandardMutationError from './StandardMutationError' import TaskIntegration from './TaskIntegration' @@ -40,6 +41,9 @@ const JiraServerIssue = new GraphQLObjectType projectKey: { type: new GraphQLNonNull(GraphQLID) }, + projectName: { + type: new GraphQLNonNull(GraphQLString) + }, teamId: { type: new GraphQLNonNull(GraphQLID), description: 'The parabol teamId this issue was fetched for' @@ -84,6 +88,10 @@ const JiraServerIssue = new GraphQLObjectType .map(({name}) => name) return fieldNames } + }, + updatedAt: { + type: new GraphQLNonNull(GraphQLISO8601Type), + description: 'The timestamp the issue was last updated' } }) }) diff --git a/packages/server/integrations/jiraServer/JiraServerRestManager.ts b/packages/server/integrations/jiraServer/JiraServerRestManager.ts index 52416b07746..36b024379a1 100644 --- a/packages/server/integrations/jiraServer/JiraServerRestManager.ts +++ b/packages/server/integrations/jiraServer/JiraServerRestManager.ts @@ -57,7 +57,9 @@ export interface JiraServerIssue { id: string key: string name: string + self: string } + updated: string } renderedFields: { description: string From 957453f744df87259ce5ab29b8f7955770300c77 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Wed, 29 May 2024 15:03:06 +0200 Subject: [PATCH 2/3] Only show the jira server logo if the team has access to a provider --- .../TeamPrompt/TeamPromptWorkDrawer.tsx | 30 +++++++++++++++---- .../WorkDrawer/JiraServerIntegrationPanel.tsx | 13 +++++--- .../graphql/types/JiraServerIntegration.ts | 1 - 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/client/components/TeamPrompt/TeamPromptWorkDrawer.tsx b/packages/client/components/TeamPrompt/TeamPromptWorkDrawer.tsx index 223b6194a62..836d3aea0e2 100644 --- a/packages/client/components/TeamPrompt/TeamPromptWorkDrawer.tsx +++ b/packages/client/components/TeamPrompt/TeamPromptWorkDrawer.tsx @@ -35,11 +35,25 @@ const TeamPromptWorkDrawer = (props: Props) => { ...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', { @@ -57,12 +71,16 @@ const TeamPromptWorkDrawer = (props: Props) => { label: 'Parabol', Component: ParabolTasksPanel }, - { - icon: , - service: 'jiraServer', - label: 'Jira Server', - Component: JiraServerIntegrationPanel - }, + ...(hasJiraServer + ? [ + { + icon: , + service: 'jiraServer', + label: 'Jira Server', + Component: JiraServerIntegrationPanel + } + ] + : []), {icon: , service: 'github', label: 'GitHub', Component: GitHubIntegrationPanel}, {icon: , service: 'jira', label: 'Jira', Component: JiraIntegrationPanel}, { diff --git a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx index aa62c2d22a6..785cfec05f4 100644 --- a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx +++ b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx @@ -24,8 +24,13 @@ const JiraServerIntegrationPanel = (props: Props) => { teamId integrations { jiraServer { - id - providerId + auth { + id + isActive + } + sharedProviders { + id + } } } } @@ -37,8 +42,8 @@ const JiraServerIntegrationPanel = (props: Props) => { const teamMember = meeting.viewerMeetingMember?.teamMember const integration = teamMember?.integrations.jiraServer - const providerId = integration?.providerId - const isActive = !!integration?.id + const providerId = integration?.sharedProviders?.[0]?.id + const isActive = !!integration?.auth?.isActive const atmosphere = useAtmosphere() const mutationProps = useMutationProps() diff --git a/packages/server/graphql/types/JiraServerIntegration.ts b/packages/server/graphql/types/JiraServerIntegration.ts index 4a2d6a5bf67..ec349fa98b1 100644 --- a/packages/server/graphql/types/JiraServerIntegration.ts +++ b/packages/server/graphql/types/JiraServerIntegration.ts @@ -152,7 +152,6 @@ const JiraServerIntegration = new GraphQLObjectType<{teamId: string; userId: str maxResults, startAt ) - console.log('GEORG issueRes', JSON.stringify(issueRes, null, 2)) if (issueRes instanceof Error) { return connectionFromTasks([], first, { From 289f45a62da70f264b71ee73af8264ed4e722811 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Thu, 30 May 2024 16:30:17 +0200 Subject: [PATCH 3/3] Minor cosmetics --- .../TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx | 5 ++++- .../TeamPrompt/WorkDrawer/JiraServerIntegrationResults.tsx | 2 +- .../TeamPrompt/WorkDrawer/JiraServerObjectCard.tsx | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx index 785cfec05f4..da6eedf1a05 100644 --- a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx +++ b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationPanel.tsx @@ -4,6 +4,7 @@ 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' @@ -71,7 +72,9 @@ const JiraServerIntegrationPanel = (props: Props) => { ) : (
-
{/* */}
+
+ +
Connect to Jira Server
Connect to Jira Server to view your issues. diff --git a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResults.tsx b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResults.tsx index 558513beeda..3beed1d4c9c 100644 --- a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResults.tsx +++ b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerIntegrationResults.tsx @@ -97,7 +97,7 @@ const JiraServerIntegrationResults = (props: Props) => { to={`/team/${teamId}/integrations`} className='mt-4 font-semibold text-sky-500 hover:text-sky-400' > - Review your JiraServer configuration + Review your Jira Server configuration
)} diff --git a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerObjectCard.tsx b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerObjectCard.tsx index 94d963674c6..dd6da759a71 100644 --- a/packages/client/components/TeamPrompt/WorkDrawer/JiraServerObjectCard.tsx +++ b/packages/client/components/TeamPrompt/WorkDrawer/JiraServerObjectCard.tsx @@ -72,7 +72,6 @@ const JiraServerObjectCard = (props: Props) => { return (
- {/**/}