mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-09 06:06:01 +00:00
Merge pull request #5797 from overleaf/ta-pdf-detach
PDF Detach GitOrigin-RevId: f69d8a87d1ba2115ad496a719106dfc7707a6ed5
This commit is contained in:
parent
c73894e56b
commit
8ca159b4b9
32 changed files with 762 additions and 147 deletions
|
@ -843,13 +843,24 @@ const ProjectController = {
|
|||
newPdfPreviewAssignment.variant === 'react-pdf-preview'
|
||||
)
|
||||
|
||||
let disableAngularRouter = shouldDisplayFeature(
|
||||
'disable_angular_router',
|
||||
user.alphaProgram
|
||||
)
|
||||
|
||||
const showPdfDetach = shouldDisplayFeature(
|
||||
'pdf_detach',
|
||||
user.alphaProgram
|
||||
)
|
||||
|
||||
const debugPdfDetach = shouldDisplayFeature('debug_pdf_detach')
|
||||
|
||||
let detachRole = null
|
||||
|
||||
if (showPdfDetach) {
|
||||
disableAngularRouter = true
|
||||
showNewPdfPreview = true
|
||||
detachRole = req.params.detachRole
|
||||
}
|
||||
|
||||
res.render('project/editor', {
|
||||
|
@ -911,7 +922,9 @@ const ProjectController = {
|
|||
),
|
||||
logsUISubvariant: logsUIVariant.subvariant,
|
||||
showPdfDetach,
|
||||
debugPdfDetach,
|
||||
showNewPdfPreview,
|
||||
disableAngularRouter,
|
||||
showNewSourceEditor: shouldDisplayFeature(
|
||||
'new_source_editor',
|
||||
false
|
||||
|
@ -925,6 +938,7 @@ const ProjectController = {
|
|||
resetServiceWorker:
|
||||
Boolean(Settings.resetServiceWorker) &&
|
||||
!shouldDisplayFeature('enable_pdf_caching', false),
|
||||
detachRole,
|
||||
})
|
||||
timer.done()
|
||||
}
|
||||
|
|
|
@ -295,7 +295,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
|||
)
|
||||
|
||||
webRouter.get(
|
||||
'/Project/:Project_id',
|
||||
'/Project/:Project_id/:detachRole(detacher|detached)?',
|
||||
RateLimiterMiddleware.rateLimit({
|
||||
endpointName: 'open-project',
|
||||
params: ['Project_id'],
|
||||
|
|
|
@ -65,53 +65,14 @@ block content
|
|||
span.sr-only #{translate("close")}
|
||||
.system-message-content(ng-bind-html="htmlContent")
|
||||
|
||||
include ./editor/left-menu
|
||||
|
||||
#chat-wrapper.full-size(
|
||||
layout="chat",
|
||||
spacing-open="{{ui.chatResizerSizeOpen}}",
|
||||
spacing-closed="{{ui.chatResizerSizeClosed}}",
|
||||
initial-size-east="250",
|
||||
init-closed-east="true",
|
||||
open-east="ui.chatOpen",
|
||||
ng-hide="state.loading",
|
||||
ng-cloak
|
||||
)
|
||||
.ui-layout-center
|
||||
include ./editor/header-react
|
||||
|
||||
include ./editor/history/toolbarV2.pug
|
||||
|
||||
main#ide-body(
|
||||
ng-cloak,
|
||||
role="main",
|
||||
ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2) }",
|
||||
layout="main",
|
||||
ng-hide="state.loading",
|
||||
resize-on="layout:chat:resize,history:toggle,layout:flat-screen:toggle,symbol-palette-toggled",
|
||||
minimum-restore-size-west="130"
|
||||
custom-toggler-pane=hasFeature('custom-togglers') ? "west" : false
|
||||
custom-toggler-msg-when-open=hasFeature('custom-togglers') ? translate("tooltip_hide_filetree") : false
|
||||
custom-toggler-msg-when-closed=hasFeature('custom-togglers') ? translate("tooltip_show_filetree") : false
|
||||
ng-keydown="handleKeyDown($event)"
|
||||
tabindex="0"
|
||||
)
|
||||
.ui-layout-west
|
||||
include ./editor/file-tree-react
|
||||
include ./editor/file-tree-history
|
||||
include ./editor/history/fileTreeV2
|
||||
|
||||
.ui-layout-center
|
||||
include ./editor/editor
|
||||
|
||||
include ./editor/file-view
|
||||
|
||||
include ./editor/history
|
||||
|
||||
if !isRestrictedTokenMember
|
||||
.ui-layout-east
|
||||
aside.chat
|
||||
chat()
|
||||
if detachRole === 'detached'
|
||||
div.full-size
|
||||
if showNewPdfPreview
|
||||
pdf-preview()
|
||||
else
|
||||
include ./editor/pdf
|
||||
else
|
||||
include ./editor/main
|
||||
|
||||
script(type="text/ng-template", id="genericMessageModalTemplate")
|
||||
.modal-header
|
||||
|
@ -183,10 +144,13 @@ block append meta
|
|||
meta(name="ol-logsUISubvariant" content=logsUISubvariant)
|
||||
meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette)
|
||||
meta(name="ol-showPdfDetach" data-type="boolean" content=showPdfDetach)
|
||||
meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach)
|
||||
meta(name="ol-disableAngularRouter" data-type="boolean" content=disableAngularRouter)
|
||||
meta(name="ol-showNewPdfPreview" data-type="boolean" content=showNewPdfPreview)
|
||||
meta(name="ol-enablePdfCaching" data-type="boolean" content=enablePdfCaching)
|
||||
meta(name="ol-trackPdfDownload" data-type="boolean" content=trackPdfDownload)
|
||||
meta(name="ol-resetServiceWorker" data-type="boolean" content=resetServiceWorker)
|
||||
meta(name="ol-detachRole" data-type="string" content=detachRole)
|
||||
|
||||
- var fileActionI18n = ['edited', 'renamed', 'created', 'deleted'].reduce((acc, i) => {acc[i] = translate('file_action_' + i); return acc}, {})
|
||||
meta(name="ol-fileActionI18n" data-type="json" content=fileActionI18n)
|
||||
|
|
47
services/web/app/views/project/editor/main.pug
Normal file
47
services/web/app/views/project/editor/main.pug
Normal file
|
@ -0,0 +1,47 @@
|
|||
include ./left-menu
|
||||
|
||||
#chat-wrapper.full-size(
|
||||
layout="chat",
|
||||
spacing-open="{{ui.chatResizerSizeOpen}}",
|
||||
spacing-closed="{{ui.chatResizerSizeClosed}}",
|
||||
initial-size-east="250",
|
||||
init-closed-east="true",
|
||||
open-east="ui.chatOpen",
|
||||
ng-hide="state.loading",
|
||||
ng-cloak
|
||||
)
|
||||
.ui-layout-center
|
||||
include ./header-react
|
||||
|
||||
include ./history/toolbarV2.pug
|
||||
|
||||
main#ide-body(
|
||||
ng-cloak,
|
||||
role="main",
|
||||
ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2) }",
|
||||
layout="main",
|
||||
ng-hide="state.loading",
|
||||
resize-on="layout:chat:resize,history:toggle,layout:flat-screen:toggle,symbol-palette-toggled",
|
||||
minimum-restore-size-west="130"
|
||||
custom-toggler-pane=hasFeature('custom-togglers') ? "west" : false
|
||||
custom-toggler-msg-when-open=hasFeature('custom-togglers') ? translate("tooltip_hide_filetree") : false
|
||||
custom-toggler-msg-when-closed=hasFeature('custom-togglers') ? translate("tooltip_show_filetree") : false
|
||||
ng-keydown="handleKeyDown($event)"
|
||||
tabindex="0"
|
||||
)
|
||||
.ui-layout-west
|
||||
include ./file-tree-react
|
||||
include ./file-tree-history
|
||||
include ./history/fileTreeV2
|
||||
|
||||
.ui-layout-center
|
||||
include ./editor
|
||||
|
||||
include ./file-view
|
||||
|
||||
include ./history
|
||||
|
||||
if !isRestrictedTokenMember
|
||||
.ui-layout-east
|
||||
aside.chat
|
||||
chat()
|
|
@ -15,6 +15,7 @@
|
|||
"autocomplete_references": "",
|
||||
"back_to_your_projects": "",
|
||||
"blocked_filename": "",
|
||||
"bring_pdf_back_to_tab": "",
|
||||
"can_edit": "",
|
||||
"cancel": "",
|
||||
"cannot_invite_non_user": "",
|
||||
|
@ -168,6 +169,7 @@
|
|||
"invalid_request": "",
|
||||
"invite_not_accepted": "",
|
||||
"layout": "",
|
||||
"layout_processing": "",
|
||||
"learn_how_to_make_documents_compile_quickly": "",
|
||||
"learn_more_about_link_sharing": "",
|
||||
"learn_more_about_the_symbol_palette": "",
|
||||
|
@ -231,6 +233,7 @@
|
|||
"official": "",
|
||||
"ok": "",
|
||||
"on": "",
|
||||
"open_pdf_in_new_tab": "",
|
||||
"optional": "",
|
||||
"or": "",
|
||||
"other_logs_and_files": "",
|
||||
|
@ -280,6 +283,7 @@
|
|||
"recompile_from_scratch": "",
|
||||
"recompile_pdf": "",
|
||||
"reconnect": "",
|
||||
"redirect_to_editor": "",
|
||||
"reference_error_relink_hint": "",
|
||||
"refresh": "",
|
||||
"refresh_page_after_linking_dropbox": "",
|
||||
|
@ -329,6 +333,7 @@
|
|||
"sync_project_to_github_explanation": "",
|
||||
"sync_to_dropbox": "",
|
||||
"sync_to_github": "",
|
||||
"tab_no_longer_connected": "",
|
||||
"tags": "",
|
||||
"template_approved_by_publisher": "",
|
||||
"terminated": "",
|
||||
|
|
|
@ -15,18 +15,19 @@ function ChatToggleButton({ chatIsOpen, unreadMessageCount, onClick }) {
|
|||
const hasUnreadMessages = unreadMessageCount > 0
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
||||
<a role="button" className={classes} href="#" onClick={onClick}>
|
||||
<Icon
|
||||
type="fw"
|
||||
modifier="comment"
|
||||
classes={{ icon: hasUnreadMessages ? 'bounce' : undefined }}
|
||||
/>
|
||||
{hasUnreadMessages ? (
|
||||
<span className="label label-info">{unreadMessageCount}</span>
|
||||
) : null}
|
||||
<p className="toolbar-label">{t('chat')}</p>
|
||||
</a>
|
||||
<div className="toolbar-item">
|
||||
<button className={classes} onClick={onClick}>
|
||||
<Icon
|
||||
type="fw"
|
||||
modifier="comment"
|
||||
classes={{ icon: hasUnreadMessages ? 'bounce' : undefined }}
|
||||
/>
|
||||
{hasUnreadMessages ? (
|
||||
<span className="label label-info">{unreadMessageCount}</span>
|
||||
) : null}
|
||||
<p className="toolbar-label">{t('chat')}</p>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,10 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
} = useEditorContext(editorContextPropTypes)
|
||||
|
||||
const {
|
||||
reattach,
|
||||
detach,
|
||||
detachMode,
|
||||
detachRole,
|
||||
changeLayout,
|
||||
chatIsOpen,
|
||||
setChatIsOpen,
|
||||
|
@ -123,6 +127,10 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
// `loading ? null : <ToolbarHeader/>` causes UI glitches
|
||||
return (
|
||||
<ToolbarHeader
|
||||
reattach={reattach}
|
||||
detach={detach}
|
||||
detachMode={detachMode}
|
||||
detachRole={detachRole}
|
||||
style={loading ? { display: 'none' } : {}}
|
||||
cobranding={cobranding}
|
||||
onShowLeftMenuClick={onShowLeftMenuClick}
|
||||
|
|
|
@ -11,11 +11,12 @@ function HistoryToggleButton({ historyIsOpen, onClick }) {
|
|||
})
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
||||
<a role="button" className={classes} href="#" onClick={onClick}>
|
||||
<Icon type="fw" modifier="history" />
|
||||
<p className="toolbar-label">{t('history')}</p>
|
||||
</a>
|
||||
<div className="toolbar-item">
|
||||
<button className={classes} onClick={onClick}>
|
||||
<Icon type="fw" modifier="history" />
|
||||
<p className="toolbar-label">{t('history')}</p>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,58 +19,107 @@ function IconCheckmark({ iconFor, pdfLayout, view }) {
|
|||
return <Icon type="" modifier="fw" />
|
||||
}
|
||||
|
||||
function LayoutDropdownButton({ handleChangeLayout, pdfLayout, view }) {
|
||||
function LayoutDropdownButton({
|
||||
reattach,
|
||||
detach,
|
||||
handleChangeLayout,
|
||||
detachMode,
|
||||
detachRole,
|
||||
pdfLayout,
|
||||
view,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// bsStyle is required for Dropdown.Toggle, but we will override style
|
||||
return (
|
||||
<ControlledDropdown id="layout-dropdown" className="toolbar-item">
|
||||
<Dropdown.Toggle className="btn-full-height" bsStyle="link">
|
||||
<Icon type="columns" modifier="fw" />
|
||||
<span className="toolbar-label">{t('layout')}</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu id="layout-dropdown-list">
|
||||
<MenuItem header>{t('layout')}</MenuItem>
|
||||
<MenuItem onSelect={() => handleChangeLayout('sideBySide')}>
|
||||
<IconCheckmark
|
||||
iconFor="sideBySide"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
/>
|
||||
<Icon type="columns" />
|
||||
{t('editor_and_pdf')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelect={() => handleChangeLayout('flat', 'editor')}
|
||||
className="menu-item-with-svg"
|
||||
>
|
||||
<IconCheckmark
|
||||
iconFor="editorOnly"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
/>
|
||||
<IconEditorOnly />
|
||||
<Trans
|
||||
i18nKey="editor_only_hide_pdf"
|
||||
components={[
|
||||
<span key="editor_only_hide_pdf" className="subdued" />,
|
||||
]}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelect={() => handleChangeLayout('flat', 'pdf')}
|
||||
className="menu-item-with-svg"
|
||||
>
|
||||
<IconCheckmark iconFor="pdfOnly" pdfLayout={pdfLayout} view={view} />
|
||||
<IconPdfOnly />
|
||||
<Trans
|
||||
i18nKey="pdf_only_hide_editor"
|
||||
components={[
|
||||
<span key="pdf_only_hide_editor" className="subdued" />,
|
||||
]}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Dropdown.Menu>
|
||||
</ControlledDropdown>
|
||||
<>
|
||||
{detachMode === 'detaching' && (
|
||||
<div aria-live="assertive" className="sr-only">
|
||||
{t('layout_processing')}
|
||||
</div>
|
||||
)}
|
||||
<ControlledDropdown
|
||||
id="layout-dropdown"
|
||||
className="toolbar-item"
|
||||
disabled={detachMode === 'detaching'}
|
||||
>
|
||||
<Dropdown.Toggle className="btn-full-height" bsStyle="link">
|
||||
{detachMode === 'detaching' ? (
|
||||
<Icon type="refresh" modifier="fw" spin />
|
||||
) : (
|
||||
<Icon type="columns" modifier="fw" />
|
||||
)}
|
||||
<span className="toolbar-label">{t('layout')}</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu id="layout-dropdown-list">
|
||||
<MenuItem header>{t('layout')}</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={detachRole === 'detacher'}
|
||||
onSelect={() => handleChangeLayout('sideBySide')}
|
||||
>
|
||||
<IconCheckmark
|
||||
iconFor="sideBySide"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
/>
|
||||
<Icon type="columns" />
|
||||
{t('editor_and_pdf')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onSelect={() => handleChangeLayout('flat', 'editor')}
|
||||
className="menu-item-with-svg"
|
||||
>
|
||||
<IconCheckmark
|
||||
iconFor="editorOnly"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
/>
|
||||
<IconEditorOnly />
|
||||
<Trans
|
||||
i18nKey="editor_only_hide_pdf"
|
||||
components={[
|
||||
<span key="editor_only_hide_pdf" className="subdued" />,
|
||||
]}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={detachRole === 'detacher'}
|
||||
onSelect={() => handleChangeLayout('flat', 'pdf')}
|
||||
className="menu-item-with-svg"
|
||||
>
|
||||
<IconCheckmark
|
||||
iconFor="pdfOnly"
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
/>
|
||||
<IconPdfOnly />
|
||||
<Trans
|
||||
i18nKey="pdf_only_hide_editor"
|
||||
components={[
|
||||
<span key="pdf_only_hide_editor" className="subdued" />,
|
||||
]}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem divider />
|
||||
|
||||
{detachRole === 'detacher' ? (
|
||||
<MenuItem onSelect={() => reattach()}>
|
||||
<Icon type="window-restore" modifier="fw" />
|
||||
{t('bring_pdf_back_to_tab')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onSelect={() => detach()}>
|
||||
<Icon type="window-restore" modifier="fw" />
|
||||
{t('open_pdf_in_new_tab')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</ControlledDropdown>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -83,7 +132,11 @@ IconCheckmark.propTypes = {
|
|||
}
|
||||
|
||||
LayoutDropdownButton.propTypes = {
|
||||
reattach: PropTypes.func.isRequired,
|
||||
detach: PropTypes.func.isRequired,
|
||||
handleChangeLayout: PropTypes.func.isRequired,
|
||||
detachMode: PropTypes.string,
|
||||
detachRole: PropTypes.string,
|
||||
pdfLayout: PropTypes.string.isRequired,
|
||||
view: PropTypes.string,
|
||||
}
|
||||
|
|
|
@ -6,11 +6,12 @@ function ShareProjectButton({ onClick }) {
|
|||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid,jsx-a11y/click-events-have-key-events,jsx-a11y/interactive-supports-focus
|
||||
<a role="button" className="btn btn-full-height" onClick={onClick}>
|
||||
<Icon type="fw" modifier="group" />
|
||||
<p className="toolbar-label">{t('share')}</p>
|
||||
</a>
|
||||
<div className="toolbar-item">
|
||||
<button className="btn btn-full-height" onClick={onClick}>
|
||||
<Icon type="fw" modifier="group" />
|
||||
<p className="toolbar-label">{t('share')}</p>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,10 @@ const [publishModalModules] = importOverleafModules('publishModal')
|
|||
const PublishButton = publishModalModules?.import.default
|
||||
|
||||
const ToolbarHeader = React.memo(function ToolbarHeader({
|
||||
reattach,
|
||||
detach,
|
||||
detachMode,
|
||||
detachRole,
|
||||
cobranding,
|
||||
onShowLeftMenuClick,
|
||||
handleChangeLayout,
|
||||
|
@ -61,7 +65,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
|
|||
)}
|
||||
<BackToProjectsButton />
|
||||
</div>
|
||||
{pdfButtonIsVisible && (
|
||||
{!window.showPdfDetach && pdfButtonIsVisible && (
|
||||
<PdfToggleButton
|
||||
onClick={togglePdfView}
|
||||
pdfViewIsOpen={pdfViewIsOpen}
|
||||
|
@ -79,7 +83,11 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
|
|||
|
||||
{window.showPdfDetach && (
|
||||
<LayoutDropdownButton
|
||||
reattach={reattach}
|
||||
detach={detach}
|
||||
handleChangeLayout={handleChangeLayout}
|
||||
detachMode={detachMode}
|
||||
detachRole={detachRole}
|
||||
pdfLayout={pdfLayout}
|
||||
view={view}
|
||||
/>
|
||||
|
@ -115,6 +123,10 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
|
|||
})
|
||||
|
||||
ToolbarHeader.propTypes = {
|
||||
reattach: PropTypes.func.isRequired,
|
||||
detach: PropTypes.func.isRequired,
|
||||
detachMode: PropTypes.string,
|
||||
detachRole: PropTypes.string,
|
||||
onShowLeftMenuClick: PropTypes.func.isRequired,
|
||||
handleChangeLayout: PropTypes.func.isRequired,
|
||||
cobranding: PropTypes.object,
|
||||
|
|
|
@ -10,17 +10,12 @@ function TrackChangesToggleButton({ trackChangesIsOpen, disabled, onClick }) {
|
|||
})
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
||||
<a
|
||||
role="button"
|
||||
disabled={disabled}
|
||||
className={classes}
|
||||
href="#"
|
||||
onClick={onClick}
|
||||
>
|
||||
<i className="review-icon" />
|
||||
<p className="toolbar-label">{t('review')}</p>
|
||||
</a>
|
||||
<div className="toolbar-item">
|
||||
<button disabled={disabled} className={classes} onClick={onClick}>
|
||||
<i className="review-icon" />
|
||||
<p className="toolbar-label">{t('review')}</p>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import { buildUrlWithDetachRole } from '../../../shared/utils/url-helper'
|
||||
|
||||
const redirect = function () {
|
||||
window.location = buildUrlWithDetachRole(null)
|
||||
}
|
||||
|
||||
function PdfOrphanRefreshButton() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={redirect}
|
||||
className="btn-orphan"
|
||||
bsStyle="primary"
|
||||
bsSize="small"
|
||||
>
|
||||
{t('redirect_to_editor')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PdfOrphanRefreshButton)
|
|
@ -1,14 +1,31 @@
|
|||
import { memo } from 'react'
|
||||
import { ButtonToolbar } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import PdfCompileButton from './pdf-compile-button'
|
||||
import PdfExpandButton from './pdf-expand-button'
|
||||
import PdfHybridLogsButton from './pdf-hybrid-logs-button'
|
||||
import PdfHybridDownloadButton from './pdf-hybrid-download-button'
|
||||
import PdfHybridCodeCheckButton from './pdf-hybrid-code-check-button'
|
||||
import PdfOrphanRefreshButton from './pdf-orphan-refresh-button'
|
||||
|
||||
function PdfPreviewHybridToolbar() {
|
||||
const { detachMode } = useLayoutContext()
|
||||
|
||||
return (
|
||||
<ButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid">
|
||||
{detachMode === 'orphan' ? (
|
||||
<PdfPreviewHybridToolbarOrphanInner />
|
||||
) : (
|
||||
<PdfPreviewHybridToolbarInner />
|
||||
)}
|
||||
</ButtonToolbar>
|
||||
)
|
||||
}
|
||||
|
||||
function PdfPreviewHybridToolbarInner() {
|
||||
return (
|
||||
<>
|
||||
<div className="toolbar-pdf-left">
|
||||
<PdfCompileButton />
|
||||
<PdfHybridLogsButton />
|
||||
|
@ -16,9 +33,21 @@ function PdfPreviewHybridToolbar() {
|
|||
</div>
|
||||
<div className="toolbar-pdf-right">
|
||||
<PdfHybridCodeCheckButton />
|
||||
<PdfExpandButton />
|
||||
{!window.showPdfDetach && <PdfExpandButton />}
|
||||
</div>
|
||||
</ButtonToolbar>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PdfPreviewHybridToolbarOrphanInner() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<div className="toolbar-pdf-orphan">
|
||||
{t('tab_no_longer_connected')}
|
||||
<PdfOrphanRefreshButton />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -401,6 +401,21 @@ if (getMeta('ol-resetServiceWorker')) {
|
|||
loadServiceWorker()
|
||||
}
|
||||
|
||||
if (getMeta('ol-disableAngularRouter')) {
|
||||
angular.module('SharelatexApp').config(function ($provide) {
|
||||
$provide.decorator('$browser', [
|
||||
'$delegate',
|
||||
function ($delegate) {
|
||||
$delegate.onUrlChange = function () {}
|
||||
$delegate.url = function () {
|
||||
return ''
|
||||
}
|
||||
return $delegate
|
||||
},
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
export default angular.bootstrap(document.body, ['SharelatexApp'])
|
||||
|
||||
function __guard__(value, transform) {
|
||||
|
|
118
services/web/frontend/js/shared/context/detach-context.js
Normal file
118
services/web/frontend/js/shared/context/detach-context.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import sysend from 'sysend'
|
||||
import getMeta from '../../utils/meta'
|
||||
import { buildUrlWithDetachRole } from '../utils/url-helper'
|
||||
import useCallbackHandlers from '../hooks/use-callback-handlers'
|
||||
|
||||
export const DetachContext = createContext()
|
||||
|
||||
DetachContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
role: PropTypes.oneOf(['detacher', 'detached', null]),
|
||||
setRole: PropTypes.func.isRequired,
|
||||
broadcastEvent: PropTypes.func.isRequired,
|
||||
addEventHandler: PropTypes.func.isRequired,
|
||||
deleteEventHandler: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
const debugPdfDetach = getMeta('ol-debugPdfDetach')
|
||||
|
||||
const SYSEND_CHANNEL = `detach-${getMeta('ol-project_id')}`
|
||||
|
||||
export function DetachProvider({ children }) {
|
||||
const [role, setRole] = useState(() => getMeta('ol-detachRole') || null)
|
||||
const {
|
||||
addHandler: addEventHandler,
|
||||
deleteHandler: deleteEventHandler,
|
||||
callHandlers: callEventHandlers,
|
||||
} = useCallbackHandlers()
|
||||
|
||||
useEffect(() => {
|
||||
if (debugPdfDetach) {
|
||||
console.log('Effect', { role })
|
||||
}
|
||||
window.history.replaceState({}, '', buildUrlWithDetachRole(role))
|
||||
}, [role])
|
||||
|
||||
useEffect(() => {
|
||||
sysend.on(SYSEND_CHANNEL, message => {
|
||||
if (debugPdfDetach) {
|
||||
console.log(`Receiving:`, message)
|
||||
}
|
||||
callEventHandlers(message)
|
||||
})
|
||||
return () => sysend.off(SYSEND_CHANNEL)
|
||||
}, [callEventHandlers])
|
||||
|
||||
const broadcastEvent = useCallback(
|
||||
(event, data) => {
|
||||
if (!role) {
|
||||
if (debugPdfDetach) {
|
||||
console.log('Not Broadcasting (no role)', {
|
||||
role,
|
||||
event,
|
||||
data,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (debugPdfDetach) {
|
||||
console.log('Broadcasting', {
|
||||
role,
|
||||
event,
|
||||
data,
|
||||
})
|
||||
}
|
||||
sysend.broadcast(SYSEND_CHANNEL, {
|
||||
role,
|
||||
event,
|
||||
data,
|
||||
})
|
||||
},
|
||||
[role]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
broadcastEvent('connected')
|
||||
}, [broadcastEvent])
|
||||
|
||||
useEffect(() => {
|
||||
const onBeforeUnload = () => broadcastEvent('closed')
|
||||
window.addEventListener('beforeunload', onBeforeUnload)
|
||||
return () => window.removeEventListener('beforeunload', onBeforeUnload)
|
||||
}, [broadcastEvent])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
role,
|
||||
setRole,
|
||||
broadcastEvent,
|
||||
addEventHandler,
|
||||
deleteEventHandler,
|
||||
}),
|
||||
[role, setRole, broadcastEvent, addEventHandler, deleteEventHandler]
|
||||
)
|
||||
|
||||
return (
|
||||
<DetachContext.Provider value={value}>{children}</DetachContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
DetachProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useDetachContext(propTypes) {
|
||||
const data = useContext(DetachContext)
|
||||
PropTypes.checkPropTypes(propTypes, data, 'data', 'DetachContext.Provider')
|
||||
return data
|
||||
}
|
|
@ -1,6 +1,14 @@
|
|||
import { createContext, useContext, useCallback, useMemo } from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import useScopeValue from '../hooks/use-scope-value'
|
||||
import usePreviousValue from '../hooks/use-previous-value'
|
||||
import useDetachLayout from '../hooks/use-detach-layout'
|
||||
import { useIdeContext } from './ide-context'
|
||||
import localStorage from '../../infrastructure/local-storage'
|
||||
|
||||
|
@ -73,8 +81,40 @@ export function LayoutProvider({ children }) {
|
|||
[setPdfLayout, setView]
|
||||
)
|
||||
|
||||
const {
|
||||
reattach,
|
||||
detach,
|
||||
mode: detachMode,
|
||||
role: detachRole,
|
||||
} = useDetachLayout()
|
||||
const previousDetachMode = usePreviousValue(detachMode)
|
||||
|
||||
useEffect(() => {
|
||||
switch (detachMode) {
|
||||
case 'detacher':
|
||||
changeLayout('flat', 'editor')
|
||||
break
|
||||
case 'detaching':
|
||||
changeLayout('flat', 'editor')
|
||||
break
|
||||
case 'detached':
|
||||
break
|
||||
case 'orphan':
|
||||
break
|
||||
case null:
|
||||
if (previousDetachMode) {
|
||||
changeLayout('sideBySide')
|
||||
}
|
||||
break
|
||||
}
|
||||
}, [detachMode, previousDetachMode, changeLayout])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
reattach,
|
||||
detach,
|
||||
detachMode,
|
||||
detachRole,
|
||||
changeLayout,
|
||||
chatIsOpen,
|
||||
leftMenuShown,
|
||||
|
@ -89,6 +129,10 @@ export function LayoutProvider({ children }) {
|
|||
view,
|
||||
}),
|
||||
[
|
||||
reattach,
|
||||
detach,
|
||||
detachMode,
|
||||
detachRole,
|
||||
changeLayout,
|
||||
chatIsOpen,
|
||||
leftMenuShown,
|
||||
|
|
|
@ -6,6 +6,7 @@ import { IdeProvider } from './ide-context'
|
|||
import { EditorProvider } from './editor-context'
|
||||
import { CompileProvider } from './compile-context'
|
||||
import { LayoutProvider } from './layout-context'
|
||||
import { DetachProvider } from './detach-context'
|
||||
import { ChatProvider } from '../../features/chat/context/chat-context'
|
||||
import { ProjectProvider } from './project-context'
|
||||
import { SplitTestProvider } from './split-test-context'
|
||||
|
@ -17,11 +18,13 @@ export function ContextRoot({ children, ide, settings }) {
|
|||
<UserProvider>
|
||||
<ProjectProvider>
|
||||
<EditorProvider settings={settings}>
|
||||
<LayoutProvider>
|
||||
<CompileProvider>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</CompileProvider>
|
||||
</LayoutProvider>
|
||||
<DetachProvider>
|
||||
<LayoutProvider>
|
||||
<CompileProvider>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</CompileProvider>
|
||||
</LayoutProvider>
|
||||
</DetachProvider>
|
||||
</EditorProvider>
|
||||
</ProjectProvider>
|
||||
</UserProvider>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
|
||||
export default function useCallbackHandlers() {
|
||||
const [handlers, setHandlers] = useState(new Set())
|
||||
|
||||
const addHandler = useCallback(
|
||||
handler => {
|
||||
setHandlers(prev => new Set(prev.add(handler)))
|
||||
},
|
||||
[setHandlers]
|
||||
)
|
||||
|
||||
const deleteHandler = useCallback(
|
||||
handler => {
|
||||
setHandlers(prev => {
|
||||
prev.delete(handler)
|
||||
return new Set(prev)
|
||||
})
|
||||
},
|
||||
[setHandlers]
|
||||
)
|
||||
|
||||
const callHandlers = useCallback(
|
||||
(...args) => {
|
||||
for (const handler of handlers) {
|
||||
handler(...args)
|
||||
}
|
||||
},
|
||||
[handlers]
|
||||
)
|
||||
|
||||
return { addHandler, deleteHandler, callHandlers }
|
||||
}
|
130
services/web/frontend/js/shared/hooks/use-detach-layout.js
Normal file
130
services/web/frontend/js/shared/hooks/use-detach-layout.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
import { useCallback, useState, useEffect } from 'react'
|
||||
import { useDetachContext } from '../context/detach-context'
|
||||
import getMeta from '../../utils/meta'
|
||||
import { buildUrlWithDetachRole } from '../utils/url-helper'
|
||||
|
||||
const debugPdfDetach = getMeta('ol-debugPdfDetach')
|
||||
|
||||
export default function useDetachLayout() {
|
||||
const {
|
||||
role,
|
||||
setRole,
|
||||
broadcastEvent,
|
||||
addEventHandler,
|
||||
deleteEventHandler,
|
||||
} = useDetachContext()
|
||||
|
||||
const [mode, setMode] = useState(() => {
|
||||
if (role === 'detacher') {
|
||||
return 'detaching'
|
||||
}
|
||||
if (role === 'detached') {
|
||||
return 'orphan'
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (debugPdfDetach) {
|
||||
console.log('Effect', { mode })
|
||||
}
|
||||
}, [mode])
|
||||
|
||||
const reattach = useCallback(() => {
|
||||
broadcastEvent('reattach')
|
||||
setRole(null)
|
||||
setMode(null)
|
||||
}, [setRole, setMode, broadcastEvent])
|
||||
|
||||
const detach = useCallback(() => {
|
||||
setRole('detacher')
|
||||
setMode('detaching')
|
||||
|
||||
window.open(buildUrlWithDetachRole('detached'), '_blank')
|
||||
}, [setRole, setMode])
|
||||
|
||||
const handleEventForDetacherFromDetached = useCallback(
|
||||
message => {
|
||||
switch (message.event) {
|
||||
case 'connected':
|
||||
broadcastEvent('up')
|
||||
setMode('detacher')
|
||||
break
|
||||
case 'up':
|
||||
setMode('detacher')
|
||||
break
|
||||
case 'closed':
|
||||
setMode(null)
|
||||
break
|
||||
}
|
||||
},
|
||||
[setMode, broadcastEvent]
|
||||
)
|
||||
|
||||
const handleEventForDetachedFromDetacher = useCallback(
|
||||
message => {
|
||||
switch (message.event) {
|
||||
case 'connected':
|
||||
broadcastEvent('up')
|
||||
setMode('detached')
|
||||
break
|
||||
case 'up':
|
||||
setMode('detached')
|
||||
break
|
||||
case 'closed':
|
||||
setMode('orphan')
|
||||
break
|
||||
case 'reattach':
|
||||
window.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
[setMode, broadcastEvent]
|
||||
)
|
||||
|
||||
const handleEventFromSelf = useCallback(
|
||||
message => {
|
||||
switch (message.event) {
|
||||
case 'closed':
|
||||
broadcastEvent('up')
|
||||
break
|
||||
}
|
||||
},
|
||||
[broadcastEvent]
|
||||
)
|
||||
|
||||
const handleEvent = useCallback(
|
||||
message => {
|
||||
if (role === 'detacher') {
|
||||
if (message.role === 'detacher') {
|
||||
handleEventFromSelf(message)
|
||||
} else if (message.role === 'detached') {
|
||||
handleEventForDetacherFromDetached(message)
|
||||
}
|
||||
} else if (role === 'detached') {
|
||||
if (message.role === 'detacher') {
|
||||
handleEventForDetachedFromDetacher(message)
|
||||
} else if (message.role === 'detached') {
|
||||
handleEventFromSelf(message)
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
role,
|
||||
handleEventForDetacherFromDetached,
|
||||
handleEventForDetachedFromDetacher,
|
||||
handleEventFromSelf,
|
||||
]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
addEventHandler(handleEvent)
|
||||
return () => deleteEventHandler(handleEvent)
|
||||
}, [addEventHandler, deleteEventHandler, handleEvent])
|
||||
|
||||
return {
|
||||
reattach,
|
||||
detach,
|
||||
mode,
|
||||
role,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export default function usePreviousValue(value) {
|
||||
const ref = useRef()
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
})
|
||||
return ref.current
|
||||
}
|
11
services/web/frontend/js/shared/utils/url-helper.js
Normal file
11
services/web/frontend/js/shared/utils/url-helper.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export function buildUrlWithDetachRole(mode) {
|
||||
const url = new URL(window.location)
|
||||
const cleanPathname = url.pathname
|
||||
.replace(/\/(detached|detacher)\/?$/, '')
|
||||
.replace(/\/$/, '')
|
||||
url.pathname = cleanPathname
|
||||
if (mode) {
|
||||
url.pathname += `/${mode}`
|
||||
}
|
||||
return url
|
||||
}
|
|
@ -35,6 +35,7 @@
|
|||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.toolbar-pdf-orphan,
|
||||
.toolbar-pdf-left,
|
||||
.toolbar-pdf-right {
|
||||
display: flex;
|
||||
|
@ -47,6 +48,14 @@
|
|||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.toolbar-pdf-orphan {
|
||||
justify-content: center;
|
||||
color: white;
|
||||
.btn {
|
||||
margin-left: @margin-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-toggle-logs {
|
||||
&:focus,
|
||||
&:active:focus {
|
||||
|
@ -58,7 +67,7 @@
|
|||
}
|
||||
|
||||
.toolbar-pdf-hybrid {
|
||||
.btn:not(.btn-recompile) {
|
||||
.btn:not(.btn-recompile):not(.btn-orphan) {
|
||||
display: inline-block;
|
||||
color: @toolbar-btn-color;
|
||||
background-color: transparent;
|
||||
|
|
|
@ -951,7 +951,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
a when (@is-overleaf-light = true) {
|
||||
button when (@is-overleaf-light = true) {
|
||||
.review-icon {
|
||||
background: url('/img/ol-icons/review-icon-light-theme.svg') top/30px
|
||||
no-repeat;
|
||||
|
|
|
@ -27,7 +27,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
> a:focus {
|
||||
> a:focus,
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
@ -429,4 +430,37 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.subdued {
|
||||
color: @dropdown-link-disabled-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-disabled-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus {
|
||||
.subdued {
|
||||
color: @dropdown-link-disabled-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
line,
|
||||
rect {
|
||||
stroke: @dropdown-link-disabled-color;
|
||||
}
|
||||
path {
|
||||
fill: @dropdown-link-disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1530,5 +1530,10 @@
|
|||
"editor_only_hide_pdf": "Editor only <0>(hide PDF)</0>",
|
||||
"pdf_only_hide_editor": "PDF only <0>(hide editor)</0>",
|
||||
"selected": "Selected",
|
||||
"project_layout_sharing_submission": "Project Layout, Sharing, and Submission"
|
||||
"project_layout_sharing_submission": "Project Layout, Sharing, and Submission",
|
||||
"open_pdf_in_new_tab": "Open PDF in new tab",
|
||||
"bring_pdf_back_to_tab": "Bring PDF back to this tab",
|
||||
"tab_no_longer_connected": "This tab is no longer connected with the editor",
|
||||
"redirect_to_editor": "Redirect to editor",
|
||||
"layout_processing": "Layout processing"
|
||||
}
|
||||
|
|
5
services/web/package-lock.json
generated
5
services/web/package-lock.json
generated
|
@ -35661,6 +35661,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sysend": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/sysend/-/sysend-1.7.1.tgz",
|
||||
"integrity": "sha512-RCbx0drkadsUAIKYSmIwf0gK4t/YAs4d7UIYa455CAAZVL2sg8eFV3Hf9QBJMCACNqD08mT5eG4v9GpNGszndA=="
|
||||
},
|
||||
"table": {
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz",
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
"rolling-rate-limiter": "^0.2.10",
|
||||
"sanitize-html": "^1.27.1",
|
||||
"scroll-into-view-if-needed": "^2.2.25",
|
||||
"sysend": "^1.7.1",
|
||||
"underscore": "^1.13.1",
|
||||
"unzipper": "^0.10.11",
|
||||
"url-parse": "^1.4.7",
|
||||
|
|
|
@ -3,21 +3,58 @@ import LayoutDropdownButton from '../../../../../frontend/js/features/editor-nav
|
|||
|
||||
describe('<LayoutDropdownButton />', function () {
|
||||
const defaultProps = {
|
||||
reattach: () => {},
|
||||
detach: () => {},
|
||||
handleChangeLayout: () => {},
|
||||
detachMode: undefined,
|
||||
detachRole: undefined,
|
||||
pdfLayout: 'flat',
|
||||
view: 'editor',
|
||||
view: 'pdf',
|
||||
}
|
||||
|
||||
it('should mark current layout option as selected (visually by checkmark, and aria-label for accessibility)', function () {
|
||||
it('should mark current layout option as selected', function () {
|
||||
// Selected is aria-label, visually we show a checkmark
|
||||
render(<LayoutDropdownButton {...defaultProps} />)
|
||||
screen.getByRole('menuitem', {
|
||||
name: 'Editor & PDF',
|
||||
})
|
||||
screen.getByRole('menuitem', {
|
||||
name: 'PDF only (hide editor)',
|
||||
name: 'Selected PDF only (hide editor)',
|
||||
})
|
||||
screen.getByRole('menuitem', {
|
||||
name: 'Editor only (hide PDF)',
|
||||
})
|
||||
screen.getByRole('menuitem', {
|
||||
name: 'Open PDF in new tab',
|
||||
})
|
||||
})
|
||||
|
||||
it('should select Editor Only when detached and show option to reattach', function () {
|
||||
const detachedProps = Object.assign({}, defaultProps, {
|
||||
detachMode: 'detacher',
|
||||
detachRole: 'detacher',
|
||||
view: 'editor',
|
||||
})
|
||||
|
||||
render(<LayoutDropdownButton {...detachedProps} />)
|
||||
|
||||
screen.getByRole('menuitem', {
|
||||
name: 'Selected Editor only (hide PDF)',
|
||||
})
|
||||
screen.getByRole('menuitem', {
|
||||
name: 'Bring PDF back to this tab',
|
||||
})
|
||||
})
|
||||
|
||||
it('should show processing when detaching', function () {
|
||||
const detachedProps = Object.assign({}, defaultProps, {
|
||||
detachMode: 'detaching',
|
||||
detachRole: 'detacher',
|
||||
view: 'editor',
|
||||
})
|
||||
|
||||
render(<LayoutDropdownButton {...detachedProps} />)
|
||||
|
||||
screen.getByText('Layout processing')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -21,6 +21,8 @@ describe('<ToolbarHeader />', function () {
|
|||
handleChangeLayout: () => {},
|
||||
pdfLayout: '',
|
||||
view: '',
|
||||
reattach: () => {},
|
||||
detach: () => {},
|
||||
}
|
||||
|
||||
describe('cobranding logo', function () {
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('<OutlinePane />', function () {
|
|||
value: {
|
||||
getItem: sinon.stub().returns(null),
|
||||
setItem: sinon.stub(),
|
||||
removeItem: sinon.stub(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,6 +6,7 @@ import sinon from 'sinon'
|
|||
import { UserProvider } from '../../../frontend/js/shared/context/user-context'
|
||||
import { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
|
||||
import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context'
|
||||
import { DetachProvider } from '../../../frontend/js/shared/context/detach-context'
|
||||
import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context'
|
||||
import { IdeProvider } from '../../../frontend/js/shared/context/ide-context'
|
||||
import { get } from 'lodash'
|
||||
|
@ -91,7 +92,9 @@ export function EditorProviders({
|
|||
<ProjectProvider>
|
||||
<EditorProvider settings={{}}>
|
||||
<CompileProvider>
|
||||
<LayoutProvider>{children}</LayoutProvider>
|
||||
<DetachProvider>
|
||||
<LayoutProvider>{children}</LayoutProvider>
|
||||
</DetachProvider>
|
||||
</CompileProvider>
|
||||
</EditorProvider>
|
||||
</ProjectProvider>
|
||||
|
|
Loading…
Add table
Reference in a new issue