new-project-button.tsx 10 KB

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