Quellcode durchsuchen

Add full project search UI (#22671)

GitOrigin-RevId: f40c85f40f4c16b4b3c26a197924cd9ac9b3db1f
Alf Eaton vor 1 Jahr
Ursprung
Commit
f069adaf15

+ 1 - 0
services/web/app/src/Features/Project/ProjectController.js

@@ -333,6 +333,7 @@ const _ProjectController = {
     const splitTests = [
     const splitTests = [
       !anonymous && 'bib-file-tpr-prompt',
       !anonymous && 'bib-file-tpr-prompt',
       'compile-log-events',
       'compile-log-events',
+      'full-project-search',
       'math-preview',
       'math-preview',
       'null-test-share-modal',
       'null-test-share-modal',
       'paywall-cta',
       'paywall-cta',

+ 2 - 0
services/web/config/settings.defaults.js

@@ -983,6 +983,8 @@ module.exports = {
     autoCompleteExtensions: [],
     autoCompleteExtensions: [],
     sectionTitleGenerators: [],
     sectionTitleGenerators: [],
     toastGenerators: [],
     toastGenerators: [],
+    editorSidebarComponents: [],
+    fileTreeToolbarComponents: [],
   },
   },
 
 
   moduleImportSequence: [
   moduleImportSequence: [

+ 13 - 4
services/web/frontend/js/features/file-tree/components/file-tree-toolbar.tsx

@@ -7,6 +7,12 @@ import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import MaterialIcon from '@/shared/components/material-icon'
 import MaterialIcon from '@/shared/components/material-icon'
 import OLButtonToolbar from '@/features/ui/components/ol/ol-button-toolbar'
 import OLButtonToolbar from '@/features/ui/components/ol/ol-button-toolbar'
+import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
+import React, { ElementType } from 'react'
+
+const fileTreeToolbarComponents = importOverleafModules(
+  'fileTreeToolbarComponents'
+) as { import: { default: ElementType }; path: string }[]
 
 
 function FileTreeToolbar() {
 function FileTreeToolbar() {
   const { fileTreeReadOnly } = useFileTreeData()
   const { fileTreeReadOnly } = useFileTreeData()
@@ -105,12 +111,14 @@ function FileTreeToolbarRight() {
   const { canRename, canDelete, startRenaming, startDeleting } =
   const { canRename, canDelete, startRenaming, startDeleting } =
     useFileTreeActionable()
     useFileTreeActionable()
 
 
-  if (!canRename && !canDelete) {
-    return null
-  }
-
   return (
   return (
     <div className="toolbar-right">
     <div className="toolbar-right">
+      {fileTreeToolbarComponents.map(
+        ({ import: { default: Component }, path }) => (
+          <Component key={path} />
+        )
+      )}
+
       {canRename ? (
       {canRename ? (
         <OLTooltip
         <OLTooltip
           id="rename"
           id="rename"
@@ -127,6 +135,7 @@ function FileTreeToolbarRight() {
           </button>
           </button>
         </OLTooltip>
         </OLTooltip>
       ) : null}
       ) : null}
+
       {canDelete ? (
       {canDelete ? (
         <OLTooltip
         <OLTooltip
           id="delete"
           id="delete"

+ 11 - 0
services/web/frontend/js/features/ide-react/components/editor-sidebar.tsx

@@ -5,6 +5,12 @@ import classNames from 'classnames'
 import { useLayoutContext } from '@/shared/context/layout-context'
 import { useLayoutContext } from '@/shared/context/layout-context'
 import { OutlineContainer } from '@/features/outline/components/outline-container'
 import { OutlineContainer } from '@/features/outline/components/outline-container'
 import { useOutlinePane } from '@/features/ide-react/hooks/use-outline-pane'
 import { useOutlinePane } from '@/features/ide-react/hooks/use-outline-pane'
+import React, { ElementType } from 'react'
+import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
+
+const editorSidebarComponents = importOverleafModules(
+  'editorSidebarComponents'
+) as { import: { default: ElementType }; path: string }[]
 
 
 export default function EditorSidebar() {
 export default function EditorSidebar() {
   const { view } = useLayoutContext()
   const { view } = useLayoutContext()
@@ -17,6 +23,11 @@ export default function EditorSidebar() {
         hidden: view === 'history',
         hidden: view === 'history',
       })}
       })}
     >
     >
+      {editorSidebarComponents.map(
+        ({ import: { default: Component }, path }) => (
+          <Component key={path} />
+        )
+      )}
       <PanelGroup autoSaveId="ide-editor-sidebar-layout" direction="vertical">
       <PanelGroup autoSaveId="ide-editor-sidebar-layout" direction="vertical">
         <Panel
         <Panel
           defaultSize={50}
           defaultSize={50}

+ 28 - 0
services/web/frontend/js/shared/context/layout-context.tsx

@@ -7,6 +7,7 @@ import {
   Dispatch,
   Dispatch,
   SetStateAction,
   SetStateAction,
   FC,
   FC,
+  useState,
 } from 'react'
 } from 'react'
 import useScopeValue from '../hooks/use-scope-value'
 import useScopeValue from '../hooks/use-scope-value'
 import useDetachLayout from '../hooks/use-detach-layout'
 import useDetachLayout from '../hooks/use-detach-layout'
@@ -17,6 +18,7 @@ import { debugConsole } from '@/utils/debugging'
 import { BinaryFile } from '@/features/file-view/types/binary-file'
 import { BinaryFile } from '@/features/file-view/types/binary-file'
 import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
 import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
 import useEventListener from '@/shared/hooks/use-event-listener'
 import useEventListener from '@/shared/hooks/use-event-listener'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 
 
 export type IdeLayout = 'sideBySide' | 'flat'
 export type IdeLayout = 'sideBySide' | 'flat'
 export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
 export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
@@ -49,8 +51,12 @@ type LayoutContextValue = {
   >
   >
   pdfLayout: IdeLayout
   pdfLayout: IdeLayout
   pdfPreviewOpen: boolean
   pdfPreviewOpen: boolean
+  projectSearchIsOpen: boolean
+  setProjectSearchIsOpen: Dispatch<SetStateAction<boolean>>
 }
 }
 
 
+const isMac = /Mac/.test(window.navigator?.platform)
+
 const debugPdfDetach = getMeta('ol-debugPdfDetach')
 const debugPdfDetach = getMeta('ol-debugPdfDetach')
 
 
 export const LayoutContext = createContext<LayoutContextValue | undefined>(
 export const LayoutContext = createContext<LayoutContextValue | undefined>(
@@ -107,6 +113,9 @@ export const LayoutProvider: FC = ({ children }) => {
   const [leftMenuShown, setLeftMenuShown] =
   const [leftMenuShown, setLeftMenuShown] =
     useScopeValue<boolean>('ui.leftMenuShown')
     useScopeValue<boolean>('ui.leftMenuShown')
 
 
+  // whether the project search is open
+  const [projectSearchIsOpen, setProjectSearchIsOpen] = useState(false)
+
   useEventListener(
   useEventListener(
     'ui.toggle-left-menu',
     'ui.toggle-left-menu',
     useCallback(
     useCallback(
@@ -124,6 +133,21 @@ export const LayoutProvider: FC = ({ children }) => {
     }, [setReviewPanelOpen])
     }, [setReviewPanelOpen])
   )
   )
 
 
+  useEventListener(
+    'keydown',
+    useCallback((event: KeyboardEvent) => {
+      if (
+        (isMac ? event.metaKey : event.ctrlKey) &&
+        event.shiftKey &&
+        event.key === 'f'
+      ) {
+        if (isSplitTestEnabled('full-project-search')) {
+          setProjectSearchIsOpen(true)
+        }
+      }
+    }, [])
+  )
+
   // whether to display the editor and preview side-by-side or full-width ("flat")
   // whether to display the editor and preview side-by-side or full-width ("flat")
   const [pdfLayout, setPdfLayout] = useScopeValue<IdeLayout>('ui.pdfLayout')
   const [pdfLayout, setPdfLayout] = useScopeValue<IdeLayout>('ui.pdfLayout')
 
 
@@ -194,6 +218,8 @@ export const LayoutProvider: FC = ({ children }) => {
       leftMenuShown,
       leftMenuShown,
       pdfLayout,
       pdfLayout,
       pdfPreviewOpen,
       pdfPreviewOpen,
+      projectSearchIsOpen,
+      setProjectSearchIsOpen,
       reviewPanelOpen,
       reviewPanelOpen,
       miniReviewPanelVisible,
       miniReviewPanelVisible,
       loadingStyleSheet,
       loadingStyleSheet,
@@ -216,6 +242,8 @@ export const LayoutProvider: FC = ({ children }) => {
       leftMenuShown,
       leftMenuShown,
       pdfLayout,
       pdfLayout,
       pdfPreviewOpen,
       pdfPreviewOpen,
+      projectSearchIsOpen,
+      setProjectSearchIsOpen,
       reviewPanelOpen,
       reviewPanelOpen,
       miniReviewPanelVisible,
       miniReviewPanelVisible,
       loadingStyleSheet,
       loadingStyleSheet,

+ 6 - 1
services/web/frontend/js/shared/context/project-context.tsx

@@ -1,7 +1,8 @@
-import { FC, createContext, useContext, useMemo } from 'react'
+import { FC, createContext, useContext, useMemo, useState } from 'react'
 import useScopeValue from '../hooks/use-scope-value'
 import useScopeValue from '../hooks/use-scope-value'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
 import { ProjectContextValue } from './types/project-context'
 import { ProjectContextValue } from './types/project-context'
+import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
 
 
 const ProjectContext = createContext<ProjectContextValue | undefined>(undefined)
 const ProjectContext = createContext<ProjectContextValue | undefined>(undefined)
 
 
@@ -43,6 +44,8 @@ export const ProjectProvider: FC = ({ children }) => {
     mainBibliographyDoc_id: mainBibliographyDocId,
     mainBibliographyDoc_id: mainBibliographyDocId,
   } = project || projectFallback
   } = project || projectFallback
 
 
+  const [projectSnapshot] = useState(() => new ProjectSnapshot(_id))
+
   const tags = useMemo(
   const tags = useMemo(
     () =>
     () =>
       (getMeta('ol-projectTags') || [])
       (getMeta('ol-projectTags') || [])
@@ -65,6 +68,7 @@ export const ProjectProvider: FC = ({ children }) => {
       tags,
       tags,
       trackChangesState,
       trackChangesState,
       mainBibliographyDocId,
       mainBibliographyDocId,
+      projectSnapshot,
     }
     }
   }, [
   }, [
     _id,
     _id,
@@ -79,6 +83,7 @@ export const ProjectProvider: FC = ({ children }) => {
     tags,
     tags,
     trackChangesState,
     trackChangesState,
     mainBibliographyDocId,
     mainBibliographyDocId,
+    projectSnapshot,
   ])
   ])
 
 
   return (
   return (

+ 2 - 0
services/web/frontend/js/shared/context/types/project-context.tsx

@@ -1,5 +1,6 @@
 import { UserId } from '../../../../../types/user'
 import { UserId } from '../../../../../types/user'
 import { PublicAccessLevel } from '../../../../../types/public-access-level'
 import { PublicAccessLevel } from '../../../../../types/public-access-level'
+import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
 
 
 export type ProjectContextMember = {
 export type ProjectContextMember = {
   _id: UserId
   _id: UserId
@@ -46,6 +47,7 @@ export type ProjectContextValue = {
     color?: string
     color?: string
   }[]
   }[]
   trackChangesState: boolean | Record<UserId | '__guests__', boolean>
   trackChangesState: boolean | Record<UserId | '__guests__', boolean>
+  projectSnapshot: ProjectSnapshot
 }
 }
 
 
 export type ProjectContextUpdateValue = Partial<ProjectContextValue>
 export type ProjectContextUpdateValue = Partial<ProjectContextValue>

+ 5 - 1
services/web/frontend/stylesheets/app/editor/ide-react.less

@@ -51,12 +51,12 @@
 
 
   // Enable ::before and ::after pseudo-elements to position themselves correctly
   // Enable ::before and ::after pseudo-elements to position themselves correctly
   position: relative;
   position: relative;
-
   background-color: @editor-resizer-bg-color;
   background-color: @editor-resizer-bg-color;
 
 
   .custom-toggler {
   .custom-toggler {
     padding: 0;
     padding: 0;
     border-width: 0;
     border-width: 0;
+
     // Override react-resizable-panels which sets a global * { cursor: ew-resize }
     // Override react-resizable-panels which sets a global * { cursor: ew-resize }
     cursor: pointer !important;
     cursor: pointer !important;
   }
   }
@@ -74,9 +74,11 @@
       width: 7px;
       width: 7px;
       height: 18px;
       height: 18px;
     }
     }
+
     &::before {
     &::before {
       top: 25%;
       top: 25%;
     }
     }
+
     &::after {
     &::after {
       top: 75%;
       top: 75%;
     }
     }
@@ -89,6 +91,7 @@
   .synctex-controls {
   .synctex-controls {
     left: -8px;
     left: -8px;
     margin: 0;
     margin: 0;
+
     // Ensure that SyncTex controls appear in front of PDF viewer controls and logs pane
     // Ensure that SyncTex controls appear in front of PDF viewer controls and logs pane
     z-index: 12;
     z-index: 12;
 
 
@@ -128,6 +131,7 @@
   height: 100%;
   height: 100%;
   background-color: @file-tree-bg;
   background-color: @file-tree-bg;
   color: var(--neutral-20);
   color: var(--neutral-20);
+  position: relative;
 }
 }
 
 
 .ide-react-symbol-palette {
 .ide-react-symbol-palette {

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss

@@ -60,6 +60,7 @@ $editor-toggler-bg-dark-color: color.adjust(
   background-color: var(--file-tree-bg);
   background-color: var(--file-tree-bg);
   height: 100%;
   height: 100%;
   color: var(--content-secondary-dark);
   color: var(--content-secondary-dark);
+  position: relative;
 }
 }
 
 
 .ide-react-body {
 .ide-react-body {