new-project-button.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import { type JSXElementConstructor, useCallback, useState } from 'react'
  2. import classnames from 'classnames'
  3. import { useTranslation } from 'react-i18next'
  4. import getMeta from '../../../utils/meta'
  5. import NewProjectButtonModal, {
  6. NewProjectButtonModalVariant,
  7. } from './new-project-button/new-project-button-modal'
  8. import AddAffiliation, { useAddAffiliation } from './add-affiliation'
  9. import { Nullable } from '../../../../../types/utils'
  10. import { sendMB } from '../../../infrastructure/event-tracking'
  11. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  12. import {
  13. Dropdown,
  14. DropdownDivider,
  15. DropdownHeader,
  16. DropdownItem,
  17. DropdownMenu,
  18. DropdownToggle,
  19. } from '@/shared/components/dropdown/dropdown-menu'
  20. import { useSendProjectListMB } from '@/features/project-list/components/project-list-events'
  21. import type { PortalTemplate } from '../../../../../types/portal-template'
  22. import { useFeatureFlag } from '@/shared/context/split-test-context'
  23. import MaterialIcon from '@/shared/components/material-icon'
  24. type SendTrackingEvent = {
  25. dropdownMenu: string
  26. dropdownOpen: boolean
  27. institutionTemplateName?: string
  28. }
  29. type Segmentation = SendTrackingEvent & {
  30. 'welcome-page-redesign': 'default'
  31. }
  32. type ModalMenuClickOptions = {
  33. modalVariant: NewProjectButtonModalVariant
  34. dropdownMenuEvent: string
  35. }
  36. type NewProjectButtonProps = {
  37. id: string
  38. buttonText?: string
  39. className?: string
  40. trackingKey?: string
  41. showAddAffiliationWidget?: boolean
  42. }
  43. function NewProjectButton({
  44. id,
  45. buttonText,
  46. className,
  47. trackingKey,
  48. showAddAffiliationWidget,
  49. }: NewProjectButtonProps) {
  50. const { t } = useTranslation()
  51. const { templateLinks } = getMeta('ol-ExposedSettings')
  52. const [modal, setModal] =
  53. useState<Nullable<NewProjectButtonModalVariant>>(null)
  54. const portalTemplates = getMeta('ol-portalTemplates') || []
  55. const { show: enableAddAffiliationWidget } = useAddAffiliation()
  56. const sendProjectListMB = useSendProjectListMB()
  57. const docxImportEnabled =
  58. useFeatureFlag('import-docx') &&
  59. getMeta('ol-ExposedSettings').enablePandocConversions
  60. const sendTrackingEvent = useCallback(
  61. ({
  62. dropdownMenu,
  63. dropdownOpen,
  64. institutionTemplateName,
  65. }: SendTrackingEvent) => {
  66. if (trackingKey) {
  67. let segmentation: Segmentation = {
  68. 'welcome-page-redesign': 'default',
  69. dropdownMenu,
  70. dropdownOpen,
  71. }
  72. if (institutionTemplateName) {
  73. segmentation = {
  74. ...segmentation,
  75. institutionTemplateName,
  76. }
  77. }
  78. sendMB(trackingKey, segmentation)
  79. }
  80. },
  81. [trackingKey]
  82. )
  83. const handleMainButtonClick = useCallback(
  84. (dropdownOpen: boolean) => {
  85. sendTrackingEvent({
  86. dropdownMenu: 'main-button',
  87. dropdownOpen,
  88. })
  89. },
  90. [sendTrackingEvent]
  91. )
  92. const handleModalMenuClick = useCallback(
  93. (
  94. e: React.MouseEvent,
  95. { modalVariant, dropdownMenuEvent }: ModalMenuClickOptions
  96. ) => {
  97. // avoid invoking the "onClick" callback on the main dropdown button
  98. e.stopPropagation()
  99. sendTrackingEvent({
  100. dropdownMenu: dropdownMenuEvent,
  101. dropdownOpen: true,
  102. })
  103. sendProjectListMB('new-project-click', { item: dropdownMenuEvent })
  104. setModal(modalVariant)
  105. },
  106. [sendProjectListMB, sendTrackingEvent]
  107. )
  108. const handlePortalTemplateClick = useCallback(
  109. (e: React.MouseEvent, template: PortalTemplate) => {
  110. // avoid invoking the "onClick" callback on the main dropdown button
  111. e.stopPropagation()
  112. sendTrackingEvent({
  113. dropdownMenu: 'institution-template',
  114. dropdownOpen: true,
  115. institutionTemplateName: template.name,
  116. })
  117. sendProjectListMB('new-project-click', {
  118. item: template.name,
  119. destinationURL: template.url,
  120. })
  121. },
  122. [sendProjectListMB, sendTrackingEvent]
  123. )
  124. const handleStaticTemplateClick = useCallback(
  125. (e: React.MouseEvent, template: { trackingKey: string; url: string }) => {
  126. // avoid invoking the "onClick" callback on the main dropdown button
  127. e.stopPropagation()
  128. sendTrackingEvent({
  129. dropdownMenu: template.trackingKey,
  130. dropdownOpen: true,
  131. })
  132. sendProjectListMB('new-project-click', {
  133. item: template.trackingKey,
  134. destinationURL: template.url,
  135. })
  136. },
  137. [sendProjectListMB, sendTrackingEvent]
  138. )
  139. const [importProjectFromGithubMenu] = importOverleafModules(
  140. 'importProjectFromGithubMenu'
  141. )
  142. const ImportProjectFromGithubMenu: JSXElementConstructor<{
  143. onClick: (e: React.MouseEvent) => void
  144. }> = importProjectFromGithubMenu?.import.default
  145. return (
  146. <>
  147. <Dropdown
  148. className={classnames('new-project-dropdown', className)}
  149. onSelect={handleMainButtonClick}
  150. onToggle={nextShow => {
  151. if (nextShow) sendProjectListMB('new-project-expand', undefined)
  152. }}
  153. >
  154. <DropdownToggle
  155. id={id}
  156. className="new-project-button"
  157. variant="primary"
  158. >
  159. {buttonText || t('new_project')}
  160. </DropdownToggle>
  161. <DropdownMenu>
  162. <li role="none">
  163. <DropdownItem
  164. onClick={e =>
  165. handleModalMenuClick(e, {
  166. modalVariant: 'blank_project',
  167. dropdownMenuEvent: 'blank-project',
  168. })
  169. }
  170. >
  171. {t('blank_project')}
  172. </DropdownItem>
  173. </li>
  174. <li role="none">
  175. <DropdownItem
  176. onClick={e =>
  177. handleModalMenuClick(e, {
  178. modalVariant: 'example_project',
  179. dropdownMenuEvent: 'example-project',
  180. })
  181. }
  182. >
  183. {t('example_project')}
  184. </DropdownItem>
  185. </li>
  186. <li role="none">
  187. <DropdownItem
  188. onClick={e =>
  189. handleModalMenuClick(e, {
  190. modalVariant: 'upload_project',
  191. dropdownMenuEvent: 'upload-project',
  192. })
  193. }
  194. >
  195. {t('upload_project')}
  196. </DropdownItem>
  197. </li>
  198. {docxImportEnabled && (
  199. <li role="none">
  200. <DropdownItem
  201. onClick={e =>
  202. handleModalMenuClick(e, {
  203. modalVariant: 'import_docx',
  204. dropdownMenuEvent: 'import-docx',
  205. })
  206. }
  207. trailingIcon={<MaterialIcon type="fiber_new" />}
  208. >
  209. {t('import_word_document')}
  210. </DropdownItem>
  211. </li>
  212. )}
  213. <li role="none">
  214. {ImportProjectFromGithubMenu && (
  215. <ImportProjectFromGithubMenu
  216. onClick={e =>
  217. handleModalMenuClick(e, {
  218. modalVariant: 'import_from_github',
  219. dropdownMenuEvent: 'import-from-github',
  220. })
  221. }
  222. />
  223. )}
  224. </li>
  225. {portalTemplates.length > 0 ? (
  226. <>
  227. <DropdownDivider />
  228. <DropdownHeader aria-hidden="true">
  229. {`${t('institution')} ${t('templates')}`}
  230. </DropdownHeader>
  231. {portalTemplates.map((portalTemplate, index) => (
  232. <li role="none" key={`portal-template-${index}`}>
  233. <DropdownItem
  234. key={`portal-template-${index}`}
  235. href={`${portalTemplate.url}#templates`}
  236. onClick={e => handlePortalTemplateClick(e, portalTemplate)}
  237. aria-label={`${portalTemplate.name} ${t('template')}`}
  238. >
  239. {portalTemplate.name}
  240. </DropdownItem>
  241. </li>
  242. ))}
  243. </>
  244. ) : null}
  245. {templateLinks && templateLinks.length > 0 && (
  246. <>
  247. <DropdownDivider />
  248. <DropdownHeader aria-hidden="true">
  249. {t('templates')}
  250. </DropdownHeader>
  251. </>
  252. )}
  253. {templateLinks?.map((templateLink, index) => (
  254. <li role="none" key={`new-project-button-template-${index}`}>
  255. <DropdownItem
  256. href={templateLink.url}
  257. onClick={e => handleStaticTemplateClick(e, templateLink)}
  258. aria-label={`${templateLink.name} ${t('template')}`}
  259. >
  260. {templateLink.name === 'view_all'
  261. ? t('view_all')
  262. : templateLink.name}
  263. </DropdownItem>
  264. </li>
  265. ))}
  266. {showAddAffiliationWidget && enableAddAffiliationWidget ? (
  267. <>
  268. <DropdownDivider />
  269. <li className="add-affiliation-mobile-wrapper">
  270. <AddAffiliation className="is-mobile" />
  271. </li>
  272. </>
  273. ) : null}
  274. </DropdownMenu>
  275. </Dropdown>
  276. <NewProjectButtonModal modal={modal} onHide={() => setModal(null)} />
  277. </>
  278. )
  279. }
  280. export default NewProjectButton