diff --git a/codegen.json b/codegen.json
index 53c0d8bdd28..27ff63a820f 100644
--- a/codegen.json
+++ b/codegen.json
@@ -13,11 +13,9 @@
"mappers": {
"AddFeatureFlagSuccess": "./types/AddFeatureFlagSuccess#AddFeatureFlagSuccessSource",
"ApplyFeatureFlagSuccess": "./types/ApplyFeatureFlagSuccess#ApplyFeatureFlagSuccessSource",
- "RemoveFeatureFlagOwnerSuccess": "./types/RemoveFeatureFlagOwnerSuccess#RemoveFeatureFlagOwnerSuccessSource",
- "DeleteFeatureFlagSuccess": "./types/DeleteFeatureFlagSuccess#DeleteFeatureFlagSuccessSource",
- "UpdateFeatureFlagSuccess": "./types/UpdateFeatureFlagSuccess#UpdateFeatureFlagSuccessSource",
"ChangeEmailDomainSuccess": "./types/ChangeEmailDomainSuccess#ChangeEmailDomainSuccessSource",
"Company": "./queries/company#CompanySource",
+ "DeleteFeatureFlagSuccess": "./types/DeleteFeatureFlagSuccess#DeleteFeatureFlagSuccessSource",
"DraftEnterpriseInvoicePayload": "./types/DraftEnterpriseInvoicePayload#DraftEnterpriseInvoicePayloadSource",
"EndTrialSuccess": "./types/EndTrialSuccess#EndTrialSuccessSource",
"File": "../public/types/File#TFile",
@@ -32,6 +30,7 @@
"PingableServices": "./types/PingableServices#PingableServicesSource",
"ProcessRecurrenceSuccess": "./types/ProcessRecurrenceSuccess#ProcessRecurrenceSuccessSource",
"RemoveAuthIdentitySuccess": "./types/RemoveAuthIdentitySuccess#RemoveAuthIdentitySuccessSource",
+ "RemoveFeatureFlagOwnerSuccess": "./types/RemoveFeatureFlagOwnerSuccess#RemoveFeatureFlagOwnerSuccessSource",
"RetrospectiveMeeting": "../../postgres/types/Meeting#RetrospectiveMeeting",
"SAML": "./types/SAML#SAMLSource",
"SetIsFreeMeetingTemplateSuccess": "./types/SetIsFreeMeetingTemplateSuccess#SetIsFreeMeetingTemplateSuccessSource",
@@ -39,6 +38,7 @@
"StartTrialSuccess": "./types/StartTrialSuccess#StartTrialSuccessSource",
"StripeFailPaymentPayload": "./mutations/stripeFailPayment#StripeFailPaymentPayloadSource",
"Team": "../../postgres/types/index#Team as TeamDB",
+ "UpdateFeatureFlagSuccess": "./types/UpdateFeatureFlagSuccess#UpdateFeatureFlagSuccessSource",
"UpgradeToTeamTierSuccess": "./mutations/upgradeToTeamTier#UpgradeToTeamTierSuccessSource",
"User": "../../postgres/types/IUser#default as IUser",
"VerifyDomainSuccess": "./types/VerifyDomainSuccess#VerifyDomainSuccessSource"
@@ -51,40 +51,19 @@
"config": {
"contextType": "../graphql#GQLContext",
"mappers": {
- "TaskEstimate": "../../postgres/types/index#TaskEstimate",
- "ReflectTemplatePromptUpdateDescriptionPayload": "./types/ReflectTemplatePromptUpdateDescriptionPayload#ReflectTemplatePromptUpdateDescriptionPayloadSource",
- "NewFeatureBroadcast": "../../postgres/types/index#NewFeature",
- "ReflectTemplatePromptUpdateGroupColorPayload": "./types/ReflectTemplatePromptUpdateGroupColorPayload#ReflectTemplatePromptUpdateGroupColorPayloadSource",
- "RemoveReflectTemplatePromptPayload": "./types/RemoveReflectTemplatePromptPayload#RemoveReflectTemplatePromptPayloadSource",
- "RenameReflectTemplatePromptPayload": "./types/RenameReflectTemplatePromptPayload#RenameReflectTemplatePromptPayloadSource",
- "MoveReflectTemplatePromptPayload": "./types/MoveReflectTemplatePromptPayload#MoveReflectTemplatePromptPayloadSource",
- "AddReflectTemplatePromptPayload": "./types/AddReflectTemplatePromptPayload#AddReflectTemplatePromptPayloadSource",
- "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource",
- "SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource",
- "AddCommentSuccess": "./types/AddCommentSuccess#AddCommentSuccessSource",
- "AddIntegrationProviderSuccess": "./types/AddIntegrationProviderSuccess#AddIntegrationProviderSuccessSource",
- "DeleteCommentSuccess": "./types/DeleteCommentSuccess#DeleteCommentSuccessSource",
- "UpdateCommentContentSuccess": "./types/UpdateCommentContentSuccess#UpdateCommentContentSuccessSource",
- "AddSlackAuthPayload": "./types/AddSlackAuthPayload#AddSlackAuthPayloadSource",
- "RemoveAgendaItemPayload": "./types/RemoveAgendaItemPayload#RemoveAgendaItemPayloadSource",
- "AddAgendaItemPayload": "./types/AddAgendaItemPayload#AddAgendaItemPayloadSource",
- "UpdateAgendaItemPayload": "./types/UpdateAgendaItemPayload#UpdateAgendaItemPayloadSource",
- "TeamMeetingSettings": "../../postgres/types/index#MeetingSettings as TeamMeetingSettingsDB",
- "PokerMeetingSettings": "../../postgres/types/index#PokerMeetingSettings as PokerMeetingSettingsDB",
- "RetrospectiveMeetingSettings": "../../postgres/types/index#RetrospectiveMeetingSettings as RetrospectiveMeetingSettingsDB",
- "RemovePokerTemplatePayload": "./types/RemovePokerTemplatePayload#RemovePokerTemplatePayloadSource",
- "JiraRemoteAvatarUrls": "./types/JiraRemoteAvatarUrls#JiraRemoteAvatarUrlsSource",
- "TemplateDimensionRef": "./types/TemplateDimensionRef#TemplateDimensionRefSource",
- "UpdateIntegrationProviderSuccess": "./types/UpdateIntegrationProviderSuccess#UpdateIntegrationProviderSuccessSource",
- "EndTeamPromptSuccess": "./types/EndTeamPromptSuccess#EndTeamPromptSuccessSource",
"AcceptRequestToJoinDomainSuccess": "./types/AcceptRequestToJoinDomainSuccess#AcceptRequestToJoinDomainSuccessSource",
"AcceptTeamInvitationPayload": "./types/AcceptTeamInvitationPayload#AcceptTeamInvitationPayloadSource",
"ActionMeeting": "../../postgres/types/Meeting#CheckInMeeting",
"ActionMeetingMember": "../../postgres/types/Meeting.d#ActionMeetingMember as ActionMeetingMemberDB",
+ "AddAgendaItemPayload": "./types/AddAgendaItemPayload#AddAgendaItemPayloadSource",
"AddApprovedOrganizationDomainsSuccess": "./types/AddApprovedOrganizationDomainsSuccess#AddApprovedOrganizationDomainsSuccessSource",
+ "AddCommentSuccess": "./types/AddCommentSuccess#AddCommentSuccessSource",
+ "AddIntegrationProviderSuccess": "./types/AddIntegrationProviderSuccess#AddIntegrationProviderSuccessSource",
"AddPokerTemplateSuccess": "./types/AddPokerTemplateSuccess#AddPokerTemplateSuccessSource",
"AddReactjiToReactableSuccess": "./types/AddReactjiToReactableSuccess#AddReactjiToReactableSuccessSource",
+ "AddReflectTemplatePromptPayload": "./types/AddReflectTemplatePromptPayload#AddReflectTemplatePromptPayloadSource",
"AddReflectTemplateSuccess": "./types/AddReflectTemplateSuccess#AddReflectTemplateSuccessSource",
+ "AddSlackAuthPayload": "./types/AddSlackAuthPayload#AddSlackAuthPayloadSource",
"AddTeamMemberIntegrationAuthSuccess": "./types/AddTeamMemberIntegrationAuthPayload#AddTeamMemberIntegrationAuthSuccessSource",
"AddTranscriptionBotSuccess": "./types/AddTranscriptionBotSuccess#AddTranscriptionBotSuccessSource",
"AddedNotification": "./types/AddedNotification#AddedNotificationSource",
@@ -103,8 +82,10 @@
"CreateImposterTokenPayload": "./types/CreateImposterTokenPayload#CreateImposterTokenPayloadSource",
"CreateStripeSubscriptionSuccess": "./types/CreateStripeSubscriptionSuccess#CreateStripeSubscriptionSuccessSource",
"CreateTaskPayload": "./types/CreateTaskPayload#CreateTaskPayloadSource",
+ "DeleteCommentSuccess": "./types/DeleteCommentSuccess#DeleteCommentSuccessSource",
"Discussion": "../../postgres/queries/generated/getDiscussionsByIdsQuery#IGetDiscussionsByIdsQueryResult",
"DomainJoinRequest": "../../database/types/DomainJoinRequest#default as DomainJoinRequestDB",
+ "EndTeamPromptSuccess": "./types/EndTeamPromptSuccess#EndTeamPromptSuccessSource",
"File": "./types/File#TFile",
"GcalIntegration": "./types/GcalIntegration#GcalIntegrationSource",
"GenerateGroupsSuccess": "./types/GenerateGroupsSuccess#GenerateGroupsSuccessSource",
@@ -117,6 +98,7 @@
"IntegrationProviderWebhook": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider",
"InviteToTeamPayload": "./types/InviteToTeamPayload#InviteToTeamPayloadSource",
"JiraIssue": "./types/JiraIssue#JiraIssueSource",
+ "JiraRemoteAvatarUrls": "./types/JiraRemoteAvatarUrls#JiraRemoteAvatarUrlsSource",
"JiraRemoteProject": "./types/JiraRemoteProject#JiraRemoteProjectSource",
"JiraSearchQuery": "../../database/types/JiraSearchQuery#default as JiraSearchQueryDB",
"JiraServerIntegration": "./types/JiraServerIntegration#JiraServerIntegrationSource",
@@ -126,6 +108,8 @@
"MattermostIntegration": "./types/MattermostIntegration#MattermostIntegrationSource",
"MeetingSeries": "../../postgres/types/MeetingSeries#MeetingSeries",
"MeetingTemplate": "../../database/types/MeetingTemplate#default",
+ "MoveReflectTemplatePromptPayload": "./types/MoveReflectTemplatePromptPayload#MoveReflectTemplatePromptPayloadSource",
+ "NewFeatureBroadcast": "../../postgres/types/index#NewFeature",
"NewMeeting": "../../postgres/types/Meeting#AnyMeeting",
"NewMeetingPhase": "./types/NewMeetingPhase#NewMeetingPhaseSource",
"NewMeetingStage": "./types/NewMeetingStage#NewMeetingStageSource",
@@ -137,44 +121,49 @@
"NotifyMentioned": "../../postgres/types/Notification.d#MentionedNotification as MentionedNotificationDB",
"NotifyPaymentRejected": "../../postgres/types/Notification.d#PaymentRejectedNotification as PaymentRejectedNotificationDB",
"NotifyPromoteToOrgLeader": "../../postgres/types/Notification.d#PromoteToBillingLeaderNotification as PromoteToBillingLeaderNotificationDB",
+ "NotifyPromptToJoinOrg": "../../postgres/types/Notification.d#PromptToJoinOrgNotification as PromptToJoinOrgNotificationDB",
"NotifyRequestToJoinOrg": "../../postgres/types/Notification.d#RequestToJoinOrgNotification as RequestToJoinOrgNotificationDB",
"NotifyResponseMentioned": "../../postgres/types/Notification.d#ResponseMentionedNotification as ResponseMentionedNotificationDB",
"NotifyResponseReplied": "../../postgres/types/Notification.d#ResponseRepliedNotification as ResponseRepliedNotificationDB",
"NotifyTaskInvolves": "../../postgres/types/Notification.d#TaskInvolvesNotification as TaskInvolvesNotificationDB",
"NotifyTeamArchived": "../../postgres/types/Notification.d#TeamArchivedNotification as TeamArchivedNotificationDB",
- "NotifyTeamsLimitReminder": "../../postgres/types/Notification.d#TeamsLimitReminderNotification as TeamsLimitReminderNotificationDB",
"NotifyTeamsLimitExceeded": "../../postgres/types/Notification.d#TeamsLimitExceededNotification as TeamsLimitExceededNotificationDB",
- "NotifyPromptToJoinOrg": "../../postgres/types/Notification.d#PromptToJoinOrgNotification as PromptToJoinOrgNotificationDB",
- "Organization": "../../postgres/types/index#Organization as OrganizationDB",
- "TemplateScaleValue": "./types/TemplateScaleValue#TemplateScaleValueSource as TemplateScaleValueSourceDB",
- "SuggestedAction": "../../postgres/types/index#SuggestedAction as SuggestedActionDB",
- "TemplateScale": "../../postgres/types/index#TemplateScale as TemplateScaleDB",
- "TemplateScaleRef": "../../postgres/types/index#TemplateScaleRef as TemplateScaleRefDB",
- "Threadable": "./types/Threadable#ThreadableSource",
+ "NotifyTeamsLimitReminder": "../../postgres/types/Notification.d#TeamsLimitReminderNotification as TeamsLimitReminderNotificationDB",
"OrgIntegrationProviders": "./types/OrgIntegrationProviders#OrgIntegrationProvidersSource",
+ "Organization": "../../postgres/types/index#Organization as OrganizationDB",
"OrganizationUser": "../../postgres/types/index#OrganizationUser as OrganizationUserDB",
"PokerMeeting": "../../postgres/types/Meeting#PokerMeeting",
"PokerMeetingMember": "../../postgres/types/Meeting.d#PokerMeetingMember as PokerMeetingMemberDB",
+ "PokerMeetingSettings": "../../postgres/types/index#PokerMeetingSettings as PokerMeetingSettingsDB",
"PokerTemplate": "../../database/types/PokerTemplate#default as PokerTemplateDB",
"RRule": "rrule-rust#RRuleSet",
"Reactable": "../../database/types/Reactable#Reactable",
"Reactji": "../types/Reactji#ReactjiSource",
"ReflectPrompt": "../../postgres/types/index#ReflectPrompt",
"ReflectTemplate": "../../database/types/ReflectTemplate#default",
+ "ReflectTemplatePromptUpdateDescriptionPayload": "./types/ReflectTemplatePromptUpdateDescriptionPayload#ReflectTemplatePromptUpdateDescriptionPayloadSource",
+ "ReflectTemplatePromptUpdateGroupColorPayload": "./types/ReflectTemplatePromptUpdateGroupColorPayload#ReflectTemplatePromptUpdateGroupColorPayloadSource",
+ "RemoveAgendaItemPayload": "./types/RemoveAgendaItemPayload#RemoveAgendaItemPayloadSource",
"RemoveApprovedOrganizationDomainsSuccess": "./types/RemoveApprovedOrganizationDomainsSuccess#RemoveApprovedOrganizationDomainsSuccessSource",
"RemoveIntegrationSearchQuerySuccess": "./types/RemoveIntegrationSearchQuerySuccess#RemoveIntegrationSearchQuerySuccessSource",
+ "RemovePokerTemplatePayload": "./types/RemovePokerTemplatePayload#RemovePokerTemplatePayloadSource",
+ "RemoveReflectTemplatePromptPayload": "./types/RemoveReflectTemplatePromptPayload#RemoveReflectTemplatePromptPayloadSource",
"RemoveTeamMemberIntegrationAuthSuccess": "./types/RemoveTeamMemberIntegrationAuthPayload#RemoveTeamMemberIntegrationAuthSuccessSource",
"RemoveTeamMemberPayload": "./types/RemoveTeamMemberPayload#RemoveTeamMemberPayloadSource",
+ "RenameReflectTemplatePromptPayload": "./types/RenameReflectTemplatePromptPayload#RenameReflectTemplatePromptPayloadSource",
"RequestToJoinDomainSuccess": "./types/RequestToJoinDomainSuccess#RequestToJoinDomainSuccessSource",
"ResetReflectionGroupsSuccess": "./types/ResetReflectionGroupsSuccess#ResetReflectionGroupsSuccessSource",
"RetroReflection": "../../postgres/types/index#RetroReflection as RetroReflectionDB",
"RetroReflectionGroup": "./types/RetroReflectionGroup#RetroReflectionGroupSource",
"RetrospectiveMeeting": "../../postgres/types/Meeting#RetrospectiveMeeting",
"RetrospectiveMeetingMember": "../../postgres/types/Meeting.d#RetroMeetingMember as RetroMeetingMemberDB",
+ "RetrospectiveMeetingSettings": "../../postgres/types/index#RetrospectiveMeetingSettings as RetrospectiveMeetingSettingsDB",
"SAML": "./types/SAML#SAMLSource",
+ "SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource",
"SetMeetingSettingsPayload": "../types/SetMeetingSettingsPayload#SetMeetingSettingsPayloadSource",
"SetNotificationStatusPayload": "./types/SetNotificationStatusPayload#SetNotificationStatusPayloadSource",
"SetOrgUserRoleSuccess": "./types/SetOrgUserRoleSuccess#SetOrgUserRoleSuccessSource",
+ "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource",
"ShareTopicSuccess": "./types/ShareTopicSuccess#ShareTopicSuccessSource",
"SlackIntegration": "../../postgres/types/index#SlackAuth as SlackAuthDB",
"SlackNotification": "../../postgres/types/index#SlackNotification as SlackNotificationDB",
@@ -182,11 +171,14 @@
"StartRetrospectiveSuccess": "./types/StartRetrospectiveSuccess#StartRetrospectiveSuccessSource",
"StartTeamPromptSuccess": "./types/StartTeamPromptSuccess#StartTeamPromptSuccessSource",
"StripeFailPaymentPayload": "./types/StripeFailPaymentPayload#StripeFailPaymentPayloadSource",
+ "SuggestedAction": "../../postgres/types/index#SuggestedAction as SuggestedActionDB",
"Task": "../../postgres/types/index#Task as TaskDB",
+ "TaskEstimate": "../../postgres/types/index#TaskEstimate",
"Team": "../../postgres/types/index#Team as TeamDB",
"TeamHealthPhase": "./types/TeamHealthPhase#TeamHealthPhaseSource",
"TeamHealthStage": "./types/TeamHealthStage#TeamHealthStageSource",
"TeamInvitation": "../../postgres/types/index/#TeamInvitation",
+ "TeamMeetingSettings": "../../postgres/types/index#MeetingSettings as TeamMeetingSettingsDB",
"TeamMember": "../../postgres/types/index#TeamMember as TeamMember",
"TeamMemberIntegrationAuthOAuth1": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth",
"TeamMemberIntegrationAuthOAuth2": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth",
@@ -197,13 +189,23 @@
"TeamPromptMeetingSettings": "../../postgres/types/index#MeetingSettings as TeamMeetingSettingsDB",
"TeamPromptResponse": "../../postgres/types/index#TeamPromptResponse as TeamPromptResponseDB",
"TemplateDimension": "../../postgres/types/index#TemplateDimension as TemplateDimensionDB",
+ "TemplateDimensionRef": "./types/TemplateDimensionRef#TemplateDimensionRefSource",
+ "TemplateScale": "../../postgres/types/index#TemplateScale as TemplateScaleDB",
+ "TemplateScaleRef": "../../postgres/types/index#TemplateScaleRef as TemplateScaleRefDB",
+ "TemplateScaleValue": "./types/TemplateScaleValue#TemplateScaleValueSource as TemplateScaleValueSourceDB",
+ "Threadable": "./types/Threadable#ThreadableSource",
"TimelineEventTeamPromptComplete": "./types/TimelineEventTeamPromptComplete#TimelineEventTeamPromptCompleteSource",
+ "ToggleAIFeaturesSuccess": "./types/ToggleAIFeaturesSuccess#ToggleAIFeaturesSuccessSource",
"ToggleFavoriteTemplateSuccess": "./types/ToggleFavoriteTemplateSuccess#ToggleFavoriteTemplateSuccessSource",
+ "ToggleFeatureFlagSuccess": "./types/ToggleFeatureFlagSuccess#ToggleFeatureFlagSuccessSource",
"ToggleSummaryEmailSuccess": "./types/ToggleSummaryEmailSuccess#ToggleSummaryEmailSuccessSource",
+ "UpdateAgendaItemPayload": "./types/UpdateAgendaItemPayload#UpdateAgendaItemPayloadSource",
"UpdateAutoJoinSuccess": "./types/UpdateAutoJoinSuccess#UpdateAutoJoinSuccessSource",
+ "UpdateCommentContentSuccess": "./types/UpdateCommentContentSuccess#UpdateCommentContentSuccessSource",
"UpdateCreditCardSuccess": "./types/UpdateCreditCardSuccess#UpdateCreditCardSuccessSource",
"UpdateDimensionFieldSuccess": "./types/UpdateDimensionFieldSuccess#UpdateDimensionFieldSuccessSource",
"UpdateGitLabDimensionFieldSuccess": "./types/UpdateGitLabDimensionFieldSuccess#UpdateGitLabDimensionFieldSuccessSource",
+ "UpdateIntegrationProviderSuccess": "./types/UpdateIntegrationProviderSuccess#UpdateIntegrationProviderSuccessSource",
"UpdateMeetingPromptSuccess": "./types/UpdateMeetingPromptSuccess#UpdateMeetingPromptSuccessSource",
"UpdateMeetingTemplateSuccess": "./types/UpdateMeetingTemplateSuccess#UpdateMeetingTemplateSuccessSource",
"UpdateOrgPayload": "./types/UpdateOrgPayload#UpdateOrgPayloadSource",
diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummary.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummary.tsx
index 4d98482b5f4..84af160d835 100644
--- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummary.tsx
+++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummary.tsx
@@ -10,7 +10,7 @@ interface Props {
}
const isServer = typeof window === 'undefined'
-const hasAI = isServer
+const hasAiApiKey = isServer
? !!process.env.OPEN_AI_API_KEY
: !!window.__ACTION__ && !!window.__ACTION__.hasOpenAI
@@ -25,7 +25,7 @@ const WholeMeetingSummary = (props: Props) => {
summary
organization {
hasStandupAISummaryFlag: featureFlag(featureName: "standupAISummary")
- hasNoAISummaryFlag: featureFlag(featureName: "noAISummary")
+ useAI
}
... on RetrospectiveMeeting {
reflectionGroups(sortBy: voteCount) {
@@ -47,15 +47,16 @@ const WholeMeetingSummary = (props: Props) => {
const {summary: wholeMeetingSummary, reflectionGroups, organization} = meeting
const reflections = reflectionGroups?.flatMap((group) => group.reflections) // reflectionCount hasn't been calculated yet so check reflections length
const hasMoreThanOneReflection = reflections?.length && reflections.length > 1
- if (!hasMoreThanOneReflection || organization.hasNoAISummaryFlag || !hasAI) return null
+ if (!hasMoreThanOneReflection || !organization.useAI || !hasAiApiKey) return null
if (!wholeMeetingSummary) return
return
} else if (meeting.__typename === 'TeamPromptMeeting') {
const {summary: wholeMeetingSummary, responses, organization} = meeting
+ const {hasStandupAISummaryFlag, useAI} = organization
if (
- !organization.hasStandupAISummaryFlag ||
- organization.hasNoAISummaryFlag ||
- !hasAI ||
+ !hasStandupAISummaryFlag ||
+ !useAI ||
+ !hasAiApiKey ||
!responses ||
responses.length === 0
) {
diff --git a/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx b/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx
index a7b1294e39c..a4606299f98 100644
--- a/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx
+++ b/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx
@@ -87,7 +87,7 @@ const NewCheckInQuestion = (props: Props) => {
}
team {
organization {
- hasNoAISummaryFlag: featureFlag(featureName: "noAISummary")
+ useAI
}
}
}
@@ -101,7 +101,7 @@ const NewCheckInQuestion = (props: Props) => {
localPhase,
facilitatorUserId,
team: {
- organization: {hasNoAISummaryFlag}
+ organization: {useAI}
}
} = meeting
const {checkInQuestion} = localPhase
@@ -226,7 +226,7 @@ const NewCheckInQuestion = (props: Props) => {
}
})
}
- const showAiIcebreaker = !hasNoAISummaryFlag && isFacilitating && window.__ACTION__.hasOpenAI
+ const showAiIcebreaker = useAI && isFacilitating && window.__ACTION__.hasOpenAI
return (
<>
diff --git a/packages/client/modules/userDashboard/components/OrgBilling/OrgDetails.tsx b/packages/client/modules/userDashboard/components/OrgBilling/OrgDetails.tsx
index 6784b5bb14b..eeaf0bbb63b 100644
--- a/packages/client/modules/userDashboard/components/OrgBilling/OrgDetails.tsx
+++ b/packages/client/modules/userDashboard/components/OrgBilling/OrgDetails.tsx
@@ -10,6 +10,8 @@ import useModal from '../../../../hooks/useModal'
import defaultOrgAvatar from '../../../../styles/theme/images/avatar-organization.svg'
import OrganizationDetails from '../Organization/OrganizationDetails'
import OrgBillingDangerZone from './OrgBillingDangerZone'
+import OrgFeatureFlags from './OrgFeatureFlags'
+import OrgFeatures from './OrgFeatures'
type Props = {
organizationRef: OrgDetails_organization$key
@@ -22,6 +24,8 @@ const OrgDetails = (props: Props) => {
fragment OrgDetails_organization on Organization {
...OrgBillingDangerZone_organization
...EditableOrgName_organization
+ ...OrgFeatureFlags_organization
+ ...OrgFeatures_organization
orgId: id
isBillingLeader
createdAt
@@ -66,6 +70,9 @@ const OrgDetails = (props: Props) => {
+
+
+
)
diff --git a/packages/client/modules/userDashboard/components/OrgBilling/OrgFeatureFlags.tsx b/packages/client/modules/userDashboard/components/OrgBilling/OrgFeatureFlags.tsx
new file mode 100644
index 00000000000..1be1901bb02
--- /dev/null
+++ b/packages/client/modules/userDashboard/components/OrgBilling/OrgFeatureFlags.tsx
@@ -0,0 +1,111 @@
+import styled from '@emotion/styled'
+import {Info as InfoIcon} from '@mui/icons-material'
+import graphql from 'babel-plugin-relay/macro'
+import React from 'react'
+import {useFragment} from 'react-relay'
+import {OrgFeatureFlags_organization$key} from '../../../../__generated__/OrgFeatureFlags_organization.graphql'
+import Panel from '../../../../components/Panel/Panel'
+import Toggle from '../../../../components/Toggle/Toggle'
+import useAtmosphere from '../../../../hooks/useAtmosphere'
+import useMutationProps from '../../../../hooks/useMutationProps'
+import ToggleFeatureFlagMutation from '../../../../mutations/ToggleFeatureFlagMutation'
+import {PALETTE} from '../../../../styles/paletteV3'
+import {ElementWidth, Layout} from '../../../../types/constEnums'
+import {Tooltip} from '../../../../ui/Tooltip/Tooltip'
+import {TooltipContent} from '../../../../ui/Tooltip/TooltipContent'
+import {TooltipTrigger} from '../../../../ui/Tooltip/TooltipTrigger'
+
+const StyledPanel = styled(Panel)<{isWide: boolean}>(({isWide}) => ({
+ maxWidth: isWide ? ElementWidth.PANEL_WIDTH : 'inherit'
+}))
+
+const PanelRow = styled('div')({
+ borderTop: `1px solid ${PALETTE.SLATE_300}`,
+ padding: Layout.ROW_GUTTER
+})
+
+const FeatureRow = styled('div')({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 8
+})
+
+const FeatureNameGroup = styled('div')({
+ display: 'flex',
+ alignItems: 'center',
+ gap: 4,
+ '& svg': {
+ display: 'block'
+ }
+})
+
+// TODO: create a migration that updates featureName to be a readable string
+// then update the references throughout the app and remove this
+const FEATURE_NAME_LOOKUP: Record = {
+ insights: 'Team Insights',
+ publicTeams: 'Public Teams',
+ relatedDiscussions: 'Related Discussions',
+ standupAISummary: 'Standup AI Summary',
+ suggestGroups: 'AI Reflection Group Suggestions'
+}
+
+interface Props {
+ organizationRef: OrgFeatureFlags_organization$key
+}
+
+const OrgFeatureFlags = (props: Props) => {
+ const {organizationRef} = props
+ const atmosphere = useAtmosphere()
+ const {onError, onCompleted} = useMutationProps()
+ const organization = useFragment(
+ graphql`
+ fragment OrgFeatureFlags_organization on Organization {
+ id
+ isOrgAdmin
+ orgFeatureFlags {
+ featureName
+ description
+ enabled
+ }
+ }
+ `,
+ organizationRef
+ )
+ const {isOrgAdmin} = organization
+
+ const handleToggle = async (featureName: string) => {
+ const variables = {
+ featureName,
+ orgId: organization.id
+ }
+ ToggleFeatureFlagMutation(atmosphere, variables, {
+ onError,
+ onCompleted
+ })
+ }
+
+ if (!isOrgAdmin) return null
+ return (
+
+
+ {organization.orgFeatureFlags.map((feature) => (
+
+
+ {FEATURE_NAME_LOOKUP[feature.featureName] || feature.featureName}
+
+
+
+
+ {feature.description}
+
+
+ handleToggle(feature.featureName)} />
+
+ ))}
+
+
+ )
+}
+
+export default OrgFeatureFlags
diff --git a/packages/client/modules/userDashboard/components/OrgBilling/OrgFeatures.tsx b/packages/client/modules/userDashboard/components/OrgBilling/OrgFeatures.tsx
new file mode 100644
index 00000000000..c9937a882cb
--- /dev/null
+++ b/packages/client/modules/userDashboard/components/OrgBilling/OrgFeatures.tsx
@@ -0,0 +1,92 @@
+import styled from '@emotion/styled'
+import {Info as InfoIcon} from '@mui/icons-material'
+import graphql from 'babel-plugin-relay/macro'
+import React from 'react'
+import {useFragment} from 'react-relay'
+import {OrgFeatures_organization$key} from '../../../../__generated__/OrgFeatures_organization.graphql'
+import Panel from '../../../../components/Panel/Panel'
+import Toggle from '../../../../components/Toggle/Toggle'
+import useAtmosphere from '../../../../hooks/useAtmosphere'
+import useMutationProps from '../../../../hooks/useMutationProps'
+import ToggleAIFeaturesMutation from '../../../../mutations/ToggleAIFeaturesMutation'
+import {PALETTE} from '../../../../styles/paletteV3'
+import {ElementWidth, Layout} from '../../../../types/constEnums'
+import {Tooltip} from '../../../../ui/Tooltip/Tooltip'
+import {TooltipContent} from '../../../../ui/Tooltip/TooltipContent'
+import {TooltipTrigger} from '../../../../ui/Tooltip/TooltipTrigger'
+
+const StyledPanel = styled(Panel)<{isWide: boolean}>(({isWide}) => ({
+ maxWidth: isWide ? ElementWidth.PANEL_WIDTH : 'inherit'
+}))
+
+const PanelRow = styled('div')({
+ borderTop: `1px solid ${PALETTE.SLATE_300}`,
+ padding: Layout.ROW_GUTTER
+})
+
+const FeatureRow = styled('div')({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 8
+})
+
+const FeatureNameGroup = styled('div')({
+ display: 'flex',
+ alignItems: 'center',
+ gap: 4,
+ '& svg': {
+ display: 'block'
+ }
+})
+
+interface Props {
+ organizationRef: OrgFeatures_organization$key
+}
+
+const OrgFeatures = (props: Props) => {
+ const {organizationRef} = props
+ const atmosphere = useAtmosphere()
+ const {onError, onCompleted} = useMutationProps()
+ const organization = useFragment(
+ graphql`
+ fragment OrgFeatures_organization on Organization {
+ id
+ isOrgAdmin
+ useAI
+ }
+ `,
+ organizationRef
+ )
+ const {id: orgId, isOrgAdmin, useAI} = organization
+
+ const handleToggle = () => {
+ const variables = {orgId}
+ ToggleAIFeaturesMutation(atmosphere, variables, {
+ onError,
+ onCompleted
+ })
+ }
+
+ if (!isOrgAdmin) return null
+ return (
+
+
+
+
+ Enable AI Features
+
+
+
+
+ Enable AI-powered features across your organization
+
+
+
+
+
+
+ )
+}
+
+export default OrgFeatures
diff --git a/packages/client/mutations/EndRetrospectiveMutation.ts b/packages/client/mutations/EndRetrospectiveMutation.ts
index 2f354021a96..9affc761b1f 100644
--- a/packages/client/mutations/EndRetrospectiveMutation.ts
+++ b/packages/client/mutations/EndRetrospectiveMutation.ts
@@ -32,7 +32,7 @@ graphql`
groupTitle
}
organization {
- hasNoAISummaryFlag: featureFlag(featureName: "noAISummary")
+ useAI
}
reflectionGroups(sortBy: voteCount) {
reflections {
@@ -125,7 +125,7 @@ export const endRetrospectiveTeamOnNext: OnNextHandler<
const reflections = reflectionGroups.flatMap((group) => group.reflections) // reflectionCount hasn't been calculated yet so check reflections length
const hasMoreThanOneReflection = reflections.length > 1
const hasOpenAISummary =
- hasMoreThanOneReflection && !organization.hasNoAISummaryFlag && window.__ACTION__.hasOpenAI
+ hasMoreThanOneReflection && organization.useAI && window.__ACTION__.hasOpenAI
const hasTeamHealth = phases.some((phase) => phase.phaseType === 'TEAM_HEALTH')
const pathname = `/new-summary/${meetingId}`
const search = new URLSearchParams()
diff --git a/packages/client/mutations/ToggleAIFeaturesMutation.ts b/packages/client/mutations/ToggleAIFeaturesMutation.ts
new file mode 100644
index 00000000000..c86b843218c
--- /dev/null
+++ b/packages/client/mutations/ToggleAIFeaturesMutation.ts
@@ -0,0 +1,41 @@
+import graphql from 'babel-plugin-relay/macro'
+import {commitMutation} from 'react-relay'
+import {ToggleAIFeaturesMutation as TToggleAIFeaturesMutation} from '../__generated__/ToggleAIFeaturesMutation.graphql'
+import {StandardMutation} from '../types/relayMutations'
+
+graphql`
+ fragment ToggleAIFeaturesMutation_organization on ToggleAIFeaturesSuccess {
+ organization {
+ id
+ useAI
+ }
+ }
+`
+
+const mutation = graphql`
+ mutation ToggleAIFeaturesMutation($orgId: ID!) {
+ toggleAIFeatures(orgId: $orgId) {
+ ... on ErrorPayload {
+ error {
+ message
+ }
+ }
+ ...ToggleAIFeaturesMutation_organization @relay(mask: false)
+ }
+ }
+`
+
+const ToggleAIFeaturesMutation: StandardMutation = (
+ atmosphere,
+ variables,
+ {onError, onCompleted}
+) => {
+ return commitMutation(atmosphere, {
+ mutation,
+ variables,
+ onCompleted,
+ onError
+ })
+}
+
+export default ToggleAIFeaturesMutation
diff --git a/packages/client/mutations/ToggleFeatureFlagMutation.ts b/packages/client/mutations/ToggleFeatureFlagMutation.ts
new file mode 100644
index 00000000000..33e1123f4e0
--- /dev/null
+++ b/packages/client/mutations/ToggleFeatureFlagMutation.ts
@@ -0,0 +1,41 @@
+import graphql from 'babel-plugin-relay/macro'
+import {commitMutation} from 'react-relay'
+import {ToggleFeatureFlagMutation as TToggleFeatureFlagMutation} from '../__generated__/ToggleFeatureFlagMutation.graphql'
+import {StandardMutation} from '../types/relayMutations'
+
+graphql`
+ fragment ToggleFeatureFlagMutation_notification on ToggleFeatureFlagSuccess {
+ featureFlag {
+ featureName
+ enabled
+ }
+ }
+`
+
+const mutation = graphql`
+ mutation ToggleFeatureFlagMutation($featureName: String!, $orgId: ID, $teamId: ID, $userId: ID) {
+ toggleFeatureFlag(featureName: $featureName, orgId: $orgId, teamId: $teamId, userId: $userId) {
+ ... on ErrorPayload {
+ error {
+ message
+ }
+ }
+ ...ToggleFeatureFlagMutation_notification @relay(mask: false)
+ }
+ }
+`
+
+const ToggleFeatureFlagMutation: StandardMutation = (
+ atmosphere,
+ variables,
+ {onError, onCompleted}
+) => {
+ return commitMutation(atmosphere, {
+ mutation,
+ variables,
+ onCompleted,
+ onError
+ })
+}
+
+export default ToggleFeatureFlagMutation
diff --git a/packages/client/subscriptions/NotificationSubscription.ts b/packages/client/subscriptions/NotificationSubscription.ts
index 2fa341d18c7..a51a7ac4c78 100644
--- a/packages/client/subscriptions/NotificationSubscription.ts
+++ b/packages/client/subscriptions/NotificationSubscription.ts
@@ -167,6 +167,13 @@ const subscription = graphql`
summary
descriptionHTML
}
+
+ ToggleFeatureFlagSuccess {
+ featureFlag {
+ featureName
+ enabled
+ }
+ }
}
}
`
diff --git a/packages/server/database/types/Organization.ts b/packages/server/database/types/Organization.ts
index 8734b1c140f..a0afd5d3b4e 100644
--- a/packages/server/database/types/Organization.ts
+++ b/packages/server/database/types/Organization.ts
@@ -15,6 +15,7 @@ interface Input {
updatedAt?: Date
showConversionModal?: boolean
payLaterClickCount?: number
+ useAI?: boolean
}
export default class Organization {
@@ -37,6 +38,7 @@ export default class Organization {
trialStartDate?: Date | null
scheduledLockAt?: Date | null
lockedAt?: Date | null
+ useAI: boolean
updatedAt: Date
constructor(input: Input) {
const {
@@ -50,7 +52,8 @@ export default class Organization {
showConversionModal,
payLaterClickCount,
picture,
- tier
+ tier,
+ useAI
} = input
this.id = id || generateUID()
this.activeDomain = activeDomain
@@ -63,5 +66,6 @@ export default class Organization {
this.picture = picture
this.showConversionModal = showConversionModal === null ? undefined : showConversionModal
this.payLaterClickCount = payLaterClickCount || 0
+ this.useAI = useAI ?? true
}
}
diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts
index da562a0416c..365f317ce0e 100644
--- a/packages/server/dataloader/customLoaderMakers.ts
+++ b/packages/server/dataloader/customLoaderMakers.ts
@@ -27,7 +27,14 @@ import {
selectTasks,
selectTeams
} from '../postgres/select'
-import {Insight, MeetingSettings, OrganizationUser, Task, Team} from '../postgres/types'
+import {
+ FeatureFlag,
+ Insight,
+ MeetingSettings,
+ OrganizationUser,
+ Task,
+ Team
+} from '../postgres/types'
import {AnyMeeting, MeetingTypeEnum} from '../postgres/types/Meeting'
import {TeamMeetingTemplate} from '../postgres/types/pg'
import {Logger} from '../utils/Logger'
@@ -37,6 +44,7 @@ import NullableDataLoader from './NullableDataLoader'
import RootDataLoader, {RegisterDependsOn} from './RootDataLoader'
import normalizeArrayResults from './normalizeArrayResults'
import normalizeResults from './normalizeResults'
+
export interface MeetingSettingsKey {
teamId: string
meetingType: MeetingTypeEnum
@@ -852,6 +860,7 @@ export const latestInsightByTeamId = (parent: RootDataLoader) => {
)
}
+// whether a feature flag is enabled for a given owner (user, team, or org)
export const featureFlagByOwnerId = (parent: RootDataLoader) => {
return new DataLoader<{ownerId: string; featureName: string}, boolean, string>(
async (keys) => {
@@ -955,3 +964,64 @@ export const publicTemplatesByType = (parent: RootDataLoader) => {
}
)
}
+
+export const allFeatureFlags = (parent: RootDataLoader) => {
+ return new DataLoader<'Organization' | 'Team' | 'User' | 'all', FeatureFlag[], string>(
+ async (scopes) => {
+ const pg = getKysely()
+ return await Promise.all(
+ scopes.map(async (scope) => {
+ const flags = await pg
+ .selectFrom('FeatureFlag')
+ .selectAll()
+ .where('expiresAt', '>', new Date())
+ .$if(scope !== 'all', (qb) => {
+ const validScope = scope as 'Organization' | 'Team' | 'User'
+ return qb.where('scope', '=', validScope)
+ })
+ .orderBy('featureName')
+ .execute()
+ return flags.map((flag) => ({...flag, isEnabled: true}))
+ })
+ )
+ },
+ {
+ ...parent.dataLoaderOptions,
+ cacheKeyFn: (scope) => scope
+ }
+ )
+}
+
+export const allFeatureFlagsByOwner = (parent: RootDataLoader) => {
+ return new DataLoader<
+ {ownerId: string; scope: 'Organization' | 'Team' | 'User'},
+ FeatureFlag[],
+ string
+ >(
+ async (keys) => {
+ const flagsByOwnerId = await Promise.all(
+ keys.map(async ({ownerId, scope}) => {
+ const allFlags = await parent.get('allFeatureFlags').load(scope)
+ const flags = await Promise.all(
+ allFlags.map(async (flag) => {
+ const isEnabled = await parent
+ .get('featureFlagByOwnerId')
+ .load({ownerId, featureName: flag.featureName})
+ return {
+ ...flag,
+ enabled: isEnabled
+ }
+ })
+ )
+ return flags
+ })
+ )
+
+ return flagsByOwnerId
+ },
+ {
+ ...parent.dataLoaderOptions,
+ cacheKeyFn: (key) => `${key.ownerId}:${key.scope}`
+ }
+ )
+}
diff --git a/packages/server/graphql/mutations/helpers/canAccessAISummary.ts b/packages/server/graphql/mutations/helpers/canAccessAI.ts
similarity index 63%
rename from packages/server/graphql/mutations/helpers/canAccessAISummary.ts
rename to packages/server/graphql/mutations/helpers/canAccessAI.ts
index 3675b02804f..f262721ca26 100644
--- a/packages/server/graphql/mutations/helpers/canAccessAISummary.ts
+++ b/packages/server/graphql/mutations/helpers/canAccessAI.ts
@@ -3,19 +3,15 @@ import {Team} from '../../../postgres/types'
import {DataLoaderWorker} from '../../graphql'
import {getFeatureTier} from '../../types/helpers/getFeatureTier'
-const canAccessAISummary = async (
+const canAccessAI = async (
team: Team,
- userId: string,
meetingType: 'standup' | 'retrospective',
dataLoader: DataLoaderWorker
) => {
const {qualAIMeetingsCount, orgId} = team
- const [noAIOrgSummary, noAIUserSummary] = await Promise.all([
- dataLoader.get('featureFlagByOwnerId').load({ownerId: orgId, featureName: 'noAISummary'}),
- dataLoader.get('featureFlagByOwnerId').load({ownerId: userId, featureName: 'noAISummary'})
- ])
+ const org = await dataLoader.get('organizations').loadNonNull(orgId)
- if (noAIOrgSummary || noAIUserSummary) return false
+ if (!org.useAI) return false
if (meetingType === 'standup') {
const hasStandupFlag = await dataLoader
.get('featureFlagByOwnerId')
@@ -27,4 +23,4 @@ const canAccessAISummary = async (
return qualAIMeetingsCount < Threshold.MAX_QUAL_AI_MEETINGS
}
-export default canAccessAISummary
+export default canAccessAI
diff --git a/packages/server/graphql/mutations/helpers/generateDiscussionPrompt.ts b/packages/server/graphql/mutations/helpers/generateDiscussionPrompt.ts
index 586bd7bf350..7a6fe3f59ba 100644
--- a/packages/server/graphql/mutations/helpers/generateDiscussionPrompt.ts
+++ b/packages/server/graphql/mutations/helpers/generateDiscussionPrompt.ts
@@ -2,7 +2,7 @@ import getKysely from '../../../postgres/getKysely'
import OpenAIServerManager from '../../../utils/OpenAIServerManager'
import sendToSentry from '../../../utils/sendToSentry'
import {DataLoaderWorker} from '../../graphql'
-import canAccessAISummary from './canAccessAISummary'
+import canAccessAI from './canAccessAI'
const generateDiscussionPrompt = async (
meetingId: string,
@@ -14,13 +14,8 @@ const generateDiscussionPrompt = async (
dataLoader.get('users').loadNonNull(facilitatorUserId),
dataLoader.get('teams').loadNonNull(teamId)
])
- const isAISummaryAccessible = await canAccessAISummary(
- team,
- facilitator.id,
- 'retrospective',
- dataLoader
- )
- if (!isAISummaryAccessible) return
+ const isAIAvailable = await canAccessAI(team, 'retrospective', dataLoader)
+ if (!isAIAvailable) return
const [reflections, reflectionGroups] = await Promise.all([
dataLoader.get('retroReflectionsByMeetingId').load(meetingId),
dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId)
diff --git a/packages/server/graphql/mutations/helpers/generateDiscussionSummary.ts b/packages/server/graphql/mutations/helpers/generateDiscussionSummary.ts
index a5871fb124b..34cf1535b07 100644
--- a/packages/server/graphql/mutations/helpers/generateDiscussionSummary.ts
+++ b/packages/server/graphql/mutations/helpers/generateDiscussionSummary.ts
@@ -5,25 +5,17 @@ import {RetrospectiveMeeting} from '../../../postgres/types/Meeting'
import OpenAIServerManager from '../../../utils/OpenAIServerManager'
import publish from '../../../utils/publish'
import {DataLoaderWorker} from '../../graphql'
-import canAccessAISummary from './canAccessAISummary'
+import canAccessAI from './canAccessAI'
const generateDiscussionSummary = async (
discussionId: string,
meeting: RetrospectiveMeeting,
dataLoader: DataLoaderWorker
) => {
- const {id: meetingId, endedAt, facilitatorUserId, teamId} = meeting
- const [facilitator, team] = await Promise.all([
- dataLoader.get('users').loadNonNull(facilitatorUserId!),
- dataLoader.get('teams').loadNonNull(teamId)
- ])
- const isAISummaryAccessible = await canAccessAISummary(
- team,
- facilitator.id,
- 'retrospective',
- dataLoader
- )
- if (!isAISummaryAccessible) return
+ const {id: meetingId, endedAt, teamId} = meeting
+ const team = await dataLoader.get('teams').loadNonNull(teamId)
+ const isAIAvailable = await canAccessAI(team, 'retrospective', dataLoader)
+ if (!isAIAvailable) return
const [comments, tasks] = await Promise.all([
dataLoader.get('commentsByDiscussionId').load(discussionId),
dataLoader.get('tasksByDiscussionId').load(discussionId)
diff --git a/packages/server/graphql/mutations/helpers/generateStandupMeetingSummary.ts b/packages/server/graphql/mutations/helpers/generateStandupMeetingSummary.ts
index 997996f7b2c..bca491b14b7 100644
--- a/packages/server/graphql/mutations/helpers/generateStandupMeetingSummary.ts
+++ b/packages/server/graphql/mutations/helpers/generateStandupMeetingSummary.ts
@@ -2,24 +2,16 @@ import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTe
import {TeamPromptMeeting} from '../../../postgres/types/Meeting'
import OpenAIServerManager from '../../../utils/OpenAIServerManager'
import {DataLoaderWorker} from '../../graphql'
-import canAccessAISummary from './canAccessAISummary'
+import canAccessAI from './canAccessAI'
const generateStandupMeetingSummary = async (
meeting: TeamPromptMeeting,
dataLoader: DataLoaderWorker
) => {
- const [facilitator, team] = await Promise.all([
- dataLoader.get('users').loadNonNull(meeting.facilitatorUserId!),
- dataLoader.get('teams').loadNonNull(meeting.teamId)
- ])
- const isAISummaryAccessible = await canAccessAISummary(
- team,
- facilitator.id,
- 'standup',
- dataLoader
- )
+ const team = await dataLoader.get('teams').loadNonNull(meeting.teamId)
+ const isAIAvailable = await canAccessAI(team, 'standup', dataLoader)
+ if (!isAIAvailable) return
- if (!isAISummaryAccessible) return
const responses = await getTeamPromptResponsesByMeetingId(meeting.id)
const contentToSummarize = responses.map((response) => response.plaintextContent)
diff --git a/packages/server/graphql/mutations/helpers/generateWholeMeetingSentimentScore.ts b/packages/server/graphql/mutations/helpers/generateWholeMeetingSentimentScore.ts
index 4400e3998b4..264cd3e0445 100644
--- a/packages/server/graphql/mutations/helpers/generateWholeMeetingSentimentScore.ts
+++ b/packages/server/graphql/mutations/helpers/generateWholeMeetingSentimentScore.ts
@@ -1,18 +1,17 @@
import {DataLoaderWorker} from '../../graphql'
+import canAccessAI from './canAccessAI'
const generateWholeMeetingSentimentScore = async (
meetingId: string,
- facilitatorUserId: string,
dataLoader: DataLoaderWorker
) => {
- const [facilitator, reflections] = await Promise.all([
- dataLoader.get('users').loadNonNull(facilitatorUserId),
+ const [meeting, reflections] = await Promise.all([
+ dataLoader.get('newMeetings').loadNonNull(meetingId),
dataLoader.get('retroReflectionsByMeetingId').load(meetingId)
])
- const hasNoAISummary = await dataLoader
- .get('featureFlagByOwnerId')
- .load({ownerId: facilitator.id, featureName: 'noAISummary'})
- if (hasNoAISummary || reflections.length === 0) return undefined
+ const team = await dataLoader.get('teams').loadNonNull(meeting.teamId)
+ const isAIAvailable = await canAccessAI(team, 'retrospective', dataLoader)
+ if (!isAIAvailable || reflections.length === 0) return undefined
const reflectionsWithSentimentScores = reflections.filter(
({sentimentScore}) => sentimentScore !== undefined
)
diff --git a/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts b/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts
index b9d8ed4932e..d34245fa906 100644
--- a/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts
+++ b/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts
@@ -2,26 +2,17 @@ import {PARABOL_AI_USER_ID} from 'parabol-client/utils/constants'
import OpenAIServerManager from '../../../utils/OpenAIServerManager'
import {DataLoaderWorker} from '../../graphql'
import isValid from '../../isValid'
-import canAccessAISummary from './canAccessAISummary'
+import canAccessAI from './canAccessAI'
const generateWholeMeetingSummary = async (
discussionIds: string[],
meetingId: string,
teamId: string,
- facilitatorUserId: string,
dataLoader: DataLoaderWorker
) => {
- const [facilitator, team] = await Promise.all([
- dataLoader.get('users').loadNonNull(facilitatorUserId),
- dataLoader.get('teams').loadNonNull(teamId)
- ])
- const isAISummaryAccessible = await canAccessAISummary(
- team,
- facilitator.id,
- 'retrospective',
- dataLoader
- )
- if (!isAISummaryAccessible) return
+ const team = await dataLoader.get('teams').loadNonNull(teamId)
+ const isAIAvailable = await canAccessAI(team, 'retrospective', dataLoader)
+ if (!isAIAvailable) return
const [commentsByDiscussions, tasksByDiscussions, reflections] = await Promise.all([
dataLoader.get('commentsByDiscussionId').loadMany(discussionIds),
dataLoader.get('tasksByDiscussionId').loadMany(discussionIds),
diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts
index 30e8316ce6f..dd968ceedd0 100644
--- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts
+++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts
@@ -34,12 +34,12 @@ const getTranscription = async (recallBotId?: string | null) => {
const summarizeRetroMeeting = async (meeting: RetrospectiveMeeting, context: InternalContext) => {
const {dataLoader} = context
- const {id: meetingId, phases, facilitatorUserId, teamId, recallBotId} = meeting
+ const {id: meetingId, phases, teamId, recallBotId} = meeting
const pg = getKysely()
const [reflectionGroups, reflections, sentimentScore] = await Promise.all([
dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId),
dataLoader.get('retroReflectionsByMeetingId').load(meetingId),
- generateWholeMeetingSentimentScore(meetingId, facilitatorUserId!, dataLoader)
+ generateWholeMeetingSentimentScore(meetingId, dataLoader)
])
const discussPhase = getPhase(phases, 'discuss')
const {stages} = discussPhase
@@ -47,7 +47,7 @@ const summarizeRetroMeeting = async (meeting: RetrospectiveMeeting, context: Int
const reflectionGroupIds = reflectionGroups.map(({id}) => id)
const [summary, transcription] = await Promise.all([
- generateWholeMeetingSummary(discussionIds, meetingId, teamId, facilitatorUserId!, dataLoader),
+ generateWholeMeetingSummary(discussionIds, meetingId, teamId, dataLoader),
getTranscription(recallBotId)
])
const commentCounts = (
diff --git a/packages/server/graphql/public/mutations/toggleAIFeatures.ts b/packages/server/graphql/public/mutations/toggleAIFeatures.ts
new file mode 100644
index 00000000000..74e1eefbcec
--- /dev/null
+++ b/packages/server/graphql/public/mutations/toggleAIFeatures.ts
@@ -0,0 +1,40 @@
+import {sql} from 'kysely'
+import {SubscriptionChannel} from '../../../../client/types/constEnums'
+import getKysely from '../../../postgres/getKysely'
+import {getUserId, isUserOrgAdmin} from '../../../utils/authorization'
+import publish from '../../../utils/publish'
+import standardError from '../../../utils/standardError'
+import {MutationResolvers} from '../resolverTypes'
+
+const toggleAIFeatures: MutationResolvers['toggleAIFeatures'] = async (
+ _source,
+ {orgId},
+ {authToken, dataLoader, socketId: mutatorId}
+) => {
+ const viewerId = getUserId(authToken)
+ const pg = getKysely()
+
+ // VALIDATION
+ if (!(await isUserOrgAdmin(viewerId, orgId, dataLoader))) {
+ return standardError(new Error('Not organization admin'))
+ }
+
+ // RESOLUTION
+ const operationId = dataLoader.share()
+ const subOptions = {mutatorId, operationId}
+
+ await pg
+ .updateTable('Organization')
+ .set({useAI: sql`NOT "useAI"`})
+ .where('id', '=', orgId)
+ .execute()
+
+ const data = {
+ orgId
+ }
+
+ publish(SubscriptionChannel.ORGANIZATION, orgId, 'ToggleAIFeaturesPayload', data, subOptions)
+ return data
+}
+
+export default toggleAIFeatures
diff --git a/packages/server/graphql/public/mutations/toggleFeatureFlag.ts b/packages/server/graphql/public/mutations/toggleFeatureFlag.ts
new file mode 100644
index 00000000000..7e447f8d1b0
--- /dev/null
+++ b/packages/server/graphql/public/mutations/toggleFeatureFlag.ts
@@ -0,0 +1,108 @@
+import {SubscriptionChannel} from 'parabol-client/types/constEnums'
+import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId'
+import getKysely from '../../../postgres/getKysely'
+import {getUserId, isUserOrgAdmin} from '../../../utils/authorization'
+import publish from '../../../utils/publish'
+import standardError from '../../../utils/standardError'
+import {MutationResolvers} from '../resolverTypes'
+
+const toggleFeatureFlag: MutationResolvers['toggleFeatureFlag'] = async (
+ _source,
+ {featureName, orgId, teamId, userId},
+ {authToken, dataLoader}
+) => {
+ const viewerId = getUserId(authToken)
+ const pg = getKysely()
+
+ if (!orgId && !teamId && !userId) {
+ return standardError(new Error('Must provide one an orgId, teamId, or userId'))
+ }
+
+ const ownerId = (orgId || teamId || userId) as string
+
+ if (orgId && !(await isUserOrgAdmin(viewerId, orgId, dataLoader))) {
+ return standardError(new Error('Not organization admin'))
+ }
+
+ if (teamId) {
+ const teamMemberId = toTeamMemberId(teamId, viewerId)
+ const teamMember = await dataLoader.get('teamMembers').load(teamMemberId)
+ if (!teamMember) {
+ return standardError(new Error('Not a member of the team'))
+ }
+ if (!teamMember.isLead) {
+ return standardError(new Error('Not team lead'))
+ }
+ }
+
+ if (userId && userId !== viewerId) {
+ return standardError(new Error('Not the user'))
+ }
+
+ const featureFlag = await pg
+ .selectFrom('FeatureFlag')
+ .selectAll()
+ .where('featureName', '=', featureName)
+ .where('expiresAt', '>', new Date())
+ .executeTakeFirst()
+
+ if (!featureFlag) {
+ return standardError(new Error('Feature flag not found or expired'))
+ }
+
+ const scope = orgId ? 'Organization' : teamId ? 'Team' : 'User'
+ if (featureFlag.scope !== scope) {
+ return standardError(
+ new Error(`Feature flag is ${featureFlag.scope}-scoped, not ${scope}-scoped`)
+ )
+ }
+
+ const existingOwner = await pg
+ .selectFrom('FeatureFlagOwner')
+ .selectAll()
+ .where('featureFlagId', '=', featureFlag.id)
+ .where((eb) =>
+ eb.or([
+ eb('orgId', '=', orgId || null),
+ eb('teamId', '=', teamId || null),
+ eb('userId', '=', userId || null)
+ ])
+ )
+ .executeTakeFirst()
+
+ const operationId = dataLoader.share()
+ const subOptions = {operationId}
+ const isEnabled = !!existingOwner
+
+ if (isEnabled) {
+ await pg
+ .deleteFrom('FeatureFlagOwner')
+ .where('featureFlagId', '=', featureFlag.id)
+ .where((eb) =>
+ eb.or([
+ eb('orgId', '=', orgId || null),
+ eb('teamId', '=', teamId || null),
+ eb('userId', '=', userId || null)
+ ])
+ )
+ .execute()
+ } else {
+ await pg
+ .insertInto('FeatureFlagOwner')
+ .values({
+ featureFlagId: featureFlag.id,
+ orgId: orgId || null,
+ teamId: teamId || null,
+ userId: userId || null
+ })
+ .execute()
+ }
+ const data = {
+ featureFlagId: featureFlag.id,
+ enabled: !isEnabled
+ }
+ publish(SubscriptionChannel.NOTIFICATION, ownerId, 'ToggleFeatureFlagPayload', data, subOptions)
+ return data
+}
+
+export default toggleFeatureFlag
diff --git a/packages/server/graphql/public/typeDefs/FeatureFlag.graphql b/packages/server/graphql/public/typeDefs/FeatureFlag.graphql
index 4582658e167..ec933cc343e 100644
--- a/packages/server/graphql/public/typeDefs/FeatureFlag.graphql
+++ b/packages/server/graphql/public/typeDefs/FeatureFlag.graphql
@@ -1,4 +1,9 @@
type FeatureFlag {
+ """
+ The ID of the feature flag
+ """
+ id: ID!
+
"""
The name of the feature flag
"""
diff --git a/packages/server/graphql/public/typeDefs/Mutation.graphql b/packages/server/graphql/public/typeDefs/Mutation.graphql
index a793fc21632..a679e5303e9 100644
--- a/packages/server/graphql/public/typeDefs/Mutation.graphql
+++ b/packages/server/graphql/public/typeDefs/Mutation.graphql
@@ -1135,6 +1135,38 @@ type Mutation {
teamDrawerType: TeamDrawer
): ToggleTeamDrawerPayload!
+ """
+ Toggle a feature flag for a specific owner (organization, team, or user)
+ """
+ toggleFeatureFlag(
+ """
+ The name of the feature flag to toggle
+ """
+ featureName: String!
+ """
+ The organization ID if toggling for an org
+ """
+ orgId: ID
+ """
+ The team ID if toggling for a team
+ """
+ teamId: ID
+ """
+ The user ID if toggling for a user
+ """
+ userId: ID
+ ): ToggleFeatureFlagPayload!
+
+ """
+ Toggle whether the organization has access to ai features
+ """
+ toggleAIFeatures(
+ """
+ The id of the org being toggled
+ """
+ orgId: ID!
+ ): ToggleAIFeaturesPayload!
+
"""
Update how a parabol dimension maps to a GitHub label
"""
diff --git a/packages/server/graphql/public/typeDefs/NotificationSubscriptionPayload.graphql b/packages/server/graphql/public/typeDefs/NotificationSubscriptionPayload.graphql
index b119801745f..56f7506527a 100644
--- a/packages/server/graphql/public/typeDefs/NotificationSubscriptionPayload.graphql
+++ b/packages/server/graphql/public/typeDefs/NotificationSubscriptionPayload.graphql
@@ -26,4 +26,5 @@ type NotificationSubscriptionPayload {
UpdatedNotification: UpdatedNotification
RemoveIntegrationSearchQuerySuccess: RemoveIntegrationSearchQuerySuccess
PersistIntegrationSearchQuerySuccess: PersistIntegrationSearchQuerySuccess
+ ToggleFeatureFlagSuccess: ToggleFeatureFlagSuccess
}
diff --git a/packages/server/graphql/public/typeDefs/Organization.graphql b/packages/server/graphql/public/typeDefs/Organization.graphql
index 85fe5a69614..e588b1652a1 100644
--- a/packages/server/graphql/public/typeDefs/Organization.graphql
+++ b/packages/server/graphql/public/typeDefs/Organization.graphql
@@ -163,6 +163,12 @@ type Organization {
"""
picture: URL
tier: TierEnum!
+
+ """
+ Whether the org has access to AI features
+ """
+ useAI: Boolean!
+
billingTier: TierEnum!
"""
@@ -195,4 +201,9 @@ type Organization {
Custom integration providers with organization scope
"""
integrationProviders: OrgIntegrationProviders!
+
+ """
+ The feature flags the org has enabled
+ """
+ orgFeatureFlags: [OwnedFeatureFlag!]!
}
diff --git a/packages/server/graphql/public/typeDefs/OrganizationSubscriptionPayload.graphql b/packages/server/graphql/public/typeDefs/OrganizationSubscriptionPayload.graphql
index 259e614cf52..bd689953ce5 100644
--- a/packages/server/graphql/public/typeDefs/OrganizationSubscriptionPayload.graphql
+++ b/packages/server/graphql/public/typeDefs/OrganizationSubscriptionPayload.graphql
@@ -9,6 +9,7 @@ type OrganizationSubscriptionPayload {
PayLaterPayload: PayLaterPayload
RemoveIntegrationProviderSuccess: RemoveIntegrationProviderSuccess
RemoveOrgUserPayload: RemoveOrgUserPayload
+ ToggleAIFeaturesSuccess: ToggleAIFeaturesSuccess
SetOrgUserRoleSuccess: SetOrgUserRoleSuccess
UpdateCreditCardPayload: UpdateCreditCardPayload
UpdateIntegrationProviderSuccess: UpdateIntegrationProviderSuccess
diff --git a/packages/server/graphql/public/typeDefs/OwnedFeatureFlag.graphql b/packages/server/graphql/public/typeDefs/OwnedFeatureFlag.graphql
new file mode 100644
index 00000000000..7d4828f7fef
--- /dev/null
+++ b/packages/server/graphql/public/typeDefs/OwnedFeatureFlag.graphql
@@ -0,0 +1,31 @@
+type OwnedFeatureFlag {
+ """
+ The ID of the feature flag
+ """
+ id: ID!
+
+ """
+ The name of the feature flag
+ """
+ featureName: String!
+
+ """
+ Description of the feature flag
+ """
+ description: String
+
+ """
+ Expiration date of the feature flag
+ """
+ expiresAt: DateTime!
+
+ """
+ The scope of the feature flag
+ """
+ scope: FeatureFlagScope!
+
+ """
+ Whether the flag is enabled for an owner or not
+ """
+ enabled: Boolean
+}
diff --git a/packages/server/graphql/public/typeDefs/ToggleAIFeaturesPayload.graphql b/packages/server/graphql/public/typeDefs/ToggleAIFeaturesPayload.graphql
new file mode 100644
index 00000000000..49f5a93d260
--- /dev/null
+++ b/packages/server/graphql/public/typeDefs/ToggleAIFeaturesPayload.graphql
@@ -0,0 +1,4 @@
+"""
+Return value for toggleAIFeatures, which could be an error
+"""
+union ToggleAIFeaturesPayload = ErrorPayload | ToggleAIFeaturesSuccess
diff --git a/packages/server/graphql/public/typeDefs/ToggleAIFeaturesSuccess.graphql b/packages/server/graphql/public/typeDefs/ToggleAIFeaturesSuccess.graphql
new file mode 100644
index 00000000000..7b1feca495a
--- /dev/null
+++ b/packages/server/graphql/public/typeDefs/ToggleAIFeaturesSuccess.graphql
@@ -0,0 +1,3 @@
+type ToggleAIFeaturesSuccess {
+ organization: Organization!
+}
diff --git a/packages/server/graphql/public/typeDefs/ToggleFeatureFlagPayload.graphql b/packages/server/graphql/public/typeDefs/ToggleFeatureFlagPayload.graphql
new file mode 100644
index 00000000000..f5ae2e9e60b
--- /dev/null
+++ b/packages/server/graphql/public/typeDefs/ToggleFeatureFlagPayload.graphql
@@ -0,0 +1 @@
+union ToggleFeatureFlagPayload = ErrorPayload | ToggleFeatureFlagSuccess
diff --git a/packages/server/graphql/public/typeDefs/ToggleFeatureFlagSuccess.graphql b/packages/server/graphql/public/typeDefs/ToggleFeatureFlagSuccess.graphql
new file mode 100644
index 00000000000..03d63361bf2
--- /dev/null
+++ b/packages/server/graphql/public/typeDefs/ToggleFeatureFlagSuccess.graphql
@@ -0,0 +1,6 @@
+type ToggleFeatureFlagSuccess {
+ """
+ The feature flag that was toggled
+ """
+ featureFlag: OwnedFeatureFlag!
+}
diff --git a/packages/server/graphql/public/types/Organization.ts b/packages/server/graphql/public/types/Organization.ts
index b705d1c355d..e44231e107f 100644
--- a/packages/server/graphql/public/types/Organization.ts
+++ b/packages/server/graphql/public/types/Organization.ts
@@ -135,7 +135,10 @@ const Organization: OrganizationResolvers = {
organizationUser.role === 'BILLING_LEADER' || organizationUser.role === 'ORG_ADMIN'
)
},
- integrationProviders: ({id: orgId}) => ({orgId})
+ integrationProviders: ({id: orgId}) => ({orgId}),
+ orgFeatureFlags: async ({id: orgId}, _args, {dataLoader}) => {
+ return dataLoader.get('allFeatureFlagsByOwner').load({ownerId: orgId, scope: 'Organization'})
+ }
}
export default Organization
diff --git a/packages/server/graphql/public/types/ToggleAIFeaturesSuccess.ts b/packages/server/graphql/public/types/ToggleAIFeaturesSuccess.ts
new file mode 100644
index 00000000000..1d07aed8aa1
--- /dev/null
+++ b/packages/server/graphql/public/types/ToggleAIFeaturesSuccess.ts
@@ -0,0 +1,13 @@
+import {ToggleAiFeaturesSuccessResolvers} from '../resolverTypes'
+
+export type ToggleAIFeaturesSuccessSource = {
+ orgId: string
+}
+
+const ToggleAIFeaturesSuccess: ToggleAiFeaturesSuccessResolvers = {
+ organization: async ({orgId}, _args, {dataLoader}) => {
+ return await dataLoader.get('organizations').loadNonNull(orgId)
+ }
+}
+
+export default ToggleAIFeaturesSuccess
diff --git a/packages/server/graphql/public/types/ToggleFeatureFlagSuccess.ts b/packages/server/graphql/public/types/ToggleFeatureFlagSuccess.ts
new file mode 100644
index 00000000000..34a678b711d
--- /dev/null
+++ b/packages/server/graphql/public/types/ToggleFeatureFlagSuccess.ts
@@ -0,0 +1,18 @@
+import {ToggleFeatureFlagSuccessResolvers} from '../resolverTypes'
+
+export type ToggleFeatureFlagSuccessSource = {
+ featureFlagId: string
+ enabled: boolean
+}
+
+const ToggleFeatureFlagSuccess: ToggleFeatureFlagSuccessResolvers = {
+ featureFlag: async ({featureFlagId, enabled}, _, {dataLoader}) => {
+ const flag = await dataLoader.get('featureFlags').loadNonNull(featureFlagId)
+ return {
+ ...flag,
+ enabled
+ }
+ }
+}
+
+export default ToggleFeatureFlagSuccess
diff --git a/packages/server/postgres/migrations/2024-10-31T14:42:16.120Z_add-use-ai-to-orgs.ts b/packages/server/postgres/migrations/2024-10-31T14:42:16.120Z_add-use-ai-to-orgs.ts
new file mode 100644
index 00000000000..2801efbfe03
--- /dev/null
+++ b/packages/server/postgres/migrations/2024-10-31T14:42:16.120Z_add-use-ai-to-orgs.ts
@@ -0,0 +1,59 @@
+import {Kysely, sql} from 'kysely'
+
+export async function up(db: Kysely): Promise {
+ await db.schema
+ .alterTable('Organization')
+ .addColumn('useAI', 'boolean', (col) => col.notNull().defaultTo(true))
+ .execute()
+
+ await db
+ .updateTable('Organization')
+ .set({useAI: false})
+ .where(
+ 'id',
+ 'in',
+ db
+ .selectFrom('FeatureFlagOwner')
+ .innerJoin('FeatureFlag', 'FeatureFlag.id', 'FeatureFlagOwner.featureFlagId')
+ .where('FeatureFlag.featureName', '=', 'noAISummary')
+ .select('FeatureFlagOwner.orgId')
+ )
+ .execute()
+
+ await db
+ .deleteFrom('FeatureFlagOwner')
+ .where(
+ 'featureFlagId',
+ 'in',
+ db.selectFrom('FeatureFlag').select('id').where('featureName', '=', 'noAISummary')
+ )
+ .execute()
+
+ await db.deleteFrom('FeatureFlag').where('featureName', '=', 'noAISummary').execute()
+}
+
+export async function down(db: Kysely): Promise {
+ const [flagId] = await db
+ .insertInto('FeatureFlag')
+ .values({
+ featureName: 'noAISummary',
+ description: 'Disable AI features for this organization',
+ expiresAt: new Date('2074-01-31T00:00:00.000Z'),
+ scope: 'Organization'
+ })
+ .returning('id')
+ .execute()
+
+ await db
+ .insertInto('FeatureFlagOwner')
+ .columns(['featureFlagId', 'orgId'])
+ .expression(
+ db
+ .selectFrom('Organization')
+ .select([sql`${flagId.id}::uuid`.as('featureFlagId'), 'id as orgId'])
+ .where('useAI', '=', false)
+ )
+ .execute()
+
+ await db.schema.alterTable('Organization').dropColumn('useAI').execute()
+}
diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts
index efae11cccfb..6223dbaecb9 100644
--- a/packages/server/postgres/select.ts
+++ b/packages/server/postgres/select.ts
@@ -138,6 +138,7 @@ export const selectOrganizations = () =>
'trialStartDate',
'scheduledLockAt',
'lockedAt',
+ 'useAI',
'updatedAt'
])
.select(({fn}) => [fn('to_json', ['creditCard']).as('creditCard')])
diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts
index a91a82d3101..af0ff428cf4 100644
--- a/packages/server/postgres/types/index.d.ts
+++ b/packages/server/postgres/types/index.d.ts
@@ -21,6 +21,7 @@ import {
} from '../select'
import {
Discussion as DiscussionPG,
+ FeatureFlag as FeatureFlagPG,
Insight as InsightPG,
OrganizationUser as OrganizationUserPG,
TaskEstimate as TaskEstimatePG,
@@ -80,6 +81,7 @@ export type TemplateScaleRef = ExtractTypeFromQueryBuilderSelect
export type PokerMeetingSettings = MeetingSettings & {meetingType: 'poker'}
export type RetrospectiveMeetingSettings = MeetingSettings & {meetingType: 'retrospective'}
+export type FeatureFlag = Selectable
export type AgendaItem = ExtractTypeFromQueryBuilderSelect