layout-dropdown-button.js 6.5 KB

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