new-project-button.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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 markdownImportEnabled =
  61. useFeatureFlag('import-markdown') &&
  62. getMeta('ol-ExposedSettings').enablePandocConversions
  63. const sendTrackingEvent = useCallback(
  64. ({
  65. dropdownMenu,
  66. dropdownOpen,
  67. institutionTemplateName,
  68. }: SendTrackingEvent) => {
  69. if (trackingKey) {
  70. let segmentation: Segmentation = {
  71. 'welcome-page-redesign': 'default',
  72. dropdownMenu,
  73. dropdownOpen,
  74. }
  75. if (institutionTemplateName) {
  76. segmentation = {
  77. ...segmentation,
  78. institutionTemplateName,
  79. }
  80. }
  81. sendMB(trackingKey, segmentation)
  82. }
  83. },
  84. [trackingKey]
  85. )
  86. const handleMainButtonClick = useCallback(
  87. (dropdownOpen: boolean) => {
  88. sendTrackingEvent({
  89. dropdownMenu: 'main-button',
  90. dropdownOpen,
  91. })
  92. },
  93. [sendTrackingEvent]
  94. )
  95. const handleModalMenuClick = useCallback(
  96. (
  97. e: React.MouseEvent,
  98. { modalVariant, dropdownMenuEvent }: ModalMenuClickOptions
  99. ) => {
  100. // avoid invoking the "onClick" callback on the main dropdown button
  101. e.stopPropagation()
  102. sendTrackingEvent({
  103. dropdownMenu: dropdownMenuEvent,
  104. dropdownOpen: true,
  105. })
  106. sendProjectListMB('new-project-click', { item: dropdownMenuEvent })
  107. setModal(modalVariant)
  108. },
  109. [sendProjectListMB, sendTrackingEvent]
  110. )
  111. const handlePortalTemplateClick = useCallback(
  112. (e: React.MouseEvent, template: PortalTemplate) => {
  113. // avoid invoking the "onClick" callback on the main dropdown button
  114. e.stopPropagation()
  115. sendTrackingEvent({
  116. dropdownMenu: 'institution-template',
  117. dropdownOpen: true,
  118. institutionTemplateName: template.name,
  119. })
  120. sendProjectListMB('new-project-click', {
  121. item: template.name,
  122. destinationURL: template.url,
  123. })
  124. },
  125. [sendProjectListMB, sendTrackingEvent]
  126. )
  127. const handleStaticTemplateClick = useCallback(
  128. (e: React.MouseEvent, template: { trackingKey: string; url: string }) => {
  129. // avoid invoking the "onClick" callback on the main dropdown button
  130. e.stopPropagation()
  131. sendTrackingEvent({
  132. dropdownMenu: template.trackingKey,
  133. dropdownOpen: true,
  134. })
  135. sendProjectListMB('new-project-click', {
  136. item: template.trackingKey,
  137. destinationURL: template.url,
  138. })
  139. },
  140. [sendProjectListMB, sendTrackingEvent]
  141. )
  142. const [importProjectFromGithubMenu] = importOverleafModules(
  143. 'importProjectFromGithubMenu'
  144. )
  145. const ImportProjectFromGithubMenu: JSXElementConstructor<{
  146. onClick: (e: React.MouseEvent) => void
  147. }> = importProjectFromGithubMenu?.import.default
  148. return (
  149. <>
  150. <Dropdown
  151. className={classnames('new-project-dropdown', className)}
  152. onSelect={handleMainButtonClick}
  153. onToggle={nextShow => {
  154. if (nextShow) sendProjectListMB('new-project-expand', undefined)
  155. }}
  156. >
  157. <DropdownToggle
  158. id={id}
  159. className="new-project-button"
  160. variant="primary"
  161. >
  162. {buttonText || t('new_project')}
  163. </DropdownToggle>
  164. <DropdownMenu>
  165. <li role="none">
  166. <DropdownItem
  167. onClick={e =>
  168. handleModalMenuClick(e, {
  169. modalVariant: 'blank_project',
  170. dropdownMenuEvent: 'blank-project',
  171. })
  172. }
  173. >
  174. {t('blank_project')}
  175. </DropdownItem>
  176. </li>
  177. <li role="none">
  178. <DropdownItem
  179. onClick={e =>
  180. handleModalMenuClick(e, {
  181. modalVariant: 'example_project',
  182. dropdownMenuEvent: 'example-project',
  183. })
  184. }
  185. >
  186. {t('example_project')}
  187. </DropdownItem>
  188. </li>
  189. <li role="none">
  190. <DropdownItem
  191. onClick={e =>
  192. handleModalMenuClick(e, {
  193. modalVariant: 'upload_project',
  194. dropdownMenuEvent: 'upload-project',
  195. })
  196. }
  197. >
  198. {t('upload_project')}
  199. </DropdownItem>
  200. </li>
  201. {docxImportEnabled && (
  202. <li role="none">
  203. <DropdownItem
  204. onClick={e =>
  205. handleModalMenuClick(e, {
  206. modalVariant: 'import_docx',
  207. dropdownMenuEvent: 'import-docx',
  208. })
  209. }
  210. trailingIcon={<MaterialIcon type="fiber_new" />}
  211. >
  212. {t('import_word_document')}
  213. </DropdownItem>
  214. </li>
  215. )}
  216. {markdownImportEnabled && (
  217. <li role="none">
  218. <DropdownItem
  219. onClick={e =>
  220. handleModalMenuClick(e, {
  221. modalVariant: 'import_markdown',
  222. dropdownMenuEvent: 'import-markdown',
  223. })
  224. }
  225. trailingIcon={<MaterialIcon type="fiber_new" />}
  226. >
  227. {t('import_markdown_file')}
  228. </DropdownItem>
  229. </li>
  230. )}
  231. <li role="none">
  232. {ImportProjectFromGithubMenu && (
  233. <ImportProjectFromGithubMenu
  234. onClick={e =>
  235. handleModalMenuClick(e, {
  236. modalVariant: 'import_from_github',
  237. dropdownMenuEvent: 'import-from-github',
  238. })
  239. }
  240. />
  241. )}
  242. </li>
  243. {portalTemplates.length > 0 ? (
  244. <>
  245. <DropdownDivider />
  246. <DropdownHeader aria-hidden="true">
  247. {`${t('institution')} ${t('templates')}`}
  248. </DropdownHeader>
  249. {portalTemplates.map((portalTemplate, index) => (
  250. <li role="none" key={`portal-template-${index}`}>
  251. <DropdownItem
  252. key={`portal-template-${index}`}
  253. href={`${portalTemplate.url}#templates`}
  254. onClick={e => handlePortalTemplateClick(e, portalTemplate)}
  255. aria-label={`${portalTemplate.name} ${t('template')}`}
  256. >
  257. {portalTemplate.name}
  258. </DropdownItem>
  259. </li>
  260. ))}
  261. </>
  262. ) : null}
  263. {templateLinks && templateLinks.length > 0 && (
  264. <>
  265. <DropdownDivider />
  266. <DropdownHeader aria-hidden="true">
  267. {t('templates')}
  268. </DropdownHeader>
  269. </>
  270. )}
  271. {templateLinks?.map((templateLink, index) => (
  272. <li role="none" key={`new-project-button-template-${index}`}>
  273. <DropdownItem
  274. href={templateLink.url}
  275. onClick={e => handleStaticTemplateClick(e, templateLink)}
  276. aria-label={`${templateLink.name} ${t('template')}`}
  277. >
  278. {templateLink.name === 'view_all'
  279. ? t('view_all')
  280. : templateLink.name}
  281. </DropdownItem>
  282. </li>
  283. ))}
  284. {showAddAffiliationWidget && enableAddAffiliationWidget ? (
  285. <>
  286. <DropdownDivider />
  287. <li className="add-affiliation-mobile-wrapper">
  288. <AddAffiliation className="is-mobile" />
  289. </li>
  290. </>
  291. ) : null}
  292. </DropdownMenu>
  293. </Dropdown>
  294. <NewProjectButtonModal modal={modal} onHide={() => setModal(null)} />
  295. </>
  296. )
  297. }
  298. export default NewProjectButton