Merge pull request #17373 from overleaf/jpa-figure-modal-options

[web] hide figure modal import options as configured server side

GitOrigin-RevId: f8907a33d413fcac238b00328eaba14d64f2f31b
This commit is contained in:
Miguel Serrano 2024-03-06 10:30:32 +01:00 committed by Copybot
parent ee4b8c0868
commit ac48d81987
7 changed files with 183 additions and 59 deletions

View file

@ -4,4 +4,7 @@ window.ExposedSettings = {
validRootDocExtensions: ['tex', 'Rtex', 'ltx', 'Rnw'], validRootDocExtensions: ['tex', 'Rtex', 'ltx', 'Rnw'],
fileIgnorePattern: fileIgnorePattern:
'**/{{__MACOSX,.git,.texpadtmp,.R}{,/**},.!(latexmkrc),*.{dvi,aux,log,toc,out,pdfsync,synctex,synctex(busy),fdb_latexmk,fls,nlo,ind,glo,gls,glg,bbl,blg,doc,docx,gz,swp}}', '**/{{__MACOSX,.git,.texpadtmp,.R}{,/**},.!(latexmkrc),*.{dvi,aux,log,toc,out,pdfsync,synctex,synctex(busy),fdb_latexmk,fls,nlo,ind,glo,gls,glg,bbl,blg,doc,docx,gz,swp}}',
hasLinkedProjectFileFeature: true,
hasLinkedProjectOutputFileFeature: true,
hasLinkUrlFeature: true,
} as typeof window.ExposedSettings } as typeof window.ExposedSettings

View file

@ -9,9 +9,14 @@ import { useTranslation } from 'react-i18next'
export const FigureModalSourcePicker: FC = () => { export const FigureModalSourcePicker: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const {
hasLinkedProjectFileFeature,
hasLinkedProjectOutputFileFeature,
hasLinkUrlFeature,
} = window.ExposedSettings
return ( return (
<div className="figure-modal-source-selector"> <div className="figure-modal-source-selector">
<div className="figure-modal-source-button-row"> <div className="figure-modal-source-button-grid">
<FigureModalSourceButton <FigureModalSourceButton
type={FigureModalSource.FILE_UPLOAD} type={FigureModalSource.FILE_UPLOAD}
title={t('replace_from_computer')} title={t('replace_from_computer')}
@ -22,18 +27,20 @@ export const FigureModalSourcePicker: FC = () => {
title={t('replace_from_project_files')} title={t('replace_from_project_files')}
icon="archive" icon="archive"
/> />
</div> {(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && (
<div className="figure-modal-source-button-row">
<FigureModalSourceButton <FigureModalSourceButton
type={FigureModalSource.OTHER_PROJECT} type={FigureModalSource.OTHER_PROJECT}
title={t('replace_from_another_project')} title={t('replace_from_another_project')}
icon="folder-open" icon="folder-open"
/> />
)}
{hasLinkUrlFeature && (
<FigureModalSourceButton <FigureModalSourceButton
type={FigureModalSource.FROM_URL} type={FigureModalSource.FROM_URL}
title={t('replace_from_url')} title={t('replace_from_url')}
icon="globe" icon="globe"
/> />
)}
</div> </div>
</div> </div>
) )

View file

@ -35,7 +35,11 @@ export const FigureModalOtherProjectSource: FC = () => {
const { _id: projectId } = useProjectContext() const { _id: projectId } = useProjectContext()
const { loading: projectsLoading, data: projects, error } = useUserProjects() const { loading: projectsLoading, data: projects, error } = useUserProjects()
const [selectedProject, setSelectedProject] = useState<null | Project>(null) const [selectedProject, setSelectedProject] = useState<null | Project>(null)
const [usingOutputFiles, setUsingOutputFiles] = useState<boolean>(false) const { hasLinkedProjectFileFeature, hasLinkedProjectOutputFileFeature } =
window.ExposedSettings
const [usingOutputFiles, setUsingOutputFiles] = useState<boolean>(
!hasLinkedProjectFileFeature
)
const [nameDirty, setNameDirty] = useState<boolean>(false) const [nameDirty, setNameDirty] = useState<boolean>(false)
const [name, setName] = useState<string>('') const [name, setName] = useState<string>('')
const [folder, setFolder] = useState<File | null>(null) const [folder, setFolder] = useState<File | null>(null)
@ -158,6 +162,7 @@ export const FigureModalOtherProjectSource: FC = () => {
}) })
}} }}
/> />
{hasLinkedProjectFileFeature && hasLinkedProjectOutputFileFeature && (
<div> <div>
or{' '} or{' '}
<Button <Button
@ -173,6 +178,7 @@ export const FigureModalOtherProjectSource: FC = () => {
</span> </span>
</Button> </Button>
</div> </div>
)}
<FileRelocator <FileRelocator
folder={folder} folder={folder}
name={name} name={name}

View file

@ -22,6 +22,11 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
}, },
[view] [view]
) )
const {
hasLinkedProjectFileFeature,
hasLinkedProjectOutputFileFeature,
hasLinkUrlFeature,
} = window.ExposedSettings
return ( return (
<ToolbarButtonMenu <ToolbarButtonMenu
id="toolbar-figure" id="toolbar-figure"
@ -43,6 +48,7 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
> >
<Icon type="archive" fw /> From project files <Icon type="archive" fw /> From project files
</ListGroupItem> </ListGroupItem>
{(hasLinkedProjectFileFeature || hasLinkedProjectOutputFileFeature) && (
<ListGroupItem <ListGroupItem
onClick={() => onClick={() =>
openFigureModal(FigureModalSource.OTHER_PROJECT, 'other-project') openFigureModal(FigureModalSource.OTHER_PROJECT, 'other-project')
@ -50,11 +56,16 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
> >
<Icon type="folder-open" fw /> From another project <Icon type="folder-open" fw /> From another project
</ListGroupItem> </ListGroupItem>
)}
{hasLinkUrlFeature && (
<ListGroupItem <ListGroupItem
onClick={() => openFigureModal(FigureModalSource.FROM_URL, 'from-url')} onClick={() =>
openFigureModal(FigureModalSource.FROM_URL, 'from-url')
}
> >
<Icon type="globe" fw /> From URL <Icon type="globe" fw /> From URL
</ListGroupItem> </ListGroupItem>
)}
</ToolbarButtonMenu> </ToolbarButtonMenu>
) )
}) })

View file

@ -136,7 +136,7 @@ const initialize = () => {
emailConfirmationDisabled: false, emailConfirmationDisabled: false,
enableSubscriptions: true, enableSubscriptions: true,
hasAffiliationsFeature: false, hasAffiliationsFeature: false,
hasLinkUrlFeature: false, hasLinkUrlFeature: true,
hasLinkedProjectFileFeature: true, hasLinkedProjectFileFeature: true,
hasLinkedProjectOutputFileFeature: true, hasLinkedProjectOutputFileFeature: true,
hasSamlFeature: true, hasSamlFeature: true,

View file

@ -99,9 +99,11 @@
cursor: pointer; cursor: pointer;
} }
.figure-modal-source-button-row { .figure-modal-source-button-grid {
display: flex; display: grid;
justify-content: space-between; justify-content: space-between;
gap: 8px;
grid-template-columns: 1fr 1fr;
margin: 0 auto; margin: 0 auto;
&:not(:first-of-type) { &:not(:first-of-type) {
@ -120,14 +122,6 @@
border: none; border: none;
padding: 0 8px; padding: 0 8px;
&:nth-child(even) {
margin-left: 4px;
}
&:nth-child(odd) {
margin-right: 4px;
}
&-title { &-title {
flex: 1 1 auto; flex: 1 1 auto;
text-align: left; text-align: left;

View file

@ -3,6 +3,7 @@ import { EditorProviders } from '../../../helpers/editor-providers'
import { mockScope, rootFolderId } from '../helpers/mock-scope' import { mockScope, rootFolderId } from '../helpers/mock-scope'
import { FC } from 'react' import { FC } from 'react'
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path' import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { ExposedSettings } from '../../../../../types/exposed-settings'
const Container: FC = ({ children }) => ( const Container: FC = ({ children }) => (
<div style={{ width: 1500, height: 785 }}>{children}</div> <div style={{ width: 1500, height: 785 }}>{children}</div>
@ -40,14 +41,8 @@ const matchUrl = (urlToMatch: RegExp | string) =>
describe('<FigureModal />', function () { describe('<FigureModal />', function () {
// TODO: rewrite these tests to be in source mode when toolbar is added there // TODO: rewrite these tests to be in source mode when toolbar is added there
// TODO: Write tests for width toggle, when we can match on source code // TODO: Write tests for width toggle, when we can match on source code
beforeEach(function () { function mount() {
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
cy.interceptMathJax()
cy.interceptEvents()
cy.interceptSpelling()
const content = '' const content = ''
const scope = mockScope(content) const scope = mockScope(content)
scope.editor.showVisual = true scope.editor.showVisual = true
@ -74,6 +69,22 @@ describe('<FigureModal />', function () {
</EditorProviders> </EditorProviders>
</Container> </Container>
) )
}
let previousExposedSettings: ExposedSettings
before(function () {
previousExposedSettings = window.ExposedSettings
})
afterEach(function () {
window.ExposedSettings = previousExposedSettings
})
beforeEach(function () {
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
cy.interceptMathJax()
cy.interceptEvents()
cy.interceptSpelling()
mount()
}) })
describe('Upload from computer source', function () { describe('Upload from computer source', function () {
@ -258,6 +269,98 @@ describe('<FigureModal />', function () {
}) })
}) })
describe('Feature flags', function () {
describe('with hasLinkUrlFeature=false', function () {
beforeEach(function () {
window.ExposedSettings = Object.assign({}, previousExposedSettings, {
hasLinkedProjectFileFeature: true,
hasLinkedProjectOutputFileFeature: true,
hasLinkUrlFeature: false,
})
mount()
clickToolbarButton('Insert Figure')
})
it('should not have import from url option', function () {
cy.findByRole('menu').within(() => {
cy.findByText('From URL').should('not.exist')
})
})
})
describe('with hasLinkedProjectFileFeature=false and hasLinkedProjectOutputFileFeature=false', function () {
beforeEach(function () {
window.ExposedSettings = Object.assign({}, previousExposedSettings, {
hasLinkedProjectFileFeature: false,
hasLinkedProjectOutputFileFeature: false,
hasLinkUrlFeature: true,
})
mount()
clickToolbarButton('Insert Figure')
})
it('should not have import from project file option', function () {
cy.findByRole('menu').within(() => {
cy.findByText('From another project').should('not.exist')
})
})
})
function setupFromAnotherProject() {
mount()
cy.interceptProjectListing()
clickToolbarButton('Insert Figure')
cy.findByRole('menu').within(() => {
cy.findByText('From another project').click()
})
cy.findByText('Select a project').click()
cy.findByRole('listbox').within(() => {
cy.findByText('My first project').click()
})
}
function expectNoOutputSwitch() {
it('should hide output switch', function () {
cy.findByText('select from output files').should('not.exist')
cy.findByText('select from source files').should('not.exist')
})
}
describe('with hasLinkedProjectFileFeature=false', function () {
beforeEach(function () {
window.ExposedSettings = Object.assign({}, previousExposedSettings, {
hasLinkedProjectFileFeature: false,
hasLinkedProjectOutputFileFeature: true,
hasLinkUrlFeature: true,
})
cy.interceptCompile()
setupFromAnotherProject()
})
expectNoOutputSwitch()
it('should show output file selector', function () {
cy.findByText('Select an output file').click()
cy.findByRole('listbox').within(() => {
cy.findByText('output.pdf').click()
})
})
})
describe('with hasLinkedProjectOutputFileFeature=false', function () {
beforeEach(function () {
window.ExposedSettings = Object.assign({}, previousExposedSettings, {
hasLinkedProjectFileFeature: true,
hasLinkedProjectOutputFileFeature: false,
hasLinkUrlFeature: true,
})
setupFromAnotherProject()
})
expectNoOutputSwitch()
it('should show source file selector', function () {
cy.findByText('Select a file').click()
cy.findByRole('listbox').within(() => {
cy.findByText('frog.jpg').click()
})
})
})
})
describe('From URL source', function () { describe('From URL source', function () {
beforeEach(function () { beforeEach(function () {
cy.interceptLinkedFile() cy.interceptLinkedFile()