mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-30 20:31:34 +02:00
Merge pull request #26583 from overleaf/td-editor-scope-values-to-context
Move scope values starting with `editor.` to contexts GitOrigin-RevId: 7ca349ceff002228cf4e931c644c8c386eb6c597
This commit is contained in:
@@ -1,22 +1,21 @@
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels'
|
||||
import React, { FC, lazy, Suspense } from 'react'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import SourceEditor from '@/features/source-editor/components/source-editor'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { EditorScopeValue } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter'
|
||||
import classNames from 'classnames'
|
||||
import { LoadingPane } from '@/features/ide-react/components/editor/loading-pane'
|
||||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
|
||||
import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
|
||||
const SymbolPalettePane = lazy(
|
||||
() => import('@/features/ide-react/components/editor/symbol-palette-pane')
|
||||
)
|
||||
|
||||
export const EditorPane: FC = () => {
|
||||
const [editor] = useScopeValue<EditorScopeValue>('editor')
|
||||
const { showSymbolPalette } = useEditorPropertiesContext()
|
||||
const { selectedEntityCount, openEntity } = useFileTreeOpenContext()
|
||||
const { isLoading } = useEditorManagerContext()
|
||||
const { currentDocumentId } = useEditorOpenDocContext()
|
||||
@@ -41,7 +40,7 @@ export const EditorPane: FC = () => {
|
||||
{isLoading && <LoadingPane />}
|
||||
</Panel>
|
||||
|
||||
{editor.showSymbolPalette && (
|
||||
{showSymbolPalette && (
|
||||
<>
|
||||
<VerticalResizeHandle id="editor-symbol-palette" />
|
||||
<Panel
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import { useIdeContext } from '@/shared/context/ide-context'
|
||||
import { OpenDocuments } from '@/features/ide-react/editor/open-documents'
|
||||
import EditorWatchdogManager from '@/features/ide-react/connection/editor-watchdog-manager'
|
||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
@@ -37,6 +35,7 @@ import { Update } from '@/features/history/services/types/update'
|
||||
import { useDebugDiffTracker } from '../hooks/use-debug-diff-tracker'
|
||||
import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
|
||||
export interface GotoOffsetOptions {
|
||||
gotoOffset: number
|
||||
@@ -52,7 +51,6 @@ interface OpenDocOptions
|
||||
|
||||
export type EditorManager = {
|
||||
getEditorType: () => EditorType | null
|
||||
showSymbolPalette: boolean
|
||||
getCurrentDocValue: () => string | null
|
||||
getCurrentDocumentId: () => DocId | null
|
||||
setIgnoringExternalUpdates: (value: boolean) => void
|
||||
@@ -62,12 +60,7 @@ export type EditorManager = {
|
||||
openFileWithId: (fileId: string) => void
|
||||
openInitialDoc: (docId: string) => void
|
||||
isLoading: boolean
|
||||
trackChanges: boolean
|
||||
jumpToLine: (options: GotoLineOptions) => void
|
||||
wantTrackChanges: boolean
|
||||
setWantTrackChanges: React.Dispatch<
|
||||
React.SetStateAction<EditorManager['wantTrackChanges']>
|
||||
>
|
||||
debugTimers: React.MutableRefObject<Record<string, number>>
|
||||
}
|
||||
|
||||
@@ -87,7 +80,6 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { scopeStore } = useIdeContext()
|
||||
const { reportError, eventEmitter, projectId, setOutOfSync } =
|
||||
useIdeReactContext()
|
||||
const { socket, closeConnection, connectionState } = useConnectionContext()
|
||||
@@ -95,11 +87,15 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
const { showGenericMessageModal, genericModalVisible, showOutOfSyncModal } =
|
||||
useModalsContext()
|
||||
const { id: userId } = useUserContext()
|
||||
|
||||
const [showSymbolPalette, setShowSymbolPalette] = useScopeValue<boolean>(
|
||||
'editor.showSymbolPalette'
|
||||
)
|
||||
const [showVisual] = useScopeValue<boolean>('editor.showVisual')
|
||||
const {
|
||||
showVisual,
|
||||
opening,
|
||||
setOpening,
|
||||
errorState,
|
||||
setErrorState,
|
||||
setTrackChanges,
|
||||
wantTrackChanges,
|
||||
} = useEditorPropertiesContext()
|
||||
const {
|
||||
currentDocumentId,
|
||||
setCurrentDocumentId,
|
||||
@@ -107,15 +103,6 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
currentDocument,
|
||||
setCurrentDocument,
|
||||
} = useEditorOpenDocContext()
|
||||
const [opening, setOpening] = useScopeValue<boolean>('editor.opening')
|
||||
const [errorState, setIsInErrorState] =
|
||||
useScopeValue<boolean>('editor.error_state')
|
||||
const [trackChanges, setTrackChanges] = useScopeValue<boolean>(
|
||||
'editor.trackChanges'
|
||||
)
|
||||
const [wantTrackChanges, setWantTrackChanges] = useScopeValue<boolean>(
|
||||
'editor.wantTrackChanges'
|
||||
)
|
||||
|
||||
const wantTrackChangesRef = useRef(wantTrackChanges)
|
||||
useEffect(() => {
|
||||
@@ -191,22 +178,6 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
|
||||
const editorOpenDocEpochRef = useRef(0)
|
||||
|
||||
// TODO: This looks dodgy because it wraps a state setter and is itself
|
||||
// stored in React state in the scope store. The problem is that it needs to
|
||||
// be exposed via the scope store because some components access it that way;
|
||||
// it would be better to simply access it from a context, but the current
|
||||
// implementation in EditorManager interacts with Angular scope to update
|
||||
// the layout. Once Angular is gone, this can become a context method.
|
||||
useEffect(() => {
|
||||
scopeStore.set('editor.toggleSymbolPalette', () => {
|
||||
setShowSymbolPalette(show => {
|
||||
const newValue = !show
|
||||
sendMB(newValue ? 'symbol-palette-show' : 'symbol-palette-hide')
|
||||
return newValue
|
||||
})
|
||||
})
|
||||
}, [scopeStore, setShowSymbolPalette])
|
||||
|
||||
const getEditorType = useCallback((): EditorType | null => {
|
||||
if (!currentDocument) {
|
||||
return null
|
||||
@@ -587,7 +558,7 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
reportError(error, meta)
|
||||
|
||||
// Tell the user about the error state.
|
||||
setIsInErrorState(true)
|
||||
setErrorState(true)
|
||||
// Ensure that the editor is locked
|
||||
setOutOfSync(true)
|
||||
// Display the "out of sync" modal
|
||||
@@ -614,7 +585,7 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
eventEmitter,
|
||||
openDoc,
|
||||
reportError,
|
||||
setIsInErrorState,
|
||||
setErrorState,
|
||||
showGenericMessageModal,
|
||||
showOutOfSyncModal,
|
||||
setOutOfSync,
|
||||
@@ -661,25 +632,20 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
const value: EditorManager = useMemo(
|
||||
() => ({
|
||||
getEditorType,
|
||||
showSymbolPalette,
|
||||
getCurrentDocValue,
|
||||
getCurrentDocumentId,
|
||||
setIgnoringExternalUpdates,
|
||||
openDocWithId,
|
||||
openDoc,
|
||||
openDocs,
|
||||
trackChanges,
|
||||
isLoading,
|
||||
openFileWithId,
|
||||
openInitialDoc,
|
||||
jumpToLine,
|
||||
wantTrackChanges,
|
||||
setWantTrackChanges,
|
||||
debugTimers,
|
||||
}),
|
||||
[
|
||||
getEditorType,
|
||||
showSymbolPalette,
|
||||
getCurrentDocValue,
|
||||
getCurrentDocumentId,
|
||||
setIgnoringExternalUpdates,
|
||||
@@ -688,11 +654,8 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
openDocs,
|
||||
openFileWithId,
|
||||
openInitialDoc,
|
||||
trackChanges,
|
||||
isLoading,
|
||||
jumpToLine,
|
||||
wantTrackChanges,
|
||||
setWantTrackChanges,
|
||||
debugTimers,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
import usePersistedState from '@/shared/hooks/use-persisted-state'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useUnstableStoreSync } from '@/shared/hooks/use-unstable-store-sync'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
|
||||
// Context value type
|
||||
export type EditorPropertiesContextValue = {
|
||||
showVisual: boolean
|
||||
setShowVisual: Dispatch<SetStateAction<boolean>>
|
||||
showSymbolPalette: boolean
|
||||
setShowSymbolPalette: Dispatch<SetStateAction<boolean>>
|
||||
toggleSymbolPalette: () => void
|
||||
opening: boolean
|
||||
setOpening: Dispatch<SetStateAction<boolean>>
|
||||
trackChanges: boolean
|
||||
setTrackChanges: Dispatch<SetStateAction<boolean>>
|
||||
wantTrackChanges: boolean
|
||||
setWantTrackChanges: Dispatch<SetStateAction<boolean>>
|
||||
errorState: boolean
|
||||
setErrorState: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const EditorPropertiesContext = createContext<
|
||||
EditorPropertiesContextValue | undefined
|
||||
>(undefined)
|
||||
|
||||
function showVisualFallbackValue() {
|
||||
const projectId = getMeta('ol-project_id')
|
||||
const editorModeKey = `editor.mode.${projectId}`
|
||||
const editorModeVal = customLocalStorage.getItem(editorModeKey)
|
||||
|
||||
if (editorModeVal) {
|
||||
// clean up the old key
|
||||
customLocalStorage.removeItem(editorModeKey)
|
||||
}
|
||||
|
||||
return editorModeVal === 'rich-text'
|
||||
}
|
||||
|
||||
export const EditorPropertiesProvider: FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [showVisual, setShowVisual] = usePersistedState(
|
||||
`editor.lastUsedMode`,
|
||||
showVisualFallbackValue(),
|
||||
{
|
||||
converter: {
|
||||
toPersisted: showVisual => (showVisual ? 'visual' : 'code'),
|
||||
fromPersisted: mode => mode === 'visual',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Sync the showVisual state with the exposed store
|
||||
useUnstableStoreSync('editor.showVisual', showVisual)
|
||||
|
||||
const [showSymbolPalette, setShowSymbolPalette] = useState(false)
|
||||
|
||||
const toggleSymbolPalette = useCallback(() => {
|
||||
setShowSymbolPalette(show => {
|
||||
const newValue = !show
|
||||
sendMB(newValue ? 'symbol-palette-show' : 'symbol-palette-hide')
|
||||
return newValue
|
||||
})
|
||||
}, [setShowSymbolPalette])
|
||||
|
||||
const [opening, setOpening] = useState(true)
|
||||
const [trackChanges, setTrackChanges] = useState(false)
|
||||
const [wantTrackChanges, setWantTrackChanges] = useState(false)
|
||||
const [errorState, setErrorState] = useState(false)
|
||||
|
||||
const value = {
|
||||
showVisual,
|
||||
setShowVisual,
|
||||
showSymbolPalette,
|
||||
setShowSymbolPalette,
|
||||
toggleSymbolPalette,
|
||||
opening,
|
||||
setOpening,
|
||||
trackChanges,
|
||||
setTrackChanges,
|
||||
wantTrackChanges,
|
||||
setWantTrackChanges,
|
||||
errorState,
|
||||
setErrorState,
|
||||
}
|
||||
|
||||
return (
|
||||
<EditorPropertiesContext.Provider value={value}>
|
||||
{children}
|
||||
</EditorPropertiesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useEditorPropertiesContext = (): EditorPropertiesContextValue => {
|
||||
const context = useContext(EditorPropertiesContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useEditorPropertiesContext is only available inside EditorPropertiesContext.Provider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from '@/features/ide-react/create-ide-event-emitter'
|
||||
import { JoinProjectPayload } from '@/features/ide-react/connection/join-project-payload'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { populateEditorScope } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
|
||||
import getMeta from '@/utils/meta'
|
||||
@@ -53,7 +52,7 @@ function populatePdfScope(store: ReactScopeValueStore) {
|
||||
store.allowNonExistentPath('pdf', true)
|
||||
}
|
||||
|
||||
export function createReactScopeValueStore(projectId: string) {
|
||||
export function createReactScopeValueStore() {
|
||||
const scopeStore = new ReactScopeValueStore()
|
||||
|
||||
// Populate the scope value store with default values that will be used by
|
||||
@@ -62,7 +61,6 @@ export function createReactScopeValueStore(projectId: string) {
|
||||
// initialization code together with the context and would only populate
|
||||
// necessary values in the store, but this is simpler for now
|
||||
populateIdeReactScope(scopeStore)
|
||||
populateEditorScope(scopeStore, projectId)
|
||||
populateProjectScope(scopeStore)
|
||||
populatePdfScope(scopeStore)
|
||||
|
||||
@@ -73,7 +71,7 @@ export function createReactScopeValueStore(projectId: string) {
|
||||
|
||||
export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const projectId = getMeta('ol-project_id')
|
||||
const [scopeStore] = useState(() => createReactScopeValueStore(projectId))
|
||||
const [scopeStore] = useState(() => createReactScopeValueStore())
|
||||
const [eventEmitter] = useState(createIdeEventEmitter)
|
||||
const [permissionsLevel, setPermissionsLevel] =
|
||||
useState<PermissionsLevel>('readOnly')
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DetachCompileProvider } from '@/shared/context/detach-compile-context'
|
||||
import { DetachProvider } from '@/shared/context/detach-context'
|
||||
import { EditorManagerProvider } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { EditorOpenDocProvider } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { EditorPropertiesProvider } from '@/features/ide-react/context/editor-properties-context'
|
||||
import { EditorProvider } from '@/shared/context/editor-context'
|
||||
import { EditorViewProvider } from '@/features/ide-react/context/editor-view-context'
|
||||
import { FileTreeDataProvider } from '@/shared/context/file-tree-data-context'
|
||||
@@ -42,6 +43,7 @@ export const ReactContextRoot: FC<
|
||||
DetachProvider,
|
||||
EditorManagerProvider,
|
||||
EditorOpenDocProvider,
|
||||
EditorPropertiesProvider,
|
||||
EditorProvider,
|
||||
EditorViewProvider,
|
||||
FileTreeDataProvider,
|
||||
@@ -79,49 +81,51 @@ export const ReactContextRoot: FC<
|
||||
<Providers.ProjectProvider>
|
||||
<Providers.SnapshotProvider>
|
||||
<Providers.DetachProvider>
|
||||
<Providers.EditorViewProvider>
|
||||
<Providers.EditorOpenDocProvider>
|
||||
<Providers.EditorProvider>
|
||||
<Providers.FileTreeDataProvider>
|
||||
<Providers.FileTreePathProvider>
|
||||
<Providers.ReferencesProvider>
|
||||
<Providers.UserFeaturesProvider>
|
||||
<Providers.PermissionsProvider>
|
||||
<Providers.RailProvider>
|
||||
<Providers.LayoutProvider>
|
||||
<Providers.ProjectSettingsProvider>
|
||||
<Providers.EditorManagerProvider>
|
||||
<Providers.LocalCompileProvider>
|
||||
<Providers.DetachCompileProvider>
|
||||
<Providers.ChatProvider>
|
||||
<Providers.FileTreeOpenProvider>
|
||||
<Providers.OnlineUsersProvider>
|
||||
<Providers.MetadataProvider>
|
||||
<Providers.OutlineProvider>
|
||||
<Providers.IdeRedesignSwitcherProvider>
|
||||
<Providers.CommandRegistryProvider>
|
||||
{children}
|
||||
</Providers.CommandRegistryProvider>
|
||||
</Providers.IdeRedesignSwitcherProvider>
|
||||
</Providers.OutlineProvider>
|
||||
</Providers.MetadataProvider>
|
||||
</Providers.OnlineUsersProvider>
|
||||
</Providers.FileTreeOpenProvider>
|
||||
</Providers.ChatProvider>
|
||||
</Providers.DetachCompileProvider>
|
||||
</Providers.LocalCompileProvider>
|
||||
</Providers.EditorManagerProvider>
|
||||
</Providers.ProjectSettingsProvider>
|
||||
</Providers.LayoutProvider>
|
||||
</Providers.RailProvider>
|
||||
</Providers.PermissionsProvider>
|
||||
</Providers.UserFeaturesProvider>
|
||||
</Providers.ReferencesProvider>
|
||||
</Providers.FileTreePathProvider>
|
||||
</Providers.FileTreeDataProvider>
|
||||
</Providers.EditorProvider>
|
||||
</Providers.EditorOpenDocProvider>
|
||||
</Providers.EditorViewProvider>
|
||||
<Providers.EditorPropertiesProvider>
|
||||
<Providers.EditorViewProvider>
|
||||
<Providers.EditorOpenDocProvider>
|
||||
<Providers.EditorProvider>
|
||||
<Providers.FileTreeDataProvider>
|
||||
<Providers.FileTreePathProvider>
|
||||
<Providers.ReferencesProvider>
|
||||
<Providers.UserFeaturesProvider>
|
||||
<Providers.PermissionsProvider>
|
||||
<Providers.RailProvider>
|
||||
<Providers.LayoutProvider>
|
||||
<Providers.ProjectSettingsProvider>
|
||||
<Providers.EditorManagerProvider>
|
||||
<Providers.LocalCompileProvider>
|
||||
<Providers.DetachCompileProvider>
|
||||
<Providers.ChatProvider>
|
||||
<Providers.FileTreeOpenProvider>
|
||||
<Providers.OnlineUsersProvider>
|
||||
<Providers.MetadataProvider>
|
||||
<Providers.OutlineProvider>
|
||||
<Providers.IdeRedesignSwitcherProvider>
|
||||
<Providers.CommandRegistryProvider>
|
||||
{children}
|
||||
</Providers.CommandRegistryProvider>
|
||||
</Providers.IdeRedesignSwitcherProvider>
|
||||
</Providers.OutlineProvider>
|
||||
</Providers.MetadataProvider>
|
||||
</Providers.OnlineUsersProvider>
|
||||
</Providers.FileTreeOpenProvider>
|
||||
</Providers.ChatProvider>
|
||||
</Providers.DetachCompileProvider>
|
||||
</Providers.LocalCompileProvider>
|
||||
</Providers.EditorManagerProvider>
|
||||
</Providers.ProjectSettingsProvider>
|
||||
</Providers.LayoutProvider>
|
||||
</Providers.RailProvider>
|
||||
</Providers.PermissionsProvider>
|
||||
</Providers.UserFeaturesProvider>
|
||||
</Providers.ReferencesProvider>
|
||||
</Providers.FileTreePathProvider>
|
||||
</Providers.FileTreeDataProvider>
|
||||
</Providers.EditorProvider>
|
||||
</Providers.EditorOpenDocProvider>
|
||||
</Providers.EditorViewProvider>
|
||||
</Providers.EditorPropertiesProvider>
|
||||
</Providers.DetachProvider>
|
||||
</Providers.SnapshotProvider>
|
||||
</Providers.ProjectProvider>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
|
||||
export type EditorScopeValue = {
|
||||
showSymbolPalette: false
|
||||
toggleSymbolPalette: () => void
|
||||
sharejs_doc: DocumentContainer | null
|
||||
opening: boolean
|
||||
trackChanges: boolean
|
||||
wantTrackChanges: boolean
|
||||
showVisual: boolean
|
||||
error_state: boolean
|
||||
}
|
||||
|
||||
export function populateEditorScope(
|
||||
store: ReactScopeValueStore,
|
||||
projectId: string
|
||||
) {
|
||||
store.set('project.name', null)
|
||||
|
||||
const editor: Omit<EditorScopeValue, 'showVisual'> = {
|
||||
showSymbolPalette: false,
|
||||
toggleSymbolPalette: () => {},
|
||||
sharejs_doc: null,
|
||||
opening: true,
|
||||
trackChanges: false,
|
||||
wantTrackChanges: false,
|
||||
error_state: false,
|
||||
}
|
||||
store.set('editor', editor)
|
||||
|
||||
store.persisted(
|
||||
'editor.showVisual',
|
||||
showVisualFallbackValue(projectId),
|
||||
`editor.lastUsedMode`,
|
||||
{
|
||||
toPersisted: showVisual => (showVisual ? 'visual' : 'code'),
|
||||
fromPersisted: mode => mode === 'visual',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function showVisualFallbackValue(projectId: string) {
|
||||
const editorModeKey = `editor.mode.${projectId}`
|
||||
const editorModeVal = customLocalStorage.getItem(editorModeKey)
|
||||
|
||||
if (editorModeVal) {
|
||||
// clean up the old key
|
||||
customLocalStorage.removeItem(editorModeKey)
|
||||
}
|
||||
|
||||
return editorModeVal === 'rich-text'
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { LoadingPane } from '@/features/ide-react/components/editor/loading-pane'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { EditorScopeValue } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter'
|
||||
import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import classNames from 'classnames'
|
||||
import SourceEditor from '@/features/source-editor/components/source-editor'
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels'
|
||||
@@ -10,9 +8,11 @@ import { VerticalResizeHandle } from '@/features/ide-react/components/resize/ver
|
||||
import { Suspense } from 'react'
|
||||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
import SymbolPalettePane from '@/features/ide-react/components/editor/symbol-palette-pane'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
|
||||
export const Editor = () => {
|
||||
const [editor] = useScopeValue<EditorScopeValue>('editor')
|
||||
const { opening, errorState, showSymbolPalette } =
|
||||
useEditorPropertiesContext()
|
||||
const { selectedEntityCount, openEntity } = useFileTreeOpenContext()
|
||||
const { currentDocumentId, currentDocument } = useEditorOpenDocContext()
|
||||
|
||||
@@ -21,9 +21,7 @@ export const Editor = () => {
|
||||
}
|
||||
|
||||
const isLoading = Boolean(
|
||||
(!currentDocument || editor.opening) &&
|
||||
!editor.error_state &&
|
||||
currentDocumentId
|
||||
(!currentDocument || opening) && !errorState && currentDocumentId
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -44,7 +42,7 @@ export const Editor = () => {
|
||||
<SourceEditor />
|
||||
{isLoading && <LoadingPane />}
|
||||
</Panel>
|
||||
{editor.showSymbolPalette && (
|
||||
{showSymbolPalette && (
|
||||
<>
|
||||
<VerticalResizeHandle id="ide-redesign-editor-symbol-palette" />
|
||||
<Panel
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { FigureModalSource } from '@/features/source-editor/components/figure-modal/figure-modal-context'
|
||||
import * as commands from '@/features/source-editor/extensions/toolbar/commands'
|
||||
import { setSectionHeadingLevel } from '@/features/source-editor/extensions/toolbar/sections'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { redo, selectAll, undo } from '@codemirror/commands'
|
||||
@@ -288,7 +288,7 @@ export const useToolbarMenuBarEditorCommands = () => {
|
||||
isTeXFile,
|
||||
])
|
||||
|
||||
const { toggleSymbolPalette } = useEditorContext()
|
||||
const { toggleSymbolPalette } = useEditorPropertiesContext()
|
||||
const symbolPaletteAvailable = getMeta('ol-symbolPaletteAvailable')
|
||||
useCommandProvider(() => {
|
||||
if (!newEditor || !editorIsVisible) {
|
||||
|
||||
@@ -31,7 +31,7 @@ import { isCursorNearViewportEdge } from '@/features/source-editor/utils/is-curs
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import { useModalsContext } from '@/features/ide-react/context/modals-context'
|
||||
import { numberOfChangesInSelection } from '../utils/changes-in-selection'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
import classNames from 'classnames'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import useReviewPanelLayout from '../hooks/use-review-panel-layout'
|
||||
@@ -104,7 +104,7 @@ const ReviewTooltipMenuContent: FC<{ onAddComment: () => void }> = ({
|
||||
const ranges = useRangesContext()
|
||||
const { acceptChanges, rejectChanges } = useRangesActionsContext()
|
||||
const { showGenericConfirmModal } = useModalsContext()
|
||||
const { wantTrackChanges } = useEditorManagerContext()
|
||||
const { wantTrackChanges } = useEditorPropertiesContext()
|
||||
const [tooltipStyle, setTooltipStyle] = useState<CSSProperties | undefined>()
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
import { useUserContext } from '@/shared/context/user-context'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
@@ -50,7 +50,7 @@ export const TrackChangesStateProvider: FC<React.PropsWithChildren> = ({
|
||||
const { socket } = useConnectionContext()
|
||||
const project = useProjectContext()
|
||||
const user = useUserContext()
|
||||
const { setWantTrackChanges } = useEditorManagerContext()
|
||||
const { setWantTrackChanges } = useEditorPropertiesContext()
|
||||
|
||||
// TODO: update project.trackChangesState instead?
|
||||
const [trackChangesValue, setTrackChangesValue] = useState<
|
||||
|
||||
@@ -2,12 +2,41 @@ import { useCallback } from 'react'
|
||||
import { DocId } from '../../../../../types/project-settings'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
const safeStringify = (value: unknown) => {
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch (e) {
|
||||
debugConsole.error('double stringify exception', e)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const safeParse = (value: string) => {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
debugConsole.error('double parse exception', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default function useOverviewFileCollapsed(docId: DocId) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const [collapsedDocs, setCollapsedDocs] = usePersistedState<
|
||||
Record<DocId, boolean>
|
||||
>(`docs_collapsed_state:${projectId}`, {}, false, true)
|
||||
Record<DocId, boolean>,
|
||||
string
|
||||
>(
|
||||
`docs_collapsed_state:${projectId}`,
|
||||
{},
|
||||
{
|
||||
converter: {
|
||||
fromPersisted: safeParse,
|
||||
toPersisted: safeStringify,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setCollapsedDocs((collapsedDocs: Record<DocId, boolean>) => {
|
||||
|
||||
@@ -20,7 +20,7 @@ export function LeaversSurveyAlert() {
|
||||
const [hide, setHide] = usePersistedState(
|
||||
'hideInstitutionalLeaversSurvey',
|
||||
false,
|
||||
true
|
||||
{ listen: true }
|
||||
)
|
||||
|
||||
function handleDismiss() {
|
||||
|
||||
@@ -224,7 +224,9 @@ function useUserEmails() {
|
||||
const [
|
||||
showInstitutionalLeaversSurveyUntil,
|
||||
setShowInstitutionalLeaversSurveyUntil,
|
||||
] = usePersistedState('showInstitutionalLeaversSurveyUntil', 0, true)
|
||||
] = usePersistedState('showInstitutionalLeaversSurveyUntil', 0, {
|
||||
listen: true,
|
||||
})
|
||||
const [state, unsafeDispatch] = useReducer(reducer, initialState)
|
||||
const dispatch = useSafeDispatch(unsafeDispatch)
|
||||
const { data, isLoading, isError, isSuccess, runAsync } =
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { ChangeEvent, FC, memo, useCallback } from 'react'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
||||
import { isValidTeXFile } from '../../../main/is-valid-tex-file'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
|
||||
function EditorSwitch() {
|
||||
const { t } = useTranslation()
|
||||
const [visual, setVisual] = useScopeValue('editor.showVisual')
|
||||
const { showVisual: visual, setShowVisual: setVisual } =
|
||||
useEditorPropertiesContext()
|
||||
const { openDocName } = useEditorOpenDocContext()
|
||||
|
||||
const richTextAvailable = openDocName ? isValidTeXFile(openDocName) : false
|
||||
|
||||
@@ -15,6 +15,7 @@ import { withinFormattingCommand } from '@/features/source-editor/utils/tree-ope
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
|
||||
export const ToolbarItems: FC<{
|
||||
state: EditorState
|
||||
@@ -30,8 +31,9 @@ export const ToolbarItems: FC<{
|
||||
listDepth,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { toggleSymbolPalette, showSymbolPalette, writefullInstance } =
|
||||
useEditorContext()
|
||||
const { showSymbolPalette, toggleSymbolPalette } =
|
||||
useEditorPropertiesContext()
|
||||
const { writefullInstance } = useEditorContext()
|
||||
const { features } = useProjectContext()
|
||||
const isActive = withinFormattingCommand(state)
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { StateEffect } from '@codemirror/state'
|
||||
|
||||
export const beforeChangeDocEffect = StateEffect.define()
|
||||
@@ -139,7 +139,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
|
||||
cursorHighlights(),
|
||||
autoPair(options.settings),
|
||||
editable(),
|
||||
search(),
|
||||
search(options.initialSearchQuery),
|
||||
phrases(options.phrases),
|
||||
spelling(options.spelling),
|
||||
shortcuts,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
KeyBinding,
|
||||
keymap,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from '@codemirror/view'
|
||||
import {
|
||||
Annotation,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
} from '@codemirror/state'
|
||||
import { sendSearchEvent } from '@/features/event-tracking/search-events'
|
||||
import { isVisual } from '@/features/source-editor/extensions/visual/visual'
|
||||
import { beforeChangeDocEffect } from '@/features/source-editor/extensions/before-change-doc'
|
||||
|
||||
const restoreSearchQueryAnnotation = Annotation.define<boolean>()
|
||||
|
||||
@@ -110,10 +112,6 @@ const highlightSelectionMatchesExtension = highlightSelectionMatches({
|
||||
wholeWords: true,
|
||||
})
|
||||
|
||||
// store the search query for use when switching between files
|
||||
// TODO: move this into EditorContext?
|
||||
let searchQuery: SearchQuery | null
|
||||
|
||||
const scrollToMatch = (range: SelectionRange, view: EditorView) => {
|
||||
const coords = {
|
||||
from: view.coordsAtPos(range.from),
|
||||
@@ -153,7 +151,7 @@ const searchEventKeymap: KeyBinding[] = [
|
||||
/**
|
||||
* A collection of extensions related to the search feature.
|
||||
*/
|
||||
export const search = () => {
|
||||
export const search = (initialSearchQuery: SearchQuery | null) => {
|
||||
let open = false
|
||||
|
||||
return [
|
||||
@@ -195,9 +193,7 @@ export const search = () => {
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
window.setTimeout(() => {
|
||||
open = false // in a timeout, so the view plugin below can run its destroy method first
|
||||
}, 0)
|
||||
open = false
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -205,8 +201,8 @@ export const search = () => {
|
||||
|
||||
// restore a stored search and re-open the search panel
|
||||
ViewPlugin.define(view => {
|
||||
if (searchQuery) {
|
||||
const _searchQuery = searchQuery
|
||||
if (initialSearchQuery) {
|
||||
const _searchQuery = initialSearchQuery
|
||||
window.setTimeout(() => {
|
||||
openSearchPanel(view)
|
||||
view.dispatch({
|
||||
@@ -217,9 +213,21 @@ export const search = () => {
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
// persist the current search query if the panel is open
|
||||
searchQuery = open ? getSearchQuery(view.state) : null
|
||||
// Fire an event containing the search query before a document change
|
||||
// so that it can be persisted for the next document
|
||||
update(update: ViewUpdate) {
|
||||
for (const tr of update.transactions) {
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(beforeChangeDocEffect)) {
|
||||
const searchQuery = open ? getSearchQuery(view.state) : null
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('search-panel-before-doc-change', {
|
||||
detail: searchQuery,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -51,14 +51,14 @@ import { updateRanges } from '@/features/source-editor/extensions/ranges'
|
||||
import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
|
||||
import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
|
||||
import { Permissions } from '@/features/ide-react/types/permissions'
|
||||
import {
|
||||
GotoOffsetOptions,
|
||||
useEditorManagerContext,
|
||||
} from '@/features/ide-react/context/editor-manager-context'
|
||||
import { GotoOffsetOptions } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { GotoLineOptions } from '@/features/ide-react/types/goto-line-options'
|
||||
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
import { SearchQuery } from '@codemirror/search'
|
||||
import { beforeChangeDocEffect } from '@/features/source-editor/extensions/before-change-doc'
|
||||
|
||||
function useCodeMirrorScope(view: EditorView) {
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
@@ -71,7 +71,6 @@ function useCodeMirrorScope(view: EditorView) {
|
||||
useCompileContext()
|
||||
|
||||
const { openDocName, currentDocument } = useEditorOpenDocContext()
|
||||
const { trackChanges } = useEditorManagerContext()
|
||||
const metadata = useMetadataContext()
|
||||
|
||||
const { id: userId } = useUserContext()
|
||||
@@ -106,7 +105,7 @@ function useCodeMirrorScope(view: EditorView) {
|
||||
|
||||
const hunspellManager = useHunspell(spellCheckLanguage)
|
||||
|
||||
const [visual] = useScopeValue<boolean>('editor.showVisual')
|
||||
const { showVisual: visual, trackChanges } = useEditorPropertiesContext()
|
||||
|
||||
const { referenceKeys } = useReferencesContext()
|
||||
|
||||
@@ -271,6 +270,16 @@ function useCodeMirrorScope(view: EditorView) {
|
||||
visual: showVisual,
|
||||
})
|
||||
|
||||
// Persist the search query in this hook when the document changes by keeping
|
||||
// a reference to the search query in sync with the editor state
|
||||
const searchQueryRef = useRef<SearchQuery | null>(null)
|
||||
useEventListener(
|
||||
'search-panel-before-doc-change',
|
||||
useCallback((event: CustomEvent) => {
|
||||
searchQueryRef.current = event.detail
|
||||
}, [])
|
||||
)
|
||||
|
||||
const { showBoundary } = useErrorBoundary()
|
||||
|
||||
const handleException = useCallback((exception: any) => {
|
||||
@@ -295,6 +304,13 @@ function useCodeMirrorScope(view: EditorView) {
|
||||
if (currentDocument) {
|
||||
debugConsole.log('creating new editor state')
|
||||
|
||||
// Warn any interested extension that the document is about to change,
|
||||
// allowing it to perform any necessary actions before creating the new
|
||||
// state. destroy() is too late because the new state is already created
|
||||
view.dispatch({
|
||||
effects: beforeChangeDocEffect.of(null),
|
||||
})
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: currentDocument.getSnapshot(),
|
||||
extensions: createExtensions({
|
||||
@@ -310,6 +326,7 @@ function useCodeMirrorScope(view: EditorView) {
|
||||
spelling: spellingRef.current,
|
||||
visual: visualRef.current,
|
||||
projectFeatures: projectFeaturesRef.current,
|
||||
initialSearchQuery: searchQueryRef.current,
|
||||
showBoundary,
|
||||
handleException,
|
||||
}),
|
||||
|
||||
@@ -27,8 +27,6 @@ export const EditorContext = createContext<
|
||||
cobranding?: Cobranding
|
||||
hasPremiumCompile?: boolean
|
||||
renameProject: (newName: string) => void
|
||||
showSymbolPalette?: boolean
|
||||
toggleSymbolPalette?: () => void
|
||||
insertSymbol?: (symbol: SymbolWithCharacter) => void
|
||||
isProjectOwner: boolean
|
||||
isRestrictedTokenMember?: boolean
|
||||
@@ -74,8 +72,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
}, [])
|
||||
|
||||
const [projectName, setProjectName] = useScopeValue('project.name')
|
||||
const [showSymbolPalette] = useScopeValue('editor.showSymbolPalette')
|
||||
const [toggleSymbolPalette] = useScopeValue('editor.toggleSymbolPalette')
|
||||
|
||||
const [inactiveTutorials, setInactiveTutorials] = useState(
|
||||
() => getMeta('ol-inactiveTutorials') || []
|
||||
@@ -183,8 +179,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
isProjectOwner: owner?._id === userId,
|
||||
isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
|
||||
isPendingEditor,
|
||||
showSymbolPalette,
|
||||
toggleSymbolPalette,
|
||||
insertSymbol,
|
||||
inactiveTutorials,
|
||||
deactivateTutorial,
|
||||
@@ -204,8 +198,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
userId,
|
||||
renameProject,
|
||||
isPendingEditor,
|
||||
showSymbolPalette,
|
||||
toggleSymbolPalette,
|
||||
insertSymbol,
|
||||
inactiveTutorials,
|
||||
deactivateTutorial,
|
||||
|
||||
@@ -248,17 +248,19 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
const [autoCompile, setAutoCompile] = usePersistedState(
|
||||
`autocompile_enabled:${projectId}`,
|
||||
false,
|
||||
true
|
||||
{ listen: true }
|
||||
)
|
||||
|
||||
// whether the compile should run in draft mode
|
||||
const [draft, setDraft] = usePersistedState(`draft:${projectId}`, false, true)
|
||||
const [draft, setDraft] = usePersistedState(`draft:${projectId}`, false, {
|
||||
listen: true,
|
||||
})
|
||||
|
||||
// whether compiling should stop on first error
|
||||
const [stopOnFirstError, setStopOnFirstError] = usePersistedState(
|
||||
`stop_on_first_error:${projectId}`,
|
||||
false,
|
||||
true
|
||||
{ listen: true }
|
||||
)
|
||||
|
||||
// whether the last compiles stopped on first error
|
||||
@@ -268,7 +270,7 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
const [stopOnValidationError, setStopOnValidationError] = usePersistedState(
|
||||
`stop_on_validation_error:${projectId}`,
|
||||
true,
|
||||
true
|
||||
{ listen: true }
|
||||
)
|
||||
|
||||
// whether the editor linter found errors
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react'
|
||||
import { useIdeContext } from '../context/ide-context'
|
||||
import { type Dispatch, type SetStateAction, useState } from 'react'
|
||||
import { useUnstableStoreSync } from '@/shared/hooks/use-unstable-store-sync'
|
||||
|
||||
/**
|
||||
* Creates a state variable that is exposed via window.overleaf.unstable.store,
|
||||
@@ -13,13 +13,7 @@ export default function useExposedState<T = any>(
|
||||
path: string
|
||||
): [T, Dispatch<SetStateAction<T>>] {
|
||||
const [value, setValue] = useState<T>(initialState)
|
||||
|
||||
const { unstableStore } = useIdeContext()
|
||||
|
||||
// Update the unstable store whenever the value changes
|
||||
useEffect(() => {
|
||||
unstableStore.set(path, value)
|
||||
}, [unstableStore, path, value])
|
||||
useUnstableStoreSync(path, value)
|
||||
|
||||
return [value, setValue]
|
||||
}
|
||||
|
||||
@@ -7,61 +7,59 @@ import {
|
||||
} from 'react'
|
||||
import _ from 'lodash'
|
||||
import localStorage from '../../infrastructure/local-storage'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
const safeStringify = (value: unknown) => {
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch (e) {
|
||||
debugConsole.error('double stringify exception', e)
|
||||
return null
|
||||
type UsePersistedStateOptions<Value, PersistedValue> = {
|
||||
listen?: boolean
|
||||
converter?: {
|
||||
toPersisted: (value: Value) => PersistedValue
|
||||
fromPersisted: (persisted: PersistedValue) => Value
|
||||
}
|
||||
}
|
||||
|
||||
const safeParse = (value: string) => {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
debugConsole.error('double parse exception', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function usePersistedState<T = any>(
|
||||
function usePersistedState<Value, PersistedValue = Value>(
|
||||
key: string,
|
||||
defaultValue?: T,
|
||||
listen = false,
|
||||
// The option below is for backward compatibility with Angular
|
||||
// which sometimes stringifies the values twice
|
||||
doubleStringifyAndParse = false
|
||||
): [T, Dispatch<SetStateAction<T>>] {
|
||||
defaultValue?: Value,
|
||||
options?: UsePersistedStateOptions<Value, PersistedValue>
|
||||
): [Value, Dispatch<SetStateAction<Value>>] {
|
||||
// Store the default value and options on first render so that they're stable
|
||||
// and use them on subsequent renders. This is important for, for example, a
|
||||
// non-primitive default value that should not change on every render.
|
||||
const [allOptions] = useState<{
|
||||
defaultValue?: Value
|
||||
options?: UsePersistedStateOptions<Value, PersistedValue>
|
||||
}>(() => ({ defaultValue, options }))
|
||||
const listen = allOptions.options?.listen || false
|
||||
const { toPersisted, fromPersisted } = allOptions.options?.converter || {}
|
||||
const storedDefaultValue = allOptions.defaultValue
|
||||
|
||||
const getItem = useCallback(
|
||||
(key: string) => {
|
||||
const item = localStorage.getItem(key)
|
||||
return doubleStringifyAndParse ? safeParse(item) : item
|
||||
return fromPersisted ? fromPersisted(item) : item
|
||||
},
|
||||
[doubleStringifyAndParse]
|
||||
[fromPersisted]
|
||||
)
|
||||
const setItem = useCallback(
|
||||
(key: string, value: unknown) => {
|
||||
const val = doubleStringifyAndParse ? safeStringify(value) : value
|
||||
(key: string, value: Value) => {
|
||||
// Nested ternary is convenient for type inference
|
||||
const val = toPersisted ? toPersisted(value) : value
|
||||
localStorage.setItem(key, val)
|
||||
},
|
||||
[doubleStringifyAndParse]
|
||||
[toPersisted]
|
||||
)
|
||||
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
return getItem(key) ?? defaultValue
|
||||
const [value, setValue] = useState<Value>(() => {
|
||||
return getItem(key) ?? storedDefaultValue
|
||||
})
|
||||
|
||||
const updateFunction = useCallback(
|
||||
(newValue: SetStateAction<T>) => {
|
||||
(newValue: SetStateAction<Value>) => {
|
||||
setValue(value => {
|
||||
const actualNewValue = _.isFunction(newValue)
|
||||
? newValue(value)
|
||||
: newValue
|
||||
|
||||
if (actualNewValue === defaultValue) {
|
||||
if (actualNewValue === storedDefaultValue) {
|
||||
localStorage.removeItem(key)
|
||||
} else {
|
||||
setItem(key, actualNewValue)
|
||||
@@ -70,7 +68,7 @@ function usePersistedState<T = any>(
|
||||
return actualNewValue
|
||||
})
|
||||
},
|
||||
[key, defaultValue, setItem]
|
||||
[key, storedDefaultValue, setItem]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -79,7 +77,7 @@ function usePersistedState<T = any>(
|
||||
if (event.key === key) {
|
||||
// note: this value is read via getItem rather than from event.newValue
|
||||
// because getItem handles deserializing the JSON that's stored in localStorage.
|
||||
setValue(getItem(key) ?? defaultValue)
|
||||
setValue(getItem(key) ?? storedDefaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +87,7 @@ function usePersistedState<T = any>(
|
||||
window.removeEventListener('storage', listener)
|
||||
}
|
||||
}
|
||||
}, [defaultValue, key, listen, getItem])
|
||||
}, [storedDefaultValue, key, listen, getItem])
|
||||
|
||||
return [value, updateFunction]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useIdeContext } from '@/shared/context/ide-context'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export function useUnstableStoreSync<T = any>(path: string, value: T) {
|
||||
const { unstableStore } = useIdeContext()
|
||||
|
||||
// Update the unstable store whenever the value changes
|
||||
useEffect(() => {
|
||||
unstableStore.set(path, value)
|
||||
}, [unstableStore, path, value])
|
||||
}
|
||||
@@ -195,7 +195,7 @@ const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
}))
|
||||
|
||||
const [ideContextValue] = useState(() => {
|
||||
const scopeStore = createReactScopeValueStore(projectId)
|
||||
const scopeStore = createReactScopeValueStore()
|
||||
for (const [key, value] of Object.entries(initialScope)) {
|
||||
scopeStore.set(key, value)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { EditorOpenDocContext } from '@/features/ide-react/context/editor-open-d
|
||||
import { DocId } from '../../../types/project-settings'
|
||||
import { StoryObj } from '@storybook/react'
|
||||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
import { EditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
|
||||
type Story = StoryObj<typeof SourceEditor>
|
||||
|
||||
@@ -83,6 +84,34 @@ const BibtexEditorOpenDocProvider: FC<React.PropsWithChildren> = ({
|
||||
</EditorOpenDocProvider>
|
||||
)
|
||||
|
||||
const VisualEditorPropertiesProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [showVisual, setShowVisual] = useState(true)
|
||||
|
||||
const value = {
|
||||
showVisual,
|
||||
setShowVisual,
|
||||
showSymbolPalette: true,
|
||||
setShowSymbolPalette: () => undefined,
|
||||
toggleSymbolPalette: () => undefined,
|
||||
opening: true,
|
||||
setOpening: () => undefined,
|
||||
trackChanges: false,
|
||||
setTrackChanges: () => undefined,
|
||||
wantTrackChanges: false,
|
||||
setWantTrackChanges: () => undefined,
|
||||
errorState: false,
|
||||
setErrorState: () => undefined,
|
||||
}
|
||||
|
||||
return (
|
||||
<EditorPropertiesContext.Provider value={value}>
|
||||
{children}
|
||||
</EditorPropertiesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const FileTreePathProvider: FC<React.PropsWithChildren> = ({ children }) => (
|
||||
<FileTreePathContext.Provider
|
||||
value={{
|
||||
@@ -218,15 +247,13 @@ export const Visual: Story = {
|
||||
providers: {
|
||||
FileTreePathProvider,
|
||||
EditorOpenDocProvider: LatexEditorOpenDocProvider,
|
||||
EditorPropertiesProvider: VisualEditorPropertiesProvider,
|
||||
},
|
||||
}),
|
||||
|
||||
(Story, { globals }) => {
|
||||
// FIXME: useScope has no effect, so this does not default to the visual editor
|
||||
// FIXME: useScope has no effect, so this does nothing
|
||||
useScope({
|
||||
editor: {
|
||||
showVisual: true,
|
||||
},
|
||||
settings: {
|
||||
...settings,
|
||||
overallTheme: globals.theme === 'default-' ? '' : globals.theme,
|
||||
@@ -235,7 +262,6 @@ export const Visual: Story = {
|
||||
})
|
||||
|
||||
useMeta({
|
||||
'ol-showSymbolPalette': true,
|
||||
'ol-mathJaxPath': 'https://unpkg.com/mathjax@3.2.2/es5/tex-svg-full.js',
|
||||
'ol-project_id': '63e21c07946dd8c76505f85a',
|
||||
})
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import { mockScope, rootFolderId } from '../helpers/mock-scope'
|
||||
import { FC } from 'react'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
@@ -41,7 +44,6 @@ describe('<FigureModal />', function () {
|
||||
function mount() {
|
||||
const content = ''
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
const FileTreePathProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -63,7 +65,16 @@ describe('<FigureModal />', function () {
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} providers={{ FileTreePathProvider }}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
FileTreePathProvider,
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Needed since eslint gets confused by mocha-each
|
||||
/* eslint-disable mocha/prefer-arrow-callback */
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import forEach from 'mocha-each'
|
||||
@@ -14,11 +17,18 @@ const mountEditor = (content: string | string[]) => {
|
||||
content = '\n' + content
|
||||
}
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
cy.viewport(1000, 800)
|
||||
cy.mount(
|
||||
<TestContainer style={{ width: 1000, height: 800 }}>
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
@@ -6,11 +9,18 @@ import { isMac } from '@/shared/utils/os'
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
@@ -7,11 +10,18 @@ const menuIconsText = 'content_copyexpand_more'
|
||||
|
||||
const mountEditor = (content = '') => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { FC } from 'react'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
@@ -43,13 +46,19 @@ const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.permissions.write = false
|
||||
scope.permissions.trackedWrite = false
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ FileTreePathProvider, PermissionsProvider }}
|
||||
providers={{
|
||||
FileTreePathProvider,
|
||||
PermissionsProvider,
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
@@ -18,11 +21,18 @@ const clickToolbarButton = (name: string) => {
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
|
||||
@@ -11,11 +14,18 @@ describe('<CodeMirrorEditor/> tooltips in Visual mode', function () {
|
||||
cy.interceptEvents()
|
||||
|
||||
const scope = mockScope('\n\n\n')
|
||||
scope.editor.showVisual = true
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Needed since eslint gets confused by mocha-each
|
||||
/* eslint-disable mocha/prefer-arrow-callback */
|
||||
import { FC } from 'react'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import forEach from 'mocha-each'
|
||||
@@ -19,7 +22,6 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||
const content = '\n'.repeat(3)
|
||||
|
||||
const scope = mockScope(content)
|
||||
scope.editor.showVisual = true
|
||||
|
||||
const FileTreePathProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -41,7 +43,16 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope} providers={{ FileTreePathProvider }}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
FileTreePathProvider,
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CodemirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -622,7 +622,7 @@ describe('<CodeMirrorEditor/>', { scrollBehavior: false }, function () {
|
||||
|
||||
const rect = selection.getRangeAt(0).getBoundingClientRect()
|
||||
expect(Math.round(rect.top)).to.be.gte(100)
|
||||
expect(Math.round(rect.left)).to.be.gte(90)
|
||||
expect(Math.round(rect.left)).to.be.gte(80)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,7 +19,6 @@ export const mockScope = (
|
||||
sharejs_doc: mockDoc(content, docOptions),
|
||||
openDocName: 'test.tex',
|
||||
currentDocumentId: docId,
|
||||
showVisual: false,
|
||||
wantTrackChanges: false,
|
||||
},
|
||||
project: {
|
||||
|
||||
@@ -27,6 +27,10 @@ import { ReactContextRoot } from '@/features/ide-react/context/react-context-roo
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import useDetachLayout from '@/shared/hooks/use-detach-layout'
|
||||
import useExposedState from '@/shared/hooks/use-exposed-state'
|
||||
import {
|
||||
EditorPropertiesContext,
|
||||
EditorPropertiesContextValue,
|
||||
} from '@/features/ide-react/context/editor-properties-context'
|
||||
import {
|
||||
type IdeLayout,
|
||||
type IdeView,
|
||||
@@ -160,6 +164,7 @@ export function EditorProviders({
|
||||
} as any as DocumentContainer,
|
||||
openDocName: null,
|
||||
currentDocumentId: null,
|
||||
wantTrackChanges: false,
|
||||
},
|
||||
project: {
|
||||
_id: projectId,
|
||||
@@ -190,6 +195,9 @@ export function EditorProviders({
|
||||
openDocName: scope.editor.openDocName,
|
||||
currentDocument: scope.editor.sharejs_doc,
|
||||
}),
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
wantTrackChanges: scope.editor.wantTrackChanges,
|
||||
}),
|
||||
LayoutProvider: makeLayoutProvider(layoutContext),
|
||||
...providers,
|
||||
}}
|
||||
@@ -251,7 +259,7 @@ const makeIdeReactProvider = (
|
||||
}))
|
||||
|
||||
const [ideContextValue] = useState(() => {
|
||||
const scopeStore = createReactScopeValueStore(PROJECT_ID)
|
||||
const scopeStore = createReactScopeValueStore()
|
||||
for (const [key, value] of Object.entries(scope)) {
|
||||
// TODO: path for nested entries
|
||||
scopeStore.set(key, value)
|
||||
@@ -438,3 +446,60 @@ const makeLayoutProvider = (
|
||||
}
|
||||
return LayoutProvider
|
||||
}
|
||||
|
||||
export function makeEditorPropertiesProvider(
|
||||
initialValues: Partial<
|
||||
Pick<
|
||||
EditorPropertiesContextValue,
|
||||
'showVisual' | 'showSymbolPalette' | 'wantTrackChanges'
|
||||
>
|
||||
>
|
||||
) {
|
||||
const EditorPropertiesProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const {
|
||||
showVisual: initialShowVisual,
|
||||
showSymbolPalette: initialShowSymbolPalette,
|
||||
wantTrackChanges: initialWantTrackChanges,
|
||||
} = initialValues
|
||||
|
||||
const [showVisual, setShowVisual] = useState(initialShowVisual || false)
|
||||
const [showSymbolPalette, setShowSymbolPalette] = useState(
|
||||
initialShowSymbolPalette || false
|
||||
)
|
||||
|
||||
function toggleSymbolPalette() {
|
||||
setShowSymbolPalette(show => !show)
|
||||
}
|
||||
|
||||
const [opening, setOpening] = useState(true)
|
||||
const [trackChanges, setTrackChanges] = useState(false)
|
||||
const [wantTrackChanges, setWantTrackChanges] = useState(
|
||||
initialWantTrackChanges || false
|
||||
)
|
||||
const [errorState, setErrorState] = useState(false)
|
||||
|
||||
const value = {
|
||||
showVisual,
|
||||
setShowVisual,
|
||||
showSymbolPalette,
|
||||
setShowSymbolPalette,
|
||||
toggleSymbolPalette,
|
||||
opening,
|
||||
setOpening,
|
||||
trackChanges,
|
||||
setTrackChanges,
|
||||
wantTrackChanges,
|
||||
setWantTrackChanges,
|
||||
errorState,
|
||||
setErrorState,
|
||||
}
|
||||
|
||||
return (
|
||||
<EditorPropertiesContext.Provider value={value}>
|
||||
{children}
|
||||
</EditorPropertiesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return EditorPropertiesProvider
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('usePersistedState', function () {
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(1)
|
||||
|
||||
const Test = () => {
|
||||
const [value] = usePersistedState(key)
|
||||
const [value] = usePersistedState<string>(key)
|
||||
|
||||
return <div>{value}</div>
|
||||
}
|
||||
@@ -139,6 +139,35 @@ describe('usePersistedState', function () {
|
||||
expect(localStorage.getItem(key)).to.equal('foobar')
|
||||
})
|
||||
|
||||
it('converts persisted value (string to boolean)', function () {
|
||||
const key = 'test:convert'
|
||||
localStorage.setItem(key, 'yep')
|
||||
|
||||
const Test = () => {
|
||||
const [value, setValue] = usePersistedState(key, true, {
|
||||
converter: {
|
||||
toPersisted(value) {
|
||||
return value ? 'yep' : 'nope'
|
||||
},
|
||||
fromPersisted(persistedValue) {
|
||||
return persistedValue === 'yep'
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setValue(false)
|
||||
}, [setValue])
|
||||
|
||||
return <div>{String(value)}</div>
|
||||
}
|
||||
|
||||
render(<Test />)
|
||||
|
||||
screen.getByText('false')
|
||||
expect(localStorage.getItem(key)).to.equal('nope')
|
||||
})
|
||||
|
||||
it('handles syncing values via storage event', async function () {
|
||||
const key = 'test:sync'
|
||||
localStorage.setItem(key, 'foo')
|
||||
@@ -149,7 +178,7 @@ describe('usePersistedState', function () {
|
||||
window.addEventListener('storage', storageEventListener)
|
||||
|
||||
const Test = () => {
|
||||
const [value, setValue] = usePersistedState(key, 'bar', true)
|
||||
const [value, setValue] = usePersistedState(key, 'bar', { listen: true })
|
||||
|
||||
useEffect(() => {
|
||||
setValue('baz')
|
||||
|
||||
Reference in New Issue
Block a user