mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
[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:
@@ -56,7 +56,9 @@ import { formatCurrency } from '../../util/currency.js'
|
||||
import UserSettingsHelper from './UserSettingsHelper.mjs'
|
||||
import AiFeatureUsageRateLimiter from '../../infrastructure/rate-limiters/AiFeatureUsageRateLimiter.mjs'
|
||||
import WorkbenchRateLimiter from '../../infrastructure/rate-limiters/WorkbenchRateLimiter.mjs'
|
||||
import PermissionsManager from '../Authorization/PermissionsManager.mjs'
|
||||
|
||||
const { checkUserPermissions } = PermissionsManager.promises
|
||||
const { isPaidSubscription } = SubscriptionHelper
|
||||
const { hasAdminAccess } = AdminAuthorizationHelper
|
||||
const { ObjectId } = mongodb
|
||||
@@ -491,6 +493,7 @@ const _ProjectController = {
|
||||
'focus-mode',
|
||||
'editor-upgrade-button-relocation',
|
||||
'markdown-visual',
|
||||
'ai-disabled-collaborators',
|
||||
].filter(Boolean)
|
||||
|
||||
const getUserValues = async userId =>
|
||||
@@ -786,32 +789,28 @@ const _ProjectController = {
|
||||
!userHasPremiumSub &&
|
||||
!userInNonIndividualSub
|
||||
|
||||
let aiFeaturesAllowed = false
|
||||
let aiFeaturesAllowedForUser = false
|
||||
let aiFeaturesAllowedForProject = false
|
||||
if (userId && Features.hasFeature('saas')) {
|
||||
try {
|
||||
// exit early if the user couldnt use ai anyways, since permissions checks are expensive
|
||||
aiFeaturesAllowedForUser = await checkUserPermissions(user, [
|
||||
'use-ai',
|
||||
])
|
||||
|
||||
const canUserWriteOrReviewProjectContent =
|
||||
privilegeLevel === PrivilegeLevels.READ_AND_WRITE ||
|
||||
privilegeLevel === PrivilegeLevels.OWNER ||
|
||||
privilegeLevel === PrivilegeLevels.REVIEW
|
||||
|
||||
if (canUserWriteOrReviewProjectContent) {
|
||||
// check permissions for user and project owner, to see if they allow AI on the project
|
||||
const permissionsResults = await Modules.promises.hooks.fire(
|
||||
'projectAllowsCapability',
|
||||
project,
|
||||
userId,
|
||||
aiFeaturesAllowedForProject = await checkUserPermissions(
|
||||
project.owner_ref,
|
||||
['use-ai']
|
||||
)
|
||||
const aiAllowed = permissionsResults.every(
|
||||
result => result === true
|
||||
)
|
||||
|
||||
aiFeaturesAllowed = aiAllowed
|
||||
}
|
||||
} catch (err) {
|
||||
// still allow users to access project if we cant get their permissions, but disable AI feature
|
||||
aiFeaturesAllowed = false
|
||||
aiFeaturesAllowedForUser = false
|
||||
aiFeaturesAllowedForProject = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,7 +827,7 @@ const _ProjectController = {
|
||||
user,
|
||||
userValues,
|
||||
userId,
|
||||
aiFeaturesAllowed,
|
||||
aiFeaturesAllowedForUser && aiFeaturesAllowedForProject,
|
||||
userIsMemberOfGroupSubscription
|
||||
)
|
||||
|
||||
@@ -867,7 +866,16 @@ const _ProjectController = {
|
||||
const hasPaidSubscription = isPaidSubscription(subscription)
|
||||
const aiFeaturesDisabled = user.aiFeatures?.enabled === false
|
||||
|
||||
const showAiFeatures = aiFeaturesAllowed && !aiFeaturesDisabled
|
||||
let showAiFeatures = aiFeaturesAllowedForUser && !aiFeaturesDisabled
|
||||
let showAiFeaturesDisabled =
|
||||
showAiFeatures && !aiFeaturesAllowedForProject
|
||||
if (
|
||||
splitTestAssignments['ai-disabled-collaborators']?.variant !== 'enabled'
|
||||
) {
|
||||
showAiFeatures = showAiFeatures && !showAiFeaturesDisabled
|
||||
showAiFeaturesDisabled = false
|
||||
}
|
||||
|
||||
// only add-on is ai based, so we only need its pricing info if ai features are usable
|
||||
const addonPrices =
|
||||
showAiFeatures && (await ProjectController._getAddonPrices(req, res))
|
||||
@@ -985,6 +993,7 @@ const _ProjectController = {
|
||||
symbolPaletteAvailable: Features.hasFeature('symbol-palette'),
|
||||
userRestrictions: Array.from(req.userRestrictions || []),
|
||||
showAiFeatures,
|
||||
showAiFeaturesDisabled,
|
||||
// default to free tier if they dont have a quota
|
||||
hasAiFreeTier:
|
||||
fullFeatureSet?.aiUsageQuota === Settings.aiFeatures?.freeQuota ||
|
||||
|
||||
@@ -24,6 +24,7 @@ meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach)
|
||||
meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette)
|
||||
meta(name="ol-symbolPaletteAvailable" data-type="boolean" content=symbolPaletteAvailable)
|
||||
meta(name="ol-showAiFeatures" data-type="boolean" content=showAiFeatures)
|
||||
meta(name="ol-showAiFeaturesDisabled" data-type="boolean" content=showAiFeaturesDisabled)
|
||||
meta(name="ol-hasUnlimitedAi" data-type="boolean" content=hasUnlimitedAi)
|
||||
meta(name="ol-hasAiFreeTier" data-type="boolean" content=hasAiFreeTier)
|
||||
meta(name="ol-detachRole" data-type="string" content=detachRole)
|
||||
|
||||
@@ -124,8 +124,10 @@
|
||||
"ai_assist_unavailable": "",
|
||||
"ai_assistance": "",
|
||||
"ai_assistant": "",
|
||||
"ai_assistant_disabled": "",
|
||||
"ai_can_make_mistakes": "",
|
||||
"ai_features": "",
|
||||
"ai_features_unavailable_on_this_project": "",
|
||||
"ai_feedback_please_provide_more_detail": "",
|
||||
"ai_feedback_tell_us_what_was_wrong_so_we_can_improve": "",
|
||||
"ai_feedback_the_answer_was_too_long": "",
|
||||
|
||||
+10
-2
@@ -13,11 +13,15 @@ import { memo } from 'react'
|
||||
import OLListGroupItem from '@/shared/components/ol/ol-list-group-item'
|
||||
import sparkleWhite from '@/shared/svgs/sparkle-small-white.svg'
|
||||
import sparkle from '@/shared/svgs/ai-sparkle-text.svg'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export const MathDropdown = memo(function MathDropdown() {
|
||||
const { t } = useTranslation()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { writefullInstance } = useEditorContext()
|
||||
const showAiFeaturesDisabled = getMeta('ol-showAiFeaturesDisabled')
|
||||
|
||||
const renderAIButton = writefullInstance || showAiFeaturesDisabled
|
||||
|
||||
return (
|
||||
<ToolbarButtonMenu
|
||||
@@ -25,13 +29,15 @@ export const MathDropdown = memo(function MathDropdown() {
|
||||
label={t('toolbar_insert_math')}
|
||||
icon={<MaterialIcon type="calculate" />}
|
||||
>
|
||||
{writefullInstance && (
|
||||
{renderAIButton && (
|
||||
<>
|
||||
<DropdownHeader className="ol-cm-toolbar-header mx-2">
|
||||
{t('toolbar_insert_math_lowercase')}
|
||||
</DropdownHeader>
|
||||
<OLListGroupItem
|
||||
aria-label={t('toolbar_generate_math')}
|
||||
disabled={showAiFeaturesDisabled}
|
||||
disabledReason={t('ai_features_unavailable_on_this_project')}
|
||||
onClick={() => {
|
||||
writefullInstance?.openEquationGenerator()
|
||||
}}
|
||||
@@ -48,7 +54,9 @@ export const MathDropdown = memo(function MathDropdown() {
|
||||
src={sparkleWhite}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{t('generate_from_text_or_image')}</span>
|
||||
<span className={showAiFeaturesDisabled ? 'opacity-50' : ''}>
|
||||
{t('generate_from_text_or_image')}
|
||||
</span>
|
||||
</OLListGroupItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
+7
-1
@@ -14,10 +14,12 @@ import useDropdown from '../../../../shared/hooks/use-dropdown'
|
||||
import * as commands from '../../extensions/toolbar/commands'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export const TableDropdown = memo(function TableDropdown() {
|
||||
const { t } = useTranslation()
|
||||
const { writefullInstance } = useEditorContext()
|
||||
const showAiFeaturesDisabled = getMeta('ol-showAiFeaturesDisabled')
|
||||
const selectSizeDropdown = useDropdown()
|
||||
const target = useRef<any>(null)
|
||||
const view = useCodeMirrorViewContext()
|
||||
@@ -46,6 +48,8 @@ export const TableDropdown = memo(function TableDropdown() {
|
||||
</DropdownHeader>
|
||||
<OLListGroupItem
|
||||
aria-label={t('toolbar_generate_table')}
|
||||
disabled={showAiFeaturesDisabled}
|
||||
disabledReason={t('ai_features_unavailable_on_this_project')}
|
||||
onClick={() => {
|
||||
writefullInstance?.openTableGenerator()
|
||||
}}
|
||||
@@ -62,7 +66,9 @@ export const TableDropdown = memo(function TableDropdown() {
|
||||
src={sparkleWhite}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{t('generate_from_text_or_image')}</span>
|
||||
<span className={showAiFeaturesDisabled ? 'opacity-50' : ''}>
|
||||
{t('generate_from_text_or_image')}
|
||||
</span>
|
||||
</OLListGroupItem>
|
||||
<div className="ol-cm-toolbar-dropdown-divider mx-2 my-0" />
|
||||
<OLListGroupItem
|
||||
|
||||
+7
-1
@@ -43,6 +43,8 @@ export const ToolbarItems: FC<{
|
||||
const isActive = withinFormattingCommand(state)
|
||||
|
||||
const symbolPaletteAvailable = getMeta('ol-symbolPaletteAvailable')
|
||||
const showAiFeaturesDisabled = getMeta('ol-showAiFeaturesDisabled')
|
||||
|
||||
const showGroup = (group: string) => !overflowed || overflowed.has(group)
|
||||
|
||||
return (
|
||||
@@ -156,7 +158,11 @@ export const ToolbarItems: FC<{
|
||||
icon="book_5"
|
||||
/>
|
||||
<InsertFigureDropdown />
|
||||
{writefullInstance ? <TableDropdown /> : <LegacyTableDropdown />}
|
||||
{writefullInstance || showAiFeaturesDisabled ? (
|
||||
<TableDropdown />
|
||||
) : (
|
||||
<LegacyTableDropdown />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showGroup('group-list') && (
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { forwardRef, useId } from 'react'
|
||||
import { ListGroupItem, ListGroupItemProps } from 'react-bootstrap'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
|
||||
const OLListGroupItem = forwardRef<HTMLElement, ListGroupItemProps>(
|
||||
function OLListGroupItem(props, ref) {
|
||||
type OLListGroupItemProps = ListGroupItemProps & {
|
||||
disabledReason?: string
|
||||
}
|
||||
|
||||
const OLListGroupItem = forwardRef<HTMLElement, OLListGroupItemProps>(
|
||||
function OLListGroupItem({ disabledReason, ...props }, ref) {
|
||||
const as = props.as ?? 'button'
|
||||
const tooltipId = useId()
|
||||
|
||||
return (
|
||||
const showDisabledTooltip = Boolean(props.disabled && disabledReason)
|
||||
|
||||
const item = (
|
||||
<ListGroupItem
|
||||
{...props}
|
||||
ref={ref}
|
||||
@@ -13,6 +21,20 @@ const OLListGroupItem = forwardRef<HTMLElement, ListGroupItemProps>(
|
||||
type={as === 'button' ? 'button' : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
if (showDisabledTooltip) {
|
||||
return (
|
||||
<OLTooltip
|
||||
id={tooltipId}
|
||||
description={disabledReason}
|
||||
overlayProps={{ placement: 'right' }}
|
||||
>
|
||||
<span className="d-block">{item}</span>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -286,6 +286,7 @@ export interface Meta {
|
||||
'ol-shouldAllowEditingDetails': boolean
|
||||
'ol-shouldLoadHotjar': boolean
|
||||
'ol-showAiFeatures': boolean
|
||||
'ol-showAiFeaturesDisabled': boolean
|
||||
'ol-showCouponField': boolean
|
||||
'ol-showGroupDiscount': boolean
|
||||
'ol-showGroupsAndEnterpriseBanner': boolean
|
||||
|
||||
@@ -157,9 +157,11 @@
|
||||
"ai_assist_unavailable": "AI features are now included in every paid plan. See the <0>pricing page</0> for what each plan includes.",
|
||||
"ai_assistance": "AI assistance",
|
||||
"ai_assistant": "AI Assistant",
|
||||
"ai_assistant_disabled": "AI Assistant disabled",
|
||||
"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_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.",
|
||||
"ai_feedback_the_answer_was_too_long": "The answer was too long",
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user