file-tree-modal-create-folder.jsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { useEffect, useState } from 'react'
  2. import PropTypes from 'prop-types'
  3. import { useTranslation } from 'react-i18next'
  4. import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
  5. import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
  6. import { DuplicateFilenameError } from '../../errors'
  7. import { isCleanFilename } from '../../util/safe-path'
  8. import OLModal, {
  9. OLModalBody,
  10. OLModalFooter,
  11. OLModalHeader,
  12. OLModalTitle,
  13. } from '@/features/ui/components/ol/ol-modal'
  14. import OLButton from '@/features/ui/components/ol/ol-button'
  15. function FileTreeModalCreateFolder() {
  16. const { t } = useTranslation()
  17. const [name, setName] = useState('')
  18. const [validName, setValidName] = useState(true)
  19. const { isCreatingFolder, inFlight, finishCreatingFolder, cancel, error } =
  20. useFileTreeActionable()
  21. useEffect(() => {
  22. if (!isCreatingFolder) {
  23. // clear the input when the modal is closed
  24. setName('')
  25. }
  26. }, [isCreatingFolder])
  27. if (!isCreatingFolder) return null // the modal will not be rendered; return early
  28. function handleHide() {
  29. cancel()
  30. }
  31. function handleCreateFolder() {
  32. finishCreatingFolder(name)
  33. }
  34. function errorMessage() {
  35. switch (error.constructor) {
  36. case DuplicateFilenameError:
  37. return t('file_already_exists')
  38. default:
  39. return t('generic_something_went_wrong')
  40. }
  41. }
  42. return (
  43. <OLModal show onHide={handleHide}>
  44. <OLModalHeader>
  45. <OLModalTitle>{t('new_folder')}</OLModalTitle>
  46. </OLModalHeader>
  47. <OLModalBody>
  48. <InputName
  49. name={name}
  50. setName={setName}
  51. validName={validName}
  52. setValidName={setValidName}
  53. handleCreateFolder={handleCreateFolder}
  54. />
  55. {!validName ? (
  56. <div
  57. role="alert"
  58. aria-label={t('files_cannot_include_invalid_characters')}
  59. className="alert alert-danger file-tree-modal-alert"
  60. >
  61. {t('files_cannot_include_invalid_characters')}
  62. </div>
  63. ) : null}
  64. {error ? (
  65. <div
  66. role="alert"
  67. aria-label={errorMessage()}
  68. className="alert alert-danger file-tree-modal-alert"
  69. >
  70. {errorMessage()}
  71. </div>
  72. ) : null}
  73. </OLModalBody>
  74. <OLModalFooter>
  75. {inFlight ? (
  76. <OLButton
  77. variant="primary"
  78. disabled
  79. isLoading={inFlight}
  80. bs3Props={{ loading: `${t('creating')}…` }}
  81. />
  82. ) : (
  83. <>
  84. <OLButton variant="secondary" onClick={handleHide}>
  85. {t('cancel')}
  86. </OLButton>
  87. <OLButton
  88. variant="primary"
  89. onClick={handleCreateFolder}
  90. disabled={!validName}
  91. >
  92. {t('create')}
  93. </OLButton>
  94. </>
  95. )}
  96. </OLModalFooter>
  97. </OLModal>
  98. )
  99. }
  100. function InputName({
  101. name,
  102. setName,
  103. validName,
  104. setValidName,
  105. handleCreateFolder,
  106. }) {
  107. const { autoFocusedRef } = useRefWithAutoFocus()
  108. function handleFocus(ev) {
  109. ev.target.setSelectionRange(0, -1)
  110. }
  111. function handleChange(ev) {
  112. setValidName(isCleanFilename(ev.target.value.trim()))
  113. setName(ev.target.value)
  114. }
  115. function handleKeyDown(ev) {
  116. if (ev.key === 'Enter' && validName) {
  117. handleCreateFolder()
  118. }
  119. }
  120. return (
  121. <input
  122. ref={autoFocusedRef}
  123. className="form-control"
  124. type="text"
  125. value={name}
  126. onKeyDown={handleKeyDown}
  127. onChange={handleChange}
  128. onFocus={handleFocus}
  129. />
  130. )
  131. }
  132. InputName.propTypes = {
  133. name: PropTypes.string.isRequired,
  134. setName: PropTypes.func.isRequired,
  135. validName: PropTypes.bool.isRequired,
  136. setValidName: PropTypes.func.isRequired,
  137. handleCreateFolder: PropTypes.func.isRequired,
  138. }
  139. export default FileTreeModalCreateFolder