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 imageExtensions = ['png', 'jpg', 'jpeg', 'gif']
const textExtensions = window.ExposedSettings.textExtensions
export default function FileView({ file, storeReferencesKeys }) { export default function FileView({ file, storeReferencesKeys }) {
const [contentLoading, setContentLoading] = useState(true) const [contentLoading, setContentLoading] = useState(true)
const [hasError, setHasError] = useState(false) const [hasError, setHasError] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
const { textExtensions } = window.ExposedSettings
const extension = file.name.split('.').pop().toLowerCase() const extension = file.name.split('.').pop().toLowerCase()
const isUnpreviewableFile = const isUnpreviewableFile =
!imageExtensions.includes(extension) && !textExtensions.includes(extension) !imageExtensions.includes(extension) && !textExtensions.includes(extension)

View file

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

View file

@ -1,44 +1,8 @@
import { v4 as uuid } from 'uuid' import { useEffect } from 'react'
import { ContextRoot } from '../js/shared/context/root-context'
import ChatPane from '../js/features/chat/components/chat-pane' 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' import useFetchMock from './hooks/use-fetch-mock'
import { generateMessages } from './fixtures/chat-messages'
const ONE_MINUTE = 60 * 1000 import { ScopeDecorator } from './decorators/scope'
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()
export const Conversation = args => { export const Conversation = args => {
useFetchMock(fetchMock => { useFetchMock(fetchMock => {
@ -66,6 +30,14 @@ export const Loading = args => {
return <ChatPane {...args} /> return <ChatPane {...args} />
} }
export const LoadingError = args => {
useFetchMock(fetchMock => {
fetchMock.get(/messages/, 500)
})
return <ChatPane {...args} />
}
export default { export default {
title: 'Editor / Chat', title: 'Editor / Chat',
component: ChatPane, component: ChatPane,
@ -76,13 +48,22 @@ export default {
resetUnreadMessages: () => {}, resetUnreadMessages: () => {},
}, },
decorators: [ decorators: [
Story => ( ScopeDecorator,
<> Story => {
<style>{'html, body, .chat { height: 100%; width: 100%; }'}</style> useEffect(() => {
<ContextRoot ide={window._ide} settings={{}}> window.MathJax = {
<Story /> Hub: {
</ContextRoot> Queue: () => {},
</> config: { tex2jax: { inlineMath: [['$', '$']] } },
), },
}
return () => {
delete window.MathJax
}
}, [])
return <Story />
},
], ],
} }

View file

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

View file

@ -1,25 +1,26 @@
import { useState } from 'react' import { useState } from 'react'
import useFetchMock from './hooks/use-fetch-mock' import useFetchMock from './hooks/use-fetch-mock'
import ContactUsModal from '../../modules/support/frontend/js/components/contact-us-modal' 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 = () => { export const Generic = () => {
const [show, setShow] = useState(true) const [show, setShow] = useState(true)
const handleHide = () => setShow(false) const handleHide = () => setShow(false)
useFetchMock(fetchMock => { 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 => { export const RequestError = args => {
useFetchMock(fetchMock => { 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 { export default {
@ -32,4 +33,5 @@ export default {
argTypes: { argTypes: {
handleHide: { action: 'close modal' }, 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 EditorSwitch from '../js/features/source-editor/components/editor-switch'
import { withContextRoot } from './utils/with-context-root' import { ScopeDecorator } from './decorators/scope'
export default { export default {
title: 'Editor / Switch', title: 'Editor / Switch',
component: EditorSwitch, component: EditorSwitch,
decorators: [ScopeDecorator],
} }
export const Switcher = () => { export const Switcher = () => {
return withContextRoot(<EditorSwitch />, { return <EditorSwitch />
editor: {
richText: false,
newSourceEditor: false,
},
})
} }

View file

@ -1,11 +1,12 @@
import MockedSocket from 'socket.io-mock' import MockedSocket from 'socket.io-mock'
import { withContextRoot } from './utils/with-context-root'
import { rootFolderBase } from './fixtures/file-tree-base' import { rootFolderBase } from './fixtures/file-tree-base'
import { rootFolderLimit } from './fixtures/file-tree-limit' import { rootFolderLimit } from './fixtures/file-tree-limit'
import FileTreeRoot from '../js/features/file-tree/components/file-tree-root' import FileTreeRoot from '../js/features/file-tree/components/file-tree-root'
import FileTreeError from '../js/features/file-tree/components/file-tree-error' import FileTreeError from '../js/features/file-tree/components/file-tree-error'
import useFetchMock from './hooks/use-fetch-mock' import useFetchMock from './hooks/use-fetch-mock'
import { ScopeDecorator } from './decorators/scope'
import { useScope } from './hooks/use-scope'
const MOCK_DELAY = 2000 const MOCK_DELAY = 2000
@ -87,24 +88,30 @@ function defaultSetupMocks(fetchMock) {
export const FullTree = args => { export const FullTree = args => {
useFetchMock(defaultSetupMocks) useFetchMock(defaultSetupMocks)
return withContextRoot(<FileTreeRoot {...args} />, { useScope({
project: DEFAULT_PROJECT, project: DEFAULT_PROJECT,
permissionsLevel: 'owner', permissionsLevel: 'owner',
}) })
return <FileTreeRoot {...args} />
} }
export const ReadOnly = args => { export const ReadOnly = args => {
return withContextRoot(<FileTreeRoot {...args} />, { useScope({
project: DEFAULT_PROJECT, project: DEFAULT_PROJECT,
permissionsLevel: 'readOnly', permissionsLevel: 'readOnly',
}) })
return <FileTreeRoot {...args} />
} }
export const Disconnected = args => { export const Disconnected = args => {
return withContextRoot(<FileTreeRoot {...args} />, { useScope({
project: DEFAULT_PROJECT, project: DEFAULT_PROJECT,
permissionsLevel: 'owner', permissionsLevel: 'owner',
}) })
return <FileTreeRoot {...args} />
} }
Disconnected.args = { isConnected: false } Disconnected.args = { isConnected: false }
@ -125,25 +132,34 @@ export const NetworkErrors = args => {
}) })
}) })
return withContextRoot(<FileTreeRoot {...args} />, { useScope({
project: DEFAULT_PROJECT, project: DEFAULT_PROJECT,
permissionsLevel: 'owner', permissionsLevel: 'owner',
}) })
return <FileTreeRoot {...args} />
} }
export const FallbackError = args => { export const FallbackError = args => {
return withContextRoot(<FileTreeError {...args} />, { useScope({
project: DEFAULT_PROJECT, project: DEFAULT_PROJECT,
}) })
return <FileTreeError {...args} />
} }
export const FilesLimit = args => { export const FilesLimit = args => {
useFetchMock(defaultSetupMocks) useFetchMock(defaultSetupMocks)
return withContextRoot(<FileTreeRoot {...args} />, { useScope({
project: { ...DEFAULT_PROJECT, rootFolder: rootFolderLimit }, project: {
...DEFAULT_PROJECT,
rootFolder: rootFolderLimit,
},
permissionsLevel: 'owner', permissionsLevel: 'owner',
}) })
return <FileTreeRoot {...args} />
} }
export default { export default {
@ -167,6 +183,7 @@ export default {
onSelect: { action: 'onSelect' }, onSelect: { action: 'onSelect' },
}, },
decorators: [ decorators: [
ScopeDecorator,
Story => ( Story => (
<> <>
<style>{'html, body, .file-tree { height: 100%; width: 100%; }'}</style> <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 FileView from '../js/features/file-view/components/file-view'
import useFetchMock from './hooks/use-fetch-mock' import useFetchMock from './hooks/use-fetch-mock'
import { ScopeDecorator } from './decorators/scope'
const bodies = { const bodies = {
latex: `\\documentclass{article} latex: `\\documentclass{article}
@ -252,11 +252,5 @@ export default {
argTypes: { argTypes: {
storeReferencesKeys: { action: 'store references keys' }, storeReferencesKeys: { action: 'store references keys' },
}, },
decorators: [ decorators: [ScopeDecorator],
Story => (
<ContextRoot ide={window._ide} settings={{}}>
<Story />
</ContextRoot>
),
],
} }

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), outputFiles: cloneDeep(outputFiles),
}, },
}, },
{ delay } { delay, overwriteRoutes: true }
) )
export const mockCompileError = (fetchMock, status = 'success', delay = 1000) => export const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
@ -97,6 +97,7 @@ export const mockCompileValidationIssues = (
export const mockClearCache = fetchMock => export const mockClearCache = fetchMock =>
fetchMock.delete('express:/project/:projectId/output', 204, { fetchMock.delete('express:/project/:projectId/output', 204, {
delay: 1000, delay: 1000,
overwriteRoutes: true,
}) })
export const mockBuildFile = fetchMock => export const mockBuildFile = fetchMock =>
@ -156,7 +157,7 @@ LaTeX Font Info: External font \`cmex10' loaded for size
return 404 return 404
} }
}, },
{ sendAsJson: false } { sendAsJson: false, overwriteRoutes: true }
) )
const mockHighlights = [ const mockHighlights = [
@ -215,7 +216,7 @@ export const mockValidPdf = fetchMock =>
xhr.send() xhr.send()
}) })
}, },
{ sendAsJson: false } { sendAsJson: false, overwriteRoutes: true }
) )
export const mockSynctex = fetchMock => 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 importOverleafModules from '../macros/import-overleaf-module.macro'
import useFetchMock from './hooks/use-fetch-mock' import { ScopeDecorator } from './decorators/scope'
const [ const [
{ {
@ -19,7 +18,10 @@ CollaboratorModal.args = {
} }
export const TeaserModal = 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} /> return <GitBridgeModal {...args} />
} }
@ -36,13 +38,5 @@ export default {
argTypes: { argTypes: {
handleHide: { action: 'handleHide' }, handleHide: { action: 'handleHide' },
}, },
decorators: [ decorators: [ScopeDecorator],
Story => (
<>
<ContextRoot ide={window._ide} settings={{}}>
<Story />
</ContextRoot>
</>
),
],
} }

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 { useEffect } from 'react'
import { withContextRoot } from './../../utils/with-context-root'
import FileTreeContext from '../../../js/features/file-tree/components/file-tree-context' import FileTreeContext from '../../../js/features/file-tree/components/file-tree-context'
import FileTreeCreateNameProvider from '../../../js/features/file-tree/contexts/file-tree-create-name' 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 FileTreeCreateFormProvider from '../../../js/features/file-tree/contexts/file-tree-create-form'
import { useFileTreeActionable } from '../../../js/features/file-tree/contexts/file-tree-actionable' import { useFileTreeActionable } from '../../../js/features/file-tree/contexts/file-tree-actionable'
import PropTypes from 'prop-types' 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 = { const defaultFileTreeContextProps = {
refProviders: { mendeley: false, zotero: false }, refProviders: { mendeley: false, zotero: false },
reindexReferences: () => { 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) => { .post('express:/project/:projectId/doc', (path, req) => {
console.log({ path, req }) console.log({ path, req })
return 204 return 204
@ -114,14 +84,10 @@ export const mockCreateFileModalFetch = fetchMock =>
}) })
export const createFileModalDecorator = export const createFileModalDecorator =
( (fileTreeContextProps = {}, createMode = 'doc') =>
fileTreeContextProps = {}, // eslint-disable-next-line react/display-name
projectProps = {},
createMode = 'doc'
// eslint-disable-next-line react/display-name
) =>
Story => { Story => {
return withContextRoot( return (
<FileTreeContext <FileTreeContext
{...defaultFileTreeContextProps} {...defaultFileTreeContextProps}
{...fileTreeContextProps} {...fileTreeContextProps}
@ -133,11 +99,7 @@ export const createFileModalDecorator =
</OpenCreateFileModal> </OpenCreateFileModal>
</FileTreeCreateFormProvider> </FileTreeCreateFormProvider>
</FileTreeCreateNameProvider> </FileTreeCreateNameProvider>
</FileTreeContext>, </FileTreeContext>
{
project: { ...DEFAULT_PROJECT, ...projectProps },
permissionsLevel: 'owner',
}
) )
} }

View file

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

View file

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

View file

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

View file

@ -1,19 +1,20 @@
import ErrorBoundaryFallback from '../js/features/pdf-preview/components/error-boundary-fallback' 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 { export default {
title: 'Editor / PDF Preview / Error Boundary', title: 'Editor / PDF Preview / Error Boundary',
component: ErrorBoundaryFallback, component: ErrorBoundaryFallback,
decorators: [ScopeDecorator],
} }
export const PreviewErrorBoundary = () => { export const PreviewErrorBoundary = () => {
return withContextRoot(<ErrorBoundaryFallback type="preview" />) return <ErrorBoundaryFallback type="preview" />
} }
export const PdfErrorBoundary = () => { export const PdfErrorBoundary = () => {
return withContextRoot(<ErrorBoundaryFallback type="pdf" />) return <ErrorBoundaryFallback type="pdf" />
} }
export const LogsErrorBoundary = () => { 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 { useCallback, useMemo, useState } from 'react'
import useFetchMock from './hooks/use-fetch-mock' import useFetchMock from './hooks/use-fetch-mock'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
@ -21,6 +20,7 @@ import {
outputFiles, outputFiles,
} from './fixtures/compile' } from './fixtures/compile'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { ScopeDecorator } from './decorators/scope'
export default { export default {
title: 'Editor / PDF Preview', title: 'Editor / PDF Preview',
@ -30,34 +30,7 @@ export default {
PdfFileList, PdfFileList,
PdfPreviewError, PdfPreviewError,
}, },
} decorators: [ScopeDecorator],
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',
},
},
} }
export const Interactive = () => { export const Interactive = () => {
@ -205,12 +178,11 @@ export const Interactive = () => {
) )
} }
return withContextRoot( return (
<div className="pdf-viewer"> <div className="pdf-viewer">
<PdfPreviewPane /> <PdfPreviewPane />
<Inner /> <Inner />
</div>, </div>
scope
) )
} }
@ -276,12 +248,11 @@ export const CompileError = () => {
) )
} }
return withContextRoot( return (
<> <>
<PdfPreviewPane /> <PdfPreviewPane />
<Inner /> <Inner />
</>, </>
scope
) )
} }
@ -309,7 +280,7 @@ export const DisplayError = () => {
mockCompile(fetchMock) mockCompile(fetchMock)
}) })
return withContextRoot( return (
<> <>
{compileErrors.map(error => ( {compileErrors.map(error => (
<div <div
@ -320,8 +291,7 @@ export const DisplayError = () => {
<PdfPreviewError error={error} /> <PdfPreviewError error={error} />
</div> </div>
))} ))}
</>, </>
scope
) )
} }
@ -332,11 +302,10 @@ export const HybridToolbar = () => {
mockEventTracking(fetchMock) mockEventTracking(fetchMock)
}) })
return withContextRoot( return (
<div className="pdf"> <div className="pdf">
<PdfPreviewHybridToolbar /> <PdfPreviewHybridToolbar />
</div>, </div>
scope
) )
} }
@ -361,11 +330,10 @@ export const Logs = () => {
mockClearCache(fetchMock) mockClearCache(fetchMock)
}) })
return withContextRoot( return (
<div className="pdf"> <div className="pdf">
<PdfLogsViewer /> <PdfLogsViewer />
</div>, </div>
scope
) )
} }
@ -393,5 +361,5 @@ export const ValidationIssues = () => {
mockBuildFile(fetchMock) 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 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 PdfSynctexControls from '../js/features/pdf-preview/components/pdf-synctex-controls'
import PdfViewer from '../js/features/pdf-preview/components/pdf-viewer' import PdfViewer from '../js/features/pdf-preview/components/pdf-viewer'
import { import {
@ -9,24 +7,13 @@ import {
mockSynctex, mockSynctex,
mockValidPdf, mockValidPdf,
} from './fixtures/compile' } from './fixtures/compile'
import { useEffect, Suspense } from 'react'
import { ScopeDecorator } from './decorators/scope'
export default { export default {
title: 'Editor / PDF Viewer', title: 'Editor / PDF Viewer',
component: PdfViewer, component: PdfViewer,
} decorators: [ScopeDecorator],
const project = {
_id: 'story-project',
}
const scope = {
project,
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
} }
export const Interactive = () => { export const Interactive = () => {
@ -45,17 +32,16 @@ export const Interactive = () => {
) )
}, []) }, [])
return withContextRoot( return (
<Suspense fallback="Loading"> <div>
<div> <div className="pdf-viewer">
<div className="pdf-viewer"> <Suspense fallback={null}>
<PdfViewer /> <PdfViewer />
</div> </Suspense>
<div style={{ position: 'absolute', top: 150, left: 50 }}>
<PdfSynctexControls />
</div>
</div> </div>
</Suspense>, <div style={{ position: 'absolute', top: 150, left: 50 }}>
scope <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 { setDefaultMeta, defaultSetupMocks } from './helpers/linking'
import { UserProvider } from '../../js/shared/context/user-context' import { UserProvider } from '../../js/shared/context/user-context'
import { SSOProvider } from '../../js/features/settings/context/sso-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 const MOCK_DELAY = 1000
@ -21,17 +24,23 @@ export const Section = args => {
export const SectionAllUnlinked = args => { export const SectionAllUnlinked = args => {
useFetchMock(defaultSetupMocks) useFetchMock(defaultSetupMocks)
setDefaultMeta()
window.metaAttributesCache.set('ol-thirdPartyIds', {}) useMeta({
window.metaAttributesCache.set('ol-user', { 'ol-thirdPartyIds': {},
features: { github: true, dropbox: true, mendeley: true, zotero: true }, 'ol-user': {
refProviders: { features: { github: true, dropbox: true, mendeley: true, zotero: true },
mendeley: false, refProviders: {
zotero: false, 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 ( return (
<UserProvider> <UserProvider>
@ -84,4 +93,5 @@ export const SectionProjetSyncSuccess = args => {
export default { export default {
title: 'Account Settings / Linking', title: 'Account Settings / Linking',
component: LinkingSection, component: LinkingSection,
decorators: [ScopeDecorator],
} }

View file

@ -1,62 +1,71 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import ShareProjectModal from '../js/features/share-project-modal/components/share-project-modal' import ShareProjectModal from '../js/features/share-project-modal/components/share-project-modal'
import useFetchMock from './hooks/use-fetch-mock' 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 => { export const LinkSharingOff = args => {
useFetchMock(setupFetchMock) useFetchMock(setupFetchMock)
const project = { useScope({
...args.project, project: {
publicAccesLevel: 'private', ...args.project,
} publicAccesLevel: 'private',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project }) return <ShareProjectModal {...args} />
} }
export const LinkSharingOn = args => { export const LinkSharingOn = args => {
useFetchMock(setupFetchMock) useFetchMock(setupFetchMock)
const project = { useScope({
...args.project, project: {
publicAccesLevel: 'tokenBased', ...args.project,
} publicAccesLevel: 'tokenBased',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project }) return <ShareProjectModal {...args} />
} }
export const LinkSharingLoading = args => { export const LinkSharingLoading = args => {
useFetchMock(setupFetchMock) useFetchMock(setupFetchMock)
const project = { useScope({
...args.project, project: {
publicAccesLevel: 'tokenBased', ...args.project,
tokens: undefined, publicAccesLevel: 'tokenBased',
} tokens: undefined,
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project }) return <ShareProjectModal {...args} />
} }
export const NonAdminLinkSharingOff = args => { export const NonAdminLinkSharingOff = args => {
const project = { useScope({
...args.project, project: {
publicAccesLevel: 'private', ...args.project,
} publicAccesLevel: 'private',
},
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
project,
}) })
return <ShareProjectModal {...args} isAdmin={false} />
} }
export const NonAdminLinkSharingOn = args => { export const NonAdminLinkSharingOn = args => {
const project = { useScope({
...args.project, project: {
publicAccesLevel: 'tokenBased', ...args.project,
} publicAccesLevel: 'tokenBased',
},
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
project,
}) })
return <ShareProjectModal {...args} isAdmin={false} />
} }
export const RestrictedTokenMember = args => { export const RestrictedTokenMember = args => {
@ -64,103 +73,64 @@ export const RestrictedTokenMember = args => {
// original value on unmount // original value on unmount
// Currently this is necessary because the context value is set from window, // Currently this is necessary because the context value is set from window,
// however in the future we should change this to set via props // however in the future we should change this to set via props
const originalIsRestrictedTokenMember = window.isRestrictedTokenMember
window.isRestrictedTokenMember = true
useEffect(() => { useEffect(() => {
const originalIsRestrictedTokenMember = window.isRestrictedTokenMember
window.isRestrictedTokenMember = true
return () => { return () => {
window.isRestrictedTokenMember = originalIsRestrictedTokenMember window.isRestrictedTokenMember = originalIsRestrictedTokenMember
} }
}) })
const project = { useScope({
...args.project, project: {
publicAccesLevel: 'tokenBased', ...args.project,
} publicAccesLevel: 'tokenBased',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project }) return <ShareProjectModal {...args} />
} }
export const LegacyLinkSharingReadAndWrite = args => { export const LegacyLinkSharingReadAndWrite = args => {
useFetchMock(setupFetchMock) useFetchMock(setupFetchMock)
const project = { useScope({
...args.project, project: {
publicAccesLevel: 'readAndWrite', ...args.project,
} publicAccesLevel: 'readAndWrite',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project }) return <ShareProjectModal {...args} />
} }
export const LegacyLinkSharingReadOnly = args => { export const LegacyLinkSharingReadOnly = args => {
useFetchMock(setupFetchMock) useFetchMock(setupFetchMock)
const project = { useScope({
...args.project, project: {
publicAccesLevel: 'readOnly', ...args.project,
} publicAccesLevel: 'readOnly',
},
})
return withContextRoot(<ShareProjectModal {...args} />, { project }) return <ShareProjectModal {...args} />
} }
export const LimitedCollaborators = args => { export const LimitedCollaborators = args => {
useFetchMock(setupFetchMock) useFetchMock(setupFetchMock)
const project = { useScope({
...args.project, project: {
features: { ...args.project,
...args.project.features, features: {
collaborators: 3, ...args.project.features,
collaborators: 3,
},
}, },
} })
return withContextRoot(<ShareProjectModal {...args} />, { project }) return <ShareProjectModal {...args} />
}
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',
},
],
} }
export default { export default {
@ -176,50 +146,9 @@ export default {
argTypes: { argTypes: {
handleHide: { action: 'hide' }, 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) { function setupFetchMock(fetchMock) {
const delay = 1000 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 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 WordCountModal from '../js/features/word-count-modal/components/word-count-modal'
import { ScopeDecorator } from './decorators/scope'
const counts = { const counts = {
headers: 4, 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.', 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
].join('\n') ].join('\n')
const project = {
_id: 'project-id',
name: 'A Project',
}
export const WordCount = args => { export const WordCount = args => {
useFetchMock(fetchMock => { useFetchMock(fetchMock => {
fetchMock.get( fetchMock.get(
@ -28,7 +23,7 @@ export const WordCount = args => {
) )
}) })
return withContextRoot(<WordCountModal {...args} />, { project }) return <WordCountModal {...args} />
} }
export const WordCountWithMessages = 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 => { export const ErrorResponse = args => {
@ -52,7 +47,7 @@ export const ErrorResponse = args => {
) )
}) })
return withContextRoot(<WordCountModal {...args} />, { project }) return <WordCountModal {...args} />
} }
export default { export default {
@ -61,4 +56,8 @@ export default {
args: { args: {
show: true, show: true,
}, },
argTypes: {
handleHide: { action: 'close modal' },
},
decorators: [ScopeDecorator],
} }

View file

@ -18,6 +18,8 @@
"modules/**/frontend/js/**/*.*", "modules/**/frontend/js/**/*.*",
"test/frontend/**/*.*", "test/frontend/**/*.*",
"modules/**/test/frontend/**/*.*", "modules/**/test/frontend/**/*.*",
"frontend/stories/**/*.*",
"modules/**/stories/**/*.*",
"cypress", "cypress",
"types" "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 currentLangCode: string
} }
ExposedSettings: ExposedSettings ExposedSettings: ExposedSettings
project_id: string
gitBridgePublicBaseUrl: string
_ide: Record<string, unknown>
} }
} }