file-tree-modal-create-folder.js 3.5 KB

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