mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Standardise scope/context usage in Storybook stories (#7842)
GitOrigin-RevId: 109a4357fc3b083ffbd3af5b8c98acf0f655f297
This commit is contained in:
parent
cb065e7f12
commit
d91ee50762
35 changed files with 643 additions and 537 deletions
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 />
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
|
|
193
services/web/frontend/stories/decorators/scope.tsx
Normal file
193
services/web/frontend/stories/decorators/scope.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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 />
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
|
|
34
services/web/frontend/stories/fixtures/chat-messages.js
Normal file
34
services/web/frontend/stories/fixtures/chat-messages.js
Normal 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
|
||||
}
|
|
@ -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 =>
|
||||
|
|
41
services/web/frontend/stories/fixtures/contacts.js
Normal file
41
services/web/frontend/stories/fixtures/contacts.js
Normal 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',
|
||||
},
|
||||
]
|
|
@ -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)
|
||||
}
|
48
services/web/frontend/stories/fixtures/project.ts
Normal file
48
services/web/frontend/stories/fixtures/project.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
}
|
|
@ -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],
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
16
services/web/frontend/stories/hooks/use-fetch-mock.tsx
Normal file
16
services/web/frontend/stories/hooks/use-fetch-mock.tsx
Normal 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])
|
||||
}
|
8
services/web/frontend/stories/hooks/use-meta.tsx
Normal file
8
services/web/frontend/stories/hooks/use-meta.tsx
Normal 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)
|
||||
}
|
||||
}
|
16
services/web/frontend/stories/hooks/use-scope.tsx
Normal file
16
services/web/frontend/stories/hooks/use-scope.tsx
Normal 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)
|
||||
}, [])
|
||||
}
|
|
@ -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'
|
||||
(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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,14 +77,9 @@ ErrorImportingFileFromReferenceProvider.decorators = [
|
|||
export const FileLimitReached = args => {
|
||||
useFetchMock(mockCreateFileModalFetch)
|
||||
|
||||
return <FileTreeModalCreateFile {...args} />
|
||||
}
|
||||
FileLimitReached.decorators = [
|
||||
createFileModalDecorator(
|
||||
{},
|
||||
{
|
||||
rootFolder: [
|
||||
{
|
||||
useScope({
|
||||
project: {
|
||||
rootFolder: {
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: Array.from({ length: 10 }, (_, index) => ({
|
||||
|
@ -91,12 +88,15 @@ FileLimitReached.decorators = [
|
|||
fileRefs: [],
|
||||
folders: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
return <FileTreeModalCreateFile {...args} />
|
||||
}
|
||||
FileLimitReached.decorators = [createFileModalDecorator()]
|
||||
|
||||
export default {
|
||||
title: 'Editor / Modals / Create File',
|
||||
component: FileTreeModalCreateFile,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
return (
|
||||
<div>
|
||||
<div className="pdf-viewer">
|
||||
<Suspense fallback={null}>
|
||||
<PdfViewer />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div style={{ position: 'absolute', top: 150, left: 50 }}>
|
||||
<PdfSynctexControls />
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>,
|
||||
scope
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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', {
|
||||
|
||||
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],
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'private',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LinkSharingOn = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
const project = {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LinkSharingLoading = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
const project = {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
tokens: undefined,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const NonAdminLinkSharingOff = args => {
|
||||
const project = {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'private',
|
||||
}
|
||||
|
||||
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
|
||||
project,
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} isAdmin={false} />
|
||||
}
|
||||
|
||||
export const NonAdminLinkSharingOn = args => {
|
||||
const project = {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
}
|
||||
|
||||
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
|
||||
project,
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
const originalIsRestrictedTokenMember = window.isRestrictedTokenMember
|
||||
window.isRestrictedTokenMember = true
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
window.isRestrictedTokenMember = originalIsRestrictedTokenMember
|
||||
}
|
||||
})
|
||||
|
||||
const project = {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LegacyLinkSharingReadAndWrite = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
const project = {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'readAndWrite',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LegacyLinkSharingReadOnly = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
const project = {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'readOnly',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return withContextRoot(<ShareProjectModal {...args} />, { project })
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LimitedCollaborators = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
const project = {
|
||||
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
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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],
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
"modules/**/frontend/js/**/*.*",
|
||||
"test/frontend/**/*.*",
|
||||
"modules/**/test/frontend/**/*.*",
|
||||
"frontend/stories/**/*.*",
|
||||
"modules/**/stories/**/*.*",
|
||||
"cypress",
|
||||
"types"
|
||||
]
|
||||
|
|
7
services/web/types/folder.ts
Normal file
7
services/web/types/folder.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type Folder = {
|
||||
_id: string
|
||||
name: string
|
||||
docs: []
|
||||
folders: []
|
||||
fileRefs: []
|
||||
}
|
30
services/web/types/project.ts
Normal file
30
services/web/types/project.ts
Normal 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[]
|
||||
}
|
8
services/web/types/user.ts
Normal file
8
services/web/types/user.ts
Normal 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 }
|
|
@ -15,5 +15,8 @@ declare global {
|
|||
currentLangCode: string
|
||||
}
|
||||
ExposedSettings: ExposedSettings
|
||||
project_id: string
|
||||
gitBridgePublicBaseUrl: string
|
||||
_ide: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue