You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DragAndDropNotice.vue 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <!--
  2. - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
  3. -
  4. - @author John Molakvoæ <skjnldsv@protonmail.com>
  5. - @author Ferdinand Thiessen <opensource@fthiessen.de>
  6. -
  7. - @license AGPL-3.0-or-later
  8. -
  9. - This program is free software: you can redistribute it and/or modify
  10. - it under the terms of the GNU Affero General Public License as
  11. - published by the Free Software Foundation, either version 3 of the
  12. - License, or (at your option) any later version.
  13. -
  14. - This program is distributed in the hope that it will be useful,
  15. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. - GNU Affero General Public License for more details.
  18. -
  19. - You should have received a copy of the GNU Affero General Public License
  20. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. -
  22. -->
  23. <template>
  24. <div v-show="dragover"
  25. data-cy-files-drag-drop-area
  26. class="files-list__drag-drop-notice"
  27. @drop="onDrop">
  28. <div class="files-list__drag-drop-notice-wrapper">
  29. <template v-if="canUpload && !isQuotaExceeded">
  30. <TrayArrowDownIcon :size="48" />
  31. <h3 class="files-list-drag-drop-notice__title">
  32. {{ t('files', 'Drag and drop files here to upload') }}
  33. </h3>
  34. </template>
  35. <!-- Not permitted to drop files here -->
  36. <template v-else>
  37. <h3 class="files-list-drag-drop-notice__title">
  38. {{ cantUploadLabel }}
  39. </h3>
  40. </template>
  41. </div>
  42. </div>
  43. </template>
  44. <script lang="ts">
  45. import { defineComponent } from 'vue'
  46. import { Folder, Permission } from '@nextcloud/files'
  47. import { showError, showSuccess } from '@nextcloud/dialogs'
  48. import { translate as t } from '@nextcloud/l10n'
  49. import { UploadStatus } from '@nextcloud/upload'
  50. import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
  51. import logger from '../logger.js'
  52. import { handleDrop } from '../services/DropService'
  53. export default defineComponent({
  54. name: 'DragAndDropNotice',
  55. components: {
  56. TrayArrowDownIcon,
  57. },
  58. props: {
  59. currentFolder: {
  60. type: Folder,
  61. required: true,
  62. },
  63. },
  64. data() {
  65. return {
  66. dragover: false,
  67. }
  68. },
  69. computed: {
  70. /**
  71. * Check if the current folder has create permissions
  72. */
  73. canUpload() {
  74. return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) !== 0
  75. },
  76. isQuotaExceeded() {
  77. return this.currentFolder?.attributes?.['quota-available-bytes'] === 0
  78. },
  79. cantUploadLabel() {
  80. if (this.isQuotaExceeded) {
  81. return this.t('files', 'Your have used your space quota and cannot upload files anymore')
  82. } else if (!this.canUpload) {
  83. return this.t('files', 'You don’t have permission to upload or create files here')
  84. }
  85. return null
  86. },
  87. },
  88. mounted() {
  89. // Add events on parent to cover both the table and DragAndDrop notice
  90. const mainContent = window.document.querySelector('main.app-content') as HTMLElement
  91. mainContent.addEventListener('dragover', this.onDragOver)
  92. mainContent.addEventListener('dragleave', this.onDragLeave)
  93. mainContent.addEventListener('drop', this.onContentDrop)
  94. },
  95. beforeDestroy() {
  96. const mainContent = window.document.querySelector('main.app-content') as HTMLElement
  97. mainContent.removeEventListener('dragover', this.onDragOver)
  98. mainContent.removeEventListener('dragleave', this.onDragLeave)
  99. mainContent.removeEventListener('drop', this.onContentDrop)
  100. },
  101. methods: {
  102. onDragOver(event: DragEvent) {
  103. // Needed to keep the drag/drop events chain working
  104. event.preventDefault()
  105. const isForeignFile = event.dataTransfer?.types.includes('Files')
  106. if (isForeignFile) {
  107. // Only handle uploading of outside files (not Nextcloud files)
  108. this.dragover = true
  109. }
  110. },
  111. onDragLeave(event: DragEvent) {
  112. // Counter bubbling, make sure we're ending the drag
  113. // only when we're leaving the current element
  114. // Avoid flickering
  115. const currentTarget = event.currentTarget as HTMLElement
  116. if (currentTarget?.contains((event.relatedTarget ?? event.target) as HTMLElement)) {
  117. return
  118. }
  119. if (this.dragover) {
  120. this.dragover = false
  121. }
  122. },
  123. onContentDrop(event: DragEvent) {
  124. logger.debug('Drag and drop cancelled, dropped on empty space', { event })
  125. event.preventDefault()
  126. if (this.dragover) {
  127. this.dragover = false
  128. }
  129. },
  130. async onDrop(event: DragEvent) {
  131. logger.debug('Dropped on DragAndDropNotice', { event })
  132. // cantUploadLabel is null if we can upload
  133. if (this.cantUploadLabel) {
  134. showError(this.cantUploadLabel)
  135. return
  136. }
  137. if (this.$el.querySelector('tbody')?.contains(event.target as Node)) {
  138. return
  139. }
  140. event.preventDefault()
  141. event.stopPropagation()
  142. if (event.dataTransfer && event.dataTransfer.items.length > 0) {
  143. // Start upload
  144. logger.debug(`Uploading files to ${this.currentFolder.path}`)
  145. // Process finished uploads
  146. const uploads = await handleDrop(event.dataTransfer)
  147. logger.debug('Upload terminated', { uploads })
  148. if (uploads.some((upload) => upload.status === UploadStatus.FAILED)) {
  149. showError(t('files', 'Some files could not be uploaded'))
  150. const failedUploads = uploads.filter((upload) => upload.status === UploadStatus.FAILED)
  151. logger.debug('Some files could not be uploaded', { failedUploads })
  152. } else {
  153. showSuccess(t('files', 'Files uploaded successfully'))
  154. }
  155. // Scroll to last successful upload in current directory if terminated
  156. const lastUpload = uploads.findLast((upload) => upload.status !== UploadStatus.FAILED
  157. && !upload.file.webkitRelativePath.includes('/')
  158. && upload.response?.headers?.['oc-fileid'])
  159. if (lastUpload !== undefined) {
  160. this.$router.push({
  161. ...this.$route,
  162. params: {
  163. view: this.$route.params?.view ?? 'files',
  164. fileid: parseInt(lastUpload.response!.headers['oc-fileid']),
  165. },
  166. })
  167. }
  168. }
  169. this.dragover = false
  170. },
  171. t,
  172. },
  173. })
  174. </script>
  175. <style lang="scss" scoped>
  176. .files-list__drag-drop-notice {
  177. display: flex;
  178. align-items: center;
  179. justify-content: center;
  180. width: 100%;
  181. // Breadcrumbs height + row thead height
  182. min-height: calc(58px + 55px);
  183. margin: 0;
  184. user-select: none;
  185. color: var(--color-text-maxcontrast);
  186. background-color: var(--color-main-background);
  187. border-color: black;
  188. h3 {
  189. margin-left: 16px;
  190. color: inherit;
  191. }
  192. &-wrapper {
  193. display: flex;
  194. align-items: center;
  195. justify-content: center;
  196. height: 15vh;
  197. max-height: 70%;
  198. padding: 0 5vw;
  199. border: 2px var(--color-border-dark) dashed;
  200. border-radius: var(--border-radius-large);
  201. }
  202. }
  203. </style>