Merge pull request #31397 from overleaf/mj-old-editor-toolbar-teardown

[web] Tear down old editor toolbar

GitOrigin-RevId: 8ba74abcc56e7bd476a9d6cae72f38486168c2ed
This commit is contained in:
Mathias Jakobsen
2026-02-11 09:07:14 +00:00
committed by Copybot
parent 4f284e15d5
commit cc1dedca7d
32 changed files with 463 additions and 1796 deletions
@@ -1,26 +0,0 @@
import { expect } from 'chai'
import { render, screen } from '@testing-library/react'
import ChatToggleButton from '../../../../../frontend/js/features/editor-navigation-toolbar/components/chat-toggle-button'
describe('<ChatToggleButton />', function () {
const defaultProps = {
chatIsOpen: false,
unreadMessageCount: 0,
onClick: () => {},
}
it('displays the number of unread messages', function () {
const props = {
...defaultProps,
unreadMessageCount: 113,
}
render(<ChatToggleButton {...props} />)
screen.getByText('113')
})
it("doesn't display the unread messages badge when the number of unread messages is zero", function () {
render(<ChatToggleButton {...defaultProps} />)
expect(screen.queryByText('0')).to.not.exist
})
})
@@ -1,237 +0,0 @@
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
import { expect } from 'chai'
import { screen, waitFor } from '@testing-library/react'
import LayoutDropdownButton from '../../../../../frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import * as eventTracking from '@/infrastructure/event-tracking'
import type { LayoutContextOwnStates } from '@/shared/context/layout-context'
describe('<LayoutDropdownButton />', function () {
let openStub: sinon.SinonStub
let sendMBSpy: sinon.SinonSpy
const defaultLayout: Partial<LayoutContextOwnStates> = {
pdfLayout: 'flat',
view: 'pdf',
chatIsOpen: false,
}
beforeEach(function () {
openStub = sinon.stub(window, 'open')
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
})
afterEach(function () {
openStub.restore()
sendMBSpy.restore()
fetchMock.removeRoutes().clearHistory()
})
it('should mark current layout option as selected', async function () {
// Selected is aria-label, visually we show a checkmark
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: defaultLayout,
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
)
expect(
screen
.getByRole('menuitem', {
name: 'PDF only (hide editor)',
})
.getAttribute('aria-selected')
).to.equal('true')
expect(
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.getAttribute('aria-selected')
).to.equal('false')
expect(
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.getAttribute('aria-selected')
).to.equal('false')
})
it('should not select any option in history view', async function () {
// Selected is aria-label, visually we show a checkmark
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: { ...defaultLayout, view: 'history' },
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
)
expect(
screen
.getByRole('menuitem', {
name: 'PDF only (hide editor)',
})
.getAttribute('aria-selected')
).to.equal('false')
expect(
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.getAttribute('aria-selected')
).to.equal('false')
expect(
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.getAttribute('aria-selected')
).to.equal('false')
})
it('should treat file and editor views the same way', async function () {
// Selected is aria-label, visually we show a checkmark
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: {
pdfLayout: 'flat',
view: 'file',
chatIsOpen: false,
},
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
)
expect(
screen
.getByRole('menuitem', {
name: 'PDF only (hide editor)',
})
.getAttribute('aria-selected')
).to.equal('false')
expect(
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.getAttribute('aria-selected')
).to.equal('true')
expect(
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.getAttribute('aria-selected')
).to.equal('false')
})
describe('on detach', async function () {
const originalBroadcastChannel = window.BroadcastChannel
beforeEach(async function () {
// @ts-expect-error
window.BroadcastChannel = true // ensure that window.BroadcastChannel is truthy
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: { ...defaultLayout, view: 'editor' },
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.click()
)
})
afterEach(function () {
window.BroadcastChannel = originalBroadcastChannel
})
it('should show processing', async function () {
await screen.findByText('Layout processing')
})
it('should record event', function () {
sinon.assert.calledWith(sendMBSpy, 'project-layout-detach')
})
})
describe('on layout change / reattach', async function () {
beforeEach(async function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: { ...defaultLayout, view: 'editor' },
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.click()
)
})
it('should not show processing', function () {
const processingText = screen.queryByText('Layout processing')
expect(processingText).to.not.exist
})
it('should record events', function () {
sinon.assert.calledWith(sendMBSpy, 'project-layout-reattach')
sinon.assert.calledWith(sendMBSpy, 'project-layout-change', {
layout: 'flat',
view: 'editor',
page: '/detacher',
'editor-redesign': 'enabled',
})
})
it('should select new menu item', function () {
screen.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
})
})
})
@@ -1,115 +1,113 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { fireEvent, render, screen } from '@testing-library/react'
import { OnlineUsersWidget } from '@/features/editor-navigation-toolbar/components/online-users-widget'
import OnlineUsersWidget from '../../../../../frontend/js/features/editor-navigation-toolbar/components/online-users-widget'
const names = ['alice', 'bob', 'charlie', 'dave', 'erin', 'frank', 'grace']
function makeUsers(count: number) {
return Array.from({ length: count }, (_, index) => ({
id: `user_${index + 1}`,
user_id: `user_id_${index + 1}`,
name: names[index % names.length],
email: `${names[index % names.length]}_email`,
}))
}
describe('<OnlineUsersWidget />', function () {
const defaultProps = {
onlineUsers: [
{
id: 'test_user',
user_id: 'test_user',
name: 'test_user',
email: 'test_email',
},
{
id: 'another_test_user',
user_id: 'another_test_user',
name: 'another_test_user',
email: 'another_test_email',
},
],
goToUser: (async () => {}) as any,
onlineUsers: makeUsers(2),
goToUser: sinon.stub(),
}
describe('with less than 4 users', function () {
describe('with less than 5 users', function () {
it('displays user initials', function () {
render(<OnlineUsersWidget {...defaultProps} />)
screen.getByText('t')
screen.getByText('a')
screen.getByText('b')
})
it('displays user name in a tooltip', async function () {
render(<OnlineUsersWidget {...defaultProps} />)
const icon = screen.getByText('t')
const icon = screen.getByText('a')
fireEvent.mouseOver(icon)
await screen.findByRole('tooltip', { name: 'test_user' })
await screen.findByRole('tooltip', { name: 'alice' })
})
it('calls "goToUser" when the user initial is clicked', function () {
const props = {
...defaultProps,
goToUser: sinon.stub(),
}
render(<OnlineUsersWidget {...props} />)
render(<OnlineUsersWidget {...defaultProps} />)
const icon = screen.getByText('t')
const icon = screen.getByText('a')
fireEvent.click(icon)
expect(props.goToUser).to.be.calledWith({
id: 'test_user',
user_id: 'test_user',
name: 'test_user',
email: 'test_email',
expect(defaultProps.goToUser).to.be.calledWith({
id: 'user_1',
user_id: 'user_id_1',
name: 'alice',
email: 'alice_email',
})
})
})
describe('with 4 users and more', function () {
describe('with 5 users', function () {
const props = {
...defaultProps,
onlineUsers: defaultProps.onlineUsers.concat([
{
id: 'user_3',
user_id: 'user_3',
name: 'user_3',
email: 'user_3',
},
{
id: 'user_4',
user_id: 'user_4',
name: 'user_4',
email: 'user_4',
},
]),
onlineUsers: makeUsers(5),
}
it('displays the count of users', function () {
it('displays user initials', function () {
render(<OnlineUsersWidget {...props} />)
screen.getByText('4')
screen.getByText('a')
screen.getByText('b')
screen.getByText('c')
screen.getByText('d')
screen.getByText('e')
})
})
describe('with more than 5 users', function () {
const props = {
...defaultProps,
onlineUsers: makeUsers(7),
}
it('displays a maximum of 4 user initials and an overflow icon', function () {
render(<OnlineUsersWidget {...props} />)
screen.getByText('a')
screen.getByText('b')
screen.getByText('c')
screen.getByText('d')
screen.getByText('+3')
})
it('displays user names on hover', function () {
it('displays the remaining users in a dropdown when the overflow icon is clicked', async function () {
render(<OnlineUsersWidget {...props} />)
const overflowButton = screen.getByText('+3')
fireEvent.click(overflowButton)
const toggleButton = screen.getByRole('button')
fireEvent.click(toggleButton)
screen.getByText('test_user')
screen.getByText('another_test_user')
screen.getByText('user_3')
screen.getByText('user_4')
await screen.findByText('erin')
await screen.findByText('frank')
await screen.findByText('grace')
})
it('calls "goToUser" when the user name is clicked', function () {
const testProps = {
...props,
goToUser: sinon.stub(),
it('calls "goToUser" when a user in the overflow dropdown is clicked', async function () {
const props = {
...defaultProps,
onlineUsers: makeUsers(7),
}
render(<OnlineUsersWidget {...testProps} />)
const toggleButton = screen.getByRole('button')
fireEvent.click(toggleButton)
render(<OnlineUsersWidget {...props} />)
const overflowButton = screen.getByText('+3')
fireEvent.click(overflowButton)
const icon = screen.getByText('user_3')
fireEvent.click(icon)
const frankButton = await screen.findByText('frank')
fireEvent.click(frankButton)
expect(testProps.goToUser).to.be.calledWith({
id: 'user_3',
user_id: 'user_3',
name: 'user_3',
email: 'user_3',
expect(props.goToUser).to.be.calledWith({
id: 'user_6',
user_id: 'user_id_6',
name: 'frank',
email: 'frank_email',
})
})
})
@@ -1,78 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { fireEvent, render, screen } from '@testing-library/react'
import ProjectNameEditableLabel from '../../../../../frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label'
describe('<ProjectNameEditableLabel />', function () {
const defaultProps = { projectName: 'test-project', onChange: () => {} }
it('displays the project name', function () {
render(<ProjectNameEditableLabel {...defaultProps} />)
screen.getByText('test-project')
})
describe('when the name is editable', function () {
const editableProps = { ...defaultProps, hasRenamePermissions: true }
it('displays an editable input when the edit button is clicked', function () {
render(<ProjectNameEditableLabel {...editableProps} />)
fireEvent.click(screen.getByRole('button'))
screen.getByRole('textbox')
})
it('displays an editable input when the project name is double clicked', function () {
render(<ProjectNameEditableLabel {...editableProps} />)
fireEvent.doubleClick(screen.getByText('test-project'))
screen.getByRole('textbox')
})
it('calls "onChange" when the project name is updated', function () {
const props = {
...editableProps,
onChange: sinon.stub(),
}
render(<ProjectNameEditableLabel {...props} />)
fireEvent.doubleClick(screen.getByText('test-project'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'new project name' } })
fireEvent.keyDown(input, { key: 'Enter' })
expect(props.onChange).to.be.calledWith('new project name')
})
it('calls "onChange" when the input loses focus', function () {
const props = {
...editableProps,
onChange: sinon.stub(),
}
render(<ProjectNameEditableLabel {...props} />)
fireEvent.doubleClick(screen.getByText('test-project'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'new project name' } })
fireEvent.blur(screen.getByRole('textbox'))
expect(props.onChange).to.be.calledWith('new project name')
})
})
describe('when the name is not editable', function () {
const nonEditableProps = { hasRenamePermissions: false, ...defaultProps }
it('the edit button is not displayed', function () {
render(<ProjectNameEditableLabel {...nonEditableProps} />)
expect(screen.queryByRole('button')).to.not.exist
})
it('does not display an editable input when the project name is double clicked', function () {
render(<ProjectNameEditableLabel {...nonEditableProps} />)
fireEvent.doubleClick(screen.getByText('test-project'))
expect(screen.queryByRole('textbox')).to.not.exist
})
})
})
@@ -1,121 +0,0 @@
import { expect } from 'chai'
import { screen } from '@testing-library/react'
import ToolbarHeader, {
type ToolbarHeaderProps,
} from '../../../../../frontend/js/features/editor-navigation-toolbar/components/toolbar-header'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
describe('<ToolbarHeader />', function () {
const defaultProps: ToolbarHeaderProps = {
onShowLeftMenuClick: () => {},
toggleChatOpen: () => {},
toggleReviewPanelOpen: () => {},
toggleHistoryOpen: () => {},
unreadMessageCount: 0,
onlineUsers: [],
goToUser: (async () => {}) as any,
projectName: 'test project',
renameProject: () => {},
openShareModal: () => {},
hasPublishPermissions: true,
chatVisible: true,
trackChangesVisible: true,
cobranding: undefined,
isRestrictedTokenMember: false,
hasRenamePermissions: true,
historyIsOpen: false,
chatIsOpen: false,
reviewPanelOpen: false,
}
beforeEach(function () {
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
describe('cobranding logo', function () {
it('is not displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
expect(screen.queryByRole('link', { name: 'variation' })).to.not.exist
})
it('is displayed when cobranding data is available', function () {
const props = {
...defaultProps,
cobranding: {
brandId: 12,
brandVariationId: 12,
brandVariationHomeUrl: 'http://cobranding',
brandVariationName: 'variation',
logoImgUrl: 'http://cobranding/logo',
},
}
renderWithEditorContext(<ToolbarHeader {...props} />)
screen.getByRole('link', { name: 'variation' })
})
})
describe('track changes toggle button', function () {
it('is displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
screen.getByText('Review')
})
it('is not displayed when "trackChangesVisible" prop is set to false', function () {
const props = {
...defaultProps,
trackChangesVisible: false,
}
renderWithEditorContext(<ToolbarHeader {...props} />)
expect(screen.queryByText('Review')).to.not.exist
})
})
describe('History toggle button', function () {
it('is displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
screen.getByText('History')
})
it('is not displayed when "isRestrictedTokenMember" prop is set to true', function () {
const props = {
...defaultProps,
isRestrictedTokenMember: true,
}
renderWithEditorContext(<ToolbarHeader {...props} />)
expect(screen.queryByText('History')).to.not.exist
})
})
describe('Chat toggle button', function () {
it('is displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
screen.getByText('Chat')
})
it('is not displayed when "chatVisible" prop is set to false', function () {
const props = {
...defaultProps,
chatVisible: false,
}
renderWithEditorContext(<ToolbarHeader {...props} />)
expect(screen.queryByText('Chat')).to.not.exist
})
})
describe('Publish button', function () {
it('is displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
screen.getByText('Submit')
})
it('is not displayed for users with no publish permissions', function () {
const props = {
...defaultProps,
hasPublishPermissions: false,
}
renderWithEditorContext(<ToolbarHeader {...props} />)
expect(screen.queryByText('Submit')).to.not.exist
})
})
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,248 @@
import { Toolbar } from '@/features/ide-react/components/toolbar/toolbar'
import {
EditorProviders,
makeEditorProvider,
} from '../../../helpers/editor-providers'
import { Cobranding } from '@ol-types/cobranding'
import partnerLogoUrl from './cobranding-logo.png'
describe('<Toolbar />', function () {
describe('cobranding', function () {
beforeEach(function () {
const cobranding: Cobranding = {
logoImgUrl: partnerLogoUrl,
brandVariationName: 'brand variation name',
brandVariationId: 1000,
brandId: 2000,
brandVariationHomeUrl: 'https://brand.example.com',
publishGuideHtml: 'string',
partner: 'partner name',
brandedMenu: true,
submitBtnHtml: 'submit to\n partner',
submitBtnHtmlNoBreaks: 'submit to partner',
}
cy.mount(
<EditorProviders
providers={{ EditorProvider: makeEditorProvider({ cobranding }) }}
>
<Toolbar />
</EditorProviders>
)
})
it('displays the cobranding logo', function () {
cy.get(`img[src="${partnerLogoUrl}"]`).should('be.visible')
})
it('shows the branded submit button', function () {
cy.findByRole('button', { name: 'submit to partner' }).should(
'be.visible'
)
})
})
describe('no cobranding', function () {
beforeEach(function () {
cy.mount(
<EditorProviders
providers={{
EditorProvider: makeEditorProvider({ cobranding: undefined }),
}}
>
<Toolbar />
</EditorProviders>
)
})
it('does not display a cobranding logo', function () {
cy.get('.ide-redesign-toolbar-cobranding-logo').should('not.exist')
})
it('does not show a submit button', function () {
cy.findByRole('button', { name: 'Submit' }).should('not.exist')
})
})
it('clicking the logo takes you to the home page', function () {
cy.mount(
<EditorProviders projectName="My title">
<Toolbar />
</EditorProviders>
)
cy.findByLabelText('Overleaf logo')
// We can't click the link in component tests, so just look at the
// parent directly
.parent()
.should('be.visible')
.should('have.attr', 'href', '/project')
})
describe('project title menu', function () {
beforeEach(function () {
cy.mount(
<EditorProviders
projectName="My title"
providers={{
EditorProvider: makeEditorProvider({
renameProject: cy.stub().as('rename-project'),
}),
}}
>
<Toolbar />
</EditorProviders>
)
})
it('displays the project title dropdown', function () {
cy.findByRole('button', { name: 'Project title options' })
.should('be.visible')
.should('contain.text', 'My title')
.click()
cy.findByRole('menu')
.should('exist')
.within(() => {
cy.findByRole('menuitem', { name: 'Download as PDF' }).should('exist')
cy.findByRole('menuitem', {
name: 'Download as source (.zip)',
}).should('exist')
cy.findByRole('menuitem', { name: 'Make a copy' }).should('exist')
cy.findByRole('menuitem', { name: 'Rename' }).should('exist')
})
})
it('allows the project to be renamed', function () {
cy.findByRole('button', { name: 'Project title options' }).click()
cy.findByRole('menuitem', { name: 'Rename' }).click()
cy.findByRole('textbox')
.should('be.visible')
.should('have.value', 'My title')
.type('New title{enter}')
cy.get('@rename-project').should('have.been.calledWith', 'New title')
})
it('should show modal when copying project', function () {
cy.findByRole('button', { name: 'Project title options' }).click()
cy.findByRole('menuitem', { name: 'Make a copy' }).click()
cy.findByRole('dialog')
.should('be.visible')
.should('contain.text', 'Copy project')
})
it('should show modal when pressing submit', function () {
cy.findByRole('button', { name: 'Project title options' }).click()
cy.findByRole('menuitem', { name: 'Submit' }).click()
cy.findByRole('dialog')
.should('be.visible')
.should('contain.text', 'Submit')
})
})
describe('history toggle', function () {
it('Should show history button', function () {
cy.mount(
<EditorProviders
projectName="My title"
providers={{
EditorProvider: makeEditorProvider({
isRestrictedTokenMember: false,
}),
}}
>
<Toolbar />
</EditorProviders>
)
cy.findByRole('button', { name: 'History' }).should('be.visible').click()
})
it('Should not show history button to restricted token members', function () {
cy.mount(
<EditorProviders
projectName="My title"
providers={{
EditorProvider: makeEditorProvider({
isRestrictedTokenMember: true,
}),
}}
>
<Toolbar />
</EditorProviders>
)
cy.findByRole('button', { name: 'History' }).should('not.exist')
})
})
it('should show share button', function () {
cy.mount(
<EditorProviders projectName="My title">
<Toolbar />
</EditorProviders>
)
cy.findByRole('button', { name: 'Share' }).should('be.visible').click()
cy.findByRole('dialog')
.should('be.visible')
.should('contain.text', 'Share Project')
})
it('should show layout button', function () {
cy.mount(
<EditorProviders projectName="My title">
<Toolbar />
</EditorProviders>
)
cy.findByRole('button', { name: 'Layout options' })
.should('be.visible')
.click()
cy.findByRole('menu')
.should('exist')
.within(() => {
cy.findByRole('menuitem', { name: 'Split view' }).should('exist')
cy.findByRole('menuitem', { name: 'Editor only' }).should('exist')
cy.findByRole('menuitem', { name: 'PDF only' }).should('exist')
cy.findByRole('menuitem', { name: 'Open PDF in separate tab' }).should(
'exist'
)
})
})
describe('for non-owner', function () {
it('should disable rename option', function () {
cy.mount(
<EditorProviders permissionsLevel="readOnly">
<Toolbar />
</EditorProviders>
)
cy.findByRole('button', { name: 'Project title options' }).click()
// FIXME: This should really be a button rather than a link
cy.findByRole('menuitem', { name: 'Rename' }).should(
'have.class',
'disabled'
)
})
})
describe('menu bar', function () {
beforeEach(function () {
cy.mount(
<EditorProviders>
<Toolbar />
</EditorProviders>
)
})
it('should have a menu bar role', function () {
cy.findByRole('menubar').should('be.visible')
})
it('should show file, view & help', function () {
cy.findByRole('menubar').within(() => {
cy.findByRole('button', { name: 'File' }).should('be.visible')
cy.findByRole('button', { name: 'View' }).should('be.visible')
cy.findByRole('button', { name: 'Help' }).should('be.visible')
})
})
// TODO: Test all the dynamic items
})
})
@@ -54,6 +54,7 @@ import { UserSettings } from '@ol-types/user-settings'
import { DetachCompileContext } from '@/shared/context/detach-compile-context'
import { type CompileContext } from '@/shared/context/local-compile-context'
import { EditorContext } from '@/shared/context/editor-context'
import { Cobranding } from '@ol-types/cobranding'
// these constants can be imported in tests instead of
// using magic strings
@@ -267,11 +268,21 @@ export function EditorProviders({
)
}
export function makeEditorProvider({ isProjectOwner = true } = {}) {
export function makeEditorProvider({
isProjectOwner = true,
cobranding = undefined,
renameProject = () => {},
isRestrictedTokenMember,
}: {
isProjectOwner?: boolean
cobranding?: Cobranding
renameProject?: () => void
isRestrictedTokenMember?: boolean
} = {}) {
const EditorProvider: FC<PropsWithChildren> = ({ children }) => {
const value = {
isProjectOwner,
renameProject: () => {},
renameProject,
isPendingEditor: false,
deactivateTutorial: () => {},
inactiveTutorials: [],
@@ -285,6 +296,8 @@ export function makeEditorProvider({ isProjectOwner = true } = {}) {
setWritefullInstance: () => {},
showUpgradeModal: false,
setShowUpgradeModal: () => {},
cobranding,
isRestrictedTokenMember,
}
return (