[web] Migrate PDF Logs to BS5 (#21062)

* [web] Migrate Logs components JSX to Bootstrap 5

* [web] Migrate logs.less to logs.scss

* [web] Remove unused class names

* [storybook] Define default Bootstrap version in Storybook

This prevents some warning in the console

* [storybook] Update pdf-preview.stories.jsx

* [storybook] Add pdf-log-entry.stories.tsx

* [storybook] Force re-renders when switching BS version

* [web] Keep files dropdown menu in bounds

* [web] Make files dropdown items not bold in BS5

* [web] Revert unrelated change

* [web] Fixup PreviewLogsPaneMaxEntries

* [web] Add style for log-entry-content-link

* [web] Replace log-entry by OLNotification in `PdfCodeCheckFailedNotice`

* [web] Use `BootstrapVersionSwitcher` instead of `isBootstrap5`

* [web] Rename `DropdownBS3` to `BS3Dropdown`

* [web] Reuse variables for `toolbar-height` and `toolbar-small-height`

* [web] Set `id` on `DropdownToggle` not `Dropdown`

* [web] Set `log-entry-btn-expand-collapse` in BS3 only

* [web] Remove `block: true` from StartFreeTrialButton in BS3

* [web] Remove unnecessary CSS in `.log-entry-header-link`

* [web] Use semantic color names

* Migrate the downloadable pdf file list to Bootstrap 5

* Remove nested BootstrapVersionSwitcher, fix "key" prop

* Update roles to: `<li role="menuitem">` `<a role="link">`

* Update `log-entry-header-link`: variant ghost and fix colors

---------

Co-authored-by: Rebeka <o.dekany@gmail.com>
GitOrigin-RevId: 89848970ab5d8a8c135335386caf24363f69a34c
This commit is contained in:
Antoine Clausse 2024-10-23 09:32:40 +02:00 committed by Copybot
parent d77ca18b0b
commit 30860ae9f9
22 changed files with 578 additions and 239 deletions

View file

@ -177,7 +177,11 @@ const preview: Preview = {
return ( return (
<> <>
{activeStyle && <style>{activeStyle.default}</style>} {activeStyle && <style>{activeStyle.default}</style>}
<Story {...context} /> <Story
{...context}
// force re-renders when switching between Bootstrap versions
key={bootstrapVersion}
/>
</> </>
) )
}, },

View file

@ -9,6 +9,9 @@ export const bsVersionDecorator: Meta = {
description: 'Bootstrap version for components', description: 'Bootstrap version for components',
control: { type: 'inline-radio' }, control: { type: 'inline-radio' },
options: ['3', '5'], options: ['3', '5'],
table: {
defaultValue: { summary: '3' },
},
}, },
}, },
args: { args: {

View file

@ -1,8 +1,9 @@
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { Button } from 'react-bootstrap' import OLButton from '@/features/ui/components/ol/ol-button'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { memo } from 'react' import { memo } from 'react'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function PdfClearCacheButton() { function PdfClearCacheButton() {
const { compiling, clearCache, clearingCache } = useCompileContext() const { compiling, clearCache, clearingCache } = useCompileContext()
@ -10,17 +11,29 @@ function PdfClearCacheButton() {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Button <OLButton
bsSize="small" size="sm"
bsStyle="danger" variant="danger"
className="logs-pane-actions-clear-cache" className="logs-pane-actions-clear-cache"
onClick={() => clearCache()} onClick={() => clearCache()}
isLoading={clearingCache}
disabled={clearingCache || compiling} disabled={clearingCache || compiling}
leadingIcon="delete"
> >
{clearingCache ? <Icon type="refresh" spin /> : <Icon type="trash-o" />} <BootstrapVersionSwitcher
&nbsp; bs3={
<>
{clearingCache ? (
<Icon type="refresh" spin />
) : (
<Icon type="trash-o" />
)}
&nbsp;
</>
}
/>
<span>{t('clear_cached_files')}</span> <span>{t('clear_cached_files')}</span>
</Button> </OLButton>
) )
} }

View file

@ -1,21 +1,29 @@
import { memo } from 'react' import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import { bsVersion } from '@/features/utils/bootstrap-5'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function PdfCodeCheckFailedNotice() { function PdfCodeCheckFailedNotice() {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="log-entry"> <OLNotification
<div className="log-entry-header log-entry-header-error"> type="error"
<div className="log-entry-header-icon-container"> content={
<Icon type="exclamation-triangle" fw /> <>
</div> <BootstrapVersionSwitcher
<h3 className="log-entry-header-title"> bs3={
<>
<Icon type="exclamation-triangle" fw />{' '}
</>
}
/>
{t('code_check_failed_explanation')} {t('code_check_failed_explanation')}
</h3> </>
</div> }
</div> className={bsVersion({ bs5: 'm-0', bs3: 'mb-2' })}
/>
) )
} }

View file

@ -1,9 +1,16 @@
import { Dropdown } from 'react-bootstrap' import { Dropdown as BS3Dropdown } from 'react-bootstrap'
import {
Dropdown,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import PdfFileList from './pdf-file-list' import PdfFileList from './pdf-file-list'
import ControlledDropdown from '../../../shared/components/controlled-dropdown' import ControlledDropdown from '../../../shared/components/controlled-dropdown'
import { memo } from 'react' import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function PdfDownloadFilesButton() { function PdfDownloadFilesButton() {
const { compiling, fileList } = useCompileContext() const { compiling, fileList } = useCompileContext()
@ -11,22 +18,42 @@ function PdfDownloadFilesButton() {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<ControlledDropdown <BootstrapVersionSwitcher
id="dropdown-files-logs-pane" bs3={
dropup <ControlledDropdown
pullRight id="dropdown-files-logs-pane"
disabled={compiling || !fileList} dropup
> pullRight
<Dropdown.Toggle disabled={compiling || !fileList}
className="dropdown-toggle btn-secondary-info btn-secondary" >
title={t('other_logs_and_files')} <BS3Dropdown.Toggle
bsSize="small" className="dropdown-toggle btn-secondary-info btn-secondary"
bsStyle={null} title={t('other_logs_and_files')}
/> bsSize="small"
<Dropdown.Menu id="dropdown-files-logs-pane-list"> bsStyle={null}
<PdfFileList fileList={fileList} /> />
</Dropdown.Menu> <BS3Dropdown.Menu id="dropdown-files-logs-pane-list">
</ControlledDropdown> <PdfFileList fileList={fileList} />
</BS3Dropdown.Menu>
</ControlledDropdown>
}
bs5={
<Dropdown drop="up">
<DropdownToggle
id="dropdown-files-logs-pane"
variant="secondary"
title={t('other_logs_and_files')}
size="sm"
disabled={compiling || !fileList}
>
{t('other_logs_and_files')}
</DropdownToggle>
<DropdownMenu id="dropdown-files-logs-pane-list">
<PdfFileList fileList={fileList} />
</DropdownMenu>
</Dropdown>
}
/>
) )
} }

View file

@ -1,7 +1,13 @@
import { MenuItem } from 'react-bootstrap' import { MenuItem as BS3MenuItem } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { memo } from 'react' import { memo } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import {
DropdownDivider,
DropdownHeader,
DropdownItem,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
function PdfFileList({ fileList }) { function PdfFileList({ fileList }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -15,37 +21,93 @@ function PdfFileList({ fileList }) {
} }
return ( return (
<> <BootstrapVersionSwitcher
<MenuItem header>{t('other_output_files')}</MenuItem> bs3={
<>
<BS3MenuItem header>{t('other_output_files')}</BS3MenuItem>
{fileList.top.map(file => ( {fileList.top.map(file => (
<MenuItem download={basename(file)} href={file.url} key={file.path}> <BS3MenuItem
<b>{file.path}</b> download={basename(file)}
</MenuItem> href={file.url}
))} key={file.path}
>
<b>{file.path}</b>
</BS3MenuItem>
))}
{fileList.other.length > 0 && fileList.top.length > 0 && ( {fileList.other.length > 0 && fileList.top.length > 0 && (
<MenuItem divider /> <BS3MenuItem divider />
)} )}
{fileList.other.map(file => ( {fileList.other.map(file => (
<MenuItem download={basename(file)} href={file.url} key={file.path}> <BS3MenuItem
<b>{file.path}</b> download={basename(file)}
</MenuItem> href={file.url}
))} key={file.path}
>
<b>{file.path}</b>
</BS3MenuItem>
))}
{fileList.archive?.fileCount > 0 && ( {fileList.archive?.fileCount && fileList.archive?.fileCount > 0 && (
<MenuItem <BS3MenuItem
download={basename(fileList.archive)} download={basename(fileList.archive)}
href={fileList.archive.url} href={fileList.archive.url}
key={fileList.archive.path} >
> <b>
<b> {t('download_all')} ({fileList.archive.fileCount})
{t('download_all')} ({fileList.archive.fileCount}) </b>
</b> </BS3MenuItem>
</MenuItem> )}
)} </>
</> }
bs5={
<>
<DropdownHeader>{t('other_output_files')}</DropdownHeader>
{fileList.top.map(file => (
<li key={file.path} role="menuitem">
<DropdownItem
role="link"
download={basename(file)}
href={file.url}
>
{file.path}
</DropdownItem>
</li>
))}
{fileList.other.length > 0 && fileList.top.length > 0 && (
<DropdownDivider divider />
)}
{fileList.other.map(file => (
<li key={file.path} role="menuitem">
<DropdownItem
role="link"
download={basename(file)}
href={file.url}
>
{file.path}
</DropdownItem>
</li>
))}
{fileList.archive?.fileCount > 0 && (
<li role="menuitem">
<DropdownItem
role="link"
download={basename(fileList.archive)}
href={fileList.archive.url}
>
{t('download_all')} ({fileList.archive.fileCount})
</DropdownItem>
</li>
)}
</>
}
/>
) )
} }

View file

@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'
import { useResizeObserver } from '../../../shared/hooks/use-resize-observer' import { useResizeObserver } from '../../../shared/hooks/use-resize-observer'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import classNames from 'classnames' import classNames from 'classnames'
import { Button } from 'react-bootstrap' import OLButton from '@/features/ui/components/ol/ol-button'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
@ -44,10 +44,13 @@ export default function PdfLogEntryRawContent({
'log-entry-content-button-container-collapsed': !expanded, 'log-entry-content-button-container-collapsed': !expanded,
})} })}
> >
<Button <OLButton
bsSize="xs" variant="secondary"
bsStyle={null} size="sm"
className="log-entry-btn-expand-collapse btn-secondary" bs3Props={{
bsSize: 'xsmall',
className: 'log-entry-btn-expand-collapse',
}}
onClick={() => setExpanded(value => !value)} onClick={() => setExpanded(value => !value)}
> >
{expanded ? ( {expanded ? (
@ -59,7 +62,7 @@ export default function PdfLogEntryRawContent({
<Icon type="angle-down" /> {t('expand')} <Icon type="angle-down" /> {t('expand')}
</> </>
)} )}
</Button> </OLButton>
</div> </div>
)} )}
</div> </div>

View file

@ -1,7 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { Button } from 'react-bootstrap' import OLButton from '@/features/ui/components/ol/ol-button'
import PdfLogEntry from './pdf-log-entry' import PdfLogEntry from './pdf-log-entry'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error' import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
@ -23,9 +23,10 @@ function PdfPreviewError({ error }) {
i18nKey="something_went_wrong_rendering_pdf_expected" i18nKey="something_went_wrong_rendering_pdf_expected"
components={[ components={[
// eslint-disable-next-line react/jsx-key // eslint-disable-next-line react/jsx-key
<Button <OLButton
bsSize="xs" variant="info"
bsStyle="info" size="sm"
bs3Props={{ bsSize: 'xsmall' }}
onClick={() => startCompile()} onClick={() => startCompile()}
/>, />,
]} ]}
@ -157,7 +158,7 @@ function PdfPreviewError({ error }) {
<ErrorLogEntry title={t('no_pdf_error_title')}> <ErrorLogEntry title={t('no_pdf_error_title')}>
{t('no_pdf_error_explanation')} {t('no_pdf_error_explanation')}
<ul className="log-entry-formatted-content-list"> <ul className="my-1 ps-3">
<li>{t('no_pdf_error_reason_unrecoverable_error')}</li> <li>{t('no_pdf_error_reason_unrecoverable_error')}</li>
<li> <li>
<Trans <Trans
@ -282,9 +283,10 @@ function TimedOutLogEntry() {
i18nKey="project_timed_out_enable_stop_on_first_error" i18nKey="project_timed_out_enable_stop_on_first_error"
components={[ components={[
// eslint-disable-next-line react/jsx-key // eslint-disable-next-line react/jsx-key
<Button <OLButton
bsSize="xs" variant="info"
bsStyle="info" size="sm"
bs3Props={{ bsSize: 'xsmall' }}
onClick={handleEnableStopOnFirstErrorClick} onClick={handleEnableStopOnFirstErrorClick}
/>, />,
]} ]}

View file

@ -1,6 +1,6 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import { Button } from 'react-bootstrap' import OLButton from '@/features/ui/components/ol/ol-button'
import PdfLogEntry from './pdf-log-entry' import PdfLogEntry from './pdf-log-entry'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error' import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
@ -28,9 +28,14 @@ export default function StopOnFirstErrorPrompt() {
// eslint-disable-next-line react/jsx-key // eslint-disable-next-line react/jsx-key
components={[<strong />]} components={[<strong />]}
/>{' '} />{' '}
<Button bsSize="xs" bsStyle="info" onClick={handleDisableButtonClick}> <OLButton
variant="info"
size="sm"
onClick={handleDisableButtonClick}
bs3Props={{ bsSize: 'xsmall' }}
>
{t('disable_stop_on_first_error')} {t('disable_stop_on_first_error')}
</Button> </OLButton>
</> </>
} }
level="info" level="info"

View file

@ -4,7 +4,7 @@ import StartFreeTrialButton from '../../../shared/components/start-free-trial-bu
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import PdfLogEntry from './pdf-log-entry' import PdfLogEntry from './pdf-log-entry'
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error' import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
import { Button } from 'react-bootstrap' import OLButton from '@/features/ui/components/ol/ol-button'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { useFeatureFlag } from '@/shared/context/split-test-context' import { useFeatureFlag } from '@/shared/context/split-test-context'
import getMeta from '@/utils/meta' import getMeta from '@/utils/meta'
@ -84,13 +84,7 @@ const CompileTimeout = memo(function CompileTimeout({
<p className="text-center"> <p className="text-center">
<StartFreeTrialButton <StartFreeTrialButton
source="compile-timeout" source="compile-timeout"
buttonProps={{ buttonProps={{ variant: 'primary', className: 'w-100' }}
variant: 'primary',
className: 'row-spaced-small',
bs3Props: {
block: true,
},
}}
> >
{hasNewPaywallCta {hasNewPaywallCta
? t('get_more_compile_time') ? t('get_more_compile_time')
@ -178,10 +172,12 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
i18nKey="enable_stop_on_first_error_under_recompile_dropdown_menu" i18nKey="enable_stop_on_first_error_under_recompile_dropdown_menu"
components={[ components={[
// eslint-disable-next-line react/jsx-key // eslint-disable-next-line react/jsx-key
<Button <OLButton
bsSize="xs" variant="link"
bsStyle="info-ghost-inline" className="btn-inline-link fw-bold"
size="sm"
onClick={handleEnableStopOnFirstErrorClick} onClick={handleEnableStopOnFirstErrorClick}
bs3Props={{ bsSize: 'xsmall' }}
/>, />,
// eslint-disable-next-line react/jsx-key // eslint-disable-next-line react/jsx-key
<strong />, <strong />,

View file

@ -3,8 +3,11 @@ import classNames from 'classnames'
import { useState, useRef } from 'react' import { useState, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useResizeObserver from '../hooks/use-resize-observer' import useResizeObserver from '../hooks/use-resize-observer'
import Tooltip from '../../../shared/components/tooltip' import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import OLButton from '@/features/ui/components/ol/ol-button'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
function PreviewLogEntryHeader({ function PreviewLogEntryHeader({
sourceLocation, sourceLocation,
@ -72,18 +75,25 @@ function PreviewLogEntryHeader({
// This essentially tells the browser that, althought the text is laid out from right-to-left, // This essentially tells the browser that, althought the text is laid out from right-to-left,
// the wrapped portion of text should follow left-to-right writing rules. // the wrapped portion of text should follow left-to-right writing rules.
const locationLink = locationLinkText ? ( const locationLink = locationLinkText ? (
<button <OLButton
variant="ghost"
className={logEntryLocationBtnClasses} className={logEntryLocationBtnClasses}
type="button"
aria-label={headerLogLocationTitle} aria-label={headerLogLocationTitle}
onClick={onSourceLocationClick} onClick={onSourceLocationClick}
> >
<Icon type="chain" /> <BootstrapVersionSwitcher
&nbsp; bs3={
<>
<Icon type="chain" />
&nbsp;
</>
}
bs5={<MaterialIcon type="link" />}
/>
<span ref={logLocationSpanRef} className="log-entry-header-link-location"> <span ref={logLocationSpanRef} className="log-entry-header-link-location">
{`\u202A${locationLinkText}\u202C`} {`\u202A${locationLinkText}\u202C`}
</span> </span>
</button> </OLButton>
) : null ) : null
const headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle const headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle
@ -95,26 +105,26 @@ function PreviewLogEntryHeader({
) : null} ) : null}
<h3 className="log-entry-header-title">{headerTitleText}</h3> <h3 className="log-entry-header-title">{headerTitleText}</h3>
{locationSpanOverflown && locationLinkText ? ( {locationSpanOverflown && locationLinkText ? (
<Tooltip <OLTooltip
id={locationLinkText} id={locationLinkText}
description={locationLinkText} description={locationLinkText}
overlayProps={{ placement: 'left' }} overlayProps={{ placement: 'left' }}
tooltipProps={{ className: 'log-location-tooltip' }} tooltipProps={{ className: 'log-location-tooltip' }}
> >
{locationLink} {locationLink}
</Tooltip> </OLTooltip>
) : ( ) : (
locationLink locationLink
)} )}
{showCloseButton ? ( {showCloseButton ? (
<button <OLButton
variant="link"
className="btn-inline-link log-entry-header-link" className="btn-inline-link log-entry-header-link"
type="button"
aria-label={t('dismiss_error_popup')} aria-label={t('dismiss_error_popup')}
onClick={onClose} onClick={onClose}
> >
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </OLButton>
) : null} ) : null}
</header> </header>
) )

View file

@ -1,11 +1,13 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap' import OLButton from '@/features/ui/components/ol/ol-button'
import PreviewLogEntryHeader from './preview-log-entry-header' import PreviewLogEntryHeader from './preview-log-entry-header'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error' import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function PreviewLogsPaneMaxEntries({ totalEntries, entriesShown, hasErrors }) { function PreviewLogsPaneMaxEntries({ totalEntries, entriesShown, hasErrors }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -34,16 +36,22 @@ function PreviewLogsPaneMaxEntries({ totalEntries, entriesShown, hasErrors }) {
{hasErrors && !stoppedOnFirstError ? ( {hasErrors && !stoppedOnFirstError ? (
<> <>
<p> <p>
<Icon type="lightbulb-o" /> <BootstrapVersionSwitcher
bs3={<Icon type="lightbulb-o" />}
bs5={
<MaterialIcon type="lightbulb" className="align-middle" />
}
/>
&nbsp; &nbsp;
<strong>{t('tip')}: </strong> <strong>{t('tip')}: </strong>
<Trans <Trans
i18nKey="log_entry_maximum_entries_enable_stop_on_first_error" i18nKey="log_entry_maximum_entries_enable_stop_on_first_error"
components={[ components={[
<Button <OLButton
variant="info"
size="sm"
key="enable-stop-on-first-error" key="enable-stop-on-first-error"
bsSize="xs" bs3Props={{ bsSize: 'xsmall' }}
bsStyle="info"
onClick={handleEnableStopOnFirstErrorClick} onClick={handleEnableStopOnFirstErrorClick}
/>, />,
// eslint-disable-next-line jsx-a11y/anchor-has-content // eslint-disable-next-line jsx-a11y/anchor-has-content
@ -58,7 +66,10 @@ function PreviewLogsPaneMaxEntries({ totalEntries, entriesShown, hasErrors }) {
</> </>
) : ( ) : (
<p> <p>
<Icon type="lightbulb-o" /> <BootstrapVersionSwitcher
bs3={<Icon type="lightbulb-o" />}
bs5={<MaterialIcon type="lightbulb" className="align-middle" />}
/>
&nbsp; &nbsp;
<strong>{t('tip')}: </strong> <strong>{t('tip')}: </strong>
{t('log_entry_maximum_entries_see_full_logs')} {t('log_entry_maximum_entries_see_full_logs')}

View file

@ -508,6 +508,8 @@ const hints = {
}, },
} }
export const ruleIds = Object.keys(hints)
if (!getMeta('ol-wikiEnabled')) { if (!getMeta('ol-wikiEnabled')) {
Object.keys(hints).forEach(ruleId => { Object.keys(hints).forEach(ruleId => {
hints[ruleId].extraInfoURL = null hints[ruleId].extraInfoURL = null

View file

@ -0,0 +1,75 @@
import PdfLogEntry from '@/features/pdf-preview/components/pdf-log-entry'
import type { Meta, StoryObj } from '@storybook/react'
import { bsVersionDecorator } from '../../.storybook/utils/with-bootstrap-switcher'
import { ruleIds } from '@/ide/human-readable-logs/HumanReadableLogsHints'
import { ScopeDecorator } from './decorators/scope'
import { useMeta } from './hooks/use-meta'
import { FC, ReactNode } from 'react'
import { useScope } from './hooks/use-scope'
import { EditorView } from '@codemirror/view'
const fakeSourceLocation = {
file: 'file.tex',
line: 12,
column: 5,
}
const fakeLogEntry = {
key: 'fake',
ruleId: 'hint_misplaced_alignment_tab_character',
message: 'Fake message',
messageComponent: 'Fake message component',
content: 'Fake content',
type: 'Error: ',
level: 'error',
contentDetails: ['Fake detail 1', 'Fake detail 2'],
file: 'fake.tex',
line: 12,
column: 5,
raw: 'Fake raw',
}
const fakeArgs = {
headerTitle: 'PDF Preview',
formattedContent: 'This is a log entry',
level: 'error' as const,
extraInfoURL: 'https://example.com',
showCloseButton: true,
showSourceLocationLink: true,
rawContent: 'This is a raw log entry',
contentDetails: ['detail 1', 'detail 2'],
ruleId: 'hint_misplaced_alignment_tab_character' as const,
sourceLocation: fakeSourceLocation,
logEntry: fakeLogEntry,
logType: 'Fake type',
}
const meta: Meta<typeof PdfLogEntry> = {
title: 'Editor / PDF Preview / Logs',
component: PdfLogEntry,
// @ts-ignore
decorators: [ScopeDecorator],
argTypes: {
ruleId: { control: 'select', options: [...ruleIds, 'other'] },
...bsVersionDecorator.argTypes,
},
args: fakeArgs,
}
export default meta
type Story = StoryObj<typeof PdfLogEntry>
const Provider: FC<{ children: ReactNode }> = ({ children }) => {
useMeta({ 'ol-showAiErrorAssistant': true })
useScope({ 'editor.view': new EditorView({ doc: '\\begin{document' }) })
return <div className="logs-pane p-2">{children}</div>
}
export const PdfLogEntryWithControls: Story = {
render: args => (
<Provider>
<PdfLogEntry {...args} />
</Provider>
),
}

View file

@ -23,6 +23,11 @@ import { cloneDeep } from 'lodash'
import { ScopeDecorator } from './decorators/scope' import { ScopeDecorator } from './decorators/scope'
import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider' import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
import { bsVersionDecorator } from '../../.storybook/utils/with-bootstrap-switcher' import { bsVersionDecorator } from '../../.storybook/utils/with-bootstrap-switcher'
import {
Dropdown,
DropdownMenu,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
export default { export default {
title: 'Editor / PDF Preview', title: 'Editor / PDF Preview',
@ -286,7 +291,7 @@ export const DisplayError = () => {
}) })
return ( return (
<> <div className="logs-pane">
{compileErrors.map(error => ( {compileErrors.map(error => (
<div <div
key={error} key={error}
@ -296,7 +301,7 @@ export const DisplayError = () => {
<PdfPreviewError error={error} /> <PdfPreviewError error={error} />
</div> </div>
))} ))}
</> </div>
) )
} }
@ -320,11 +325,22 @@ export const FileList = () => {
}, []) }, [])
return ( return (
<div className="dropdown open"> <BootstrapVersionSwitcher
<div className="dropdown-menu"> bs3={
<PdfFileList fileList={fileList} /> <div className="dropdown open">
</div> <div className="dropdown-menu">
</div> <PdfFileList fileList={fileList} />
</div>
</div>
}
bs5={
<Dropdown>
<DropdownMenu id="dropdown-files-logs-pane-list" show>
<PdfFileList fileList={fileList} />
</DropdownMenu>
</Dropdown>
}
/>
) )
} }

View file

@ -32,11 +32,6 @@
overflow: hidden; overflow: hidden;
} }
.log-entry-first-error-popup {
border-radius: 0;
overflow: auto;
}
.log-entry-header { .log-entry-header {
padding: 3px @padding-sm; padding: 3px @padding-sm;
display: flex; display: flex;
@ -182,40 +177,6 @@
} }
} }
.log-entry-formatted-content-list {
margin: @margin-xs 0;
padding-left: @padding-md;
}
.first-error-popup {
position: absolute;
z-index: 1;
top: @toolbar-small-height + 2px;
right: @padding-xs;
width: 90%;
max-width: 450px;
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
animation: fade-in 0.15s linear 0s 1 none;
background-color: #fff;
&::before {
content: '';
.triangle(top, @padding-sm, @padding-xs, @ol-red);
top: -@padding-xs;
right: @padding-xl;
}
}
.first-error-popup-actions {
display: flex;
justify-content: space-between;
padding: 0 @padding-sm @padding-sm @padding-sm;
border-radius: 0 0 @border-radius-base @border-radius-base;
}
.first-error-btn {
.no-outline-ring-on-click;
}
.log-location-tooltip { .log-location-tooltip {
word-break: break-all; word-break: break-all;
&.tooltip.in { &.tooltip.in {

View file

@ -113,14 +113,12 @@
} }
.pdf-viewer, .pdf-viewer,
.pdf-logs,
.pdf-errors, .pdf-errors,
.pdf-uncompiled { .pdf-uncompiled {
.full-size; .full-size;
top: @pdf-top-offset; top: @pdf-top-offset;
} }
.pdf-logs,
.pdf-errors, .pdf-errors,
.pdf-uncompiled, .pdf-uncompiled,
.pdf-validation-problems { .pdf-validation-problems {
@ -327,72 +325,6 @@
} }
} }
.pdf-logs {
overflow: auto;
.alert {
font-size: 0.9rem;
margin-bottom: @line-height-computed / 2;
cursor: pointer;
.line-no {
float: right;
color: @log-line-no-color;
font-weight: 700;
.fa {
opacity: 0;
}
}
.entry-message {
font-weight: 700;
//font-family: @font-family-monospace;
}
.entry-content {
white-space: pre-wrap;
font-size: 0.8rem;
//font-family: @font-family-monospace;
}
&:hover .line-no {
color: inherit;
.fa {
opacity: 1;
}
}
&.alert-danger {
background-color: tint(@alert-danger-bg, 15%);
&:hover {
background-color: @alert-danger-bg;
}
}
&.alert-warning {
background-color: tint(@alert-warning-bg, 15%);
&:hover {
background-color: @alert-warning-bg;
}
}
&.alert-info {
background-color: tint(@alert-info-bg, 15%);
&:hover {
background-color: @alert-info-bg;
}
}
}
pre {
font-size: 12px;
white-space: pre-wrap;
}
.dropdown {
position: relative;
}
.force-recompile {
margin-top: 10px;
text-align: right;
}
}
.synctex-controls { .synctex-controls {
margin-right: -8px; margin-right: -8px;
position: absolute; position: absolute;
@ -443,9 +375,6 @@
} }
.editor-dark { .editor-dark {
.pdf-logs {
background-color: lighten(@editor-dark-background-color, 10%);
}
.pdfjs-viewer { .pdfjs-viewer {
background-color: lighten(@editor-dark-background-color, 10%); background-color: lighten(@editor-dark-background-color, 10%);
} }
@ -604,21 +533,13 @@
@editor-and-logs-pane-toolbars-height: @toolbar-small-height + @toolbar-height; @editor-and-logs-pane-toolbars-height: @toolbar-small-height + @toolbar-height;
@btn-small-height: (@padding-small-vertical * 2)+ (@font-size-small * @btn-small-height: (@padding-small-vertical * 2)+ (@font-size-small *
@line-height-small); @line-height-small); // 5px * 2 + 14px * 1.5 = 31px
#download-dropdown-list,
#dropdown-files-logs-pane-list { #dropdown-files-logs-pane-list {
overflow-y: auto; overflow-y: auto;
.dropdown-header { .dropdown-header {
white-space: nowrap; white-space: nowrap;
} }
}
#download-dropdown-list {
max-height: calc(
~'100vh - ' @editor-and-logs-pane-toolbars-height ~' - ' @margin-md
);
}
#dropdown-files-logs-pane-list {
max-height: calc( max-height: calc(
~'100vh - ' @editor-and-logs-pane-toolbars-height ~' - ' @btn-small-height ~' - ' ~'100vh - ' @editor-and-logs-pane-toolbars-height ~' - ' @btn-small-height ~' - '
@margin-md @margin-md

View file

@ -3,6 +3,8 @@ $footer-height: 50px;
// Header // Header
$header-height: 68px; $header-height: 68px;
$toolbar-height: 40px;
$toolbar-small-height: 32px;
// Forms // Forms
$form-group-margin-bottom: $spacing-06; $form-group-margin-bottom: $spacing-06;

View file

@ -9,6 +9,7 @@
@import 'editor/hotkeys'; @import 'editor/hotkeys';
@import 'editor/left-menu'; @import 'editor/left-menu';
@import 'editor/loading-screen'; @import 'editor/loading-screen';
@import 'editor/logs';
@import 'editor/outline'; @import 'editor/outline';
@import 'editor/file-tree'; @import 'editor/file-tree';
@import 'editor/file-view'; @import 'editor/file-view';

View file

@ -0,0 +1,203 @@
@import '../../foundations/colors';
.logs-pane {
position: absolute;
inset: 0;
overflow-y: auto;
background-color: var(--bg-dark-secondary);
z-index: 11; // above the PDF viewer + controls
top: var(--toolbar-small-height);
.logs-pane-content {
display: flex;
flex-direction: column;
padding: 10px;
gap: 10px;
min-height: 100%;
}
.logs-pane-actions {
display: flex;
flex-wrap: wrap;
place-content: flex-end flex-end;
padding: var(--spacing-03) 0;
flex-grow: 1;
align-items: flex-end;
gap: var(--spacing-04);
}
.log-entry {
border-radius: var(--border-radius-base);
overflow: hidden;
}
.log-entry-header {
padding: var(--spacing-02) var(--spacing-04);
display: flex;
align-items: flex-start;
gap: var(--spacing-04);
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0;
color: var(--content-primary-dark);
.material-symbols {
@include body-base;
}
}
.log-entry-header-error {
background-color: var(--content-danger);
}
.log-entry-header-link-error {
@include ol-button-variant(
$color: var(--content-primary-dark),
$background: var(--bg-danger-02),
$hover-background: var(--red-70)
);
}
.log-entry-header-warning {
background-color: var(--content-warning-dark);
}
.log-entry-header-link-warning {
@include ol-button-variant(
$color: var(--content-primary-dark),
$background: var(--bg-warning-01),
$hover-background: var(--bg-warning-02)
);
}
.log-entry-header-typesetting {
background-color: var(--blue-50);
}
.log-entry-header-link-typesetting {
@include ol-button-variant(
$color: var(--content-primary-dark),
$background: var(--blue-60),
$hover-background: var(--blue-70)
);
}
.log-entry-header-raw,
.log-entry-header-info {
background-color: var(--bg-dark-tertiary);
}
.log-entry-header-link-raw,
.log-entry-header-link-info {
@include ol-button-variant(
$color: var(--content-primary-dark),
$background: var(--bg-dark-secondary),
$hover-background: var(--bg-dark-primary)
);
}
.log-entry-header-success {
background-color: var(--green-50);
}
.log-entry-header-link-success {
@include ol-button-variant(
$color: var(--content-primary-dark),
$background: var(--green-60),
$hover-background: var(--green-70)
);
}
.log-entry-header-title {
@include body-base;
flex-grow: 1;
font-weight: bold;
margin: 0;
color: var(--content-primary-dark);
}
.log-entry-header-link {
color: var(--content-primary-dark);
border-width: 0;
max-width: 33%;
text-decoration: none; // needed for the "close button"
padding: 0 var(--spacing-03);
.button-content {
min-width: 0; // needed to display the ellipsis on overflow
}
}
.log-entry-header-link-location {
white-space: nowrap;
direction: rtl;
text-overflow: ellipsis;
overflow: hidden;
}
.log-entry-content {
background-color: var(--bg-light-primary);
padding: var(--spacing-04);
}
.log-entry-content-raw {
font-size: var(--font-size-01);
color: var(--content-secondary);
padding: var(--spacing-03);
margin: 0;
white-space: pre-wrap;
}
.log-entry-content-button-container {
position: relative;
height: 40px;
margin-top: 0;
transition:
margin 0.15s ease-in-out,
opacity 0.15s ease-in-out;
padding-bottom: var(--spacing-04);
text-align: center;
background-image: linear-gradient(
0deg,
var(--bg-light-tertiary) 0%,
transparent 100%
);
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
}
.log-entry-content-button-container-collapsed {
margin-top: -40px;
}
.log-entry-content-raw-container {
background-color: var(--bg-light-tertiary);
border-radius: var(--border-radius-base);
overflow: hidden;
margin-top: var(--spacing-03);
}
}
.btn-secondary-compile-timeout-override {
color: var(--content-primary);
background-color: var(--bg-light-primary);
border-color: var(--border-primary);
border-width: 1px;
}
.log-entry-formatted-content,
.log-entry-content-link {
font-size: var(--font-size-02);
margin-top: var(--spacing-02);
&:first-of-type {
margin-top: 0;
}
}
.log-location-tooltip {
word-break: break-all;
& > .tooltip-inner {
max-width: 450px;
text-align: left;
}
}

View file

@ -136,7 +136,6 @@
} }
.pdf-viewer, .pdf-viewer,
.pdf-logs,
.pdf-errors, .pdf-errors,
.pdf-uncompiled { .pdf-uncompiled {
@extend .full-size; @extend .full-size;
@ -378,3 +377,17 @@
max-width: none; max-width: none;
} }
} }
#dropdown-files-logs-pane-list {
overflow-y: auto;
.dropdown-header {
white-space: nowrap;
}
// This keeps the dropdown menu inside the Logs div.
// "spacing-11" is there to compensate the button height and the margin around the logs.
max-height: calc(
100vh - #{$toolbar-small-height + $toolbar-height + $spacing-11}
);
}

View file

@ -1,3 +1,5 @@
@import '../../abstracts/variables';
:root { :root {
--toolbar-border-color: var(--neutral-80); --toolbar-border-color: var(--neutral-80);
--toolbar-header-bg-color: var(--neutral-90); --toolbar-header-bg-color: var(--neutral-90);
@ -7,7 +9,8 @@
--toolbar-btn-hover-color: var(--white); --toolbar-btn-hover-color: var(--white);
--toolbar-btn-active-color: var(--white); --toolbar-btn-active-color: var(--white);
--toolbar-btn-active-bg-color: var(--green-50); --toolbar-btn-active-bg-color: var(--green-50);
--toolbar-small-height: 32px; --toolbar-height: #{$toolbar-height};
--toolbar-small-height: #{$toolbar-small-height};
--toolbar-alt-bg-color: var(--neutral-80); --toolbar-alt-bg-color: var(--neutral-80);
--formatting-btn-color: var(--white); --formatting-btn-color: var(--white);
--formatting-btn-bg: var(--neutral-80); --formatting-btn-bg: var(--neutral-80);
@ -208,8 +211,6 @@
} }
&.toolbar-header { &.toolbar-header {
--toolbar-height: 40px;
align-items: stretch; align-items: stretch;
background-color: var(--toolbar-header-bg-color); background-color: var(--toolbar-header-bg-color);
position: absolute; position: absolute;
@ -315,7 +316,7 @@
} }
.toolbar-editor { .toolbar-editor {
height: 32px; height: var(--toolbar-small-height);
background-color: var(--editor-toolbar-bg); background-color: var(--editor-toolbar-bg);
padding: 0 5px; padding: 0 5px;
overflow: hidden; overflow: hidden;