new-project-button.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import { type JSXElementConstructor, useCallback, useState } from 'react'
  2. import {
  3. Dropdown as BS3Dropdown,
  4. MenuItem as BS3MenuItem,
  5. } from 'react-bootstrap'
  6. import { useTranslation } from 'react-i18next'
  7. import ControlledDropdown from '../../../shared/components/controlled-dropdown'
  8. import getMeta from '../../../utils/meta'
  9. import NewProjectButtonModal, {
  10. NewProjectButtonModalVariant,
  11. } from './new-project-button/new-project-button-modal'
  12. import AddAffiliation, { useAddAffiliation } from './add-affiliation'
  13. import { Nullable } from '../../../../../types/utils'
  14. import { sendMB } from '../../../infrastructure/event-tracking'
  15. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  16. import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
  17. import {
  18. Dropdown,
  19. DropdownDivider,
  20. DropdownHeader,
  21. DropdownItem,
  22. DropdownMenu,
  23. DropdownToggle,
  24. } from '@/features/ui/components/bootstrap-5/dropdown-menu'
  25. type SendTrackingEvent = {
  26. dropdownMenu: string
  27. dropdownOpen: boolean
  28. institutionTemplateName?: string
  29. }
  30. type Segmentation = SendTrackingEvent & {
  31. 'welcome-page-redesign': 'default'
  32. }
  33. type ModalMenuClickOptions = {
  34. modalVariant: NewProjectButtonModalVariant
  35. dropdownMenuEvent: string
  36. }
  37. type NewProjectButtonProps = {
  38. id: string
  39. buttonText?: string
  40. className?: string
  41. trackingKey?: string
  42. showAddAffiliationWidget?: boolean
  43. }
  44. function NewProjectButton({
  45. id,
  46. buttonText,
  47. className,
  48. trackingKey,
  49. showAddAffiliationWidget,
  50. }: NewProjectButtonProps) {
  51. const { t } = useTranslation()
  52. const { templateLinks } = getMeta('ol-ExposedSettings')
  53. const [modal, setModal] =
  54. useState<Nullable<NewProjectButtonModalVariant>>(null)
  55. const portalTemplates = getMeta('ol-portalTemplates') || []
  56. const { show: enableAddAffiliationWidget } = useAddAffiliation()
  57. const sendTrackingEvent = useCallback(
  58. ({
  59. dropdownMenu,
  60. dropdownOpen,
  61. institutionTemplateName,
  62. }: SendTrackingEvent) => {
  63. if (trackingKey) {
  64. let segmentation: Segmentation = {
  65. 'welcome-page-redesign': 'default',
  66. dropdownMenu,
  67. dropdownOpen,
  68. }
  69. if (institutionTemplateName) {
  70. segmentation = {
  71. ...segmentation,
  72. institutionTemplateName,
  73. }
  74. }
  75. sendMB(trackingKey, segmentation)
  76. }
  77. },
  78. [trackingKey]
  79. )
  80. const handleMainButtonClick = useCallback(
  81. (dropdownOpen: boolean) => {
  82. sendTrackingEvent({
  83. dropdownMenu: 'main-button',
  84. dropdownOpen,
  85. })
  86. },
  87. [sendTrackingEvent]
  88. )
  89. const handleModalMenuClick = useCallback(
  90. (
  91. e: React.MouseEvent,
  92. { modalVariant, dropdownMenuEvent }: ModalMenuClickOptions
  93. ) => {
  94. // avoid invoking the "onClick" callback on the main dropdown button
  95. e.stopPropagation()
  96. sendTrackingEvent({
  97. dropdownMenu: dropdownMenuEvent,
  98. dropdownOpen: true,
  99. })
  100. setModal(modalVariant)
  101. },
  102. [sendTrackingEvent]
  103. )
  104. const handlePortalTemplateClick = useCallback(
  105. (e: React.MouseEvent, institutionTemplateName: string) => {
  106. // avoid invoking the "onClick" callback on the main dropdown button
  107. e.stopPropagation()
  108. sendTrackingEvent({
  109. dropdownMenu: 'institution-template',
  110. dropdownOpen: true,
  111. institutionTemplateName,
  112. })
  113. },
  114. [sendTrackingEvent]
  115. )
  116. const handleStaticTemplateClick = useCallback(
  117. (e: React.MouseEvent, templateTrackingKey: string) => {
  118. // avoid invoking the "onClick" callback on the main dropdown button
  119. e.stopPropagation()
  120. sendTrackingEvent({
  121. dropdownMenu: templateTrackingKey,
  122. dropdownOpen: true,
  123. })
  124. },
  125. [sendTrackingEvent]
  126. )
  127. const [importProjectFromGithubMenu] = importOverleafModules(
  128. 'importProjectFromGithubMenu'
  129. )
  130. const ImportProjectFromGithubMenu: JSXElementConstructor<{
  131. onClick: (e: React.MouseEvent) => void
  132. }> = importProjectFromGithubMenu?.import.default
  133. return (
  134. <>
  135. <BootstrapVersionSwitcher
  136. bs3={
  137. <ControlledDropdown id={id} className={className}>
  138. <BS3Dropdown.Toggle
  139. noCaret
  140. className="new-project-button"
  141. bsStyle="primary"
  142. >
  143. {buttonText || t('new_project')}
  144. </BS3Dropdown.Toggle>
  145. <BS3Dropdown.Menu>
  146. <BS3MenuItem
  147. onClick={(e: React.MouseEvent) =>
  148. handleModalMenuClick(e, {
  149. modalVariant: 'blank_project',
  150. dropdownMenuEvent: 'blank-project',
  151. })
  152. }
  153. >
  154. {t('blank_project')}
  155. </BS3MenuItem>
  156. <BS3MenuItem
  157. onClick={(e: React.MouseEvent) =>
  158. handleModalMenuClick(e, {
  159. modalVariant: 'example_project',
  160. dropdownMenuEvent: 'example-project',
  161. })
  162. }
  163. >
  164. {t('example_project')}
  165. </BS3MenuItem>
  166. <BS3MenuItem
  167. onClick={(e: React.MouseEvent) =>
  168. handleModalMenuClick(e, {
  169. modalVariant: 'upload_project',
  170. dropdownMenuEvent: 'upload-project',
  171. })
  172. }
  173. >
  174. {t('upload_project')}
  175. </BS3MenuItem>
  176. {ImportProjectFromGithubMenu && (
  177. <ImportProjectFromGithubMenu
  178. onClick={e =>
  179. handleModalMenuClick(e, {
  180. modalVariant: 'import_from_github',
  181. dropdownMenuEvent: 'import-from-github',
  182. })
  183. }
  184. />
  185. )}
  186. {portalTemplates.length > 0 ? (
  187. <>
  188. <BS3MenuItem divider />
  189. <div aria-hidden="true" className="dropdown-header">
  190. {`${t('institution')} ${t('templates')}`}
  191. </div>
  192. {portalTemplates.map((portalTemplate, index) => (
  193. <BS3MenuItem
  194. key={`portal-template-${index}`}
  195. href={`${portalTemplate.url}#templates`}
  196. onClick={(e: React.MouseEvent) =>
  197. handlePortalTemplateClick(e, portalTemplate.name)
  198. }
  199. >
  200. {portalTemplate.name}
  201. </BS3MenuItem>
  202. ))}
  203. </>
  204. ) : null}
  205. {templateLinks && templateLinks.length > 0 && (
  206. <>
  207. <BS3MenuItem divider />
  208. <div aria-hidden="true" className="dropdown-header">
  209. {t('templates')}
  210. </div>
  211. </>
  212. )}
  213. {templateLinks?.map((templateLink, index) => (
  214. <BS3MenuItem
  215. key={`new-project-button-template-${index}`}
  216. href={templateLink.url}
  217. onClick={(e: React.MouseEvent) =>
  218. handleStaticTemplateClick(e, templateLink.trackingKey)
  219. }
  220. >
  221. {templateLink.name === 'view_all'
  222. ? t('view_all')
  223. : templateLink.name}
  224. </BS3MenuItem>
  225. ))}
  226. {showAddAffiliationWidget && enableAddAffiliationWidget ? (
  227. <>
  228. <BS3MenuItem divider />
  229. <li className="add-affiliation-mobile-wrapper">
  230. <AddAffiliation className="is-mobile" />
  231. </li>
  232. </>
  233. ) : null}
  234. </BS3Dropdown.Menu>
  235. </ControlledDropdown>
  236. }
  237. bs5={
  238. <Dropdown className={className} onSelect={handleMainButtonClick}>
  239. <DropdownToggle
  240. id={id}
  241. className="new-project-button"
  242. variant="primary"
  243. >
  244. {buttonText || t('new_project')}
  245. </DropdownToggle>
  246. <DropdownMenu>
  247. <li role="none">
  248. <DropdownItem
  249. onClick={e =>
  250. handleModalMenuClick(e, {
  251. modalVariant: 'blank_project',
  252. dropdownMenuEvent: 'blank-project',
  253. })
  254. }
  255. >
  256. {t('blank_project')}
  257. </DropdownItem>
  258. </li>
  259. <li role="none">
  260. <DropdownItem
  261. onClick={e =>
  262. handleModalMenuClick(e, {
  263. modalVariant: 'example_project',
  264. dropdownMenuEvent: 'example-project',
  265. })
  266. }
  267. >
  268. {t('example_project')}
  269. </DropdownItem>
  270. </li>
  271. <li role="none">
  272. <DropdownItem
  273. onClick={e =>
  274. handleModalMenuClick(e, {
  275. modalVariant: 'upload_project',
  276. dropdownMenuEvent: 'upload-project',
  277. })
  278. }
  279. >
  280. {t('upload_project')}
  281. </DropdownItem>
  282. </li>
  283. <li role="none">
  284. {ImportProjectFromGithubMenu && (
  285. <ImportProjectFromGithubMenu
  286. onClick={e =>
  287. handleModalMenuClick(e, {
  288. modalVariant: 'import_from_github',
  289. dropdownMenuEvent: 'import-from-github',
  290. })
  291. }
  292. />
  293. )}
  294. </li>
  295. {portalTemplates.length > 0 ? (
  296. <>
  297. <DropdownDivider />
  298. <DropdownHeader aria-hidden="true">
  299. {`${t('institution')} ${t('templates')}`}
  300. </DropdownHeader>
  301. {portalTemplates.map((portalTemplate, index) => (
  302. <li role="none" key={`portal-template-${index}`}>
  303. <DropdownItem
  304. key={`portal-template-${index}`}
  305. href={`${portalTemplate.url}#templates`}
  306. onClick={e =>
  307. handlePortalTemplateClick(e, portalTemplate.name)
  308. }
  309. aria-label={`${portalTemplate.name} ${t('template')}`}
  310. >
  311. {portalTemplate.name}
  312. </DropdownItem>
  313. </li>
  314. ))}
  315. </>
  316. ) : null}
  317. {templateLinks && templateLinks.length > 0 && (
  318. <>
  319. <DropdownDivider />
  320. <DropdownHeader aria-hidden="true">
  321. {t('templates')}
  322. </DropdownHeader>
  323. </>
  324. )}
  325. {templateLinks?.map((templateLink, index) => (
  326. <li role="none" key={`new-project-button-template-${index}`}>
  327. <DropdownItem
  328. href={templateLink.url}
  329. onClick={e =>
  330. handleStaticTemplateClick(e, templateLink.trackingKey)
  331. }
  332. aria-label={`${templateLink.name} ${t('template')}`}
  333. >
  334. {templateLink.name === 'view_all'
  335. ? t('view_all')
  336. : templateLink.name}
  337. </DropdownItem>
  338. </li>
  339. ))}
  340. {showAddAffiliationWidget && enableAddAffiliationWidget ? (
  341. <>
  342. <DropdownDivider />
  343. <li className="add-affiliation-mobile-wrapper">
  344. <AddAffiliation className="is-mobile" />
  345. </li>
  346. </>
  347. ) : null}
  348. </DropdownMenu>
  349. </Dropdown>
  350. }
  351. />
  352. <NewProjectButtonModal modal={modal} onHide={() => setModal(null)} />
  353. </>
  354. )
  355. }
  356. export default NewProjectButton