global-toasts.tsx 3.3 KB

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