diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx
index 1f1c899ef7b..ca4705f9733 100644
--- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx
+++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx
@@ -93,7 +93,7 @@ const WholeMeetingSummaryResult = ({meetingRef}: Props) => {
|
diff --git a/packages/client/modules/teamDashboard/components/TeamDashHeader/TeamDashHeader.tsx b/packages/client/modules/teamDashboard/components/TeamDashHeader/TeamDashHeader.tsx
index f1c764adfaf..91805c5cf19 100644
--- a/packages/client/modules/teamDashboard/components/TeamDashHeader/TeamDashHeader.tsx
+++ b/packages/client/modules/teamDashboard/components/TeamDashHeader/TeamDashHeader.tsx
@@ -95,10 +95,14 @@ const TeamDashHeader = (props: Props) => {
...DashboardAvatars_team
id
name
+ hasInsightsFlag: featureFlag(featureName: "insights")
organization {
id
name
}
+ viewerTeamMember {
+ isLead
+ }
teamMembers(sortBy: "preferredName") {
...InviteTeamMemberAvatar_teamMembers
...DashboardAvatar_teamMember
@@ -109,15 +113,23 @@ const TeamDashHeader = (props: Props) => {
`,
teamRef
)
- const {organization, id: teamId, name: teamName, teamMembers} = team
+ 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'},
- {label: 'Insights', path: 'insights'}
+ ...(canViewInsights ? [{label: 'Insights', path: 'insights'}] : [])
]
const activePath = location.pathname.split('/').pop()
diff --git a/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightContent.tsx b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightContent.tsx
new file mode 100644
index 00000000000..849181d07e6
--- /dev/null
+++ b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightContent.tsx
@@ -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 (
+
+
+
+ Insights - {dateRange}
+
+
Summarized {meetingsCount} meetings
+
+ {wins && wins.length > 0 && (
+ <>
+
Wins
+
+ What wins has "{teamName}" seen during this timeframe?
+
+
+ {wins.map((win, index) => (
+ -
+
+
+ ))}
+
+ >
+ )}
+
+ {challenges && challenges.length > 0 && (
+ <>
+
Challenges
+
+ What challenges has "{teamName}" faced during this timeframe?
+
+
+ {challenges.map((challenge, index) => (
+ -
+
+
+ ))}
+
+ >
+ )}
+
+ )
+}
+
+export default TeamInsightContent
diff --git a/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightEmptyState.tsx b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightEmptyState.tsx
new file mode 100644
index 00000000000..9a4c13487e3
--- /dev/null
+++ b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightEmptyState.tsx
@@ -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 (
+
+
+
+
+ Your team has completed {meetingsCount}{' '}
+ {plural(meetingsCount, 'meeting')}.
+
+ {canGenerateInsight ? (
+
+ This should be{' '}
+
+ good fodder
+ {' '}
+ for some interesting insights!
+
+ ) : (
+
+ Create more meetings to start generating insights for your team.
+
+ )}
+
+ {canGenerateInsight && (
+
+ )}
+ {error && (
+
{error.message}
+ )}
+
+ )
+}
+
+export default TeamInsightEmptyState
diff --git a/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsights.tsx b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsights.tsx
index b10f1eb9238..9232897bdd8 100644
--- a/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsights.tsx
+++ b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsights.tsx
@@ -1,8 +1,9 @@
-import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'
import graphql from 'babel-plugin-relay/macro'
import React from 'react'
import {PreloadedQuery, usePreloadedQuery} from 'react-relay'
import {TeamInsightsQuery} from '../../../../__generated__/TeamInsightsQuery.graphql'
+import TeamInsightContent from './TeamInsightContent'
+import TeamInsightEmptyState from './TeamInsightEmptyState'
interface Props {
queryRef: PreloadedQuery
@@ -12,36 +13,52 @@ const query = graphql`
query TeamInsightsQuery($teamId: ID!) {
viewer {
team(teamId: $teamId) {
- id
+ ...TeamInsights_team @relay(mask: false)
name
+ insight {
+ wins
+ ...TeamInsightContent_team
+ }
}
}
}
`
-const Insights = (props: Props) => {
+graphql`
+ fragment TeamInsights_team on Team {
+ id
+ retroMeetingsCount
+ }
+`
+
+const TeamInsights = (props: Props) => {
const {queryRef} = props
const data = usePreloadedQuery(query, queryRef)
- // TODO: use the query rather than just console logging it
- console.log('🚀 ~ data:', data)
+ const {viewer} = data
+ const {team} = viewer
+ const {id: teamId, insight, name, retroMeetingsCount} = team ?? {}
return (
Only you (as Team Lead) can see Team Insights. Insights
are auto-generated.{' '}
-
+
Give us feedback
-
-
-
- Insights - Aug to Sep 2024
-
-
+ {insight ? (
+
+ ) : (
+
+ )}
)
}
-export default Insights
+export default TeamInsights
diff --git a/packages/client/mutations/EndRetrospectiveMutation.ts b/packages/client/mutations/EndRetrospectiveMutation.ts
index c7f3d2c1df2..e2c37a4ec49 100644
--- a/packages/client/mutations/EndRetrospectiveMutation.ts
+++ b/packages/client/mutations/EndRetrospectiveMutation.ts
@@ -52,6 +52,7 @@ graphql`
activeMeetings {
id
}
+ ...TeamInsights_team
insights {
...TeamDashInsights_insights
}
diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts
index d7bbe485b4f..b0324478e70 100644
--- a/packages/client/mutations/GenerateInsightMutation.ts
+++ b/packages/client/mutations/GenerateInsightMutation.ts
@@ -5,8 +5,14 @@ import {StandardMutation} from '../types/relayMutations'
graphql`
fragment GenerateInsightMutation_team on GenerateInsightSuccess {
- wins
- challenges
+ team {
+ id
+ insight {
+ wins
+ challenges
+ meetingsCount
+ }
+ }
}
`
diff --git a/packages/client/shared/gqlIds/InsightId.ts b/packages/client/shared/gqlIds/InsightId.ts
new file mode 100644
index 00000000000..ebe082ca92c
--- /dev/null
+++ b/packages/client/shared/gqlIds/InsightId.ts
@@ -0,0 +1,7 @@
+export const InsightId = {
+ join: (ownerId: string, databaseId: number) => `insight:${ownerId}:${databaseId}`,
+ split: (id: string) => {
+ const [, ownerId, databaseId] = id.split(':')
+ return {ownerId, databaseId}
+ }
+}
diff --git a/packages/client/styles/theme/global.css b/packages/client/styles/theme/global.css
index b1a76203c76..4b8d81dbb31 100644
--- a/packages/client/styles/theme/global.css
+++ b/packages/client/styles/theme/global.css
@@ -188,7 +188,7 @@
@apply m-0 rounded-[1px] border-l-2 border-solid border-l-slate-500 bg-slate-200 py-0 px-[8px] font-mono font-[13px] leading-normal;
}
-.summary-link-style a {
+.link-style a {
@apply text-sky-500;
text-decoration: underline;
}
diff --git a/packages/client/subscriptions/TeamSubscription.ts b/packages/client/subscriptions/TeamSubscription.ts
index 17527fdee9b..3e7b524d4ab 100644
--- a/packages/client/subscriptions/TeamSubscription.ts
+++ b/packages/client/subscriptions/TeamSubscription.ts
@@ -46,6 +46,9 @@ const subscription = graphql`
subscription TeamSubscription {
teamSubscription {
fieldName
+ GenerateInsightSuccess {
+ ...GenerateInsightMutation_team @relay(mask: false)
+ }
UpdateRecurrenceSettingsSuccess {
...UpdateRecurrenceSettingsMutation_team @relay(mask: false)
}
diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts
index dd75986ec34..86cabe39472 100644
--- a/packages/server/dataloader/customLoaderMakers.ts
+++ b/packages/server/dataloader/customLoaderMakers.ts
@@ -25,7 +25,7 @@ import getMeetingTaskEstimates, {
MeetingTaskEstimatesResult
} from '../postgres/queries/getMeetingTaskEstimates'
import {selectMeetingSettings, selectNewMeetings, selectTeams} from '../postgres/select'
-import {MeetingSettings, OrganizationUser, Team} from '../postgres/types'
+import {Insight, MeetingSettings, OrganizationUser, Team} from '../postgres/types'
import {AnyMeeting, MeetingTypeEnum} from '../postgres/types/Meeting'
import {Logger} from '../utils/Logger'
import getRedis from '../utils/getRedis'
@@ -832,6 +832,25 @@ export const meetingCount = (parent: RootDataLoader, dependsOn: RegisterDependsO
)
}
+export const latestInsightByTeamId = (parent: RootDataLoader) => {
+ return new NullableDataLoader(
+ async (teamIds) => {
+ const pg = getKysely()
+ const insights = await pg
+ .selectFrom('Insight')
+ .where('teamId', 'in', teamIds)
+ .selectAll()
+ .orderBy('createdAt', 'desc')
+ .execute()
+
+ return teamIds.map((teamId) => insights.find((insight) => insight.teamId === teamId) || null)
+ },
+ {
+ ...parent.dataLoaderOptions
+ }
+ )
+}
+
export const featureFlagByOwnerId = (parent: RootDataLoader) => {
return new DataLoader<{ownerId: string; featureName: string}, boolean, string>(
async (keys) => {
diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts
index c7ec6cbb459..d463384f32d 100644
--- a/packages/server/graphql/public/mutations/generateInsight.ts
+++ b/packages/server/graphql/public/mutations/generateInsight.ts
@@ -1,4 +1,8 @@
+import {SubscriptionChannel} from 'parabol-client/types/constEnums'
+import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId'
import getKysely from '../../../postgres/getKysely'
+import {getUserId} from '../../../utils/authorization'
+import publish from '../../../utils/publish'
import standardError from '../../../utils/standardError'
import {MutationResolvers} from '../resolverTypes'
import {getSummaries} from './helpers/getSummaries'
@@ -7,8 +11,25 @@ import {getTopics} from './helpers/getTopics'
const generateInsight: MutationResolvers['generateInsight'] = async (
_source,
{teamId, startDate, endDate, useSummaries = true, prompt},
- {dataLoader}
+ context
) => {
+ const {dataLoader, socketId: mutatorId, authToken} = context
+ const viewerId = getUserId(authToken)
+ const teamMemberId = toTeamMemberId(teamId, viewerId)
+ const teamMember = await dataLoader.get('teamMembers').loadNonNull(teamMemberId)
+ const isLead = teamMember.isLead
+ if (!isLead) {
+ return standardError(new Error('Only team leads can generate insights'), {userId: viewerId})
+ }
+ const hasInsightsFlag = await dataLoader
+ .get('featureFlagByOwnerId')
+ .load({ownerId: teamId, featureName: 'insights'})
+ if (!hasInsightsFlag) {
+ return standardError(new Error('Insights are not enabled for this team'), {userId: viewerId})
+ }
+
+ const operationId = dataLoader.share()
+ const subOptions = {operationId, mutatorId}
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
return standardError(
new Error('Invalid date format. Please use ISO 8601 format (e.g., 2024-01-01T00:00:00Z).')
@@ -26,21 +47,32 @@ const generateInsight: MutationResolvers['generateInsight'] = async (
if ('error' in response) {
return response
}
- const {wins, challenges} = response
+ const {wins, challenges, meetingIds} = response
const pg = getKysely()
- await pg
+ const [insertedInsight] = await pg
.insertInto('Insight')
.values({
teamId,
wins,
challenges,
+ meetingsCount: meetingIds.length,
startDateTime: startDate,
endDateTime: endDate
})
+ .returning(['id'])
.execute()
- return response
+ if (!insertedInsight) {
+ return standardError(new Error('Failed to insert insight'))
+ }
+ const data = {
+ teamId
+ }
+
+ publish(SubscriptionChannel.TEAM, teamId, 'GenerateInsightSuccess', data, subOptions)
+
+ return data
}
export default generateInsight
diff --git a/packages/server/graphql/public/typeDefs/GenerateInsightPayload.graphql b/packages/server/graphql/public/typeDefs/GenerateInsightPayload.graphql
new file mode 100644
index 00000000000..13629c3d050
--- /dev/null
+++ b/packages/server/graphql/public/typeDefs/GenerateInsightPayload.graphql
@@ -0,0 +1,4 @@
+"""
+Return value for generateInsight, which could be an error
+"""
+union GenerateInsightPayload = ErrorPayload | GenerateInsightSuccess
diff --git a/packages/server/graphql/public/typeDefs/GenerateInsightSuccess.graphql b/packages/server/graphql/public/typeDefs/GenerateInsightSuccess.graphql
new file mode 100644
index 00000000000..b40ebf68e2d
--- /dev/null
+++ b/packages/server/graphql/public/typeDefs/GenerateInsightSuccess.graphql
@@ -0,0 +1,3 @@
+type GenerateInsightSuccess {
+ team: Team!
+}
diff --git a/packages/server/graphql/public/typeDefs/Insight.graphql b/packages/server/graphql/public/typeDefs/Insight.graphql
new file mode 100644
index 00000000000..3f5e4d55f33
--- /dev/null
+++ b/packages/server/graphql/public/typeDefs/Insight.graphql
@@ -0,0 +1,39 @@
+type Insight {
+ id: ID!
+ teamId: ID!
+
+ """
+ Start date and time of the insight period
+ """
+ startDateTime: DateTime!
+
+ """
+ End date and time of the insight period
+ """
+ endDateTime: DateTime!
+
+ """
+ List of AI generated wins
+ """
+ wins: [String!]!
+
+ """
+ List of AI generated challenges
+ """
+ challenges: [String!]!
+
+ """
+ The count of the meetings used to generate the insight
+ """
+ meetingsCount: Int!
+
+ """
+ Timestamp when the insight was created
+ """
+ createdAt: DateTime!
+
+ """
+ Timestamp when the insight was last updated
+ """
+ updatedAt: DateTime!
+}
diff --git a/packages/server/graphql/public/typeDefs/Mutation.graphql b/packages/server/graphql/public/typeDefs/Mutation.graphql
index b71bc91fe3c..a793fc21632 100644
--- a/packages/server/graphql/public/typeDefs/Mutation.graphql
+++ b/packages/server/graphql/public/typeDefs/Mutation.graphql
@@ -422,6 +422,17 @@ type Mutation {
invitees: [Email!]!
): InviteToTeamPayload!
+ """
+ Generate an insight for a team
+ """
+ generateInsight(
+ teamId: ID!
+ startDate: DateTime!
+ endDate: DateTime!
+ useSummaries: Boolean
+ prompt: String
+ ): GenerateInsightPayload!
+
"""
Move a template dimension
"""
diff --git a/packages/server/graphql/public/typeDefs/Team.graphql b/packages/server/graphql/public/typeDefs/Team.graphql
index 5bed3eae97e..dccbb9e7eb9 100644
--- a/packages/server/graphql/public/typeDefs/Team.graphql
+++ b/packages/server/graphql/public/typeDefs/Team.graphql
@@ -128,6 +128,11 @@ type Team {
): NewMeeting
organization: Organization!
+ """
+ The AI insight for the team. Null if not enough data or feature flag isn't set
+ """
+ insight: Insight
+
"""
The agenda items for the upcoming or current meeting
"""
@@ -191,4 +196,9 @@ type Team {
The team member that is the team lead
"""
teamLead: TeamMember!
+
+ """
+ The number of retro meetings the team has had
+ """
+ retroMeetingsCount: Int!
}
diff --git a/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql b/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql
index 98cafd8758b..5d988e06788 100644
--- a/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql
+++ b/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql
@@ -71,4 +71,5 @@ type TeamSubscriptionPayload {
UpdateRecurrenceSettingsSuccess: UpdateRecurrenceSettingsSuccess
UpdateDimensionFieldSuccess: UpdateDimensionFieldSuccess
UpdateTemplateCategorySuccess: UpdateTemplateCategorySuccess
+ GenerateInsightSuccess: GenerateInsightSuccess
}
diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql
deleted file mode 100644
index d07b656a8f0..00000000000
--- a/packages/server/graphql/public/typeDefs/generateInsight.graphql
+++ /dev/null
@@ -1,34 +0,0 @@
-extend type Mutation {
- """
- Generate an insight for a team
- """
- generateInsight(
- teamId: ID!
- startDate: DateTime!
- endDate: DateTime!
- useSummaries: Boolean
- prompt: String
- ): GenerateInsightPayload!
-}
-
-"""
-Return value for generateInsight, which could be an error
-"""
-union GenerateInsightPayload = ErrorPayload | GenerateInsightSuccess
-
-type GenerateInsightSuccess {
- """
- The insights generated focusing on the wins of the team
- """
- wins: [String!]!
-
- """
- The insights generated focusing on the challenges team are facing
- """
- challenges: [String!]!
-
- """
- The meetings that were used to generate the insights
- """
- meetings: [RetrospectiveMeeting!]!
-}
diff --git a/packages/server/graphql/public/types/GenerateInsightSuccess.ts b/packages/server/graphql/public/types/GenerateInsightSuccess.ts
index a22c5a7e1cd..53a3ddd4200 100644
--- a/packages/server/graphql/public/types/GenerateInsightSuccess.ts
+++ b/packages/server/graphql/public/types/GenerateInsightSuccess.ts
@@ -1,18 +1,12 @@
-import isValid from '../../isValid'
import {GenerateInsightSuccessResolvers} from '../resolverTypes'
export type GenerateInsightSuccessSource = {
- wins: string[]
- challenges: string[]
- meetingIds: string[]
+ teamId: string
}
const GenerateInsightSuccess: GenerateInsightSuccessResolvers = {
- wins: ({wins}) => wins,
- challenges: ({challenges}) => challenges,
- meetings: async ({meetingIds}, _args, {dataLoader}) => {
- const meetings = await dataLoader.get('newMeetings').loadMany(meetingIds)
- return meetings.filter(isValid).filter((m) => m.meetingType === 'retrospective')
+ team: async ({teamId}, _args, {dataLoader}) => {
+ return await dataLoader.get('teams').loadNonNull(teamId)
}
}
diff --git a/packages/server/graphql/public/types/Team.ts b/packages/server/graphql/public/types/Team.ts
index 8cd5ccbbae9..1c3975fd4dc 100644
--- a/packages/server/graphql/public/types/Team.ts
+++ b/packages/server/graphql/public/types/Team.ts
@@ -1,4 +1,5 @@
import TeamInsightsId from 'parabol-client/shared/gqlIds/TeamInsightsId'
+import {InsightId} from '../../../../client/shared/gqlIds/InsightId'
import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId'
import {getUserId, isTeamMember} from '../../../utils/authorization'
import {getFeatureTier} from '../../types/helpers/getFeatureTier'
@@ -55,6 +56,19 @@ const Team: TeamResolvers = {
const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId)
return teamMembers.find((teamMember) => teamMember.isLead)!
},
+ retroMeetingsCount: async ({id: teamId}, _args, {dataLoader}) => {
+ const meetings = await dataLoader.get('completedMeetingsByTeamId').load(teamId)
+ const retroMeetings = meetings.filter((meeting) => meeting.meetingType === 'retrospective')
+ return retroMeetings.length
+ },
+ insight: async ({id: teamId}, _args, {dataLoader}) => {
+ const insight = await dataLoader.get('latestInsightByTeamId').load(teamId)
+ if (!insight) return null
+ return {
+ ...insight,
+ id: InsightId.join(teamId, insight.id)
+ }
+ },
featureFlag: async ({id: teamId}, {featureName}, {dataLoader}) => {
return await dataLoader.get('featureFlagByOwnerId').load({ownerId: teamId, featureName})
}
diff --git a/packages/server/postgres/migrations/1728596433080_addMeetingsCountToInsight.ts b/packages/server/postgres/migrations/1728596433080_addMeetingsCountToInsight.ts
new file mode 100644
index 00000000000..015d54f428a
--- /dev/null
+++ b/packages/server/postgres/migrations/1728596433080_addMeetingsCountToInsight.ts
@@ -0,0 +1,22 @@
+import {Client} from 'pg'
+import getPgConfig from '../getPgConfig'
+
+export async function up() {
+ const client = new Client(getPgConfig())
+ await client.connect()
+ await client.query(`
+ ALTER TABLE "Insight"
+ ADD COLUMN "meetingsCount" INTEGER NOT NULL DEFAULT 0;
+ `)
+ await client.end()
+}
+
+export async function down() {
+ const client = new Client(getPgConfig())
+ await client.connect()
+ await client.query(`
+ ALTER TABLE "Insight"
+ DROP COLUMN IF EXISTS "meetingsCount";
+ `)
+ await client.end()
+}
diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts
index 587e2372564..11efe61389e 100644
--- a/packages/server/postgres/types/index.d.ts
+++ b/packages/server/postgres/types/index.d.ts
@@ -1,6 +1,7 @@
import {SelectQueryBuilder, Selectable} from 'kysely'
import {
Discussion as DiscussionPG,
+ Insight as InsightPG,
OrganizationUser as OrganizationUserPG,
TeamMember as TeamMemberPG
} from '../pg.d'
@@ -72,6 +73,7 @@ export type SlackNotification = ExtractTypeFromQueryBuilderSelect
export type ReflectPrompt = ExtractTypeFromQueryBuilderSelect
+export type Insight = Selectable
export type NewMeeting = ExtractTypeFromQueryBuilderSelect
export type NewFeature = ExtractTypeFromQueryBuilderSelect
diff --git a/static/images/illustrations/insights-empty-state.png b/static/images/illustrations/insights-empty-state.png
new file mode 100644
index 00000000000..f4f445c4fd5
Binary files /dev/null and b/static/images/illustrations/insights-empty-state.png differ