[web] Disabled AI state for project collaborators (#34256)

* [web] Disabled AI state for project collaborators

Displays a disabled state, along with an informative tooltip in Editor AI controls.

GitOrigin-RevId: a749cd25d54f6ef3d7bf4276343400894bc193a4
This commit is contained in:
Miguel Serrano
2026-06-10 11:58:01 +00:00
committed by Copybot
parent a4d682719b
commit c981330cc1
10 changed files with 318 additions and 24 deletions
@@ -250,6 +250,12 @@ describe('ProjectController', function () {
}),
}
ctx.PermissionsManager = {
promises: {
checkUserPermissions: sinon.stub().resolves(false),
},
}
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
@@ -515,6 +521,13 @@ describe('ProjectController', function () {
})
)
vi.doMock(
'../../../../app/src/Features/Authorization/PermissionsManager',
() => ({
default: ctx.PermissionsManager,
})
)
ctx.ProjectController = (await import(MODULE_PATH)).default
ctx.projectName = '£12321jkj9ujkljds'
@@ -1510,6 +1523,230 @@ describe('ProjectController', function () {
})
})
})
describe('AI features availability', function () {
beforeEach(function (ctx) {
ctx.Features.hasFeature.withArgs('saas').returns(true)
ctx.Modules.promises.hooks.fire = sinon.stub().resolves([[true]])
ctx.settings.localizedAddOnsPricing = {
USD: {
assistant: {
annual: 60,
monthly: 5,
annualDividedByTwelve: 5,
},
},
}
})
it('should set showAiFeatures to true when the user has the use-ai permission', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.resolves(true)
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(true)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeatures to false when the user lacks the use-ai permission', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.resolves(false)
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeatures to false when the user has disabled ai features', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.resolves(true)
ctx.user.aiFeatures = { enabled: false }
ctx.UserModel.findById.returns({
exec: sinon.stub().resolves(ctx.user),
})
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeatures to false when the permission check throws', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.rejects(
new Error('permission check failed')
)
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeatures to false when the project owner permission check throws', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.resolves(true)
ctx.PermissionsManager.promises.checkUserPermissions
.onSecondCall()
.rejects(new Error('permission check failed'))
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeatures to false when the user can use ai but the project disallows it', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.callsFake(
subject => Promise.resolve(typeof subject !== 'string')
)
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(false)
expect(opts.showAiFeaturesDisabled).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeaturesDisabled to false when the user can use ai and the project allows it', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.resolves(true)
ctx.Modules.promises.hooks.fire = sinon.stub().resolves([true])
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeaturesDisabled).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeaturesDisabled to false when the user lacks the use-ai permission', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.resolves(false)
ctx.Modules.promises.hooks.fire = sinon.stub().resolves([[false]])
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeaturesDisabled).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeaturesDisabled to false when the user has disabled ai features', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.resolves(true)
ctx.Modules.promises.hooks.fire = sinon.stub().resolves([[false]])
ctx.user.aiFeatures = { enabled: false }
ctx.UserModel.findById.returns({
exec: sinon.stub().resolves(ctx.user),
})
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeaturesDisabled).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
describe("when the 'ai-disabled-collaborators' variant is enabled", function () {
beforeEach(function (ctx) {
ctx.SplitTestHandler.promises.getAssignment
.withArgs(ctx.req, ctx.res, 'ai-disabled-collaborators')
.resolves({ variant: 'enabled' })
})
it('should set showAiFeatures to true and showAiFeaturesDisabled to true when the user can use ai but the project disallows it', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.callsFake(
subject => Promise.resolve(typeof subject !== 'string')
)
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(true)
expect(opts.showAiFeaturesDisabled).to.equal(true)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeatures to true and showAiFeaturesDisabled to false when the user can use ai and the project allows it', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.resolves(true)
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(true)
expect(opts.showAiFeaturesDisabled).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeatures to false and showAiFeaturesDisabled to false when the user lacks the use-ai permission', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.resolves(false)
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(false)
expect(opts.showAiFeaturesDisabled).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
it('should set showAiFeatures to false and showAiFeaturesDisabled to false when the user has disabled ai features', async function (ctx) {
ctx.PermissionsManager.promises.checkUserPermissions.callsFake(
subject => Promise.resolve(typeof subject !== 'string')
)
ctx.user.aiFeatures = { enabled: false }
ctx.UserModel.findById.returns({
exec: sinon.stub().resolves(ctx.user),
})
await new Promise((resolve, reject) => {
ctx.res.render = (pageName, opts) => {
expect(opts.showAiFeatures).to.equal(false)
expect(opts.showAiFeaturesDisabled).to.equal(false)
resolve()
}
ctx.ProjectController.loadEditor(ctx.req, ctx.res, err => {
if (err) reject(err)
})
})
})
})
})
})
describe('userProjectsJson', function () {