diff --git a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx index 3fceffb2256..5d8e873f25e 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx @@ -72,6 +72,7 @@ const ActivityDetailsSidebar = (props: Props) => { featureFlags { gcal adHocTeams + noTemplateLimit } ...AdhocTeamMultiSelect_viewer organizations { @@ -342,7 +343,9 @@ const ActivityDetailsSidebar = (props: Props) => { /> )} - {selectedTeam.tier === 'starter' && !selectedTemplate.isFree ? ( + {selectedTeam.tier === 'starter' && + !viewer.featureFlags.noTemplateLimit && + !selectedTemplate.isFree ? (
Upgrade to the Team Plan to create custom activities unlocking your team’s diff --git a/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx b/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx index b836b084977..dec65509954 100644 --- a/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx +++ b/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx @@ -95,6 +95,14 @@ const ReflectTemplateDetails = (props: Props) => { id orgId tier + viewerTeamMember { + user { + id + featureFlags { + noTemplateLimit + } + } + } } } `, @@ -103,7 +111,8 @@ const ReflectTemplateDetails = (props: Props) => { const {teamTemplates, team} = settings const activeTemplate = settings.activeTemplate ?? settings.selectedTemplate const {id: templateId, name: templateName, prompts, illustrationUrl} = activeTemplate - const {id: teamId, orgId, tier} = team + const {id: teamId, orgId, tier, viewerTeamMember} = team + const noTemplateLimit = viewerTeamMember?.user?.featureFlags?.noTemplateLimit const lowestScope = getTemplateList(teamId, orgId, activeTemplate) const isOwner = activeTemplate.teamId === teamId const description = useTemplateDescription(lowestScope, activeTemplate, tier) @@ -158,6 +167,7 @@ const ReflectTemplateDetails = (props: Props) => { template={activeTemplate} teamId={teamId} tier={tier} + noTemplateLimit={noTemplateLimit} orgId={orgId} /> )} diff --git a/packages/client/modules/meeting/components/SelectTemplate.tsx b/packages/client/modules/meeting/components/SelectTemplate.tsx index 593f9ffeb21..d101abe3304 100644 --- a/packages/client/modules/meeting/components/SelectTemplate.tsx +++ b/packages/client/modules/meeting/components/SelectTemplate.tsx @@ -54,11 +54,12 @@ interface Props { template: SelectTemplate_template$key teamId: string tier?: TierEnum + noTemplateLimit?: boolean orgId?: string } const SelectTemplate = (props: Props) => { - const {template: templateRef, closePortal, teamId, tier, orgId} = props + const {template: templateRef, closePortal, teamId, tier, noTemplateLimit, orgId} = props const template = useFragment( graphql` fragment SelectTemplate_template on MeetingTemplate { @@ -90,7 +91,7 @@ const SelectTemplate = (props: Props) => { }) history.push(`/me/organizations/${orgId}`) } - const showUpgradeCTA = !isFree && tier === 'starter' && scope === 'PUBLIC' + const showUpgradeCTA = !isFree && tier === 'starter' && scope === 'PUBLIC' && !noTemplateLimit if (showUpgradeCTA) { return ( diff --git a/packages/server/graphql/mutations/selectTemplate.ts b/packages/server/graphql/mutations/selectTemplate.ts index bb4a8dd5ca6..1264df32f14 100644 --- a/packages/server/graphql/mutations/selectTemplate.ts +++ b/packages/server/graphql/mutations/selectTemplate.ts @@ -30,9 +30,10 @@ const selectTemplate = { const viewerId = getUserId(authToken) // AUTH - const template = (await dataLoader - .get('meetingTemplates') - .load(selectedTemplateId)) as MeetingTemplate + const [template, viewer] = await Promise.all([ + dataLoader.get('meetingTemplates').load(selectedTemplateId) as Promise, + dataLoader.get('users').loadNonNull(viewerId) + ]) if (!template || !template.isActive) { console.log('no template', selectedTemplateId, template) @@ -50,7 +51,11 @@ const selectTemplate = { return standardError(new Error('Template is scoped to organization'), {userId: viewerId}) } } else if (scope === 'PUBLIC') { - if (!isFree && viewerTeam.tier === 'starter') { + if ( + !isFree && + !viewer.featureFlags.includes('noTemplateLimit') && + viewerTeam.tier === 'starter' + ) { return standardError(new Error('User does not have access to this premium template'), { userId: viewerId }) diff --git a/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts b/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts index 9c49c9e9a7a..aa57923b70d 100644 --- a/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts +++ b/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts @@ -2,22 +2,40 @@ import getRethink from '../../../database/rethinkDriver' import MeetingSettingsPoker from '../../../database/types/MeetingSettingsPoker' import MeetingSettingsRetrospective from '../../../database/types/MeetingSettingsRetrospective' import {GQLContext} from '../../graphql' +import {getUserId} from '../../../utils/authorization' +import isValid from '../../isValid' const resolveSelectedTemplate = (fallbackTemplateId: string) => async ( source: MeetingSettingsPoker | MeetingSettingsRetrospective, _args: unknown, - {dataLoader}: GQLContext + {authToken, dataLoader}: GQLContext ) => { + const viewerId = getUserId(authToken) const {id: settingsId, selectedTemplateId, teamId} = source - const [team, template] = await Promise.all([ + const [team, template, viewer] = await Promise.all([ dataLoader.get('teams').loadNonNull(teamId), - dataLoader.get('meetingTemplates').load(selectedTemplateId) + dataLoader.get('meetingTemplates').load(selectedTemplateId), + dataLoader.get('users').loadNonNull(viewerId) ]) const {tier} = team - if (template?.isFree || template?.scope !== 'PUBLIC' || tier !== 'starter') { - return template + if (template) { + if ( + template.isFree || + template.scope !== 'PUBLIC' || + tier !== 'starter' || + viewer.featureFlags.includes('noTemplateLimit') + ) { + return template + } + // if anyone on the team has the noTemplateLimit flag, they might have selected a non-starter template + const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) + const userIds = teamMembers.map(({userId}) => userId) + const users = (await dataLoader.get('users').loadMany(userIds)).filter(isValid) + if (users.some(({featureFlags}) => featureFlags.includes('noTemplateLimit'))) { + return template + } } // there may be holes in our template deletion or reselection logic, so doing this to be safe source.selectedTemplateId = fallbackTemplateId