first commit
This commit is contained in:
58
services/web/frontend/stories/chat.stories.jsx
Normal file
58
services/web/frontend/stories/chat.stories.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import ChatPane from '../js/features/chat/components/chat-pane'
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import { generateMessages } from './fixtures/chat-messages'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
import { UserProvider } from '@/shared/context/user-context'
|
||||
|
||||
export const Conversation = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.get(/messages/, generateMessages(35)).post(/messages/, {})
|
||||
})
|
||||
|
||||
return <ChatPane {...args} />
|
||||
}
|
||||
|
||||
export const NoMessages = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.get(/messages/, [])
|
||||
})
|
||||
|
||||
return <ChatPane {...args} />
|
||||
}
|
||||
|
||||
export const Loading = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.get(/messages/, generateMessages(6), {
|
||||
delay: 1000 * 10,
|
||||
})
|
||||
})
|
||||
|
||||
return <ChatPane {...args} />
|
||||
}
|
||||
|
||||
export const LoadingError = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.get(/messages/, 500)
|
||||
})
|
||||
|
||||
return <ChatPane {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / Chat',
|
||||
component: ChatPane,
|
||||
argTypes: {
|
||||
resetUnreadMessages: { action: 'resetUnreadMessages' },
|
||||
},
|
||||
args: {
|
||||
resetUnreadMessages: () => {},
|
||||
},
|
||||
decorators: [
|
||||
ScopeDecorator,
|
||||
Story => (
|
||||
<UserProvider>
|
||||
<Story />
|
||||
</UserProvider>
|
||||
),
|
||||
],
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import CloneProjectModal from '../js/features/clone-project-modal/components/clone-project-modal'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
|
||||
export const Success = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/clone',
|
||||
{ status: 200 },
|
||||
{ delay: 250 }
|
||||
)
|
||||
})
|
||||
|
||||
return <CloneProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const GenericErrorResponse = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/clone',
|
||||
{ status: 500 },
|
||||
{ delay: 250 }
|
||||
)
|
||||
})
|
||||
|
||||
return <CloneProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const SpecificErrorResponse = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/clone',
|
||||
{ status: 400, body: 'The project name is not valid' },
|
||||
{ delay: 250 }
|
||||
)
|
||||
})
|
||||
|
||||
return <CloneProjectModal {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / Modals / Clone Project',
|
||||
component: CloneProjectModal,
|
||||
args: {
|
||||
show: true,
|
||||
projectName: 'Project 1',
|
||||
projectTags: [
|
||||
{
|
||||
_id: 'tag-1',
|
||||
name: 'Category 1',
|
||||
color: '#c0ffee',
|
||||
},
|
||||
],
|
||||
},
|
||||
argTypes: {
|
||||
handleHide: { action: 'close modal' },
|
||||
openProject: { action: 'open project' },
|
||||
handleAfterCloned: { action: 'after cloned' },
|
||||
},
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
88
services/web/frontend/stories/contact-us-modal.stories.tsx
Normal file
88
services/web/frontend/stories/contact-us-modal.stories.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ComponentProps, useCallback, useState } from 'react'
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import ContactUsModal from '../../modules/support/frontend/js/components/contact-us-modal'
|
||||
import fixedHelpSuggestionSearch from '../../modules/support/test/frontend/util/fixed-help-suggestion-search'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
type ContactUsModalProps = ComponentProps<typeof ContactUsModal>
|
||||
|
||||
function GenericContactUsModal(args: ContactUsModalProps) {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post('/support', { status: 200 }, { delay: 1000 })
|
||||
})
|
||||
|
||||
return (
|
||||
<ContactUsModal
|
||||
helpSuggestionSearch={fixedHelpSuggestionSearch}
|
||||
{...args}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const Generic = (args: ContactUsModalProps) => (
|
||||
<GenericContactUsModal {...args} />
|
||||
)
|
||||
|
||||
const ContactUsModalWithRequestError = (args: ContactUsModalProps) => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post('/support', { status: 404 }, { delay: 250 })
|
||||
})
|
||||
|
||||
return (
|
||||
<ContactUsModal
|
||||
helpSuggestionSearch={fixedHelpSuggestionSearch}
|
||||
{...args}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const RequestError = (args: ContactUsModalProps) => (
|
||||
<ContactUsModalWithRequestError {...args} />
|
||||
)
|
||||
|
||||
const ContactUsModalWithAcknowledgement = (
|
||||
args: Omit<ContactUsModalProps, 'show' | 'handleHide'>
|
||||
) => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post('/support', { status: 200 }, { delay: 1000 })
|
||||
})
|
||||
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
const hideModal = useCallback((event?: Event) => {
|
||||
event?.preventDefault()
|
||||
setShow(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLButton onClick={() => setShow(true)}>Contact Us</OLButton>
|
||||
<ContactUsModal
|
||||
show={show}
|
||||
handleHide={hideModal}
|
||||
helpSuggestionSearch={fixedHelpSuggestionSearch}
|
||||
{...args}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithAcknowledgement = (args: ContactUsModalProps) => {
|
||||
const { show, handleHide, ...rest } = args
|
||||
return <ContactUsModalWithAcknowledgement {...rest} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Modals / Contact Us',
|
||||
component: ContactUsModal,
|
||||
args: {
|
||||
show: true,
|
||||
handleHide: () => {},
|
||||
autofillProjectUrl: true,
|
||||
},
|
||||
argTypes: {
|
||||
handleHide: { action: 'close modal' },
|
||||
},
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
247
services/web/frontend/stories/decorators/scope.tsx
Normal file
247
services/web/frontend/stories/decorators/scope.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { get } from 'lodash'
|
||||
import { User, UserId } from '../../../types/user'
|
||||
import { Project } from '../../../types/project'
|
||||
import {
|
||||
mockBuildFile,
|
||||
mockCompile,
|
||||
mockCompileError,
|
||||
} from '../fixtures/compile'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import { useMeta } from '../hooks/use-meta'
|
||||
import SocketIOShim, { SocketIOMock } from '@/ide/connection/SocketIoShim'
|
||||
import { IdeContext } from '@/shared/context/ide-context'
|
||||
import {
|
||||
IdeReactContext,
|
||||
createReactScopeValueStore,
|
||||
} from '@/features/ide-react/context/ide-react-context'
|
||||
import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
|
||||
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
|
||||
import { ConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { Socket } from '@/features/ide-react/connection/types/socket'
|
||||
import { ConnectionState } from '@/features/ide-react/connection/types/connection-state'
|
||||
import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
|
||||
|
||||
const scopeWatchers: [string, (value: any) => void][] = []
|
||||
|
||||
const initialize = () => {
|
||||
const user: User = {
|
||||
id: 'story-user' as UserId,
|
||||
email: 'story-user@example.com',
|
||||
allowedFreeTrial: true,
|
||||
features: { dropbox: true, symbolPalette: true },
|
||||
}
|
||||
|
||||
const project: Project = {
|
||||
_id: '63e21c07946dd8c76505f85a',
|
||||
name: 'A Project',
|
||||
features: { mendeley: true, zotero: true, referencesSearch: true },
|
||||
tokens: {},
|
||||
owner: {
|
||||
_id: 'a-user',
|
||||
email: 'stories@overleaf.com',
|
||||
},
|
||||
members: [],
|
||||
invites: [],
|
||||
rootDoc_id: '5e74f1a7ce17ae0041dfd056',
|
||||
rootFolder: [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{ _id: 'test-file-id', name: 'testfile.tex' },
|
||||
{ _id: 'test-bib-file-id', name: 'testsources.bib' },
|
||||
],
|
||||
fileRefs: [{ _id: 'test-image-id', name: 'frog.jpg', hash: '42' }],
|
||||
folders: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const scope = {
|
||||
user,
|
||||
project,
|
||||
$watch: (key: string, callback: () => void) => {
|
||||
scopeWatchers.push([key, callback])
|
||||
},
|
||||
$applyAsync: (callback: () => void) => {
|
||||
window.setTimeout(() => {
|
||||
callback()
|
||||
for (const [key, watcher] of scopeWatchers) {
|
||||
watcher(get(ide.$scope, key))
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
$on: (eventName: string, callback: () => void) => {
|
||||
//
|
||||
},
|
||||
$broadcast: () => {},
|
||||
ui: {
|
||||
chatOpen: true,
|
||||
pdfLayout: 'flat',
|
||||
},
|
||||
settings: {
|
||||
pdfViewer: 'js',
|
||||
syntaxValidation: true,
|
||||
},
|
||||
editor: {
|
||||
richText: false,
|
||||
sharejs_doc: {
|
||||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
hasBufferedOps: () => false,
|
||||
},
|
||||
open_doc_name: 'testfile.tex',
|
||||
},
|
||||
hasLintingError: false,
|
||||
permissionsLevel: 'owner',
|
||||
}
|
||||
|
||||
const ide = {
|
||||
$scope: scope,
|
||||
socket: new SocketIOShim.SocketShimNoop(
|
||||
new SocketIOMock()
|
||||
) as unknown as Socket,
|
||||
}
|
||||
|
||||
// window.metaAttributesCache is reset in preview.tsx
|
||||
window.metaAttributesCache.set('ol-user', user)
|
||||
window.metaAttributesCache.set('ol-project_id', project._id)
|
||||
window.metaAttributesCache.set(
|
||||
'ol-gitBridgePublicBaseUrl',
|
||||
'https://git.stories.com'
|
||||
)
|
||||
|
||||
window._ide = ide
|
||||
}
|
||||
|
||||
type ScopeDecoratorOptions = {
|
||||
mockCompileOnLoad: boolean
|
||||
providers?: Record<string, any>
|
||||
}
|
||||
|
||||
export const ScopeDecorator = (
|
||||
Story: any,
|
||||
opts: ScopeDecoratorOptions = { mockCompileOnLoad: true },
|
||||
meta: Record<string, any> = {}
|
||||
) => {
|
||||
initialize()
|
||||
|
||||
// mock compile on load
|
||||
useFetchMock(fetchMock => {
|
||||
if (opts.mockCompileOnLoad) {
|
||||
mockCompile(fetchMock)
|
||||
mockCompileError(fetchMock)
|
||||
mockBuildFile(fetchMock)
|
||||
}
|
||||
})
|
||||
|
||||
// clear scopeWatchers on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
scopeWatchers.length = 0
|
||||
}
|
||||
}, [])
|
||||
|
||||
// set values on window.metaAttributesCache (created in initialize, above)
|
||||
useMeta(meta)
|
||||
|
||||
return (
|
||||
<ReactContextRoot
|
||||
providers={{
|
||||
ConnectionProvider,
|
||||
IdeReactProvider,
|
||||
...opts.providers,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</ReactContextRoot>
|
||||
)
|
||||
}
|
||||
|
||||
const ConnectionProvider: FC = ({ children }) => {
|
||||
const [value] = useState(() => {
|
||||
const connectionState: ConnectionState = {
|
||||
readyState: WebSocket.OPEN,
|
||||
forceDisconnected: false,
|
||||
inactiveDisconnect: false,
|
||||
reconnectAt: null,
|
||||
forcedDisconnectDelay: 0,
|
||||
lastConnectionAttempt: 0,
|
||||
error: '',
|
||||
}
|
||||
return {
|
||||
socket: window._ide.socket as Socket,
|
||||
connectionState,
|
||||
isConnected: true,
|
||||
isStillReconnecting: false,
|
||||
secondsUntilReconnect: () => 0,
|
||||
tryReconnectNow: () => {},
|
||||
registerUserActivity: () => {},
|
||||
closeConnection: () => {},
|
||||
getSocketDebuggingInfo: () => ({
|
||||
client_id: 'fakeClientId',
|
||||
transport: 'fakeTransport',
|
||||
publicId: 'fakePublicId',
|
||||
lastUserActivity: 0,
|
||||
connectionState,
|
||||
externalHeartbeat: {
|
||||
currentStart: 0,
|
||||
lastSuccess: 0,
|
||||
lastLatency: 0,
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<ConnectionContext.Provider value={value}>
|
||||
{children}
|
||||
</ConnectionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const IdeReactProvider: FC = ({ children }) => {
|
||||
const projectId = 'project-123'
|
||||
const [startedFreeTrial, setStartedFreeTrial] = useState(false)
|
||||
|
||||
const [ideReactContextValue] = useState(() => ({
|
||||
projectId,
|
||||
eventEmitter: new IdeEventEmitter(),
|
||||
startedFreeTrial,
|
||||
setStartedFreeTrial,
|
||||
reportError: () => {},
|
||||
projectJoined: true,
|
||||
}))
|
||||
|
||||
const [ideContextValue] = useState(() => {
|
||||
const ide = window._ide
|
||||
const scopeStore = createReactScopeValueStore(projectId)
|
||||
for (const [key, value] of Object.entries(ide.$scope)) {
|
||||
scopeStore.set(key, value)
|
||||
}
|
||||
const scopeEventEmitter = new ReactScopeEventEmitter(new IdeEventEmitter())
|
||||
|
||||
window.overleaf = {
|
||||
...window.overleaf,
|
||||
unstable: {
|
||||
...window.overleaf?.unstable,
|
||||
store: scopeStore,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
...ide,
|
||||
scopeStore,
|
||||
scopeEventEmitter,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<IdeReactContext.Provider value={ideReactContextValue}>
|
||||
<IdeContext.Provider value={ideContextValue}>
|
||||
{children}
|
||||
</IdeContext.Provider>
|
||||
</IdeReactContext.Provider>
|
||||
)
|
||||
}
|
13
services/web/frontend/stories/deprecated-browser.stories.tsx
Normal file
13
services/web/frontend/stories/deprecated-browser.stories.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { DeprecatedBrowser } from '@/shared/components/deprecated-browser'
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Project List / Deprecated Browser',
|
||||
component: DeprecatedBrowser,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof DeprecatedBrowser>
|
||||
|
||||
export const Notification: Story = {}
|
@@ -0,0 +1,57 @@
|
||||
import ActionsMenu from '../../js/features/editor-left-menu/components/actions-menu'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import { mockCompile, mockCompileError } from '../fixtures/compile'
|
||||
import { document, mockDocument } from '../fixtures/document'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import { useScope } from '../hooks/use-scope'
|
||||
|
||||
export default {
|
||||
title: 'Editor / Left Menu / Actions Menu',
|
||||
component: ActionsMenu,
|
||||
decorators: [
|
||||
(Story: any) => ScopeDecorator(Story, { mockCompileOnLoad: false }),
|
||||
],
|
||||
}
|
||||
|
||||
export const NotCompiled = () => {
|
||||
window.metaAttributesCache.set('ol-anonymous', false)
|
||||
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompileError(fetchMock, 'failure')
|
||||
})
|
||||
|
||||
return (
|
||||
<div id="left-menu" className="shown">
|
||||
<ActionsMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const CompileSuccess = () => {
|
||||
window.metaAttributesCache.set('ol-anonymous', false)
|
||||
|
||||
useScope({
|
||||
editor: {
|
||||
sharejs_doc: mockDocument(document.tex),
|
||||
},
|
||||
})
|
||||
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock)
|
||||
fetchMock.get('express:/project/:projectId/wordcount', {
|
||||
texcount: {
|
||||
encode: 'ascii',
|
||||
textWords: 10,
|
||||
headers: 11,
|
||||
mathInline: 12,
|
||||
mathDisplay: 13,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<div id="left-menu" className="shown">
|
||||
<ActionsMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
import DownloadMenu from '../../js/features/editor-left-menu/components/download-menu'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import { mockCompile, mockCompileError } from '../fixtures/compile'
|
||||
import { document, mockDocument } from '../fixtures/document'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import { useScope } from '../hooks/use-scope'
|
||||
|
||||
export default {
|
||||
title: 'Editor / Left Menu / Download Menu',
|
||||
component: DownloadMenu,
|
||||
decorators: [
|
||||
(Story: any) => ScopeDecorator(Story, { mockCompileOnLoad: false }),
|
||||
],
|
||||
}
|
||||
|
||||
export const NotCompiled = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompileError(fetchMock, 'failure')
|
||||
})
|
||||
|
||||
return (
|
||||
<div id="left-menu" className="shown">
|
||||
<DownloadMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const CompileSuccess = () => {
|
||||
useScope({
|
||||
editor: {
|
||||
sharejs_doc: mockDocument(document.tex),
|
||||
},
|
||||
})
|
||||
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock)
|
||||
})
|
||||
|
||||
return (
|
||||
<div id="left-menu" className="shown">
|
||||
<DownloadMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
import HelpMenu from '../../js/features/editor-left-menu/components/help-menu'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
|
||||
export default {
|
||||
title: 'Editor / Left Menu / Help Menu',
|
||||
component: HelpMenu,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
||||
|
||||
export const ShowSupport = () => {
|
||||
window.metaAttributesCache.set('ol-showSupport', true)
|
||||
window.metaAttributesCache.set('ol-user', {
|
||||
email: 'sherlock@holmes.co.uk',
|
||||
first_name: 'Sherlock',
|
||||
last_name: 'Holmes',
|
||||
})
|
||||
|
||||
return (
|
||||
<div id="left-menu" className="shown">
|
||||
<HelpMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const HideSupport = () => {
|
||||
window.metaAttributesCache.set('ol-showSupport', false)
|
||||
|
||||
return (
|
||||
<div id="left-menu" className="shown">
|
||||
<HelpMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import SyncMenu from '../../js/features/editor-left-menu/components/sync-menu'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import { useScope } from '../hooks/use-scope'
|
||||
|
||||
export default {
|
||||
title: 'Editor / Left Menu / Sync Menu',
|
||||
component: SyncMenu,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
||||
|
||||
export const WriteAccess = () => {
|
||||
window.metaAttributesCache.set('ol-anonymous', false)
|
||||
window.metaAttributesCache.set('ol-gitBridgeEnabled', true)
|
||||
useScope({
|
||||
permissionsLevel: 'owner',
|
||||
})
|
||||
|
||||
return (
|
||||
<div id="left-menu" className="shown">
|
||||
<SyncMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ReadOnlyAccess = () => {
|
||||
window.metaAttributesCache.set('ol-anonymous', false)
|
||||
window.metaAttributesCache.set('ol-gitBridgeEnabled', true)
|
||||
useScope({
|
||||
permissionsLevel: 'readOnly',
|
||||
})
|
||||
|
||||
return (
|
||||
<div id="left-menu" className="shown">
|
||||
<SyncMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
import ToolbarHeader from '../js/features/editor-navigation-toolbar/components/toolbar-header'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
|
||||
export const UpToThreeConnectedUsers = args => {
|
||||
return <ToolbarHeader {...args} />
|
||||
}
|
||||
UpToThreeConnectedUsers.args = {
|
||||
onlineUsers: ['a', 'c', 'd'].map(c => ({
|
||||
user_id: c,
|
||||
name: `${c}_user name`,
|
||||
})),
|
||||
}
|
||||
|
||||
export const ManyConnectedUsers = args => {
|
||||
return <ToolbarHeader {...args} />
|
||||
}
|
||||
ManyConnectedUsers.args = {
|
||||
onlineUsers: ['a', 'c', 'd', 'e', 'f'].map(c => ({
|
||||
user_id: c,
|
||||
name: `${c}_user name`,
|
||||
})),
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / Toolbar',
|
||||
component: ToolbarHeader,
|
||||
argTypes: {
|
||||
goToUser: { action: 'goToUser' },
|
||||
renameProject: { action: 'renameProject' },
|
||||
toggleHistoryOpen: { action: 'toggleHistoryOpen' },
|
||||
toggleReviewPanelOpen: { action: 'toggleReviewPanelOpen' },
|
||||
toggleChatOpen: { action: 'toggleChatOpen' },
|
||||
openShareModal: { action: 'openShareModal' },
|
||||
onShowLeftMenuClick: { action: 'onShowLeftMenuClick' },
|
||||
},
|
||||
args: {
|
||||
projectName: 'Overleaf Project',
|
||||
onlineUsers: [{ user_id: 'abc', name: 'overleaf' }],
|
||||
unreadMessageCount: 0,
|
||||
},
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
12
services/web/frontend/stories/editor-switch.stories.jsx
Normal file
12
services/web/frontend/stories/editor-switch.stories.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import EditorSwitch from '../js/features/source-editor/components/editor-switch'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
|
||||
export default {
|
||||
title: 'Editor / Switch',
|
||||
component: EditorSwitch,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
||||
|
||||
export const Switcher = () => {
|
||||
return <EditorSwitch />
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
import { LayoutDropdownButtonUi } from '@/features/editor-navigation-toolbar/components/layout-dropdown-button'
|
||||
import { Meta } from '@storybook/react'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
export const LayoutDropdown = (
|
||||
props: ComponentProps<typeof LayoutDropdownButtonUi>
|
||||
) => (
|
||||
<div className="toolbar toolbar-header justify-content-end m-4">
|
||||
<div className="toolbar-right">
|
||||
<LayoutDropdownButtonUi {...props} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const meta: Meta<typeof LayoutDropdownButtonUi> = {
|
||||
title: 'Editor / Toolbar / Layout Dropdown',
|
||||
component: LayoutDropdownButtonUi,
|
||||
argTypes: {
|
||||
view: {
|
||||
control: 'select',
|
||||
options: [null, 'editor', 'file', 'pdf', 'history'],
|
||||
},
|
||||
detachRole: {
|
||||
control: 'select',
|
||||
options: ['detacher', 'detached'],
|
||||
},
|
||||
pdfLayout: {
|
||||
control: 'select',
|
||||
options: ['sideBySide', 'flat'],
|
||||
},
|
||||
},
|
||||
parameters: { actions: { argTypesRegex: '^handle.*' } },
|
||||
}
|
||||
|
||||
export default meta
|
@@ -0,0 +1,54 @@
|
||||
import { FC } from 'react'
|
||||
import type { Meta } from '@storybook/react'
|
||||
import PdfCompileButton from '@/features/pdf-preview/components/pdf-compile-button'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import { CompileContext } from '@/shared/context/local-compile-context'
|
||||
import { DetachCompileContext } from '@/shared/context/detach-compile-context'
|
||||
|
||||
export const CompileButton: FC<CompileContext> = (props: CompileContext) => (
|
||||
<DetachCompileContext.Provider value={props}>
|
||||
<div className="pdf m-5">
|
||||
<div className="toolbar toolbar-pdf toolbar-pdf-hybrid btn-toolbar">
|
||||
<div className="toolbar-pdf-left">
|
||||
<PdfCompileButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DetachCompileContext.Provider>
|
||||
)
|
||||
|
||||
const args: Partial<CompileContext> = {
|
||||
autoCompile: false,
|
||||
compiling: false,
|
||||
draft: false,
|
||||
hasChanges: false,
|
||||
stopOnFirstError: false,
|
||||
stopOnValidationError: false,
|
||||
animateCompileDropdownArrow: false,
|
||||
}
|
||||
|
||||
const meta: Meta<typeof CompileButton> = {
|
||||
title: 'Editor / Toolbar / Compile Button',
|
||||
component: CompileButton,
|
||||
// @ts-ignore
|
||||
decorators: [ScopeDecorator],
|
||||
argTypes: {
|
||||
startCompile: { action: 'startCompile' },
|
||||
setAutoCompile: { action: 'setAutoCompile' },
|
||||
setCompiling: { action: 'setCompiling' },
|
||||
setDraft: { action: 'setDraft' },
|
||||
setStopOnFirstError: { action: 'setStopOnFirstError' },
|
||||
setError: { action: 'setError' },
|
||||
setHasLintingError: { action: 'setHasLintingError' },
|
||||
setPosition: { action: 'setPosition' },
|
||||
setStopOnValidationError: { action: 'setStopOnValidationError' },
|
||||
recompileFromScratch: { action: 'recompileFromScratch' },
|
||||
stopCompile: { action: 'stopCompile' },
|
||||
setAnimateCompileDropdownArrow: {
|
||||
action: 'setAnimateCompileDropdownArrow',
|
||||
},
|
||||
},
|
||||
args,
|
||||
}
|
||||
|
||||
export default meta
|
35
services/web/frontend/stories/feedback-badge.stories.tsx
Normal file
35
services/web/frontend/stories/feedback-badge.stories.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
import { FeedbackBadge } from '@/shared/components/feedback-badge'
|
||||
|
||||
export const WithDefaultText = () => {
|
||||
return (
|
||||
<FeedbackBadge
|
||||
url="https://example.com"
|
||||
id="storybook-feedback-with-text"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithCustomText = () => {
|
||||
const FeedbackContent = () => (
|
||||
<>
|
||||
This is an example.
|
||||
<br />
|
||||
Click to find out more
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<FeedbackBadge
|
||||
url="https://example.com"
|
||||
id="storybook-feedback-with-text"
|
||||
text={<FeedbackContent />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Feedback Badge',
|
||||
component: FeedbackBadge,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
180
services/web/frontend/stories/file-tree.stories.jsx
Normal file
180
services/web/frontend/stories/file-tree.stories.jsx
Normal file
@@ -0,0 +1,180 @@
|
||||
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'
|
||||
import { useIdeContext } from '@/shared/context/ide-context'
|
||||
|
||||
const MOCK_DELAY = 2000
|
||||
|
||||
const DEFAULT_PROJECT = {
|
||||
_id: '123abc',
|
||||
name: 'Some Project',
|
||||
rootDocId: '5e74f1a7ce17ae0041dfd056',
|
||||
rootFolder: rootFolderBase,
|
||||
}
|
||||
|
||||
function defaultSetupMocks(fetchMock, socket) {
|
||||
fetchMock
|
||||
.post(
|
||||
/\/project\/\w+\/(file|doc|folder)\/\w+\/rename/,
|
||||
(path, req) => {
|
||||
const body = JSON.parse(req.body)
|
||||
const entityId = path.match(/([^/]+)\/rename$/)[1]
|
||||
socket.emitToClient('reciveEntityRename', entityId, body.name)
|
||||
return 204
|
||||
},
|
||||
|
||||
{
|
||||
delay: MOCK_DELAY,
|
||||
}
|
||||
)
|
||||
.post(
|
||||
/\/project\/\w+\/folder/,
|
||||
(_path, req) => {
|
||||
const body = JSON.parse(req.body)
|
||||
const newFolder = {
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
docs: [],
|
||||
_id: Math.random().toString(16).replace(/0\./, 'random-test-id-'),
|
||||
name: body.name,
|
||||
}
|
||||
socket.emitToClient('reciveNewFolder', body.parent_folder_id, newFolder)
|
||||
return newFolder
|
||||
},
|
||||
{
|
||||
delay: MOCK_DELAY,
|
||||
}
|
||||
)
|
||||
.delete(
|
||||
/\/project\/\w+\/(file|doc|folder)\/\w+/,
|
||||
path => {
|
||||
const entityId = path.match(/[^/]+$/)[0]
|
||||
socket.emitToClient('removeEntity', entityId)
|
||||
return 204
|
||||
},
|
||||
{
|
||||
delay: MOCK_DELAY,
|
||||
}
|
||||
)
|
||||
.post(/\/project\/\w+\/(file|doc|folder)\/\w+\/move/, (path, req) => {
|
||||
const body = JSON.parse(req.body)
|
||||
const entityId = path.match(/([^/]+)\/move/)[1]
|
||||
socket.emitToClient('reciveEntityMove', entityId, body.folder_id)
|
||||
return 204
|
||||
})
|
||||
}
|
||||
|
||||
export const FullTree = args => {
|
||||
const { socket } = useIdeContext()
|
||||
useFetchMock(fetchMock => defaultSetupMocks(fetchMock, socket))
|
||||
|
||||
useScope({
|
||||
project: DEFAULT_PROJECT,
|
||||
permissionsLevel: 'owner',
|
||||
})
|
||||
|
||||
return <FileTreeRoot {...args} />
|
||||
}
|
||||
|
||||
export const ReadOnly = args => {
|
||||
useScope({
|
||||
project: DEFAULT_PROJECT,
|
||||
permissionsLevel: 'readOnly',
|
||||
})
|
||||
|
||||
return <FileTreeRoot {...args} />
|
||||
}
|
||||
|
||||
export const Disconnected = args => {
|
||||
useScope({
|
||||
project: DEFAULT_PROJECT,
|
||||
permissionsLevel: 'owner',
|
||||
})
|
||||
|
||||
return <FileTreeRoot {...args} />
|
||||
}
|
||||
Disconnected.args = { isConnected: false }
|
||||
|
||||
export const NetworkErrors = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock
|
||||
.post(/\/project\/\w+\/folder/, 500, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
.post(/\/project\/\w+\/(file|doc|folder)\/\w+\/rename/, 500, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
.post(/\/project\/\w+\/(file|doc|folder)\/\w+\/move/, 500, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
.delete(/\/project\/\w+\/(file|doc|folder)\/\w+/, 500, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
})
|
||||
|
||||
useScope({
|
||||
project: DEFAULT_PROJECT,
|
||||
permissionsLevel: 'owner',
|
||||
})
|
||||
|
||||
return <FileTreeRoot {...args} />
|
||||
}
|
||||
|
||||
export const FallbackError = args => {
|
||||
useScope({
|
||||
project: DEFAULT_PROJECT,
|
||||
})
|
||||
|
||||
return <FileTreeError {...args} />
|
||||
}
|
||||
|
||||
export const FilesLimit = args => {
|
||||
const { socket } = useIdeContext()
|
||||
useFetchMock(fetchMock => defaultSetupMocks(fetchMock, socket))
|
||||
|
||||
useScope({
|
||||
project: {
|
||||
...DEFAULT_PROJECT,
|
||||
rootFolder: rootFolderLimit,
|
||||
},
|
||||
permissionsLevel: 'owner',
|
||||
})
|
||||
|
||||
return <FileTreeRoot {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / File Tree',
|
||||
component: FileTreeRoot,
|
||||
args: {
|
||||
setStartedFreeTrial: () => {
|
||||
console.log('started free trial')
|
||||
},
|
||||
refProviders: {},
|
||||
setRefProviderEnabled: provider => {
|
||||
console.log(`ref provider ${provider} enabled`)
|
||||
},
|
||||
isConnected: true,
|
||||
},
|
||||
argTypes: {
|
||||
onInit: { action: 'onInit' },
|
||||
onSelect: { action: 'onSelect' },
|
||||
},
|
||||
decorators: [
|
||||
ScopeDecorator,
|
||||
Story => (
|
||||
<>
|
||||
<style>{'html, body, .file-tree { height: 100%; width: 100%; }'}</style>
|
||||
<div className="editor-sidebar full-size">
|
||||
<div className="file-tree">
|
||||
<Story />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
],
|
||||
}
|
203
services/web/frontend/stories/file-view/file-view.stories.jsx
Normal file
203
services/web/frontend/stories/file-view/file-view.stories.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
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}
|
||||
\\begin{document}
|
||||
First document. This is a simple example, with no
|
||||
extra parameters or packages included.
|
||||
\\end{document}`,
|
||||
bibtex: `@book{latexcompanion,
|
||||
author = "Michel Goossens and Frank Mittelbach and Alexander Samarin",
|
||||
title = "The \\LaTeX\\ Companion",
|
||||
year = "1993",
|
||||
publisher = "Addison-Wesley",
|
||||
address = "Reading, Massachusetts"
|
||||
}`,
|
||||
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`,
|
||||
}
|
||||
|
||||
const setupFetchMock = fetchMock => {
|
||||
return fetchMock
|
||||
.head('express:/project/:project_id/blob/:hash', {
|
||||
status: 201,
|
||||
headers: { 'Content-Length': 10000 },
|
||||
})
|
||||
.post('express:/project/:project_id/linked_file/:file_id/refresh', {
|
||||
status: 204,
|
||||
})
|
||||
.post('express:/project/:project_id/references/indexAll', {
|
||||
status: 204,
|
||||
})
|
||||
}
|
||||
|
||||
const fileData = {
|
||||
id: 'file-id',
|
||||
name: 'file.tex',
|
||||
created: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const FileFromUrl = args => {
|
||||
useFetchMock(fetchMock =>
|
||||
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||
body: bodies.latex,
|
||||
})
|
||||
)
|
||||
|
||||
return <FileView {...args} />
|
||||
}
|
||||
FileFromUrl.args = {
|
||||
file: {
|
||||
...fileData,
|
||||
linkedFileData: {
|
||||
url: 'https://example.com/source-file.tex',
|
||||
provider: 'url',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const FileFromProjectWithLinkableProjectId = args => {
|
||||
useFetchMock(fetchMock =>
|
||||
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||
body: bodies.latex,
|
||||
})
|
||||
)
|
||||
|
||||
return <FileView {...args} />
|
||||
}
|
||||
FileFromProjectWithLinkableProjectId.args = {
|
||||
file: {
|
||||
...fileData,
|
||||
linkedFileData: {
|
||||
source_project_id: 'source-project-id',
|
||||
source_entity_path: '/source-file.tex',
|
||||
provider: 'project_file',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const FileFromProjectWithoutLinkableProjectId = args => {
|
||||
useFetchMock(fetchMock =>
|
||||
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||
body: bodies.latex,
|
||||
})
|
||||
)
|
||||
|
||||
return <FileView {...args} />
|
||||
}
|
||||
FileFromProjectWithoutLinkableProjectId.args = {
|
||||
file: {
|
||||
...fileData,
|
||||
linkedFileData: {
|
||||
v1_source_doc_id: 'v1-source-id',
|
||||
source_entity_path: '/source-file.tex',
|
||||
provider: 'project_file',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const FileFromProjectOutputWithLinkableProject = args => {
|
||||
useFetchMock(fetchMock =>
|
||||
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||
body: bodies.latex,
|
||||
})
|
||||
)
|
||||
|
||||
return <FileView {...args} />
|
||||
}
|
||||
FileFromProjectOutputWithLinkableProject.args = {
|
||||
file: {
|
||||
...fileData,
|
||||
linkedFileData: {
|
||||
source_project_id: 'source_project_id',
|
||||
source_output_file_path: '/source-file.tex',
|
||||
provider: 'project_output_file',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const FileFromProjectOutputWithoutLinkableProjectId = args => {
|
||||
useFetchMock(fetchMock =>
|
||||
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||
body: bodies.latex,
|
||||
})
|
||||
)
|
||||
|
||||
return <FileView {...args} />
|
||||
}
|
||||
FileFromProjectOutputWithoutLinkableProjectId.args = {
|
||||
file: {
|
||||
...fileData,
|
||||
linkedFileData: {
|
||||
v1_source_doc_id: 'v1-source-id',
|
||||
source_output_file_path: '/source-file.tex',
|
||||
provider: 'project_output_file',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ImageFile = args => {
|
||||
useFetchMock(setupFetchMock) // NOTE: can't mock img src request
|
||||
|
||||
return <FileView {...args} />
|
||||
}
|
||||
ImageFile.storyName = 'Image File (Error)'
|
||||
ImageFile.args = {
|
||||
file: {
|
||||
...fileData,
|
||||
id: '60097ca20454610027c442a8',
|
||||
name: 'file.jpg',
|
||||
linkedFileData: {
|
||||
source_project_id: 'source_project_id',
|
||||
source_entity_path: '/source-file.jpg',
|
||||
provider: 'project_file',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const TextFile = args => {
|
||||
useFetchMock(fetchMock =>
|
||||
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||
body: bodies.text,
|
||||
})
|
||||
)
|
||||
return <FileView {...args} />
|
||||
}
|
||||
TextFile.args = {
|
||||
file: {
|
||||
...fileData,
|
||||
linkedFileData: {
|
||||
source_project_id: 'source-project-id',
|
||||
source_entity_path: '/source-file.txt',
|
||||
provider: 'project_file',
|
||||
},
|
||||
name: 'file.txt',
|
||||
},
|
||||
}
|
||||
|
||||
export const UploadedFile = args => {
|
||||
useFetchMock(fetchMock =>
|
||||
setupFetchMock(fetchMock).head('express:/project/:project_id/blob/:hash', {
|
||||
status: 500,
|
||||
})
|
||||
)
|
||||
return <FileView {...args} />
|
||||
}
|
||||
UploadedFile.storyName = 'Uploaded File (Error)'
|
||||
UploadedFile.args = {
|
||||
file: {
|
||||
...fileData,
|
||||
linkedFileData: null,
|
||||
name: 'file.jpg',
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / FileView',
|
||||
component: FileView,
|
||||
argTypes: {
|
||||
storeReferencesKeys: { action: 'store references keys' },
|
||||
},
|
||||
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 @@
|
||||
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',
|
||||
}
|
||||
|
||||
let nextMessageId = 1
|
||||
|
||||
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: '' + nextMessageId++,
|
||||
content: `message #${i}`,
|
||||
user: author,
|
||||
timestamp,
|
||||
})
|
||||
}
|
||||
return messages
|
||||
}
|
220
services/web/frontend/stories/fixtures/compile.js
Normal file
220
services/web/frontend/stories/fixtures/compile.js
Normal file
@@ -0,0 +1,220 @@
|
||||
import examplePdf from './storybook-example.pdf'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
export const dispatchDocChanged = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('doc:changed', { detail: { doc_id: 'foo' } })
|
||||
)
|
||||
}
|
||||
|
||||
export const outputFiles = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
build: '123',
|
||||
url: '/build/output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'output.bbl',
|
||||
build: '123',
|
||||
url: '/build/output.bbl',
|
||||
type: 'bbl',
|
||||
},
|
||||
{
|
||||
path: 'output.bib',
|
||||
build: '123',
|
||||
url: '/build/output.bib',
|
||||
type: 'bib',
|
||||
},
|
||||
{
|
||||
path: 'example.txt',
|
||||
build: '123',
|
||||
url: '/build/example.txt',
|
||||
type: 'txt',
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
build: '123',
|
||||
url: '/build/output.log',
|
||||
type: 'log',
|
||||
},
|
||||
{
|
||||
path: 'output.blg',
|
||||
build: '123',
|
||||
url: '/build/output.blg',
|
||||
type: 'blg',
|
||||
},
|
||||
]
|
||||
|
||||
export const mockCompile = (fetchMock, delay = 1000) =>
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/compile',
|
||||
{
|
||||
body: {
|
||||
status: 'success',
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
pdfDownloadDomain: '',
|
||||
outputFiles: cloneDeep(outputFiles),
|
||||
},
|
||||
},
|
||||
{ delay }
|
||||
)
|
||||
|
||||
export const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/compile',
|
||||
{
|
||||
body: {
|
||||
status,
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
},
|
||||
},
|
||||
{ delay, overwriteRoutes: true }
|
||||
)
|
||||
|
||||
export const mockCompileValidationIssues = (
|
||||
fetchMock,
|
||||
validationProblems,
|
||||
delay = 1000
|
||||
) =>
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/compile',
|
||||
() => {
|
||||
return {
|
||||
body: {
|
||||
status: 'validation-problems',
|
||||
validationProblems,
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
},
|
||||
}
|
||||
},
|
||||
{ delay }
|
||||
)
|
||||
|
||||
export const mockClearCache = fetchMock =>
|
||||
fetchMock.delete('express:/project/:projectId/output', 204, {
|
||||
delay: 1000,
|
||||
})
|
||||
|
||||
export const mockBuildFile = fetchMock =>
|
||||
fetchMock.get('express:/build/:file', (url, options, request) => {
|
||||
const { pathname } = new URL(url, 'https://example.com')
|
||||
|
||||
switch (pathname) {
|
||||
case '/build/output.blg':
|
||||
return 'This is BibTeX, Version 4.0' // FIXME
|
||||
|
||||
case '/build/output.log':
|
||||
return `
|
||||
The LaTeX compiler output
|
||||
* With a lot of details
|
||||
|
||||
Wrapped in an HTML <pre> element with
|
||||
preformatted text which is to be presented exactly
|
||||
as written in the HTML file
|
||||
|
||||
(whitespace included™)
|
||||
|
||||
The text is typically rendered using a non-proportional ("monospace") font.
|
||||
|
||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||
(Font) <7> on input line 18.
|
||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||
(Font) <5> on input line 18.
|
||||
! Undefined control sequence.
|
||||
<recently read> \\Zlpha
|
||||
|
||||
main.tex, line 23
|
||||
|
||||
`
|
||||
|
||||
case '/build/output.pdf':
|
||||
return new Promise(resolve => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.addEventListener('load', () => {
|
||||
resolve({
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
||||
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
||||
},
|
||||
body: xhr.response,
|
||||
})
|
||||
})
|
||||
xhr.open('GET', examplePdf)
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.send()
|
||||
})
|
||||
|
||||
default:
|
||||
console.log(pathname)
|
||||
return 404
|
||||
}
|
||||
})
|
||||
|
||||
const mockHighlights = [
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 509.999878,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 486.089539,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 498.044708,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 521.955078,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
]
|
||||
|
||||
export const mockEventTracking = fetchMock =>
|
||||
fetchMock.get('express:/event/:event', 204)
|
||||
|
||||
export const mockValidPdf = fetchMock =>
|
||||
fetchMock.get('express:/build/output.pdf', (url, options, request) => {
|
||||
return new Promise(resolve => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.addEventListener('load', () => {
|
||||
resolve({
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
||||
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
||||
'Accept-Ranges': 'bytes',
|
||||
},
|
||||
body: xhr.response,
|
||||
})
|
||||
})
|
||||
xhr.open('GET', examplePdf)
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.send()
|
||||
})
|
||||
})
|
||||
|
||||
export const mockSynctex = fetchMock =>
|
||||
fetchMock
|
||||
.get('express:/project/:projectId/sync/code', () => {
|
||||
return { pdf: cloneDeep(mockHighlights) }
|
||||
})
|
||||
.get('express:/project/:projectId/sync/pdf', () => {
|
||||
return { code: [{ file: 'main.tex', line: 100 }] }
|
||||
})
|
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',
|
||||
},
|
||||
]
|
40
services/web/frontend/stories/fixtures/document.ts
Normal file
40
services/web/frontend/stories/fixtures/document.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export function mockDocument(text: string) {
|
||||
return {
|
||||
doc_id: 'story-doc',
|
||||
getSnapshot: () => text,
|
||||
hasBufferedOps: () => false,
|
||||
}
|
||||
}
|
||||
|
||||
export const document = {
|
||||
tex: `\\documentclass{article}
|
||||
|
||||
% Language setting
|
||||
% Replace \`english' with e.g. \`spanish' to change the document language
|
||||
\\usepackage[english]{babel}
|
||||
|
||||
% Set page size and margins
|
||||
% Replace \`letterpaper' with\`a4paper' for UK/EU standard size
|
||||
\\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry}
|
||||
|
||||
% Useful packages
|
||||
\\usepackage{amsmath}
|
||||
\\usepackage{graphicx}
|
||||
\\usepackage[colorlinks=true, allcolors=blue]{hyperref}
|
||||
|
||||
\\title{Your Paper}
|
||||
\\author{You}
|
||||
|
||||
\\begin{document}
|
||||
\\maketitle
|
||||
|
||||
\\begin{abstract}
|
||||
Your abstract.
|
||||
\\end{abstract}
|
||||
|
||||
\\section{Introduction}
|
||||
|
||||
Your introduction goes here! Simply start writing your document and use the Recompile button to view the updated PDF preview. Examples of commonly used commands and features are listed below, to help you get started.
|
||||
|
||||
Once you're familiar with the editor, you can find various project setting in the Overleaf menu, accessed via the button in the very top left of the editor. To view tutorials, user guides, and further documentation, please visit our \\href{https://www.overleaf.com/learn}{help library}, or head to our plans page to \\href{https://www.overleaf.com/user/subscription/plans}{choose your plan}.`,
|
||||
}
|
44
services/web/frontend/stories/fixtures/file-tree-base.js
Normal file
44
services/web/frontend/stories/fixtures/file-tree-base.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export const rootFolderBase = [
|
||||
{
|
||||
_id: '5e74f1a7ce17ae0041dfd054',
|
||||
name: 'rootFolder',
|
||||
folders: [
|
||||
{
|
||||
_id: '5f638e58b652df0026c5c8f5',
|
||||
name: 'a folder',
|
||||
folders: [
|
||||
{
|
||||
_id: '5f956f62700e19000177daa0',
|
||||
name: 'sub folder',
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
docs: [],
|
||||
},
|
||||
],
|
||||
fileRefs: [
|
||||
{ _id: '5cffb9d93da45d3995d05362', name: 'file-in-a-folder.pdf' },
|
||||
],
|
||||
docs: [
|
||||
{ _id: '5f46786322d556004e72a555', name: 'doc-in-a-folder.tex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
_id: '5f638e68b652df0026c5c8f6',
|
||||
name: 'another folder',
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
docs: [],
|
||||
},
|
||||
],
|
||||
fileRefs: [{ _id: '5f11c78e0924770027412a67', name: 'univers.jpg' }],
|
||||
docs: [
|
||||
{ _id: '5e74f1a7ce17ae0041dfd056', name: 'main.tex' },
|
||||
{ _id: '5f46789522d556004e72a556', name: 'perso.bib' },
|
||||
{
|
||||
_id: '5da532e29019e800015321c6',
|
||||
name: 'zotero.bib',
|
||||
linkedFileData: { provider: 'zotero' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
52
services/web/frontend/stories/fixtures/file-tree-limit.js
Normal file
52
services/web/frontend/stories/fixtures/file-tree-limit.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const FILE_PER_FOLDER = 2
|
||||
const DOC_PER_FOLDER = 3
|
||||
const FOLDER_PER_FOLDER = 2
|
||||
const MAX_DEPTH = 7
|
||||
|
||||
function fakeId() {
|
||||
return Math.random().toString(16).replace(/0\./, 'random-test-id-')
|
||||
}
|
||||
|
||||
function makeFileRefs(path) {
|
||||
const fileRefs = []
|
||||
|
||||
for (let index = 0; index < FILE_PER_FOLDER; index++) {
|
||||
fileRefs.push({ _id: fakeId(), name: `${path}-file-${index}.jpg` })
|
||||
}
|
||||
return fileRefs
|
||||
}
|
||||
|
||||
function makeDocs(path) {
|
||||
const docs = []
|
||||
|
||||
for (let index = 0; index < DOC_PER_FOLDER; index++) {
|
||||
docs.push({ _id: fakeId(), name: `${path}-doc-${index}.tex` })
|
||||
}
|
||||
return docs
|
||||
}
|
||||
|
||||
function makeFolders(path, depth = 0) {
|
||||
const folders = []
|
||||
|
||||
for (let index = 0; index < FOLDER_PER_FOLDER; index++) {
|
||||
const folderPath = `${path}-folder-${index}`
|
||||
folders.push({
|
||||
_id: fakeId(),
|
||||
name: folderPath,
|
||||
folders: depth < MAX_DEPTH ? makeFolders(folderPath, depth + 1) : [],
|
||||
fileRefs: makeFileRefs(folderPath),
|
||||
docs: makeDocs(folderPath),
|
||||
})
|
||||
}
|
||||
return folders
|
||||
}
|
||||
|
||||
export const rootFolderLimit = [
|
||||
{
|
||||
_id: fakeId(),
|
||||
name: 'rootFolder',
|
||||
folders: makeFolders('root'),
|
||||
fileRefs: makeFileRefs('root'),
|
||||
docs: makeDocs('root'),
|
||||
},
|
||||
]
|
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: '63e21c07946dd8c76505f85a',
|
||||
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',
|
||||
},
|
||||
],
|
||||
}
|
BIN
services/web/frontend/stories/fixtures/storybook-example.pdf
Normal file
BIN
services/web/frontend/stories/fixtures/storybook-example.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,157 @@
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import DocumentDiffViewer from '../../js/features/history/components/diff-view/document-diff-viewer'
|
||||
import React from 'react'
|
||||
import { Highlight } from '../../js/features/history/services/types/doc'
|
||||
|
||||
const highlights: Highlight[] = [
|
||||
{
|
||||
type: 'addition',
|
||||
range: { from: 3, to: 10 },
|
||||
hue: 200,
|
||||
label: 'Added by Wombat on Monday',
|
||||
},
|
||||
{
|
||||
type: 'deletion',
|
||||
range: { from: 15, to: 25 },
|
||||
hue: 62,
|
||||
label: 'Deleted by Duck on Monday',
|
||||
},
|
||||
{
|
||||
type: 'addition',
|
||||
range: { from: 100, to: 400 },
|
||||
hue: 200,
|
||||
label: 'Added by Wombat on Friday',
|
||||
},
|
||||
{
|
||||
type: 'deletion',
|
||||
range: { from: 564, to: 565 },
|
||||
hue: 200,
|
||||
label: 'Deleted by Wombat on Friday',
|
||||
},
|
||||
{
|
||||
type: 'addition',
|
||||
range: { from: 1770, to: 1780 },
|
||||
hue: 200,
|
||||
label: 'Added by Wombat on Tuesday',
|
||||
},
|
||||
]
|
||||
|
||||
const doc = `\\documentclass{article}
|
||||
|
||||
% Language setting
|
||||
% Replace \`english' with e.g. \`spanish' to change the document language
|
||||
\\usepackage[english]{babel}
|
||||
|
||||
% Set page size and margins
|
||||
% Replace \`letterpaper' with \`a4paper' for UK/EU standard size
|
||||
\\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry}
|
||||
|
||||
% Useful packages
|
||||
\\usepackage{amsmath}
|
||||
\\usepackage{graphicx}
|
||||
\\usepackage[colorlinks=true, allcolors=blue]{hyperref}
|
||||
|
||||
\\title{Your Paper}
|
||||
\\author{You}
|
||||
|
||||
\\begin{document}
|
||||
\\maketitle
|
||||
|
||||
\\begin{abstract}
|
||||
Your abstract.
|
||||
\\end{abstract}
|
||||
|
||||
\\section{Introduction}
|
||||
|
||||
Your introduction goes here! Simply start writing your document and use the Recompile button to view the updated PDF preview. Examples of commonly used commands and features are listed below, to help you get started.
|
||||
|
||||
Once you're familiar with the editor, you can find various project settings in the Overleaf menu, accessed via the button in the very top left of the editor. To view tutorials, user guides, and further documentation, please visit our \\href{https://www.overleaf.com/learn}{help library}, or head to our plans page to \\href{https://www.overleaf.com/user/subscription/plans}{choose your plan}.
|
||||
|
||||
\\begin{enumerate}
|
||||
\\item The labels consists of sequential numbers
|
||||
\\begin{itemize}
|
||||
\\item The individual entries are indicated with a black dot, a so-called bullet
|
||||
\\item The text in the entries may be of any length
|
||||
\\begin{description}
|
||||
\\item[Note:] I would like to describe something here
|
||||
\\item[Caveat!] And give a warning
|
||||
\\end{description}
|
||||
\\end{itemize}
|
||||
\\item The numbers starts at 1 with each use of the \\text{enumerate} environment
|
||||
\\end{enumerate}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
\\bibliographystyle{alpha}
|
||||
\\bibliography{sample}
|
||||
|
||||
\\end{document}`
|
||||
|
||||
export default {
|
||||
title: 'History / Document Diff Viewer',
|
||||
component: DocumentDiffViewer,
|
||||
args: { doc, highlights },
|
||||
argTypes: {
|
||||
doc: {
|
||||
table: { disable: true },
|
||||
},
|
||||
highlights: {
|
||||
table: { disable: true },
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
ScopeDecorator,
|
||||
(Story: React.ComponentType) => (
|
||||
<div style={{ height: '90vh' }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const Highlights = (
|
||||
args: React.ComponentProps<typeof DocumentDiffViewer>
|
||||
) => {
|
||||
return <DocumentDiffViewer {...args} />
|
||||
}
|
||||
|
||||
export const ScrollToFirstHighlight = (
|
||||
args: React.ComponentProps<typeof DocumentDiffViewer>
|
||||
) => {
|
||||
const lastHighlightOnly = args.highlights.slice(-1)
|
||||
return <DocumentDiffViewer {...args} highlights={lastHighlightOnly} />
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
import HistoryVersionComponent from '../../js/features/history/components/change-list/history-version'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import { HistoryProvider } from '../../js/features/history/context/history-context'
|
||||
import { disableControlsOf } from '../utils/arg-types'
|
||||
|
||||
const update = {
|
||||
fromV: 3,
|
||||
toV: 4,
|
||||
meta: {
|
||||
users: [
|
||||
{
|
||||
first_name: 'john.doe',
|
||||
last_name: '',
|
||||
email: 'john.doe@test.com',
|
||||
id: '631710ab1094c5002647184e',
|
||||
},
|
||||
],
|
||||
start_ts: 1681220036419,
|
||||
end_ts: 1681220036419,
|
||||
},
|
||||
labels: [
|
||||
{
|
||||
id: '643561cdfa2b2beac88f0024',
|
||||
comment: 'tag-1',
|
||||
version: 4,
|
||||
user_id: '123',
|
||||
created_at: '2023-04-11T13:34:05.856Z',
|
||||
user_display_name: 'john.doe',
|
||||
},
|
||||
{
|
||||
id: '643561d1fa2b2beac88f0025',
|
||||
comment: 'tag-2',
|
||||
version: 4,
|
||||
user_id: '123',
|
||||
created_at: '2023-04-11T13:34:09.280Z',
|
||||
user_display_name: 'john.doe',
|
||||
},
|
||||
],
|
||||
pathnames: [],
|
||||
project_ops: [{ add: { pathname: 'name.tex' }, atV: 3 }],
|
||||
}
|
||||
|
||||
export const HistoryVersion = (
|
||||
args: React.ComponentProps<typeof HistoryVersionComponent>
|
||||
) => {
|
||||
return (
|
||||
<HistoryProvider>
|
||||
<HistoryVersionComponent {...args} />
|
||||
</HistoryProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'History / Change list',
|
||||
component: HistoryVersionComponent,
|
||||
args: {
|
||||
update,
|
||||
currentUserId: '1',
|
||||
projectId: '123',
|
||||
comparing: false,
|
||||
faded: false,
|
||||
showDivider: false,
|
||||
selectionState: false,
|
||||
setSelection: () => {},
|
||||
dropdownOpen: false,
|
||||
dropdownActive: false,
|
||||
setActiveDropdownItem: () => {},
|
||||
closeDropdownForItem: () => {},
|
||||
},
|
||||
argTypes: {
|
||||
...disableControlsOf(
|
||||
'update',
|
||||
'currentUserId',
|
||||
'projectId',
|
||||
'setSelection',
|
||||
'dropdownOpen',
|
||||
'dropdownActive',
|
||||
'setActiveDropdownItem',
|
||||
'closeDropdownForItem'
|
||||
),
|
||||
},
|
||||
decorators: [
|
||||
ScopeDecorator,
|
||||
(Story: React.ComponentType) => (
|
||||
<div className="history-react">
|
||||
<div className="change-list">
|
||||
<div className="history-version-list-container">
|
||||
<Story />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
import LabelListItemComponent from '../../js/features/history/components/change-list/label-list-item'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import { HistoryProvider } from '../../js/features/history/context/history-context'
|
||||
import { disableControlsOf } from '../utils/arg-types'
|
||||
|
||||
const labels = [
|
||||
{
|
||||
id: '643561cdfa2b2beac88f0024',
|
||||
comment: 'tag-1',
|
||||
version: 1,
|
||||
user_id: '123',
|
||||
created_at: '2023-04-11T13:34:05.856Z',
|
||||
user_display_name: 'john.doe',
|
||||
},
|
||||
{
|
||||
id: '643561d1fa2b2beac88f0025',
|
||||
comment: 'tag-2',
|
||||
version: 1,
|
||||
user_id: '123',
|
||||
created_at: '2023-04-11T13:34:09.280Z',
|
||||
user_display_name: 'john.doe',
|
||||
},
|
||||
]
|
||||
|
||||
export const LabelVersion = (
|
||||
args: React.ComponentProps<typeof LabelListItemComponent>
|
||||
) => {
|
||||
return (
|
||||
<HistoryProvider>
|
||||
<LabelListItemComponent {...args} />
|
||||
</HistoryProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'History / Change list',
|
||||
component: LabelListItemComponent,
|
||||
args: {
|
||||
labels,
|
||||
version: 1,
|
||||
currentUserId: '1',
|
||||
projectId: '123',
|
||||
comparing: false,
|
||||
selectionState: false,
|
||||
selectable: false,
|
||||
setSelection: () => {},
|
||||
dropdownOpen: false,
|
||||
dropdownActive: false,
|
||||
setActiveDropdownItem: () => {},
|
||||
closeDropdownForItem: () => {},
|
||||
},
|
||||
argTypes: {
|
||||
...disableControlsOf(
|
||||
'labels',
|
||||
'version',
|
||||
'currentUserId',
|
||||
'projectId',
|
||||
'setSelection',
|
||||
'dropdownOpen',
|
||||
'dropdownActive',
|
||||
'setActiveDropdownItem',
|
||||
'closeDropdownForItem'
|
||||
),
|
||||
},
|
||||
decorators: [
|
||||
ScopeDecorator,
|
||||
(Story: React.ComponentType) => (
|
||||
<div className="history-react">
|
||||
<div className="change-list">
|
||||
<div className="history-version-list-container">
|
||||
<Story />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react'
|
||||
import ToggleSwitchComponent from '../../js/features/history/components/change-list/toggle-switch'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import { HistoryProvider } from '../../js/features/history/context/history-context'
|
||||
|
||||
export const HistoryAndLabelsToggleSwitch = () => {
|
||||
const [labelsOnly, setLabelsOnly] = useState(false)
|
||||
|
||||
return (
|
||||
<HistoryProvider>
|
||||
<ToggleSwitchComponent
|
||||
labelsOnly={labelsOnly}
|
||||
setLabelsOnly={setLabelsOnly}
|
||||
/>
|
||||
</HistoryProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'History / Change list',
|
||||
component: ToggleSwitchComponent,
|
||||
argTypes: {
|
||||
labelsOnly: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
setLabelsOnly: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
ScopeDecorator,
|
||||
(Story: React.ComponentType) => (
|
||||
<div className="history-react">
|
||||
<div className="change-list">
|
||||
<div className="history-header history-toggle-switch-container">
|
||||
<Story />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
19
services/web/frontend/stories/hooks/use-fetch-mock.tsx
Normal file
19
services/web/frontend/stories/hooks/use-fetch-mock.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useLayoutEffect } from 'react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
/**
|
||||
* Run callback to mock fetch routes, call removeRoutes() and unmockGlobal() when unmounted
|
||||
*/
|
||||
export default function useFetchMock(
|
||||
callback: (value: typeof fetchMock) => void
|
||||
) {
|
||||
fetchMock.mockGlobal()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
callback(fetchMock)
|
||||
return () => {
|
||||
fetchMock.removeRoutes()
|
||||
fetchMock.unmockGlobal()
|
||||
}
|
||||
}, [callback])
|
||||
}
|
10
services/web/frontend/stories/hooks/use-meta.tsx
Normal file
10
services/web/frontend/stories/hooks/use-meta.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { PartialMeta } from '@/utils/meta'
|
||||
|
||||
/**
|
||||
* Set values on window.metaAttributesCache, for use in Storybook stories
|
||||
*/
|
||||
export const useMeta = (meta: PartialMeta) => {
|
||||
for (const [key, value] of Object.entries(meta)) {
|
||||
window.metaAttributesCache.set(key as keyof PartialMeta, value)
|
||||
}
|
||||
}
|
30
services/web/frontend/stories/hooks/use-scope.tsx
Normal file
30
services/web/frontend/stories/hooks/use-scope.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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<typeof scope | null>(null)
|
||||
if (scopeRef.current === null) {
|
||||
scopeRef.current = scope
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (scopeRef.current) {
|
||||
for (const [path, value] of Object.entries(scopeRef.current)) {
|
||||
let existingValue: typeof value | undefined
|
||||
try {
|
||||
existingValue = window.overleaf.unstable.store.get(path)
|
||||
} catch {
|
||||
// allowed not to exist
|
||||
}
|
||||
if (typeof existingValue === 'object' && typeof value === 'object') {
|
||||
window.overleaf.unstable.store.set(path, merge(existingValue, value))
|
||||
} else {
|
||||
window.overleaf.unstable.store.set(path, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}
|
27
services/web/frontend/stories/hotkeys-modal.stories.jsx
Normal file
27
services/web/frontend/stories/hotkeys-modal.stories.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import HotkeysModal from '../js/features/hotkeys-modal/components/hotkeys-modal'
|
||||
|
||||
export const ReviewEnabled = args => {
|
||||
return <HotkeysModal {...args} />
|
||||
}
|
||||
|
||||
export const ReviewDisabled = args => {
|
||||
return <HotkeysModal {...args} trackChangesVisible={false} />
|
||||
}
|
||||
|
||||
export const MacModifier = args => {
|
||||
return <HotkeysModal {...args} isMac />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / Modals / Hotkeys',
|
||||
component: HotkeysModal,
|
||||
args: {
|
||||
animation: false,
|
||||
show: true,
|
||||
isMac: false,
|
||||
trackChangesVisible: true,
|
||||
},
|
||||
argTypes: {
|
||||
handleHide: { action: 'handleHide' },
|
||||
},
|
||||
}
|
50
services/web/frontend/stories/icon.stories.jsx
Normal file
50
services/web/frontend/stories/icon.stories.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Icon from '../js/shared/components/icon'
|
||||
|
||||
export const Type = args => {
|
||||
return (
|
||||
<>
|
||||
<Icon {...args} />
|
||||
<div>
|
||||
<a
|
||||
href="https://fontawesome.com/v4.7.0/icons/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Font Awesome icons
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Type.args = {
|
||||
type: 'tasks',
|
||||
}
|
||||
|
||||
export const Spinner = args => {
|
||||
return <Icon {...args} />
|
||||
}
|
||||
Spinner.args = {
|
||||
type: 'spinner',
|
||||
spin: true,
|
||||
}
|
||||
|
||||
export const FixedWidth = args => {
|
||||
return <Icon {...args} />
|
||||
}
|
||||
FixedWidth.args = {
|
||||
type: 'tasks',
|
||||
fw: true,
|
||||
}
|
||||
|
||||
export const AccessibilityLabel = args => {
|
||||
return <Icon {...args} />
|
||||
}
|
||||
AccessibilityLabel.args = {
|
||||
type: 'check',
|
||||
accessibilityLabel: 'Check',
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Icon',
|
||||
component: Icon,
|
||||
}
|
26
services/web/frontend/stories/input-switch.stories.tsx
Normal file
26
services/web/frontend/stories/input-switch.stories.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import OLFormSwitch from '@/features/ui/components/ol/ol-form-switch'
|
||||
import { disableControlsOf } from './utils/arg-types'
|
||||
|
||||
export const Unchecked = () => {
|
||||
return <OLFormSwitch onChange={() => {}} checked={false} />
|
||||
}
|
||||
|
||||
export const UncheckedDisabled = () => {
|
||||
return <OLFormSwitch onChange={() => {}} checked={false} disabled />
|
||||
}
|
||||
|
||||
export const Checked = () => {
|
||||
return <OLFormSwitch onChange={() => {}} checked />
|
||||
}
|
||||
|
||||
export const CheckedDisabled = () => {
|
||||
return <OLFormSwitch onChange={() => {}} checked disabled />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Input Switch',
|
||||
component: OLFormSwitch,
|
||||
argTypes: {
|
||||
...disableControlsOf('inputRef'),
|
||||
},
|
||||
}
|
45
services/web/frontend/stories/loading/loading.stories.tsx
Normal file
45
services/web/frontend/stories/loading/loading.stories.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { LoadingUI } from '@/features/ide-react/components/loading'
|
||||
import { EditorProviders } from '../../../test/frontend/helpers/editor-providers'
|
||||
import { PartialMeta } from '@/utils/meta'
|
||||
|
||||
const meta: Meta<typeof LoadingUI> = {
|
||||
title: 'Loading Page / Loading',
|
||||
component: LoadingUI,
|
||||
argTypes: {
|
||||
errorCode: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'',
|
||||
'io-not-loaded',
|
||||
'unable-to-join',
|
||||
'i18n-error',
|
||||
'unhandled-error-code',
|
||||
],
|
||||
},
|
||||
progress: { control: { type: 'range', min: 0, max: 100 } },
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof LoadingUI>
|
||||
|
||||
const errorMessages = {
|
||||
translationIoNotLoaded: 'Could not connect to the WebSocket server',
|
||||
translationLoadErrorMessage: 'Could not load translations',
|
||||
translationUnableToJoin: 'Could not connect to collaboration server',
|
||||
}
|
||||
|
||||
export const LoadingPage: Story = {
|
||||
render: args => {
|
||||
for (const [key, value] of Object.entries(errorMessages)) {
|
||||
window.metaAttributesCache.set(`ol-${key}` as keyof PartialMeta, value)
|
||||
}
|
||||
return (
|
||||
<EditorProviders>
|
||||
<LoadingUI {...args} />
|
||||
</EditorProviders>
|
||||
)
|
||||
},
|
||||
}
|
35
services/web/frontend/stories/menu-bar.stories.tsx
Normal file
35
services/web/frontend/stories/menu-bar.stories.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { DropdownDivider } from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import { MenuBar } from '@/shared/components/menu-bar/menu-bar'
|
||||
import { MenuBarDropdown } from '@/shared/components/menu-bar/menu-bar-dropdown'
|
||||
import { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
|
||||
import { Meta } from '@storybook/react/*'
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<MenuBar id="toolbar-menu-bar-item">
|
||||
<MenuBarDropdown title="File" id="file">
|
||||
<MenuBarOption title="New File" />
|
||||
<MenuBarOption title="New Project" />
|
||||
</MenuBarDropdown>
|
||||
<MenuBarDropdown title="Edit" id="edit">
|
||||
<MenuBarOption title="Undo" />
|
||||
<MenuBarOption title="Redo" />
|
||||
<DropdownDivider />
|
||||
<MenuBarOption title="Cut" />
|
||||
<MenuBarOption title="Copy" />
|
||||
<MenuBarOption title="Paste" />
|
||||
</MenuBarDropdown>
|
||||
<MenuBarDropdown title="View" id="view">
|
||||
<MenuBarOption title="PDF only" />
|
||||
</MenuBarDropdown>
|
||||
</MenuBar>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof MenuBar> = {
|
||||
title: 'Shared / Components / MenuBar',
|
||||
component: MenuBar,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
@@ -0,0 +1,118 @@
|
||||
import { useEffect } from 'react'
|
||||
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'
|
||||
|
||||
const defaultFileTreeContextProps = {
|
||||
refProviders: { mendeley: false, zotero: false },
|
||||
setRefProviderEnabled: provider => {
|
||||
console.log(`ref provider ${provider} enabled`)
|
||||
},
|
||||
setStartedFreeTrial: () => {
|
||||
console.log('started free trial')
|
||||
},
|
||||
initialSelectedEntityId: 'entity-1',
|
||||
onSelect: () => {
|
||||
console.log('selected')
|
||||
},
|
||||
}
|
||||
|
||||
export const mockCreateFileModalFetch = fetchMock =>
|
||||
fetchMock
|
||||
.get('path:/user/projects', {
|
||||
projects: [
|
||||
{
|
||||
_id: 'project-1',
|
||||
name: 'Project One',
|
||||
},
|
||||
{
|
||||
_id: 'project-2',
|
||||
name: 'Project Two',
|
||||
},
|
||||
],
|
||||
})
|
||||
.get('path:/mendeley/groups', {
|
||||
groups: [
|
||||
{
|
||||
id: 'group-1',
|
||||
name: 'Group One',
|
||||
},
|
||||
{
|
||||
id: 'group-2',
|
||||
name: 'Group Two',
|
||||
},
|
||||
],
|
||||
})
|
||||
.get('path:/zotero/groups', {
|
||||
groups: [
|
||||
{
|
||||
id: 'group-1',
|
||||
name: 'Group One',
|
||||
},
|
||||
{
|
||||
id: 'group-2',
|
||||
name: 'Group Two',
|
||||
},
|
||||
],
|
||||
})
|
||||
.get('express:/project/:projectId/entities', {
|
||||
entities: [
|
||||
{
|
||||
path: '/foo.tex',
|
||||
},
|
||||
{
|
||||
path: '/bar.tex',
|
||||
},
|
||||
],
|
||||
})
|
||||
.post('express:/project/:projectId/doc', (path, req) => {
|
||||
console.log({ path, req })
|
||||
return 204
|
||||
})
|
||||
.post('express:/project/:projectId/upload', (path, req) => {
|
||||
console.log({ path, req })
|
||||
return 204
|
||||
})
|
||||
.post('express:/project/:projectId/linked_file', (path, req) => {
|
||||
console.log({ path, req })
|
||||
return 204
|
||||
})
|
||||
|
||||
export const createFileModalDecorator =
|
||||
(fileTreeContextProps = {}, createMode = 'doc') =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
Story => {
|
||||
return (
|
||||
<FileTreeContext
|
||||
{...defaultFileTreeContextProps}
|
||||
{...fileTreeContextProps}
|
||||
>
|
||||
<FileTreeCreateNameProvider>
|
||||
<FileTreeCreateFormProvider>
|
||||
<OpenCreateFileModal createMode={createMode}>
|
||||
<Story />
|
||||
</OpenCreateFileModal>
|
||||
</FileTreeCreateFormProvider>
|
||||
</FileTreeCreateNameProvider>
|
||||
</FileTreeContext>
|
||||
)
|
||||
}
|
||||
|
||||
function OpenCreateFileModal({ children, createMode }) {
|
||||
const { startCreatingFile } = useFileTreeActionable()
|
||||
|
||||
useEffect(() => {
|
||||
startCreatingFile(createMode)
|
||||
}, [createMode]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
OpenCreateFileModal.propTypes = {
|
||||
createMode: PropTypes.string,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
ModalFooterDecorator,
|
||||
ModalContentDecorator,
|
||||
} from '../modal-decorators'
|
||||
import { FileTreeModalCreateFileFooterContent } from '../../../js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-footer'
|
||||
|
||||
export const Valid = args => <FileTreeModalCreateFileFooterContent {...args} />
|
||||
|
||||
export const Invalid = args => (
|
||||
<FileTreeModalCreateFileFooterContent {...args} />
|
||||
)
|
||||
Invalid.args = {
|
||||
valid: false,
|
||||
}
|
||||
|
||||
export const Inflight = args => (
|
||||
<FileTreeModalCreateFileFooterContent {...args} />
|
||||
)
|
||||
Inflight.args = {
|
||||
inFlight: true,
|
||||
}
|
||||
|
||||
export const FileLimitWarning = args => (
|
||||
<FileTreeModalCreateFileFooterContent {...args} />
|
||||
)
|
||||
FileLimitWarning.args = {
|
||||
fileCount: {
|
||||
status: 'warning',
|
||||
value: 1990,
|
||||
limit: 2000,
|
||||
},
|
||||
}
|
||||
|
||||
export const FileLimitError = args => (
|
||||
<FileTreeModalCreateFileFooterContent {...args} />
|
||||
)
|
||||
FileLimitError.args = {
|
||||
fileCount: {
|
||||
status: 'error',
|
||||
value: 2000,
|
||||
limit: 2000,
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / Modals / Create File / Footer',
|
||||
component: FileTreeModalCreateFileFooterContent,
|
||||
args: {
|
||||
fileCount: {
|
||||
status: 'success',
|
||||
limit: 10,
|
||||
value: 1,
|
||||
},
|
||||
valid: true,
|
||||
inFlight: false,
|
||||
newFileCreateMode: 'doc',
|
||||
},
|
||||
argTypes: {
|
||||
cancel: { action: 'cancel' },
|
||||
},
|
||||
decorators: [ModalFooterDecorator, ModalContentDecorator],
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
createFileModalDecorator,
|
||||
mockCreateFileModalFetch,
|
||||
} 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'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export const MinimalFeatures = args => {
|
||||
useFetchMock(mockCreateFileModalFetch)
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasLinkUrlFeature: false,
|
||||
hasLinkedProjectFileFeature: false,
|
||||
hasLinkedProjectOutputFileFeature: false,
|
||||
})
|
||||
|
||||
return <FileTreeModalCreateFile {...args} />
|
||||
}
|
||||
MinimalFeatures.decorators = [createFileModalDecorator()]
|
||||
|
||||
export const WithExtraFeatures = args => {
|
||||
useFetchMock(mockCreateFileModalFetch)
|
||||
|
||||
getMeta('ol-ExposedSettings').hasLinkUrlFeature = true
|
||||
|
||||
return <FileTreeModalCreateFile {...args} />
|
||||
}
|
||||
WithExtraFeatures.decorators = [
|
||||
createFileModalDecorator({
|
||||
refProviders: { mendeley: true, zotero: true },
|
||||
}),
|
||||
]
|
||||
|
||||
export const ErrorImportingFileFromExternalURL = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCreateFileModalFetch(fetchMock)
|
||||
|
||||
fetchMock.post('express:/project/:projectId/linked_file', 500)
|
||||
})
|
||||
|
||||
getMeta('ol-ExposedSettings').hasLinkUrlFeature = true
|
||||
|
||||
return <FileTreeModalCreateFile {...args} />
|
||||
}
|
||||
ErrorImportingFileFromExternalURL.decorators = [createFileModalDecorator()]
|
||||
|
||||
export const ErrorImportingFileFromReferenceProvider = args => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCreateFileModalFetch(fetchMock)
|
||||
|
||||
fetchMock.post('express:/project/:projectId/linked_file', 500)
|
||||
})
|
||||
|
||||
return <FileTreeModalCreateFile {...args} />
|
||||
}
|
||||
ErrorImportingFileFromReferenceProvider.decorators = [
|
||||
createFileModalDecorator({
|
||||
refProviders: { mendeley: true, zotero: true },
|
||||
}),
|
||||
]
|
||||
|
||||
export const FileLimitReached = args => {
|
||||
useFetchMock(mockCreateFileModalFetch)
|
||||
|
||||
useScope({
|
||||
project: {
|
||||
rootFolder: {
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: Array.from({ length: 10 }, (_, index) => ({
|
||||
_id: `entity-${index}`,
|
||||
})),
|
||||
fileRefs: [],
|
||||
folders: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return <FileTreeModalCreateFile {...args} />
|
||||
}
|
||||
FileLimitReached.decorators = [createFileModalDecorator()]
|
||||
|
||||
export default {
|
||||
title: 'Editor / Modals / Create File',
|
||||
component: FileTreeModalCreateFile,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
import FileTreeCreateNameInput from '../../../js/features/file-tree/components/file-tree-create/file-tree-create-name-input'
|
||||
import FileTreeCreateNameProvider from '../../../js/features/file-tree/contexts/file-tree-create-name'
|
||||
import {
|
||||
BlockedFilenameError,
|
||||
DuplicateFilenameError,
|
||||
} from '../../../js/features/file-tree/errors'
|
||||
import { ModalBodyDecorator, ModalContentDecorator } from '../modal-decorators'
|
||||
|
||||
export const DefaultLabel = args => (
|
||||
<FileTreeCreateNameProvider initialName="example.tex">
|
||||
<FileTreeCreateNameInput {...args} />
|
||||
</FileTreeCreateNameProvider>
|
||||
)
|
||||
|
||||
export const CustomLabel = args => (
|
||||
<FileTreeCreateNameProvider initialName="example.tex">
|
||||
<FileTreeCreateNameInput {...args} />
|
||||
</FileTreeCreateNameProvider>
|
||||
)
|
||||
CustomLabel.args = {
|
||||
label: 'File Name in this Project',
|
||||
}
|
||||
|
||||
export const FocusName = args => (
|
||||
<FileTreeCreateNameProvider initialName="example.tex">
|
||||
<FileTreeCreateNameInput {...args} />
|
||||
</FileTreeCreateNameProvider>
|
||||
)
|
||||
FocusName.args = {
|
||||
focusName: true,
|
||||
}
|
||||
|
||||
export const CustomPlaceholder = args => (
|
||||
<FileTreeCreateNameProvider>
|
||||
<FileTreeCreateNameInput {...args} />
|
||||
</FileTreeCreateNameProvider>
|
||||
)
|
||||
CustomPlaceholder.args = {
|
||||
placeholder: 'Enter a file name…',
|
||||
}
|
||||
|
||||
export const DuplicateError = args => (
|
||||
<FileTreeCreateNameProvider initialName="main.tex">
|
||||
<FileTreeCreateNameInput {...args} />
|
||||
</FileTreeCreateNameProvider>
|
||||
)
|
||||
DuplicateError.args = {
|
||||
error: new DuplicateFilenameError(),
|
||||
}
|
||||
|
||||
export const BlockedError = args => (
|
||||
<FileTreeCreateNameProvider initialName="main.tex">
|
||||
<FileTreeCreateNameInput {...args} />
|
||||
</FileTreeCreateNameProvider>
|
||||
)
|
||||
BlockedError.args = {
|
||||
error: new BlockedFilenameError(),
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / Modals / Create File / File Name Input',
|
||||
component: FileTreeCreateNameInput,
|
||||
decorators: [ModalBodyDecorator, ModalContentDecorator],
|
||||
args: {
|
||||
inFlight: false,
|
||||
},
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
import ErrorMessage from '../../../js/features/file-tree/components/file-tree-create/error-message'
|
||||
import { FetchError } from '../../../js/infrastructure/fetch-json'
|
||||
import {
|
||||
BlockedFilenameError,
|
||||
DuplicateFilenameError,
|
||||
InvalidFilenameError,
|
||||
} from '../../../js/features/file-tree/errors'
|
||||
|
||||
export const KeyedErrors = () => {
|
||||
return (
|
||||
<>
|
||||
<ErrorMessage error="name-exists" />
|
||||
<ErrorMessage error="too-many-files" />
|
||||
<ErrorMessage error="remote-service-error" />
|
||||
<ErrorMessage error="rate-limit-hit" />
|
||||
{/* <ErrorMessage error="not-logged-in" /> */}
|
||||
<ErrorMessage error="something-else" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const FetchStatusErrors = () => {
|
||||
return (
|
||||
<>
|
||||
<ErrorMessage
|
||||
error={
|
||||
new FetchError(
|
||||
'There was an error',
|
||||
'/story',
|
||||
{},
|
||||
new Response(null, { status: 400 })
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ErrorMessage
|
||||
error={
|
||||
new FetchError(
|
||||
'There was an error',
|
||||
'/story',
|
||||
{},
|
||||
new Response(null, { status: 403 })
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ErrorMessage
|
||||
error={
|
||||
new FetchError(
|
||||
'There was an error',
|
||||
'/story',
|
||||
{},
|
||||
new Response(null, { status: 429 })
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ErrorMessage
|
||||
error={
|
||||
new FetchError(
|
||||
'There was an error',
|
||||
'/story',
|
||||
{},
|
||||
new Response(null, { status: 500 })
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const FetchDataErrors = () => {
|
||||
return (
|
||||
<>
|
||||
<ErrorMessage
|
||||
error={
|
||||
new FetchError('Error', '/story', {}, new Response(), {
|
||||
message: 'There was an error!',
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ErrorMessage
|
||||
error={
|
||||
new FetchError('Error', '/story', {}, new Response(), {
|
||||
message: {
|
||||
text: 'There was an error with some text!',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const SpecificClassErrors = () => {
|
||||
return (
|
||||
<>
|
||||
<ErrorMessage error={new DuplicateFilenameError()} />
|
||||
<ErrorMessage error={new InvalidFilenameError()} />
|
||||
<ErrorMessage error={new BlockedFilenameError()} />
|
||||
<ErrorMessage error={new Error()} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / Modals / Create File / Error Message',
|
||||
component: ErrorMessage,
|
||||
}
|
29
services/web/frontend/stories/modals/modal-decorators.jsx
Normal file
29
services/web/frontend/stories/modals/modal-decorators.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Wrap modal content in modal classes, without modal behaviours
|
||||
*/
|
||||
|
||||
export function ModalContentDecorator(Story) {
|
||||
return (
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<Story />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModalBodyDecorator(Story) {
|
||||
return (
|
||||
<div className="modal-body">
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModalFooterDecorator(Story) {
|
||||
return (
|
||||
<div className="modal-footer">
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { UnsavedDocsLockedAlert } from '@/features/ide-react/components/unsaved-docs/unsaved-docs-locked-alert'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
|
||||
export default {
|
||||
title: 'Editor / Modals / Unsaved Docs Locked',
|
||||
component: UnsavedDocsLockedAlert,
|
||||
decorators: [Story => ScopeDecorator(Story)],
|
||||
} satisfies Meta
|
||||
|
||||
type Story = StoryObj<typeof UnsavedDocsLockedAlert>
|
||||
|
||||
export const Locked: Story = {}
|
383
services/web/frontend/stories/notification.stories.tsx
Normal file
383
services/web/frontend/stories/notification.stories.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
import fetchMock from 'fetch-mock'
|
||||
import Notification from '../js/shared/components/notification'
|
||||
import { postJSON } from '../js/infrastructure/fetch-json'
|
||||
import useAsync from '../js/shared/hooks/use-async'
|
||||
|
||||
type Args = React.ComponentProps<typeof Notification>
|
||||
|
||||
export const NotificationInfo = (args: Args) => {
|
||||
return <Notification {...args} isDismissible />
|
||||
}
|
||||
|
||||
export const NotificationSuccess = (args: Args) => {
|
||||
return <Notification {...args} isDismissible type="success" />
|
||||
}
|
||||
|
||||
export const NotificationWarning = (args: Args) => {
|
||||
return <Notification {...args} isDismissible type="warning" />
|
||||
}
|
||||
|
||||
export const NotificationError = (args: Args) => {
|
||||
return <Notification {...args} isDismissible type="error" />
|
||||
}
|
||||
|
||||
export const NotificationWithActionBelowContent = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
content={
|
||||
<div>
|
||||
<p>The CTA will always go below the content on small screens.</p>
|
||||
<p>
|
||||
We can also opt to always put the CTA below the content on all
|
||||
screens
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
isDismissible
|
||||
isActionBelowContent
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const NotificationWithTitle = (args: Args) => {
|
||||
return <Notification {...args} title="Some title" />
|
||||
}
|
||||
|
||||
export const NotificationWithAction = (args: Args) => {
|
||||
return <Notification {...args} isDismissible={false} />
|
||||
}
|
||||
|
||||
export const NotificationDismissible = (args: Args) => {
|
||||
return <Notification {...args} action={undefined} />
|
||||
}
|
||||
|
||||
export const APlainNotification = (args: Args) => {
|
||||
return <Notification {...args} action={undefined} isDismissible={false} />
|
||||
}
|
||||
|
||||
export const NotificationWithMultipleParagraphsAndActionAndDismissible = (
|
||||
args: Args
|
||||
) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
content={
|
||||
<div>
|
||||
<p>
|
||||
<b>Lorem ipsum</b>
|
||||
</p>
|
||||
<p>
|
||||
Dolor sit amet, consectetur adipiscing elit. Proin lacus velit,
|
||||
faucibus vitae feugiat sit amet, <a href="/">Some link</a> iaculis
|
||||
ut mi.
|
||||
</p>
|
||||
<p>
|
||||
Vel eros donec ac odio tempor orci dapibus ultrices in. Fermentum
|
||||
iaculis eu non diam phasellus.
|
||||
</p>
|
||||
<p>Aliquam at tempor risus. Vestibulum bibendum ut </p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const NotificationWithMultipleParagraphsAndDismissible = (
|
||||
args: Args
|
||||
) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
action={undefined}
|
||||
content={
|
||||
<div>
|
||||
<p>
|
||||
<b>Lorem ipsum</b>
|
||||
</p>
|
||||
<p>
|
||||
Dolor sit amet, consectetur adipiscing elit. Proin lacus velit,
|
||||
faucibus vitae feugiat sit amet, <a href="/">Some link</a> iaculis
|
||||
ut mi.
|
||||
</p>
|
||||
<p>
|
||||
Vel eros donec ac odio tempor orci dapibus ultrices in. Fermentum
|
||||
iaculis eu non diam phasellus.
|
||||
</p>
|
||||
<p>Aliquam at tempor risus. Vestibulum bibendum ut </p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const MultipleParagraphsAndAction = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
isDismissible={false}
|
||||
content={
|
||||
<div>
|
||||
<p>
|
||||
<b>Lorem ipsum</b>
|
||||
</p>
|
||||
<p>
|
||||
Dolor sit amet, consectetur adipiscing elit. Proin lacus velit,
|
||||
faucibus vitae feugiat sit amet, <a href="/">Some link</a> iaculis
|
||||
ut mi.
|
||||
</p>
|
||||
<p>
|
||||
Vel eros donec ac odio tempor orci dapibus ultrices in. Fermentum
|
||||
iaculis eu non diam phasellus.
|
||||
</p>
|
||||
<p>Aliquam at tempor risus. Vestibulum bibendum ut </p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const MultipleParagraphs = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
action={undefined}
|
||||
isDismissible={false}
|
||||
content={
|
||||
<div>
|
||||
<p>
|
||||
<b>Lorem ipsum</b>
|
||||
</p>
|
||||
<p>
|
||||
Dolor sit amet, consectetur adipiscing elit. Proin lacus velit,
|
||||
faucibus vitae feugiat sit amet, <a href="/">Some link</a> iaculis
|
||||
ut mi.
|
||||
</p>
|
||||
<p>
|
||||
Vel eros donec ac odio tempor orci dapibus ultrices in. Fermentum
|
||||
iaculis eu non diam phasellus.
|
||||
</p>
|
||||
<p>Aliquam at tempor risus. Vestibulum bibendum ut </p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ShortText = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
action={undefined}
|
||||
isDismissible={false}
|
||||
content={<p>Lorem ipsum</p>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ShortTextAndDismissible = (args: Args) => {
|
||||
return (
|
||||
<Notification {...args} action={undefined} content={<p>Lorem ipsum</p>} />
|
||||
)
|
||||
}
|
||||
|
||||
export const ShortTextAndActionLinkAsButton = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
isDismissible={false}
|
||||
content={<p>Lorem ipsum</p>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ShortTextAndActionAsLink = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
content={<p>Lorem ipsum</p>}
|
||||
action={<a href="/">An action</a>}
|
||||
isDismissible={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export const ShortTextAndActionAsLinkButStyledAsButton = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
content={<p>Lorem ipsum</p>}
|
||||
action={
|
||||
<a href="/" className="btn btn-secondary btn-sm">
|
||||
An action
|
||||
</a>
|
||||
}
|
||||
isDismissible={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const LongActionButton = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
action={
|
||||
<button className="btn btn-secondary btn-sm">
|
||||
Action that has a lot of text
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const LongActionLink = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
action={<a href="/">Action that has a lot of text</a>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const CustomIcon = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
customIcon={<div style={{ marginTop: '-4px' }}>🎉</div>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const MultipleButtons = (args: Args) => {
|
||||
return (
|
||||
<Notification
|
||||
{...args}
|
||||
content={<p>Lorem ipsum</p>}
|
||||
action={
|
||||
<>
|
||||
<button className="btn btn-secondary btn-sm">button1</button>
|
||||
<button className="btn btn-secondary btn-sm">button2</button>
|
||||
</>
|
||||
}
|
||||
type="info"
|
||||
isActionBelowContent
|
||||
isDismissible
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const OverlayedWithCustomClass = (args: Args) => {
|
||||
return (
|
||||
<>
|
||||
<Notification
|
||||
{...args}
|
||||
content={
|
||||
<p>
|
||||
This can be <b>any HTML</b> passed to the component. For example,
|
||||
paragraphs, headers, <code>code samples</code>,{' '}
|
||||
<a href="/">links</a>, etc are all supported.
|
||||
</p>
|
||||
}
|
||||
className="ol-overlay"
|
||||
action={
|
||||
<>
|
||||
<button className="btn btn-secondary btn-sm">button1</button>
|
||||
<button className="btn btn-secondary btn-sm">button2</button>
|
||||
</>
|
||||
}
|
||||
type="info"
|
||||
isActionBelowContent
|
||||
isDismissible
|
||||
/>
|
||||
<div>
|
||||
<p>we need filler content, so here are some jokes</p>
|
||||
<ul>
|
||||
<li>Did you hear about the circus fire? It was in tents!</li>
|
||||
<li>How do you catch a squirrel? Climb a tree and act like a nut!</li>
|
||||
<li>
|
||||
Did you hear about the guy who invented Lifesavers? They say he made
|
||||
a mint!
|
||||
</li>
|
||||
<li>
|
||||
Why couldn't the bicycle stand up by itself? It was two tired.
|
||||
</li>
|
||||
<li>
|
||||
did one hat say to the other?" "Stay here! I'm going on ahead.
|
||||
</li>
|
||||
<li>
|
||||
Why did Billy get fired from the banana factory? He kept throwing
|
||||
away the bent ones.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const SuccessFlow = (args: Args) => {
|
||||
console.log('.....render')
|
||||
fetchMock.post('express:/test-success', { status: 200 }, { delay: 250 })
|
||||
|
||||
const { isLoading, isSuccess, runAsync } = useAsync()
|
||||
function handleClick() {
|
||||
console.log('clicked')
|
||||
runAsync(postJSON('/test-success')).catch(console.error)
|
||||
}
|
||||
|
||||
const ctaText = isLoading ? 'Processing' : 'Click'
|
||||
const action = (
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => handleClick()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{ctaText}
|
||||
</button>
|
||||
)
|
||||
|
||||
const startNotification = (
|
||||
<Notification
|
||||
{...args}
|
||||
action={action}
|
||||
title="An example notification flow"
|
||||
content={
|
||||
<p>
|
||||
This story shows 2 notifications, and it's up to the parent component
|
||||
to determine which to show. There's a successful request made after
|
||||
clicking the action and so the parent component then renders the
|
||||
success notification.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
)
|
||||
const successNotification = (
|
||||
<Notification
|
||||
{...args}
|
||||
action={<a href="/">Now follow this link to go home</a>}
|
||||
type="success"
|
||||
content={<p>Success! You made a successful request.</p>}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isSuccess) return successNotification
|
||||
return startNotification
|
||||
}
|
||||
|
||||
export const ContentAsAString = (args: Args) => {
|
||||
return <Notification {...args} content="An alert" />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Notification',
|
||||
component: Notification,
|
||||
args: {
|
||||
content: (
|
||||
<p>
|
||||
This can be <b>any HTML</b> passed to the component. For example,
|
||||
paragraphs, headers, <code>code samples</code>, <a href="/">links</a>,
|
||||
etc are all supported.
|
||||
</p>
|
||||
),
|
||||
action: <button className="btn btn-secondary btn-sm">An action</button>,
|
||||
isDismissible: true,
|
||||
},
|
||||
}
|
61
services/web/frontend/stories/outline.stories.jsx
Normal file
61
services/web/frontend/stories/outline.stories.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import OutlinePane from '../js/features/outline/components/outline-pane'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
|
||||
export const Basic = args => <OutlinePane {...args} />
|
||||
Basic.args = {
|
||||
outline: [{ line: 1, title: 'Hello', level: 1 }],
|
||||
}
|
||||
|
||||
export const Nested = args => <OutlinePane {...args} />
|
||||
Nested.args = {
|
||||
outline: [
|
||||
{
|
||||
line: 1,
|
||||
title: 'Section',
|
||||
level: 1,
|
||||
children: [
|
||||
{
|
||||
line: 2,
|
||||
title: 'Subsection',
|
||||
level: 2,
|
||||
children: [
|
||||
{
|
||||
line: 3,
|
||||
title: 'Subsubsection',
|
||||
level: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const NoSections = args => <OutlinePane {...args} />
|
||||
NoSections.args = {}
|
||||
|
||||
export const NonTexFile = args => <OutlinePane {...args} />
|
||||
NonTexFile.args = {
|
||||
isTexFile: false,
|
||||
}
|
||||
|
||||
export const PartialResult = args => <OutlinePane {...args} />
|
||||
PartialResult.args = {
|
||||
isPartial: true,
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / Outline',
|
||||
component: OutlinePane,
|
||||
argTypes: {
|
||||
jumpToLine: { action: 'jumpToLine' },
|
||||
onToggle: { action: 'onToggle' },
|
||||
toggleExpanded: { action: 'toggleExpanded' },
|
||||
},
|
||||
args: {
|
||||
isTexFile: true,
|
||||
outline: [],
|
||||
expanded: true,
|
||||
},
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
19
services/web/frontend/stories/pagination.stories.jsx
Normal file
19
services/web/frontend/stories/pagination.stories.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import Pagination from '../js/shared/components/pagination'
|
||||
|
||||
export const Interactive = args => {
|
||||
return <Pagination {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Pagination',
|
||||
component: Pagination,
|
||||
args: {
|
||||
currentPage: 1,
|
||||
totalPages: 10,
|
||||
handlePageClick: () => {},
|
||||
},
|
||||
argTypes: {
|
||||
currentPage: { control: { type: 'number', min: 1, max: 10, step: 1 } },
|
||||
totalPages: { control: { disable: true } },
|
||||
},
|
||||
}
|
74
services/web/frontend/stories/pdf-log-entry.stories.tsx
Normal file
74
services/web/frontend/stories/pdf-log-entry.stories.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import PdfLogEntry from '@/features/pdf-preview/components/pdf-log-entry'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { ruleIds } from '@/ide/human-readable-logs/HumanReadableLogsHints'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
import { useMeta } from './hooks/use-meta'
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { useScope } from './hooks/use-scope'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { LogEntry } from '@/features/pdf-preview/util/types'
|
||||
|
||||
const fakeSourceLocation = {
|
||||
file: 'file.tex',
|
||||
line: 12,
|
||||
column: 5,
|
||||
}
|
||||
|
||||
const fakeLogEntry: LogEntry = {
|
||||
key: 'fake',
|
||||
ruleId: 'hint_misplaced_alignment_tab_character',
|
||||
message: 'Fake message',
|
||||
messageComponent: 'Fake message component',
|
||||
content: 'Fake content',
|
||||
type: 'Error: ',
|
||||
level: 'error',
|
||||
contentDetails: ['Fake detail 1', 'Fake detail 2'],
|
||||
file: 'fake.tex',
|
||||
line: 12,
|
||||
column: 5,
|
||||
raw: 'Fake raw',
|
||||
}
|
||||
|
||||
const fakeArgs = {
|
||||
headerTitle: 'PDF Preview',
|
||||
formattedContent: 'This is a log entry',
|
||||
level: 'error' as const,
|
||||
extraInfoURL: 'https://example.com',
|
||||
showCloseButton: true,
|
||||
showSourceLocationLink: true,
|
||||
rawContent: 'This is a raw log entry',
|
||||
contentDetails: ['detail 1', 'detail 2'],
|
||||
ruleId: 'hint_misplaced_alignment_tab_character' as const,
|
||||
sourceLocation: fakeSourceLocation,
|
||||
logEntry: fakeLogEntry,
|
||||
logType: 'Fake type',
|
||||
}
|
||||
|
||||
const meta: Meta<typeof PdfLogEntry> = {
|
||||
title: 'Editor / PDF Preview / Logs',
|
||||
component: PdfLogEntry,
|
||||
// @ts-ignore
|
||||
decorators: [ScopeDecorator],
|
||||
argTypes: {
|
||||
ruleId: { control: 'select', options: [...ruleIds, 'other'] },
|
||||
},
|
||||
args: fakeArgs,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof PdfLogEntry>
|
||||
|
||||
const Provider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
useMeta({ 'ol-showAiErrorAssistant': true })
|
||||
useScope({ 'editor.view': new EditorView({ doc: '\\begin{document' }) })
|
||||
return <div className="logs-pane p-2">{children}</div>
|
||||
}
|
||||
|
||||
export const PdfLogEntryWithControls: Story = {
|
||||
render: args => (
|
||||
<Provider>
|
||||
<PdfLogEntry {...args} />
|
||||
</Provider>
|
||||
),
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
import PdfPreviewErrorBoundaryFallback from '../js/features/pdf-preview/components/pdf-preview-error-boundary-fallback'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
|
||||
export default {
|
||||
title: 'Editor / PDF Preview / Error Boundary',
|
||||
component: PdfPreviewErrorBoundaryFallback,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
||||
|
||||
export const PreviewErrorBoundary = () => {
|
||||
return <PdfPreviewErrorBoundaryFallback type="preview" />
|
||||
}
|
||||
|
||||
export const PdfErrorBoundary = () => {
|
||||
return <PdfPreviewErrorBoundaryFallback type="pdf" />
|
||||
}
|
||||
|
||||
export const LogsErrorBoundary = () => {
|
||||
return <PdfPreviewErrorBoundaryFallback type="logs" />
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
import { useLocalCompileContext } from '../js/shared/context/local-compile-context'
|
||||
import { useEffect } from 'react'
|
||||
import { PdfPreviewMessages } from '../js/features/pdf-preview/components/pdf-preview-messages'
|
||||
import CompileTimeWarningUpgradePrompt from '@/features/pdf-preview/components/compile-time-warning-upgrade-prompt'
|
||||
|
||||
export default {
|
||||
title: 'Editor / PDF Preview / Messages',
|
||||
component: PdfPreviewMessages,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
||||
|
||||
export const Dismissible = () => {
|
||||
const { setShowCompileTimeWarning } = useLocalCompileContext()
|
||||
|
||||
useEffect(() => {
|
||||
setShowCompileTimeWarning(true)
|
||||
}, [setShowCompileTimeWarning])
|
||||
|
||||
return (
|
||||
<div style={{ width: 800, position: 'relative' }}>
|
||||
<PdfPreviewMessages>
|
||||
<CompileTimeWarningUpgradePrompt />
|
||||
</PdfPreviewMessages>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
import { PdfPreviewMessages } from '@/features/pdf-preview/components/pdf-preview-messages'
|
||||
import { CompileTimeWarningUpgradePromptInner } from '@/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner'
|
||||
|
||||
export default {
|
||||
title: 'Editor / PDF Preview / Messages',
|
||||
component: PdfPreviewMessages,
|
||||
decorators: [
|
||||
ScopeDecorator,
|
||||
(Story: any) => (
|
||||
<div style={{ width: 800, position: 'relative' }}>
|
||||
<PdfPreviewMessages>
|
||||
<Story />
|
||||
</PdfPreviewMessages>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const CompileTimeoutWarningActive = (args: any) => {
|
||||
return <CompileTimeWarningUpgradePromptInner {...args} />
|
||||
}
|
||||
CompileTimeoutWarningActive.argTypes = {
|
||||
handleDismissWarning: { action: 'dismiss warning' },
|
||||
}
|
375
services/web/frontend/stories/pdf-preview.stories.jsx
Normal file
375
services/web/frontend/stories/pdf-preview.stories.jsx
Normal file
@@ -0,0 +1,375 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import PdfPreviewPane from '../js/features/pdf-preview/components/pdf-preview-pane'
|
||||
import PdfPreview from '../js/features/pdf-preview/components/pdf-preview'
|
||||
import PdfFileList from '../js/features/pdf-preview/components/pdf-file-list'
|
||||
import { buildFileList } from '../js/features/pdf-preview/util/file-list'
|
||||
import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
|
||||
import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
|
||||
import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
|
||||
import { useDetachCompileContext as useCompileContext } from '../js/shared/context/detach-compile-context'
|
||||
import {
|
||||
dispatchDocChanged,
|
||||
mockBuildFile,
|
||||
mockClearCache,
|
||||
mockCompile,
|
||||
mockCompileError,
|
||||
mockCompileValidationIssues,
|
||||
mockEventTracking,
|
||||
outputFiles,
|
||||
} from './fixtures/compile'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
|
||||
export default {
|
||||
title: 'Editor / PDF Preview',
|
||||
component: PdfPreview,
|
||||
subcomponents: {
|
||||
PdfPreviewHybridToolbar,
|
||||
PdfFileList,
|
||||
PdfPreviewError,
|
||||
},
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
||||
|
||||
export const Interactive = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock)
|
||||
mockBuildFile(fetchMock)
|
||||
mockClearCache(fetchMock)
|
||||
})
|
||||
|
||||
const Inner = () => {
|
||||
const context = useCompileContext()
|
||||
|
||||
const { setHasLintingError } = context
|
||||
|
||||
const toggleLintingError = useCallback(() => {
|
||||
setHasLintingError(value => !value)
|
||||
}, [setHasLintingError])
|
||||
|
||||
const values = useMemo(() => {
|
||||
const entries = Object.entries(context).sort((a, b) => {
|
||||
return a[0].localeCompare(b[0])
|
||||
})
|
||||
|
||||
const values = { boolean: [], other: [] }
|
||||
|
||||
for (const entry of entries) {
|
||||
const type = typeof entry[1]
|
||||
|
||||
if (type === 'boolean') {
|
||||
values.boolean.push(entry)
|
||||
} else if (type !== 'function') {
|
||||
values.other.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}, [context])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
background: 'white',
|
||||
float: 'left',
|
||||
zIndex: 10,
|
||||
position: 'absolute',
|
||||
top: 60,
|
||||
bottom: 60,
|
||||
right: 20,
|
||||
left: 400,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0px 2px 5px #ccc',
|
||||
borderRadius: 3,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
fontSize: 14,
|
||||
gap: 20,
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ height: '100%', overflow: 'auto', flexShrink: 0 }}>
|
||||
<table>
|
||||
<tbody>
|
||||
{values.boolean.map(([key, value]) => {
|
||||
return (
|
||||
<tr key={key} style={{ border: '1px solid #ddd' }}>
|
||||
<td style={{ padding: 5 }}>{value ? '🟢' : '🔴'}</td>
|
||||
<th style={{ padding: 5 }}>{key}</th>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
margin: '10px 0',
|
||||
}}
|
||||
>
|
||||
<OLButton onClick={dispatchDocChanged}>
|
||||
trigger doc change
|
||||
</OLButton>
|
||||
<OLButton onClick={toggleLintingError}>
|
||||
toggle linting error
|
||||
</OLButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||
<table
|
||||
style={{
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<tbody>
|
||||
{values.other.map(([key, value]) => {
|
||||
return (
|
||||
<tr
|
||||
key={key}
|
||||
style={{
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
>
|
||||
<th
|
||||
style={{
|
||||
verticalAlign: 'top',
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</th>
|
||||
<td
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
margin: '0 10px',
|
||||
fontSize: 10,
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreviewPane />
|
||||
<Inner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const compileStatuses = [
|
||||
'autocompile-backoff',
|
||||
'clear-cache',
|
||||
'clsi-maintenance',
|
||||
'compile-in-progress',
|
||||
'exited',
|
||||
'failure',
|
||||
'generic',
|
||||
'project-too-large',
|
||||
'rate-limited',
|
||||
'success',
|
||||
'terminated',
|
||||
'timedout',
|
||||
'too-recently-compiled',
|
||||
'unavailable',
|
||||
'validation-problems',
|
||||
'foo',
|
||||
]
|
||||
|
||||
export const CompileError = () => {
|
||||
const [status, setStatus] = useState('success')
|
||||
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompileError(fetchMock, status, 0)
|
||||
mockBuildFile(fetchMock)
|
||||
})
|
||||
|
||||
const Inner = () => {
|
||||
const { startCompile } = useCompileContext()
|
||||
|
||||
const handleStatusChange = useCallback(
|
||||
event => {
|
||||
setStatus(event.target.value)
|
||||
window.setTimeout(() => {
|
||||
startCompile()
|
||||
}, 0)
|
||||
},
|
||||
[startCompile]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
background: 'white',
|
||||
padding: 10,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
{'status: '}
|
||||
<select value={status} onInput={handleStatusChange}>
|
||||
{compileStatuses.map(status => (
|
||||
<option key={status}>{status}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PdfPreviewPane />
|
||||
<Inner />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const compileErrors = [
|
||||
'autocompile-backoff',
|
||||
'clear-cache',
|
||||
'clsi-maintenance',
|
||||
'compile-in-progress',
|
||||
'exited',
|
||||
'failure',
|
||||
'generic',
|
||||
'project-too-large',
|
||||
'rate-limited',
|
||||
'success',
|
||||
'terminated',
|
||||
'timedout',
|
||||
'too-recently-compiled',
|
||||
'unavailable',
|
||||
'validation-problems',
|
||||
'foo',
|
||||
]
|
||||
|
||||
export const DisplayError = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="logs-pane">
|
||||
{compileErrors.map(error => (
|
||||
<div
|
||||
key={error}
|
||||
style={{ background: '#5d6879', padding: 10, margin: 5 }}
|
||||
>
|
||||
<div style={{ fontFamily: 'monospace', color: 'white' }}>{error}</div>
|
||||
<PdfPreviewError error={error} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const HybridToolbar = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock, 500)
|
||||
mockBuildFile(fetchMock)
|
||||
mockEventTracking(fetchMock)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="pdf">
|
||||
<PdfPreviewHybridToolbar />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FileList = () => {
|
||||
const fileList = useMemo(() => {
|
||||
return buildFileList(cloneDeep(outputFiles))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownMenu id="dropdown-files-logs-pane-list" show>
|
||||
<PdfFileList fileList={fileList} />
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export const Logs = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompileError(fetchMock, 400, 0)
|
||||
mockBuildFile(fetchMock)
|
||||
mockClearCache(fetchMock)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="pdf">
|
||||
<PdfPreviewProvider>
|
||||
<PdfLogsViewer />
|
||||
</PdfPreviewProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const validationProblems = {
|
||||
sizeCheck: {
|
||||
resources: [
|
||||
{ path: 'foo/bar', kbSize: 76221 },
|
||||
{ path: 'bar/baz', kbSize: 2342 },
|
||||
],
|
||||
},
|
||||
mainFile: true,
|
||||
conflictedPaths: [
|
||||
{
|
||||
path: 'foo/bar',
|
||||
},
|
||||
{
|
||||
path: 'foo/baz',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const ValidationIssues = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompileValidationIssues(fetchMock, validationProblems, 0)
|
||||
mockBuildFile(fetchMock)
|
||||
})
|
||||
|
||||
return <PdfPreviewPane />
|
||||
}
|
50
services/web/frontend/stories/pdf-viewer.stories.jsx
Normal file
50
services/web/frontend/stories/pdf-viewer.stories.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import PdfSynctexControls from '../js/features/pdf-preview/components/pdf-synctex-controls'
|
||||
import PdfViewer from '../js/features/pdf-preview/components/pdf-viewer'
|
||||
import {
|
||||
mockBuildFile,
|
||||
mockCompile,
|
||||
mockSynctex,
|
||||
mockValidPdf,
|
||||
} from './fixtures/compile'
|
||||
import { useEffect, Suspense } from 'react'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
|
||||
export default {
|
||||
title: 'Editor / PDF Viewer',
|
||||
component: PdfViewer,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
||||
|
||||
export const Interactive = () => {
|
||||
useFetchMock(fetchMock => {
|
||||
mockCompile(fetchMock)
|
||||
mockBuildFile(fetchMock)
|
||||
mockValidPdf(fetchMock)
|
||||
mockSynctex(fetchMock)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(`cursor:editor:update`, {
|
||||
detail: { row: 10, position: 10 },
|
||||
})
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pdf-viewer">
|
||||
<Suspense fallback={null}>
|
||||
<PdfPreviewProvider>
|
||||
<PdfViewer />
|
||||
</PdfPreviewProvider>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div style={{ position: 'absolute', top: 150, left: 50 }}>
|
||||
<PdfSynctexControls />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
import AddAffiliation from '../../js/features/project-list/components/add-affiliation'
|
||||
import { ProjectListProvider } from '../../js/features/project-list/context/project-list-context'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import { projectsData } from '../../../test/frontend/features/project-list/fixtures/projects-data'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export const Add = (args: any) => {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
isOverleaf: true,
|
||||
})
|
||||
window.metaAttributesCache.set('ol-userAffiliations', [])
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/api\/project/, {
|
||||
projects: projectsData,
|
||||
totalSize: projectsData.length,
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<AddAffiliation {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / Affiliation',
|
||||
component: AddAffiliation,
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
import { ColorPicker } from '../../js/features/project-list/components/color-picker/color-picker'
|
||||
import { ColorPickerProvider } from '../../js/features/project-list/context/color-picker-context'
|
||||
|
||||
export const Select = (args: any) => {
|
||||
return (
|
||||
<ColorPickerProvider>
|
||||
<ColorPicker {...args} />
|
||||
</ColorPickerProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / Color Picker',
|
||||
component: ColorPicker,
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
import ProjectListTable from '../../js/features/project-list/components/table/project-list-table'
|
||||
import { ProjectListProvider } from '../../js/features/project-list/context/project-list-context'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import { projectsData } from '../../../test/frontend/features/project-list/fixtures/projects-data'
|
||||
|
||||
export const Successful = (args: any) => {
|
||||
window.metaAttributesCache.set('ol-user_id', '624333f147cfd8002622a1d3')
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/api\/project/, {
|
||||
projects: projectsData,
|
||||
totalSize: projectsData.length,
|
||||
})
|
||||
fetchMock.post(
|
||||
/\/compile/,
|
||||
{
|
||||
status: 'success',
|
||||
compileGroup: 'standard',
|
||||
clsiServerId: 'server-1',
|
||||
outputFiles: [{ path: 'output.pdf', build: '123-321' }],
|
||||
},
|
||||
{
|
||||
delay: 1_000,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<ProjectListTable {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const Failure = (args: any) => {
|
||||
window.metaAttributesCache.set('ol-user_id', '624333f147cfd8002622a1d3')
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/api\/project/, {
|
||||
projects: projectsData,
|
||||
totalSize: projectsData.length,
|
||||
})
|
||||
fetchMock.post(
|
||||
/\/compile/,
|
||||
{ status: 'failure', outputFiles: [] },
|
||||
{ delay: 1_000 }
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<ProjectListTable {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / PDF download',
|
||||
component: ProjectListTable,
|
||||
decorators: [
|
||||
(Story: any) => (
|
||||
<div className="project-list-react">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
import CurrentPlanWidget from '../../js/features/project-list/components/current-plan-widget/current-plan-widget'
|
||||
|
||||
export const FreePlan = (args: any) => {
|
||||
window.metaAttributesCache.set('ol-usersBestSubscription', {
|
||||
type: 'free',
|
||||
})
|
||||
|
||||
return <CurrentPlanWidget {...args} />
|
||||
}
|
||||
|
||||
export const PaidPlanTrialLastDay = (args: any) => {
|
||||
window.metaAttributesCache.set('ol-usersBestSubscription', {
|
||||
type: 'individual',
|
||||
remainingTrialDays: 1,
|
||||
plan: {
|
||||
name: 'Individual',
|
||||
},
|
||||
subscription: {
|
||||
name: 'Example Name',
|
||||
},
|
||||
})
|
||||
|
||||
return <CurrentPlanWidget {...args} />
|
||||
}
|
||||
|
||||
export const PaidPlanRemainingDays = (args: any) => {
|
||||
window.metaAttributesCache.set('ol-usersBestSubscription', {
|
||||
type: 'individual',
|
||||
remainingTrialDays: 5,
|
||||
plan: {
|
||||
name: 'Individual',
|
||||
},
|
||||
subscription: {
|
||||
name: 'Example Name',
|
||||
},
|
||||
})
|
||||
|
||||
return <CurrentPlanWidget {...args} />
|
||||
}
|
||||
|
||||
export const PaidPlanActive = (args: any) => {
|
||||
window.metaAttributesCache.set('ol-usersBestSubscription', {
|
||||
type: 'individual',
|
||||
plan: {
|
||||
name: 'Individual',
|
||||
},
|
||||
subscription: {
|
||||
name: 'Example Name',
|
||||
},
|
||||
})
|
||||
|
||||
return <CurrentPlanWidget {...args} />
|
||||
}
|
||||
|
||||
export const PausedPlan = (args: any) => {
|
||||
window.metaAttributesCache.set('ol-usersBestSubscription', {
|
||||
type: 'individual',
|
||||
plan: {
|
||||
name: 'Individual',
|
||||
},
|
||||
subscription: {
|
||||
name: 'Example Name',
|
||||
recurlyStatus: {
|
||||
state: 'paused',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return <CurrentPlanWidget {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / Current Plan Widget',
|
||||
component: CurrentPlanWidget,
|
||||
}
|
128
services/web/frontend/stories/project-list/helpers/emails.ts
Normal file
128
services/web/frontend/stories/project-list/helpers/emails.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { merge, cloneDeep } from 'lodash'
|
||||
import { type FetchMock } from 'fetch-mock'
|
||||
import { UserEmailData } from '../../../../types/user-email'
|
||||
import {
|
||||
Institution,
|
||||
Notification,
|
||||
} from '../../../../types/project/dashboard/notification'
|
||||
import { DeepPartial, DeepReadonly } from '../../../../types/utils'
|
||||
import { Project } from '../../../../types/project/dashboard/api'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
const MOCK_DELAY = 1000
|
||||
|
||||
const fakeInstitutionData = {
|
||||
email: 'email@example.com',
|
||||
institutionEmail: 'institution@example.com',
|
||||
institutionId: 123,
|
||||
institutionName: 'Abc Institution',
|
||||
requestedEmail: 'requested@example.com',
|
||||
} as DeepReadonly<Institution>
|
||||
|
||||
export const fakeReconfirmationUsersData = {
|
||||
affiliation: {
|
||||
institution: {
|
||||
ssoEnabled: false,
|
||||
ssoBeta: false,
|
||||
name: 'Abc Institution',
|
||||
},
|
||||
},
|
||||
samlProviderId: 'Saml Provider',
|
||||
email: 'reconfirmation-email@overleaf.com',
|
||||
default: false,
|
||||
} as DeepReadonly<UserEmailData>
|
||||
|
||||
export function defaultSetupMocks(fetchMock: FetchMock) {
|
||||
// at least one project is required to show some notifications
|
||||
const projects = [{}] as Project[]
|
||||
fetchMock.post(/\/api\/project/, {
|
||||
status: 200,
|
||||
body: {
|
||||
projects,
|
||||
totalSize: projects.length,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setDefaultMeta() {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
emailConfirmationDisabled: false,
|
||||
samlInitPath: '/fakeSaml',
|
||||
appName: 'Overleaf',
|
||||
})
|
||||
window.metaAttributesCache.set('ol-notificationsInstitution', [])
|
||||
window.metaAttributesCache.set('ol-userEmails', [])
|
||||
}
|
||||
|
||||
export function errorsMocks(fetchMock: FetchMock) {
|
||||
defaultSetupMocks(fetchMock)
|
||||
fetchMock.post(/\/user\/emails\/*/, 500, { delay: MOCK_DELAY })
|
||||
fetchMock.post(
|
||||
/\/project\/[A-Za-z0-9]+\/invite\/token\/[A-Za-z0-9]+\/accept/,
|
||||
500,
|
||||
{ delay: MOCK_DELAY }
|
||||
)
|
||||
fetchMock.post(/\/user\/emails\/send-reconfirmation/, 500, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
}
|
||||
|
||||
export function setInstitutionMeta(institutionData: Partial<Institution>) {
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set('ol-notificationsInstitution', [
|
||||
merge(cloneDeep(fakeInstitutionData), institutionData),
|
||||
])
|
||||
}
|
||||
|
||||
export function institutionSetupMocks(fetchMock: FetchMock) {
|
||||
defaultSetupMocks(fetchMock)
|
||||
fetchMock.delete(/\/notifications\/*/, 200, { delay: MOCK_DELAY })
|
||||
}
|
||||
|
||||
export function setCommonMeta(notificationData: DeepPartial<Notification>) {
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set('ol-notifications', [notificationData])
|
||||
}
|
||||
|
||||
export function commonSetupMocks(fetchMock: FetchMock) {
|
||||
defaultSetupMocks(fetchMock)
|
||||
fetchMock.post(
|
||||
/\/project\/[A-Za-z0-9]+\/invite\/token\/[A-Za-z0-9]+\/accept/,
|
||||
200,
|
||||
{ delay: MOCK_DELAY }
|
||||
)
|
||||
}
|
||||
|
||||
export function setReconfirmationMeta() {
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set('ol-userEmails', [fakeReconfirmationUsersData])
|
||||
}
|
||||
|
||||
export function reconfirmationSetupMocks(fetchMock: FetchMock) {
|
||||
defaultSetupMocks(fetchMock)
|
||||
fetchMock.post(/\/user\/emails\/resend_confirmation/, 200, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
}
|
||||
|
||||
export function setReconfirmAffiliationMeta() {
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set(
|
||||
'ol-reconfirmedViaSAML',
|
||||
fakeReconfirmationUsersData.samlProviderId
|
||||
)
|
||||
}
|
||||
|
||||
export function reconfirmAffiliationSetupMocks(fetchMock: FetchMock) {
|
||||
defaultSetupMocks(fetchMock)
|
||||
fetchMock.post(/\/api\/project/, {
|
||||
status: 200,
|
||||
body: {
|
||||
projects: [{}],
|
||||
totalSize: 0,
|
||||
},
|
||||
})
|
||||
fetchMock.post(/\/user\/emails\/send-reconfirmation/, 200, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
import INRBanner from '@/features/project-list/components/notifications/ads/inr-banner'
|
||||
|
||||
export const Default = () => {
|
||||
return <INRBanner />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / INR Banner',
|
||||
component: INRBanner,
|
||||
}
|
@@ -0,0 +1,107 @@
|
||||
import NewProjectButton from '@/features/project-list/components/new-project-button'
|
||||
import { ProjectListProvider } from '@/features/project-list/context/project-list-context'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||
|
||||
const templateLinks = [
|
||||
{
|
||||
name: 'Journal articles',
|
||||
url: '/gallery/tagged/academic-journal',
|
||||
},
|
||||
{
|
||||
name: 'Books',
|
||||
url: '/gallery/tagged/book',
|
||||
},
|
||||
{
|
||||
name: 'Formal letters',
|
||||
url: '/gallery/tagged/formal-letter',
|
||||
},
|
||||
{
|
||||
name: 'Assignments',
|
||||
url: '/gallery/tagged/homework',
|
||||
},
|
||||
{
|
||||
name: 'Posters',
|
||||
url: '/gallery/tagged/poster',
|
||||
},
|
||||
{
|
||||
name: 'Presentations',
|
||||
url: '/gallery/tagged/presentation',
|
||||
},
|
||||
{
|
||||
name: 'Reports',
|
||||
url: '/gallery/tagged/report',
|
||||
},
|
||||
{
|
||||
name: 'CVs and résumés',
|
||||
url: '/gallery/tagged/cv',
|
||||
},
|
||||
{
|
||||
name: 'Theses',
|
||||
url: '/gallery/tagged/thesis',
|
||||
},
|
||||
{
|
||||
name: 'view_all',
|
||||
url: '/latex/templates',
|
||||
},
|
||||
]
|
||||
|
||||
export const Success = () => {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
templateLinks,
|
||||
})
|
||||
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(
|
||||
'express:/project/new',
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
project_id: '123',
|
||||
},
|
||||
},
|
||||
{ delay: 250 }
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<SplitTestProvider>
|
||||
<NewProjectButton id="new-project-button-story" />
|
||||
</SplitTestProvider>
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const Error = () => {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
templateLinks,
|
||||
})
|
||||
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(
|
||||
'express:/project/new',
|
||||
{
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Something went horribly wrong!',
|
||||
},
|
||||
},
|
||||
{ delay: 250 }
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<SplitTestProvider>
|
||||
<NewProjectButton id="new-project-button-story" />
|
||||
</SplitTestProvider>
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / New Project Button',
|
||||
component: NewProjectButton,
|
||||
}
|
@@ -0,0 +1,346 @@
|
||||
import UserNotifications from '../../js/features/project-list/components/notifications/user-notifications'
|
||||
import { ProjectListProvider } from '../../js/features/project-list/context/project-list-context'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import {
|
||||
commonSetupMocks,
|
||||
errorsMocks,
|
||||
fakeReconfirmationUsersData,
|
||||
institutionSetupMocks,
|
||||
reconfirmAffiliationSetupMocks,
|
||||
reconfirmationSetupMocks,
|
||||
setCommonMeta,
|
||||
setInstitutionMeta,
|
||||
setReconfirmAffiliationMeta,
|
||||
setReconfirmationMeta,
|
||||
} from './helpers/emails'
|
||||
import { useMeta } from '../hooks/use-meta'
|
||||
|
||||
export const ProjectInvite = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({
|
||||
templateKey: 'notification_project_invite',
|
||||
messageOpts: {
|
||||
projectId: '123',
|
||||
projectName: 'Abc Project',
|
||||
userName: 'fakeUser',
|
||||
token: 'abcdef',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const ProjectInviteNetworkError = (args: any) => {
|
||||
useFetchMock(errorsMocks)
|
||||
setCommonMeta({
|
||||
templateKey: 'notification_project_invite',
|
||||
messageOpts: {
|
||||
projectId: '123',
|
||||
projectName: 'Abc Project',
|
||||
userName: 'fakeUser',
|
||||
token: 'abcdef',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const Wfh2020UpgradeOffer = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({
|
||||
_id: 1,
|
||||
templateKey: 'wfh_2020_upgrade_offer',
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const IPMatchedAffiliationSsoEnabled = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_ip_matched_affiliation',
|
||||
messageOpts: {
|
||||
university_name: 'Abc University',
|
||||
institutionId: '456',
|
||||
ssoEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const IPMatchedAffiliationSsoDisabled = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_ip_matched_affiliation',
|
||||
messageOpts: {
|
||||
university_name: 'Abc University',
|
||||
institutionId: '456',
|
||||
ssoEnabled: false,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const TpdsFileLimit = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_tpds_file_limit',
|
||||
messageOpts: {
|
||||
projectName: 'Abc Project',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const DropBoxDuplicateProjectNames = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_dropbox_duplicate_project_names',
|
||||
messageOpts: {
|
||||
projectName: 'Abc Project',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const DropBoxUnlinkedDueToLapsedReconfirmation = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_dropbox_unlinked_due_to_lapsed_reconfirmation',
|
||||
})
|
||||
|
||||
useMeta({
|
||||
'ol-user': { features: {} },
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const NotificationGroupInvitation = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_group_invitation',
|
||||
messageOpts: {
|
||||
inviterName: 'John Doe',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const NotificationGroupInvitationCancelSubscription = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_group_invitation',
|
||||
messageOpts: {
|
||||
inviterName: 'John Doe',
|
||||
},
|
||||
})
|
||||
|
||||
window.metaAttributesCache.set('ol-hasIndividualRecurlySubscription', true)
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const NonSpecificMessage = (args: any) => {
|
||||
useFetchMock(commonSetupMocks)
|
||||
setCommonMeta({ _id: 1, html: 'Non specific message' })
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const InstitutionSsoAvailable = (args: any) => {
|
||||
useFetchMock(institutionSetupMocks)
|
||||
setInstitutionMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_institution_sso_available',
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const InstitutionSsoLinked = (args: any) => {
|
||||
useFetchMock(institutionSetupMocks)
|
||||
setInstitutionMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_institution_sso_linked',
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const InstitutionSsoNonCanonical = (args: any) => {
|
||||
useFetchMock(institutionSetupMocks)
|
||||
setInstitutionMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_institution_sso_non_canonical',
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const InstitutionSsoAlreadyRegistered = (args: any) => {
|
||||
useFetchMock(institutionSetupMocks)
|
||||
setInstitutionMeta({
|
||||
_id: 1,
|
||||
templateKey: 'notification_institution_sso_already_registered',
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const InstitutionSsoError = (args: any) => {
|
||||
useFetchMock(institutionSetupMocks)
|
||||
setInstitutionMeta({
|
||||
templateKey: 'notification_institution_sso_error',
|
||||
error: {
|
||||
message: 'message',
|
||||
translatedMessage: 'Translated Message',
|
||||
tryAgain: true,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const ResendConfirmationEmail = (args: any) => {
|
||||
useFetchMock(reconfirmationSetupMocks)
|
||||
setReconfirmationMeta()
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const ResendConfirmationEmailNetworkError = (args: any) => {
|
||||
useFetchMock(errorsMocks)
|
||||
setReconfirmationMeta()
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const ReconfirmAffiliation = (args: any) => {
|
||||
useFetchMock(reconfirmAffiliationSetupMocks)
|
||||
setReconfirmAffiliationMeta()
|
||||
window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [
|
||||
fakeReconfirmationUsersData,
|
||||
])
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const ReconfirmAffiliationNetworkError = (args: any) => {
|
||||
useFetchMock(errorsMocks)
|
||||
setReconfirmAffiliationMeta()
|
||||
window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [
|
||||
fakeReconfirmationUsersData,
|
||||
])
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const ReconfirmedAffiliationSuccess = (args: any) => {
|
||||
useFetchMock(reconfirmAffiliationSetupMocks)
|
||||
setReconfirmAffiliationMeta()
|
||||
window.metaAttributesCache.set('ol-userEmails', [fakeReconfirmationUsersData])
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<UserNotifications {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / Notifications',
|
||||
component: UserNotifications,
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import ProjectListTable from '../../js/features/project-list/components/table/project-list-table'
|
||||
import { ProjectListProvider } from '../../js/features/project-list/context/project-list-context'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import {
|
||||
copyableProject,
|
||||
projectsData,
|
||||
} from '../../../test/frontend/features/project-list/fixtures/projects-data'
|
||||
import { useMeta } from '../hooks/use-meta'
|
||||
import { tags } from '../../../test/frontend/features/project-list/fixtures/tags-data'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
const MOCK_DELAY = 500
|
||||
|
||||
export const Interactive = (args: any) => {
|
||||
window.metaAttributesCache.set('ol-user_id', '624333f147cfd8002622a1d3')
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(
|
||||
/\/api\/project/,
|
||||
{ projects: projectsData, totalSize: projectsData.length },
|
||||
{ delay: MOCK_DELAY }
|
||||
)
|
||||
fetchMock.post(
|
||||
'express:/project/:projectId/clone',
|
||||
() => ({
|
||||
project_id: uuid(),
|
||||
name: copyableProject.name,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
owner: {
|
||||
_id: copyableProject.owner?.id,
|
||||
email: copyableProject.owner?.id,
|
||||
first_name: copyableProject.owner?.firstName,
|
||||
last_name: copyableProject.owner?.lastName,
|
||||
},
|
||||
}),
|
||||
{ delay: MOCK_DELAY }
|
||||
)
|
||||
})
|
||||
|
||||
useMeta({
|
||||
'ol-tags': tags,
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<ProjectListTable {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / Project Table',
|
||||
component: ProjectListTable,
|
||||
decorators: [
|
||||
(Story: any) => (
|
||||
<div className="project-list-react">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import SearchForm from '../../js/features/project-list/components/search-form'
|
||||
import { ProjectListProvider } from '../../js/features/project-list/context/project-list-context'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import { projectsData } from '../../../test/frontend/features/project-list/fixtures/projects-data'
|
||||
|
||||
export const Search = (args: any) => {
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/api\/project/, {
|
||||
projects: projectsData,
|
||||
totalSize: projectsData.length,
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectListProvider>
|
||||
<SearchForm {...args} />
|
||||
</ProjectListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / Project Search',
|
||||
component: SearchForm,
|
||||
args: {
|
||||
inputValue: '',
|
||||
},
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import SurveyWidget from '../../js/features/project-list/components/survey-widget'
|
||||
|
||||
export const Survey = (args: any) => {
|
||||
localStorage.clear()
|
||||
window.metaAttributesCache.set('ol-survey', {
|
||||
name: 'my-survey',
|
||||
preText: 'To help shape the future of Overleaf',
|
||||
linkText: 'Click here!',
|
||||
url: 'https://example.com/my-survey',
|
||||
})
|
||||
|
||||
return <SurveyWidget {...args} />
|
||||
}
|
||||
|
||||
export const UndefinedSurvey = (args: any) => {
|
||||
localStorage.clear()
|
||||
|
||||
return <SurveyWidget {...args} />
|
||||
}
|
||||
|
||||
export const EmptySurvey = (args: any) => {
|
||||
localStorage.clear()
|
||||
window.metaAttributesCache.set('ol-survey', {})
|
||||
|
||||
return <SurveyWidget {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / Survey Widget',
|
||||
component: SurveyWidget,
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
import SystemMessages from '@/shared/components/system-messages'
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import { type FetchMock } from 'fetch-mock'
|
||||
|
||||
export const SystemMessage = (args: any) => {
|
||||
useFetchMock((fetchMock: FetchMock) => {
|
||||
fetchMock.get(/\/system\/messages/, [
|
||||
{
|
||||
_id: 1,
|
||||
content: `
|
||||
Closing this message will mark it as hidden.
|
||||
Remove it from the local storage to make it appear again.
|
||||
`,
|
||||
},
|
||||
{
|
||||
_id: 'protected',
|
||||
content: 'A protected message content - cannot be closed',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
return <SystemMessages {...args} />
|
||||
}
|
||||
|
||||
export const TranslationMessage = (args: any) => {
|
||||
useFetchMock((fetchMock: FetchMock) => {
|
||||
fetchMock.get(/\/system\/messages/, [])
|
||||
})
|
||||
|
||||
window.metaAttributesCache.set('ol-suggestedLanguage', {
|
||||
url: '/dev/null',
|
||||
lngName: 'German',
|
||||
imgUrl: 'https://flagcdn.com/w40/de.png',
|
||||
})
|
||||
|
||||
return <SystemMessages {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Project List / System Messages',
|
||||
component: SystemMessages,
|
||||
}
|
23
services/web/frontend/stories/radio-chip.stories.tsx
Normal file
23
services/web/frontend/stories/radio-chip.stories.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import RadioChip from '../js/shared/components/radio-chip'
|
||||
|
||||
type Args = React.ComponentProps<typeof RadioChip>
|
||||
|
||||
export const RadioChipDefault = (args: Args) => {
|
||||
return <RadioChip {...args} />
|
||||
}
|
||||
|
||||
export const RadioChipDisabled = (args: Args) => {
|
||||
return <RadioChip {...args} disabled />
|
||||
}
|
||||
|
||||
export const RadioChipDisabledSelected = (args: Args) => {
|
||||
return <RadioChip {...args} checked disabled />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / RadioChip',
|
||||
component: RadioChip,
|
||||
args: {
|
||||
label: 'Option',
|
||||
},
|
||||
}
|
61
services/web/frontend/stories/select.stories.tsx
Normal file
61
services/web/frontend/stories/select.stories.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Select } from '../js/shared/components/select'
|
||||
|
||||
const items = [1, 2, 3, 4].map(index => ({
|
||||
key: index,
|
||||
value: `Demo item ${index}`,
|
||||
group: index >= 3 ? 'Large numbers' : undefined,
|
||||
}))
|
||||
|
||||
export const Base = () => {
|
||||
return (
|
||||
<Select
|
||||
items={items}
|
||||
itemToString={x => String(x?.value)}
|
||||
itemToKey={x => String(x.key)}
|
||||
defaultText="Choose an item"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithSubtitles = () => {
|
||||
return (
|
||||
<Select
|
||||
items={items}
|
||||
itemToString={x => String(x?.value)}
|
||||
itemToKey={x => String(x.key)}
|
||||
itemToSubtitle={x => x?.group ?? ''}
|
||||
defaultText="Choose an item"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithSelectedIcon = () => {
|
||||
return (
|
||||
<Select
|
||||
items={items}
|
||||
itemToString={x => String(x?.value)}
|
||||
itemToKey={x => String(x.key)}
|
||||
itemToSubtitle={x => x?.group ?? ''}
|
||||
defaultText="Choose an item"
|
||||
selectedIcon
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithDisabledItem = () => {
|
||||
return (
|
||||
<Select
|
||||
items={items}
|
||||
itemToString={x => String(x?.value)}
|
||||
itemToKey={x => String(x.key)}
|
||||
itemToDisabled={x => x?.key === 1}
|
||||
itemToSubtitle={x => x?.group ?? ''}
|
||||
defaultText="Choose an item"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Select',
|
||||
component: Select,
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import AccountInfoSection from '../../js/features/settings/components/account-info-section'
|
||||
import { setDefaultMeta, defaultSetupMocks } from './helpers/account-info'
|
||||
import { UserProvider } from '../../js/shared/context/user-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export const Success = args => {
|
||||
setDefaultMeta()
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<AccountInfoSection {...args} />
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const ReadOnly = args => {
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set('ol-isExternalAuthenticationSystemUsed', true)
|
||||
window.metaAttributesCache.set('ol-shouldAllowEditingDetails', false)
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<AccountInfoSection {...args} />
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const NoEmailInput = args => {
|
||||
setDefaultMeta()
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
})
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<AccountInfoSection {...args} />
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const Error = args => {
|
||||
setDefaultMeta()
|
||||
useFetchMock(fetchMock => fetchMock.post(/\/user\/settings/, 500))
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<AccountInfoSection {...args} />
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Account Info',
|
||||
component: AccountInfoSection,
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
import useFetchMock from './../hooks/use-fetch-mock'
|
||||
import Input from '../../js/features/settings/components/emails/add-email/input'
|
||||
|
||||
export const EmailInput = (args: any) => {
|
||||
useFetchMock(fetchMock =>
|
||||
fetchMock.get(/\/institutions\/domains/, [
|
||||
{
|
||||
hostname: 'autocomplete.edu',
|
||||
university: { id: 123, name: 'Auto Complete University' },
|
||||
},
|
||||
])
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Input {...args} />
|
||||
<br />
|
||||
<div>
|
||||
Use <code>autocomplete.edu</code> as domain to trigger an autocomplete
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Emails and Affiliations',
|
||||
component: Input,
|
||||
argTypes: {
|
||||
onChange: { action: 'change' },
|
||||
},
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import BetaProgramSection from '../../js/features/settings/components/beta-program-section'
|
||||
import { UserProvider } from '../../js/shared/context/user-context'
|
||||
|
||||
export const SectionNotEnrolled = args => {
|
||||
window.metaAttributesCache.set('ol-user', { betaProgram: false })
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<BetaProgramSection {...args} />
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const SectionEnrolled = args => {
|
||||
window.metaAttributesCache.set('ol-user', { betaProgram: true })
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<BetaProgramSection {...args} />
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Beta Program',
|
||||
component: BetaProgramSection,
|
||||
}
|
43
services/web/frontend/stories/settings/emails.stories.jsx
Normal file
43
services/web/frontend/stories/settings/emails.stories.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import EmailsSection from '../../js/features/settings/components/emails-section'
|
||||
import useFetchMock from './../hooks/use-fetch-mock'
|
||||
import {
|
||||
setDefaultMeta,
|
||||
setReconfirmationMeta,
|
||||
defaultSetupMocks,
|
||||
reconfirmationSetupMocks,
|
||||
errorsMocks,
|
||||
emailLimitSetupMocks,
|
||||
} from './helpers/emails'
|
||||
|
||||
export const EmailsList = args => {
|
||||
useFetchMock(defaultSetupMocks)
|
||||
setDefaultMeta()
|
||||
|
||||
return <EmailsSection {...args} />
|
||||
}
|
||||
|
||||
export const EmailLimitList = args => {
|
||||
useFetchMock(emailLimitSetupMocks)
|
||||
setDefaultMeta()
|
||||
|
||||
return <EmailsSection {...args} />
|
||||
}
|
||||
|
||||
export const ReconfirmationEmailsList = args => {
|
||||
useFetchMock(reconfirmationSetupMocks)
|
||||
setReconfirmationMeta()
|
||||
|
||||
return <EmailsSection {...args} />
|
||||
}
|
||||
|
||||
export const NetworkErrors = args => {
|
||||
useFetchMock(errorsMocks)
|
||||
setDefaultMeta()
|
||||
|
||||
return <EmailsSection {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Emails and Affiliations',
|
||||
component: EmailsSection,
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
const MOCK_DELAY = 1000
|
||||
|
||||
export function defaultSetupMocks(fetchMock) {
|
||||
fetchMock.post(/\/user\/settings/, 200, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
}
|
||||
|
||||
export function setDefaultMeta() {
|
||||
window.metaAttributesCache.set('ol-user', {
|
||||
...window.metaAttributesCache.get('ol-user'),
|
||||
email: 'sherlock@holmes.co.uk',
|
||||
first_name: 'Sherlock',
|
||||
last_name: 'Holmes',
|
||||
})
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: false,
|
||||
})
|
||||
window.metaAttributesCache.set('ol-isExternalAuthenticationSystemUsed', false)
|
||||
window.metaAttributesCache.set('ol-shouldAllowEditingDetails', true)
|
||||
}
|
219
services/web/frontend/stories/settings/helpers/emails.js
Normal file
219
services/web/frontend/stories/settings/helpers/emails.js
Normal file
@@ -0,0 +1,219 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
const MOCK_DELAY = 1000
|
||||
|
||||
const fakeUsersData = [
|
||||
{
|
||||
affiliation: {
|
||||
institution: {
|
||||
confirmed: true,
|
||||
id: 1,
|
||||
name: 'Overleaf',
|
||||
},
|
||||
licence: 'pro_plus',
|
||||
},
|
||||
confirmedAt: '2022-03-09T10:59:44.139Z',
|
||||
email: 'foo@overleaf.com',
|
||||
default: true,
|
||||
emailHasInstitutionLicence: true,
|
||||
},
|
||||
{
|
||||
confirmedAt: '2022-03-10T10:59:44.139Z',
|
||||
email: 'bar@overleaf.com',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
affiliation: {
|
||||
institution: {
|
||||
confirmed: true,
|
||||
id: 2,
|
||||
name: 'Overleaf',
|
||||
},
|
||||
licence: 'pro_plus',
|
||||
department: 'Art & Art History',
|
||||
role: 'Postdoc',
|
||||
},
|
||||
email: 'baz@overleaf.com',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
email: 'qux@overleaf.com',
|
||||
default: false,
|
||||
},
|
||||
]
|
||||
const fakeReconfirmationUsersData = [
|
||||
{
|
||||
affiliation: {
|
||||
institution: {
|
||||
confirmed: true,
|
||||
isUniversity: true,
|
||||
id: 4,
|
||||
name: 'Reconfirmable Email Highlighted',
|
||||
},
|
||||
licence: 'pro_plus',
|
||||
inReconfirmNotificationPeriod: true,
|
||||
},
|
||||
email: 'reconfirmation-highlighted@overleaf.com',
|
||||
confirmedAt: '2022-03-09T10:59:44.139Z',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
affiliation: {
|
||||
institution: {
|
||||
confirmed: true,
|
||||
isUniversity: true,
|
||||
id: 4,
|
||||
name: 'Reconfirmable Emails Primary',
|
||||
},
|
||||
licence: 'pro_plus',
|
||||
inReconfirmNotificationPeriod: true,
|
||||
},
|
||||
email: 'reconfirmation-nonsso@overleaf.com',
|
||||
confirmedAt: '2022-03-09T10:59:44.139Z',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
affiliation: {
|
||||
institution: {
|
||||
confirmed: true,
|
||||
ssoEnabled: true,
|
||||
isUniversity: true,
|
||||
id: 3,
|
||||
name: 'Reconfirmable SSO',
|
||||
},
|
||||
licence: 'pro_plus',
|
||||
inReconfirmNotificationPeriod: true,
|
||||
},
|
||||
email: 'reconfirmation-sso@overleaf.com',
|
||||
confirmedAt: '2022-03-09T10:59:44.139Z',
|
||||
samlProviderId: 'reconfirmation-sso-provider-id',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
affiliation: {
|
||||
institution: {
|
||||
confirmed: true,
|
||||
isUniversity: true,
|
||||
ssoEnabled: true,
|
||||
id: 5,
|
||||
name: 'Reconfirmed SSO',
|
||||
},
|
||||
licence: 'pro_plus',
|
||||
},
|
||||
confirmedAt: '2022-03-09T10:59:44.139Z',
|
||||
email: 'sso@overleaf.com',
|
||||
samlProviderId: 'sso-reconfirmed-provider-id',
|
||||
default: false,
|
||||
},
|
||||
]
|
||||
|
||||
const fakeInstitutions = [
|
||||
{
|
||||
id: 9326,
|
||||
name: 'Unknown',
|
||||
country_code: 'al',
|
||||
departments: ['New department'],
|
||||
},
|
||||
]
|
||||
const fakeInstitution = {
|
||||
id: 123,
|
||||
name: 'test',
|
||||
country_code: 'de',
|
||||
departments: [],
|
||||
team_id: null,
|
||||
}
|
||||
const bazFakeInstitution = {
|
||||
id: 2,
|
||||
name: 'Baz',
|
||||
country_code: 'de',
|
||||
departments: ['Custom department 1', 'Custom department 2'],
|
||||
team_id: null,
|
||||
}
|
||||
|
||||
const fakeInstitutionDomain1 = [
|
||||
{
|
||||
university: {
|
||||
id: 1234,
|
||||
ssoEnabled: true,
|
||||
name: 'Auto Complete University',
|
||||
},
|
||||
hostname: 'autocomplete.edu',
|
||||
confirmed: true,
|
||||
},
|
||||
]
|
||||
|
||||
const fakeInstitutionDomain2 = [
|
||||
{
|
||||
university: {
|
||||
id: 5678,
|
||||
ssoEnabled: false,
|
||||
name: 'Fake Auto Complete University',
|
||||
},
|
||||
hostname: 'fake-autocomplete.edu',
|
||||
confirmed: true,
|
||||
},
|
||||
]
|
||||
|
||||
export function defaultSetupMocks(fetchMock) {
|
||||
fetchMock
|
||||
.get(/\/user\/emails/, fakeUsersData, { delay: MOCK_DELAY })
|
||||
.get(/\/institutions\/list\/2/, bazFakeInstitution, { delay: MOCK_DELAY })
|
||||
.get(/\/institutions\/list\/\d+/, fakeInstitution, { delay: MOCK_DELAY })
|
||||
.get(/\/institutions\/list\?country_code=.*/, fakeInstitutions, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
.get(/\/institutions\/domains\?hostname=a/, fakeInstitutionDomain1)
|
||||
.get(/\/institutions\/domains\?hostname=f/, fakeInstitutionDomain2)
|
||||
.get(/\/institutions\/domains/, [])
|
||||
.post(/\/user\/emails\/*/, 200, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
}
|
||||
|
||||
export function reconfirmationSetupMocks(fetchMock) {
|
||||
defaultSetupMocks(fetchMock)
|
||||
fetchMock.get(/\/user\/emails/, fakeReconfirmationUsersData, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
}
|
||||
|
||||
export function emailLimitSetupMocks(fetchMock) {
|
||||
const userData = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
userData.push({ email: `example${i}@overleaf.com` })
|
||||
}
|
||||
defaultSetupMocks(fetchMock)
|
||||
fetchMock.get(/\/user\/emails/, userData, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
}
|
||||
|
||||
export function errorsMocks(fetchMock) {
|
||||
fetchMock
|
||||
.get(/\/user\/emails/, fakeUsersData, { delay: MOCK_DELAY })
|
||||
.post(/\/user\/emails\/*/, 500)
|
||||
}
|
||||
|
||||
export function setDefaultMeta() {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
hasSamlFeature: true,
|
||||
samlInitPath: 'saml/init',
|
||||
})
|
||||
localStorage.setItem(
|
||||
'showInstitutionalLeaversSurveyUntil',
|
||||
(Date.now() - 1000 * 60 * 60).toString()
|
||||
)
|
||||
}
|
||||
|
||||
export function setReconfirmationMeta() {
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set(
|
||||
'ol-reconfirmationRemoveEmail',
|
||||
'reconfirmation-highlighted@overleaf.com'
|
||||
)
|
||||
window.metaAttributesCache.set(
|
||||
'ol-reconfirmedViaSAML',
|
||||
'sso-reconfirmed-provider-id'
|
||||
)
|
||||
}
|
17
services/web/frontend/stories/settings/helpers/leave.js
Normal file
17
services/web/frontend/stories/settings/helpers/leave.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
const MOCK_DELAY = 1000
|
||||
|
||||
export function defaultSetupMocks(fetchMock) {
|
||||
fetchMock.post(/\/user\/delete/, 200, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
}
|
||||
|
||||
export function setDefaultMeta() {
|
||||
window.metaAttributesCache.set('ol-usersEmail', 'user@primary.com')
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
isOverleaf: true,
|
||||
})
|
||||
window.metaAttributesCache.set('ol-hasPassword', true)
|
||||
}
|
78
services/web/frontend/stories/settings/helpers/linking.js
Normal file
78
services/web/frontend/stories/settings/helpers/linking.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const MOCK_DELAY = 1000
|
||||
|
||||
export function defaultSetupMocks(fetchMock) {
|
||||
fetchMock
|
||||
.post('/user/oauth-unlink', 200, { delay: MOCK_DELAY })
|
||||
.get(
|
||||
'express:/user/tpds/queues',
|
||||
{ tpdsToWeb: 0, webToTpds: 0 },
|
||||
{ delay: MOCK_DELAY }
|
||||
)
|
||||
}
|
||||
|
||||
export function setDefaultMeta() {
|
||||
window.metaAttributesCache.set('ol-user', {
|
||||
...window.metaAttributesCache.get('ol-user'),
|
||||
features: { github: true, dropbox: true, mendeley: false, zotero: false },
|
||||
refProviders: {
|
||||
mendeley: true,
|
||||
zotero: true,
|
||||
},
|
||||
})
|
||||
window.metaAttributesCache.set('ol-github', { enabled: false })
|
||||
window.metaAttributesCache.set('ol-dropbox', { registered: true })
|
||||
window.metaAttributesCache.set('ol-thirdPartyIds', {
|
||||
collabratec: 'collabratec-id',
|
||||
google: 'google-id',
|
||||
})
|
||||
|
||||
window.metaAttributesCache.set('ol-oauthProviders', {
|
||||
collabratec: {
|
||||
descriptionKey: 'linked_collabratec_description',
|
||||
descriptionOptions: { appName: 'Overleaf' },
|
||||
name: 'IEEE Collabratec®',
|
||||
hideWhenNotLinked: true,
|
||||
linkPath: '/collabratec/auth/link',
|
||||
},
|
||||
google: {
|
||||
descriptionKey: 'login_with_service',
|
||||
descriptionOptions: { service: 'Google' },
|
||||
name: 'Google',
|
||||
linkPath: '/auth/google',
|
||||
},
|
||||
orcid: {
|
||||
descriptionKey: 'oauth_orcid_description',
|
||||
descriptionOptions: {
|
||||
link: '/blog/434',
|
||||
appName: 'Overleaf',
|
||||
},
|
||||
name: 'ORCID',
|
||||
linkPath: '/auth/orcid',
|
||||
},
|
||||
})
|
||||
window.metaAttributesCache.set('ol-hideLinkingWidgets', true)
|
||||
window.metaAttributesCache.delete('ol-ssoErrorMessage')
|
||||
}
|
||||
|
||||
export function setPersonalAccessTokensMeta() {
|
||||
function generateToken(_id) {
|
||||
const oneYearFromNow = new Date()
|
||||
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1)
|
||||
|
||||
const tokenHasBeenUsed = Math.random() > 0.5
|
||||
|
||||
return {
|
||||
_id,
|
||||
accessTokenPartial: 'olp_abc' + _id,
|
||||
createdAt: new Date(),
|
||||
accessTokenExpiresAt: oneYearFromNow,
|
||||
lastUsedAt: tokenHasBeenUsed ? new Date() : undefined,
|
||||
}
|
||||
}
|
||||
const tokens = []
|
||||
for (let i = 0; i < 6; i++) {
|
||||
tokens.push(generateToken(i))
|
||||
}
|
||||
|
||||
window.metaAttributesCache.set('ol-personalAccessTokens', tokens)
|
||||
}
|
35
services/web/frontend/stories/settings/helpers/password.js
Normal file
35
services/web/frontend/stories/settings/helpers/password.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
const MOCK_DELAY = 1000
|
||||
|
||||
export function defaultSetupMocks(fetchMock) {
|
||||
fetchMock.post(
|
||||
/\/user\/password\/update/,
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
message: {
|
||||
type: 'success',
|
||||
email: 'tim.alby@overleaf.com',
|
||||
text: 'Password changed',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
delay: MOCK_DELAY,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function setDefaultMeta() {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
isOverleaf: true,
|
||||
})
|
||||
window.metaAttributesCache.set('ol-isExternalAuthenticationSystemUsed', false)
|
||||
window.metaAttributesCache.set('ol-hasPassword', true)
|
||||
window.metaAttributesCache.set('ol-passwordStrengthOptions', {
|
||||
length: {
|
||||
min: 2,
|
||||
},
|
||||
})
|
||||
}
|
71
services/web/frontend/stories/settings/leave.stories.jsx
Normal file
71
services/web/frontend/stories/settings/leave.stories.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import LeaveModal from '../../js/features/settings/components/leave/modal'
|
||||
import LeaveSection from '../../js/features/settings/components/leave-section'
|
||||
import { setDefaultMeta, defaultSetupMocks } from './helpers/leave'
|
||||
|
||||
export const Section = args => {
|
||||
useFetchMock(defaultSetupMocks)
|
||||
setDefaultMeta()
|
||||
|
||||
return <LeaveSection {...args} />
|
||||
}
|
||||
Section.component = LeaveSection
|
||||
Section.parameters = { controls: { include: [], hideNoControlsWarning: true } }
|
||||
|
||||
export const ModalSuccess = args => {
|
||||
setDefaultMeta()
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
return <LeaveModal {...args} />
|
||||
}
|
||||
|
||||
export const ModalWithoutPassword = args => {
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set('ol-hasPassword', false)
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
return <LeaveModal {...args} />
|
||||
}
|
||||
|
||||
export const ModalAuthError = args => {
|
||||
setDefaultMeta()
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/user\/delete/, 403)
|
||||
})
|
||||
|
||||
return <LeaveModal {...args} />
|
||||
}
|
||||
|
||||
export const ModalServerError = args => {
|
||||
setDefaultMeta()
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/user\/delete/, 500)
|
||||
})
|
||||
|
||||
return <LeaveModal {...args} />
|
||||
}
|
||||
|
||||
export const ModalSubscriptionError = args => {
|
||||
setDefaultMeta()
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/user\/delete/, {
|
||||
status: 422,
|
||||
body: {
|
||||
error: 'SubscriptionAdminDeletionError',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return <LeaveModal {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Leave',
|
||||
component: LeaveModal,
|
||||
args: {
|
||||
isOpen: true,
|
||||
},
|
||||
argTypes: {
|
||||
handleClose: { action: 'handleClose' },
|
||||
},
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import EmailsSection from '../../js/features/settings/components/emails-section'
|
||||
import { UserEmailsProvider } from '../../js/features/settings/context/user-email-context'
|
||||
import { LeaversSurveyAlert } from '../../js/features/settings/components/leavers-survey-alert'
|
||||
import localStorage from '@/infrastructure/local-storage'
|
||||
|
||||
export const SurveyAlert = () => {
|
||||
localStorage.setItem(
|
||||
'showInstitutionalLeaversSurveyUntil',
|
||||
Date.now() + 1000 * 60 * 60
|
||||
)
|
||||
return (
|
||||
<UserEmailsProvider>
|
||||
<LeaversSurveyAlert />
|
||||
</UserEmailsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Survey Alerts',
|
||||
component: EmailsSection,
|
||||
}
|
96
services/web/frontend/stories/settings/linking.stories.jsx
Normal file
96
services/web/frontend/stories/settings/linking.stories.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import LinkingSection from '../../js/features/settings/components/linking-section'
|
||||
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
|
||||
|
||||
export const Section = args => {
|
||||
useFetchMock(defaultSetupMocks)
|
||||
setDefaultMeta()
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<SSOProvider>
|
||||
<LinkingSection {...args} />
|
||||
</SSOProvider>
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const SectionAllUnlinked = args => {
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
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 },
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setDefaultMeta()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<SSOProvider>
|
||||
<LinkingSection {...args} />
|
||||
</SSOProvider>
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const SectionSSOErrors = args => {
|
||||
useFetchMock(fetchMock =>
|
||||
fetchMock.post('/user/oauth-unlink', 500, { delay: MOCK_DELAY })
|
||||
)
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set('ol-hideLinkingWidgets', true)
|
||||
window.metaAttributesCache.set(
|
||||
'ol-ssoErrorMessage',
|
||||
'Account already linked to another Overleaf user'
|
||||
)
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<SSOProvider>
|
||||
<LinkingSection {...args} />
|
||||
</SSOProvider>
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const SectionProjetSyncSuccess = args => {
|
||||
useFetchMock(defaultSetupMocks)
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set('ol-github', { enabled: true })
|
||||
window.metaAttributesCache.set(
|
||||
'ol-projectSyncSuccessMessage',
|
||||
'Thanks, we’ve successfully linked your GitHub account to Overleaf. You can now export your Overleaf projects to GitHub, or import projects from your GitHub repositories.'
|
||||
)
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<SSOProvider>
|
||||
<LinkingSection {...args} />
|
||||
</SSOProvider>
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Linking',
|
||||
component: LinkingSection,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
import NewsletterSection from '../../js/features/settings/components/newsletter-section'
|
||||
|
||||
export const Section = args => {
|
||||
return <NewsletterSection {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Newsletter',
|
||||
component: NewsletterSection,
|
||||
}
|
81
services/web/frontend/stories/settings/page.stories.jsx
Normal file
81
services/web/frontend/stories/settings/page.stories.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import SettingsPageRoot from '../../js/features/settings/components/root'
|
||||
import {
|
||||
setDefaultMeta as setDefaultLeaveMeta,
|
||||
defaultSetupMocks as defaultSetupLeaveMocks,
|
||||
} from './helpers/leave'
|
||||
import {
|
||||
setDefaultMeta as setDefaultAccountInfoMeta,
|
||||
defaultSetupMocks as defaultSetupAccountInfoMocks,
|
||||
} from './helpers/account-info'
|
||||
import {
|
||||
setDefaultMeta as setDefaultPasswordMeta,
|
||||
defaultSetupMocks as defaultSetupPasswordMocks,
|
||||
} from './helpers/password'
|
||||
import {
|
||||
setDefaultMeta as setDefaultEmailsMeta,
|
||||
defaultSetupMocks as defaultSetupEmailsMocks,
|
||||
} from './helpers/emails'
|
||||
import {
|
||||
setDefaultMeta as setDefaultLinkingMeta,
|
||||
defaultSetupMocks as defaultSetupLinkingMocks,
|
||||
setPersonalAccessTokensMeta,
|
||||
} from './helpers/linking'
|
||||
import { UserProvider } from '../../js/shared/context/user-context'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export const Overleaf = args => {
|
||||
setDefaultLeaveMeta()
|
||||
setDefaultAccountInfoMeta()
|
||||
setDefaultPasswordMeta()
|
||||
setDefaultEmailsMeta()
|
||||
setDefaultLinkingMeta()
|
||||
useFetchMock(fetchMock => {
|
||||
defaultSetupLeaveMocks(fetchMock)
|
||||
defaultSetupAccountInfoMocks(fetchMock)
|
||||
defaultSetupPasswordMocks(fetchMock)
|
||||
defaultSetupEmailsMocks(fetchMock)
|
||||
defaultSetupLinkingMocks(fetchMock)
|
||||
})
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<SettingsPageRoot {...args} />
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const OverleafWithAccessTokens = args => {
|
||||
setPersonalAccessTokensMeta()
|
||||
return Overleaf(args)
|
||||
}
|
||||
|
||||
export const ServerPro = args => {
|
||||
setDefaultAccountInfoMeta()
|
||||
setDefaultPasswordMeta()
|
||||
setPersonalAccessTokensMeta()
|
||||
useFetchMock(fetchMock => {
|
||||
defaultSetupAccountInfoMocks(fetchMock)
|
||||
defaultSetupPasswordMocks(fetchMock)
|
||||
})
|
||||
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: false,
|
||||
isOverleaf: false,
|
||||
})
|
||||
window.metaAttributesCache.set('ol-hideLinkingWidgets', true)
|
||||
window.metaAttributesCache.delete('ol-oauthProviders')
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
<SettingsPageRoot {...args} />
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Full Page',
|
||||
component: SettingsPageRoot,
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
49
services/web/frontend/stories/settings/password.stories.jsx
Normal file
49
services/web/frontend/stories/settings/password.stories.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import PasswordSection from '../../js/features/settings/components/password-section'
|
||||
import { setDefaultMeta, defaultSetupMocks } from './helpers/password'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export const Success = args => {
|
||||
setDefaultMeta()
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
return <PasswordSection {...args} />
|
||||
}
|
||||
|
||||
export const ManagedExternally = args => {
|
||||
setDefaultMeta()
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
isOverleaf: false,
|
||||
})
|
||||
window.metaAttributesCache.set('ol-isExternalAuthenticationSystemUsed', true)
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
return <PasswordSection {...args} />
|
||||
}
|
||||
|
||||
export const NoExistingPassword = args => {
|
||||
setDefaultMeta()
|
||||
window.metaAttributesCache.set('ol-hasPassword', false)
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
return <PasswordSection {...args} />
|
||||
}
|
||||
|
||||
export const Error = args => {
|
||||
setDefaultMeta()
|
||||
useFetchMock(fetchMock =>
|
||||
fetchMock.post(/\/user\/password\/update/, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Your old password is wrong',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
return <PasswordSection {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Password',
|
||||
component: PasswordSection,
|
||||
}
|
10
services/web/frontend/stories/settings/sessions.stories.jsx
Normal file
10
services/web/frontend/stories/settings/sessions.stories.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import SessionsSection from '../../js/features/settings/components/sessions-section'
|
||||
|
||||
export const Section = args => {
|
||||
return <SessionsSection {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Sessions',
|
||||
component: SessionsSection,
|
||||
}
|
56
services/web/frontend/stories/settings/sso-alert.stories.tsx
Normal file
56
services/web/frontend/stories/settings/sso-alert.stories.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import EmailsSection from '../../js/features/settings/components/emails-section'
|
||||
import { SSOAlert } from '../../js/features/settings/components/emails/sso-alert'
|
||||
|
||||
export const Info = () => {
|
||||
window.metaAttributesCache.set('ol-institutionLinked', {
|
||||
universityName: 'Overleaf University',
|
||||
})
|
||||
return <SSOAlert />
|
||||
}
|
||||
|
||||
export const InfoWithEntitlement = () => {
|
||||
window.metaAttributesCache.set('ol-institutionLinked', {
|
||||
universityName: 'Overleaf University',
|
||||
hasEntitlement: true,
|
||||
})
|
||||
return <SSOAlert />
|
||||
}
|
||||
|
||||
export const NonCanonicalEmail = () => {
|
||||
window.metaAttributesCache.set('ol-institutionLinked', {
|
||||
universityName: 'Overleaf University',
|
||||
})
|
||||
window.metaAttributesCache.set(
|
||||
'ol-institutionEmailNonCanonical',
|
||||
'user@example.com'
|
||||
)
|
||||
return <SSOAlert />
|
||||
}
|
||||
|
||||
export const Error = () => {
|
||||
window.metaAttributesCache.set('ol-samlError', {
|
||||
translatedMessage: 'There was an Error',
|
||||
})
|
||||
return <SSOAlert />
|
||||
}
|
||||
|
||||
export const ErrorTranslated = () => {
|
||||
window.metaAttributesCache.set('ol-samlError', {
|
||||
translatedMessage: 'Translated Error Message',
|
||||
message: 'There was an Error',
|
||||
})
|
||||
return <SSOAlert />
|
||||
}
|
||||
|
||||
export const ErrorWithTryAgain = () => {
|
||||
window.metaAttributesCache.set('ol-samlError', {
|
||||
message: 'There was an Error',
|
||||
tryAgain: true,
|
||||
})
|
||||
return <SSOAlert />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / SSO Alerts',
|
||||
component: EmailsSection,
|
||||
}
|
182
services/web/frontend/stories/share-project-modal.stories.jsx
Normal file
182
services/web/frontend/stories/share-project-modal.stories.jsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import ShareProjectModal from '../js/features/share-project-modal/components/share-project-modal'
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
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)
|
||||
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'private',
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LinkSharingOn = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LinkSharingLoading = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const NonProjectOwnerLinkSharingOff = args => {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'private',
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const NonProjectOwnerLinkSharingOn = args => {
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const RestrictedTokenMember = args => {
|
||||
// Override isRestrictedTokenMember to be true
|
||||
// Currently this is necessary because the context value is set from window,
|
||||
// however in the future we should change this to set via props
|
||||
window.metaAttributesCache.set('ol-isRestrictedTokenMember', true)
|
||||
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LegacyLinkSharingReadAndWrite = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'readAndWrite',
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LegacyLinkSharingReadOnly = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
publicAccesLevel: 'readOnly',
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export const LimitedCollaborators = args => {
|
||||
useFetchMock(setupFetchMock)
|
||||
|
||||
useScope({
|
||||
project: {
|
||||
...args.project,
|
||||
features: {
|
||||
...args.project.features,
|
||||
collaborators: 3,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return <ShareProjectModal {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Editor / Modals / Share Project',
|
||||
component: ShareProjectModal,
|
||||
args: {
|
||||
show: true,
|
||||
animation: false,
|
||||
user: {},
|
||||
project: {
|
||||
...project,
|
||||
owner: {
|
||||
...project.owner,
|
||||
_id: 'story-user',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
handleHide: { action: 'hide' },
|
||||
},
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
||||
|
||||
function setupFetchMock(fetchMock) {
|
||||
const delay = 1000
|
||||
|
||||
fetchMock
|
||||
// list contacts
|
||||
.get('express:/user/contacts', { contacts }, { delay })
|
||||
// access tokens
|
||||
.get(
|
||||
'express:/project/:projectId/tokens',
|
||||
{ tokens: project.tokens },
|
||||
{ delay }
|
||||
)
|
||||
// change privacy setting
|
||||
.post('express:/project/:projectId/settings/admin', 200, { delay })
|
||||
// update project member (e.g. set privilege level)
|
||||
.put('express:/project/:projectId/users/:userId', 200, { delay })
|
||||
// remove project member
|
||||
.delete('express:/project/:projectId/users/:userId', 200, { delay })
|
||||
// transfer ownership
|
||||
.post('express:/project/:projectId/transfer-ownership', 200, {
|
||||
delay,
|
||||
})
|
||||
// send invite
|
||||
.post('express:/project/:projectId/invite', 200, { delay })
|
||||
// delete invite
|
||||
.delete('express:/project/:projectId/invite/:inviteId', 204, {
|
||||
delay,
|
||||
})
|
||||
// resend invite
|
||||
.post('express:/project/:projectId/invite/:inviteId/resend', 200, {
|
||||
delay,
|
||||
})
|
||||
// send analytics event
|
||||
.post('express:/event/:key', 200)
|
||||
}
|
@@ -0,0 +1,409 @@
|
||||
import SourceEditor from '../../js/features/source-editor/components/source-editor'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import { useScope } from '../hooks/use-scope'
|
||||
import { useMeta } from '../hooks/use-meta'
|
||||
import { FC } from 'react'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import RangesTracker from '@overleaf/ranges-tracker'
|
||||
|
||||
const FileTreePathProvider: FC = ({ children }) => (
|
||||
<FileTreePathContext.Provider
|
||||
value={{
|
||||
dirname: () => null,
|
||||
findEntityByPath: () => null,
|
||||
pathInFolder: () => null,
|
||||
previewByPath: (path: string) =>
|
||||
path === 'frog.jpg'
|
||||
? {
|
||||
extension: 'png',
|
||||
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpaJVETuIOGSogmBBVMRRq1CECqFWaNXB5NIvaNKQpLg4Cq4FBz8Wqw4uzro6uAqC4AeIq4uToouU+L+k0CLWg+N+vLv3uHsHCNUi06y2cUDTbTMRi4qp9KoYeIWAPnShB6Mys4w5SYqj5fi6h4+vdxGe1frcn6NbzVgM8InEs8wwbeIN4ulN2+C8TxxieVklPiceM+mCxI9cVzx+45xzWeCZITOZmCcOEYu5JlaamOVNjXiKOKxqOuULKY9VzluctWKZ1e/JXxjM6CvLXKc5hBgWsQQJIhSUUUARNiK06qRYSNB+tIV/0PVL5FLIVQAjxwJK0CC7fvA/+N2tlZ2c8JKCUaD9xXE+hoHALlCrOM73sePUTgD/M3ClN/ylKjDzSXqloYWPgN5t4OK6oSl7wOUOMPBkyKbsSn6aQjYLvJ/RN6WB/lugc83rrb6P0wcgSV3Fb4CDQ2AkR9nrLd7d0dzbv2fq/f0ARfNylZJUgMQAAAAGYktHRABuAP8AAGHZRr4AAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnAhELEhgyPeVkAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAyVJREFUeNrt1rEJgDAURVGVNCmS2hS6PziCteIYWjuEbiEfOWeEV1xe35bt6QhjnlYjBJLzbYRABhMAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBbwX6m13QqB5HwbIZBSLyN4WACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBfCFVMtphUBKvYwQSBsPI3hYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIF8IUXjtUMuBMh1xAAAAAASUVORK5CYII=',
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileTreePathContext.Provider>
|
||||
)
|
||||
|
||||
export default {
|
||||
title: 'Editor / Source Editor',
|
||||
component: SourceEditor,
|
||||
decorators: [
|
||||
(Story: any) =>
|
||||
ScopeDecorator(Story, {
|
||||
mockCompileOnLoad: true,
|
||||
providers: { FileTreePathProvider },
|
||||
}),
|
||||
(Story: any) => (
|
||||
<div style={{ height: '90vh' }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
const settings = {
|
||||
fontSize: 12,
|
||||
fontFamily: 'monaco',
|
||||
lineHeight: 'normal',
|
||||
editorTheme: 'textmate',
|
||||
overallTheme: '',
|
||||
mode: 'default',
|
||||
autoComplete: true,
|
||||
autoPairDelimiters: true,
|
||||
trackChanges: true,
|
||||
syntaxValidation: false,
|
||||
}
|
||||
|
||||
const permissions = {
|
||||
write: true,
|
||||
}
|
||||
|
||||
export const Latex = (args: any, { globals: { theme } }: any) => {
|
||||
// FIXME: useScope has no effect
|
||||
useScope({
|
||||
editor: {
|
||||
sharejs_doc: mockDoc(content.tex, changes.tex),
|
||||
open_doc_name: 'example.tex',
|
||||
},
|
||||
rootFolder: {
|
||||
name: 'rootFolder',
|
||||
id: 'root-folder-id',
|
||||
type: 'folder',
|
||||
children: [
|
||||
{
|
||||
name: 'example.tex.tex',
|
||||
id: 'example-doc-id',
|
||||
type: 'doc',
|
||||
selected: false,
|
||||
$$hashKey: 'object:89',
|
||||
},
|
||||
{
|
||||
name: 'frog.jpg',
|
||||
id: 'frog-image-id',
|
||||
type: 'file',
|
||||
linkedFileData: null,
|
||||
created: '2023-05-04T16:11:04.352Z',
|
||||
$$hashKey: 'object:108',
|
||||
},
|
||||
],
|
||||
selected: false,
|
||||
},
|
||||
settings: {
|
||||
...settings,
|
||||
overallTheme: theme === 'default-' ? '' : theme,
|
||||
},
|
||||
permissions,
|
||||
})
|
||||
|
||||
useMeta({
|
||||
'ol-showSymbolPalette': true,
|
||||
})
|
||||
|
||||
return <SourceEditor />
|
||||
}
|
||||
|
||||
export const Markdown = (args: any, { globals: { theme } }: any) => {
|
||||
useScope({
|
||||
editor: {
|
||||
sharejs_doc: mockDoc(content.md, changes.md),
|
||||
open_doc_name: 'example.md',
|
||||
},
|
||||
settings: {
|
||||
...settings,
|
||||
overallTheme: theme === 'default-' ? '' : theme,
|
||||
},
|
||||
permissions,
|
||||
})
|
||||
|
||||
return <SourceEditor />
|
||||
}
|
||||
|
||||
export const Visual = (args: any, { globals: { theme } }: any) => {
|
||||
useScope({
|
||||
editor: {
|
||||
sharejs_doc: mockDoc(content.tex, changes.tex),
|
||||
open_doc_name: 'example.tex',
|
||||
showVisual: true,
|
||||
},
|
||||
settings: {
|
||||
...settings,
|
||||
overallTheme: theme === 'default-' ? '' : theme,
|
||||
},
|
||||
permissions,
|
||||
})
|
||||
useMeta({
|
||||
'ol-showSymbolPalette': true,
|
||||
'ol-mathJaxPath': 'https://unpkg.com/mathjax@3.2.2/es5/tex-svg-full.js',
|
||||
'ol-project_id': '63e21c07946dd8c76505f85a',
|
||||
})
|
||||
|
||||
return <SourceEditor />
|
||||
}
|
||||
|
||||
export const Bibtex = (args: any, { globals: { theme } }: any) => {
|
||||
useScope({
|
||||
editor: {
|
||||
sharejs_doc: mockDoc(content.bib, changes.bib),
|
||||
open_doc_name: 'example.bib',
|
||||
},
|
||||
settings: {
|
||||
...settings,
|
||||
overallTheme: theme === 'default-' ? '' : theme,
|
||||
},
|
||||
permissions,
|
||||
})
|
||||
|
||||
return <SourceEditor />
|
||||
}
|
||||
|
||||
const MAX_DOC_LENGTH = 2 * 1024 * 1024 // ol-maxDocLength
|
||||
|
||||
const mockDoc = (content: string, changes: Array<Record<string, any>> = []) => {
|
||||
const mockShareJSDoc = {
|
||||
getText() {
|
||||
return content
|
||||
},
|
||||
on() {
|
||||
// do nothing
|
||||
},
|
||||
insert() {
|
||||
// do nothing
|
||||
},
|
||||
del() {
|
||||
// do nothing
|
||||
},
|
||||
emit: (...args: any[]) => {
|
||||
console.log(...args)
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
doc_id: 'story-doc',
|
||||
getSnapshot: () => {
|
||||
return content
|
||||
},
|
||||
attachToCM6: (cm6: any) => {
|
||||
cm6.attachShareJs(mockShareJSDoc, MAX_DOC_LENGTH)
|
||||
},
|
||||
detachFromCM6: () => {
|
||||
// Do nothing
|
||||
},
|
||||
on: () => {
|
||||
// Do nothing
|
||||
},
|
||||
off: () => {
|
||||
// Do nothing
|
||||
},
|
||||
setTrackChangesIdSeeds: () => {
|
||||
// Do nothing
|
||||
},
|
||||
setTrackingChanges: () => {
|
||||
// Do nothing
|
||||
},
|
||||
getTrackingChanges: () => {
|
||||
return true
|
||||
},
|
||||
getInflightOp: () => {
|
||||
return null
|
||||
},
|
||||
getPendingOp: () => {
|
||||
return null
|
||||
},
|
||||
ranges: new RangesTracker(changes, []),
|
||||
hasBufferedOps: () => false,
|
||||
}
|
||||
}
|
||||
|
||||
const changes: Record<string, Array<Record<string, any>>> = {
|
||||
tex: [
|
||||
{
|
||||
id: '1',
|
||||
op: {
|
||||
i: 'Your introduction goes here! Simply start writing your document and use the Recompile button to view the updated PDF preview. Examples of commonly used commands and features are listed below, to help you get started.',
|
||||
p: 583,
|
||||
},
|
||||
meta: {
|
||||
user_id: '1',
|
||||
ts: new Date().toString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
md: [],
|
||||
bib: [],
|
||||
}
|
||||
|
||||
const content = {
|
||||
tex: `\\documentclass{article}
|
||||
|
||||
% Language setting
|
||||
% Replace \`english' with e.g. \`spanish' to change the document language
|
||||
\\usepackage[english]{babel}
|
||||
|
||||
% Set page size and margins
|
||||
% Replace \`letterpaper' with \`a4paper' for UK/EU standard size
|
||||
\\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry}
|
||||
|
||||
% Useful packages
|
||||
\\usepackage{amsmath}
|
||||
\\usepackage{graphicx}
|
||||
\\usepackage[colorlinks=true, allcolors=blue]{hyperref}
|
||||
|
||||
\\title{Your Paper}
|
||||
\\author{You}
|
||||
|
||||
\\begin{document}
|
||||
\\maketitle
|
||||
|
||||
\\begin{abstract}
|
||||
Your abstract.
|
||||
\\end{abstract}
|
||||
|
||||
\\section{Introduction}
|
||||
|
||||
Your introduction goes here! Simply start writing your document and use the Recompile button to view the updated PDF preview. Examples of commonly used commands and features are listed below, to help you get started.
|
||||
|
||||
Once you're familiar with the editor, you can find various project settings in the Overleaf menu, accessed via the button in the very top left of the editor. To view tutorials, user guides, and further documentation, please visit our \\href{https://www.overleaf.com/learn}{help library}, or head to our plans page to \\href{https://www.overleaf.com/user/subscription/plans}{choose your plan}.
|
||||
|
||||
\\section{Some examples to get started}
|
||||
|
||||
\\subsection{How to create Sections and Subsections}
|
||||
|
||||
Simply use the section and subsection commands, as in this example document! With Overleaf, all the formatting and numbering is handled automatically according to the template you've chosen. If you're using Rich Text mode, you can also create new section and subsections via the buttons in the editor toolbar.
|
||||
|
||||
\\subsection{How to include Figures}
|
||||
|
||||
First you have to upload the image file from your computer using the upload link in the file-tree menu. Then use the includegraphics command to include it in your document. Use the figure environment and the caption command to add a number and a caption to your figure. See the code for Figure \\ref{fig:frog} in this section for an example.
|
||||
|
||||
Note that your figure will automatically be placed in the most appropriate place for it, given the surrounding text and taking into account other figures or tables that may be close by. You can find out more about adding images to your documents in this help article on \\href{https://www.overleaf.com/learn/how-to/Including_images_on_Overleaf}{including images on Overleaf}.
|
||||
|
||||
\\begin{figure}
|
||||
\\centering
|
||||
\\includegraphics[width=0.25\\linewidth]{frog.jpg}
|
||||
\\caption{This frog was uploaded via the file-tree menu.}\\label{fig:frog}
|
||||
\\end{figure}
|
||||
|
||||
\\subsection{How to add Tables}
|
||||
|
||||
Use the table and tabular environments for basic tables --- see Table~\\ref{tab:widgets}, for example. For more information, please see this help article on \\href{https://www.overleaf.com/learn/latex/tables}{tables}.
|
||||
|
||||
\\begin{table}
|
||||
\\centering
|
||||
\\begin{tabular}{l|r}
|
||||
Item & Quantity \\\\\\hline
|
||||
Widgets & 42 \\\\
|
||||
Gadgets & 13
|
||||
\\end{tabular}
|
||||
\\caption{\\label{tab:widgets}An example table.}
|
||||
\\end{table}
|
||||
|
||||
\\subsection{How to add Comments and Track Changes}
|
||||
|
||||
Comments can be added to your project by highlighting some text and clicking \`\`Add comment'' in the top right of the editor pane. To view existing comments, click on the Review menu in the toolbar above. To reply to a comment, click on the Reply button in the lower right corner of the comment. You can close the Review pane by clicking its name on the toolbar when you're done reviewing for the time being.
|
||||
|
||||
Track changes are available on all our \\href{https://www.overleaf.com/user/subscription/plans}{premium plans}, and can be toggled on or off using the option at the top of the Review pane. Track changes allow you to keep track of every change made to the document, along with the person making the change.
|
||||
|
||||
\\subsection{How to add Lists}
|
||||
|
||||
You can make lists with automatic numbering \\dots
|
||||
|
||||
\\begin{enumerate}
|
||||
\\item Like this,
|
||||
\\item and like this.
|
||||
\\end{enumerate}
|
||||
\\dots or bullet points \\dots
|
||||
\\begin{itemize}
|
||||
\\item Like this,
|
||||
\\item and like this.
|
||||
\\end{itemize}
|
||||
|
||||
\\subsection{How to write Mathematics}
|
||||
|
||||
\\LaTeX{} is great at typesetting mathematics. Let $X_1, X_2, \\ldots, X_n$ be a sequence of independent and identically distributed random variables with $\\text{E}[X_i] = \\mu$ and $\\text{Var}[X_i] = \\sigma^2 < \\infty$, and let
|
||||
\\[S_n = \\frac{X_1 + X_2 + \\cdots + X_n}{n}
|
||||
= \\frac{1}{n}\\sum_{i}^{n} X_i\\]
|
||||
denote their mean. Then as $n$ approaches infinity, the random variables $\\sqrt{n}(S_n - \\mu)$ converge in distribution to a normal $\\mathcal{N}(0, \\sigma^2)$.
|
||||
|
||||
|
||||
\\subsection{How to change the margins and paper size}
|
||||
|
||||
Usually the template you're using will have the page margins and paper size set correctly for that use-case. For example, if you're using a journal article template provided by the journal publisher, that template will be formatted according to their requirements. In these cases, it's best not to alter the margins directly.
|
||||
|
||||
If however you're using a more general template, such as this one, and would like to alter the margins, a common way to do so is via the geometry package. You can find the geometry package loaded in the preamble at the top of this example file, and if you'd like to learn more about how to adjust the settings, please visit this help article on \\href{https://www.overleaf.com/learn/latex/page_size_and_margins}{page size and margins}.
|
||||
|
||||
\\subsection{How to change the document language and spell check settings}
|
||||
|
||||
Overleaf supports many different languages, including multiple different languages within one document.
|
||||
|
||||
To configure the document language, simply edit the option provided to the babel package in the preamble at the top of this example project. To learn more about the different options, please visit this help article on \\href{https://www.overleaf.com/learn/latex/International_language_support}{international language support}.
|
||||
|
||||
To change the spell check language, simply open the Overleaf menu at the top left of the editor window, scroll down to the spell check setting, and adjust accordingly.
|
||||
|
||||
\\subsection{How to add Citations and a References List}
|
||||
|
||||
You can simply upload a \\verb|.bib| file containing your BibTeX entries, created with a tool such as JabRef. You can then cite entries from it, like this: \\cite{greenwade93}. Just remember to specify a bibliography style, as well as the filename of the \\verb|.bib|. You can find a \\href{https://www.overleaf.com/help/97-how-to-include-a-bibliography-using-bibtex}{video tutorial here} to learn more about BibTeX.
|
||||
|
||||
If you have an \\href{https://www.overleaf.com/user/subscription/plans}{upgraded account}, you can also import your Mendeley or Zotero library directly as a \\verb|.bib| file, via the upload menu in the file-tree.
|
||||
|
||||
\\subsection{Good luck!}
|
||||
|
||||
We hope you find Overleaf useful, and do take a look at our \\href{https://www.overleaf.com/learn}{help library} for more tutorials and user guides! Please also let us know if you have any feedback using the Contact Us link at the bottom of the Overleaf menu --- or use the contact form at \\url{https://www.overleaf.com/contact}.
|
||||
|
||||
\\bibliographystyle{alpha}
|
||||
\\bibliography{sample}
|
||||
|
||||
\\end{document}`,
|
||||
md: `# Heading
|
||||
|
||||
This is **bold**
|
||||
|
||||
This is _italic_`,
|
||||
bib: `@book{texbook,
|
||||
author = {Donald E. Knuth},
|
||||
year = {1986},
|
||||
title = {The {\\TeX} Book},
|
||||
publisher = {Addison-Wesley Professional}
|
||||
}
|
||||
|
||||
@book{latex:companion,
|
||||
author = {Frank Mittelbach and Michel Gossens
|
||||
and Johannes Braams and David Carlisle
|
||||
and Chris Rowley},
|
||||
year = {2004},
|
||||
title = {The {\\LaTeX} Companion},
|
||||
publisher = {Addison-Wesley Professional},
|
||||
edition = {2}
|
||||
}
|
||||
|
||||
@book{latex2e,
|
||||
author = {Leslie Lamport},
|
||||
year = {1994},
|
||||
title = {{\\LaTeX}: a Document Preparation System},
|
||||
publisher = {Addison Wesley},
|
||||
address = {Massachusetts},
|
||||
edition = {2}
|
||||
}
|
||||
|
||||
@article{knuth:1984,
|
||||
title={Literate Programming},
|
||||
author={Donald E. Knuth},
|
||||
journal={The Computer Journal},
|
||||
volume={27},
|
||||
number={2},
|
||||
pages={97--111},
|
||||
year={1984},
|
||||
publisher={Oxford University Press}
|
||||
}
|
||||
|
||||
@inproceedings{lesk:1977,
|
||||
title={Computer Typesetting of Technical Journals on {UNIX}},
|
||||
author={Michael Lesk and Brian Kernighan},
|
||||
booktitle={Proceedings of American Federation of
|
||||
Information Processing Societies: 1977
|
||||
National Computer Conference},
|
||||
pages={879--888},
|
||||
year={1977},
|
||||
address={Dallas, Texas}
|
||||
}
|
||||
`,
|
||||
}
|
137
services/web/frontend/stories/split-test-badge.stories.jsx
Normal file
137
services/web/frontend/stories/split-test-badge.stories.jsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import SplitTestBadge from '../js/shared/components/split-test-badge'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
import { SplitTestContext } from '../js/shared/context/split-test-context'
|
||||
|
||||
const splitTestContextValue = {
|
||||
splitTestVariants: {
|
||||
'storybook-test': 'active',
|
||||
},
|
||||
splitTestInfo: {
|
||||
'storybook-test': {
|
||||
phase: 'alpha',
|
||||
badgeInfo: {
|
||||
url: '/alpha/participate',
|
||||
tooltipText: 'This is an alpha feature',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Alpha = args => {
|
||||
splitTestContextValue.splitTestVariants = {
|
||||
'storybook-test': 'active',
|
||||
}
|
||||
splitTestContextValue.splitTestInfo = {
|
||||
'storybook-test': {
|
||||
phase: 'alpha',
|
||||
badgeInfo: {
|
||||
url: '/alpha/participate',
|
||||
tooltipText: 'This is an alpha feature',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return <SplitTestBadge {...args} />
|
||||
}
|
||||
|
||||
export const AlphaNotDisplayed = args => {
|
||||
splitTestContextValue.splitTestVariants = {
|
||||
'storybook-test': 'default',
|
||||
}
|
||||
splitTestContextValue.splitTestInfo = {
|
||||
'storybook-test': {
|
||||
phase: 'alpha',
|
||||
badgeInfo: {
|
||||
url: '/alpha/participate',
|
||||
tooltipText: 'This is an alpha feature',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return <SplitTestBadge {...args} />
|
||||
}
|
||||
|
||||
export const Beta = args => {
|
||||
splitTestContextValue.splitTestVariants = {
|
||||
'storybook-test': 'active',
|
||||
}
|
||||
splitTestContextValue.splitTestInfo = {
|
||||
'storybook-test': {
|
||||
phase: 'beta',
|
||||
badgeInfo: {
|
||||
url: '/beta/participate',
|
||||
tooltipText: 'This is a beta feature',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return <SplitTestBadge {...args} />
|
||||
}
|
||||
|
||||
export const BetaNotDisplayed = args => {
|
||||
splitTestContextValue.splitTestVariants = {
|
||||
'storybook-test': 'default',
|
||||
}
|
||||
splitTestContextValue.splitTestInfo = {
|
||||
'storybook-test': {
|
||||
phase: 'beta',
|
||||
badgeInfo: {
|
||||
url: '/beta/participate',
|
||||
tooltipText: 'This is a beta feature',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return <SplitTestBadge {...args} />
|
||||
}
|
||||
|
||||
export const Release = args => {
|
||||
splitTestContextValue.splitTestVariants = {
|
||||
'storybook-test': 'active',
|
||||
}
|
||||
splitTestContextValue.splitTestInfo = {
|
||||
'storybook-test': {
|
||||
phase: 'release',
|
||||
badgeInfo: {
|
||||
url: '/feedback/form',
|
||||
tooltipText: 'This is a new feature',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return <SplitTestBadge {...args} />
|
||||
}
|
||||
|
||||
export const ReleaseNotDisplayed = args => {
|
||||
splitTestContextValue.splitTestVariants = {
|
||||
'storybook-test': 'default',
|
||||
}
|
||||
splitTestContextValue.splitTestInfo = {
|
||||
'storybook-test': {
|
||||
phase: 'release',
|
||||
badgeInfo: {
|
||||
url: '/feedback/form',
|
||||
tooltipText: 'This is a new feature',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return <SplitTestBadge {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Split Test Badge',
|
||||
component: SplitTestBadge,
|
||||
args: {
|
||||
splitTestName: 'storybook-test',
|
||||
displayOnVariants: ['active'],
|
||||
},
|
||||
decorators: [
|
||||
(Story, context) => (
|
||||
<SplitTestContext.Provider value={splitTestContextValue}>
|
||||
<Story />
|
||||
</SplitTestContext.Provider>
|
||||
),
|
||||
ScopeDecorator,
|
||||
],
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
import StartFreeTrialButton from '../js/shared/components/start-free-trial-button'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
|
||||
export const Default = args => {
|
||||
return <StartFreeTrialButton {...args} />
|
||||
}
|
||||
|
||||
export const CustomText = args => {
|
||||
return (
|
||||
<StartFreeTrialButton {...args}>Some Custom Text!</StartFreeTrialButton>
|
||||
)
|
||||
}
|
||||
|
||||
export const ButtonStyle = args => {
|
||||
return (
|
||||
<StartFreeTrialButton
|
||||
{...args}
|
||||
buttonProps={{
|
||||
variant: 'danger',
|
||||
size: 'lg',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Start Free Trial Button',
|
||||
component: StartFreeTrialButton,
|
||||
args: {
|
||||
source: 'storybook',
|
||||
},
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
import GroupInvites from '@/features/subscription/components/group-invites/group-invites'
|
||||
import type { TeamInvite } from '../../../../types/team-invite'
|
||||
import { useMeta } from '../../hooks/use-meta'
|
||||
import { ScopeDecorator } from '../../decorators/scope'
|
||||
|
||||
export const GroupInvitesDefault = () => {
|
||||
const teamInvites: TeamInvite[] = [
|
||||
{
|
||||
email: 'email1@exammple.com',
|
||||
token: 'token123',
|
||||
inviterName: 'inviter1@example.com',
|
||||
sentAt: new Date(),
|
||||
_id: '123abc',
|
||||
},
|
||||
{
|
||||
email: 'email2@exammple.com',
|
||||
token: 'token456',
|
||||
inviterName: 'inviter2@example.com',
|
||||
sentAt: new Date(),
|
||||
_id: '456bcd',
|
||||
},
|
||||
]
|
||||
|
||||
useMeta({ 'ol-teamInvites': teamInvites })
|
||||
|
||||
return (
|
||||
<div className="content content-alt team-invite">
|
||||
<GroupInvites />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Subscription / Group Invites',
|
||||
component: GroupInvites,
|
||||
args: {
|
||||
show: true,
|
||||
},
|
||||
argTypes: {
|
||||
handleHide: { action: 'close modal' },
|
||||
onDisableSSO: { action: 'callback' },
|
||||
},
|
||||
decorators: [ScopeDecorator],
|
||||
}
|
55
services/web/frontend/stories/switcher.stories.tsx
Normal file
55
services/web/frontend/stories/switcher.stories.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import OLToggleButton from '@/features/ui/components/ol/ol-toggle-button'
|
||||
import OLToggleButtonGroup from '@/features/ui/components/ol/ol-toggle-button-group'
|
||||
|
||||
export const Base = () => {
|
||||
return (
|
||||
<OLToggleButtonGroup
|
||||
type="radio"
|
||||
name="figure-width"
|
||||
defaultValue="0.5"
|
||||
aria-label="Image width"
|
||||
>
|
||||
<OLToggleButton variant="secondary" id="width-25p" value="0.25">
|
||||
¼ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-50p" value="0.5">
|
||||
½ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-75p" value="0.75">
|
||||
¾ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-100p" value="1.0">
|
||||
Full width
|
||||
</OLToggleButton>
|
||||
</OLToggleButtonGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export const Disabled = () => {
|
||||
return (
|
||||
<OLToggleButtonGroup
|
||||
type="radio"
|
||||
name="figure-width"
|
||||
defaultValue="0.5"
|
||||
aria-label="Image width"
|
||||
>
|
||||
<OLToggleButton variant="secondary" id="width-25p" disabled value="0.25">
|
||||
¼ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-50p" disabled value="0.5">
|
||||
½ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-75p" disabled value="0.75">
|
||||
¾ width
|
||||
</OLToggleButton>
|
||||
<OLToggleButton variant="secondary" id="width-100p" disabled value="1.0">
|
||||
Full width
|
||||
</OLToggleButton>
|
||||
</OLToggleButtonGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Shared / Components / Toggle Button Group',
|
||||
component: OLToggleButtonGroup,
|
||||
}
|
60
services/web/frontend/stories/ui/badge.stories.tsx
Normal file
60
services/web/frontend/stories/ui/badge.stories.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import Badge from '@/features/ui/components/bootstrap-5/badge'
|
||||
import Icon from '@/shared/components/icon'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const meta: Meta<typeof Badge> = {
|
||||
title: 'Shared / Components / Badge',
|
||||
component: Badge,
|
||||
args: {
|
||||
children: 'Badge',
|
||||
},
|
||||
argTypes: {
|
||||
bg: {
|
||||
options: ['light', 'info', 'primary', 'warning', 'danger'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
prepend: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
className: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Badge>
|
||||
|
||||
export const BadgeDefault: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<Badge
|
||||
className={classnames({ 'text-dark': args.bg === 'light' })}
|
||||
{...args}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
BadgeDefault.args = {
|
||||
bg: meta.argTypes!.bg!.options![0],
|
||||
}
|
||||
|
||||
export const BadgePrepend: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<Badge
|
||||
className={classnames({ 'text-dark': args.bg === 'light' })}
|
||||
prepend={<Icon type="star" fw />}
|
||||
{...args}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
BadgePrepend.args = {
|
||||
bg: meta.argTypes!.bg!.options![0],
|
||||
}
|
50
services/web/frontend/stories/ui/button.stories.tsx
Normal file
50
services/web/frontend/stories/ui/button.stories.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Button from '@/features/ui/components/bootstrap-5/button'
|
||||
import { Meta } from '@storybook/react'
|
||||
|
||||
type Args = React.ComponentProps<typeof Button>
|
||||
|
||||
export const NewButton = (args: Args) => {
|
||||
return <Button {...args} />
|
||||
}
|
||||
|
||||
export const ButtonWithLeadingIcon = (args: Args) => {
|
||||
return <Button leadingIcon="add" {...args} />
|
||||
}
|
||||
|
||||
export const ButtonWithTrailingIcon = (args: Args) => {
|
||||
return <Button trailingIcon="add" {...args} />
|
||||
}
|
||||
|
||||
export const ButtonWithIcons = (args: Args) => {
|
||||
return <Button trailingIcon="add" leadingIcon="add" {...args} />
|
||||
}
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'Shared / Components / Button',
|
||||
component: Button,
|
||||
args: {
|
||||
children: 'A Button',
|
||||
disabled: false,
|
||||
isLoading: false,
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'radio',
|
||||
options: ['small', 'default', 'large'],
|
||||
},
|
||||
variant: {
|
||||
control: 'radio',
|
||||
options: [
|
||||
'primary',
|
||||
'secondary',
|
||||
'ghost',
|
||||
'danger',
|
||||
'danger-ghost',
|
||||
'premium',
|
||||
'premium-secondary',
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
236
services/web/frontend/stories/ui/dropdown-menu.stories.tsx
Normal file
236
services/web/frontend/stories/ui/dropdown-menu.stories.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
DropdownDivider,
|
||||
DropdownHeader,
|
||||
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import type { Meta } from '@storybook/react'
|
||||
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
|
||||
type Args = React.ComponentProps<typeof DropdownMenu>
|
||||
|
||||
export const Default = (args: Args) => {
|
||||
return (
|
||||
<DropdownMenu show>
|
||||
<li>
|
||||
<DropdownItem eventKey="1" href="#/action-1">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem eventKey="2" href="#/action-2">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<DropdownDivider />
|
||||
<li>
|
||||
<DropdownItem eventKey="3" disabled={args.disabled} href="#/action-3">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export const Active = (args: Args) => {
|
||||
return (
|
||||
<DropdownMenu show>
|
||||
<li>
|
||||
<DropdownItem eventKey="1" href="#/action-1">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem
|
||||
eventKey="2"
|
||||
active
|
||||
href="#/action-2"
|
||||
trailingIcon="check"
|
||||
>
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<DropdownDivider />
|
||||
<li>
|
||||
<DropdownItem eventKey="3" disabled={args.disabled} href="#/action-3">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export const MultipleSelection = (args: Args) => {
|
||||
return (
|
||||
<DropdownMenu show>
|
||||
<DropdownHeader>Header</DropdownHeader>
|
||||
<li>
|
||||
<DropdownItem
|
||||
eventKey="1"
|
||||
href="#/action-1"
|
||||
leadingIcon={<DropdownItem.EmptyLeadingIcon />}
|
||||
>
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem eventKey="2" href="#/action-2" leadingIcon="check">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem eventKey="3" href="#/action-3" leadingIcon="check">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export const Danger = (args: Args) => {
|
||||
return (
|
||||
<DropdownMenu show>
|
||||
<li>
|
||||
<DropdownItem eventKey="1" disabled={args.disabled} href="#/action-1">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem eventKey="2" href="#/action-2">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<DropdownDivider />
|
||||
<li>
|
||||
<DropdownItem eventKey="3" href="#/action-3" variant="danger">
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export const Description = (args: Args) => {
|
||||
return (
|
||||
<DropdownMenu show>
|
||||
<li>
|
||||
<DropdownItem
|
||||
disabled={args.disabled}
|
||||
eventKey="1"
|
||||
href="#/action-1"
|
||||
description="Description of the menu"
|
||||
>
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem
|
||||
active
|
||||
eventKey="2"
|
||||
href="#/action-2"
|
||||
description="Description of the menu"
|
||||
trailingIcon="check"
|
||||
>
|
||||
Example
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export const LeadingIcon = (args: Args) => {
|
||||
return (
|
||||
<DropdownMenu show>
|
||||
<OLDropdownMenuItem
|
||||
disabled={args.disabled}
|
||||
eventKey="1"
|
||||
href="#/action-1"
|
||||
leadingIcon="view_column_2"
|
||||
>
|
||||
Editor & PDF
|
||||
</OLDropdownMenuItem>
|
||||
<OLDropdownMenuItem
|
||||
active
|
||||
eventKey="2"
|
||||
href="#/action-2"
|
||||
leadingIcon="terminal"
|
||||
>
|
||||
Editor only
|
||||
</OLDropdownMenuItem>
|
||||
<OLDropdownMenuItem
|
||||
eventKey="3"
|
||||
href="#/action-3"
|
||||
leadingIcon="picture_as_pdf"
|
||||
>
|
||||
PDF only
|
||||
</OLDropdownMenuItem>
|
||||
<OLDropdownMenuItem
|
||||
eventKey="4"
|
||||
href="#/action-4"
|
||||
leadingIcon="select_window"
|
||||
>
|
||||
PDF in separate tab
|
||||
</OLDropdownMenuItem>
|
||||
<OLDropdownMenuItem
|
||||
eventKey="5"
|
||||
href="#/action-5"
|
||||
leadingIcon="align_space_even"
|
||||
description="Some description"
|
||||
>
|
||||
With a description
|
||||
</OLDropdownMenuItem>
|
||||
<OLDropdownMenuItem
|
||||
eventKey="6"
|
||||
href="#/action-6"
|
||||
leadingIcon="align_space_even"
|
||||
className="dropdown-item-material-icon-small"
|
||||
>
|
||||
Small icon
|
||||
</OLDropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export const TrailingIcon = (args: Args) => {
|
||||
return (
|
||||
<DropdownMenu show>
|
||||
<OLDropdownMenuItem eventKey="1" href="#/action-1" trailingIcon="check">
|
||||
Tick
|
||||
</OLDropdownMenuItem>
|
||||
<OLDropdownMenuItem
|
||||
eventKey="2"
|
||||
href="#/action-2"
|
||||
trailingIcon="check"
|
||||
description="Some description"
|
||||
>
|
||||
With a description
|
||||
</OLDropdownMenuItem>
|
||||
<OLDropdownMenuItem
|
||||
eventKey="3"
|
||||
href="#/action-3"
|
||||
leadingIcon="align_space_even"
|
||||
trailingIcon="check"
|
||||
description="Some description"
|
||||
>
|
||||
With a leading icon
|
||||
</OLDropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof DropdownMenu> = {
|
||||
title: 'Shared / Components / DropdownMenu',
|
||||
component: DropdownMenu,
|
||||
argTypes: {
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
},
|
||||
show: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
@@ -0,0 +1,62 @@
|
||||
import { useRef, useLayoutEffect } from 'react'
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<(typeof Form)['Check']> = {
|
||||
title: 'Shared / Components / Form',
|
||||
component: Form.Check,
|
||||
argTypes: {
|
||||
id: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
label: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
defaultChecked: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<(typeof Form)['Check']>
|
||||
|
||||
export const Checkbox: Story = {
|
||||
args: {
|
||||
id: 'id-1',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const CheckboxChecked: Story = {
|
||||
args: {
|
||||
id: 'id-1',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
defaultChecked: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const CheckboxIndeterminate = (args: Story['args']) => {
|
||||
const ref = useRef<HTMLInputElement>()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.indeterminate = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <Form.Check ref={ref} {...args} />
|
||||
}
|
||||
CheckboxIndeterminate.args = {
|
||||
id: 'id-2',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
}
|
300
services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx
Normal file
300
services/web/frontend/stories/ui/form/form-input-bs5.stories.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group'
|
||||
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import FormFeedback from '@/features/ui/components/bootstrap-5/form/form-feedback'
|
||||
|
||||
const meta: Meta<React.ComponentProps<typeof FormControl>> = {
|
||||
title: 'Shared / Components / Form / Input',
|
||||
component: FormControl,
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<React.ComponentProps<typeof FormControl>>
|
||||
|
||||
export const Default: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl defaultValue="Large input" size="lg" {...args} />
|
||||
<FormText>Helper</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl defaultValue="Regular input" {...args} />
|
||||
<FormText>Helper</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl defaultValue="Small input" size="sm" {...args} />
|
||||
<FormText>Helper</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
Default.args = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
export const Info: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="info">Info</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="info">Info</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="info">Info</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormFeedback type="invalid">Error</FormFeedback>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormFeedback type="invalid">Error</FormFeedback>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormFeedback type="invalid">Error</FormFeedback>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Warning: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="warning">Warning</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="warning">Warning</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="warning">Warning</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="success">Success</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="success">Success</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="success">Success</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const WithIcons: Story = {
|
||||
render: args => {
|
||||
const handleClear = () => {
|
||||
alert('Clicked clear button')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
prepend={<MaterialIcon type="search" />}
|
||||
append={
|
||||
<button
|
||||
type="button"
|
||||
className="form-control-search-clear-btn"
|
||||
onClick={handleClear}
|
||||
>
|
||||
<MaterialIcon type="clear" />
|
||||
</button>
|
||||
}
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
prepend={<MaterialIcon type="search" />}
|
||||
append={
|
||||
<button
|
||||
type="button"
|
||||
className="form-control-search-clear-btn"
|
||||
onClick={handleClear}
|
||||
>
|
||||
<MaterialIcon type="clear" />
|
||||
</button>
|
||||
}
|
||||
{...args}
|
||||
/>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
prepend={<MaterialIcon type="search" />}
|
||||
append={
|
||||
<button
|
||||
type="button"
|
||||
className="form-control-search-clear-btn"
|
||||
onClick={handleClear}
|
||||
>
|
||||
<MaterialIcon type="clear" />
|
||||
</button>
|
||||
}
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
</FormGroup>
|
||||
<br />
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Disabled state</Form.Label>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
prepend={<MaterialIcon type="search" />}
|
||||
append={
|
||||
<button
|
||||
type="button"
|
||||
className="form-control-search-clear-btn"
|
||||
onClick={handleClear}
|
||||
disabled
|
||||
>
|
||||
<MaterialIcon type="clear" />
|
||||
</button>
|
||||
}
|
||||
disabled
|
||||
{...args}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<(typeof Form)['Check']> = {
|
||||
title: 'Shared / Components / Form',
|
||||
component: Form.Check,
|
||||
argTypes: {
|
||||
id: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
label: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
defaultChecked: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<(typeof Form)['Check']>
|
||||
|
||||
export const Radio: Story = {
|
||||
args: {
|
||||
id: 'id-1',
|
||||
type: 'radio',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const RadioChecked: Story = {
|
||||
args: {
|
||||
id: 'id-1',
|
||||
type: 'radio',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
defaultChecked: true,
|
||||
},
|
||||
}
|
@@ -0,0 +1,220 @@
|
||||
import { Form, FormSelectProps } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group'
|
||||
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
|
||||
const meta: Meta<FormSelectProps> = {
|
||||
title: 'Shared / Components / Form / Select',
|
||||
component: Form.Select,
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<FormSelectProps>
|
||||
|
||||
export const Default: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText>Helper</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText>Helper</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText>Helper</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
Default.args = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
export const Info: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="info">Info</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="info">Info</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="info">Info</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" isInvalid {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="error">Error</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select isInvalid {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="error">Error</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" isInvalid {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="error">Error</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Warning: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="warning">Warning</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="warning">Warning</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="warning">Warning</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="lg" {...args}>
|
||||
<option>Large select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="success">Success</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select {...args}>
|
||||
<option>Regular select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="success">Success</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Select size="sm" {...args}>
|
||||
<option>Small select</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
</Form.Select>
|
||||
<FormText type="success">Success</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
@@ -0,0 +1,227 @@
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group'
|
||||
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
|
||||
|
||||
const meta: Meta<React.ComponentProps<typeof FormControl>> = {
|
||||
title: 'Shared / Components / Form / Textarea',
|
||||
component: FormControl,
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<React.ComponentProps<typeof FormControl>>
|
||||
|
||||
export const Default: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText>Helper</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl as="textarea" defaultValue="Regular input" {...args} />
|
||||
<FormText>Helper</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText>Helper</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
Default.args = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
export const Info: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="info">Info</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="info">Info</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="info">Info</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="error">Error</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="error">Error</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
isInvalid
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="error">Error</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Warning: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="warning">Warning</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="warning">Warning</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="warning">Warning</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<>
|
||||
<FormGroup controlId="id-1">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Large input"
|
||||
size="lg"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="success">Success</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-2">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Regular input"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="success">Success</FormText>
|
||||
</FormGroup>
|
||||
<hr />
|
||||
<FormGroup controlId="id-3">
|
||||
<Form.Label>Label</Form.Label>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder="Placeholder"
|
||||
defaultValue="Small input"
|
||||
size="sm"
|
||||
{...args}
|
||||
/>
|
||||
<FormText type="success">Success</FormText>
|
||||
</FormGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
40
services/web/frontend/stories/ui/icon-button.stories.tsx
Normal file
40
services/web/frontend/stories/ui/icon-button.stories.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import IconButton from '@/features/ui/components/bootstrap-5/icon-button'
|
||||
import type { Meta } from '@storybook/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Args = React.ComponentProps<typeof IconButton>
|
||||
|
||||
export const Icon = (args: Args) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return <IconButton accessibilityLabel={t('add')} disabled {...args} />
|
||||
}
|
||||
|
||||
const meta: Meta<typeof IconButton> = {
|
||||
title: 'Shared / Components / IconButton',
|
||||
component: IconButton,
|
||||
args: {
|
||||
disabled: false,
|
||||
icon: 'add',
|
||||
isLoading: false,
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'radio',
|
||||
options: ['small', 'default', 'large'],
|
||||
},
|
||||
variant: {
|
||||
control: 'radio',
|
||||
options: [
|
||||
'primary',
|
||||
'secondary',
|
||||
'ghost',
|
||||
'danger',
|
||||
'danger-ghost',
|
||||
'premium',
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
29
services/web/frontend/stories/ui/row.stories.tsx
Normal file
29
services/web/frontend/stories/ui/row.stories.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Container, Row, Col } from 'react-bootstrap-5'
|
||||
import { Meta } from '@storybook/react'
|
||||
|
||||
type Args = React.ComponentProps<typeof Row>
|
||||
|
||||
export const ColumnRowCell = (args: Args) => {
|
||||
return (
|
||||
<Container style={{ border: '3px solid green' }}>
|
||||
<Row {...args} style={{ border: '1px solid black' }}>
|
||||
<Col sm={6} style={{ border: '1px solid red' }}>
|
||||
<div style={{ backgroundColor: '#ddd' }}>Col 1</div>
|
||||
</Col>
|
||||
<Col sm={6} style={{ border: '1px solid red' }}>
|
||||
<div style={{ backgroundColor: '#ddd' }}>Col 2</div>
|
||||
</Col>
|
||||
<Col sm={{ span: 10, offset: 2 }} style={{ border: '1px solid red' }}>
|
||||
<div style={{ backgroundColor: '#ddd' }}>Col 3</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof Row> = {
|
||||
title: 'Shared / Components / Column-Row-Cell',
|
||||
component: Row,
|
||||
}
|
||||
|
||||
export default meta
|
61
services/web/frontend/stories/ui/split-button.stories.tsx
Normal file
61
services/web/frontend/stories/ui/split-button.stories.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Fragment } from 'react'
|
||||
import type { Meta } from '@storybook/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownHeader,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import Button from '@/features/ui/components/bootstrap-5/button'
|
||||
import { ButtonGroup } from 'react-bootstrap-5'
|
||||
|
||||
type Args = React.ComponentProps<typeof Dropdown>
|
||||
|
||||
export const Sizes = (args: Args) => {
|
||||
const { t } = useTranslation()
|
||||
const sizes = {
|
||||
Large: 'lg',
|
||||
Regular: undefined,
|
||||
Small: 'sm',
|
||||
} as const
|
||||
const variants = ['primary', 'secondary', 'danger'] as const
|
||||
|
||||
return Object.entries(sizes).map(([label, size]) => (
|
||||
<Fragment key={`${label}-${size}`}>
|
||||
<h4>{label}</h4>
|
||||
<div style={{ display: 'inline-flex', gap: '10px' }}>
|
||||
{variants.map(variant => (
|
||||
<Dropdown key={variant} as={ButtonGroup}>
|
||||
<Button variant={variant} size={size}>
|
||||
Split Button
|
||||
</Button>
|
||||
<DropdownToggle
|
||||
split
|
||||
variant={variant}
|
||||
id={`split-btn-${variant}-${size}`}
|
||||
size={size}
|
||||
aria-label={t('expand')}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownHeader>Header</DropdownHeader>
|
||||
<DropdownItem as="button">Action 1</DropdownItem>
|
||||
<DropdownItem as="button">Action 2</DropdownItem>
|
||||
<DropdownItem as="button">Action 3</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
const meta: Meta<typeof Dropdown> = {
|
||||
title: 'Shared/Components/SplitButton',
|
||||
component: Dropdown,
|
||||
args: {
|
||||
align: { sm: 'start' },
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user