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

Add integration panel to new editor

GitOrigin-RevId: 85e038c645e40d0ea596ed35d31448caa232e298
This commit is contained in:
David
2025-02-18 10:09:02 +00:00
committed by Copybot
parent 76301e0cc8
commit f83dec3c0e
17 changed files with 304 additions and 109 deletions

View File

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

View File

@@ -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": "",

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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)) {

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

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

View File

@@ -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;
}

View File

@@ -392,6 +392,7 @@
"confirming": "Confirming",
"conflicting_paths_found": "Conflicting Paths Found",
"congratulations_youve_successfully_join_group": "Congratulations! Youve 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 dont 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": "Youre using <b>Overleaf Premium</b>",