Merge pull request #24391 from overleaf/jdt-move-wf-features-to-ol

Move Writefull table + equation generator to OL toolbar

GitOrigin-RevId: b7bc5b1cde5687360c4f60cb64924da139ccfbe9
This commit is contained in:
Jimmy Domagala-Tang
2025-04-14 09:15:58 -04:00
committed by Copybot
parent 958ff0f3bf
commit 0e49a5d9b0
13 changed files with 329 additions and 76 deletions
@@ -355,6 +355,7 @@ const _ProjectController = {
'editor-redesign',
'paywall-change-compile-timeout',
'overleaf-assist-bundle',
'wf-feature-rebrand',
].filter(Boolean)
const getUserValues = async userId =>
@@ -405,6 +405,7 @@
"discount_of": "",
"discover_the_fastest_way_to_search_and_cite": "",
"dismiss_error_popup": "",
"display": "",
"display_deleted_user": "",
"display_math": "",
"do_you_need_edit_access": "",
@@ -608,6 +609,7 @@
"full_doc_history": "",
"full_width": "",
"future_payments": "",
"generate_from_text_or_image": "",
"generate_token": "",
"generic_if_problem_continues_contact_us": "",
"generic_linked_file_compile_error": "",
@@ -789,6 +791,7 @@
"include_the_error_message_and_ai_response": "",
"increase_indent": "",
"increased_compile_timeout": "",
"inline": "",
"inline_math": "",
"inr_discount_modal_info": "",
"inr_discount_modal_title": "",
@@ -1483,6 +1486,7 @@
"select_image_from_project_files": "",
"select_project": "",
"select_projects": "",
"select_size": "",
"select_tag": "",
"select_user": "",
"selected": "",
@@ -1789,6 +1793,8 @@
"toolbar_editor": "",
"toolbar_format_bold": "",
"toolbar_format_italic": "",
"toolbar_generate_math": "",
"toolbar_generate_table": "",
"toolbar_increase_indent": "",
"toolbar_insert_citation": "",
"toolbar_insert_cross_reference": "",
@@ -1798,6 +1804,7 @@
"toolbar_insert_link": "",
"toolbar_insert_math": "",
"toolbar_insert_math_and_symbols": "",
"toolbar_insert_math_lowercase": "",
"toolbar_insert_misc": "",
"toolbar_insert_table": "",
"toolbar_list_indentation": "",
@@ -1,25 +1,59 @@
import { DropdownHeader } from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { ToolbarButtonMenu } from './button-menu'
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
import MaterialIcon from '../../../../shared/components/material-icon'
import { useTranslation } from 'react-i18next'
import { useCodeMirrorViewContext } from '../codemirror-context'
import { useEditorContext } from '@/shared/context/editor-context'
import {
wrapInDisplayMath,
wrapInInlineMath,
} from '../../extensions/toolbar/commands'
import { memo } from 'react'
import OLListGroupItem from '@/features/ui/components/ol/ol-list-group-item'
import sparkleWhite from '@/shared/svgs/sparkle-small-white.svg'
import sparkle from '@/shared/svgs/ai-sparkle-text.svg'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
export const MathDropdown = memo(function MathDropdown() {
const { t } = useTranslation()
const view = useCodeMirrorViewContext()
const { writefullInstance } = useEditorContext()
const wfRebrandEnabled = isSplitTestEnabled('wf-feature-rebrand')
return (
<ToolbarButtonMenu
id="toolbar-math"
label={t('toolbar_insert_math')}
icon={<MaterialIcon type="calculate" />}
>
{wfRebrandEnabled && writefullInstance && (
<>
<DropdownHeader className="ol-cm-toolbar-header mx-2">
{t('toolbar_insert_math_lowercase')}
</DropdownHeader>
<OLListGroupItem
aria-label={t('toolbar_generate_math')}
onClick={event => {
writefullInstance?.openEquationGenerator()
}}
>
<img
alt="sparkle"
className="ol-cm-toolbar-ai-sparkle-gradient"
src={sparkle}
aria-hidden="true"
/>
<img
alt="sparkle"
className="ol-cm-toolbar-ai-sparkle-white"
src={sparkleWhite}
aria-hidden="true"
/>
<span>{t('generate_from_text_or_image')}</span>
</OLListGroupItem>
</>
)}
<OLListGroupItem
aria-label={t('toolbar_insert_inline_math')}
onClick={event => {
@@ -30,7 +64,7 @@ export const MathDropdown = memo(function MathDropdown() {
}}
>
<MaterialIcon type="123" />
<span>{t('toolbar_insert_inline_math')}</span>
<span>{t('inline')}</span>
</OLListGroupItem>
<OLListGroupItem
aria-label={t('toolbar_insert_display_math')}
@@ -42,7 +76,7 @@ export const MathDropdown = memo(function MathDropdown() {
}}
>
<MaterialIcon type="view_day" />
<span>{t('toolbar_insert_display_math')}</span>
<span>{t('display')}</span>
</OLListGroupItem>
</ToolbarButtonMenu>
)
@@ -0,0 +1,101 @@
import { DropdownHeader } from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { ToolbarButtonMenu } from './button-menu'
import MaterialIcon from '../../../../shared/components/material-icon'
import { useTranslation } from 'react-i18next'
import { useEditorContext } from '@/shared/context/editor-context'
import { memo, useRef, useCallback } from 'react'
import OLListGroupItem from '@/features/ui/components/ol/ol-list-group-item'
import sparkleWhite from '@/shared/svgs/sparkle-small-white.svg'
import sparkle from '@/shared/svgs/ai-sparkle-text.svg'
import { TableInserterDropdown } from './table-inserter-dropdown'
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
import OLPopover from '@/features/ui/components/ol/ol-popover'
import useDropdown from '../../../../shared/hooks/use-dropdown'
import * as commands from '../../extensions/toolbar/commands'
import { useCodeMirrorViewContext } from '../codemirror-context'
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
export const TableDropdown = memo(function TableDropdown() {
const { t } = useTranslation()
const { writefullInstance } = useEditorContext()
const { open, onToggle, ref } = useDropdown()
const target = useRef<any>(null)
const view = useCodeMirrorViewContext()
const onSizeSelected = useCallback(
(sizeX: number, sizeY: number) => {
onToggle(false)
commands.insertTable(view, sizeX, sizeY)
emitToolbarEvent(view, 'table-generator-insert-table')
view.focus()
},
[view, onToggle]
)
return (
<>
<div ref={target}>
<ToolbarButtonMenu
id="toolbar-table"
label={t('toolbar_insert_table')}
icon={<MaterialIcon type="table_chart" />}
>
<DropdownHeader className="ol-cm-toolbar-header mx-2">
{t('toolbar_table_insert_table_lowercase')}
</DropdownHeader>
<OLListGroupItem
aria-label={t('toolbar_generate_table')}
onClick={event => {
writefullInstance?.openTableGenerator()
}}
>
<img
alt="sparkle"
className="ol-cm-toolbar-ai-sparkle-gradient"
src={sparkle}
aria-hidden="true"
/>
<img
alt="sparkle"
className="ol-cm-toolbar-ai-sparkle-white"
src={sparkleWhite}
aria-hidden="true"
/>
<span>{t('generate_from_text_or_image')}</span>
</OLListGroupItem>
<div className="ol-cm-toolbar-dropdown-divider mx-2 my-0" />
<OLListGroupItem
aria-label={t('toolbar_insert_table')}
onMouseDown={event => {
event.preventDefault()
event.stopPropagation()
}}
onClick={() => {
onToggle(!open)
}}
>
<span>{t('select_size')}</span>
</OLListGroupItem>
</ToolbarButtonMenu>
<OLOverlay
show={open}
target={target.current}
placement="bottom"
container={view.dom}
containerPadding={0}
transition
rootClose
onHide={() => onToggle(false)}
>
<OLPopover
id="toolbar-table-menu"
ref={ref}
className="ol-cm-toolbar-button-menu-popover ol-cm-toolbar-button-menu-popover-unstyled"
>
<TableInserterDropdown onSizeSelected={onSizeSelected} />
</OLPopover>
</OLOverlay>
</div>
</>
)
})
@@ -0,0 +1,128 @@
import { FC, memo, useCallback, useRef, useState } from 'react'
import * as commands from '../../extensions/toolbar/commands'
import { useTranslation } from 'react-i18next'
import useDropdown from '../../../../shared/hooks/use-dropdown'
import { useCodeMirrorViewContext } from '../codemirror-context'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
import OLPopover from '@/features/ui/components/ol/ol-popover'
import MaterialIcon from '../../../../shared/components/material-icon'
import classNames from 'classnames'
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
export const LegacyTableDropdown = memo(() => {
const { t } = useTranslation()
const { open, onToggle, ref } = useDropdown()
const view = useCodeMirrorViewContext()
const target = useRef<any>(null)
const onSizeSelected = useCallback(
(sizeX: number, sizeY: number) => {
onToggle(false)
commands.insertTable(view, sizeX, sizeY)
emitToolbarEvent(view, 'table-generator-insert-table')
view.focus()
},
[view, onToggle]
)
return (
<>
<OLTooltip
hidden={open}
id="toolbar-table"
description={<div>{t('toolbar_insert_table')}</div>}
overlayProps={{ placement: 'bottom' }}
>
<button
type="button"
className="ol-cm-toolbar-button btn"
aria-label={t('toolbar_insert_table')}
onMouseDown={event => {
event.preventDefault()
event.stopPropagation()
}}
onClick={() => {
onToggle(!open)
}}
ref={target}
>
<MaterialIcon type="table_chart" />
</button>
</OLTooltip>
<OLOverlay
show={open}
target={target.current}
placement="bottom"
container={view.dom}
containerPadding={0}
transition
rootClose
onHide={() => onToggle(false)}
>
<OLPopover
id="toolbar-table-menu"
ref={ref}
className="ol-cm-toolbar-button-menu-popover ol-cm-toolbar-button-menu-popover-unstyled"
>
<div className="ol-cm-toolbar-table-grid-popover">
<SizeGrid sizeX={10} sizeY={10} onSizeSelected={onSizeSelected} />
</div>
</OLPopover>
</OLOverlay>
</>
)
})
LegacyTableDropdown.displayName = 'TableInserterDropdown'
const range = (start: number, end: number) =>
Array.from({ length: end - start + 1 }, (v, k) => k + start)
const SizeGrid: FC<{
sizeX: number
sizeY: number
onSizeSelected: (sizeX: number, sizeY: number) => void
}> = ({ sizeX, sizeY, onSizeSelected }) => {
const [currentSize, setCurrentSize] = useState<{
sizeX: number
sizeY: number
}>({ sizeX: 0, sizeY: 0 })
const { t } = useTranslation()
let label = t('toolbar_table_insert_table_lowercase')
if (currentSize.sizeX > 0 && currentSize.sizeY > 0) {
label = t('toolbar_table_insert_size_table', {
size: `${currentSize.sizeY}×${currentSize.sizeX}`,
})
}
return (
<>
<div className="ol-cm-toolbar-table-size-label">{label}</div>
<table
className="ol-cm-toolbar-table-grid"
onMouseLeave={() => {
setCurrentSize({ sizeX: 0, sizeY: 0 })
}}
>
<tbody>
{range(1, sizeY).map(y => (
<tr key={y}>
{range(1, sizeX).map(x => (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<td
className={classNames('ol-cm-toolbar-table-cell', {
active: currentSize.sizeX >= x && currentSize.sizeY >= y,
})}
key={x}
onMouseEnter={() => {
setCurrentSize({ sizeX: x, sizeY: y })
}}
onMouseUp={() => onSizeSelected(x, y)}
/>
))}
</tr>
))}
</tbody>
</table>
</>
)
}
@@ -1,78 +1,18 @@
import { FC, memo, useCallback, useRef, useState } from 'react'
import * as commands from '../../extensions/toolbar/commands'
import { useTranslation } from 'react-i18next'
import useDropdown from '../../../../shared/hooks/use-dropdown'
import { useCodeMirrorViewContext } from '../codemirror-context'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
import OLPopover from '@/features/ui/components/ol/ol-popover'
import MaterialIcon from '../../../../shared/components/material-icon'
import { FC, useState } from 'react'
import classNames from 'classnames'
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
export const TableInserterDropdown = memo(() => {
const { t } = useTranslation()
const { open, onToggle, ref } = useDropdown()
const view = useCodeMirrorViewContext()
const target = useRef<any>(null)
const onSizeSelected = useCallback(
(sizeX: number, sizeY: number) => {
onToggle(false)
commands.insertTable(view, sizeX, sizeY)
emitToolbarEvent(view, 'table-generator-insert-table')
view.focus()
},
[view, onToggle]
)
import { useTranslation } from 'react-i18next'
export const TableInserterDropdown = ({
onSizeSelected,
}: {
onSizeSelected: (sizeX: number, sizeY: number) => void
}) => {
return (
<>
<OLTooltip
hidden={open}
id="toolbar-table"
description={<div>{t('toolbar_insert_table')}</div>}
overlayProps={{ placement: 'bottom' }}
>
<button
type="button"
className="ol-cm-toolbar-button btn"
aria-label={t('toolbar_insert_table')}
onMouseDown={event => {
event.preventDefault()
event.stopPropagation()
}}
onClick={() => {
onToggle(!open)
}}
ref={target}
>
<MaterialIcon type="table_chart" />
</button>
</OLTooltip>
<OLOverlay
show={open}
target={target.current}
placement="bottom"
container={view.dom}
containerPadding={0}
transition
rootClose
onHide={() => onToggle(false)}
>
<OLPopover
id="toolbar-table-menu"
ref={ref}
className="ol-cm-toolbar-button-menu-popover ol-cm-toolbar-button-menu-popover-unstyled"
>
<div className="ol-cm-toolbar-table-grid-popover">
<SizeGrid sizeX={10} sizeY={10} onSizeSelected={onSizeSelected} />
</div>
</OLPopover>
</OLOverlay>
</>
<div className="ol-cm-toolbar-table-grid-popover">
<SizeGrid sizeX={10} sizeY={10} onSizeSelected={onSizeSelected} />
</div>
)
})
}
TableInserterDropdown.displayName = 'TableInserterDropdown'
const range = (start: number, end: number) =>
@@ -9,8 +9,10 @@ import getMeta from '../../../../utils/meta'
import { InsertFigureDropdown } from './insert-figure-dropdown'
import { useTranslation } from 'react-i18next'
import { MathDropdown } from './math-dropdown'
import { TableInserterDropdown } from './table-inserter-dropdown'
import { TableDropdown } from './table-dropdown'
import { LegacyTableDropdown } from './table-inserter-dropdown-legacy'
import { withinFormattingCommand } from '@/features/source-editor/utils/tree-operations/formatting'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import { isMac } from '@/shared/utils/os'
export const ToolbarItems: FC<{
@@ -27,12 +29,15 @@ export const ToolbarItems: FC<{
listDepth,
}) {
const { t } = useTranslation()
const { toggleSymbolPalette, showSymbolPalette } = useEditorContext()
const { toggleSymbolPalette, showSymbolPalette, writefullInstance } =
useEditorContext()
const isActive = withinFormattingCommand(state)
const symbolPaletteAvailable = getMeta('ol-symbolPaletteAvailable')
const showGroup = (group: string) => !overflowed || overflowed.has(group)
const wfRebrandEnabled = isSplitTestEnabled('wf-feature-rebrand')
return (
<>
{showGroup('group-history') && (
@@ -142,7 +147,11 @@ export const ToolbarItems: FC<{
icon="book_5"
/>
<InsertFigureDropdown />
<TableInserterDropdown />
{wfRebrandEnabled && writefullInstance ? (
<TableDropdown />
) : (
<LegacyTableDropdown />
)}
</div>
)}
{showGroup('group-list') && (
@@ -59,6 +59,26 @@ const toolbarTheme = EditorView.theme({
},
},
},
'.ol-cm-toolbar-header': {
color: 'var(--toolbar-btn-color)',
},
'.ol-cm-toolbar-dropdown-divider': {
borderBottom: '1px solid',
borderColor: 'var(--toolbar-dropdown-divider-color)',
},
// here render both the icons, and hide one depending on if its dark or light mode with &.overall-theme-dark
'.ol-cm-toolbar-ai-sparkle-gradient': {
display: 'block',
},
'.ol-cm-toolbar-ai-sparkle-white': {
display: 'none',
},
'&.overall-theme-dark .ol-cm-toolbar-ai-sparkle-gradient': {
display: 'none',
},
'&.overall-theme-dark .ol-cm-toolbar-ai-sparkle-white': {
display: 'block',
},
'.ol-cm-toolbar-button-menu-popover': {
backgroundColor: 'initial',
'& > .popover-content, & > .popover-body': {
@@ -28,4 +28,6 @@ export interface WritefullAPI {
name: eventName,
callback: (detail: WritefullEvents[eventName]) => void
): void
openTableGenerator(): void
openEquationGenerator(): void
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" fill="none"><g fill="#fff" clip-path="url(#a)"><path d="M14.87 12.18c-4.74-1.07-5.48-1.8-6.55-6.54a.48.48 0 0 0-.93 0c-1.07 4.74-1.8 5.47-6.54 6.54a.48.48 0 0 0 0 .93c4.74 1.08 5.47 1.8 6.54 6.55a.48.48 0 0 0 .93 0c1.07-4.74 1.8-5.47 6.55-6.55a.48.48 0 0 0 0-.93Zm4.28-7.38c-2.52-.56-2.87-.92-3.44-3.44a.48.48 0 0 0-.93 0c-.57 2.52-.92 2.88-3.44 3.44a.48.48 0 0 0 0 .93c2.52.57 2.87.93 3.44 3.45a.48.48 0 0 0 .93 0c.57-2.52.92-2.88 3.44-3.45a.48.48 0 0 0 0-.93Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .5h20v20H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 617 B

@@ -199,6 +199,7 @@ $link-ui-visited-dark: $blue-40;
--border-active: var(--blue-50);
--border-danger: var(--red-50);
--border-divider: var(--neutral-20);
--border-dark-divider: var(--neutral-70);
--link-web: var(--green-50);
--link-web-hover: var(--green-60);
--link-web-visited: var(--green-50);
@@ -22,6 +22,7 @@
--editor-header-logo-background: url(../../../../../public/img/ol-brand/overleaf-o-white.svg)
center / contain no-repeat;
--editor-toolbar-bg: var(--neutral-80);
--toolbar-dropdown-divider-color: var(--border-dark-divider);
}
.ide-redesign-main {
@@ -70,6 +71,7 @@
--editor-header-logo-background: url(../../../../../public/img/ol-brand/overleaf-o.svg)
center / contain no-repeat;
--editor-toolbar-bg: var(--white);
--toolbar-dropdown-divider-color: var(--border-divider);
.ide-redesign-main {
--toolbar-alt-bg-color: var(--bg-light-secondary);
+7
View File
@@ -529,6 +529,7 @@
"discover_the_fastest_way_to_search_and_cite": "Discover the fastest way to search and cite",
"discover_why_over_people_worldwide_trust_overleaf": "Discover why over __count__ million people worldwide trust Overleaf with their work.",
"dismiss_error_popup": "Dismiss first error alert",
"display": "Display",
"display_deleted_user": "Display deleted users",
"display_math": "Display math",
"do_not_have_acct_or_do_not_want_to_link": "If you dont have an <b>__appName__</b> account, or if you dont want to link to your <b>__institutionName__</b> account, please click <b>__clickText__</b>.",
@@ -807,6 +808,7 @@
"gallery_page_title": "Gallery - Templates, Examples and Articles written in LaTeX",
"gallery_show_more_tags": "Show more",
"general": "General",
"generate_from_text_or_image": "From text or image",
"generate_token": "Generate token",
"generic_if_problem_continues_contact_us": "If the problem continues please contact us",
"generic_linked_file_compile_error": "This projects output files are not available because it failed to compile. Please open the project to see the compilation error details.",
@@ -1019,6 +1021,7 @@
"increased_compile_timeout": "Increased compile timeout",
"individuals": "Individuals",
"info": "Info",
"inline": "Inline",
"inline_math": "Inline math",
"inr_discount_modal_info": "Get document history, track changes, additional collaborators, and more at Purchasing Power Parity prices.",
"inr_discount_modal_title": "70% off all Overleaf premium plans for users in India",
@@ -1948,6 +1951,7 @@
"select_image_from_project_files": "Select image from project files",
"select_project": "Select __project__",
"select_projects": "Select Projects",
"select_size": "Select size",
"select_tag": "Select tag __tagName__",
"select_user": "Select user",
"selected": "Selected",
@@ -2316,6 +2320,8 @@
"toolbar_editor": "Editor tools",
"toolbar_format_bold": "Format Bold",
"toolbar_format_italic": "Format Italic",
"toolbar_generate_math": "Generate Math",
"toolbar_generate_table": "Generate Table",
"toolbar_increase_indent": "Increase Indent",
"toolbar_insert_citation": "Insert Citation",
"toolbar_insert_cross_reference": "Insert Cross-reference",
@@ -2325,6 +2331,7 @@
"toolbar_insert_link": "Insert Link",
"toolbar_insert_math": "Insert Math",
"toolbar_insert_math_and_symbols": "Insert Math and Symbols",
"toolbar_insert_math_lowercase": "Insert math",
"toolbar_insert_misc": "Insert Misc (links, citations, cross-references, figures, tables)",
"toolbar_insert_table": "Insert Table",
"toolbar_list_indentation": "List and Indentation",