unsaved-docs.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  2. import { useEditorContext } from '@/shared/context/editor-context'
  3. import { FC, useCallback, useEffect, useRef, useState } from 'react'
  4. import { PermissionsLevel } from '@/features/ide-react/types/permissions'
  5. import { UnsavedDocsLockedModal } from '@/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal'
  6. import { UnsavedDocsAlert } from '@/features/ide-react/components/unsaved-docs/unsaved-docs-alert'
  7. import useEventListener from '@/shared/hooks/use-event-listener'
  8. import { createPortal } from 'react-dom'
  9. import { useGlobalAlertsContainer } from '@/features/ide-react/context/global-alerts-context'
  10. const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved
  11. export const UnsavedDocs: FC = () => {
  12. const { openDocs } = useEditorManagerContext()
  13. const { permissionsLevel, setPermissionsLevel } = useEditorContext()
  14. const [isLocked, setIsLocked] = useState(false)
  15. const [unsavedDocs, setUnsavedDocs] = useState(new Map<string, number>())
  16. const globalAlertsContainer = useGlobalAlertsContainer()
  17. // always contains the latest value
  18. const previousUnsavedDocsRef = useRef(unsavedDocs)
  19. useEffect(() => {
  20. previousUnsavedDocsRef.current = unsavedDocs
  21. }, [unsavedDocs])
  22. // always contains the latest value
  23. const permissionsLevelRef = useRef(permissionsLevel)
  24. useEffect(() => {
  25. permissionsLevelRef.current = permissionsLevel
  26. }, [permissionsLevel])
  27. // warn if the window is being closed with unsaved changes
  28. useEventListener(
  29. 'beforeunload',
  30. useCallback(
  31. event => {
  32. if (openDocs.hasUnsavedChanges()) {
  33. // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
  34. event.preventDefault()
  35. }
  36. },
  37. [openDocs]
  38. )
  39. )
  40. // keep track of which docs are currently unsaved, and how long they've been unsaved for
  41. // NOTE: openDocs should never change, so it's safe to use as a dependency here
  42. useEffect(() => {
  43. const interval = window.setInterval(() => {
  44. const unsavedDocs = new Map()
  45. const unsavedDocIds = openDocs.unsavedDocIds()
  46. for (const docId of unsavedDocIds) {
  47. const unsavedSeconds =
  48. (previousUnsavedDocsRef.current.get(docId) ?? 0) + 1
  49. unsavedDocs.set(docId, unsavedSeconds)
  50. }
  51. // avoid setting the unsavedDocs state to a new empty Map every second
  52. if (unsavedDocs.size > 0 || previousUnsavedDocsRef.current.size > 0) {
  53. previousUnsavedDocsRef.current = unsavedDocs
  54. setUnsavedDocs(unsavedDocs)
  55. }
  56. }, 1000)
  57. return () => {
  58. window.clearInterval(interval)
  59. }
  60. }, [openDocs])
  61. const maxUnsavedSeconds = Math.max(0, ...unsavedDocs.values())
  62. // lock the editor if at least one doc has been unsaved for too long
  63. useEffect(() => {
  64. setIsLocked(maxUnsavedSeconds > MAX_UNSAVED_SECONDS)
  65. }, [maxUnsavedSeconds])
  66. // display a modal and set the permissions level to readOnly if docs have been unsaved for too long
  67. const originalPermissionsLevelRef = useRef<PermissionsLevel | null>(null)
  68. useEffect(() => {
  69. if (isLocked) {
  70. originalPermissionsLevelRef.current = permissionsLevelRef.current
  71. // TODO: what if the real permissions level changes in the meantime?
  72. // TODO: perhaps the "locked" state should be stored in the editor context instead?
  73. setPermissionsLevel('readOnly')
  74. setIsLocked(true)
  75. } else {
  76. if (originalPermissionsLevelRef.current) {
  77. setPermissionsLevel(originalPermissionsLevelRef.current)
  78. }
  79. }
  80. }, [isLocked, setPermissionsLevel])
  81. // remove the modal (and unlock the page) if the connection has been re-established and all the docs have been saved
  82. useEffect(() => {
  83. if (unsavedDocs.size === 0 && permissionsLevelRef.current === 'readOnly') {
  84. setIsLocked(false)
  85. }
  86. }, [unsavedDocs])
  87. return (
  88. <>
  89. {isLocked && <UnsavedDocsLockedModal />}
  90. {unsavedDocs.size > 0 &&
  91. globalAlertsContainer &&
  92. createPortal(
  93. <UnsavedDocsAlert unsavedDocs={unsavedDocs} />,
  94. globalAlertsContainer
  95. )}
  96. </>
  97. )
  98. }