diff --git a/services/web/frontend/js/features/history/components/change-list/changes.tsx b/services/web/frontend/js/features/history/components/change-list/changes.tsx
index ea5e495be3..6e3c1ca30e 100644
--- a/services/web/frontend/js/features/history/components/change-list/changes.tsx
+++ b/services/web/frontend/js/features/history/components/change-list/changes.tsx
@@ -23,6 +23,7 @@ function Changes({ pathnames, projectOps }: ChangesProps) {
{pathname}
@@ -41,6 +42,7 @@ function Changes({ pathnames, projectOps }: ChangesProps) {
{getProjectOpDoc(op)}
diff --git a/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx b/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx
index 8053792db9..f4c528670c 100644
--- a/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx
+++ b/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx
@@ -1,16 +1,8 @@
import { useTranslation } from 'react-i18next'
-import Icon from '../../../../shared/components/icon'
import { useCallback, useEffect, useState } from 'react'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import StartFreeTrialButton from '../../../../shared/components/start-free-trial-button'
-
-function FeatureItem({ text }: { text: string }) {
- return (
-
- {text}
-
- )
-}
+import UpgradeBenefits from '@/shared/components/upgrade-benefits'
export function OwnerPaywallPrompt() {
const { t } = useTranslation()
@@ -34,16 +26,7 @@ export function OwnerPaywallPrompt() {
{t('upgrade_to_get_feature', { feature: 'full project history' })}
-
+
(
className="history-version-badge"
data-testid="history-version-badge"
{...props}
+ translate={isPseudoCurrentStateLabel ? 'yes' : 'no'}
>
{isPseudoCurrentStateLabel
? t('history_label_project_current_state')
@@ -147,24 +148,32 @@ function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) {
const isPseudoCurrentStateLabel = isPseudoLabel(label)
const currentLabelData = allLabels?.find(({ id }) => id === label.id)
- const labelOwnerName =
- currentLabelData && !isPseudoLabel(currentLabelData)
- ? currentLabelData.user_display_name
- : t('anonymous')
+ const isAnonymous = !currentLabelData || isPseudoLabel(currentLabelData)
+ const labelOwnerName = isAnonymous
+ ? t('anonymous')
+ : currentLabelData.user_display_name
+ const labelOwnerNameComponent = isAnonymous ? (
+ labelOwnerName
+ ) : (
+ {labelOwnerName}
+ )
return !isPseudoCurrentStateLabel ? (
- {t('history_label_created_by')} {labelOwnerName}
+ {t('history_label_created_by')} {labelOwnerNameComponent}
diff --git a/services/web/frontend/js/features/history/components/change-list/toggle-switch.tsx b/services/web/frontend/js/features/history/components/change-list/toggle-switch.tsx
index 478630808c..078f092c51 100644
--- a/services/web/frontend/js/features/history/components/change-list/toggle-switch.tsx
+++ b/services/web/frontend/js/features/history/components/change-list/toggle-switch.tsx
@@ -61,7 +61,9 @@ function ToggleSwitch({ labelsOnly, setLabelsOnly }: ToggleSwitchProps) {
return (
- {t('history_view_a11y_description')}
+
+ {t('history_view_a11y_description')}
+
- {userName}
+
+ {userName}
+
>
)
}
diff --git a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx
index e3543ef527..7088588e27 100644
--- a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx
+++ b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-doc.tsx
@@ -42,6 +42,7 @@ function HistoryFileTreeDoc({
aria-selected={selected}
aria-label={name}
tabIndex={0}
+ translate="no"
>
diff --git a/services/web/frontend/js/features/history/context/history-context.tsx b/services/web/frontend/js/features/history/context/history-context.tsx
index 4fa7fabea6..a7a0746e3c 100644
--- a/services/web/frontend/js/features/history/context/history-context.tsx
+++ b/services/web/frontend/js/features/history/context/history-context.tsx
@@ -78,13 +78,10 @@ const updatesInfoInitialState: HistoryContextValue['updatesInfo'] = {
function useHistory() {
const { view } = useLayoutContext()
const user = useUserContext()
- const project = useProjectContext()
+ const { projectId, project, features } = useProjectContext()
const userId = user.id
- const projectId = project._id
- const projectOwnerId = project.owner?._id
- const userHasFullFeature = Boolean(
- project.features?.versioning || user.isAdmin
- )
+ const projectOwnerId = project?.owner?._id
+ const userHasFullFeature = Boolean(features.versioning || user.isAdmin)
const currentUserIsOwner = projectOwnerId === userId
const [selection, setSelection] = useState(selectionInitialState)
diff --git a/services/web/frontend/js/features/history/context/hooks/use-restore-deleted-file.ts b/services/web/frontend/js/features/history/context/hooks/use-restore-deleted-file.ts
index 2521454ebc..621bd9b148 100644
--- a/services/web/frontend/js/features/history/context/hooks/use-restore-deleted-file.ts
+++ b/services/web/frontend/js/features/history/context/hooks/use-restore-deleted-file.ts
@@ -21,7 +21,7 @@ type RestorationState =
export function useRestoreDeletedFile() {
const { projectId } = useHistoryContext()
- const { setView } = useLayoutContext()
+ const { restoreView } = useLayoutContext()
const { openDocWithId, openFileWithId } = useEditorManagerContext()
const { showBoundary } = useErrorBoundary()
const { fileTreeData } = useFileTreeData()
@@ -37,7 +37,7 @@ export function useRestoreDeletedFile() {
if (result) {
setState('complete')
const { _id: id } = result.entity
- setView('editor')
+ restoreView()
if (restoredFileMetadata.type === 'doc') {
openDocWithId(id)
@@ -52,7 +52,7 @@ export function useRestoreDeletedFile() {
restoredFileMetadata,
openDocWithId,
openFileWithId,
- setView,
+ restoreView,
])
useEffect(() => {
diff --git a/services/web/frontend/js/features/history/context/hooks/use-restore-project.ts b/services/web/frontend/js/features/history/context/hooks/use-restore-project.ts
index 71b1b6af12..7813ae0c5c 100644
--- a/services/web/frontend/js/features/history/context/hooks/use-restore-project.ts
+++ b/services/web/frontend/js/features/history/context/hooks/use-restore-project.ts
@@ -7,7 +7,7 @@ type RestorationState = 'initial' | 'restoring' | 'restored' | 'error'
export const useRestoreProject = () => {
const { showBoundary } = useErrorBoundary()
- const { setView } = useLayoutContext()
+ const { restoreView } = useLayoutContext()
const [restorationState, setRestorationState] =
useState('initial')
@@ -18,14 +18,14 @@ export const useRestoreProject = () => {
restoreProjectToVersion(projectId, version)
.then(() => {
setRestorationState('restored')
- setView('editor')
+ restoreView()
})
.catch(err => {
setRestorationState('error')
showBoundary(err)
})
},
- [showBoundary, setView]
+ [showBoundary, restoreView]
)
return {
diff --git a/services/web/frontend/js/features/history/context/hooks/use-restore-selected-file.ts b/services/web/frontend/js/features/history/context/hooks/use-restore-selected-file.ts
index 168495bc92..d514129e9a 100644
--- a/services/web/frontend/js/features/history/context/hooks/use-restore-selected-file.ts
+++ b/services/web/frontend/js/features/history/context/hooks/use-restore-selected-file.ts
@@ -22,7 +22,7 @@ type RestoreState =
export function useRestoreSelectedFile() {
const { projectId } = useHistoryContext()
- const { setView } = useLayoutContext()
+ const { restoreView } = useLayoutContext()
const { openDocWithId, openFileWithId } = useEditorManagerContext()
const { showBoundary } = useErrorBoundary()
const { fileTreeData } = useFileTreeData()
@@ -38,7 +38,7 @@ export function useRestoreSelectedFile() {
if (result) {
setState('complete')
const { _id: id } = result.entity
- setView('editor')
+ restoreView()
if (restoredFileMetadata.type === 'doc') {
openDocWithId(id)
@@ -53,7 +53,7 @@ export function useRestoreSelectedFile() {
restoredFileMetadata,
openDocWithId,
openFileWithId,
- setView,
+ restoreView,
])
useEffect(() => {
diff --git a/services/web/frontend/js/features/history/services/types/file.ts b/services/web/frontend/js/features/history/services/types/file.ts
index 4fb1330087..e517fdb518 100644
--- a/services/web/frontend/js/features/history/services/types/file.ts
+++ b/services/web/frontend/js/features/history/services/types/file.ts
@@ -30,9 +30,5 @@ export interface FileRenamed extends FileWithEditable {
operation: Extract
}
-export type FileDiff =
- | FileAdded
- | FileRemoved
- | FileEdited
- | FileRenamed
- | FileUnchanged
+export type FileChanged = FileAdded | FileRemoved | FileEdited | FileRenamed
+export type FileDiff = FileChanged | FileUnchanged
diff --git a/services/web/frontend/js/features/history/utils/file-diff.ts b/services/web/frontend/js/features/history/utils/file-diff.ts
index 3d5febbb10..c78fd3275c 100644
--- a/services/web/frontend/js/features/history/utils/file-diff.ts
+++ b/services/web/frontend/js/features/history/utils/file-diff.ts
@@ -1,24 +1,24 @@
-import type {
+import {
+ FileChanged,
FileDiff,
FileRemoved,
FileRenamed,
- FileWithEditable,
} from '../services/types/file'
+export function isFileChanged(fileDiff: FileDiff): fileDiff is FileChanged {
+ return 'operation' in fileDiff
+}
+
export function isFileRenamed(fileDiff: FileDiff): fileDiff is FileRenamed {
- return (fileDiff as FileRenamed).operation === 'renamed'
+ return isFileChanged(fileDiff) && fileDiff.operation === 'renamed'
}
export function isFileRemoved(fileDiff: FileDiff): fileDiff is FileRemoved {
- return (fileDiff as FileRemoved).operation === 'removed'
-}
-
-function isFileWithEditable(fileDiff: FileDiff): fileDiff is FileWithEditable {
- return 'editable' in (fileDiff as FileWithEditable)
+ return isFileChanged(fileDiff) && fileDiff.operation === 'removed'
}
export function isFileEditable(fileDiff: FileDiff) {
- return isFileWithEditable(fileDiff)
+ return 'editable' in fileDiff
? fileDiff.editable
: fileDiff.operation === 'edited'
}
diff --git a/services/web/frontend/js/features/history/utils/file-tree.ts b/services/web/frontend/js/features/history/utils/file-tree.ts
index 423621902c..e997457e8d 100644
--- a/services/web/frontend/js/features/history/utils/file-tree.ts
+++ b/services/web/frontend/js/features/history/utils/file-tree.ts
@@ -1,6 +1,6 @@
import _ from 'lodash'
import type { FileDiff, FileRenamed } from '../services/types/file'
-import { isFileEditable, isFileRemoved } from './file-diff'
+import { isFileChanged, isFileEditable, isFileRemoved } from './file-diff'
export type FileTreeEntity = {
name?: string
@@ -65,22 +65,29 @@ export function fileTreeDiffToFileTreeData(
const folders: HistoryFileTree[] = []
const docs: HistoryDoc[] = []
- for (const file of fileTreeDiff) {
- if (file.type === 'file') {
- const deletedAtV = isFileRemoved(file) ? file.deletedAtV : undefined
-
- const newDoc: HistoryDoc = {
- pathname: file.pathname ?? '',
- name: file.name ?? '',
- deletedAtV,
- editable: isFileEditable(file),
- operation: 'operation' in file ? file.operation : undefined,
+ for (const fileDiff of fileTreeDiff) {
+ if (fileDiff.type === 'file') {
+ if (isFileChanged(fileDiff)) {
+ docs.push({
+ pathname: fileDiff.pathname ?? '',
+ name: fileDiff.name ?? '',
+ editable: isFileEditable(fileDiff),
+ operation: fileDiff.operation,
+ deletedAtV: isFileRemoved(fileDiff) ? fileDiff.deletedAtV : undefined,
+ })
+ } else {
+ docs.push({
+ pathname: fileDiff.pathname ?? '',
+ name: fileDiff.name ?? '',
+ editable: isFileEditable(fileDiff),
+ })
}
-
- docs.push(newDoc)
- } else if (file.type === 'folder') {
- if (file.children) {
- const folder = fileTreeDiffToFileTreeData(file.children, file.name)
+ } else if (fileDiff.type === 'folder') {
+ if (fileDiff.children) {
+ const folder = fileTreeDiffToFileTreeData(
+ fileDiff.children,
+ fileDiff.name
+ )
folders.push(folder)
}
}
diff --git a/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-bottom-text.jsx b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-bottom-text.jsx
index db6507e823..a11b79f004 100644
--- a/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-bottom-text.jsx
+++ b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-bottom-text.jsx
@@ -10,8 +10,9 @@ export default function HotkeysModalBottomText() {
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
eventTracking.sendMB('left-menu-hotkeys-template')}
- href="/articles/overleaf-keyboard-shortcuts/qykqfvmxdnjf"
+ href="https://www.overleaf.com/articles/overleaf-keyboard-shortcuts/qykqfvmxdnjf"
target="_blank"
+ rel="noreferrer"
/>,
]}
/>
diff --git a/services/web/frontend/js/features/ide-react/components/editor-and-pdf.tsx b/services/web/frontend/js/features/ide-react/components/editor-and-pdf.tsx
index 6a9e4cd441..a7ae12515e 100644
--- a/services/web/frontend/js/features/ide-react/components/editor-and-pdf.tsx
+++ b/services/web/frontend/js/features/ide-react/components/editor-and-pdf.tsx
@@ -58,7 +58,10 @@ export const EditorAndPdf: FC = () => {
>
{selectedEntityCount === 0 && }
{selectedEntityCount === 1 && openEntity?.type === 'fileRef' && (
-
+
)}
{selectedEntityCount > 1 && (
diff --git a/services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx b/services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx
index 77ffcd6cd4..b93a18db9f 100644
--- a/services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx
+++ b/services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx
@@ -1,23 +1,24 @@
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 { EditorScopeValue } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter'
+import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
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('editor')
+ const { showSymbolPalette } = useEditorPropertiesContext()
const { selectedEntityCount, openEntity } = useFileTreeOpenContext()
- const { currentDocumentId, isLoading } = useEditorManagerContext()
+ const { isLoading } = useEditorManagerContext()
+ const { currentDocumentId } = useEditorOpenDocContext()
if (!currentDocumentId) {
return null
@@ -39,7 +40,7 @@ export const EditorPane: FC = () => {
{isLoading && }
- {editor.showSymbolPalette && (
+ {showSymbolPalette && (
<>
import('@/features/ide-redesign/components/main-layout')
@@ -30,6 +31,7 @@ export default function IdePage() {
useEditingSessionHeartbeat() // send a batched event when user is active
useRegisterUserActivity() // record activity and ensure connection when user is active
useHasLintingError() // pass editor:lint hasLintingError to the compiler
+ useStatusFavicon() // update the favicon based on the compile status
const newEditor = useIsNewEditorEnabled()
const canAccessNewEditor = canUseNewEditor()
diff --git a/services/web/frontend/js/features/ide-react/components/modals/generic-confirm-modal.tsx b/services/web/frontend/js/features/ide-react/components/modals/generic-confirm-modal.tsx
index de35bee52f..fd27985ba6 100644
--- a/services/web/frontend/js/features/ide-react/components/modals/generic-confirm-modal.tsx
+++ b/services/web/frontend/js/features/ide-react/components/modals/generic-confirm-modal.tsx
@@ -25,10 +25,10 @@ function GenericConfirmModal({
message,
confirmLabel,
primaryVariant = 'primary',
+ onConfirm,
...modalProps
}: GenericConfirmModalProps) {
const { t } = useTranslation()
- const handleConfirmClick = modalProps.onConfirm
return (
@@ -42,7 +42,7 @@ function GenericConfirmModal({
modalProps.onHide()}>
{t('cancel')}
-
+
{confirmLabel || t('ok')}
diff --git a/services/web/frontend/js/features/ide-react/components/resize/horizontal-toggler.tsx b/services/web/frontend/js/features/ide-react/components/resize/horizontal-toggler.tsx
index fba41ee8d8..a5079d397a 100644
--- a/services/web/frontend/js/features/ide-react/components/resize/horizontal-toggler.tsx
+++ b/services/web/frontend/js/features/ide-react/components/resize/horizontal-toggler.tsx
@@ -1,5 +1,6 @@
import classNames from 'classnames'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import MaterialIcon from '@/shared/components/material-icon'
type HorizontalTogglerType = 'west' | 'east'
@@ -42,7 +43,16 @@ export function HorizontalToggler({
aria-label={description}
title=""
onClick={() => setIsOpen(!isOpen)}
- />
+ >
+
+
)
}
diff --git a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx
index 8224bb6e62..7f19737c90 100644
--- a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx
+++ b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx
@@ -1,5 +1,5 @@
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
-import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { PermissionsLevel } from '@/features/ide-react/types/permissions'
import { UnsavedDocsLockedAlert } from '@/features/ide-react/components/unsaved-docs/unsaved-docs-locked-alert'
@@ -12,7 +12,7 @@ const MAX_UNSAVED_SECONDS = 30 // lock the editor after this time if unsaved
export const UnsavedDocs: FC = () => {
const { openDocs, debugTimers } = useEditorManagerContext()
- const { permissionsLevel, setPermissionsLevel } = useEditorContext()
+ const { permissionsLevel, setPermissionsLevel } = useIdeReactContext()
const [isLocked, setIsLocked] = useState(false)
const [unsavedDocs, setUnsavedDocs] = useState(new Map())
const globalAlertsContainer = useGlobalAlertsContainer()
diff --git a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx
index e830d7ec1a..fcc430dcdb 100644
--- a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx
+++ b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx
@@ -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'
@@ -35,10 +33,9 @@ import { EditorType } from '@/features/ide-react/editor/types/editor-type'
import { DocId } from '../../../../../types/project-settings'
import { Update } from '@/features/history/services/types/update'
import { useDebugDiffTracker } from '../hooks/use-debug-diff-tracker'
-import { useEditorContext } from '@/shared/context/editor-context'
-import useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only'
-import { BinaryFile } from '@/features/file-view/types/binary-file'
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
@@ -54,9 +51,6 @@ interface OpenDocOptions
export type EditorManager = {
getEditorType: () => EditorType | null
- showSymbolPalette: boolean
- currentDocument: DocumentContainer | null
- currentDocumentId: DocId | null
getCurrentDocValue: () => string | null
getCurrentDocumentId: () => DocId | null
setIgnoringExternalUpdates: (value: boolean) => void
@@ -65,15 +59,8 @@ export type EditorManager = {
openDocs: OpenDocuments
openFileWithId: (fileId: string) => void
openInitialDoc: (docId: string) => void
- openDocName: string | null
- setOpenDocName: (openDocName: string) => void
isLoading: boolean
- trackChanges: boolean
jumpToLine: (options: GotoLineOptions) => void
- wantTrackChanges: boolean
- setWantTrackChanges: React.Dispatch<
- React.SetStateAction
- >
debugTimers: React.MutableRefObject>
}
@@ -93,36 +80,29 @@ export const EditorManagerProvider: FC = ({
children,
}) => {
const { t } = useTranslation()
- const { scopeStore } = useIdeContext()
- const { reportError, eventEmitter, projectId } = useIdeReactContext()
- const { setOutOfSync } = useEditorContext()
+ const { reportError, eventEmitter, projectId, setOutOfSync } =
+ useIdeReactContext()
const { socket, closeConnection, connectionState } = useConnectionContext()
- const { view, setView } = useLayoutContext()
+ const { view, setView, setOpenFile } = useLayoutContext()
const { showGenericMessageModal, genericModalVisible, showOutOfSyncModal } =
useModalsContext()
const { id: userId } = useUserContext()
-
- const [showSymbolPalette, setShowSymbolPalette] = useScopeValue(
- 'editor.showSymbolPalette'
- )
- const [showVisual] = useScopeValue('editor.showVisual')
- const [currentDocument, setCurrentDocument] =
- useScopeValue('editor.sharejs_doc')
- const [currentDocumentId, setCurrentDocumentId] = useScopeValue(
- 'editor.open_doc_id'
- )
- const [openDocName, setOpenDocName] = useScopeValue(
- 'editor.open_doc_name'
- )
- const [opening, setOpening] = useScopeValue('editor.opening')
- const [errorState, setIsInErrorState] =
- useScopeValue('editor.error_state')
- const [trackChanges, setTrackChanges] = useScopeValue(
- 'editor.trackChanges'
- )
- const [wantTrackChanges, setWantTrackChanges] = useScopeValue(
- 'editor.wantTrackChanges'
- )
+ const {
+ showVisual,
+ opening,
+ setOpening,
+ errorState,
+ setErrorState,
+ setTrackChanges,
+ wantTrackChanges,
+ } = useEditorPropertiesContext()
+ const {
+ currentDocumentId,
+ setCurrentDocumentId,
+ setOpenDocName,
+ currentDocument,
+ setCurrentDocument,
+ } = useEditorOpenDocContext()
const wantTrackChangesRef = useRef(wantTrackChanges)
useEffect(() => {
@@ -198,22 +178,6 @@ export const EditorManagerProvider: FC = ({
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
@@ -521,8 +485,6 @@ export const EditorManagerProvider: FC = ({
[fileTreeData, openDoc]
)
- const [, setOpenFile] = useScopeValueSetterOnly('openFile')
-
const openFileWithId = useCallback(
(fileRefId: string) => {
const fileRef = findFileRefEntityById(fileTreeData, fileRefId)
@@ -596,7 +558,7 @@ export const EditorManagerProvider: FC = ({
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
@@ -623,7 +585,7 @@ export const EditorManagerProvider: FC = ({
eventEmitter,
openDoc,
reportError,
- setIsInErrorState,
+ setErrorState,
showGenericMessageModal,
showOutOfSyncModal,
setOutOfSync,
@@ -670,31 +632,20 @@ export const EditorManagerProvider: FC = ({
const value: EditorManager = useMemo(
() => ({
getEditorType,
- showSymbolPalette,
- currentDocument,
- currentDocumentId,
getCurrentDocValue,
getCurrentDocumentId,
setIgnoringExternalUpdates,
openDocWithId,
openDoc,
openDocs,
- openDocName,
- setOpenDocName,
- trackChanges,
isLoading,
openFileWithId,
openInitialDoc,
jumpToLine,
- wantTrackChanges,
- setWantTrackChanges,
debugTimers,
}),
[
getEditorType,
- showSymbolPalette,
- currentDocument,
- currentDocumentId,
getCurrentDocValue,
getCurrentDocumentId,
setIgnoringExternalUpdates,
@@ -703,13 +654,8 @@ export const EditorManagerProvider: FC = ({
openDocs,
openFileWithId,
openInitialDoc,
- openDocName,
- setOpenDocName,
- trackChanges,
isLoading,
jumpToLine,
- wantTrackChanges,
- setWantTrackChanges,
debugTimers,
]
)
diff --git a/services/web/frontend/js/features/ide-react/context/editor-open-doc-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-open-doc-context.tsx
new file mode 100644
index 0000000000..61de80dfde
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/context/editor-open-doc-context.tsx
@@ -0,0 +1,66 @@
+import {
+ createContext,
+ Dispatch,
+ FC,
+ PropsWithChildren,
+ SetStateAction,
+ useContext,
+ useState,
+} from 'react'
+import { DocId } from '../../../../../types/project-settings'
+import useExposedState from '@/shared/hooks/use-exposed-state'
+import { DocumentContainer } from '@/features/ide-react/editor/document-container'
+
+export interface EditorOpenDocContextState {
+ currentDocumentId: DocId | null
+ openDocName: string | null
+ currentDocument: DocumentContainer | null
+}
+
+interface EditorOpenDocContextValue extends EditorOpenDocContextState {
+ setCurrentDocumentId: Dispatch>
+ setOpenDocName: Dispatch>
+ setCurrentDocument: Dispatch>
+}
+
+export const EditorOpenDocContext = createContext<
+ EditorOpenDocContextValue | undefined
+>(undefined)
+
+export const EditorOpenDocProvider: FC = ({ children }) => {
+ const [currentDocumentId, setCurrentDocumentId] =
+ useExposedState(null, 'editor.open_doc_id')
+ const [openDocName, setOpenDocName] = useExposedState(
+ null,
+ 'editor.open_doc_name'
+ )
+ const [currentDocument, setCurrentDocument] =
+ useState(null)
+
+ const value = {
+ currentDocumentId,
+ setCurrentDocumentId,
+ openDocName,
+ setOpenDocName,
+ currentDocument,
+ setCurrentDocument,
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useEditorOpenDocContext = (): EditorOpenDocContextValue => {
+ const context = useContext(EditorOpenDocContext)
+
+ if (!context) {
+ throw new Error(
+ 'useEditorOpenDocContext is only available inside EditorOpenDocContext.Provider'
+ )
+ }
+
+ return context
+}
diff --git a/services/web/frontend/js/features/ide-react/context/editor-properties-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-properties-context.tsx
new file mode 100644
index 0000000000..e8fb82efa1
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/context/editor-properties-context.tsx
@@ -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>
+ showSymbolPalette: boolean
+ setShowSymbolPalette: Dispatch>
+ toggleSymbolPalette: () => void
+ opening: boolean
+ setOpening: Dispatch>
+ trackChanges: boolean
+ setTrackChanges: Dispatch>
+ wantTrackChanges: boolean
+ setWantTrackChanges: Dispatch>
+ errorState: boolean
+ setErrorState: Dispatch>
+}
+
+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 = ({
+ 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 (
+
+ {children}
+
+ )
+}
+
+export const useEditorPropertiesContext = (): EditorPropertiesContextValue => {
+ const context = useContext(EditorPropertiesContext)
+ if (!context) {
+ throw new Error(
+ 'useEditorPropertiesContext is only available inside EditorPropertiesContext.Provider'
+ )
+ }
+ return context
+}
diff --git a/services/web/frontend/js/features/ide-react/context/editor-view-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-view-context.tsx
new file mode 100644
index 0000000000..e6bc055afc
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/context/editor-view-context.tsx
@@ -0,0 +1,49 @@
+import {
+ createContext,
+ Dispatch,
+ FC,
+ PropsWithChildren,
+ SetStateAction,
+ useContext,
+} from 'react'
+import { EditorView } from '@codemirror/view'
+import useExposedState from '@/shared/hooks/use-exposed-state'
+
+export type EditorContextValue = {
+ view: EditorView | null
+ setView: Dispatch>
+}
+
+// This provides access to the CodeMirror EditorView instance outside the editor
+// component itself, including external extensions (in particular, Writefull)
+export const EditorViewContext = createContext(
+ undefined
+)
+
+export const EditorViewProvider: FC = ({ children }) => {
+ const [view, setView] = useExposedState(
+ null,
+ 'editor.view'
+ )
+
+ const value = {
+ view,
+ setView,
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useEditorViewContext = (): EditorContextValue => {
+ const context = useContext(EditorViewContext)
+ if (!context) {
+ throw new Error(
+ 'useEditorViewContext is only available inside EditorViewProvider'
+ )
+ }
+ return context
+}
diff --git a/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx b/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx
index b46ac158c7..8c9078514c 100644
--- a/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx
+++ b/services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx
@@ -11,8 +11,7 @@ import {
import { useProjectContext } from '@/shared/context/project-context'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
-import useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only'
-import { BinaryFile } from '@/features/file-view/types/binary-file'
+import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
import {
FileTreeDocumentFindResult,
FileTreeFileRefFindResult,
@@ -22,6 +21,7 @@ import { debugConsole } from '@/utils/debugging'
import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
import { sendMB } from '@/infrastructure/event-tracking'
import { FileRef } from '../../../../../types/file-ref'
+import { useLayoutContext } from '@/shared/context/layout-context'
const FileTreeOpenContext = createContext<
| {
@@ -39,11 +39,13 @@ const FileTreeOpenContext = createContext<
export const FileTreeOpenProvider: FC = ({
children,
}) => {
- const { rootDocId, owner } = useProjectContext()
+ const { project } = useProjectContext()
+ const rootDocId = project?.rootDocId
+ const projectOwner = project?.owner?._id
const { eventEmitter, projectJoined } = useIdeReactContext()
- const { openDocWithId, currentDocumentId, openInitialDoc } =
- useEditorManagerContext()
- const [, setOpenFile] = useScopeValueSetterOnly('openFile')
+ const { openDocWithId, openInitialDoc } = useEditorManagerContext()
+ const { currentDocumentId } = useEditorOpenDocContext()
+ const { setOpenFile } = useLayoutContext()
const [openEntity, setOpenEntity] = useState<
FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
>(null)
@@ -81,7 +83,7 @@ export const FileTreeOpenProvider: FC = ({
openDocWithId(selected.entity._id, { keepCurrentView: true })
if (selected.entity.name.endsWith('.bib')) {
sendMB('open-bib-file', {
- projectOwner: owner._id,
+ projectOwner,
isSampleFile: selected.entity.name === 'sample.bib',
linkedFileProvider: null,
})
@@ -96,7 +98,7 @@ export const FileTreeOpenProvider: FC = ({
if (openFile) {
if (selected?.entity?.name?.endsWith('.bib')) {
sendMB('open-bib-file', {
- projectOwner: owner._id,
+ projectOwner,
isSampleFile: false,
linkedFileProvider: (selected.entity as FileRef).linkedFileData
?.provider,
@@ -105,7 +107,7 @@ export const FileTreeOpenProvider: FC = ({
window.dispatchEvent(new CustomEvent('file-view:file-opened'))
}
},
- [fileTreeReady, setOpenFile, openDocWithId, owner]
+ [fileTreeReady, setOpenFile, openDocWithId, projectOwner]
)
const handleFileTreeDelete = useCallback(
diff --git a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
index 51ecbdc6c9..cad238f53c 100644
--- a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
+++ b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
@@ -8,7 +8,6 @@ import React, {
useCallback,
} from 'react'
import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
-import populateLayoutScope from '@/features/ide-react/scope-adapters/layout-context-adapter'
import { IdeProvider } from '@/shared/context/ide-context'
import {
createIdeEventEmitter,
@@ -16,10 +15,12 @@ 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'
+import { type PermissionsLevel } from '@/features/ide-react/types/permissions'
+import { useProjectContext } from '@/shared/context/project-context'
+import { ProjectMetadata } from '@/shared/context/types/project-metadata'
const LOADED_AT = new Date()
@@ -32,57 +33,31 @@ type IdeReactContextValue = {
>
reportError: (error: any, meta?: Record) => void
projectJoined: boolean
+ permissionsLevel: PermissionsLevel
+ setPermissionsLevel: (permissionsLevel: PermissionsLevel) => void
+ setOutOfSync: (value: boolean) => void
}
export const IdeReactContext = createContext(
undefined
)
-function populateIdeReactScope(store: ReactScopeValueStore) {
- store.set('settings', {})
-}
-
-function populateProjectScope(store: ReactScopeValueStore) {
- store.allowNonExistentPath('project', true)
- store.set('permissionsLevel', 'readOnly')
- store.set('permissions', {
- read: true,
- write: false,
- admin: false,
- comment: true,
- })
-}
-
-function populatePdfScope(store: ReactScopeValueStore) {
- store.allowNonExistentPath('pdf', true)
-}
-
-export function createReactScopeValueStore(projectId: string) {
- const scopeStore = new ReactScopeValueStore()
-
- // Populate the scope value store with default values that will be used by
- // nested contexts that refer to scope values. The ideal would be to leave
- // initialization of store values up to the nested context, which would keep
- // 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)
- populateLayoutScope(scopeStore)
- populateProjectScope(scopeStore)
- populatePdfScope(scopeStore)
-
- scopeStore.allowNonExistentPath('hasLintingError')
-
- return scopeStore
-}
-
export const IdeReactProvider: FC = ({ children }) => {
const projectId = getMeta('ol-project_id')
- const [scopeStore] = useState(() => createReactScopeValueStore(projectId))
const [eventEmitter] = useState(createIdeEventEmitter)
+ const [permissionsLevel, setPermissionsLevel] =
+ useState('readOnly')
+ const [outOfSync, setOutOfSync] = useState(false)
const [scopeEventEmitter] = useState(
() => new ReactScopeEventEmitter(eventEmitter)
)
+ const [unstableStore] = useState(() => {
+ const store = new ReactScopeValueStore()
+ // Add dummy editor.ready key for Writefull, that relies on this calling
+ // back once after watching it
+ store.set('editor.ready', undefined)
+ return store
+ })
const [startedFreeTrial, setStartedFreeTrial] = useState(false)
const release = getMeta('ol-ExposedSettings')?.sentryRelease ?? null
@@ -91,6 +66,8 @@ export const IdeReactProvider: FC = ({ children }) => {
const [projectJoined, setProjectJoined] = useState(false)
const { socket, getSocketDebuggingInfo } = useConnectionContext()
+ const { joinProject, project } = useProjectContext()
+ const spellCheckLanguage = project?.spellCheckLanguage
const reportError = useCallback(
(error: any, meta?: Record) => {
@@ -102,7 +79,7 @@ export const IdeReactProvider: FC = ({ children }) => {
performance_now: performance.now(),
release,
client_load: LOADED_AT,
- spellCheckLanguage: scopeStore.get('project.spellCheckLanguage'),
+ spellCheckLanguage,
...getSocketDebuggingInfo(),
}
@@ -121,39 +98,39 @@ export const IdeReactProvider: FC = ({ children }) => {
},
})
},
- [release, projectId, getSocketDebuggingInfo, scopeStore]
+ [release, projectId, getSocketDebuggingInfo, spellCheckLanguage]
)
// Populate scope values when joining project, then fire project:joined event
useEffect(() => {
function handleJoinProjectResponse({
- project: { rootDoc_id: rootDocId, ..._project },
+ project: {
+ rootDoc_id: rootDocId,
+ publicAccesLevel: publicAccessLevel,
+ ..._project
+ },
permissionsLevel,
}: JoinProjectPayload) {
- const project = { ..._project, rootDocId }
- scopeStore.set('project', project)
- scopeStore.set('permissionsLevel', permissionsLevel)
- // Make watchers update immediately
- scopeStore.flushUpdates()
+ const project = { ..._project, rootDocId, publicAccessLevel }
+
+ // Cast the project from the payload as ProjectMetadata to ensure it has
+ // the correct type for the context. It must be close enough because the
+ // data structure hasn't changed and it worked previously. This type
+ // coercion was previously sidestepped by adding the project to the scope
+ // store, which does not enforce types.
+ joinProject(project as unknown as ProjectMetadata)
+
+ setPermissionsLevel(permissionsLevel)
eventEmitter.emit('project:joined', { project, permissionsLevel })
setProjectJoined(true)
}
- function handleMainBibliographyDocUpdated(payload: string) {
- scopeStore.set('project.mainBibliographyDoc_id', payload)
- }
-
socket.on('joinProjectResponse', handleJoinProjectResponse)
- socket.on('mainBibliographyDocUpdated', handleMainBibliographyDocUpdated)
return () => {
socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
- socket.removeListener(
- 'mainBibliographyDocUpdated',
- handleMainBibliographyDocUpdated
- )
}
- }, [socket, eventEmitter, scopeStore])
+ }, [socket, eventEmitter, joinProject])
const ide = useMemo(() => {
return {
@@ -168,19 +145,30 @@ export const IdeReactProvider: FC = ({ children }) => {
eventEmitter,
startedFreeTrial,
setStartedFreeTrial,
+ permissionsLevel: outOfSync ? 'readOnly' : permissionsLevel,
+ setPermissionsLevel,
+ setOutOfSync,
projectId,
reportError,
projectJoined,
}),
- [eventEmitter, projectId, projectJoined, reportError, startedFreeTrial]
+ [
+ eventEmitter,
+ outOfSync,
+ permissionsLevel,
+ projectId,
+ projectJoined,
+ reportError,
+ startedFreeTrial,
+ ]
)
return (
{children}
diff --git a/services/web/frontend/js/features/ide-react/context/metadata-context.tsx b/services/web/frontend/js/features/ide-react/context/metadata-context.tsx
index 4d5ab28050..f8aeec6ca9 100644
--- a/services/web/frontend/js/features/ide-react/context/metadata-context.tsx
+++ b/services/web/frontend/js/features/ide-react/context/metadata-context.tsx
@@ -10,10 +10,9 @@ import {
} from 'react'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
-import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
+import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
import { getJSON, postJSON } from '@/infrastructure/fetch-json'
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
-import { useEditorContext } from '@/shared/context/editor-context'
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
import useEventListener from '@/shared/hooks/use-event-listener'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
@@ -49,12 +48,11 @@ export const MetadataContext = createContext<
export const MetadataProvider: FC = ({ children }) => {
const { t } = useTranslation()
- const { eventEmitter, projectId } = useIdeReactContext()
+ const { eventEmitter, permissionsLevel, projectId } = useIdeReactContext()
const { socket } = useConnectionContext()
const { onlineUsersCount } = useOnlineUsersContext()
- const { permissionsLevel } = useEditorContext()
const permissions = usePermissionsContext()
- const { currentDocument } = useEditorManagerContext()
+ const { currentDocument } = useEditorOpenDocContext()
const { showGenericMessageModal } = useModalsContext()
const [documents, setDocuments] = useState({})
diff --git a/services/web/frontend/js/features/ide-react/context/online-users-context.tsx b/services/web/frontend/js/features/ide-react/context/online-users-context.tsx
index 1195f9ae7c..84a1b7d71f 100644
--- a/services/web/frontend/js/features/ide-react/context/online-users-context.tsx
+++ b/services/web/frontend/js/features/ide-react/context/online-users-context.tsx
@@ -18,7 +18,7 @@ import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
import { debugConsole } from '@/utils/debugging'
import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
import { getHueForUserId } from '@/shared/utils/colors'
-import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
+import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
export type OnlineUser = {
id: string
@@ -70,7 +70,7 @@ export const OnlineUsersProvider: FC = ({
}) => {
const { eventEmitter } = useIdeReactContext()
const { socket } = useConnectionContext()
- const { currentDocumentId } = useEditorManagerContext()
+ const { currentDocumentId } = useEditorOpenDocContext()
const { fileTreeData } = useFileTreeData()
const [onlineUsers, setOnlineUsers] = useState>({})
diff --git a/services/web/frontend/js/features/ide-react/context/outline-context.tsx b/services/web/frontend/js/features/ide-react/context/outline-context.tsx
index 8e582e607d..40c2cb1928 100644
--- a/services/web/frontend/js/features/ide-react/context/outline-context.tsx
+++ b/services/web/frontend/js/features/ide-react/context/outline-context.tsx
@@ -14,7 +14,7 @@ import * as eventTracking from '@/infrastructure/event-tracking'
import { isValidTeXFile } from '@/main/is-valid-tex-file'
import localStorage from '@/infrastructure/local-storage'
import { useProjectContext } from '@/shared/context/project-context'
-import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
+import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
export type PartialFlatOutline = {
level: number
@@ -118,13 +118,13 @@ export const OutlineProvider: FC = ({ children }) => {
[flatOutline, currentlyHighlightedLine]
)
- const { openDocName } = useEditorManagerContext()
+ const { openDocName } = useEditorOpenDocContext()
const isTexFile = useMemo(
() => (openDocName ? isValidTeXFile(openDocName) : false),
[openDocName]
)
- const { _id: projectId } = useProjectContext()
+ const { projectId } = useProjectContext()
const storageKey = `file_outline.expanded.${projectId}`
const [outlineExpanded, setOutlineExpanded] = useState(
diff --git a/services/web/frontend/js/features/ide-react/context/permissions-context.tsx b/services/web/frontend/js/features/ide-react/context/permissions-context.tsx
index 1e10a2cd11..c3621aac23 100644
--- a/services/web/frontend/js/features/ide-react/context/permissions-context.tsx
+++ b/services/web/frontend/js/features/ide-react/context/permissions-context.tsx
@@ -1,12 +1,11 @@
-import { createContext, useContext, useEffect } from 'react'
+import { createContext, useContext, useEffect, useState } from 'react'
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
-import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import getMeta from '@/utils/meta'
import {
Permissions,
PermissionsLevel,
} from '@/features/ide-react/types/permissions'
-import useScopeValue from '@/shared/hooks/use-scope-value'
import { DeepReadonly } from '../../../../../types/utils'
import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
import { useProjectContext } from '@/shared/context/project-context'
@@ -79,18 +78,27 @@ const noTrackChangesPermissionsMap: typeof permissionsMap = {
owner: permissionsMap.owner,
}
+const defaultPermissions: Permissions = {
+ read: true,
+ write: true,
+ admin: false,
+ comment: true,
+ resolveOwnComments: false,
+ resolveAllComments: false,
+ trackedWrite: true,
+ labelVersion: false,
+}
+
export const PermissionsProvider: React.FC = ({
children,
}) => {
const [permissions, setPermissions] =
- useScopeValue>('permissions')
+ useState(defaultPermissions)
const { connectionState } = useConnectionContext()
- const { permissionsLevel } = useEditorContext() as {
- permissionsLevel: PermissionsLevel
- }
+ const { permissionsLevel } = useIdeReactContext()
const hasViewerPermissions = useViewerPermissions()
const anonymous = getMeta('ol-anonymous')
- const project = useProjectContext()
+ const { features } = useProjectContext()
useEffect(() => {
let activePermissionsMap
@@ -98,7 +106,7 @@ export const PermissionsProvider: React.FC = ({
activePermissionsMap = linkSharingWarningPermissionsMap
} else if (anonymous) {
activePermissionsMap = anonymousPermissionsMap
- } else if (!project.features.trackChanges) {
+ } else if (!features.trackChanges) {
activePermissionsMap = noTrackChangesPermissionsMap
} else {
activePermissionsMap = permissionsMap
@@ -109,7 +117,7 @@ export const PermissionsProvider: React.FC = ({
permissionsLevel,
setPermissions,
hasViewerPermissions,
- project.features.trackChanges,
+ features.trackChanges,
])
useEffect(() => {
diff --git a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx
index 0eeb870a2b..6236f2bfe2 100644
--- a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx
+++ b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx
@@ -1,10 +1,13 @@
-import { FC } from 'react'
+import React, { FC, PropsWithChildren } from 'react'
import { ChatProvider } from '@/features/chat/context/chat-context'
import { ConnectionProvider } from './connection-context'
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'
import { FileTreeOpenProvider } from '@/features/ide-react/context/file-tree-open-context'
import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
@@ -30,7 +33,7 @@ import { CommandRegistryProvider } from './command-registry-context'
export const ReactContextRoot: FC<
React.PropsWithChildren<{
- providers?: Record
+ providers?: Record>
}>
> = ({ children, providers = {} }) => {
const Providers = {
@@ -39,7 +42,10 @@ export const ReactContextRoot: FC<
DetachCompileProvider,
DetachProvider,
EditorManagerProvider,
+ EditorOpenDocProvider,
+ EditorPropertiesProvider,
EditorProvider,
+ EditorViewProvider,
FileTreeDataProvider,
FileTreeOpenProvider,
FileTreePathProvider,
@@ -69,57 +75,63 @@ export const ReactContextRoot: FC<
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/services/web/frontend/js/features/ide-react/context/snapshot-context.tsx b/services/web/frontend/js/features/ide-react/context/snapshot-context.tsx
index 817e03fe86..34b782b5bf 100644
--- a/services/web/frontend/js/features/ide-react/context/snapshot-context.tsx
+++ b/services/web/frontend/js/features/ide-react/context/snapshot-context.tsx
@@ -55,7 +55,7 @@ export const SnapshotContext = createContext<
>(undefined)
export const SnapshotProvider: FC = ({ children }) => {
- const { _id: projectId } = useProjectContext()
+ const { projectId } = useProjectContext()
const [snapshotLoadingState, setSnapshotLoadingState] =
useState('')
const [snapshotUpdater] = useState(() => new SnapshotUpdater(projectId))
diff --git a/services/web/frontend/js/features/ide-react/hooks/use-socket-listeners.ts b/services/web/frontend/js/features/ide-react/hooks/use-socket-listeners.ts
index 6b2f305593..55d9f01ba9 100644
--- a/services/web/frontend/js/features/ide-react/hooks/use-socket-listeners.ts
+++ b/services/web/frontend/js/features/ide-react/hooks/use-socket-listeners.ts
@@ -4,7 +4,7 @@ import {
listProjectInvites,
listProjectMembers,
} from '@/features/share-project-modal/utils/api'
-import useScopeValue from '@/shared/hooks/use-scope-value'
+import { useProjectContext } from '@/shared/context/project-context'
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
@@ -12,17 +12,13 @@ import { debugConsole } from '@/utils/debugging'
import { useCallback } from 'react'
import { PublicAccessLevel } from '../../../../../types/public-access-level'
import { useLocation } from '@/shared/hooks/use-location'
-import { useEditorContext } from '@/shared/context/editor-context'
function useSocketListeners() {
const { t } = useTranslation()
const { socket } = useConnectionContext()
- const { projectId } = useIdeReactContext()
const { showGenericMessageModal } = useModalsContext()
- const { permissionsLevel } = useEditorContext()
- const [, setPublicAccessLevel] = useScopeValue('project.publicAccesLevel')
- const [, setProjectMembers] = useScopeValue('project.members')
- const [, setProjectInvites] = useScopeValue('project.invites')
+ const { permissionsLevel } = useIdeReactContext()
+ const { projectId, updateProject } = useProjectContext()
const location = useLocation()
useSocketListener(
@@ -53,10 +49,10 @@ function useSocketListeners() {
useCallback(
(data: { newAccessLevel?: PublicAccessLevel }) => {
if (data.newAccessLevel) {
- setPublicAccessLevel(data.newAccessLevel)
+ updateProject({ publicAccessLevel: data.newAccessLevel })
}
},
- [setPublicAccessLevel]
+ [updateProject]
)
)
@@ -67,13 +63,13 @@ function useSocketListeners() {
listProjectMembers(projectId)
.then(({ members }) => {
if (members) {
- setProjectMembers(members)
+ updateProject({ members })
}
})
.catch(err => {
debugConsole.error('Error fetching members for project', err)
})
- }, [projectId, setProjectMembers])
+ }, [projectId, updateProject])
)
useSocketListener(
@@ -85,7 +81,7 @@ function useSocketListeners() {
listProjectMembers(projectId)
.then(({ members }) => {
if (members) {
- setProjectMembers(members)
+ updateProject({ members })
}
})
.catch(err => {
@@ -97,7 +93,7 @@ function useSocketListeners() {
listProjectInvites(projectId)
.then(({ invites }) => {
if (invites) {
- setProjectInvites(invites)
+ updateProject({ invites })
}
})
.catch(err => {
@@ -105,7 +101,29 @@ function useSocketListeners() {
})
}
},
- [projectId, setProjectInvites, setProjectMembers, permissionsLevel]
+ [projectId, updateProject, permissionsLevel]
+ )
+ )
+
+ useSocketListener(
+ socket,
+ 'mainBibliographyDocUpdated',
+ useCallback(
+ (payload: string) => {
+ updateProject({ mainBibliographyDocId: payload })
+ },
+ [updateProject]
+ )
+ )
+
+ useSocketListener(
+ socket,
+ 'projectNameUpdated',
+ useCallback(
+ (payload: string) => {
+ updateProject({ name: payload })
+ },
+ [updateProject]
)
)
}
diff --git a/services/web/frontend/js/features/ide-react/hooks/use-status-favicon.ts b/services/web/frontend/js/features/ide-react/hooks/use-status-favicon.ts
new file mode 100644
index 0000000000..c65d49e042
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/hooks/use-status-favicon.ts
@@ -0,0 +1,75 @@
+import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
+import { useEffect, useState } from 'react'
+import usePreviousValue from '@/shared/hooks/use-previous-value'
+
+const RESET_AFTER_MS = 5_000
+
+const COMPILE_ICONS = {
+ ERROR: '/favicon-error.svg',
+ COMPILING: '/favicon-compiling.svg',
+ COMPILED: '/favicon-compiled.svg',
+ UNCOMPILED: '/favicon.svg',
+}
+
+type CompileStatus = keyof typeof COMPILE_ICONS
+
+const useCompileStatus = (): CompileStatus => {
+ const compileContext = useCompileContext()
+ if (compileContext.uncompiled) return 'UNCOMPILED'
+ if (compileContext.compiling) return 'COMPILING'
+ if (compileContext.error) return 'ERROR'
+ return 'COMPILED'
+}
+
+const removeFavicon = () => {
+ const existingFavicons = document.head.querySelectorAll(
+ "link[rel='icon']"
+ ) as NodeListOf
+ existingFavicons.forEach(favicon => {
+ if (favicon.href.endsWith('.svg')) favicon.parentNode?.removeChild(favicon)
+ })
+}
+
+const updateFavicon = (status: CompileStatus = 'UNCOMPILED') => {
+ removeFavicon()
+ const linkElement = document.createElement('link')
+ linkElement.rel = 'icon'
+ linkElement.href = COMPILE_ICONS[status]
+ linkElement.type = 'image/svg+xml'
+ linkElement.setAttribute('data-compile-status', 'true')
+ document.head.appendChild(linkElement)
+}
+
+const isActive = () => !document.hidden
+
+const useIsWindowActive = () => {
+ const [isWindowActive, setIsWindowActive] = useState(isActive())
+ useEffect(() => {
+ const handleVisibilityChange = () => setIsWindowActive(isActive())
+ document.addEventListener('visibilitychange', handleVisibilityChange)
+ return () => {
+ document.removeEventListener('visibilitychange', handleVisibilityChange)
+ }
+ }, [])
+ return isWindowActive
+}
+
+export const useStatusFavicon = () => {
+ const compileStatus = useCompileStatus()
+ const previousCompileStatus = usePreviousValue(compileStatus)
+ const isWindowActive = useIsWindowActive()
+
+ useEffect(() => {
+ if (previousCompileStatus !== compileStatus) {
+ return updateFavicon(compileStatus)
+ }
+
+ if (
+ isWindowActive &&
+ (compileStatus === 'COMPILED' || compileStatus === 'ERROR')
+ ) {
+ const timeout = setTimeout(updateFavicon, RESET_AFTER_MS)
+ return () => clearTimeout(timeout)
+ }
+ }, [compileStatus, isWindowActive, previousCompileStatus])
+}
diff --git a/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts b/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts
deleted file mode 100644
index 42e03ff8f6..0000000000
--- a/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts
+++ /dev/null
@@ -1,58 +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
- open_doc_id: string | null
- open_doc_name: string | 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 = {
- showSymbolPalette: false,
- toggleSymbolPalette: () => {},
- sharejs_doc: null,
- open_doc_id: null,
- open_doc_name: 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'
-}
diff --git a/services/web/frontend/js/features/ide-react/scope-adapters/layout-context-adapter.ts b/services/web/frontend/js/features/ide-react/scope-adapters/layout-context-adapter.ts
deleted file mode 100644
index 88f252f70b..0000000000
--- a/services/web/frontend/js/features/ide-react/scope-adapters/layout-context-adapter.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { ReactScopeValueStore } from '../scope-value-store/react-scope-value-store'
-import getMeta from '@/utils/meta'
-
-const reviewPanelStorageKey = `ui.reviewPanelOpen.${getMeta('ol-project_id')}`
-
-export default function populateLayoutScope(store: ReactScopeValueStore) {
- store.set('ui.view', 'editor')
- store.set('openFile', null)
- store.persisted('ui.chatOpen', false, 'ui.chatOpen')
- store.persisted('ui.reviewPanelOpen', false, reviewPanelStorageKey)
- store.set('ui.leftMenuShown', false)
- store.set('ui.miniReviewPanelVisible', false)
- store.set('ui.pdfLayout', 'sideBySide')
-}
diff --git a/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx b/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx
index 9949b98c7f..1c21df0f82 100644
--- a/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx
@@ -83,7 +83,7 @@ export default function Breadcrumbs() {
const numOutlineItems = outlineHierarchy.length
return (
-
+
{folderHierarchy.map(folder => (
{folder.name}
diff --git a/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx b/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx
index 057ea69266..9a4ffe3a1b 100644
--- a/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx
@@ -27,8 +27,6 @@ function getAvatarStyle(user?: User) {
}
function Message({ message, fromSelf }: MessageProps) {
- const userAvailable = message.user?.id && message.user.email
-
return (
@@ -36,7 +34,7 @@ function Message({ message, fromSelf }: MessageProps) {
{!fromSelf && (
- {userAvailable
+ {message.user?.id && message.user.email
? message.user.first_name || message.user.email
: t('deleted_user')}
@@ -49,7 +47,7 @@ function Message({ message, fromSelf }: MessageProps) {
{!fromSelf && index === message.contents.length - 1 ? (
- {userAvailable ? (
+ {message.user?.id && message.user.email ? (
message.user.first_name?.charAt(0) ||
message.user.email.charAt(0)
) : (
diff --git a/services/web/frontend/js/features/ide-redesign/components/editor-panel.tsx b/services/web/frontend/js/features/ide-redesign/components/editor-panel.tsx
index 18fcad1395..d8204da5da 100644
--- a/services/web/frontend/js/features/ide-redesign/components/editor-panel.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/editor-panel.tsx
@@ -12,7 +12,10 @@ export default function EditorPanel() {
{selectedEntityCount === 0 &&
}
{selectedEntityCount === 1 && openEntity?.type === 'fileRef' && (
-
+
)}
{selectedEntityCount > 1 && (
diff --git a/services/web/frontend/js/features/ide-redesign/components/editor.tsx b/services/web/frontend/js/features/ide-redesign/components/editor.tsx
index 6c5e0b40db..785ba1fb99 100644
--- a/services/web/frontend/js/features/ide-redesign/components/editor.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/editor.tsx
@@ -1,8 +1,6 @@
import { LoadingPane } from '@/features/ide-react/components/editor/loading-pane'
-import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
-import { EditorScopeValue } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter'
+import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
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,20 +8,20 @@ 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
('editor')
+ const { opening, errorState, showSymbolPalette } =
+ useEditorPropertiesContext()
const { selectedEntityCount, openEntity } = useFileTreeOpenContext()
- const { currentDocumentId } = useEditorManagerContext()
+ const { currentDocumentId, currentDocument } = useEditorOpenDocContext()
if (!currentDocumentId) {
return null
}
const isLoading = Boolean(
- (!editor.sharejs_doc || editor.opening) &&
- !editor.error_state &&
- editor.open_doc_id
+ (!currentDocument || opening) && !errorState && currentDocumentId
)
return (
@@ -44,7 +42,7 @@ export const Editor = () => {
{isLoading && }
- {editor.showSymbolPalette && (
+ {showSymbolPalette && (
<>
0}
+ hasErrors={
+ includeErrors &&
+ logEntries?.errors &&
+ logEntries?.errors.length > 0
+ }
/>
)}
diff --git a/services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry-header.tsx b/services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry-header.tsx
index ce43af3744..3b4c9f6347 100644
--- a/services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry-header.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry-header.tsx
@@ -104,12 +104,13 @@ function LogEntryHeader({
overlayProps={{ placement: 'bottom' }}
>
)}
{actionComponents.map(({ import: { default: Component }, path }) => (
-
+
))}
)}
diff --git a/services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry.tsx b/services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry.tsx
index a7539450ce..4ff66ba6d3 100644
--- a/services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry.tsx
@@ -40,7 +40,7 @@ type LogEntryProps = {
}
function LogEntry(props: LogEntryProps) {
- const [collapsed, setCollapsed] = useState(true)
+ const [collapsed, setCollapsed] = useState(props.index !== 0)
return (
{
- if (!inactiveTutorials.includes(TUTORIAL_KEY)) {
- tryShowingPopup()
- }
- }, [tryShowingPopup, inactiveTutorials])
-
- const onHide = useCallback(() => {
- hideUntilReload()
- }, [hideUntilReload])
-
- const onClose = useCallback(() => {
- completeTutorial({
- action: 'complete',
- event: 'promo-dismiss',
- })
- }, [completeTutorial])
-
- if (!target) {
- return null
- }
-
- return (
-
-
-
- {t('error_logs_have_had_an_update')}
-
-
-
-
- )
-}
diff --git a/services/web/frontend/js/features/ide-redesign/components/file-tree/file-tree-action-buttons.tsx b/services/web/frontend/js/features/ide-redesign/components/file-tree/file-tree-action-buttons.tsx
index 93760b97ab..bcac7c4ba9 100644
--- a/services/web/frontend/js/features/ide-redesign/components/file-tree/file-tree-action-buttons.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/file-tree/file-tree-action-buttons.tsx
@@ -6,11 +6,13 @@ import React from 'react'
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import FileTreeActionButton from './file-tree-action-button'
+import { useRailContext } from '../../contexts/rail-context'
export default function FileTreeActionButtons() {
const { t } = useTranslation()
const { fileTreeReadOnly } = useFileTreeData()
const { write } = usePermissionsContext()
+ const { handlePaneCollapse } = useRailContext()
const {
canCreate,
@@ -91,7 +93,7 @@ export default function FileTreeActionButtons() {
id="upload"
description={t('upload')}
onClick={uploadWithAnalytics}
- iconType="upload_file"
+ iconType="upload"
/>
)}
{canBulkDelete && (
@@ -102,6 +104,12 @@ export default function FileTreeActionButtons() {
iconType="delete"
/>
)}
+
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx b/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx
index bac1787e10..07aaa647a9 100644
--- a/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx
@@ -86,9 +86,10 @@ const OnlineUserWidget = ({
const OnlineUserCircle = ({ user }: { user: OnlineUser }) => {
const backgroundColor = getBackgroundColorForUserId(user.user_id)
+ const [character] = [...user.name]
return (
- {user.name.charAt(0)}
+ {character}
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/rail-panel-header.tsx b/services/web/frontend/js/features/ide-redesign/components/rail-panel-header.tsx
index 94ac2f42af..47d45c6c46 100644
--- a/services/web/frontend/js/features/ide-redesign/components/rail-panel-header.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/rail-panel-header.tsx
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'
import { useRailContext } from '../contexts/rail-context'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import React from 'react'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
export default function RailPanelHeader({
title,
@@ -18,13 +19,19 @@ export default function RailPanelHeader({
)
diff --git a/services/web/frontend/js/features/ide-redesign/components/rail.tsx b/services/web/frontend/js/features/ide-redesign/components/rail.tsx
index f3c741155c..42374583ab 100644
--- a/services/web/frontend/js/features/ide-redesign/components/rail.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/rail.tsx
@@ -1,11 +1,4 @@
-import {
- FC,
- forwardRef,
- ReactElement,
- useCallback,
- useMemo,
- useRef,
-} from 'react'
+import { FC, forwardRef, ReactElement, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Nav, NavLink, Tab, TabContainer } from 'react-bootstrap'
import MaterialIcon, {
@@ -38,7 +31,6 @@ import { RailHelpContactUsModal } from './help/contact-us'
import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
import DictionarySettingsModal from './settings/editor-settings/dictionary-settings-modal'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
-import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { useChatContext } from '@/features/chat/context/chat-context'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
import {
@@ -51,7 +43,9 @@ import { useDetachCompileContext as useCompileContext } from '@/shared/context/d
import OldErrorPane from './error-logs/old-error-pane'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useSurveyUrl } from '../hooks/use-survey-url'
-import NewErrorLogsPromo from './error-logs/new-error-logs-promo'
+import { useProjectContext } from '@/shared/context/project-context'
+import usePreviousValue from '@/shared/hooks/use-previous-value'
+import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
type RailElement = {
icon: AvailableUnfilledIcon
@@ -61,6 +55,7 @@ type RailElement = {
title: string
hide?: boolean
disabled?: boolean
+ mountOnFirstLoad?: boolean
}
type RailActionButton = {
@@ -111,10 +106,9 @@ export const RailLayout = () => {
setResizing,
} = useRailContext()
const { logEntries } = useCompileContext()
+ const { features } = useProjectContext()
const errorLogsDisabled = !logEntries
- const errorsTabRef = useRef
(null)
-
const { view, setLeftMenuShown } = useLayoutContext()
const { markMessagesAsRead } = useChatContext()
@@ -130,6 +124,9 @@ export const RailLayout = () => {
icon: 'description',
title: t('file_tree'),
component: ,
+ // NOTE: We always need to mount the file tree on first load
+ // since it is responsible for opening the initial document.
+ mountOnFirstLoad: true,
},
{
key: 'full-project-search',
@@ -149,6 +146,8 @@ export const RailLayout = () => {
icon: 'rate_review',
title: t('review_panel'),
component: null,
+ hide: !features.trackChangesVisible,
+ disabled: view !== 'editor',
},
{
key: 'chat',
@@ -167,7 +166,7 @@ export const RailLayout = () => {
disabled: errorLogsDisabled,
},
],
- [t, errorLogsDisabled, newErrorlogs]
+ [t, features.trackChangesVisible, newErrorlogs, errorLogsDisabled, view]
)
const railActions: RailAction[] = useMemo(
@@ -191,6 +190,19 @@ export const RailLayout = () => {
[setLeftMenuShown, t, sendEvent]
)
+ useCommandProvider(
+ () => [
+ {
+ id: 'open-settings',
+ handler: () => {
+ setLeftMenuShown(true)
+ },
+ label: t('settings'),
+ },
+ ],
+ [t, setLeftMenuShown]
+ )
+
const onTabSelect = useCallback(
(key: string | null) => {
if (key === selectedTab) {
@@ -225,6 +237,18 @@ export const RailLayout = () => {
const isReviewPanelOpen = selectedTab === 'review-panel'
+ const prevTab = usePreviousValue(selectedTab)
+
+ const tabHasChanged = useMemo(() => {
+ return prevTab !== selectedTab
+ }, [prevTab, selectedTab])
+
+ const onCollapse = useCallback(() => {
+ if (!tabHasChanged) {
+ handlePaneCollapse()
+ }
+ }, [tabHasChanged, handlePaneCollapse])
+
return (
{
.filter(({ hide }) => !hide)
.map(({ icon, key, indicator, title, disabled }) => (
{
{railActions?.map(action => (
))}
- {newErrorlogs && }
{
maxSize={80}
ref={panelRef}
collapsible
- onCollapse={handlePaneCollapse}
+ onCollapse={onCollapse}
onExpand={handlePaneExpand}
>
{isHistoryView && }
@@ -282,8 +304,12 @@ export const RailLayout = () => {
{railTabs
.filter(({ hide }) => !hide)
- .map(({ key, component }) => (
-
+ .map(({ key, component, mountOnFirstLoad }) => (
+
{component}
))}
@@ -400,13 +426,17 @@ const RailActionElement = ({ action }: { action: RailAction }) => {
description={action.title}
overlayProps={{ delay: 0, placement: 'right' }}
>
-
+ aria-label={action.title}
+ >
+
+
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/editor-theme-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/editor-theme-setting.tsx
index eba0fc5b6c..1a1a3f1c1e 100644
--- a/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/editor-theme-setting.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/editor-theme-setting.tsx
@@ -41,6 +41,7 @@ export default function EditorThemeSetting() {
options={options}
onChange={setEditorTheme}
value={editorTheme}
+ translateOptions="no"
/>
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/font-family-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/font-family-setting.tsx
index d0310e2b1b..f1c279cf23 100644
--- a/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/font-family-setting.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/font-family-setting.tsx
@@ -27,6 +27,7 @@ export default function FontFamilySetting() {
onChange={setFontFamily}
value={fontFamily}
width="wide"
+ translateOptions="no"
/>
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/compiler-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/compiler-setting.tsx
index c007a0608e..1da6a167c6 100644
--- a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/compiler-setting.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/compiler-setting.tsx
@@ -38,6 +38,7 @@ export default function CompilerSetting() {
options={OPTIONS}
onChange={setCompiler}
value={compiler}
+ translateOptions="no"
/>
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/image-name-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/image-name-setting.tsx
index 0574657971..0a7166ae68 100644
--- a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/image-name-setting.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/image-name-setting.tsx
@@ -38,6 +38,7 @@ export default function ImageNameSetting() {
options={options}
onChange={setImageName}
value={imageName}
+ translateOptions="no"
/>
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx
index bd493a018a..719ea4da5a 100644
--- a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx
@@ -44,6 +44,7 @@ export default function RootDocumentSetting() {
options={validDocsOptions}
onChange={setRootDocId}
value={rootDocId}
+ translateOptions="no"
/>
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/dropdown-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/dropdown-setting.tsx
index 47c5c54ef0..3d9db79c3d 100644
--- a/services/web/frontend/js/features/ide-redesign/components/settings/dropdown-setting.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/dropdown-setting.tsx
@@ -31,6 +31,7 @@ type SettingsMenuSelectProps = {
disabled?: boolean
width?: 'default' | 'wide'
loading?: boolean
+ translateOptions?: 'yes' | 'no'
}
export default function DropdownSetting({
@@ -44,6 +45,7 @@ export default function DropdownSetting({
disabled = false,
width = 'default',
loading = false,
+ translateOptions,
}: SettingsMenuSelectProps) {
const handleChange: ChangeEventHandler = useCallback(
event => {
@@ -78,6 +80,7 @@ export default function DropdownSetting({
onChange={handleChange}
value={value?.toString()}
disabled={disabled}
+ translate={translateOptions}
>
{options.map(option => (
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx
index b9a1fddbfe..99b3cea3eb 100644
--- a/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx
@@ -1,7 +1,8 @@
import MaterialIcon, {
AvailableUnfilledIcon,
} from '@/shared/components/material-icon'
-import { ReactElement, useMemo, useState } from 'react'
+import { ReactElement } from 'react'
+
import {
Nav,
NavLink,
@@ -9,10 +10,6 @@ import {
TabContent,
TabPane,
} from 'react-bootstrap'
-import { useTranslation } from 'react-i18next'
-import EditorSettings from './editor-settings/editor-settings'
-import AppearanceSettings from './appearance-settings/appearance-settings'
-import CompilerSettings from './compiler-settings/compiler-settings'
export type SettingsEntry = SettingsLink | SettingsTab
@@ -30,41 +27,15 @@ type SettingsLink = {
title: string
}
-export const SettingsModalBody = () => {
- const { t } = useTranslation()
- const settingsTabs: SettingsEntry[] = useMemo(
- () => [
- {
- key: 'editor',
- title: t('editor'),
- icon: 'code',
- component: ,
- },
- {
- key: 'compiler',
- title: t('compiler'),
- icon: 'picture_as_pdf',
- component: ,
- },
- {
- key: 'appearance',
- title: t('appearance'),
- icon: 'brush',
- component: ,
- },
- {
- key: 'account_settings',
- title: t('account_settings'),
- icon: 'settings',
- href: '/user/settings',
- },
- ],
- [t]
- )
- const [activeTab, setActiveTab] = useState(
- settingsTabs[0]?.key
- )
-
+export const SettingsModalBody = ({
+ activeTab,
+ setActiveTab,
+ settingsTabs,
+}: {
+ activeTab: string | null | undefined
+ setActiveTab: (tab: string | null | undefined) => void
+ settingsTabs: SettingsEntry[]
+}) => {
return (
{
// TODO ide-redesign-cleanup: Either rename the field, or introduce a separate
// one
const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
const { t } = useTranslation()
+ const settingsTabs: SettingsEntry[] = useMemo(
+ () => [
+ {
+ key: 'editor',
+ title: t('editor'),
+ icon: 'code',
+ component: ,
+ },
+ {
+ key: 'compiler',
+ title: t('compiler'),
+ icon: 'picture_as_pdf',
+ component: ,
+ },
+ {
+ key: 'appearance',
+ title: t('appearance'),
+ icon: 'brush',
+ component: ,
+ },
+ {
+ key: 'account_settings',
+ title: t('account_settings'),
+ icon: 'settings',
+ href: '/user/settings',
+ },
+ {
+ key: 'subscription',
+ title: t('subscription'),
+ icon: 'account_balance',
+ href: '/user/subscription',
+ },
+ ],
+ [t]
+ )
+ const [activeTab, setActiveTab] = useState(
+ settingsTabs[0]?.key
+ )
return (
setLeftMenuShown(false)}
size="lg"
+ backdropClassName={
+ activeTab === 'appearance'
+ ? 'ide-settings-modal-transparent-backdrop'
+ : undefined
+ }
>
{t('settings')}
-
+
)
diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/download-project.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/download-project.tsx
index 712c874309..9be57c130e 100644
--- a/services/web/frontend/js/features/ide-redesign/components/toolbar/download-project.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/download-project.tsx
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
export const DownloadProjectZip = () => {
const { t } = useTranslation()
- const { _id: projectId } = useProjectContext()
+ const { projectId } = useProjectContext()
const sendDownloadEvent = useCallback(() => {
sendMB('download-zip-button-click', {
projectId,
@@ -44,7 +44,7 @@ export const DownloadProjectZip = () => {
export const DownloadProjectPDF = () => {
const { t } = useTranslation()
const { pdfDownloadUrl, pdfUrl } = useCompileContext()
- const { _id: projectId } = useProjectContext()
+ const { projectId } = useProjectContext()
const sendDownloadEvent = useCallback(() => {
sendMB('download-pdf-button-click', {
projectId,
diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/duplicate-project.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/duplicate-project.tsx
index 74f868cc91..1cfa4581b7 100644
--- a/services/web/frontend/js/features/ide-redesign/components/toolbar/duplicate-project.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/duplicate-project.tsx
@@ -36,7 +36,7 @@ export const DuplicateProject = () => {
return (
<>
- {t('copy')}
+ {t('make_a_copy')}
{
setShowSwitcherModal(true)
}, [setShowSwitcherModal])
const surveyURL = useSurveyUrl()
+
return (
<>
diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/logos.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/logos.tsx
new file mode 100644
index 0000000000..8f6c60682c
--- /dev/null
+++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/logos.tsx
@@ -0,0 +1,46 @@
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import MaterialIcon from '@/shared/components/material-icon'
+import { useTranslation } from 'react-i18next'
+import { Cobranding } from '../../../../../../types/cobranding'
+
+type ToolbarLogosProps = {
+ cobranding?: Cobranding
+}
+
+export const ToolbarLogos = ({ cobranding }: ToolbarLogosProps) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+ {cobranding && cobranding.logoImgUrl && (
+ <>
+
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx
index d2593d7d07..acb737b33b 100644
--- a/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx
@@ -72,6 +72,10 @@ export const ToolbarMenuBar = () => {
id: 'file-download',
children: ['download-as-source-zip', 'download-pdf'],
},
+ {
+ id: 'settings',
+ children: ['open-settings'],
+ },
],
[]
)
diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/project-title.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/project-title.tsx
index 61e29023a0..b975909f6e 100644
--- a/services/web/frontend/js/features/ide-redesign/components/toolbar/project-title.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/project-title.tsx
@@ -9,6 +9,7 @@ import { useProjectContext } from '@/shared/context/project-context'
import { useTranslation } from 'react-i18next'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { DownloadProjectPDF, DownloadProjectZip } from './download-project'
import { useCallback, useState } from 'react'
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
@@ -16,11 +17,13 @@ import EditableLabel from './editable-label'
import { DuplicateProject } from './duplicate-project'
const [publishModalModules] = importOverleafModules('publishModal')
-const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
+const SubmitProjectButton = publishModalModules?.import.NewPublishDropdownButton
export const ToolbarProjectTitle = () => {
+ const { cobranding } = useEditorContext()
const { t } = useTranslation()
- const { permissionsLevel, renameProject } = useEditorContext()
+ const { renameProject } = useEditorContext()
+ const { permissionsLevel } = useIdeReactContext()
const { name } = useProjectContext()
const shouldDisplaySubmitButton =
(permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite') &&
@@ -58,14 +61,16 @@ export const ToolbarProjectTitle = () => {
id="project-title-options"
className="ide-redesign-toolbar-project-dropdown-toggle ide-redesign-toolbar-dropdown-toggle-subdued fw-bold ide-redesign-toolbar-button-subdued"
>
-
{name}
+
+ {name}
+
- {shouldDisplaySubmitButton && (
+ {shouldDisplaySubmitButton && !cobranding && (
<>
diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/show-history-button.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/show-history-button.tsx
index 5bf25c8852..5540dc5d3b 100644
--- a/services/web/frontend/js/features/ide-redesign/components/toolbar/show-history-button.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/show-history-button.tsx
@@ -7,15 +7,18 @@ import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
export default function ShowHistoryButton() {
const { t } = useTranslation()
- const { view, setView } = useLayoutContext()
+ const { view, setView, restoreView } = useLayoutContext()
const { sendEvent } = useEditorAnalytics()
const toggleHistoryOpen = useCallback(() => {
const action = view === 'history' ? 'close' : 'open'
sendEvent('navigation-clicked-history', { action })
-
- setView(view === 'history' ? 'editor' : 'history')
- }, [view, setView, sendEvent])
+ if (view === 'history') {
+ restoreView()
+ } else {
+ setView('history')
+ }
+ }, [view, setView, sendEvent, restoreView])
return (
diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx
index 298e6f8e93..abe2012231 100644
--- a/services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx
@@ -1,5 +1,3 @@
-import MaterialIcon from '@/shared/components/material-icon'
-import { useTranslation } from 'react-i18next'
import { ToolbarMenuBar } from './menu-bar'
import { ToolbarProjectTitle } from './project-title'
import { OnlineUsers } from './online-users'
@@ -11,17 +9,28 @@ import { useLayoutContext } from '@/shared/context/layout-context'
import BackToEditorButton from '@/features/editor-navigation-toolbar/components/back-to-editor-button'
import { useCallback } from 'react'
import * as eventTracking from '../../../../infrastructure/event-tracking'
-import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import { ToolbarLogos } from './logos'
+import { useEditorContext } from '@/shared/context/editor-context'
+import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import UpgradeButton from './upgrade-button'
import getMeta from '@/utils/meta'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
+
+const [publishModalModules] = importOverleafModules('publishModal')
+const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
export const Toolbar = () => {
- const { view, setView } = useLayoutContext()
+ const { view, restoreView } = useLayoutContext()
+ const { cobranding } = useEditorContext()
+ const { permissionsLevel } = useIdeReactContext()
+ const shouldDisplaySubmitButton =
+ (permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite') &&
+ SubmitProjectButton
const handleBackToEditorClick = useCallback(() => {
eventTracking.sendMB('navigation-clicked-history', { action: 'close' })
- setView('editor')
- }, [setView])
+ restoreView()
+ }, [restoreView])
if (view === 'history') {
return (
@@ -37,43 +46,22 @@ export const Toolbar = () => {
return (
- )
-}
-
-const ToolbarMenus = () => {
- const { t } = useTranslation()
- return (
-
- )
-}
-
-const ToolbarButtons = () => {
- return (
-
-
-
-
-
-
- {getMeta('ol-showUpgradePrompt') &&
}
+
+
+
+
+
+ {shouldDisplaySubmitButton && cobranding && (
+
+ )}
+
+ {getMeta('ol-showUpgradePrompt') && }
+
)
}
diff --git a/services/web/frontend/js/features/ide-redesign/components/tooltip-promo.tsx b/services/web/frontend/js/features/ide-redesign/components/tooltip-promo.tsx
new file mode 100644
index 0000000000..f5d21376b1
--- /dev/null
+++ b/services/web/frontend/js/features/ide-redesign/components/tooltip-promo.tsx
@@ -0,0 +1,73 @@
+import Close from '@/shared/components/close'
+import { useEditorContext } from '@/shared/context/editor-context'
+import useTutorial from '@/shared/hooks/promotions/use-tutorial'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
+import classNames from 'classnames'
+import { useCallback, useEffect } from 'react'
+import { Overlay, OverlayProps, Popover } from 'react-bootstrap'
+
+export default function TooltipPromotion({
+ target,
+ tutorialKey,
+ eventData,
+ className,
+ content,
+ header,
+ placement = 'bottom',
+ splitTestName,
+}: {
+ target: HTMLElement | null
+ tutorialKey: string
+ eventData: Record
+ className?: string
+ content: string
+ header?: string
+ placement?: OverlayProps['placement']
+ splitTestName?: string
+}) {
+ const { inactiveTutorials } = useEditorContext()
+ const { showPopup, tryShowingPopup, hideUntilReload, dismissTutorial } =
+ useTutorial(tutorialKey, eventData)
+
+ useEffect(() => {
+ if (!inactiveTutorials.includes(tutorialKey)) {
+ tryShowingPopup()
+ }
+ }, [tryShowingPopup, inactiveTutorials, tutorialKey])
+
+ const isInSplitTestIfNeeded = splitTestName
+ ? isSplitTestEnabled(splitTestName)
+ : true
+
+ const onHide = useCallback(() => {
+ hideUntilReload()
+ }, [hideUntilReload])
+
+ if (!target || !isInSplitTestIfNeeded) {
+ return null
+ }
+
+ return (
+
+
+ {header && (
+
+ {header}
+
+
+ )}
+
+
+ {content}
+ {!header && }
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/ide-redesign/contexts/rail-context.tsx b/services/web/frontend/js/features/ide-redesign/contexts/rail-context.tsx
index c52671b42b..a98d3693c5 100644
--- a/services/web/frontend/js/features/ide-redesign/contexts/rail-context.tsx
+++ b/services/web/frontend/js/features/ide-redesign/contexts/rail-context.tsx
@@ -1,6 +1,6 @@
import { sendSearchEvent } from '@/features/event-tracking/search-events'
-import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
import useEventListener from '@/shared/hooks/use-event-listener'
+import usePersistedState from '@/shared/hooks/use-persisted-state'
import { isMac } from '@/shared/utils/os'
import {
createContext,
@@ -9,6 +9,7 @@ import {
SetStateAction,
useCallback,
useContext,
+ useLayoutEffect,
useMemo,
useRef,
useState,
@@ -44,7 +45,7 @@ const RailContext = createContext<
>(undefined)
export const RailProvider: FC = ({ children }) => {
- const [isOpen, setIsOpen] = useState(true)
+ const [isOpen, setIsOpen] = usePersistedState('rail-is-open', true)
const [resizing, setResizing] = useState(false)
const [activeModal, setActiveModalInternal] = useState(
null
@@ -55,23 +56,36 @@ export const RailProvider: FC = ({ children }) => {
}, [])
const panelRef = useRef(null)
- useCollapsiblePanel(isOpen, panelRef)
const togglePane = useCallback(() => {
setIsOpen(value => !value)
- }, [])
+ }, [setIsOpen])
const handlePaneExpand = useCallback(() => {
setIsOpen(true)
- }, [])
+ }, [setIsOpen])
const handlePaneCollapse = useCallback(() => {
setIsOpen(false)
- }, [])
+ }, [setIsOpen])
- // NOTE: The file tree **MUST** be the first tab to be opened
- // since it is responsible for opening the initial document.
- const [selectedTab, setSelectedTab] = useState('file-tree')
+ const [selectedTab, setSelectedTab] = usePersistedState(
+ 'selected-rail-tab',
+ 'file-tree'
+ )
+
+ // Keep the panel collapse/expanded state in sync with isOpen and selectedTab
+ useLayoutEffect(() => {
+ const panelHandle = panelRef.current
+
+ if (panelHandle) {
+ if (isOpen) {
+ panelHandle.expand()
+ } else {
+ panelHandle.collapse()
+ }
+ }
+ }, [isOpen, selectedTab])
const openTab = useCallback(
(tab: RailTabKey) => {
diff --git a/services/web/frontend/js/features/ide-redesign/hooks/use-toolbar-menu-editor-commands.tsx b/services/web/frontend/js/features/ide-redesign/hooks/use-toolbar-menu-editor-commands.tsx
index 250a12c776..ba96d049f8 100644
--- a/services/web/frontend/js/features/ide-redesign/hooks/use-toolbar-menu-editor-commands.tsx
+++ b/services/web/frontend/js/features/ide-redesign/hooks/use-toolbar-menu-editor-commands.tsx
@@ -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'
@@ -177,6 +177,7 @@ export const useToolbarMenuBarEditorCommands = () => {
handler: () => {
commands.addComment()
},
+ disabled: state.selection.main.empty,
},
/************************************
* Format menu
@@ -286,9 +287,10 @@ export const useToolbarMenuBarEditorCommands = () => {
newEditor,
trackedWrite,
isTeXFile,
+ state.selection.main.empty,
])
- const { toggleSymbolPalette } = useEditorContext()
+ const { toggleSymbolPalette } = useEditorPropertiesContext()
const symbolPaletteAvailable = getMeta('ol-symbolPaletteAvailable')
useCommandProvider(() => {
if (!newEditor || !editorIsVisible) {
diff --git a/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts b/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts
index ecd492cd5f..5b53ae8a6e 100644
--- a/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts
+++ b/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts
@@ -1,7 +1,9 @@
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { isInExperiment } from '@/utils/labs-utils'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
-export const canUseNewEditor = () => isInExperiment('editor-redesign')
+export const canUseNewEditor = () =>
+ isInExperiment('editor-redesign') || isSplitTestEnabled('editor-redesign')
export const useIsNewEditorEnabled = () => {
const { userSettings } = useUserSettingsContext()
diff --git a/services/web/frontend/js/features/navbar/index.ts b/services/web/frontend/js/features/navbar/index.ts
new file mode 100644
index 0000000000..d9d7c28bcf
--- /dev/null
+++ b/services/web/frontend/js/features/navbar/index.ts
@@ -0,0 +1,9 @@
+const toggleButton = document.getElementById('navbar-toggle-btn') as HTMLElement
+
+toggleButton?.addEventListener('click', () => {
+ // Delay allows Bootstrap to update aria-expanded first
+ setTimeout(() => {
+ const isExpanded = toggleButton.getAttribute('aria-expanded') === 'true'
+ document.body.classList.toggle('no-scroll', isExpanded)
+ }, 5)
+})
diff --git a/services/web/frontend/js/features/outline/components/outline-item.tsx b/services/web/frontend/js/features/outline/components/outline-item.tsx
index 08a6b0f942..05539bdf13 100644
--- a/services/web/frontend/js/features/outline/components/outline-item.tsx
+++ b/services/web/frontend/js/features/outline/components/outline-item.tsx
@@ -62,6 +62,7 @@ const OutlineItem = memo(function OutlineItem({
role="treeitem"
aria-current={isHighlighted}
aria-label={outlineItem.title}
+ translate="no"
>
{!!outlineItem.children && (
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx
index bf0b0378d2..4a6991f30b 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx
@@ -9,7 +9,7 @@ import MaterialIcon from '@/shared/components/material-icon'
function PdfHybridDownloadButton() {
const { pdfDownloadUrl } = useCompileContext()
- const { _id: projectId } = useProjectContext()
+ const { projectId } = useProjectContext()
const { t } = useTranslation()
const description = pdfDownloadUrl
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx
index 9d0cfca638..e0ed7fdb78 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx
@@ -23,7 +23,7 @@ type PdfJsViewerProps = {
}
function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
- const { _id: projectId } = useProjectContext()
+ const { projectId } = useProjectContext()
const { setError, firstRenderDone, highlights, position, setPosition } =
useCompileContext()
@@ -497,7 +497,12 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
onKeyDown={handleKeyDown}
tabIndex={-1}
>
-
+
{toolbarInfoLoaded && (
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.tsx
index 0e9cc5246d..7c3258f8c6 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.tsx
@@ -3,7 +3,6 @@ import { useResizeObserver } from '../../../shared/hooks/use-resize-observer'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import OLButton from '@/features/ui/components/ol/ol-button'
-import Icon from '../../../shared/components/icon'
export default function PdfLogEntryRawContent({
rawContent,
@@ -39,7 +38,7 @@ export default function PdfLogEntryRawContent({
height: expanded || !needsExpander ? 'auto' : collapsedSize,
}}
>
-
+
{rawContent.trim()}
@@ -53,17 +52,10 @@ export default function PdfLogEntryRawContent({
setExpanded(value => !value)}
>
- {expanded ? (
- <>
- {t('collapse')}
- >
- ) : (
- <>
- {t('expand')}
- >
- )}
+ {expanded ? t('collapse') : t('expand')}
)}
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.tsx
index f9fbcae42a..f9f91c84fa 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.tsx
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'
-import { memo, useState } from 'react'
+import { memo } from 'react'
import classnames from 'classnames'
import PdfValidationIssue from './pdf-validation-issue'
import StopOnFirstErrorPrompt from './stop-on-first-error-prompt'
@@ -14,7 +14,6 @@ import PdfCodeCheckFailedNotice from './pdf-code-check-failed-notice'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import PdfLogEntry from './pdf-log-entry'
import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
-import TimeoutUpgradePaywallPrompt from './timeout-upgrade-paywall-prompt'
import getMeta from '@/utils/meta'
function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
@@ -26,7 +25,6 @@ function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
validationIssues,
showLogs,
stoppedOnFirstError,
- isProjectOwner,
} = useCompileContext()
const { loadingError } = usePdfPreviewContext()
@@ -35,22 +33,12 @@ function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
const { t } = useTranslation()
- const [
- isShowingPrimaryCompileTimeoutPaywall,
- setIsShowingPrimaryCompileTimeoutPaywall,
- ] = useState(false)
- const isPaywallChangeCompileTimeoutEnabled = getMeta(
- 'ol-isPaywallChangeCompileTimeoutEnabled'
- )
-
- const isCompileTimeoutPaywallDisplay =
- isProjectOwner && isPaywallChangeCompileTimeoutEnabled
-
return (
{codeCheckFailed &&
}
@@ -60,13 +48,7 @@ function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
{loadingError &&
}
{compileTimeout < 60 && error === 'timedout' ? (
- isCompileTimeoutPaywallDisplay ? (
-
- ) : (
-
- )
+
) : (
<>{error &&
}>
)}
@@ -92,12 +74,10 @@ function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
/>
)}
- {!isShowingPrimaryCompileTimeoutPaywall && (
-
- )}
+
)
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx
index e063c20c76..82f2946d63 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx
@@ -40,7 +40,7 @@ function PdfPreviewPane() {
{compileTimeout < 60 && }
}>
-
+
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-provider.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-provider.tsx
index 0b625bc79b..46c20b24d6 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-provider.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-provider.tsx
@@ -12,7 +12,7 @@ export const usePdfPreviewContext = () => {
const context = useContext(PdfPreviewContext)
if (!context) {
throw new Error(
- 'usePdfPreviewContext is only avalable inside PdfPreviewProvider'
+ 'usePdfPreviewContext is only available inside PdfPreviewProvider'
)
}
return context
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview.tsx
index b0e692e201..e7757e99cd 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview.tsx
@@ -3,10 +3,16 @@ import { memo } from 'react'
import withErrorBoundary from '../../../infrastructure/error-boundary'
import PdfPreviewErrorBoundaryFallback from './pdf-preview-error-boundary-fallback'
import { useLayoutContext } from '../../../shared/context/layout-context'
+import { VisualPreview } from './visual-preview'
+import { useEditorViewContext } from '@/features/ide-react/context/editor-view-context'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
function PdfPreview() {
const { detachRole } = useLayoutContext()
+ const { view } = useEditorViewContext()
+ const visualPreviewEnabled = useFeatureFlag('visual-preview')
if (detachRole === 'detacher') return null
+ if (visualPreviewEnabled && view) return
return
}
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.tsx
index 0167a98db1..aee9a12549 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.tsx
@@ -10,6 +10,7 @@ import MaterialIcon from '@/shared/components/material-icon'
import { Spinner } from 'react-bootstrap'
import { Placement } from 'react-bootstrap/types'
import useSynctex from '../hooks/use-synctex'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
const GoToCodeButton = memo(function GoToCodeButton({
syncToCode,
@@ -135,6 +136,11 @@ function PdfSynctexControls() {
syncToPdfInFlight,
canSyncToPdf,
} = useSynctex()
+ const visualPreviewEnabled = useFeatureFlag('visual-preview')
+
+ if (visualPreviewEnabled) {
+ return null
+ }
if (!position) {
return null
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-zoom-dropdown.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-zoom-dropdown.tsx
index 75d0663c80..d0dc2cdbaa 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-zoom-dropdown.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-zoom-dropdown.tsx
@@ -172,7 +172,7 @@ function PdfZoomDropdown({
function Shortcut({ keys }: { keys: string[] }) {
return (
-
+
{keys.map((key, idx) => (
{
window.dispatchEvent(new Event('editor:focus'))
})
diff --git a/services/web/frontend/js/features/pdf-preview/components/timeout-message-after-paywall-dismissal.tsx b/services/web/frontend/js/features/pdf-preview/components/timeout-message-after-paywall-dismissal.tsx
deleted file mode 100644
index 64ef0fbfc1..0000000000
--- a/services/web/frontend/js/features/pdf-preview/components/timeout-message-after-paywall-dismissal.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import getMeta from '@/utils/meta'
-import { Trans, useTranslation } from 'react-i18next'
-import { memo, useMemo } from 'react'
-import { useDetachCompileContext } from '@/shared/context/detach-compile-context'
-import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
-import MaterialIcon from '@/shared/components/material-icon'
-import * as eventTracking from '@/infrastructure/event-tracking'
-import PdfLogEntry from './pdf-log-entry'
-
-type TimeoutMessageProps = {
- segmentation?: eventTracking.Segmentation
-}
-
-function TimeoutMessageAfterPaywallDismissal({
- segmentation,
-}: TimeoutMessageProps) {
- const { lastCompileOptions, isProjectOwner } = useDetachCompileContext()
- return (
-
-
- {getMeta('ol-ExposedSettings').enableSubscriptions && (
-
- )}
-
- )
-}
-
-type CompileTimeoutProps = {
- isProjectOwner: boolean
- segmentation?: eventTracking.Segmentation
-}
-
-const CompileTimeout = memo(function CompileTimeout({
- isProjectOwner,
- segmentation,
-}: CompileTimeoutProps) {
- const { t } = useTranslation()
-
- const eventSegmentation = useMemo(
- () => ({
- ...segmentation,
- 'paywall-version': 'secondary',
- }),
- [segmentation]
- )
-
- return (
-
- }
- formattedContent={
- getMeta('ol-ExposedSettings').enableSubscriptions && (
- <>
-
- {isProjectOwner ? (
-
-
{t('your_project_need_more_time_to_compile')}
-
{t('upgrade_to_unlock_more_time')}
-
- ) : (
-
-
{t('this_project_need_more_time_to_compile')}
-
{t('upgrade_to_unlock_more_time')}
-
- )}
-
-
- {isProjectOwner === false && (
- ,
- ]}
- />
- )}
-
- {isProjectOwner && (
-
-
- {t('try_for_free')}
-
-
- )}
- >
- )
- }
- // @ts-ignore
- entryAriaLabel={t('your_compile_timed_out')}
- level="error"
- />
- )
-})
-
-type PreventTimeoutHelpMessageProps = {
- lastCompileOptions: any
- segmentation?: eventTracking.Segmentation
-}
-
-const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
- lastCompileOptions,
- segmentation,
-}: PreventTimeoutHelpMessageProps) {
- const { t } = useTranslation()
-
- function sendInfoClickEvent() {
- eventTracking.sendMB('paywall-info-click', {
- 'paywall-type': 'compile-timeout',
- content: 'blog',
- ...segmentation,
- })
- }
-
- const compileTimeoutChangesBlogLink = (
- /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
-
- )
-
- return (
-
- {segmentation?.['10s-timeout-warning'] === 'enabled' && (
-
-
-
-
-
- )}
- {t('common_causes_of_compile_timeouts_include')}:
-
-
- {t('large_or_high_resolution_images_taking_too_long_to_process')}
-
-
- ,
- ]}
- />
- {!lastCompileOptions.stopOnFirstError && (
- <>
- {' '}
- ,
- // eslint-disable-next-line react/jsx-key
- ,
- ]}
- />{' '}
- >
- )}
-
-
-
- ,
- ]}
- />
-
- >
- }
- // @ts-ignore
- entryAriaLabel={t('reasons_for_compile_timeouts')}
- level="raw"
- />
- )
-})
-
-export default memo(TimeoutMessageAfterPaywallDismissal)
diff --git a/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-paywall-prompt.tsx b/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-paywall-prompt.tsx
deleted file mode 100644
index db01b0da7a..0000000000
--- a/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-paywall-prompt.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import {
- Dispatch,
- SetStateAction,
- useCallback,
- useEffect,
- useMemo,
- useState,
-} from 'react'
-import getMeta from '@/utils/meta'
-import * as eventTracking from '@/infrastructure/event-tracking'
-import TimeoutMessageAfterPaywallDismissal from './timeout-message-after-paywall-dismissal'
-import { UpgradePrompt } from '@/shared/components/upgrade-prompt'
-import { useDetachCompileContext } from '@/shared/context/detach-compile-context'
-
-const studentRoles = [
- 'High-school student',
- 'Undergraduate student',
- "Master's student (e.g. MSc, MA)",
- 'Doctoral student (e.g. PhD, MD, EngD)',
-]
-
-interface TimeoutUpgradePaywallPromptProps {
- setIsShowingPrimary?: Dispatch>
-}
-
-function TimeoutUpgradePaywallPrompt({
- setIsShowingPrimary,
-}: TimeoutUpgradePaywallPromptProps) {
- const odcRole = getMeta('ol-odcRole')
- const planPrices = getMeta('ol-paywallPlans')
- const isStudent = useMemo(() => studentRoles.includes(odcRole), [odcRole])
- const { isProjectOwner } = useDetachCompileContext()
-
- const [isPaywallDismissed, setIsPaywallDismissed] = useState(false)
- const { reducedTimeoutWarning, compileTimeout } =
- getMeta('ol-compileSettings')
-
- const sharedSegmentation = useMemo(
- () => ({
- '10s-timeout-warning': reducedTimeoutWarning,
- 'is-owner': isProjectOwner,
- compileTime: compileTimeout,
- }),
- [isProjectOwner, reducedTimeoutWarning, compileTimeout]
- )
-
- const sendPaywallEvent = useCallback(
- (event: string, segmentation?: eventTracking.Segmentation) => {
- eventTracking.sendMB(event, {
- 'paywall-type': 'compile-timeout',
- 'paywall-version': 'primary',
- ...sharedSegmentation,
- ...segmentation,
- })
- },
- [sharedSegmentation]
- )
-
- function onClose() {
- sendPaywallEvent('paywall-dismiss')
- setIsPaywallDismissed(true)
- if (setIsShowingPrimary) {
- setIsShowingPrimary(false)
- }
- }
-
- function onClickInfoLink() {
- sendPaywallEvent('paywall-info-click', { content: 'plans' })
- }
-
- function onClickPaywall() {
- sendPaywallEvent('paywall-click', {
- plan: isStudent ? 'student' : 'collaborator',
- })
- }
-
- useEffect(() => {
- sendPaywallEvent('paywall-prompt', {
- plan: isStudent ? 'student' : 'collaborator',
- })
- if (setIsShowingPrimary) {
- setIsShowingPrimary(true)
- }
- }, [isStudent, setIsShowingPrimary, sendPaywallEvent])
-
- return (
-
- {!isPaywallDismissed ? (
-
- ) : (
-
- )}
-
- )
-}
-
-export default TimeoutUpgradePaywallPrompt
diff --git a/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx b/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx
new file mode 100644
index 0000000000..074d26501a
--- /dev/null
+++ b/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx
@@ -0,0 +1,165 @@
+import { FC, useEffect, useRef, useState } from 'react'
+import {
+ CodeMirrorStateContext,
+ CodeMirrorViewContext,
+} from '@/features/source-editor/components/codemirror-context'
+import { EditorView } from '@codemirror/view'
+import { EditorState, StateEffect } from '@codemirror/state'
+import useIsMounted from '@/shared/hooks/use-is-mounted'
+import { docName } from '@/features/source-editor/extensions/doc-name'
+import {
+ language,
+ Metadata,
+ setMetadata,
+} from '@/features/source-editor/extensions/language'
+import { showContentWhenParsed } from '@/features/source-editor/extensions/visual/visual'
+import { usePhrases } from '@/features/source-editor/hooks/use-phrases'
+import {
+ setEditorTheme,
+ theme,
+} from '@/features/source-editor/extensions/theme'
+import { useFileTreeData } from '@/shared/context/file-tree-data-context'
+import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
+import {
+ visualHighlightStyle,
+ visualTheme,
+} from '@/features/source-editor/extensions/visual/visual-theme'
+import { tableGeneratorTheme } from '@/features/source-editor/extensions/visual/table-generator'
+import { atomicDecorations } from '@/features/source-editor/extensions/visual/atomic-decorations'
+import { markDecorations } from '@/features/source-editor/extensions/visual/mark-decorations'
+import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
+import { isValidTeXFile } from '@/main/is-valid-tex-file'
+import { mousedown } from '@/features/source-editor/extensions/visual/selection'
+
+export const VisualPreview: FC<{ view: EditorView }> = ({ view }) => {
+ const [previewState, setPreviewState] = useState()
+
+ const { fileTreeData } = useFileTreeData()
+ const { previewByPath } = useFileTreePathContext()
+ const { currentDocument, openDocName } = useEditorOpenDocContext()
+ const phrases = usePhrases()
+
+ const isMountedRef = useIsMounted()
+ const viewRef = useRef()
+ const containerRef = useRef(null)
+ const previewByPathRef = useRef(previewByPath)
+ const metadataRef = useRef({
+ labels: new Set(),
+ packageNames: new Set(),
+ referenceKeys: new Set(),
+ commands: [],
+ fileTreeData,
+ })
+
+ useEffect(() => {
+ if (!currentDocument) {
+ return
+ }
+
+ const state = EditorState.create({
+ doc: view.state.doc,
+ extensions: [
+ EditorView.lineWrapping,
+ EditorState.readOnly.of(true),
+ EditorView.editable.of(false),
+ EditorState.phrases.of(phrases),
+ docName('main.tex'),
+ language('main.tex', metadataRef.current, { syntaxValidation: false }),
+ theme({
+ fontSize: 14,
+ fontFamily: 'monaco',
+ lineHeight: 'normal',
+ activeOverallTheme: 'light',
+ }),
+ EditorView.theme({
+ '&.cm-editor': {
+ background: '#fff',
+ },
+ '.ol-cm-preamble-wrapper, .ol-cm-end-document-widget': {
+ visibility: 'hidden',
+ },
+ }),
+ visualTheme,
+ visualHighlightStyle,
+ tableGeneratorTheme,
+ mousedown,
+ atomicDecorations({
+ previewByPath: previewByPathRef.current,
+ }),
+ markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
+ showContentWhenParsed,
+ EditorView.contentAttributes.of({ 'aria-label': 'Visual preview' }),
+ ],
+ })
+
+ const preview = new EditorView({
+ state,
+ dispatchTransactions(trs) {
+ preview.update(trs)
+ if (isMountedRef.current) {
+ setPreviewState(preview.state)
+ }
+ },
+ scrollTo: EditorView.scrollIntoView(state.selection.main, {
+ y: 'center',
+ }),
+ })
+
+ setEditorTheme('overleaf').then(spec => {
+ preview.dispatch(spec)
+ })
+
+ containerRef.current?.replaceChildren(preview.dom)
+
+ viewRef.current = preview
+
+ view.dispatch({
+ effects: StateEffect.appendConfig.of([
+ EditorView.updateListener.of(update => {
+ if (update.docChanged) {
+ for (const tr of update.transactions) {
+ preview.dispatch({
+ changes: tr.changes,
+ })
+ }
+ }
+
+ if (update.selectionSet) {
+ preview.dispatch({
+ effects: EditorView.scrollIntoView(update.state.selection.main, {
+ y: 'center',
+ }),
+ })
+ }
+ }),
+ ]),
+ })
+ }, [phrases, view, currentDocument, isMountedRef])
+
+ useEffect(() => {
+ if (fileTreeData) {
+ metadataRef.current.fileTreeData = fileTreeData
+ window.setTimeout(() => {
+ viewRef.current?.dispatch(setMetadata(metadataRef.current))
+ })
+ }
+ }, [fileTreeData, view])
+
+ useEffect(() => {
+ return () => {
+ viewRef.current?.destroy()
+ }
+ }, [view])
+
+ if (!openDocName || !isValidTeXFile(openDocName)) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts b/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts
index d849786026..771a2d3b4e 100644
--- a/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts
+++ b/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts
@@ -1,14 +1,20 @@
-import { useEffect } from 'react'
+import { useCallback, useEffect } from 'react'
import { useLayoutContext } from '@/shared/context/layout-context'
+import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
+import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
+import { useEditorContext } from '@/shared/context/editor-context'
/**
* This hook adds an event listener for events dispatched from the editor to the compile logs pane
*/
export const useLogEvents = (setShowLogs: (show: boolean) => void) => {
const { pdfLayout, setView } = useLayoutContext()
+ const newEditor = useIsNewEditorEnabled()
+ const { openTab: openRailTab } = useRailContext()
+ const { hasPremiumSuggestion } = useEditorContext()
- useEffect(() => {
- const listener = (event: Event) => {
+ const handleViewCompileLogEntryEventOldEditor = useCallback(
+ (event: Event) => {
const { id, suggestFix } = (
event as CustomEvent<{ id: string; suggestFix?: boolean }>
).detail
@@ -52,6 +58,68 @@ export const useLogEvents = (setShowLogs: (show: boolean) => void) => {
}
}
})
+ },
+ [pdfLayout, setView, setShowLogs]
+ )
+
+ const handleViewCompileLogEntryEventNewEditor = useCallback(
+ (event: Event) => {
+ const { id, suggestFix } = (
+ event as CustomEvent<{ id: string; suggestFix?: boolean }>
+ ).detail
+
+ openRailTab('errors')
+
+ window.setTimeout(() => {
+ const logEntry = document.querySelector(
+ `.log-entry[data-log-entry-id="${id}"]`
+ )
+
+ if (logEntry) {
+ logEntry.scrollIntoView({
+ block: 'start',
+ inline: 'nearest',
+ })
+
+ const expandCollapseButton =
+ logEntry.querySelector(
+ 'button[data-action="expand-collapse"]'
+ )
+
+ const collapsed = expandCollapseButton?.dataset.collapsed === 'true'
+
+ if (collapsed) {
+ expandCollapseButton.click()
+ }
+
+ if (suggestFix) {
+ if (hasPremiumSuggestion) {
+ logEntry
+ .querySelector(
+ 'button[data-action="suggest-fix"]'
+ )
+ ?.click()
+ } else {
+ window.dispatchEvent(
+ new CustomEvent('aiAssist:showPaywall', {
+ detail: { origin: 'suggest-fix' },
+ })
+ )
+ }
+ }
+ }
+ })
+ },
+ [openRailTab, hasPremiumSuggestion]
+ )
+
+ useEffect(() => {
+ const listener = (event: Event) => {
+ if (newEditor) {
+ handleViewCompileLogEntryEventNewEditor(event)
+ } else {
+ handleViewCompileLogEntryEventOldEditor(event)
+ }
}
window.addEventListener('editor:view-compile-log-entry', listener)
@@ -59,5 +127,9 @@ export const useLogEvents = (setShowLogs: (show: boolean) => void) => {
return () => {
window.removeEventListener('editor:view-compile-log-entry', listener)
}
- }, [pdfLayout, setView, setShowLogs])
+ }, [
+ handleViewCompileLogEntryEventNewEditor,
+ handleViewCompileLogEntryEventOldEditor,
+ newEditor,
+ ])
}
diff --git a/services/web/frontend/js/features/pdf-preview/hooks/use-synctex.ts b/services/web/frontend/js/features/pdf-preview/hooks/use-synctex.ts
index 77986bfeac..51c8eefc7c 100644
--- a/services/web/frontend/js/features/pdf-preview/hooks/use-synctex.ts
+++ b/services/web/frontend/js/features/pdf-preview/hooks/use-synctex.ts
@@ -13,6 +13,7 @@ import * as eventTracking from '../../../infrastructure/event-tracking'
import { debugConsole } from '@/utils/debugging'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
+import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
import useEventListener from '@/shared/hooks/use-event-listener'
import { CursorPosition } from '@/features/ide-react/types/cursor-position'
import { isValidTeXFile } from '@/main/is-valid-tex-file'
@@ -27,15 +28,16 @@ export default function useSynctex(): {
syncToCodeInFlight: boolean
canSyncToPdf: boolean
} {
- const { _id: projectId, rootDocId } = useProjectContext()
+ const { projectId, project } = useProjectContext()
+ const rootDocId = project?.rootDocId
const { clsiServerId, pdfFile, position, setShowLogs, setHighlights } =
useCompileContext()
const { selectedEntities } = useFileTreeData()
const { findEntityByPath, dirname, pathInFolder } = useFileTreePathContext()
- const { getCurrentDocumentId, openDocWithId, openDocName } =
- useEditorManagerContext()
+ const { openDocName } = useEditorOpenDocContext()
+ const { getCurrentDocumentId, openDocWithId } = useEditorManagerContext()
const [cursorPosition, setCursorPosition] = useState(
() => {
diff --git a/services/web/frontend/js/features/pdf-preview/util/output-files.js b/services/web/frontend/js/features/pdf-preview/util/output-files.js
index 3ee0dc1180..0903ec10e3 100644
--- a/services/web/frontend/js/features/pdf-preview/util/output-files.js
+++ b/services/web/frontend/js/features/pdf-preview/util/output-files.js
@@ -143,6 +143,10 @@ export const handleLogFiles = async (outputFiles, data, signal) => {
return result
}
+/**
+ * @typedef {import('../../../../../types/annotation').Annotation} Annotation
+ * @returns {Record}
+ */
export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) {
const rootDocDirname = dirname(fileTreeData, rootDocId)
diff --git a/services/web/frontend/js/features/preview/components/preview-log-entry-header.tsx b/services/web/frontend/js/features/preview/components/preview-log-entry-header.tsx
index 8ce318b1c8..f5b1f49ce6 100644
--- a/services/web/frontend/js/features/preview/components/preview-log-entry-header.tsx
+++ b/services/web/frontend/js/features/preview/components/preview-log-entry-header.tsx
@@ -86,7 +86,11 @@ function PreviewLogEntryHeader({
onClick={onSourceLocationClick}
>
-
+