From 0113c8ee9f4c631aa5ffbdb83a49a85f7c17a7b8 Mon Sep 17 00:00:00 2001
From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com>
Date: Mon, 17 Aug 2026 12:06:29 +0300
Subject: [PATCH] Merge pull request #35936 from
overleaf/ii-workspaces-subscription-admin-settings
Shared Workspace settings for subscription admins
GitOrigin-RevId: 61cc45a48aaf1709905e98b3cccf09b50e2afa84
---
.../app/src/Features/Email/EmailBuilder.mjs | 82 ++++++++
.../Subscription/SubscriptionController.mjs | 1 +
services/web/app/src/models/GroupPolicy.mjs | 3 +
.../web/frontend/extracted-translations.json | 14 ++
.../dashboard/managed-group-subscriptions.tsx | 17 +-
services/web/frontend/js/utils/meta.ts | 1 +
services/web/locales/en.json | 14 ++
.../managed-group-subscriptions.test.tsx | 135 +++++++++++++
.../test/unit/src/Email/EmailBuilder.test.mjs | 177 ++++++++++++++++++
.../subscription/dashboard/subscription.ts | 1 +
10 files changed, 440 insertions(+), 5 deletions(-)
diff --git a/services/web/app/src/Features/Email/EmailBuilder.mjs b/services/web/app/src/Features/Email/EmailBuilder.mjs
index 9db0cbdd00..8a6c3f1ef7 100644
--- a/services/web/app/src/Features/Email/EmailBuilder.mjs
+++ b/services/web/app/src/Features/Email/EmailBuilder.mjs
@@ -1300,6 +1300,88 @@ templates.groupAiFeaturesEnabled = NoCTAEmailTemplate({
},
})
+templates.groupSharedWorkspaceDisabledForOwner = ctaTemplate({
+ subject() {
+ return `Your shared workspace has been disabled`
+ },
+ title() {
+ return `Your projects are back in your personal list.`
+ },
+ greeting(opts) {
+ return opts.firstName ? `Hi ${opts.firstName},` : 'Hi there,'
+ },
+ message(opts) {
+ const groupName = _.escape(opts.groupName)
+ return [
+ `Your group administrator has disabled the shared workspace for ${groupName}.`,
+ `Projects you owned in the workspace have been returned to your personal projects list. They’re no longer visible to the whole group — only collaborators you’d actively shared them with still have access.`,
+ `You don’t need to do anything. You can find your projects in your projects list as usual.`,
+ ]
+ },
+ ctaText() {
+ return 'Open your projects'
+ },
+ ctaURL() {
+ return `${settings.siteUrl}/project`
+ },
+})
+
+templates.groupSharedWorkspaceDisabledForMember = ctaTemplate({
+ subject() {
+ return `Your group's shared workspace has been disabled`
+ },
+ title() {
+ return `Here's what's changed for your group.`
+ },
+ greeting(opts) {
+ return opts.firstName ? `Hi ${opts.firstName},` : 'Hi there,'
+ },
+ message(opts) {
+ const groupName = _.escape(opts.groupName)
+ return [
+ `Your group administrator has disabled the shared workspace for ${groupName}.`,
+ `Projects that were in the shared workspace have been returned to their owners. You’ll still have access to any projects their owners have shared with you directly — but projects you could only see through the shared workspace may no longer be visible.`,
+ `You don’t need to do anything. If you’re not sure whether you still have access to a project, check with the project owner or your group administrator.`,
+ ]
+ },
+ ctaText() {
+ return 'Open your projects'
+ },
+ ctaURL() {
+ return `${settings.siteUrl}/project`
+ },
+})
+
+templates.groupSharedWorkspaceDisabledForAdmin = ctaTemplate({
+ subject(opts) {
+ return `Shared workspace disabled for ${opts.groupName}`
+ },
+ title() {
+ return `Here's a summary of what changed for your group.`
+ },
+ greeting(opts) {
+ return opts.firstName ? `Hi ${opts.firstName},` : 'Hi there,'
+ },
+ message(opts) {
+ const groupName = _.escape(opts.groupName)
+ return [
+ `You’ve disabled the shared workspace for ${groupName}. Here's a summary of what happened.`,
+ '
' +
+ '- Projects in the workspace have been returned to their owners and are no longer visible to the whole group.
' +
+ '- Each project is now shared only with the collaborators the owner had already added.
' +
+ '- Group members have been notified of the change.
' +
+ '
',
+ `No further action is needed on your part.`,
+ ]
+ },
+ ctaText() {
+ return 'Open your projects'
+ },
+ ctaURL() {
+ return `${settings.siteUrl}/project`
+ },
+})
+
templates.groupDomainCapturedByGroupChanged = ctaTemplate({
subject(opts) {
return opts.domainCapturedByGroup
diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.mjs b/services/web/app/src/Features/Subscription/SubscriptionController.mjs
index 8c3631997c..ed83ab68ba 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionController.mjs
+++ b/services/web/app/src/Features/Subscription/SubscriptionController.mjs
@@ -186,6 +186,7 @@ async function userSubscriptionPage(req, res) {
'group-discount-10'
)
await SplitTestHandler.promises.getAssignment(req, res, 'ai-toggling')
+ await SplitTestHandler.promises.getAssignment(req, res, 'shared-workspace')
await SplitTestHandler.promises.getAssignment(
req,
res,
diff --git a/services/web/app/src/models/GroupPolicy.mjs b/services/web/app/src/models/GroupPolicy.mjs
index 4e87d1fbb1..2d8a5c903c 100644
--- a/services/web/app/src/models/GroupPolicy.mjs
+++ b/services/web/app/src/models/GroupPolicy.mjs
@@ -30,6 +30,9 @@ export const GroupPolicySchema = new Schema(
// User can't use the Dropbox feature
userCannotUseDropbox: Boolean,
+
+ // User can't use the shared workspace feature
+ userCannotUseSharedWorkspace: Boolean,
},
{ minimize: false }
)
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 11478096eb..3df344bf53 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -499,9 +499,13 @@
"disable_equation_preview": "",
"disable_equation_preview_confirm": "",
"disable_equation_preview_enable_in_settings": "",
+ "disable_shared_workspace": "",
+ "disable_shared_workspace_modal_body": "",
+ "disable_shared_workspace_modal_title": "",
"disable_single_sign_on": "",
"disable_sso": "",
"disable_stop_on_first_error": "",
+ "disable_workspace": "",
"disabled": "",
"disabling": "",
"disagree": "",
@@ -645,8 +649,12 @@
"enable": "",
"enable_ai_features": "",
"enable_managed_users": "",
+ "enable_shared_workspace": "",
+ "enable_shared_workspace_modal_body": "",
+ "enable_shared_workspace_modal_title": "",
"enable_single_sign_on": "",
"enable_sso": "",
+ "enable_workspace": "",
"enabled": "",
"enables_real_time_syntax_checking_in_the_editor": "",
"enabling": "",
@@ -1896,6 +1904,12 @@
"share_project": "",
"share_project_name": "",
"shared_with_you": "",
+ "shared_workspace": "",
+ "shared_workspace_failed_to_disable": "",
+ "shared_workspace_failed_to_enable": "",
+ "shared_workspace_is_enabled": "",
+ "shared_workspace_managed_by_overleaf_support": "",
+ "shared_workspace_section_subtext": "",
"sharelatex_beta_program": "",
"sharing_permissions": "",
"shortcut_to_open_advanced_reference_search": "",
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx b/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx
index aefa2dda13..b226bcbcce 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx
@@ -100,6 +100,7 @@ export default function ManagedGroupSubscriptions() {
const isSharingPermissionsEnabled = useFeatureFlag(
'sharing-updates-sharing-permissions'
)
+ const isSharedWorkspaceEnabled = useFeatureFlag('shared-workspace')
const aiTogglingSplitTestEnabled = useFeatureFlag('ai-toggling')
if (!managedGroupSubscriptions) {
@@ -115,12 +116,18 @@ 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. When the `ai-toggling` split
- // test is enabled, it's always displayed for non-managed groups that have the feature on.
- // This flag will be deleted once AI Features toggling is available to all groups.
+ // Shared Workspace is available to both managed and non-managed groups, so while the
+ // `shared-workspace` split test has the group opted in, the section is rendered whenever
+ // Overleaf Support hasn't disabled the feature for them.
+ // AI Features toggling is narrower: it's only interactive for managed groups, so for
+ // non-managed groups the section falls back to displaying notifications for features that
+ // have been disabled by Overleaf Support. When the `ai-toggling` split test is enabled,
+ // it's also displayed for non-managed groups that have the feature on. Both flags will be
+ // deleted once their features are available to all groups.
const shouldDisplayFeatureControls =
+ (isSharedWorkspaceEnabled &&
+ subscription.planLevelName === 'Pro' &&
+ subscription.features?.sharedWorkspace !== false) ||
(subscription.features?.aiToggling && aiTogglingSplitTestEnabled) ||
subscription.managedUsersEnabled ||
subscription.groupPolicy?.userCannotUseAIFeatures ||
diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts
index 89c9b8b301..7eb5840882 100644
--- a/services/web/frontend/js/utils/meta.ts
+++ b/services/web/frontend/js/utils/meta.ts
@@ -296,6 +296,7 @@ export interface Meta {
'ol-script-logs': ScriptLogType[]
'ol-settingsGroupSSO': { enabled: boolean } | undefined
'ol-settingsPlans': Plan[]
+ 'ol-sharedWorkspaceEnabled': boolean
'ol-sharingPermissions': SharingPermissions
'ol-shouldAllowEditingDetails': boolean
'ol-shouldLoadHotjar': boolean
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index f13f34cf43..417f67506f 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -642,9 +642,13 @@
"disable_equation_preview": "Disable equation preview",
"disable_equation_preview_confirm": "This will disable equation preview for you in all projects.",
"disable_equation_preview_enable_in_settings": "You can enable it again in Settings.",
+ "disable_shared_workspace": "Disable shared workspace",
+ "disable_shared_workspace_modal_body": "Projects in the workspace will be transferred to their owners and shared only with their active collaborators. Group members will no longer have access to the shared workspace.",
+ "disable_shared_workspace_modal_title": "Disable shared workspace?",
"disable_single_sign_on": "Disable single sign-on",
"disable_sso": "Disable SSO",
"disable_stop_on_first_error": "Disable “Stop on first error”",
+ "disable_workspace": "Disable workspace",
"disabled": "Disabled",
"disabling": "Disabling",
"disagree": "Disagree",
@@ -817,8 +821,12 @@
"enable": "Enable",
"enable_ai_features": "Enable AI features",
"enable_managed_users": "Enable Managed Users",
+ "enable_shared_workspace": "Enable shared workspace",
+ "enable_shared_workspace_modal_body": "Projects added to the workspace will be visible to the whole group. Once enabled, shared workspace can’t be turned off without transferring projects back to their owners.",
+ "enable_shared_workspace_modal_title": "Enable shared workspace?",
"enable_single_sign_on": "Enable single sign-on",
"enable_sso": "Enable SSO",
+ "enable_workspace": "Enable workspace",
"enabled": "Enabled",
"enables_real_time_syntax_checking_in_the_editor": "Enables real-time syntax checking in the editor",
"enabling": "Enabling",
@@ -2429,6 +2437,12 @@
"share_project": "Share Project",
"share_project_name": "Share __projectName__",
"shared_with_you": "Shared with you",
+ "shared_workspace": "Shared workspace",
+ "shared_workspace_failed_to_disable": "Shared workspace couldn’t be disabled. Try again or contact __appName__ support if the problem continues.",
+ "shared_workspace_failed_to_enable": "Shared workspace couldn’t be enabled. Try again or contact __appName__ support if the problem continues.",
+ "shared_workspace_is_enabled": "Shared workspace is enabled",
+ "shared_workspace_managed_by_overleaf_support": "Shared workspace is managed by __appName__ support.",
+ "shared_workspace_section_subtext": "All group members can see and access projects in the shared workspace.",
"sharelatex_beta_program": "__appName__ beta program",
"sharing_permissions": "Sharing permissions",
"shortcut_to_open_advanced_reference_search": "(__ctrlSpace__ or __altSpace__)",
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx
index e30accd1f4..85847c2c83 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx
@@ -2,6 +2,7 @@ import { expect } from 'chai'
import { screen } from '@testing-library/react'
import ManagedGroupSubscriptions from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-group-subscriptions'
import { ManagedGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
+import { MetaTag } from '@/utils/meta'
import {
cleanUpContext,
renderWithSubscriptionDashContext,
@@ -531,5 +532,139 @@ describe('', function () {
expect(screen.queryByText('Feature controls')).to.be.null
})
})
+
+ describe('Shared Workspace', function () {
+ // metaAttributesCache isn't cleared between tests, so ol-splitTestVariants leaks from
+ // earlier tests. Every test below sets it explicitly to stay independent of run order.
+ function splitTestVariants(enabled: boolean): MetaTag {
+ return {
+ name: 'ol-splitTestVariants',
+ value: enabled ? { 'shared-workspace': 'enabled' } : {},
+ }
+ }
+
+ it('renders the feature controls row for a non-managed group when the feature is enabled', async function () {
+ renderWithSubscriptionDashContext(, {
+ metaTags: [
+ {
+ name: 'ol-managedGroupSubscriptions',
+ value: makeSubscription({
+ managedUsersEnabled: false,
+ features: {
+ groupSSO: false,
+ managedUsers: false,
+ sharedWorkspace: true,
+ },
+ }),
+ },
+ { name: 'ol-usersEmail', value: adminEmail },
+ splitTestVariants(true),
+ ],
+ })
+ await screen.findByText('Feature controls')
+ })
+
+ it('renders the feature controls row for a non-managed group when the feature is unset', async function () {
+ renderWithSubscriptionDashContext(, {
+ metaTags: [
+ {
+ name: 'ol-managedGroupSubscriptions',
+ value: makeSubscription({
+ managedUsersEnabled: false,
+ features: { groupSSO: false, managedUsers: false },
+ }),
+ },
+ { name: 'ol-usersEmail', value: adminEmail },
+ splitTestVariants(true),
+ ],
+ })
+ await screen.findByText('Feature controls')
+ })
+
+ it('does not render the feature controls row when the split test is not enabled', function () {
+ renderWithSubscriptionDashContext(, {
+ metaTags: [
+ {
+ name: 'ol-managedGroupSubscriptions',
+ value: makeSubscription({
+ managedUsersEnabled: false,
+ features: {
+ groupSSO: false,
+ managedUsers: false,
+ sharedWorkspace: true,
+ },
+ }),
+ },
+ { name: 'ol-usersEmail', value: adminEmail },
+ splitTestVariants(false),
+ ],
+ })
+ expect(screen.queryByText('Feature controls')).to.be.null
+ })
+
+ it('does not render the feature controls row when Overleaf Support has disabled the feature', function () {
+ renderWithSubscriptionDashContext(, {
+ metaTags: [
+ {
+ name: 'ol-managedGroupSubscriptions',
+ value: makeSubscription({
+ managedUsersEnabled: false,
+ features: {
+ groupSSO: false,
+ managedUsers: false,
+ sharedWorkspace: false,
+ },
+ }),
+ },
+ { name: 'ol-usersEmail', value: adminEmail },
+ splitTestVariants(true),
+ ],
+ })
+ expect(screen.queryByText('Feature controls')).to.be.null
+ })
+
+ it('does not render the feature controls row when the user is not the group admin', function () {
+ renderWithSubscriptionDashContext(, {
+ metaTags: [
+ {
+ name: 'ol-managedGroupSubscriptions',
+ value: makeSubscription({
+ managedUsersEnabled: false,
+ features: {
+ groupSSO: false,
+ managedUsers: false,
+ sharedWorkspace: true,
+ },
+ }),
+ },
+ { name: 'ol-usersEmail', value: 'other@example.com' },
+ splitTestVariants(true),
+ ],
+ })
+ expect(screen.queryByText('Feature controls')).to.be.null
+ })
+
+ it('does not render the feature controls row when the group is not on a Pro plan', function () {
+ renderWithSubscriptionDashContext(, {
+ metaTags: [
+ {
+ name: 'ol-managedGroupSubscriptions',
+ value: makeSubscription({
+ managedUsersEnabled: false,
+ planLevelName: 'Standard',
+ features: {
+ groupSSO: false,
+ managedUsers: false,
+ sharedWorkspace: true,
+ },
+ }),
+ },
+ { name: 'ol-usersEmail', value: adminEmail },
+ splitTestVariants(true),
+ ],
+ })
+ expect(screen.queryByText('Feature controls')).to.be.null
+ })
+ })
})
})
diff --git a/services/web/test/unit/src/Email/EmailBuilder.test.mjs b/services/web/test/unit/src/Email/EmailBuilder.test.mjs
index 050f01e870..66c97eea4a 100644
--- a/services/web/test/unit/src/Email/EmailBuilder.test.mjs
+++ b/services/web/test/unit/src/Email/EmailBuilder.test.mjs
@@ -1122,6 +1122,183 @@ describe('EmailBuilder', function () {
})
})
+ describe('groupSharedWorkspaceDisabledForOwner', function () {
+ beforeEach(function (ctx) {
+ ctx.emailAddress = 'example@overleaf.com'
+ ctx.opts = {
+ to: ctx.emailAddress,
+ groupName: 'Example Group',
+ firstName: 'Joe',
+ }
+ ctx.email = ctx.EmailBuilder.buildEmail(
+ 'groupSharedWorkspaceDisabledForOwner',
+ ctx.opts
+ )
+ ctx.expectedUrl = `${ctx.settings.siteUrl}/project`
+ })
+
+ it('should build the email', function (ctx) {
+ expect(ctx.email.html).to.exist
+ expect(ctx.email.text).to.exist
+ })
+
+ describe('HTML email', function () {
+ it('should include a CTA button and a fallback CTA link', function (ctx) {
+ const dom = cheerio.load(ctx.email.html)
+ expect(ctx.email.subject).to.equal(
+ 'Your shared workspace has been disabled'
+ )
+ expect(ctx.email.html).to.contain(
+ 'Your projects are back in your personal list.'
+ )
+ expect(ctx.email.html).to.contain(
+ `Your group administrator has disabled the shared workspace for ${ctx.opts.groupName}.`
+ )
+ const buttonLink = dom('a:contains("Open your projects")')
+ expect(buttonLink.length).to.equal(1)
+ expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
+ expect(ctx.email.html).to.contain('copy and paste this link')
+ expect(ctx.email.html).to.contain(ctx.expectedUrl)
+ })
+ })
+
+ describe('plain text email', function () {
+ it('should contain the CTA link', function (ctx) {
+ expect(ctx.email.text).to.contain(ctx.expectedUrl)
+ })
+ })
+
+ it('escapes the group name in the HTML email', function (ctx) {
+ const email = ctx.EmailBuilder.buildEmail(
+ 'groupSharedWorkspaceDisabledForOwner',
+ { ...ctx.opts, groupName: '' }
+ )
+ expect(email.html).to.not.contain('')
+ expect(email.html).to.contain('<script>alert(1)</script>')
+ })
+ })
+
+ describe('groupSharedWorkspaceDisabledForMember', function () {
+ beforeEach(function (ctx) {
+ ctx.emailAddress = 'example@overleaf.com'
+ ctx.opts = {
+ to: ctx.emailAddress,
+ groupName: 'Example Group',
+ firstName: 'Joe',
+ }
+ ctx.email = ctx.EmailBuilder.buildEmail(
+ 'groupSharedWorkspaceDisabledForMember',
+ ctx.opts
+ )
+ ctx.expectedUrl = `${ctx.settings.siteUrl}/project`
+ })
+
+ it('should build the email', function (ctx) {
+ expect(ctx.email.html).to.exist
+ expect(ctx.email.text).to.exist
+ })
+
+ describe('HTML email', function () {
+ it('should include a CTA button and a fallback CTA link', function (ctx) {
+ const dom = cheerio.load(ctx.email.html)
+ expect(ctx.email.subject).to.equal(
+ "Your group's shared workspace has been disabled"
+ )
+ expect(ctx.email.html).to.contain(
+ "Here's what's changed for your group."
+ )
+ expect(ctx.email.html).to.contain(
+ `Your group administrator has disabled the shared workspace for ${ctx.opts.groupName}.`
+ )
+ const buttonLink = dom('a:contains("Open your projects")')
+ expect(buttonLink.length).to.equal(1)
+ expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
+ expect(ctx.email.html).to.contain('copy and paste this link')
+ expect(ctx.email.html).to.contain(ctx.expectedUrl)
+ })
+ })
+
+ describe('plain text email', function () {
+ it('should contain the CTA link', function (ctx) {
+ expect(ctx.email.text).to.contain(ctx.expectedUrl)
+ })
+ })
+
+ it('escapes the group name in the HTML email', function (ctx) {
+ const email = ctx.EmailBuilder.buildEmail(
+ 'groupSharedWorkspaceDisabledForMember',
+ { ...ctx.opts, groupName: '' }
+ )
+ expect(email.html).to.not.contain('')
+ expect(email.html).to.contain('<script>alert(1)</script>')
+ })
+ })
+
+ describe('groupSharedWorkspaceDisabledForAdmin', function () {
+ beforeEach(function (ctx) {
+ ctx.emailAddress = 'example@overleaf.com'
+ ctx.opts = {
+ to: ctx.emailAddress,
+ groupName: 'Example Group',
+ firstName: 'Joe',
+ }
+ ctx.email = ctx.EmailBuilder.buildEmail(
+ 'groupSharedWorkspaceDisabledForAdmin',
+ ctx.opts
+ )
+ ctx.expectedUrl = `${ctx.settings.siteUrl}/project`
+ })
+
+ it('should build the email', function (ctx) {
+ expect(ctx.email.html).to.exist
+ expect(ctx.email.text).to.exist
+ })
+
+ describe('HTML email', function () {
+ it('should include a CTA button and a fallback CTA link', function (ctx) {
+ const dom = cheerio.load(ctx.email.html)
+ expect(ctx.email.subject).to.equal(
+ `Shared workspace disabled for ${ctx.opts.groupName}`
+ )
+ expect(ctx.email.html).to.contain(
+ "Here's a summary of what changed for your group."
+ )
+ expect(ctx.email.html).to.contain(
+ `You’ve disabled the shared workspace for ${ctx.opts.groupName}. Here's a summary of what happened.`
+ )
+ expect(ctx.email.html).to.contain(
+ 'Projects in the workspace have been returned to their owners and are no longer visible to the whole group.'
+ )
+ expect(ctx.email.html).to.contain(
+ 'Each project is now shared only with the collaborators the owner had already added.'
+ )
+ expect(ctx.email.html).to.contain(
+ 'Group members have been notified of the change.'
+ )
+ const buttonLink = dom('a:contains("Open your projects")')
+ expect(buttonLink.length).to.equal(1)
+ expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
+ expect(ctx.email.html).to.contain('copy and paste this link')
+ expect(ctx.email.html).to.contain(ctx.expectedUrl)
+ })
+ })
+
+ describe('plain text email', function () {
+ it('should contain the CTA link', function (ctx) {
+ expect(ctx.email.text).to.contain(ctx.expectedUrl)
+ })
+ })
+
+ it('escapes the group name in the HTML email', function (ctx) {
+ const email = ctx.EmailBuilder.buildEmail(
+ 'groupSharedWorkspaceDisabledForAdmin',
+ { ...ctx.opts, groupName: '' }
+ )
+ expect(email.html).to.not.contain('')
+ expect(email.html).to.contain('<script>alert(1)</script>')
+ })
+ })
+
describe('groupDomainCapturedByGroupChanged', function () {
it('should build active-domain-capture email', function (ctx) {
const email = ctx.EmailBuilder.buildEmail(
diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts
index b19cf08dba..1050106433 100644
--- a/services/web/types/subscription/dashboard/subscription.ts
+++ b/services/web/types/subscription/dashboard/subscription.ts
@@ -102,6 +102,7 @@ export type ManagedGroupSubscription = {
groupSSO: boolean | null
managedUsers: boolean | null
aiToggling?: boolean | null
+ sharedWorkspace?: boolean | null
}
managedUsersEnabled: boolean
groupPolicy?: GroupPolicy