Standardise scope/context usage in Storybook stories (#7842)

GitOrigin-RevId: 109a4357fc3b083ffbd3af5b8c98acf0f655f297
This commit is contained in:
Alf Eaton 2022-05-16 10:38:20 +01:00 committed by Copybot
parent cb065e7f12
commit d91ee50762
35 changed files with 643 additions and 537 deletions

View file

@ -9,14 +9,14 @@ import Icon from '../../../shared/components/icon'
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif']
const textExtensions = window.ExposedSettings.textExtensions
export default function FileView({ file, storeReferencesKeys }) {
const [contentLoading, setContentLoading] = useState(true)
const [hasError, setHasError] = useState(false)
const { t } = useTranslation()
const { textExtensions } = window.ExposedSettings
const extension = file.name.split('.').pop().toLowerCase()
const isUnpreviewableFile =
!imageExtensions.includes(extension) && !textExtensions.includes(extension)

View file

@ -1,7 +1,7 @@
// Conditionally enable Sentry based on whether the DSN token is set
import getMeta from '../utils/meta'
const reporterPromise = window.ExposedSettings.sentryDsn
const reporterPromise = window.ExposedSettings?.sentryDsn
? sentryReporter()
: nullReporter()

View file

@ -1,44 +1,8 @@
import { v4 as uuid } from 'uuid'
import { ContextRoot } from '../js/shared/context/root-context'
import { useEffect } from 'react'
import ChatPane from '../js/features/chat/components/chat-pane'
import { stubMathJax } from '../../test/frontend/features/chat/components/stubs'
import useFetchMock from './hooks/use-fetch-mock'
const ONE_MINUTE = 60 * 1000
const user = {
id: 'fake_user',
first_name: 'mortimer',
email: 'fake@example.com',
}
const user2 = {
id: 'another_fake_user',
first_name: 'leopold',
email: 'another_fake@example.com',
}
function generateMessages(count) {
const messages = []
let timestamp = new Date().getTime() // newest message goes first
for (let i = 0; i <= count; i++) {
const author = Math.random() > 0.5 ? user : user2
// modify the timestamp so the previous message has 70% chances to be within 5 minutes from
// the current one, for grouping purposes
timestamp -= (4.3 + Math.random()) * ONE_MINUTE
messages.push({
id: uuid(),
content: `message #${i}`,
user: author,
timestamp,
})
}
return messages
}
stubMathJax()
import { generateMessages } from './fixtures/chat-messages'
import { ScopeDecorator } from './decorators/scope'
export const Conversation = args => {
useFetchMock(fetchMock => {
@ -66,6 +30,14 @@ export const Loading = args => {
return <ChatPane {...args} />
}
export const LoadingError = args => {
useFetchMock(fetchMock => {
fetchMock.get(/messages/, 500)
})
return <ChatPane {...args} />
}
export default {
title: 'Editor / Chat',
component: ChatPane,
@ -76,13 +48,22 @@ export default {
resetUnreadMessages: () => {},
},
decorators: [
Story => (
<>
<style>{'html, body, .chat { height: 100%; width: 100%; }'}</style>
<ContextRoot ide={window._ide} settings={{}}>
<Story />
</ContextRoot>
</>
),
ScopeDecorator,
Story => {
useEffect(() => {
window.MathJax = {
Hub: {
Queue: () => {},
config: { tex2jax: { inlineMath: [['$', '$']] } },
},
}
return () => {
delete window.MathJax
}
}, [])
return <Story />
},
],
}

View file

@ -1,8 +1,6 @@
import useFetchMock from './hooks/use-fetch-mock'
import { withContextRoot } from './utils/with-context-root'
import CloneProjectModal from '../js/features/clone-project-modal/components/clone-project-modal'
const project = { _id: 'original-project', name: 'Project Title' }
import { ScopeDecorator } from './decorators/scope'
export const Success = args => {
useFetchMock(fetchMock => {
@ -13,7 +11,7 @@ export const Success = args => {
)
})
return withContextRoot(<CloneProjectModal {...args} />, { project })
return <CloneProjectModal {...args} />
}
export const GenericErrorResponse = args => {
@ -25,7 +23,7 @@ export const GenericErrorResponse = args => {
)
})
return withContextRoot(<CloneProjectModal {...args} />, { project })
return <CloneProjectModal {...args} />
}
export const SpecificErrorResponse = args => {
@ -37,7 +35,7 @@ export const SpecificErrorResponse = args => {
)
})
return withContextRoot(<CloneProjectModal {...args} />, { project })
return <CloneProjectModal {...args} />
}
export default {
@ -50,4 +48,5 @@ export default {
handleHide: { action: 'close modal' },
openProject: { action: 'open project' },
},
decorators: [ScopeDecorator],
}

View file

@ -1,25 +1,26 @@
import { useState } from 'react'
import useFetchMock from './hooks/use-fetch-mock'
import ContactUsModal from '../../modules/support/frontend/js/components/contact-us-modal'
import { withContextRoot } from './utils/with-context-root'
import { ScopeDecorator } from './decorators/scope'
export const Generic = () => {
const [show, setShow] = useState(true)
const handleHide = () => setShow(false)
useFetchMock(fetchMock => {
fetchMock.post('express:/support', { status: 200 }, { delay: 1000 })
fetchMock.post('/support', { status: 200 }, { delay: 1000 })
})
return withContextRoot(<ContactUsModal show={show} handleHide={handleHide} />)
return <ContactUsModal show={show} handleHide={handleHide} />
}
export const RequestError = args => {
useFetchMock(fetchMock => {
fetchMock.post('express:/support', { status: 404 }, { delay: 250 })
fetchMock.post('/support', { status: 404 }, { delay: 250 })
})
return withContextRoot(<ContactUsModal {...args} />)
return <ContactUsModal {...args} />
}
export default {
@ -32,4 +33,5 @@ export default {
argTypes: {
handleHide: { action: 'close modal' },
},
decorators: [ScopeDecorator],
}

View file

@ -0,0 +1,193 @@
import { useEffect, useMemo } from 'react'
import { get } from 'lodash'
import { ContextRoot } from '../../js/shared/context/root-context'
import { User } from '../../../types/user'
import { Project } from '../../../types/project'
import {
mockBuildFile,
mockCompile,
mockCompileError,
} from '../fixtures/compile'
import useFetchMock from '../hooks/use-fetch-mock'
const scopeWatchers = []
const initialize = () => {
const user: User = {
id: 'story-user',
email: 'story-user@example.com',
allowedFreeTrial: true,
features: { dropbox: true, symbolPalette: true },
}
const project: Project = {
_id: 'a-project',
name: 'A Project',
features: { mendeley: true, zotero: true },
tokens: {},
owner: {
_id: 'a-user',
email: 'stories@overleaf.com',
},
members: [],
invites: [],
rootDocId: '5e74f1a7ce17ae0041dfd056',
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
fileRefs: [],
folders: [],
},
],
}
const scope = {
user,
project,
$watch: (key, callback) => {
scopeWatchers.push([key, callback])
},
$applyAsync: callback => {
window.setTimeout(() => {
callback()
for (const [key, watcher] of scopeWatchers) {
watcher(get(ide.$scope, key))
}
}, 0)
},
$on: (eventName, callback) => {
//
},
$broadcast: () => {},
$root: {
_references: {
keys: [],
},
},
ui: {
chatOpen: true,
pdfLayout: 'flat',
},
settings: {
pdfViewer: 'js',
syntaxValidation: true,
},
toggleHistory: () => {},
editor: {
richText: false,
newSourceEditor: false,
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
hasLintingError: false,
permissionsLevel: 'owner',
}
const ide = {
$scope: scope,
socket: {
on: () => {},
removeListener: () => {},
},
fileTreeManager: {
findEntityById: () => null,
findEntityByPath: () => null,
getEntityPath: () => null,
getRootDocDirname: () => undefined,
},
editorManager: {
getCurrentDocId: () => 'foo',
openDoc: (id, options) => {
console.log('open doc', id, options)
},
},
metadataManager: {
metadata: {
state: {
documents: {},
},
},
},
}
window.user = user
window.ExposedSettings = {
appName: 'Overleaf',
maxEntitiesPerProject: 10,
maxUploadSize: 5 * 1024 * 1024,
enableSubscriptions: true,
textExtensions: [
'tex',
'latex',
'sty',
'cls',
'bst',
'bib',
'bibtex',
'txt',
'tikz',
'mtx',
'rtex',
'md',
'asy',
'latexmkrc',
'lbx',
'bbx',
'cbx',
'm',
'lco',
'dtx',
'ins',
'ist',
'def',
'clo',
'ldf',
'rmd',
'lua',
'gv',
'mf',
],
}
window.project_id = project._id
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', user)
window.gitBridgePublicBaseUrl = 'https://git.stories.com'
window._ide = ide
return ide
}
export const ScopeDecorator = Story => {
// mock compile on load
useFetchMock(fetchMock => {
mockCompile(fetchMock)
mockCompileError(fetchMock)
mockBuildFile(fetchMock)
})
// clear scopeWatchers on unmount
useEffect(() => {
return () => {
scopeWatchers.length = 0
}
}, [])
const ide = useMemo(() => {
return initialize()
}, [])
return (
<ContextRoot ide={ide} settings={{}}>
<Story />
</ContextRoot>
)
}

View file

@ -1,16 +1,12 @@
import EditorSwitch from '../js/features/source-editor/components/editor-switch'
import { withContextRoot } from './utils/with-context-root'
import { ScopeDecorator } from './decorators/scope'
export default {
title: 'Editor / Switch',
component: EditorSwitch,
decorators: [ScopeDecorator],
}
export const Switcher = () => {
return withContextRoot(<EditorSwitch />, {
editor: {
richText: false,
newSourceEditor: false,
},
})
return <EditorSwitch />
}

View file

@ -1,11 +1,12 @@
import MockedSocket from 'socket.io-mock'
import { withContextRoot } from './utils/with-context-root'
import { rootFolderBase } from './fixtures/file-tree-base'
import { rootFolderLimit } from './fixtures/file-tree-limit'
import FileTreeRoot from '../js/features/file-tree/components/file-tree-root'
import FileTreeError from '../js/features/file-tree/components/file-tree-error'
import useFetchMock from './hooks/use-fetch-mock'
import { ScopeDecorator } from './decorators/scope'
import { useScope } from './hooks/use-scope'
const MOCK_DELAY = 2000
@ -87,24 +88,30 @@ function defaultSetupMocks(fetchMock) {
export const FullTree = args => {
useFetchMock(defaultSetupMocks)
return withContextRoot(<FileTreeRoot {...args} />, {
useScope({
project: DEFAULT_PROJECT,
permissionsLevel: 'owner',
})
return <FileTreeRoot {...args} />
}
export const ReadOnly = args => {
return withContextRoot(<FileTreeRoot {...args} />, {
useScope({
project: DEFAULT_PROJECT,
permissionsLevel: 'readOnly',
})
return <FileTreeRoot {...args} />
}
export const Disconnected = args => {
return withContextRoot(<FileTreeRoot {...args} />, {
useScope({
project: DEFAULT_PROJECT,
permissionsLevel: 'owner',
})
return <FileTreeRoot {...args} />
}
Disconnected.args = { isConnected: false }
@ -125,25 +132,34 @@ export const NetworkErrors = args => {
})
})
return withContextRoot(<FileTreeRoot {...args} />, {
useScope({
project: DEFAULT_PROJECT,
permissionsLevel: 'owner',
})
return <FileTreeRoot {...args} />
}
export const FallbackError = args => {
return withContextRoot(<FileTreeError {...args} />, {
useScope({
project: DEFAULT_PROJECT,
})
return <FileTreeError {...args} />
}
export const FilesLimit = args => {
useFetchMock(defaultSetupMocks)
return withContextRoot(<FileTreeRoot {...args} />, {
project: { ...DEFAULT_PROJECT, rootFolder: rootFolderLimit },
useScope({
project: {
...DEFAULT_PROJECT,
rootFolder: rootFolderLimit,
},
permissionsLevel: 'owner',
})
return <FileTreeRoot {...args} />
}
export default {
@ -167,6 +183,7 @@ export default {
onSelect: { action: 'onSelect' },
},
decorators: [
ScopeDecorator,
Story => (
<>
<style>{'html, body, .file-tree { height: 100%; width: 100%; }'}</style>

View file

@ -1,6 +1,6 @@
import { ContextRoot } from '../js/shared/context/root-context'
import FileView from '../js/features/file-view/components/file-view'
import useFetchMock from './hooks/use-fetch-mock'
import { ScopeDecorator } from './decorators/scope'
const bodies = {
latex: `\\documentclass{article}
@ -252,11 +252,5 @@ export default {
argTypes: {
storeReferencesKeys: { action: 'store references keys' },
},
decorators: [
Story => (
<ContextRoot ide={window._ide} settings={{}}>
<Story />
</ContextRoot>
),
],
decorators: [ScopeDecorator],
}

View file

@ -0,0 +1,34 @@
import { v4 as uuid } from 'uuid'
const ONE_MINUTE = 60 * 1000
const user = {
id: 'fake_user',
first_name: 'mortimer',
email: 'fake@example.com',
}
const user2 = {
id: 'another_fake_user',
first_name: 'leopold',
email: 'another_fake@example.com',
}
export function generateMessages(count) {
const messages = []
let timestamp = new Date().getTime() // newest message goes first
for (let i = 0; i <= count; i++) {
const author = Math.random() > 0.5 ? user : user2
// modify the timestamp so the previous message has 70% chances to be within 5 minutes from
// the current one, for grouping purposes
timestamp -= (4.3 + Math.random()) * ONE_MINUTE
messages.push({
id: uuid(),
content: `message #${i}`,
user: author,
timestamp,
})
}
return messages
}

View file

@ -58,7 +58,7 @@ export const mockCompile = (fetchMock, delay = 1000) =>
outputFiles: cloneDeep(outputFiles),
},
},
{ delay }
{ delay, overwriteRoutes: true }
)
export const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
@ -97,6 +97,7 @@ export const mockCompileValidationIssues = (
export const mockClearCache = fetchMock =>
fetchMock.delete('express:/project/:projectId/output', 204, {
delay: 1000,
overwriteRoutes: true,
})
export const mockBuildFile = fetchMock =>
@ -156,7 +157,7 @@ LaTeX Font Info: External font \`cmex10' loaded for size
return 404
}
},
{ sendAsJson: false }
{ sendAsJson: false, overwriteRoutes: true }
)
const mockHighlights = [
@ -215,7 +216,7 @@ export const mockValidPdf = fetchMock =>
xhr.send()
})
},
{ sendAsJson: false }
{ sendAsJson: false, overwriteRoutes: true }
)
export const mockSynctex = fetchMock =>

View file

@ -0,0 +1,41 @@
export const contacts = [
// user with edited name
{
type: 'user',
email: 'test-user@example.com',
first_name: 'Test',
last_name: 'User',
name: 'Test User',
},
// user with default name (email prefix)
{
type: 'user',
email: 'test@example.com',
first_name: 'test',
},
// no last name
{
type: 'user',
first_name: 'Eratosthenes',
email: 'eratosthenes@example.com',
},
// more users
{
type: 'user',
first_name: 'Claudius',
last_name: 'Ptolemy',
email: 'ptolemy@example.com',
},
{
type: 'user',
first_name: 'Abd al-Rahman',
last_name: 'Al-Sufi',
email: 'al-sufi@example.com',
},
{
type: 'user',
first_name: 'Nicolaus',
last_name: 'Copernicus',
email: 'copernicus@example.com',
},
]

View file

@ -1,71 +0,0 @@
import sinon from 'sinon'
export function setupContext() {
window.project_id = '1234'
window.user = {
id: 'fake_user',
allowedFreeTrial: true,
}
const $scope = {
...window._ide?.$scope,
user: window.user,
project: {
_id: window.project_id,
name: 'Project Fake Name',
features: {},
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [],
fileRefs: [],
},
],
},
$watch: () => {},
$applyAsync: () => {},
$broadcast: () => {},
ui: {
chatOpen: true,
pdfLayout: 'flat',
},
settings: {
pdfViewer: 'js',
},
toggleHistory: () => {},
}
window._ide = {
...window._ide,
$scope,
socket: {
on: sinon.stub(),
removeListener: sinon.stub(),
},
fileTreeManager: {
findEntityById: () => null,
findEntityByPath: () => null,
getEntityPath: () => null,
getRootDocDirname: () => undefined,
},
editorManager: {
getCurrentDocId: () => 'foo',
openDoc: (id, options) => {
console.log('open doc', id, options)
},
},
metadataManager: {
metadata: {
state: {
documents: {},
},
},
},
}
window.ExposedSettings = window.ExposedSettings || {}
window.ExposedSettings.appName = 'Overleaf'
window.gitBridgePublicBaseUrl = 'https://git.stories.com'
window.metaAttributesCache = window.metaAttributesCache || new Map()
window.metaAttributesCache.set('ol-user', window.user)
}

View file

@ -0,0 +1,48 @@
import { Project } from '../../../types/project'
export const project: Project = {
_id: 'a-project',
name: 'A Project',
features: {
collaborators: -1, // unlimited
},
publicAccesLevel: 'private',
tokens: {
readOnly: 'ro-token',
readAndWrite: 'rw-token',
},
owner: {
_id: 'project-owner',
email: 'stories@overleaf.com',
},
members: [
{
_id: 'viewer-member',
type: 'user',
privileges: 'readOnly',
name: 'Viewer User',
email: 'viewer@example.com',
},
{
_id: 'author-member',
type: 'user',
privileges: 'readAndWrite',
name: 'Author User',
email: 'author@example.com',
},
],
invites: [
{
_id: 'test-invite-1',
privileges: 'readOnly',
name: 'Invited Viewer',
email: 'invited-viewer@example.com',
},
{
_id: 'test-invite-2',
privileges: 'readAndWrite',
name: 'Invited Author',
email: 'invited-author@example.com',
},
],
}

View file

@ -1,6 +1,5 @@
import { ContextRoot } from '../js/shared/context/root-context'
import importOverleafModules from '../macros/import-overleaf-module.macro'
import useFetchMock from './hooks/use-fetch-mock'
import { ScopeDecorator } from './decorators/scope'
const [
{
@ -19,7 +18,10 @@ CollaboratorModal.args = {
}
export const TeaserModal = args => {
useFetchMock(fetchMock => fetchMock.post('express:/event/:key', 202))
// TODO: mock navigator.sendBeacon?
// useFetchMock(fetchMock => {
// fetchMock.post('express:/event/:key', 202)
// })
return <GitBridgeModal {...args} />
}
@ -36,13 +38,5 @@ export default {
argTypes: {
handleHide: { action: 'handleHide' },
},
decorators: [
Story => (
<>
<ContextRoot ide={window._ide} settings={{}}>
<Story />
</ContextRoot>
</>
),
],
decorators: [ScopeDecorator],
}

View file

@ -1,22 +0,0 @@
import { useEffect } from 'react'
import fetchMock from 'fetch-mock'
fetchMock.config.fallbackToNetwork = true
/**
* Run callback to mock fetch routes, call restore() when unmounted
*/
export default function useFetchMock(callback) {
useEffect(() => {
return () => {
fetchMock.restore()
}
}, [])
// Running fetchMock.restore() here as well,
// in case there was an error before the component was unmounted.
fetchMock.restore()
// The callback has to be run here, rather than in useEffect,
// so it's run before the component is rendered.
callback(fetchMock)
}

View file

@ -0,0 +1,16 @@
import { useLayoutEffect } from 'react'
import fetchMock from 'fetch-mock'
fetchMock.config.fallbackToNetwork = true
/**
* Run callback to mock fetch routes, call restore() when unmounted
*/
export default function useFetchMock(callback) {
useLayoutEffect(() => {
callback(fetchMock)
return () => {
fetchMock.restore()
}
}, [callback])
}

View file

@ -0,0 +1,8 @@
/**
* Set values on window.metaAttributesCache, for use in Storybook stories
*/
export const useMeta = (meta: Record<string, unknown>) => {
for (const [key, value] of Object.entries(meta)) {
window.metaAttributesCache.set(key, value)
}
}

View file

@ -0,0 +1,16 @@
import { merge } from 'lodash'
import { useLayoutEffect, useRef } from 'react'
/**
* Merge properties with the scope object, for use in Storybook stories
*/
export const useScope = (scope: Record<string, unknown>) => {
const scopeRef = useRef(null)
if (scopeRef.current === null) {
scopeRef.current = scope
}
useLayoutEffect(() => {
merge(window._ide.$scope, scopeRef.current)
}, [])
}

View file

@ -1,27 +1,10 @@
import { useEffect } from 'react'
import { withContextRoot } from './../../utils/with-context-root'
import FileTreeContext from '../../../js/features/file-tree/components/file-tree-context'
import FileTreeCreateNameProvider from '../../../js/features/file-tree/contexts/file-tree-create-name'
import FileTreeCreateFormProvider from '../../../js/features/file-tree/contexts/file-tree-create-form'
import { useFileTreeActionable } from '../../../js/features/file-tree/contexts/file-tree-actionable'
import PropTypes from 'prop-types'
export const DEFAULT_PROJECT = {
_id: '123abc',
name: 'Some Project',
rootDocId: '5e74f1a7ce17ae0041dfd056',
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [],
fileRefs: [],
},
],
features: { mendeley: true, zotero: true },
}
const defaultFileTreeContextProps = {
refProviders: { mendeley: false, zotero: false },
reindexReferences: () => {
@ -87,19 +70,6 @@ export const mockCreateFileModalFetch = fetchMock =>
},
],
})
.post('express:/project/:projectId/compile', {
status: 'success',
outputFiles: [
{
build: 'foo',
path: 'baz.jpg',
},
{
build: 'foo',
path: 'ball.jpg',
},
],
})
.post('express:/project/:projectId/doc', (path, req) => {
console.log({ path, req })
return 204
@ -114,14 +84,10 @@ export const mockCreateFileModalFetch = fetchMock =>
})
export const createFileModalDecorator =
(
fileTreeContextProps = {},
projectProps = {},
createMode = 'doc'
// eslint-disable-next-line react/display-name
) =>
(fileTreeContextProps = {}, createMode = 'doc') =>
// eslint-disable-next-line react/display-name
Story => {
return withContextRoot(
return (
<FileTreeContext
{...defaultFileTreeContextProps}
{...fileTreeContextProps}
@ -133,11 +99,7 @@ export const createFileModalDecorator =
</OpenCreateFileModal>
</FileTreeCreateFormProvider>
</FileTreeCreateNameProvider>
</FileTreeContext>,
{
project: { ...DEFAULT_PROJECT, ...projectProps },
permissionsLevel: 'owner',
}
</FileTreeContext>
)
}

View file

@ -5,6 +5,8 @@ import {
} from './create-file-modal-decorator'
import FileTreeModalCreateFile from '../../../js/features/file-tree/components/modals/file-tree-modal-create-file'
import useFetchMock from '../../hooks/use-fetch-mock'
import { ScopeDecorator } from '../../decorators/scope'
import { useScope } from '../../hooks/use-scope'
export const MinimalFeatures = args => {
useFetchMock(mockCreateFileModalFetch)
@ -75,28 +77,26 @@ ErrorImportingFileFromReferenceProvider.decorators = [
export const FileLimitReached = args => {
useFetchMock(mockCreateFileModalFetch)
useScope({
project: {
rootFolder: {
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 10 }, (_, index) => ({
_id: `entity-${index}`,
})),
fileRefs: [],
folders: [],
},
},
})
return <FileTreeModalCreateFile {...args} />
}
FileLimitReached.decorators = [
createFileModalDecorator(
{},
{
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 10 }, (_, index) => ({
_id: `entity-${index}`,
})),
fileRefs: [],
folders: [],
},
],
}
),
]
FileLimitReached.decorators = [createFileModalDecorator()]
export default {
title: 'Editor / Modals / Create File',
component: FileTreeModalCreateFile,
decorators: [ScopeDecorator],
}

View file

@ -1,5 +1,4 @@
import ErrorMessage from '../../../js/features/file-tree/components/file-tree-create/error-message'
import { createFileModalDecorator } from './create-file-modal-decorator'
import { FetchError } from '../../../js/infrastructure/fetch-json'
import {
BlockedFilenameError,
@ -19,7 +18,6 @@ export const KeyedErrors = () => {
</>
)
}
KeyedErrors.decorators = [createFileModalDecorator()]
export const FetchStatusErrors = () => {
return (

View file

@ -1,5 +1,5 @@
import OutlinePane from '../js/features/outline/components/outline-pane'
import { ContextRoot } from '../js/shared/context/root-context'
import { ScopeDecorator } from './decorators/scope'
export const Basic = args => <OutlinePane {...args} />
Basic.args = {
@ -52,11 +52,5 @@ export default {
jumpToLine: () => {},
onToggle: () => {},
},
decorators: [
Story => (
<ContextRoot ide={window._ide} settings={{}}>
<Story />
</ContextRoot>
),
],
decorators: [ScopeDecorator],
}

View file

@ -1,19 +1,20 @@
import ErrorBoundaryFallback from '../js/features/pdf-preview/components/error-boundary-fallback'
import { withContextRoot } from './utils/with-context-root'
import { ScopeDecorator } from './decorators/scope'
export default {
title: 'Editor / PDF Preview / Error Boundary',
component: ErrorBoundaryFallback,
decorators: [ScopeDecorator],
}
export const PreviewErrorBoundary = () => {
return withContextRoot(<ErrorBoundaryFallback type="preview" />)
return <ErrorBoundaryFallback type="preview" />
}
export const PdfErrorBoundary = () => {
return withContextRoot(<ErrorBoundaryFallback type="pdf" />)
return <ErrorBoundaryFallback type="pdf" />
}
export const LogsErrorBoundary = () => {
return withContextRoot(<ErrorBoundaryFallback type="logs" />)
return <ErrorBoundaryFallback type="logs" />
}

View file

@ -1,4 +1,3 @@
import { withContextRoot } from './utils/with-context-root'
import { useCallback, useMemo, useState } from 'react'
import useFetchMock from './hooks/use-fetch-mock'
import { Button } from 'react-bootstrap'
@ -21,6 +20,7 @@ import {
outputFiles,
} from './fixtures/compile'
import { cloneDeep } from 'lodash'
import { ScopeDecorator } from './decorators/scope'
export default {
title: 'Editor / PDF Preview',
@ -30,34 +30,7 @@ export default {
PdfFileList,
PdfPreviewError,
},
}
const project = {
_id: 'a-project',
name: 'A Project',
features: {},
tokens: {},
owner: {
_id: 'a-user',
email: 'stories@overleaf.com',
},
members: [],
invites: [],
}
const scope = {
project,
settings: {
syntaxValidation: true,
},
hasLintingError: false,
$applyAsync: () => {},
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
decorators: [ScopeDecorator],
}
export const Interactive = () => {
@ -205,12 +178,11 @@ export const Interactive = () => {
)
}
return withContextRoot(
return (
<div className="pdf-viewer">
<PdfPreviewPane />
<Inner />
</div>,
scope
</div>
)
}
@ -276,12 +248,11 @@ export const CompileError = () => {
)
}
return withContextRoot(
return (
<>
<PdfPreviewPane />
<Inner />
</>,
scope
</>
)
}
@ -309,7 +280,7 @@ export const DisplayError = () => {
mockCompile(fetchMock)
})
return withContextRoot(
return (
<>
{compileErrors.map(error => (
<div
@ -320,8 +291,7 @@ export const DisplayError = () => {
<PdfPreviewError error={error} />
</div>
))}
</>,
scope
</>
)
}
@ -332,11 +302,10 @@ export const HybridToolbar = () => {
mockEventTracking(fetchMock)
})
return withContextRoot(
return (
<div className="pdf">
<PdfPreviewHybridToolbar />
</div>,
scope
</div>
)
}
@ -361,11 +330,10 @@ export const Logs = () => {
mockClearCache(fetchMock)
})
return withContextRoot(
return (
<div className="pdf">
<PdfLogsViewer />
</div>,
scope
</div>
)
}
@ -393,5 +361,5 @@ export const ValidationIssues = () => {
mockBuildFile(fetchMock)
})
return withContextRoot(<PdfPreviewPane />, scope)
return <PdfPreviewPane />
}

View file

@ -1,6 +1,4 @@
import { useEffect, Suspense } from 'react'
import useFetchMock from './hooks/use-fetch-mock'
import { withContextRoot } from './utils/with-context-root'
import PdfSynctexControls from '../js/features/pdf-preview/components/pdf-synctex-controls'
import PdfViewer from '../js/features/pdf-preview/components/pdf-viewer'
import {
@ -9,24 +7,13 @@ import {
mockSynctex,
mockValidPdf,
} from './fixtures/compile'
import { useEffect, Suspense } from 'react'
import { ScopeDecorator } from './decorators/scope'
export default {
title: 'Editor / PDF Viewer',
component: PdfViewer,
}
const project = {
_id: 'story-project',
}
const scope = {
project,
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
decorators: [ScopeDecorator],
}
export const Interactive = () => {
@ -45,17 +32,16 @@ export const Interactive = () => {
)
}, [])
return withContextRoot(
<Suspense fallback="Loading">
<div>
<div className="pdf-viewer">
return (
<div>
<div className="pdf-viewer">
<Suspense fallback={null}>
<PdfViewer />
</div>
<div style={{ position: 'absolute', top: 150, left: 50 }}>
<PdfSynctexControls />
</div>
</Suspense>
</div>
</Suspense>,
scope
<div style={{ position: 'absolute', top: 150, left: 50 }}>
<PdfSynctexControls />
</div>
</div>
)
}

View file

@ -3,6 +3,9 @@ import LinkingSection from '../../js/features/settings/components/linking-sectio
import { setDefaultMeta, defaultSetupMocks } from './helpers/linking'
import { UserProvider } from '../../js/shared/context/user-context'
import { SSOProvider } from '../../js/features/settings/context/sso-context'
import { ScopeDecorator } from '../decorators/scope'
import { useEffect } from 'react'
import { useMeta } from '../hooks/use-meta'
const MOCK_DELAY = 1000
@ -21,17 +24,23 @@ export const Section = args => {
export const SectionAllUnlinked = args => {
useFetchMock(defaultSetupMocks)
setDefaultMeta()
window.metaAttributesCache.set('ol-thirdPartyIds', {})
window.metaAttributesCache.set('ol-user', {
features: { github: true, dropbox: true, mendeley: true, zotero: true },
refProviders: {
mendeley: false,
zotero: false,
useMeta({
'ol-thirdPartyIds': {},
'ol-user': {
features: { github: true, dropbox: true, mendeley: true, zotero: true },
refProviders: {
mendeley: false,
zotero: false,
},
},
'ol-github': { enabled: false },
'ol-dropbox': { registered: false },
})
window.metaAttributesCache.set('ol-github', { enabled: false })
window.metaAttributesCache.set('ol-dropbox', { registered: false })
useEffect(() => {
setDefaultMeta()
}, [])
return (
<UserProvider>
@ -84,4 +93,5 @@ export const SectionProjetSyncSuccess = args => {
export default {
title: 'Account Settings / Linking',
component: LinkingSection,
decorators: [ScopeDecorator],
}

View file

@ -1,62 +1,71 @@
import { useEffect } from 'react'
import ShareProjectModal from '../js/features/share-project-modal/components/share-project-modal'
import useFetchMock from './hooks/use-fetch-mock'
import { withContextRoot } from './utils/with-context-root'
import { useScope } from './hooks/use-scope'
import { ScopeDecorator } from './decorators/scope'
import { contacts } from './fixtures/contacts'
import { project } from './fixtures/project'
export const LinkSharingOff = args => {
useFetchMock(setupFetchMock)
const project = {
...args.project,
publicAccesLevel: 'private',
}
useScope({
project: {
...args.project,
publicAccesLevel: 'private',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project })
return <ShareProjectModal {...args} />
}
export const LinkSharingOn = args => {
useFetchMock(setupFetchMock)
const project = {
...args.project,
publicAccesLevel: 'tokenBased',
}
useScope({
project: {
...args.project,
publicAccesLevel: 'tokenBased',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project })
return <ShareProjectModal {...args} />
}
export const LinkSharingLoading = args => {
useFetchMock(setupFetchMock)
const project = {
...args.project,
publicAccesLevel: 'tokenBased',
tokens: undefined,
}
useScope({
project: {
...args.project,
publicAccesLevel: 'tokenBased',
tokens: undefined,
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project })
return <ShareProjectModal {...args} />
}
export const NonAdminLinkSharingOff = args => {
const project = {
...args.project,
publicAccesLevel: 'private',
}
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
project,
useScope({
project: {
...args.project,
publicAccesLevel: 'private',
},
})
return <ShareProjectModal {...args} isAdmin={false} />
}
export const NonAdminLinkSharingOn = args => {
const project = {
...args.project,
publicAccesLevel: 'tokenBased',
}
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
project,
useScope({
project: {
...args.project,
publicAccesLevel: 'tokenBased',
},
})
return <ShareProjectModal {...args} isAdmin={false} />
}
export const RestrictedTokenMember = args => {
@ -64,103 +73,64 @@ export const RestrictedTokenMember = args => {
// original value on unmount
// Currently this is necessary because the context value is set from window,
// however in the future we should change this to set via props
const originalIsRestrictedTokenMember = window.isRestrictedTokenMember
window.isRestrictedTokenMember = true
useEffect(() => {
const originalIsRestrictedTokenMember = window.isRestrictedTokenMember
window.isRestrictedTokenMember = true
return () => {
window.isRestrictedTokenMember = originalIsRestrictedTokenMember
}
})
const project = {
...args.project,
publicAccesLevel: 'tokenBased',
}
useScope({
project: {
...args.project,
publicAccesLevel: 'tokenBased',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project })
return <ShareProjectModal {...args} />
}
export const LegacyLinkSharingReadAndWrite = args => {
useFetchMock(setupFetchMock)
const project = {
...args.project,
publicAccesLevel: 'readAndWrite',
}
useScope({
project: {
...args.project,
publicAccesLevel: 'readAndWrite',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project })
return <ShareProjectModal {...args} />
}
export const LegacyLinkSharingReadOnly = args => {
useFetchMock(setupFetchMock)
const project = {
...args.project,
publicAccesLevel: 'readOnly',
}
useScope({
project: {
...args.project,
publicAccesLevel: 'readOnly',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project })
return <ShareProjectModal {...args} />
}
export const LimitedCollaborators = args => {
useFetchMock(setupFetchMock)
const project = {
...args.project,
features: {
...args.project.features,
collaborators: 3,
useScope({
project: {
...args.project,
features: {
...args.project.features,
collaborators: 3,
},
},
}
})
return withContextRoot(<ShareProjectModal {...args} />, { project })
}
const project = {
_id: 'a-project',
name: 'A Project',
features: {
collaborators: -1, // unlimited
},
publicAccesLevel: 'private',
tokens: {
readOnly: 'ro-token',
readAndWrite: 'rw-token',
},
owner: {
_id: 'fakeOwnerId',
email: 'stories@overleaf.com',
},
members: [
{
_id: 'viewer-member',
type: 'user',
privileges: 'readOnly',
name: 'Viewer User',
email: 'viewer@example.com',
},
{
_id: 'author-member',
type: 'user',
privileges: 'readAndWrite',
name: 'Author User',
email: 'author@example.com',
},
],
invites: [
{
_id: 'test-invite-1',
privileges: 'readOnly',
name: 'Invited Viewer',
email: 'invited-viewer@example.com',
},
{
_id: 'test-invite-2',
privileges: 'readAndWrite',
name: 'Invited Author',
email: 'invited-author@example.com',
},
],
return <ShareProjectModal {...args} />
}
export default {
@ -176,50 +146,9 @@ export default {
argTypes: {
handleHide: { action: 'hide' },
},
decorators: [ScopeDecorator],
}
const contacts = [
// user with edited name
{
type: 'user',
email: 'test-user@example.com',
first_name: 'Test',
last_name: 'User',
name: 'Test User',
},
// user with default name (email prefix)
{
type: 'user',
email: 'test@example.com',
first_name: 'test',
},
// no last name
{
type: 'user',
first_name: 'Eratosthenes',
email: 'eratosthenes@example.com',
},
// more users
{
type: 'user',
first_name: 'Claudius',
last_name: 'Ptolemy',
email: 'ptolemy@example.com',
},
{
type: 'user',
first_name: 'Abd al-Rahman',
last_name: 'Al-Sufi',
email: 'al-sufi@example.com',
},
{
type: 'user',
first_name: 'Nicolaus',
last_name: 'Copernicus',
email: 'copernicus@example.com',
},
]
function setupFetchMock(fetchMock) {
const delay = 1000

View file

@ -1,38 +0,0 @@
import { ContextRoot } from '../../js/shared/context/root-context'
import _ from 'lodash'
// 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 scopeWatchers = []
const ide = {
...window._ide,
$scope: {
...window._ide.$scope,
...scope,
$watch: (key, callback) => {
scopeWatchers.push([key, callback])
},
$applyAsync: callback => {
window.setTimeout(() => {
callback()
for (const [key, watcher] of scopeWatchers) {
watcher(_.get(ide.$scope, key))
}
}, 0)
},
$on: (eventName, callback) => {
//
},
},
}
return (
<ContextRoot ide={ide} settings={{}}>
{Story}
</ContextRoot>
)
}

View file

@ -1,6 +1,6 @@
import useFetchMock from './hooks/use-fetch-mock'
import { withContextRoot } from './utils/with-context-root'
import WordCountModal from '../js/features/word-count-modal/components/word-count-modal'
import { ScopeDecorator } from './decorators/scope'
const counts = {
headers: 4,
@ -14,11 +14,6 @@ const messages = [
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
].join('\n')
const project = {
_id: 'project-id',
name: 'A Project',
}
export const WordCount = args => {
useFetchMock(fetchMock => {
fetchMock.get(
@ -28,7 +23,7 @@ export const WordCount = args => {
)
})
return withContextRoot(<WordCountModal {...args} />, { project })
return <WordCountModal {...args} />
}
export const WordCountWithMessages = args => {
@ -40,7 +35,7 @@ export const WordCountWithMessages = args => {
)
})
return withContextRoot(<WordCountModal {...args} />, { project })
return <WordCountModal {...args} />
}
export const ErrorResponse = args => {
@ -52,7 +47,7 @@ export const ErrorResponse = args => {
)
})
return withContextRoot(<WordCountModal {...args} />, { project })
return <WordCountModal {...args} />
}
export default {
@ -61,4 +56,8 @@ export default {
args: {
show: true,
},
argTypes: {
handleHide: { action: 'close modal' },
},
decorators: [ScopeDecorator],
}

View file

@ -18,6 +18,8 @@
"modules/**/frontend/js/**/*.*",
"test/frontend/**/*.*",
"modules/**/test/frontend/**/*.*",
"frontend/stories/**/*.*",
"modules/**/stories/**/*.*",
"cypress",
"types"
]

View file

@ -0,0 +1,7 @@
export type Folder = {
_id: string
name: string
docs: []
folders: []
fileRefs: []
}

View file

@ -0,0 +1,30 @@
import { MongoUser } from './user'
import { Folder } from './folder'
type ProjectMember = {
_id: string
type: 'user'
privileges: 'readOnly' | 'readAndWrite'
name: string
email: string
}
type ProjectInvite = {
_id: string
privileges: 'readOnly' | 'readAndWrite'
name: string
email: string
}
export type Project = {
_id: string
name: string
features: Record<string, unknown>
publicAccesLevel?: string
tokens: Record<string, unknown>
owner: MongoUser
members: ProjectMember[]
invites: ProjectInvite[]
rootDocId?: string
rootFolder?: Folder[]
}

View file

@ -0,0 +1,8 @@
export type User = {
id: string
email: string
allowedFreeTrial?: boolean
features?: Record<string, boolean>
}
export type MongoUser = Pick<User, Exclude<keyof User, 'id'>> & { _id: string }

View file

@ -15,5 +15,8 @@ declare global {
currentLangCode: string
}
ExposedSettings: ExposedSettings
project_id: string
gitBridgePublicBaseUrl: string
_ide: Record<string, unknown>
}
}