new-project-button.tsx 10 KB

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