mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Add IdeProvider (#4161)
GitOrigin-RevId: cab09354cf4b325a1ea3814a8c4c49fac7c831be
This commit is contained in:
parent
9d2edb0c45
commit
ad3c66b36e
13 changed files with 302 additions and 333 deletions
|
@ -84,12 +84,11 @@ export default function ShareProjectModal({
|
|||
show,
|
||||
animation = true,
|
||||
isAdmin,
|
||||
ide,
|
||||
}) {
|
||||
const [inFlight, setInFlight] = useState(false)
|
||||
const [error, setError] = useState()
|
||||
|
||||
const [project, setProject] = useScopeValue('project', ide.$scope, true)
|
||||
const [project, setProject] = useScopeValue('project', true)
|
||||
|
||||
// reset error when the modal is opened
|
||||
useEffect(() => {
|
||||
|
@ -167,8 +166,5 @@ ShareProjectModal.propTypes = {
|
|||
animation: PropTypes.bool,
|
||||
handleHide: PropTypes.func.isRequired,
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
ide: PropTypes.shape({
|
||||
$scope: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
|
|
@ -9,8 +9,7 @@ App.component(
|
|||
'shareProjectModal',
|
||||
react2angular(
|
||||
rootContext.use(ShareProjectModal),
|
||||
Object.keys(ShareProjectModal.propTypes),
|
||||
['ide']
|
||||
Object.keys(ShareProjectModal.propTypes)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -13,11 +13,11 @@ CompileContext.Provider.propTypes = {
|
|||
}),
|
||||
}
|
||||
|
||||
export function CompileProvider({ children, $scope }) {
|
||||
const [pdfUrl] = useScopeValue('pdf.url', $scope)
|
||||
const [pdfDownloadUrl] = useScopeValue('pdf.downloadUrl', $scope)
|
||||
const [logEntries] = useScopeValue('pdf.logEntries', $scope)
|
||||
const [uncompiled] = useScopeValue('pdf.uncompiled', $scope)
|
||||
export function CompileProvider({ children }) {
|
||||
const [pdfUrl] = useScopeValue('pdf.url')
|
||||
const [pdfDownloadUrl] = useScopeValue('pdf.downloadUrl')
|
||||
const [logEntries] = useScopeValue('pdf.logEntries')
|
||||
const [uncompiled] = useScopeValue('pdf.uncompiled')
|
||||
|
||||
const value = {
|
||||
pdfUrl,
|
||||
|
@ -27,17 +27,12 @@ export function CompileProvider({ children, $scope }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CompileContext.Provider value={value}>
|
||||
{children}
|
||||
</CompileContext.Provider>
|
||||
</>
|
||||
<CompileContext.Provider value={value}>{children}</CompileContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
CompileProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
$scope: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
export function useCompileContext(propTypes) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { createContext, useCallback, useContext, useEffect } from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
import useScopeValue from './util/scope-value-hook'
|
||||
import useBrowserWindow from '../hooks/use-browser-window'
|
||||
import { useIdeContext } from './ide-context'
|
||||
|
||||
export const EditorContext = createContext()
|
||||
|
||||
|
@ -30,7 +31,9 @@ EditorContext.Provider.propTypes = {
|
|||
}),
|
||||
}
|
||||
|
||||
export function EditorProvider({ children, ide, settings }) {
|
||||
export function EditorProvider({ children, settings }) {
|
||||
const ide = useIdeContext()
|
||||
|
||||
const cobranding = window.brandVariation
|
||||
? {
|
||||
logoImgUrl: window.brandVariation.logo_url,
|
||||
|
@ -45,26 +48,12 @@ export function EditorProvider({ children, ide, settings }) {
|
|||
}
|
||||
: undefined
|
||||
|
||||
const ownerId =
|
||||
ide.$scope.project && ide.$scope.project.owner
|
||||
? ide.$scope.project.owner._id
|
||||
: null
|
||||
|
||||
const [loading] = useScopeValue('state.loading', ide.$scope)
|
||||
|
||||
const [projectRootDocId] = useScopeValue('project.rootDoc_id', ide.$scope)
|
||||
|
||||
const [projectName, setProjectName] = useScopeValue(
|
||||
'project.name',
|
||||
ide.$scope
|
||||
)
|
||||
|
||||
const [compileGroup] = useScopeValue(
|
||||
'project.features.compileGroup',
|
||||
ide.$scope
|
||||
)
|
||||
|
||||
const [rootFolder] = useScopeValue('rootFolder', ide.$scope)
|
||||
const [loading] = useScopeValue('state.loading')
|
||||
const [projectRootDocId] = useScopeValue('project.rootDoc_id')
|
||||
const [projectName, setProjectName] = useScopeValue('project.name')
|
||||
const [compileGroup] = useScopeValue('project.features.compileGroup')
|
||||
const [rootFolder] = useScopeValue('rootFolder')
|
||||
const [ownerId] = useScopeValue('project.owner.id')
|
||||
|
||||
const renameProject = useCallback(
|
||||
newName => {
|
||||
|
@ -112,22 +101,25 @@ export function EditorProvider({ children, ide, settings }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorContext.Provider value={editorContextValue}>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
</>
|
||||
<EditorContext.Provider value={editorContextValue}>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
EditorProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
ide: PropTypes.any.isRequired,
|
||||
settings: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
export function useEditorContext(propTypes) {
|
||||
const data = useContext(EditorContext)
|
||||
PropTypes.checkPropTypes(propTypes, data, 'data', 'EditorContext.Provider')
|
||||
return data
|
||||
const context = useContext(EditorContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useEditorContext is only available inside EditorProvider')
|
||||
}
|
||||
|
||||
PropTypes.checkPropTypes(propTypes, context, 'data', 'EditorContext.Provider')
|
||||
|
||||
return context
|
||||
}
|
||||
|
|
30
services/web/frontend/js/shared/context/ide-context.js
Normal file
30
services/web/frontend/js/shared/context/ide-context.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React, { createContext, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const IdeContext = createContext()
|
||||
|
||||
IdeContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
$scope: PropTypes.object.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export function useIdeContext() {
|
||||
const context = useContext(IdeContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useIdeContext is only available inside IdeProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export function IdeProvider({ ide, children }) {
|
||||
return <IdeContext.Provider value={ide}>{children}</IdeContext.Provider>
|
||||
}
|
||||
IdeProvider.propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
ide: PropTypes.shape({
|
||||
$scope: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import React, { createContext, useContext, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import useScopeValue from './util/scope-value-hook'
|
||||
import { useIdeContext } from './ide-context'
|
||||
|
||||
export const LayoutContext = createContext()
|
||||
|
||||
|
@ -18,8 +19,11 @@ LayoutContext.Provider.propTypes = {
|
|||
}).isRequired,
|
||||
}
|
||||
|
||||
export function LayoutProvider({ children, $scope }) {
|
||||
const [view, _setView] = useScopeValue('ui.view', $scope)
|
||||
export function LayoutProvider({ children }) {
|
||||
const { $scope } = useIdeContext()
|
||||
|
||||
const [view, _setView] = useScopeValue('ui.view')
|
||||
|
||||
const setView = useCallback(
|
||||
value => {
|
||||
_setView(value)
|
||||
|
@ -30,19 +34,14 @@ export function LayoutProvider({ children, $scope }) {
|
|||
[$scope, _setView]
|
||||
)
|
||||
|
||||
const [chatIsOpen, setChatIsOpen] = useScopeValue('ui.chatOpen', $scope)
|
||||
const [chatIsOpen, setChatIsOpen] = useScopeValue('ui.chatOpen')
|
||||
const [reviewPanelOpen, setReviewPanelOpen] = useScopeValue(
|
||||
'ui.reviewPanelOpen',
|
||||
$scope
|
||||
)
|
||||
const [leftMenuShown, setLeftMenuShown] = useScopeValue(
|
||||
'ui.leftMenuShown',
|
||||
$scope
|
||||
'ui.reviewPanelOpen'
|
||||
)
|
||||
const [leftMenuShown, setLeftMenuShown] = useScopeValue('ui.leftMenuShown')
|
||||
const [pdfLayout] = useScopeValue('ui.pdfLayout')
|
||||
|
||||
const [pdfLayout] = useScopeValue('ui.pdfLayout', $scope)
|
||||
|
||||
const layoutContextValue = {
|
||||
const value = {
|
||||
view,
|
||||
setView,
|
||||
chatIsOpen,
|
||||
|
@ -55,17 +54,12 @@ export function LayoutProvider({ children, $scope }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={layoutContextValue}>
|
||||
{children}
|
||||
</LayoutContext.Provider>
|
||||
<LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
LayoutProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
$scope: PropTypes.shape({
|
||||
toggleHistory: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export function useLayoutContext(propTypes) {
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
|
|||
import createSharedContext from 'react2angular-shared-context'
|
||||
|
||||
import { ApplicationProvider } from './application-context'
|
||||
import { IdeProvider } from './ide-context'
|
||||
import { EditorProvider } from './editor-context'
|
||||
import { CompileProvider } from './compile-context'
|
||||
import { LayoutProvider } from './layout-context'
|
||||
|
@ -13,17 +14,19 @@ export function ContextRoot({ children, ide, settings }) {
|
|||
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<EditorProvider ide={ide} settings={settings}>
|
||||
<CompileProvider $scope={ide.$scope}>
|
||||
<LayoutProvider $scope={ide.$scope}>
|
||||
{isAnonymousUser ? (
|
||||
children
|
||||
) : (
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
)}
|
||||
</LayoutProvider>
|
||||
</CompileProvider>
|
||||
</EditorProvider>
|
||||
<IdeProvider ide={ide}>
|
||||
<EditorProvider settings={settings}>
|
||||
<CompileProvider>
|
||||
<LayoutProvider>
|
||||
{isAnonymousUser ? (
|
||||
children
|
||||
) : (
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
)}
|
||||
</LayoutProvider>
|
||||
</CompileProvider>
|
||||
</EditorProvider>
|
||||
</IdeProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import _ from 'lodash'
|
||||
import { useIdeContext } from '../ide-context'
|
||||
|
||||
/**
|
||||
* Binds a property in an Angular scope making it accessible in a React
|
||||
* component. The interface is compatible with React.useState(), including
|
||||
* the option of passing a function to the setter.
|
||||
*
|
||||
* @param {string} path - dot '.' path of a property in `sourceScope`.
|
||||
* @param {object} $scope - Angular $scope containing the value to bind.
|
||||
* @param {string} path - dot '.' path of a property in the Angular scope.
|
||||
* @param {boolean} deep
|
||||
* @returns {[any, function]} - Binded value and setter function tuple.
|
||||
*/
|
||||
export default function useScopeValue(path, $scope, deep = false) {
|
||||
export default function useScopeValue(path, deep = false) {
|
||||
const { $scope } = useIdeContext({
|
||||
$scope: PropTypes.object.isRequired,
|
||||
})
|
||||
|
||||
const [value, setValue] = useState(() => _.get($scope, path))
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -3,6 +3,7 @@ import PreviewLogsPane from '../js/features/preview/components/preview-logs-pane
|
|||
import { EditorProvider } from '../js/shared/context/editor-context'
|
||||
import { ApplicationProvider } from '../js/shared/context/application-context'
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import { IdeProvider } from '../js/shared/context/ide-context'
|
||||
|
||||
export const TimedOutError = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
|
@ -25,9 +26,11 @@ export const TimedOutError = args => {
|
|||
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<EditorProvider ide={ide} settings={{}}>
|
||||
<PreviewLogsPane {...args} />
|
||||
</EditorProvider>
|
||||
<IdeProvider ide={ide}>
|
||||
<EditorProvider settings={{}}>
|
||||
<PreviewLogsPane {...args} />
|
||||
</EditorProvider>
|
||||
</IdeProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
@ -58,9 +61,11 @@ export const TimedOutErrorWithPriorityCompile = args => {
|
|||
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<EditorProvider ide={ide} settings={{}}>
|
||||
<PreviewLogsPane {...args} />
|
||||
</EditorProvider>
|
||||
<IdeProvider ide={ide}>
|
||||
<EditorProvider settings={{}}>
|
||||
<PreviewLogsPane {...args} />
|
||||
</EditorProvider>
|
||||
</IdeProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import ShareProjectModal from '../js/features/share-project-modal/components/share-project-modal'
|
||||
import { ContextRoot } from '../js/shared/context/root-context'
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import { withContextRoot } from './utils/with-context-root'
|
||||
|
||||
export const LinkSharingOff = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
@ -11,9 +11,7 @@ export const LinkSharingOff = args => {
|
|||
publicAccesLevel: 'private',
|
||||
}
|
||||
|
||||
return renderWithContext(
|
||||
<ShareProjectModal {...args} ide={ideWithProject(project)} />
|
||||
)
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
}
|
||||
|
||||
export const LinkSharingOn = args => {
|
||||
|
@ -24,9 +22,7 @@ export const LinkSharingOn = args => {
|
|||
publicAccesLevel: 'tokenBased',
|
||||
}
|
||||
|
||||
return renderWithContext(
|
||||
<ShareProjectModal {...args} ide={ideWithProject(project)} />
|
||||
)
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
}
|
||||
|
||||
export const LinkSharingLoading = args => {
|
||||
|
@ -38,9 +34,7 @@ export const LinkSharingLoading = args => {
|
|||
tokens: undefined,
|
||||
}
|
||||
|
||||
return renderWithContext(
|
||||
<ShareProjectModal {...args} ide={ideWithProject(project)} />
|
||||
)
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
}
|
||||
|
||||
export const NonAdminLinkSharingOff = args => {
|
||||
|
@ -49,13 +43,9 @@ export const NonAdminLinkSharingOff = args => {
|
|||
publicAccesLevel: 'private',
|
||||
}
|
||||
|
||||
return renderWithContext(
|
||||
<ShareProjectModal
|
||||
{...args}
|
||||
isAdmin={false}
|
||||
ide={ideWithProject(project)}
|
||||
/>
|
||||
)
|
||||
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
|
||||
project,
|
||||
})
|
||||
}
|
||||
|
||||
export const NonAdminLinkSharingOn = args => {
|
||||
|
@ -64,13 +54,9 @@ export const NonAdminLinkSharingOn = args => {
|
|||
publicAccesLevel: 'tokenBased',
|
||||
}
|
||||
|
||||
return renderWithContext(
|
||||
<ShareProjectModal
|
||||
{...args}
|
||||
isAdmin={false}
|
||||
ide={ideWithProject(project)}
|
||||
/>
|
||||
)
|
||||
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
|
||||
project,
|
||||
})
|
||||
}
|
||||
|
||||
export const RestrictedTokenMember = args => {
|
||||
|
@ -91,9 +77,7 @@ export const RestrictedTokenMember = args => {
|
|||
publicAccesLevel: 'tokenBased',
|
||||
}
|
||||
|
||||
return renderWithContext(
|
||||
<ShareProjectModal {...args} ide={ideWithProject(project)} />
|
||||
)
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
}
|
||||
|
||||
export const LegacyLinkSharingReadAndWrite = args => {
|
||||
|
@ -104,9 +88,7 @@ export const LegacyLinkSharingReadAndWrite = args => {
|
|||
publicAccesLevel: 'readAndWrite',
|
||||
}
|
||||
|
||||
return renderWithContext(
|
||||
<ShareProjectModal {...args} ide={ideWithProject(project)} />
|
||||
)
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
}
|
||||
|
||||
export const LegacyLinkSharingReadOnly = args => {
|
||||
|
@ -117,9 +99,7 @@ export const LegacyLinkSharingReadOnly = args => {
|
|||
publicAccesLevel: 'readOnly',
|
||||
}
|
||||
|
||||
return renderWithContext(
|
||||
<ShareProjectModal {...args} ide={ideWithProject(project)} />
|
||||
)
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
}
|
||||
|
||||
export const LimitedCollaborators = args => {
|
||||
|
@ -133,9 +113,7 @@ export const LimitedCollaborators = args => {
|
|||
},
|
||||
}
|
||||
|
||||
return renderWithContext(
|
||||
<ShareProjectModal {...args} ide={ideWithProject(project)} />
|
||||
)
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
}
|
||||
|
||||
const project = {
|
||||
|
@ -199,18 +177,6 @@ export default {
|
|||
},
|
||||
}
|
||||
|
||||
// Unfortunately, we cannot currently use decorators here, since we need to
|
||||
// set a value on window, before the contexts are rendered.
|
||||
// When using decorators, the contexts are rendered before the story, so we
|
||||
// don't have the opportunity to set the window value first.
|
||||
function renderWithContext(Story) {
|
||||
return (
|
||||
<ContextRoot ide={window._ide} settings={{}}>
|
||||
{Story}
|
||||
</ContextRoot>
|
||||
)
|
||||
}
|
||||
|
||||
const contacts = [
|
||||
// user with edited name
|
||||
{
|
||||
|
@ -282,13 +248,3 @@ function setupFetchMock(fetchMock) {
|
|||
// send analytics event
|
||||
.post('express:/event/:key', 200)
|
||||
}
|
||||
|
||||
function ideWithProject(project) {
|
||||
return {
|
||||
$scope: {
|
||||
$watch: () => () => {},
|
||||
$applyAsync: () => {},
|
||||
project,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
22
services/web/frontend/stories/utils/with-context-root.js
Normal file
22
services/web/frontend/stories/utils/with-context-root.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React from 'react'
|
||||
import { ContextRoot } from '../../js/shared/context/root-context'
|
||||
|
||||
// Unfortunately, we cannot currently use decorators here, since we need to
|
||||
// set a value on window, before the contexts are rendered.
|
||||
// When using decorators, the contexts are rendered before the story, so we
|
||||
// don't have the opportunity to set the window value first.
|
||||
export function withContextRoot(Story, scope) {
|
||||
const ide = {
|
||||
...window._ide,
|
||||
$scope: {
|
||||
...window._ide.$scope,
|
||||
...scope,
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextRoot ide={ide} settings={{}}>
|
||||
{Story}
|
||||
</ContextRoot>
|
||||
)
|
||||
}
|
|
@ -4,16 +4,17 @@ import React from 'react'
|
|||
import {
|
||||
screen,
|
||||
fireEvent,
|
||||
render,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { get } from 'lodash'
|
||||
|
||||
import ShareProjectModal from '../../../../../frontend/js/features/share-project-modal/components/share-project-modal'
|
||||
import {
|
||||
renderWithEditorContext,
|
||||
cleanUpContext,
|
||||
EditorProviders,
|
||||
} from '../../../helpers/render-with-context'
|
||||
import * as locationModule from '../../../../../frontend/js/features/share-project-modal/utils/location'
|
||||
|
||||
|
@ -73,23 +74,7 @@ describe('<ShareProjectModal/>', function () {
|
|||
},
|
||||
]
|
||||
|
||||
const ideWithProject = project => {
|
||||
const scope = { project }
|
||||
|
||||
return {
|
||||
$scope: {
|
||||
$watch: (path, callback) => {
|
||||
callback(get(scope, path))
|
||||
return () => null
|
||||
},
|
||||
$applyAsync: () => {},
|
||||
...scope,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const modalProps = {
|
||||
ide: ideWithProject(project),
|
||||
show: true,
|
||||
isAdmin: true,
|
||||
handleHide: sinon.stub(),
|
||||
|
@ -105,7 +90,9 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('renders the modal', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project },
|
||||
})
|
||||
|
||||
await screen.findByText('Share Project')
|
||||
})
|
||||
|
@ -114,7 +101,8 @@ describe('<ShareProjectModal/>', function () {
|
|||
const handleHide = sinon.stub()
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} handleHide={handleHide} />
|
||||
<ShareProjectModal {...modalProps} handleHide={handleHide} />,
|
||||
{ scope: { project } }
|
||||
)
|
||||
|
||||
const [
|
||||
|
@ -129,12 +117,9 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('handles access level "private"', async function () {
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({ ...project, publicAccesLevel: 'private' })}
|
||||
/>
|
||||
)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project: { ...project, publicAccesLevel: 'private' } },
|
||||
})
|
||||
|
||||
await screen.findByText(
|
||||
'Link sharing is off, only invited users can view this project.'
|
||||
|
@ -148,12 +133,9 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('handles access level "tokenBased"', async function () {
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({ ...project, publicAccesLevel: 'tokenBased' })}
|
||||
/>
|
||||
)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project: { ...project, publicAccesLevel: 'tokenBased' } },
|
||||
})
|
||||
|
||||
await screen.findByText('Link sharing is on')
|
||||
await screen.findByRole('button', { name: 'Turn off link sharing' })
|
||||
|
@ -165,12 +147,9 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('handles legacy access level "readAndWrite"', async function () {
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({ ...project, publicAccesLevel: 'readAndWrite' })}
|
||||
/>
|
||||
)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project: { ...project, publicAccesLevel: 'readAndWrite' } },
|
||||
})
|
||||
|
||||
await screen.findByText(
|
||||
'This project is public and can be edited by anyone with the URL.'
|
||||
|
@ -179,12 +158,9 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('handles legacy access level "readOnly"', async function () {
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({ ...project, publicAccesLevel: 'readOnly' })}
|
||||
/>
|
||||
)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project: { ...project, publicAccesLevel: 'readOnly' } },
|
||||
})
|
||||
|
||||
await screen.findByText(
|
||||
'This project is public and can be viewed but not edited by anyone with the URL'
|
||||
|
@ -202,16 +178,18 @@ describe('<ShareProjectModal/>', function () {
|
|||
]
|
||||
|
||||
// render as admin: actions should be present
|
||||
const { rerender } = renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
isAdmin
|
||||
/>
|
||||
const { rerender } = render(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ShareProjectModal {...modalProps} isAdmin />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
await screen.findByRole('button', { name: 'Turn off link sharing' })
|
||||
|
@ -219,15 +197,17 @@ describe('<ShareProjectModal/>', function () {
|
|||
|
||||
// render as non-admin (non-owner), link sharing on: actions should be missing and message should be present
|
||||
rerender(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
isAdmin={false}
|
||||
/>
|
||||
<EditorProviders
|
||||
scope={{
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ShareProjectModal {...modalProps} isAdmin={false} />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
await screen.findByText(
|
||||
|
@ -242,15 +222,17 @@ describe('<ShareProjectModal/>', function () {
|
|||
|
||||
// render as non-admin (non-owner), link sharing off: actions should be missing and message should be present
|
||||
rerender(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'private',
|
||||
})}
|
||||
isAdmin={false}
|
||||
/>
|
||||
<EditorProviders
|
||||
scope={{
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'private',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ShareProjectModal {...modalProps} isAdmin={false} />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
await screen.findByText(
|
||||
|
@ -265,13 +247,10 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('only shows read-only token link to restricted token members', async function () {
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({ ...project, publicAccesLevel: 'tokenBased' })}
|
||||
/>,
|
||||
{ isRestrictedTokenMember: true }
|
||||
)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
isRestrictedTokenMember: true,
|
||||
scope: { project: { ...project, publicAccesLevel: 'tokenBased' } },
|
||||
})
|
||||
|
||||
// no buttons
|
||||
expect(screen.queryByRole('button', { name: 'Turn on link sharing' })).to.be
|
||||
|
@ -312,17 +291,16 @@ describe('<ShareProjectModal/>', function () {
|
|||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
members,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.queryAllByText('project-owner@example.com')).to.have.length(1)
|
||||
expect(screen.queryAllByText('member-author@example.com')).to.have.length(1)
|
||||
|
@ -356,16 +334,15 @@ describe('<ShareProjectModal/>', function () {
|
|||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const [, closeButton] = screen.getAllByRole('button', {
|
||||
name: 'Close',
|
||||
|
@ -391,16 +368,15 @@ describe('<ShareProjectModal/>', function () {
|
|||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const [, closeButton] = screen.getAllByRole('button', {
|
||||
name: 'Close',
|
||||
|
@ -425,16 +401,15 @@ describe('<ShareProjectModal/>', function () {
|
|||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
members,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const [, closeButton] = await screen.getAllByRole('button', {
|
||||
name: 'Close',
|
||||
|
@ -468,16 +443,15 @@ describe('<ShareProjectModal/>', function () {
|
|||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
members,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.queryAllByText('member-viewer@example.com')).to.have.length(1)
|
||||
|
||||
|
@ -508,16 +482,15 @@ describe('<ShareProjectModal/>', function () {
|
|||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
members,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.queryAllByText('member-viewer@example.com')).to.have.length(1)
|
||||
|
||||
|
@ -551,15 +524,14 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('sends invites to input email addresses', async function () {
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const [inputElement] = await screen.findAllByLabelText(
|
||||
'Share with your collaborators'
|
||||
|
@ -641,24 +613,21 @@ describe('<ShareProjectModal/>', function () {
|
|||
it('displays a message when the collaborator limit is reached', async function () {
|
||||
fetchMock.post('/event/project-sharing-paywall-prompt', {})
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
user: {
|
||||
id: '123abd',
|
||||
allowedFreeTrial: true,
|
||||
},
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
features: {
|
||||
collaborators: 0,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
{
|
||||
user: {
|
||||
id: '123abd',
|
||||
allowedFreeTrial: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.queryByLabelText('Share with your collaborators')).to.be.null
|
||||
|
||||
|
@ -668,15 +637,14 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('handles server error responses', async function () {
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// loading contacts
|
||||
await waitFor(() => {
|
||||
|
@ -738,25 +706,19 @@ describe('<ShareProjectModal/>', function () {
|
|||
|
||||
const watchCallbacks = {}
|
||||
|
||||
const ideWithProject = project => {
|
||||
const scopeWithProject = project => {
|
||||
return {
|
||||
$scope: {
|
||||
$watch: (path, callback, deep) => {
|
||||
watchCallbacks[path] = callback
|
||||
return () => {}
|
||||
},
|
||||
$applyAsync: () => {},
|
||||
project,
|
||||
$watch: (path, callback) => {
|
||||
watchCallbacks[path] = callback
|
||||
return () => {}
|
||||
},
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
ide={ideWithProject({ ...project, publicAccesLevel: 'private' })}
|
||||
/>
|
||||
)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: scopeWithProject({ ...project, publicAccesLevel: 'private' }),
|
||||
})
|
||||
|
||||
await screen.findByText(
|
||||
'Link sharing is off, only invited users can view this project.'
|
||||
|
@ -799,7 +761,9 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('avoids selecting unmatched contact', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project },
|
||||
})
|
||||
|
||||
const [inputElement] = await screen.findAllByLabelText(
|
||||
'Share with your collaborators'
|
||||
|
|
|
@ -8,6 +8,8 @@ import { ApplicationProvider } from '../../../frontend/js/shared/context/applica
|
|||
import { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
|
||||
import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context'
|
||||
import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context'
|
||||
import { IdeProvider } from '../../../frontend/js/shared/context/ide-context'
|
||||
import { get } from 'lodash'
|
||||
|
||||
export function EditorProviders({
|
||||
user = { id: '123abd' },
|
||||
|
@ -17,6 +19,7 @@ export function EditorProviders({
|
|||
removeListener: sinon.stub(),
|
||||
},
|
||||
isRestrictedTokenMember = false,
|
||||
scope,
|
||||
children,
|
||||
}) {
|
||||
window.user = user || window.user
|
||||
|
@ -24,38 +27,44 @@ export function EditorProviders({
|
|||
window.project_id = projectId != null ? projectId : window.project_id
|
||||
window.isRestrictedTokenMember = isRestrictedTokenMember
|
||||
|
||||
window._ide = {
|
||||
$scope: {
|
||||
project: {
|
||||
owner: {
|
||||
_id: '124abd',
|
||||
},
|
||||
const $scope = {
|
||||
project: {
|
||||
owner: {
|
||||
_id: '124abd',
|
||||
},
|
||||
ui: {
|
||||
chatOpen: true,
|
||||
pdfLayout: 'flat',
|
||||
},
|
||||
$watch: () => {},
|
||||
toggleHistory: () => {},
|
||||
},
|
||||
socket,
|
||||
ui: {
|
||||
chatOpen: true,
|
||||
pdfLayout: 'flat',
|
||||
},
|
||||
$watch: (path, callback) => {
|
||||
callback(get($scope, path))
|
||||
return () => null
|
||||
},
|
||||
$applyAsync: () => {},
|
||||
toggleHistory: () => {},
|
||||
...scope,
|
||||
}
|
||||
|
||||
window._ide = { $scope, socket }
|
||||
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<EditorProvider ide={window._ide} settings={{}}>
|
||||
<LayoutProvider $scope={window._ide.$scope}>{children}</LayoutProvider>
|
||||
</EditorProvider>
|
||||
<IdeProvider ide={window._ide}>
|
||||
<EditorProvider settings={{}}>
|
||||
<LayoutProvider>{children}</LayoutProvider>
|
||||
</EditorProvider>
|
||||
</IdeProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderWithEditorContext(component, contextProps) {
|
||||
return render(component, {
|
||||
// eslint-disable-next-line react/display-name
|
||||
wrapper: ({ children }) => (
|
||||
<EditorProviders {...contextProps}>{children}</EditorProviders>
|
||||
),
|
||||
})
|
||||
const EditorProvidersWrapper = ({ children }) => (
|
||||
<EditorProviders {...contextProps}>{children}</EditorProviders>
|
||||
)
|
||||
|
||||
return render(component, { wrapper: EditorProvidersWrapper })
|
||||
}
|
||||
|
||||
export function ChatProviders({ children, ...props }) {
|
||||
|
@ -67,12 +76,11 @@ export function ChatProviders({ children, ...props }) {
|
|||
}
|
||||
|
||||
export function renderWithChatContext(component, props) {
|
||||
return render(component, {
|
||||
// eslint-disable-next-line react/display-name
|
||||
wrapper: ({ children }) => (
|
||||
<ChatProviders {...props}>{children}</ChatProviders>
|
||||
),
|
||||
})
|
||||
const ChatProvidersWrapper = ({ children }) => (
|
||||
<ChatProviders {...props}>{children}</ChatProviders>
|
||||
)
|
||||
|
||||
return render(component, { wrapper: ChatProvidersWrapper })
|
||||
}
|
||||
|
||||
export function cleanUpContext() {
|
||||
|
|
Loading…
Reference in a new issue