global-toasts.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { OLToast, OLToastProps } from '@/shared/components/ol/ol-toast'
  2. import useEventListener from '@/shared/hooks/use-event-listener'
  3. import { Fragment, memo, ReactElement, useCallback, useState } from 'react'
  4. import { debugConsole } from '@/utils/debugging'
  5. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  6. import { OLToastContainer } from '@/shared/components/ol/ol-toast-container'
  7. import clipboardToastGenerators from '@/features/source-editor/components/clipboard-toasts'
  8. import importDocumentFeedbackToastGenerators from '@/features/project-list/components/new-project-button/import-document-feedback-toast'
  9. import exportDocumentToastGenerators from '@/features/ide-react/components/toolbar/export-document-toasts'
  10. import pythonOutputToastGenerators from '@/features/ide-react/components/editor/python/python-output-toasts'
  11. const moduleGeneratorsImport = importOverleafModules('toastGenerators') as {
  12. import: { default: GlobalToastGeneratorEntry[] }
  13. }[]
  14. const moduleGenerators = moduleGeneratorsImport.map(
  15. ({ import: { default: listEntry } }) => listEntry
  16. )
  17. export type GlobalToastGeneratorEntry = {
  18. key: string
  19. generator: GlobalToastGenerator
  20. }
  21. type GlobalToastGenerator = (
  22. args: Record<string, any>
  23. ) => Omit<OLToastProps, 'onDismiss'>
  24. const GENERATOR_LIST: GlobalToastGeneratorEntry[] = [
  25. ...moduleGenerators.flat(),
  26. ...clipboardToastGenerators,
  27. ...importDocumentFeedbackToastGenerators,
  28. ...exportDocumentToastGenerators,
  29. ...pythonOutputToastGenerators,
  30. ]
  31. const GENERATOR_MAP: Map<string, GlobalToastGenerator> = new Map(
  32. GENERATOR_LIST.map(({ key, generator }) => [key, generator])
  33. )
  34. let toastCounter = 1
  35. export const GlobalToasts = memo(function GlobalToasts() {
  36. const [toasts, setToasts] = useState<
  37. { component: ReactElement; id: string; handle?: string }[]
  38. >([])
  39. const removeToast = useCallback((id: string) => {
  40. setToasts(current => current.filter(toast => toast.id !== id))
  41. }, [])
  42. const createToast = useCallback(
  43. (id: string, key: string, data: any): ReactElement | null => {
  44. const generator = GENERATOR_MAP.get(key)
  45. if (!generator) {
  46. debugConsole.error('No toast generator found for key:', key)
  47. return null
  48. }
  49. const props = generator(data)
  50. if (!props.autoHide && !props.isDismissible) {
  51. // We don't want any toasts that are not dismissible and don't auto-hide
  52. props.isDismissible = true
  53. }
  54. if (props.autoHide && !props.isDismissible && props.delay !== undefined) {
  55. // If the toast is auto-hiding but not dismissible, we need to make sure the delay is not too long
  56. props.delay = Math.min(props.delay, 60_000)
  57. }
  58. return <OLToast {...props} onDismiss={() => removeToast(id)} />
  59. },
  60. [removeToast]
  61. )
  62. const addToast = useCallback(
  63. (key: string, handle?: string, data?: any) => {
  64. const id = `toast-${toastCounter++}`
  65. const component = createToast(id, key, data)
  66. if (!component) {
  67. return
  68. }
  69. setToasts(current => [...current, { id, handle, component }])
  70. },
  71. [createToast]
  72. )
  73. const showToastListener = useCallback(
  74. (event: CustomEvent) => {
  75. if (!event.detail?.key) {
  76. debugConsole.error('No key provided for toast')
  77. return
  78. }
  79. const { key, handle, ...rest } = event.detail
  80. addToast(key, handle, rest)
  81. },
  82. [addToast]
  83. )
  84. useEventListener('ide:show-toast', showToastListener)
  85. const dismissToastListener = useCallback((event: CustomEvent) => {
  86. const { handle } = event.detail || {}
  87. if (!handle) {
  88. debugConsole.error('No handle provided for dismissing toast')
  89. return
  90. }
  91. setToasts(current => current.filter(toast => toast.handle !== handle))
  92. }, [])
  93. useEventListener('ide:dismiss-toast', dismissToastListener)
  94. return (
  95. <OLToastContainer className="global-toasts">
  96. {toasts.map(({ component, id }) => (
  97. <Fragment key={id}>{component}</Fragment>
  98. ))}
  99. </OLToastContainer>
  100. )
  101. })