mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
[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:
@@ -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": "",
|
||||
|
||||
+18
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>.",
|
||||
|
||||
+113
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user