Przeglądaj źródła

Merge pull request #23597 from overleaf/dp-integrations-panel

Add integration panel to new editor

GitOrigin-RevId: 85e038c645e40d0ea596ed35d31448caa232e298
David 1 rok temu
rodzic
commit
46e9c69d8e

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

@@ -983,6 +983,7 @@ module.exports = {
     toastGenerators: [],
     editorSidebarComponents: [],
     fileTreeToolbarComponents: [],
+    integrationPanelComponents: [],
   },
 
   moduleImportSequence: [

+ 6 - 0
services/web/frontend/extracted-translations.json

@@ -301,6 +301,7 @@
   "confirming": "",
   "conflicting_paths_found": "",
   "congratulations_youve_successfully_join_group": "",
+  "connect_overleaf_with_github": "",
   "connected_users": "",
   "connection_lost": "",
   "contact_group_admin": "",
@@ -423,6 +424,7 @@
   "draft_sso_configuration": "",
   "drag_here": "",
   "drag_here_paste_an_image_or": "",
+  "dropbox": "",
   "dropbox_checking_sync_status": "",
   "dropbox_duplicate_project_names": "",
   "dropbox_duplicate_project_names_suggestion": "",
@@ -615,6 +617,7 @@
   "git_bridge_modal_use_previous_token": "",
   "git_integration": "",
   "git_integration_info": "",
+  "github": "",
   "github_commit_message_placeholder": "",
   "github_credentials_expired": "",
   "github_empty_repository_error": "",
@@ -791,6 +794,7 @@
   "institution_has_overleaf_subscription": "",
   "institution_templates": "",
   "institutional_leavers_survey_notification": "",
+  "integrate_overleaf_with_dropbox": "",
   "integrations": "",
   "integrations_like_github": "",
   "interested_in_cheaper_personal_plan": "",
@@ -888,6 +892,7 @@
   "link_accounts": "",
   "link_accounts_and_add_email": "",
   "link_institutional_email_get_started": "",
+  "link_overleaf_with_git": "",
   "link_sharing": "",
   "link_sharing_is_off_short": "",
   "link_sharing_is_on": "",
@@ -1191,6 +1196,7 @@
   "plus_x_additional_licenses_for_a_total_of_y_users": "",
   "postal_code": "",
   "postal_code_sentence_case": "",
+  "premium": "",
   "premium_feature": "",
   "premium_features": "",
   "premium_plan_label": "",

+ 4 - 4
services/web/frontend/js/features/ide-react/context/react-context-root.tsx

@@ -17,7 +17,7 @@ import { OnlineUsersProvider } from '@/features/ide-react/context/online-users-c
 import { OutlineProvider } from '@/features/ide-react/context/outline-context'
 import { PermissionsProvider } from '@/features/ide-react/context/permissions-context'
 import { ProjectProvider } from '@/shared/context/project-context'
-import { RailTabProvider } from '@/features/ide-redesign/contexts/rail-tab-context'
+import { RailProvider } from '@/features/ide-redesign/contexts/rail-context'
 import { ProjectSettingsProvider } from '@/features/editor-left-menu/context/project-settings-context'
 import { ReferencesProvider } from '@/features/ide-react/context/references-context'
 import { SnapshotProvider } from '@/features/ide-react/context/snapshot-context'
@@ -49,7 +49,7 @@ export const ReactContextRoot: FC<{ providers?: Record<string, FC> }> = ({
     PermissionsProvider,
     ProjectProvider,
     ProjectSettingsProvider,
-    RailTabProvider,
+    RailProvider,
     ReferencesProvider,
     SnapshotProvider,
     SplitTestProvider,
@@ -83,9 +83,9 @@ export const ReactContextRoot: FC<{ providers?: Record<string, FC> }> = ({
                                               <Providers.OnlineUsersProvider>
                                                 <Providers.MetadataProvider>
                                                   <Providers.OutlineProvider>
-                                                    <Providers.RailTabProvider>
+                                                    <Providers.RailProvider>
                                                       {children}
-                                                    </Providers.RailTabProvider>
+                                                    </Providers.RailProvider>
                                                   </Providers.OutlineProvider>
                                                 </Providers.MetadataProvider>
                                               </Providers.OnlineUsersProvider>

+ 42 - 0
services/web/frontend/js/features/ide-redesign/components/integrations-panel/integration-card.tsx

@@ -0,0 +1,42 @@
+import OLBadge from '@/features/ui/components/ol/ol-badge'
+import MaterialIcon from '@/shared/components/material-icon'
+import { useTranslation } from 'react-i18next'
+
+export default function IntegrationCard({
+  onClick,
+  title,
+  description,
+  icon,
+  showPaywallBadge,
+}: {
+  onClick: () => void
+  title: string
+  description: string
+  icon: React.ReactNode
+  showPaywallBadge: boolean
+}) {
+  const { t } = useTranslation()
+
+  return (
+    <button onClick={onClick} className="integrations-panel-card-button">
+      <div className="integrations-panel-card-contents">
+        {icon}
+        <div className="integrations-panel-card-inner">
+          <header className="integrations-panel-card-header">
+            <div className="integrations-panel-card-title">{title}</div>
+            {showPaywallBadge && (
+              <OLBadge
+                prepend={<MaterialIcon type="star" />}
+                bg="light"
+                className="integrations-panel-card-premium-badge"
+              >
+                {t('premium')}
+              </OLBadge>
+            )}
+          </header>
+          <p className="integrations-panel-card-description">{description}</p>
+        </div>
+      </div>
+    </button>
+  )
+}

+ 29 - 0
services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx

@@ -0,0 +1,29 @@
+import { ElementType } from 'react'
+import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
+import MaterialIcon from '@/shared/components/material-icon'
+import OlButton from '@/features/ui/components/ol/ol-button'
+import { useRailContext } from '../../contexts/rail-context'
+
+const integrationPanelComponents = importOverleafModules(
+  'integrationPanelComponents'
+) as { import: { default: ElementType }; path: string }[]
+
+export default function IntegrationsPanel() {
+  const { handlePaneCollapse } = useRailContext()
+
+  return (
+    <div className="integrations-panel">
+      <header className="integrations-panel-header">
+        <h4 className="integrations-panel-title">Integrations</h4>
+        <OlButton onClick={handlePaneCollapse} variant="ghost" size="sm">
+          <MaterialIcon type="close" />
+        </OlButton>
+      </header>
+      {integrationPanelComponents.map(
+        ({ import: { default: Component }, path }) => (
+          <Component key={path} />
+        )
+      )}
+    </div>
+  )
+}

+ 2 - 2
services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx

@@ -1,7 +1,7 @@
 import OLButton from '@/features/ui/components/ol/ol-button'
 import MaterialIcon from '@/shared/components/material-icon'
 import { useTranslation } from 'react-i18next'
-import { useRailTabContext } from '../../contexts/rail-tab-context'
+import { useRailContext } from '../../contexts/rail-context'
 import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
 import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
@@ -11,7 +11,7 @@ function PdfErrorState() {
   // TODO ide-redesign-cleanup: rename showLogs to something else and check usages
   const { showLogs } = useCompileContext()
   const { t } = useTranslation()
-  const { setSelectedTab: setSelectedRailTab } = useRailTabContext()
+  const { setSelectedTab: setSelectedRailTab } = useRailContext()
   const newEditor = useFeatureFlag('editor-redesign')
 
   if (!newEditor || (!loadingError && !showLogs)) {

+ 6 - 5
services/web/frontend/js/features/ide-redesign/components/rail.tsx

@@ -6,15 +6,15 @@ import MaterialIcon, {
 import { Panel } from 'react-resizable-panels'
 import { useLayoutContext } from '@/shared/context/layout-context'
 import { ErrorIndicator, ErrorPane } from './errors'
-import { RailTabKey, useRailTabContext } from '../contexts/rail-tab-context'
+import { RailTabKey, useRailContext } from '../contexts/rail-context'
 import FileTreeOutlinePanel from './file-tree-outline-panel'
 import { ChatIndicator, ChatPane } from './chat'
 import getMeta from '@/utils/meta'
 import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
 import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
-import { useRail } from '../hooks/use-rail'
 import { useTranslation } from 'react-i18next'
 import classNames from 'classnames'
+import IntegrationsPanel from './integrations-panel/integrations-panel'
 
 type RailElement = {
   icon: AvailableUnfilledIcon
@@ -41,7 +41,7 @@ const RAIL_TABS: RailElement[] = [
   {
     key: 'integrations',
     icon: 'integration_instructions',
-    component: <>Integrations</>,
+    component: <IntegrationsPanel />,
   },
   {
     key: 'review-panel',
@@ -66,14 +66,15 @@ const RAIL_TABS: RailElement[] = [
 export const RailLayout = () => {
   const { t } = useTranslation()
   const {
+    selectedTab,
+    setSelectedTab,
     isOpen,
     setIsOpen,
     panelRef,
     handlePaneCollapse,
     handlePaneExpand,
     togglePane,
-  } = useRail()
-  const { selectedTab, setSelectedTab } = useRailTabContext()
+  } = useRailContext()
 
   const { setLeftMenuShown } = useLayoutContext()
   const railActions: RailAction[] = useMemo(

+ 96 - 0
services/web/frontend/js/features/ide-redesign/contexts/rail-context.tsx

@@ -0,0 +1,96 @@
+import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
+import {
+  createContext,
+  Dispatch,
+  FC,
+  SetStateAction,
+  useCallback,
+  useContext,
+  useMemo,
+  useRef,
+  useState,
+} from 'react'
+import { ImperativePanelHandle } from 'react-resizable-panels'
+
+export type RailTabKey =
+  | 'file-tree'
+  | 'integrations'
+  | 'review-panel'
+  | 'chat'
+  | 'errors'
+
+const RailContext = createContext<
+  | {
+      selectedTab: RailTabKey
+      setSelectedTab: Dispatch<SetStateAction<RailTabKey>>
+      isOpen: boolean
+      setIsOpen: Dispatch<SetStateAction<boolean>>
+      panelRef: React.RefObject<ImperativePanelHandle>
+      togglePane: () => void
+      handlePaneExpand: () => void
+      handlePaneCollapse: () => void
+      resizing: boolean
+      setResizing: Dispatch<SetStateAction<boolean>>
+    }
+  | undefined
+>(undefined)
+
+export const RailProvider: FC = ({ children }) => {
+  const [isOpen, setIsOpen] = useState(true)
+  const [resizing, setResizing] = useState(false)
+  const panelRef = useRef<ImperativePanelHandle>(null)
+  useCollapsiblePanel(isOpen, panelRef)
+
+  const togglePane = useCallback(() => {
+    setIsOpen(value => !value)
+  }, [])
+
+  const handlePaneExpand = useCallback(() => {
+    setIsOpen(true)
+  }, [])
+
+  const handlePaneCollapse = useCallback(() => {
+    setIsOpen(false)
+  }, [])
+
+  // 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<RailTabKey>('file-tree')
+
+  const value = useMemo(
+    () => ({
+      selectedTab,
+      setSelectedTab,
+      isOpen,
+      setIsOpen,
+      panelRef,
+      togglePane,
+      handlePaneExpand,
+      handlePaneCollapse,
+      resizing,
+      setResizing,
+    }),
+    [
+      selectedTab,
+      setSelectedTab,
+      isOpen,
+      setIsOpen,
+      panelRef,
+      togglePane,
+      handlePaneExpand,
+      handlePaneCollapse,
+      resizing,
+      setResizing,
+    ]
+  )
+
+  return <RailContext.Provider value={value}>{children}</RailContext.Provider>
+}
+
+export const useRailContext = () => {
+  const context = useContext(RailContext)
+  if (!context) {
+    throw new Error('useRailContext is only available inside RailProvider')
+  }
+  return context
+}

+ 0 - 52
services/web/frontend/js/features/ide-redesign/contexts/rail-tab-context.tsx

@@ -1,52 +0,0 @@
-import {
-  createContext,
-  Dispatch,
-  FC,
-  SetStateAction,
-  useContext,
-  useMemo,
-  useState,
-} from 'react'
-
-export type RailTabKey =
-  | 'file-tree'
-  | 'integrations'
-  | 'review-panel'
-  | 'chat'
-  | 'errors'
-
-const RailTabContext = createContext<
-  | {
-      selectedTab: RailTabKey
-      setSelectedTab: Dispatch<SetStateAction<RailTabKey>>
-    }
-  | undefined
->(undefined)
-
-export const RailTabProvider: FC = ({ children }) => {
-  // 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<RailTabKey>('file-tree')
-
-  const value = useMemo(
-    () => ({
-      selectedTab,
-      setSelectedTab,
-    }),
-    [selectedTab, setSelectedTab]
-  )
-
-  return (
-    <RailTabContext.Provider value={value}>{children}</RailTabContext.Provider>
-  )
-}
-
-export const useRailTabContext = () => {
-  const context = useContext(RailTabContext)
-  if (!context) {
-    throw new Error(
-      'useRailTabContext is only available inside RailTabProvider'
-    )
-  }
-  return context
-}

+ 32 - 4
services/web/frontend/js/features/ide-redesign/hooks/use-pdf-pane.tsx

@@ -1,7 +1,35 @@
-import { useRail } from './use-rail'
+import { useCallback, useRef, useState } from 'react'
+import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
+import { ImperativePanelHandle } from 'react-resizable-panels'
 
+// FIXME: This is temporary, to avoid clashing with the existing usePdfPane
+// which uses the layout context. That's the correct approach.
 export const usePdfPane = () => {
-  // FIXME: This is temporary, to avoid clashing with the existing usePdfPane
-  // which uses the layout context. That's the correct approach.
-  return useRail()
+  const [isOpen, setIsOpen] = useState(true)
+  const [resizing, setResizing] = useState(false)
+  const panelRef = useRef<ImperativePanelHandle>(null)
+  useCollapsiblePanel(isOpen, panelRef)
+
+  const togglePane = useCallback(() => {
+    setIsOpen(value => !value)
+  }, [])
+
+  const handlePaneExpand = useCallback(() => {
+    setIsOpen(true)
+  }, [])
+
+  const handlePaneCollapse = useCallback(() => {
+    setIsOpen(false)
+  }, [])
+
+  return {
+    isOpen,
+    setIsOpen,
+    panelRef,
+    togglePane,
+    handlePaneExpand,
+    handlePaneCollapse,
+    resizing,
+    setResizing,
+  }
 }

+ 0 - 33
services/web/frontend/js/features/ide-redesign/hooks/use-rail.tsx

@@ -1,33 +0,0 @@
-import { useCallback, useRef, useState } from 'react'
-import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
-import { ImperativePanelHandle } from 'react-resizable-panels'
-
-export const useRail = () => {
-  const [isOpen, setIsOpen] = useState(true)
-  const [resizing, setResizing] = useState(false)
-  const panelRef = useRef<ImperativePanelHandle>(null)
-  useCollapsiblePanel(isOpen, panelRef)
-
-  const togglePane = useCallback(() => {
-    setIsOpen(value => !value)
-  }, [])
-
-  const handlePaneExpand = useCallback(() => {
-    setIsOpen(true)
-  }, [])
-
-  const handlePaneCollapse = useCallback(() => {
-    setIsOpen(false)
-  }, [])
-
-  return {
-    isOpen,
-    setIsOpen,
-    panelRef,
-    togglePane,
-    handlePaneExpand,
-    handlePaneCollapse,
-    resizing,
-    setResizing,
-  }
-}

+ 3 - 3
services/web/frontend/js/shared/svgs/dropbox-logo.jsx → services/web/frontend/js/shared/svgs/dropbox-logo.tsx

@@ -1,8 +1,8 @@
-function DropboxLogo() {
+function DropboxLogo({ size = 40 }: { size?: number }) {
   return (
     <svg
-      width="40"
-      height="40"
+      width={size}
+      height={size}
       viewBox="0 0 40 40"
       fill="none"
       xmlns="http://www.w3.org/2000/svg"

+ 3 - 3
services/web/frontend/js/shared/svgs/git-bridge-logo.jsx → services/web/frontend/js/shared/svgs/git-bridge-logo.tsx

@@ -1,8 +1,8 @@
-function GitBridgeLogo() {
+function GitBridgeLogo({ size = 40 }: { size?: number }) {
   return (
     <svg
-      width="40"
-      height="40"
+      width={size}
+      height={size}
       viewBox="0 0 40 40"
       fill="none"
       xmlns="http://www.w3.org/2000/svg"

+ 3 - 3
services/web/frontend/js/shared/svgs/github-logo.jsx → services/web/frontend/js/shared/svgs/github-logo.tsx

@@ -1,8 +1,8 @@
-function GithubLogo() {
+function GithubLogo({ size = 40 }: { size?: number }) {
   return (
     <svg
-      width="40"
-      height="40"
+      width={size}
+      height={size}
       viewBox="0 0 40 40"
       fill="none"
       xmlns="http://www.w3.org/2000/svg"

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/components/all.scss

@@ -42,3 +42,4 @@
 @import 'menu-bar';
 @import 'invite';
 @import 'upgrade-prompt';
+@import 'integrations-panel';

+ 72 - 0
services/web/frontend/stylesheets/bootstrap-5/components/integrations-panel.scss

@@ -0,0 +1,72 @@
+.integrations-panel {
+  background-color: var(--white);
+  height: 100%;
+}
+
+.integrations-panel-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: var(--spacing-03) var(--spacing-04);
+}
+
+.integrations-panel-title {
+  font-size: var(--font-size-02);
+  color: var(--content-primary);
+  margin-bottom: 0;
+}
+
+.integrations-panel-card-button {
+  all: unset;
+  background-color: var(--white);
+  width: 100%;
+
+  &:hover {
+    background-color: var(--bg-light-secondary) !important;
+  }
+}
+
+.integrations-panel-card-contents {
+  display: flex;
+  margin: 0 var(--spacing-04);
+  padding: var(--spacing-04) 0;
+  gap: var(--spacing-04);
+  border-bottom: 1px solid var(--border-divider);
+}
+
+.integrations-panel-card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  .material-symbols {
+    background: linear-gradient(
+      245.63deg,
+      #214475 0%,
+      #254c84 28.54%,
+      #6597e0 96.69%
+    );
+    color: transparent;
+    background-clip: text;
+  }
+}
+
+.integrations-panel-card-inner {
+  flex: 1;
+}
+
+.integrations-panel-card-premium-badge {
+  color: var(--content-primary);
+  font-weight: 600;
+}
+
+.integrations-panel-card-title {
+  font-size: var(--font-size-02);
+  color: var(--content-primary);
+}
+
+.integrations-panel-card-description {
+  font-size: var(--font-size-01);
+  color: var(--content-secondary);
+  margin-bottom: 0;
+}

+ 4 - 0
services/web/locales/en.json

@@ -392,6 +392,7 @@
   "confirming": "Confirming",
   "conflicting_paths_found": "Conflicting Paths Found",
   "congratulations_youve_successfully_join_group": "Congratulations! You‘ve successfully joined the group subscription.",
+  "connect_overleaf_with_github": "Connect __appName__ with Github for easy project syncing and real-time version control.",
   "connected_users": "Connected Users",
   "connecting": "Connecting",
   "connection_lost": "Connection lost",
@@ -1029,6 +1030,7 @@
   "institutional": "Institutional",
   "institutional_leavers_survey_notification": "Provide some quick feedback to receive a 25% discount on an annual subscription!",
   "institutional_login_unknown": "Sorry, we don’t know which institution issued that email address. You can browse our <a href=\"__link__\">list of institutions</a> to find yours, or you can use one of the other options below.",
+  "integrate_overleaf_with_dropbox": "Integrate __appName__ with Dropbox to sync your LaTeX projects and keep files up to date automatically.",
   "integrations": "Integrations",
   "integrations_like_github": "Integrations like GitHub Sync",
   "interested_in_cheaper_personal_plan": "Would you be interested in the cheaper <0>__price__</0> Personal plan?",
@@ -1168,6 +1170,7 @@
   "link_accounts": "Link Accounts",
   "link_accounts_and_add_email": "Link Accounts and Add Email",
   "link_institutional_email_get_started": "Link an institutional email address to your account to get started.",
+  "link_overleaf_with_git": "Link __appName__ with Git for seamless project syncing and version control across your repositories.",
   "link_sharing": "Link sharing",
   "link_sharing_is_off_short": "Link sharing is off",
   "link_sharing_is_on": "Link sharing is on",
@@ -1599,6 +1602,7 @@
   "position": "Position",
   "postal_code": "Postal Code",
   "postal_code_sentence_case": "Postal code",
+  "premium": "Premium",
   "premium_feature": "Premium feature",
   "premium_features": "Premium features",
   "premium_plan_label": "You’re using <b>Overleaf Premium</b>",