unsaved-docs.tsx 3.8 KB

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