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

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