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