outline-pane.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import React, { useState, useEffect } from 'react'
  2. import PropTypes from 'prop-types'
  3. import classNames from 'classnames'
  4. import { useTranslation } from 'react-i18next'
  5. import OutlineRoot from './outline-root'
  6. import Icon from '../../../shared/components/icon'
  7. import localStorage from '../../../infrastructure/local-storage'
  8. import withErrorBoundary from '../../../infrastructure/error-boundary'
  9. import { useProjectContext } from '../../../shared/context/project-context'
  10. import Tooltip from '../../../shared/components/tooltip'
  11. const OutlinePane = React.memo(function OutlinePane({
  12. isTexFile,
  13. outline,
  14. jumpToLine,
  15. onToggle,
  16. eventTracking,
  17. highlightedLine,
  18. show,
  19. isPartial = false,
  20. }) {
  21. const { t } = useTranslation()
  22. const { _id: projectId } = useProjectContext({
  23. _id: PropTypes.string.isRequired,
  24. })
  25. const storageKey = `file_outline.expanded.${projectId}`
  26. const [expanded, setExpanded] = useState(() => {
  27. const storedExpandedState = localStorage.getItem(storageKey) !== false
  28. return storedExpandedState
  29. })
  30. const isOpen = isTexFile && expanded
  31. useEffect(() => {
  32. onToggle(isOpen)
  33. }, [isOpen, onToggle])
  34. const headerClasses = classNames('outline-pane', {
  35. 'outline-pane-disabled': !isTexFile,
  36. })
  37. function handleExpandCollapseClick() {
  38. if (isTexFile) {
  39. localStorage.setItem(storageKey, !expanded)
  40. eventTracking.sendMB(expanded ? 'outline-collapse' : 'outline-expand')
  41. setExpanded(!expanded)
  42. }
  43. }
  44. // NOTE: This flag is for disabling the rendering of the component. Used while
  45. // both an Angular and React-based file outline is present in the code base.
  46. if (!show) {
  47. return null
  48. }
  49. return (
  50. <div className={headerClasses}>
  51. <header className="outline-header">
  52. <button
  53. className="outline-header-expand-collapse-btn"
  54. disabled={!isTexFile}
  55. onClick={handleExpandCollapseClick}
  56. aria-label={expanded ? t('hide_outline') : t('show_outline')}
  57. >
  58. <Icon
  59. type={isOpen ? 'angle-down' : 'angle-right'}
  60. className="outline-caret-icon"
  61. />
  62. <h4 className="outline-header-name">{t('file_outline')}</h4>
  63. {isPartial && (
  64. <Tooltip
  65. id="partial-outline"
  66. description={t('partial_outline_warning')}
  67. overlayProps={{ placement: 'top' }}
  68. >
  69. <span role="status">
  70. <Icon
  71. type="exclamation-triangle"
  72. aria-label={t('partial_outline_warning')}
  73. />
  74. </span>
  75. </Tooltip>
  76. )}
  77. </button>
  78. </header>
  79. {expanded && isTexFile ? (
  80. <div className="outline-body">
  81. <OutlineRoot
  82. outline={outline}
  83. jumpToLine={jumpToLine}
  84. highlightedLine={highlightedLine}
  85. />
  86. </div>
  87. ) : null}
  88. </div>
  89. )
  90. })
  91. OutlinePane.propTypes = {
  92. isTexFile: PropTypes.bool.isRequired,
  93. outline: PropTypes.array.isRequired,
  94. jumpToLine: PropTypes.func.isRequired,
  95. onToggle: PropTypes.func.isRequired,
  96. eventTracking: PropTypes.object.isRequired,
  97. highlightedLine: PropTypes.number,
  98. show: PropTypes.bool.isRequired,
  99. isPartial: PropTypes.bool,
  100. }
  101. export default withErrorBoundary(OutlinePane)