[web] AI Feature controls for managed groups (#35193)

* [web] AI Feature controls for managed groups

Adds a Feature Controls section to the group settings with a switch to control whether AI features are enabled or disabled

GitOrigin-RevId: c5d5ed80c0cc3435075bb2c58b43fc08192dd46b
This commit is contained in:
Miguel Serrano
2026-09-17 11:19:42 +00:00
committed by Copybot
parent e2a8bf768b
commit a84257ff81
9 changed files with 218 additions and 2 deletions
@@ -1122,6 +1122,42 @@ templates.groupMemberLimitWarning = ctaTemplate({
},
})
templates.groupAiFeaturesDisabled = NoCTAEmailTemplate({
subject() {
return `AI features have been disabled for your ${settings.appName} group`
},
title() {
return `AI features disabled`
},
greeting(opts) {
return opts.firstName ? `Hi ${opts.firstName},` : 'Hi there,'
},
message() {
return [
`Your organization has disabled AI features for your ${settings.appName} group. This means you won't see AI Assistant or other ${settings.appName} AI tools in the editor.`,
`If you have questions about this change, please contact your group administrator.`,
]
},
})
templates.groupAiFeaturesEnabled = NoCTAEmailTemplate({
subject() {
return `AI features have been enabled for your ${settings.appName} group`
},
title() {
return `AI features enabled`
},
greeting(opts) {
return opts.firstName ? `Hi ${opts.firstName},` : 'Hi there,'
},
message() {
return [
`Your organization has enabled AI features for your ${settings.appName} group. You now have access to AI Assistant and other ${settings.appName} AI tools in the editor.`,
`If you have questions about this change, please contact your group administrator.`,
]
},
})
templates.groupDomainCapturedByGroupChanged = ctaTemplate({
subject(opts) {
return opts.domainCapturedByGroup
@@ -34,13 +34,15 @@ const SubscriptionLocator = {
return subscription
},
async getManagedGroupSubscriptions(userOrId) {
async getManagedGroupSubscriptions(userOrId, populate = []) {
if (!Features.hasFeature('saas')) return []
// eslint-disable-next-line no-restricted-syntax
return await Subscription.find({
manager_ids: userOrId,
groupPlan: true,
})
.populate('admin_id', ['_id', 'email'])
.populate(populate)
.exec()
},
@@ -82,7 +82,11 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
SubscriptionLocator.getMemberSubscriptions(user, cb)
},
managedGroupSubscriptions(cb) {
SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
SubscriptionLocator.getManagedGroupSubscriptions(
user,
['groupPolicy'],
cb
)
},
currentInstitutionsWithLicence(cb) {
InstitutionsGetter.getCurrentInstitutionsWithLicence(
@@ -139,6 +143,14 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
id => id.toString() === user._id.toString()
)
const groupPolicy = group.groupPolicy
? {
userCannotUseChat: group.groupPolicy.userCannotUseChat,
userCannotUseDropbox: group.groupPolicy.userCannotUseDropbox,
userCannotUseAIFeatures: group.groupPolicy.userCannotUseAIFeatures,
}
: undefined
const groupDataForView = {
_id: group._id,
planCode: group.planCode,
@@ -146,6 +158,8 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
teamName: group.teamName,
admin_id: { _id: group.admin_id._id, email: group.admin_id.email },
features: group.features,
managedUsersEnabled: !!group.managedUsersEnabled,
groupPolicy,
userIsGroupMember,
}
@@ -122,6 +122,11 @@
"ai_assistant_disabled": "",
"ai_can_make_mistakes": "",
"ai_features": "",
"ai_features_couldnt_be_toggled": "",
"ai_features_managed_by_overleaf_support": "",
"ai_features_off": "",
"ai_features_on": "",
"ai_features_section_subtext": "",
"ai_features_unavailable_on_this_project": "",
"ai_feedback_please_provide_more_detail": "",
"ai_feedback_tell_us_what_was_wrong_so_we_can_improve": "",
@@ -539,7 +544,10 @@
"dropbox_duplicate_project_names": "",
"dropbox_duplicate_project_names_suggestion": "",
"dropbox_for_link_share_projs": "",
"dropbox_integration": "",
"dropbox_integration_lowercase": "",
"dropbox_integration_managed_by_overleaf_support": "",
"dropbox_integration_section_description": "",
"dropbox_sync_both": "",
"dropbox_sync_description": "",
"dropbox_sync_error": "",
@@ -665,7 +673,10 @@
"fast": "",
"fast_draft": "",
"fast_optimize_images": "",
"feature_controls": "",
"feature_disabled_by_organization_contact_support": "",
"feature_enabled_or_disabled": "",
"feature_settings_subtext": "",
"features_like_track_changes": "",
"feedback": "",
"figure": "",
@@ -943,6 +954,9 @@
"imported_from_zotero_at_date": "",
"importing": "",
"importing_and_merging_changes_in_github": "",
"in_editor_chat": "",
"in_editor_chat_managed_by_overleaf_support": "",
"in_editor_chat_section_description": "",
"in_order_to_match_institutional_metadata_2": "",
"in_order_to_match_institutional_metadata_associated": "",
"inactive": "",
@@ -111,6 +111,16 @@ export default function ManagedGroupSubscriptions() {
{managedGroupSubscriptions.map(subscription => {
const isAdmin = usersEmail === subscription.admin_id.email
// Feature controls are currently rendered only for managed groups, where the admin can
// toggle AI Feature on/off. For non-managed groups, the section only displays notifications
// for features that have been disabled by Overleaf Support.
// This flag will be deleted once AI Features toggling is available to all groups.
const shouldDisplayFeatureControls =
subscription.managedUsersEnabled ||
subscription.groupPolicy?.userCannotUseAIFeatures ||
subscription.groupPolicy?.userCannotUseChat ||
subscription.groupPolicy?.userCannotUseDropbox
return (
<div key={`managed-group-${subscription._id}`}>
<h2 className="h3 fw-bold">{t('group_management')}</h2>
@@ -159,6 +169,14 @@ export default function ManagedGroupSubscriptions() {
icon="share"
/>
)}
{shouldDisplayFeatureControls && (
<RowLink
href={`/manage/groups/${subscription._id}/feature-settings`}
heading={t('feature_controls')}
subtext={t('feature_settings_subtext')}
icon="toggle_off"
/>
)}
<RowLink
href={`/manage/groups/${subscription._id}/audit-logs`}
heading={t('audit_logs')}
@@ -392,3 +392,7 @@ h4.group-settings-title {
.remove-domain-confirm-name {
color: var(--content-danger);
}
.feature-settings-feature-switch {
padding-left: 0;
}
+13
View File
@@ -152,6 +152,11 @@
"ai_assistant_explanation": "A LaTeX-fluent AI Assistant built into your editor.",
"ai_can_make_mistakes": "AI can make mistakes. Review fixes before you apply them.",
"ai_features": "AI features",
"ai_features_couldnt_be_toggled": "AI features couldn’t be toggled. Try again, or contact Overleaf support if the problem continues.",
"ai_features_managed_by_overleaf_support": "AI features are managed by Overleaf support.",
"ai_features_off": "AI features off",
"ai_features_on": "AI features on",
"ai_features_section_subtext": "Includes AI Assistant, Writefull, and other AI tools that help group members write, edit, and work with LaTeX.",
"ai_features_unavailable_on_this_project": "AI features have been turned off for this project",
"ai_feedback_please_provide_more_detail": "Please provide more detail about what was wrong (optional)",
"ai_feedback_tell_us_what_was_wrong_so_we_can_improve": "Tell us what was wrong so we can improve.",
@@ -704,6 +709,8 @@
"dropbox_integration": "Dropbox integration",
"dropbox_integration_explanation": "Disable the Dropbox integration",
"dropbox_integration_lowercase": "Dropbox integration",
"dropbox_integration_managed_by_overleaf_support": "Dropbox integration is managed by Overleaf support.",
"dropbox_integration_section_description": "Group members can sync their Overleaf projects with Dropbox to access and edit files outside of Overleaf.",
"dropbox_successfully_linked_description": "Thanks, we’ve successfully linked your Dropbox account to __appName__.",
"dropbox_sync": "Dropbox Sync",
"dropbox_sync_both": "Sending and receiving updates",
@@ -864,9 +871,12 @@
"fast_draft": "Fast [draft]",
"fast_optimize_images": "Fast [optimize images]",
"fastest": "Fastest",
"feature_controls": "Feature controls",
"feature_disabled_by_organization_contact_support": "This feature isn’t available for your group. <0>Contact Overleaf support</0> to enable it.",
"feature_enabled_or_disabled": "Feature enabled/disabled",
"feature_included": "Feature included",
"feature_not_included": "Feature not included",
"feature_settings_subtext": "View and manage features available to your group members",
"featured": "Featured",
"featured_latex_templates": "Featured LaTeX Templates",
"features": "Features",
@@ -1204,6 +1214,9 @@
"imported_from_zotero_at_date": "Imported from Zotero at __formattedDate__ __relativeDate__",
"importing": "Importing",
"importing_and_merging_changes_in_github": "Importing and merging changes in GitHub",
"in_editor_chat": "In-editor chat",
"in_editor_chat_managed_by_overleaf_support": "In-editor chat is managed by Overleaf support.",
"in_editor_chat_section_description": "Lets collaborators leave and reply to comments directly within a project.",
"in_order_to_have_a_secure_account_make_sure_your_password": "To help keep your account secure, make sure your new password:",
"in_order_to_match_institutional_metadata_2": "In order to match your institutional metadata, we’ve linked your account using <0>__email__</0>.",
"in_order_to_match_institutional_metadata_associated": "In order to match your institutional metadata, your account is associated with the email <b>__email__</b>.",
@@ -22,6 +22,7 @@ function getManagedGroupSubscriptions(
groupSSO,
managedUsers,
},
managedUsersEnabled: false,
teamName: 'GAS',
}
@@ -36,6 +37,7 @@ function getManagedGroupSubscriptions(
groupSSO,
managedUsers,
},
managedUsersEnabled: false,
teamName: 'GASWPLC',
}
@@ -50,6 +52,7 @@ function getManagedGroupSubscriptions(
groupSSO,
managedUsers,
},
managedUsersEnabled: false,
teamName: 'Testing',
}
@@ -64,6 +67,7 @@ function getManagedGroupSubscriptions(
groupSSO,
managedUsers,
},
managedUsersEnabled: false,
teamName: 'Testing Another',
}
@@ -306,4 +310,113 @@ describe('<ManagedGroupSubscriptions />', function () {
expect(screen.queryByText('Configure and manage SSO and Managed Users')).to
.not.exist
})
describe('Feature controls row', function () {
const adminEmail = 'admin@example.com'
function makeSubscription(
overrides: Partial<ManagedGroupSubscription>
): ManagedGroupSubscription[] {
return [
{
_id: 'sub123',
userIsGroupMember: false,
planLevelName: 'Pro',
admin_id: { email: adminEmail },
features: { groupSSO: false, managedUsers: false },
managedUsersEnabled: false,
teamName: 'Test Group',
...overrides,
},
]
}
describe('when managedUsersEnabled === true', function () {
it('renders the feature controls row when the user is admin', async function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: makeSubscription({ managedUsersEnabled: true }),
},
{ name: 'ol-usersEmail', value: adminEmail },
],
})
await screen.findByText('Feature controls')
})
it('does not render the feature controls row when the user is not the group admin', function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: makeSubscription({ managedUsersEnabled: true }),
},
{ name: 'ol-usersEmail', value: 'other@example.com' },
],
})
expect(screen.queryByText('Feature controls')).to.be.null
})
})
describe('when managedUsersEnabled === false', function () {
it('renders the feature controls row when ai features are disabled', async function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: makeSubscription({
groupPolicy: { userCannotUseAIFeatures: true },
}),
},
{ name: 'ol-usersEmail', value: adminEmail },
],
})
await screen.findByText('Feature controls')
})
it('renders the feature controls row when chat is disabled', async function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: makeSubscription({
groupPolicy: { userCannotUseChat: true },
}),
},
{ name: 'ol-usersEmail', value: adminEmail },
],
})
await screen.findByText('Feature controls')
})
it('renders the feature controls row when dropbox is disabled', async function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: makeSubscription({
groupPolicy: { userCannotUseDropbox: true },
}),
},
{ name: 'ol-usersEmail', value: adminEmail },
],
})
await screen.findByText('Feature controls')
})
it('does not render the feature controls row when no group policies are set', function () {
renderWithSubscriptionDashContext(<ManagedGroupSubscriptions />, {
metaTags: [
{
name: 'ol-managedGroupSubscriptions',
value: makeSubscription({ managedUsersEnabled: false }),
},
{ name: 'ol-usersEmail', value: adminEmail },
],
})
expect(screen.queryByText('Feature controls')).to.be.null
})
})
})
})
@@ -103,6 +103,8 @@ export type ManagedGroupSubscription = {
groupSSO: boolean | null
managedUsers: boolean | null
}
managedUsersEnabled: boolean
groupPolicy?: GroupPolicy
teamName?: string
}