Skip to content

Commit

Permalink
feat: generate insight from empty state (#10323)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickoferrall authored Oct 16, 2024
1 parent 57dde60 commit cfd8c78
Show file tree
Hide file tree
Showing 17 changed files with 307 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const TeamDashHeader = (props: Props) => {
...DashboardAvatars_team
id
name
hasInsightsFlag: featureFlag(featureName: "insights")
organization {
id
name
Expand All @@ -112,16 +113,23 @@ const TeamDashHeader = (props: Props) => {
`,
teamRef
)
const {organization, id: teamId, name: teamName, teamMembers, viewerTeamMember} = team
const isViewerTeamLead = viewerTeamMember?.isLead
const {
organization,
id: teamId,
name: teamName,
teamMembers,
viewerTeamMember,
hasInsightsFlag
} = team
const {name: orgName, id: orgId} = organization
const canViewInsights = viewerTeamMember?.isLead && hasInsightsFlag
const {history} = useRouter()

const tabs = [
{label: 'Activity', path: 'activity'},
{label: 'Tasks', path: 'tasks'},
{label: 'Integrations', path: 'integrations'},
...(isViewerTeamLead ? [{label: 'Insights', path: 'insights'}] : [])
...(canViewInsights ? [{label: 'Insights', path: 'insights'}] : [])
]

const activePath = location.pathname.split('/').pop()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'
import graphql from 'babel-plugin-relay/macro'
import dayjs from 'dayjs'
import {marked} from 'marked'
import React from 'react'
import {useFragment} from 'react-relay'
import sanitizeHtml from 'sanitize-html'
import {TeamInsightContent_team$key} from '../../../../__generated__/TeamInsightContent_team.graphql'

interface Props {
teamName: string
insightRef: TeamInsightContent_team$key
}

const TeamInsightContent = (props: Props) => {
const {insightRef, teamName} = props
const insight = useFragment(
graphql`
fragment TeamInsightContent_team on Insight {
meetingsCount
wins
challenges
startDateTime
endDateTime
}
`,
insightRef
)
const {meetingsCount, wins, challenges} = insight

const renderMarkdown = (text: string) => {
const renderedText = marked(text, {
gfm: true,
breaks: true
}) as string
return sanitizeHtml(renderedText, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['a']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
a: ['href', 'target', 'rel']
},
transformTags: {
a: (tagName, attribs) => {
return {
tagName,
attribs: {
...attribs,
target: '_blank',
rel: 'noopener noreferrer'
}
}
}
}
})
}

const formatDateRange = (start: string, end: string) => {
const startDate = dayjs(start)
const endDate = dayjs(end)

if (startDate.year() === endDate.year()) {
return `${startDate.format('MMM')} to ${endDate.format('MMM YYYY')}`
} else {
return `${startDate.format('MMM YYYY')} to ${endDate.format('MMM YYYY')}`
}
}

const dateRange = insight
? formatDateRange(insight.startDateTime, insight.endDateTime)
: 'Date range not available'

return (
<div className='mx-auto aspect-[1/1.414] w-[640px] max-w-3xl overflow-y-auto rounded-lg bg-white px-[56px] pt-8 shadow-md'>
<h2 className='mb-4 mt-0 flex items-center pt-0 text-2xl font-semibold leading-9'>
<AutoAwesomeIcon className='mr-2 h-9 w-9 text-grape-500' />
<span>Insights - {dateRange}</span>
</h2>
<p className='mb-6 text-sm text-slate-600'>Summarized {meetingsCount} meetings</p>

{wins && wins.length > 0 && (
<>
<h3 className='mb-0 text-lg font-semibold text-slate-700'>Wins</h3>
<p className='mb-2 mt-0 text-sm italic text-slate-600'>
What wins has "{teamName}" seen during this timeframe?
</p>
<ul className='mb-6 list-disc space-y-0 pl-6'>
{wins.map((win, index) => (
<li key={index} className='text-sm text-slate-700'>
<span
className='link-style'
dangerouslySetInnerHTML={{__html: renderMarkdown(win)}}
/>
</li>
))}
</ul>
</>
)}

{challenges && challenges.length > 0 && (
<>
<h3 className='mb-0 text-lg font-semibold text-slate-700'>Challenges</h3>
<p className='mb-2 mt-0 text-sm italic text-slate-600'>
What challenges has "{teamName}" faced during this timeframe?
</p>
<ul className='list-disc space-y-0 pl-6'>
{challenges.map((challenge, index) => (
<li key={index} className='text-sm text-slate-700'>
<span
className='link-style'
dangerouslySetInnerHTML={{__html: renderMarkdown(challenge)}}
/>
</li>
))}
</ul>
</>
)}
</div>
)
}

export default TeamInsightContent
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import AddIcon from '@mui/icons-material/Add'
import React from 'react'
import insightsEmptyStateImg from '../../../../../../static/images/illustrations/insights-empty-state.png'
import useAtmosphere from '../../../../hooks/useAtmosphere'
import useMutationProps from '../../../../hooks/useMutationProps'
import GenerateInsightMutation from '../../../../mutations/GenerateInsightMutation'
import plural from '../../../../utils/plural'

interface Props {
meetingsCount?: number
teamId?: string
}

const TeamInsightEmptyState = (props: Props) => {
const {meetingsCount, teamId} = props
const atmosphere = useAtmosphere()
const {onError, error, onCompleted, submitMutation, submitting} = useMutationProps()

if (meetingsCount === undefined || !teamId) return null

const canGenerateInsight = meetingsCount > 1

const handleGenerateInsight = () => {
if (submitting) return
submitMutation()
const now = new Date()
const threeMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate())
GenerateInsightMutation(
atmosphere,
{
teamId,
startDate: threeMonthsAgo.toISOString(), // TODO: let users choose date range
endDate: now.toISOString()
},
{onError, onCompleted}
)
}

return (
<div className='flex flex-col items-center text-center'>
<img src={insightsEmptyStateImg} alt='Empty state' width={300} height={300} />
<div className='space-y-1'>
<p className='mt-4 text-lg leading-tight'>
Your team has completed <strong>{meetingsCount}</strong>{' '}
{plural(meetingsCount, 'meeting')}.
</p>
{canGenerateInsight ? (
<p className='text-lg leading-tight'>
This should be{' '}
<strong>
<em>good fodder</em>
</strong>{' '}
for some interesting insights!
</p>
) : (
<p className='text-lg leading-tight'>
Create more meetings to start generating insights for your team.
</p>
)}
</div>
{canGenerateInsight && (
<button
className='mt-6 flex items-center rounded-full bg-grape-500 py-2 px-6 font-bold text-white transition-all duration-200 ease-in-out hover:cursor-pointer hover:bg-grape-600 hover:shadow-md disabled:opacity-50'
onClick={handleGenerateInsight}
disabled={submitting}
>
<AddIcon className='mr-2 h-5 w-5' />
{submitting ? 'Generating...' : 'Generate Insights'}
</button>
)}
{error && (
<div className='mt-2 pr-4 text-xs font-semibold text-tomato-500'>{error.message}</div>
)}
</div>
)
}

export default TeamInsightEmptyState
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'
import graphql from 'babel-plugin-relay/macro'
import dayjs from 'dayjs'
import {marked} from 'marked'
import React from 'react'
import {PreloadedQuery, usePreloadedQuery} from 'react-relay'
import sanitizeHtml from 'sanitize-html'
import {TeamInsightsQuery} from '../../../../__generated__/TeamInsightsQuery.graphql'
import TeamInsightContent from './TeamInsightContent'
import TeamInsightEmptyState from './TeamInsightEmptyState'

interface Props {
queryRef: PreloadedQuery<TeamInsightsQuery>
Expand All @@ -15,67 +13,30 @@ const query = graphql`
query TeamInsightsQuery($teamId: ID!) {
viewer {
team(teamId: $teamId) {
id
...TeamInsights_team @relay(mask: false)
name
insight {
meetingsCount
wins
challenges
startDateTime
endDateTime
...TeamInsightContent_team
}
}
}
}
`

const Insights = (props: Props) => {
graphql`
fragment TeamInsights_team on Team {
id
retroMeetingsCount
}
`

const TeamInsights = (props: Props) => {
const {queryRef} = props
const data = usePreloadedQuery<TeamInsightsQuery>(query, queryRef)
const {viewer} = data
const {team} = viewer
const {insight, name} = team ?? {}

const renderMarkdown = (text: string) => {
const renderedText = marked(text, {
gfm: true,
breaks: true
}) as string
return sanitizeHtml(renderedText, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['a']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
a: ['href', 'target', 'rel']
},
transformTags: {
a: (tagName, attribs) => {
return {
tagName,
attribs: {
...attribs,
target: '_blank',
rel: 'noopener noreferrer'
}
}
}
}
})
}

const formatDateRange = (start: string, end: string) => {
const startDate = dayjs(start)
const endDate = dayjs(end)

if (startDate.year() === endDate.year()) {
return `${startDate.format('MMM')} to ${endDate.format('MMM YYYY')}`
} else {
return `${startDate.format('MMM YYYY')} to ${endDate.format('MMM YYYY')}`
}
}

const dateRange = insight
? formatDateRange(insight.startDateTime, insight.endDateTime)
: 'Date range not available'
const {id: teamId, insight, name, retroMeetingsCount} = team ?? {}

return (
<div className='mb-8 space-y-6'>
Expand All @@ -91,45 +52,13 @@ const Insights = (props: Props) => {
Give us feedback
</a>
</p>
<div className='mx-auto aspect-[1/1.414] w-[640px] max-w-3xl overflow-y-auto rounded-lg bg-white px-[56px] pt-8 shadow-md'>
<h2 className='mb-4 mt-0 flex items-center pt-0 text-2xl font-semibold leading-9'>
<AutoAwesomeIcon className='mr-2 h-9 w-9 text-grape-500' />
<span>Insights - {dateRange}</span>
</h2>
<p className='mb-6 text-sm text-slate-600'>Summarized {insight?.meetingsCount} meetings</p>

<h3 className='mb-0 text-lg font-semibold text-slate-700'>Wins</h3>
<p className='mb-2 mt-0 text-sm italic text-slate-600'>
What wins has "{name}" seen during this timeframe?
</p>
<ul className='mb-6 list-disc space-y-0 pl-6'>
{insight?.wins.map((win, index) => (
<li key={index} className='text-sm text-slate-700'>
<span
className='link-style'
dangerouslySetInnerHTML={{__html: renderMarkdown(win)}}
/>
</li>
))}
</ul>

<h3 className='mb-0 text-lg font-semibold text-slate-700'>Challenges</h3>
<p className='mb-2 mt-0 text-sm italic text-slate-600'>
What challenges has "{name}" faced during this timeframe?
</p>
<ul className='list-disc space-y-0 pl-6'>
{insight?.challenges.map((challenge, index) => (
<li key={index} className='text-sm text-slate-700'>
<span
className='link-style'
dangerouslySetInnerHTML={{__html: renderMarkdown(challenge)}}
/>
</li>
))}
</ul>
</div>
{insight ? (
<TeamInsightContent insightRef={insight} teamName={name!} />
) : (
<TeamInsightEmptyState teamId={teamId} meetingsCount={retroMeetingsCount} />
)}
</div>
)
}

export default Insights
export default TeamInsights
1 change: 1 addition & 0 deletions packages/client/mutations/EndRetrospectiveMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ graphql`
activeMeetings {
id
}
...TeamInsights_team
insights {
...TeamDashInsights_insights
}
Expand Down
Loading

0 comments on commit cfd8c78

Please sign in to comment.