layout-dropdown-button.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import { useCallback } from 'react'
  2. import PropTypes from 'prop-types'
  3. import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
  4. import { Trans, useTranslation } from 'react-i18next'
  5. import Icon from '../../../shared/components/icon'
  6. import IconChecked from '../../../shared/components/icon-checked'
  7. import ControlledDropdown from '../../../shared/components/controlled-dropdown'
  8. import IconEditorOnly from './icon-editor-only'
  9. import IconPdfOnly from './icon-pdf-only'
  10. import { useLayoutContext } from '../../../shared/context/layout-context'
  11. import * as eventTracking from '../../../infrastructure/event-tracking'
  12. function IconPlaceholder() {
  13. return <Icon type="" fw />
  14. }
  15. function IconRefresh() {
  16. return <Icon type="refresh" fw spin />
  17. }
  18. function IconLayout() {
  19. return <Icon type="columns" fw />
  20. }
  21. function IconDetach() {
  22. return <Icon type="window-restore" fw />
  23. }
  24. function IconCheckmark({ iconFor, pdfLayout, view, detachRole }) {
  25. if (detachRole === 'detacher' || view === 'history') {
  26. return <IconPlaceholder />
  27. }
  28. if (
  29. iconFor === 'editorOnly' &&
  30. pdfLayout === 'flat' &&
  31. (view === 'editor' || view === 'file')
  32. ) {
  33. return <IconChecked />
  34. } else if (iconFor === 'pdfOnly' && pdfLayout === 'flat' && view === 'pdf') {
  35. return <IconChecked />
  36. } else if (iconFor === 'sideBySide' && pdfLayout === 'sideBySide') {
  37. return <IconChecked />
  38. }
  39. // return empty icon for placeholder
  40. return <IconPlaceholder />
  41. }
  42. function LayoutDropdownButton() {
  43. const { t } = useTranslation()
  44. const {
  45. reattach,
  46. detach,
  47. detachIsLinked,
  48. detachRole,
  49. changeLayout,
  50. view,
  51. pdfLayout,
  52. } = useLayoutContext(layoutContextPropTypes)
  53. const handleDetach = useCallback(() => {
  54. detach()
  55. eventTracking.sendMB('project-layout-detach')
  56. }, [detach])
  57. const handleReattach = useCallback(() => {
  58. if (detachRole !== 'detacher') {
  59. return
  60. }
  61. reattach()
  62. eventTracking.sendMB('project-layout-reattach')
  63. }, [detachRole, reattach])
  64. const handleChangeLayout = useCallback(
  65. (newLayout, newView) => {
  66. handleReattach()
  67. changeLayout(newLayout, newView)
  68. eventTracking.sendMB('project-layout-change', {
  69. layout: newLayout,
  70. view: newView,
  71. })
  72. },
  73. [changeLayout, handleReattach]
  74. )
  75. const processing = !detachIsLinked && detachRole === 'detacher'
  76. // bsStyle is required for Dropdown.Toggle, but we will override style
  77. return (
  78. <>
  79. {processing && (
  80. <div aria-live="assertive" className="sr-only">
  81. {t('layout_processing')}
  82. </div>
  83. )}
  84. <ControlledDropdown
  85. id="layout-dropdown"
  86. className="toolbar-item layout-dropdown"
  87. pullRight
  88. >
  89. <Dropdown.Toggle className="btn-full-height" bsStyle="link">
  90. {processing ? <IconRefresh /> : <IconLayout />}
  91. <span className="toolbar-label">{t('layout')}</span>
  92. <OverlayTrigger
  93. placement="bottom"
  94. overlay={<Tooltip id="pdf-detach-badge">Beta feature</Tooltip>}
  95. delayHide={100}
  96. >
  97. <span className="beta-badge" />
  98. </OverlayTrigger>
  99. </Dropdown.Toggle>
  100. <Dropdown.Menu id="layout-dropdown-list">
  101. <MenuItem onSelect={() => handleChangeLayout('sideBySide')}>
  102. <IconCheckmark
  103. iconFor="sideBySide"
  104. pdfLayout={pdfLayout}
  105. view={view}
  106. detachRole={detachRole}
  107. />
  108. <Icon type="columns" />
  109. {t('editor_and_pdf')}
  110. </MenuItem>
  111. <MenuItem
  112. onSelect={() => handleChangeLayout('flat', 'editor')}
  113. className="menu-item-with-svg"
  114. >
  115. <IconCheckmark
  116. iconFor="editorOnly"
  117. pdfLayout={pdfLayout}
  118. view={view}
  119. detachRole={detachRole}
  120. />
  121. <IconEditorOnly />
  122. <span>
  123. <Trans
  124. i18nKey="editor_only_hide_pdf"
  125. components={[
  126. <span key="editor_only_hide_pdf" className="subdued" />,
  127. ]}
  128. />
  129. </span>
  130. </MenuItem>
  131. <MenuItem
  132. onSelect={() => handleChangeLayout('flat', 'pdf')}
  133. className="menu-item-with-svg"
  134. >
  135. <IconCheckmark
  136. iconFor="pdfOnly"
  137. pdfLayout={pdfLayout}
  138. view={view}
  139. detachRole={detachRole}
  140. />
  141. <IconPdfOnly />
  142. <span>
  143. <Trans
  144. i18nKey="pdf_only_hide_editor"
  145. components={[
  146. <span key="pdf_only_hide_editor" className="subdued" />,
  147. ]}
  148. />
  149. </span>
  150. </MenuItem>
  151. {detachRole === 'detacher' ? (
  152. <MenuItem>
  153. {detachIsLinked ? <IconChecked /> : <IconRefresh />}
  154. <IconDetach />
  155. {t('pdf_in_separate_tab')}
  156. </MenuItem>
  157. ) : (
  158. <MenuItem onSelect={handleDetach}>
  159. <IconPlaceholder />
  160. <IconDetach />
  161. {t('pdf_in_separate_tab')}
  162. </MenuItem>
  163. )}
  164. <MenuItem divider />
  165. <div className="pdf-detach-survey">
  166. <span>
  167. <span className="beta-badge" />
  168. </span>
  169. <span className="pdf-detach-survey-text">
  170. The Layout menu and opening the PDF in a new tab are beta
  171. features.{' '}
  172. <a
  173. href="https://docs.google.com/forms/d/e/1FAIpQLScuxQA8Az9NQwvYgC6FALG7FEtCCj4e8of27e_L0SXGrJFRMw/viewform"
  174. target="_blank"
  175. rel="noopener noreferrer"
  176. >
  177. {t('give_feedback')}.
  178. </a>
  179. </span>
  180. </div>
  181. </Dropdown.Menu>
  182. </ControlledDropdown>
  183. </>
  184. )
  185. }
  186. export default LayoutDropdownButton
  187. IconCheckmark.propTypes = {
  188. iconFor: PropTypes.string.isRequired,
  189. pdfLayout: PropTypes.string.isRequired,
  190. view: PropTypes.string,
  191. detachRole: PropTypes.string,
  192. }
  193. const layoutContextPropTypes = {
  194. reattach: PropTypes.func.isRequired,
  195. detach: PropTypes.func.isRequired,
  196. changeLayout: PropTypes.func.isRequired,
  197. detachIsLinked: PropTypes.bool,
  198. detachRole: PropTypes.string,
  199. pdfLayout: PropTypes.string.isRequired,
  200. view: PropTypes.string,
  201. }