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.

deleteAction.ts 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /**
  2. * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
  3. *
  4. * @author John Molakvoæ <skjnldsv@protonmail.com>
  5. *
  6. * @license AGPL-3.0-or-later
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License as
  10. * published by the Free Software Foundation, either version 3 of the
  11. * License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. import { emit } from '@nextcloud/event-bus'
  23. import { Permission, Node, View, FileAction, FileType } from '@nextcloud/files'
  24. import { showInfo } from '@nextcloud/dialogs'
  25. import { translate as t, translatePlural as n } from '@nextcloud/l10n'
  26. import axios from '@nextcloud/axios'
  27. import CloseSvg from '@mdi/svg/svg/close.svg?raw'
  28. import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw'
  29. import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw'
  30. import logger from '../logger.js'
  31. const canUnshareOnly = (nodes: Node[]) => {
  32. return nodes.every(node => node.attributes['is-mount-root'] === true
  33. && node.attributes['mount-type'] === 'shared')
  34. }
  35. const canDisconnectOnly = (nodes: Node[]) => {
  36. return nodes.every(node => node.attributes['is-mount-root'] === true
  37. && node.attributes['mount-type'] === 'external')
  38. }
  39. const isMixedUnshareAndDelete = (nodes: Node[]) => {
  40. if (nodes.length === 1) {
  41. return false
  42. }
  43. const hasSharedItems = nodes.some(node => canUnshareOnly([node]))
  44. const hasDeleteItems = nodes.some(node => !canUnshareOnly([node]))
  45. return hasSharedItems && hasDeleteItems
  46. }
  47. const isAllFiles = (nodes: Node[]) => {
  48. return !nodes.some(node => node.type !== FileType.File)
  49. }
  50. const isAllFolders = (nodes: Node[]) => {
  51. return !nodes.some(node => node.type !== FileType.Folder)
  52. }
  53. const displayName = (nodes: Node[], view: View) => {
  54. /**
  55. * If we're in the trashbin, we can only delete permanently
  56. */
  57. if (view.id === 'trashbin') {
  58. return t('files', 'Delete permanently')
  59. }
  60. /**
  61. * If we're in the sharing view, we can only unshare
  62. */
  63. if (isMixedUnshareAndDelete(nodes)) {
  64. return t('files', 'Delete and unshare')
  65. }
  66. /**
  67. * If those nodes are all the root node of a
  68. * share, we can only unshare them.
  69. */
  70. if (canUnshareOnly(nodes)) {
  71. if (nodes.length === 1) {
  72. return t('files', 'Leave this share')
  73. }
  74. return t('files', 'Leave these shares')
  75. }
  76. /**
  77. * If those nodes are all the root node of an
  78. * external storage, we can only disconnect it.
  79. */
  80. if (canDisconnectOnly(nodes)) {
  81. if (nodes.length === 1) {
  82. return t('files', 'Disconnect storage')
  83. }
  84. return t('files', 'Disconnect storages')
  85. }
  86. /**
  87. * If we're only selecting files, use proper wording
  88. */
  89. if (isAllFiles(nodes)) {
  90. if (nodes.length === 1) {
  91. return t('files', 'Delete file')
  92. }
  93. return t('files', 'Delete files')
  94. }
  95. /**
  96. * If we're only selecting folders, use proper wording
  97. */
  98. if (isAllFolders(nodes)) {
  99. if (nodes.length === 1) {
  100. return t('files', 'Delete folder')
  101. }
  102. return t('files', 'Delete folders')
  103. }
  104. return t('files', 'Delete')
  105. }
  106. export const action = new FileAction({
  107. id: 'delete',
  108. displayName,
  109. iconSvgInline: (nodes: Node[]) => {
  110. if (canUnshareOnly(nodes)) {
  111. return CloseSvg
  112. }
  113. if (canDisconnectOnly(nodes)) {
  114. return NetworkOffSvg
  115. }
  116. return TrashCanSvg
  117. },
  118. enabled(nodes: Node[]) {
  119. return nodes.length > 0 && nodes
  120. .map(node => node.permissions)
  121. .every(permission => (permission & Permission.DELETE) !== 0)
  122. },
  123. async exec(node: Node) {
  124. try {
  125. await axios.delete(node.encodedSource)
  126. // Let's delete even if it's moved to the trashbin
  127. // since it has been removed from the current view
  128. // and changing the view will trigger a reload anyway.
  129. emit('files:node:deleted', node)
  130. return true
  131. } catch (error) {
  132. logger.error('Error while deleting a file', { error, source: node.source, node })
  133. return false
  134. }
  135. },
  136. async execBatch(nodes: Node[], view: View, dir: string): Promise<(boolean | null)[]> {
  137. const confirm = await new Promise<boolean>(resolve => {
  138. if (nodes.length >= 5 && !canUnshareOnly(nodes) && !canDisconnectOnly(nodes)) {
  139. // TODO use a proper dialog from @nextcloud/dialogs when available
  140. window.OC.dialogs.confirmDestructive(
  141. t('files', 'You are about to delete {count} items.', { count: nodes.length }),
  142. t('files', 'Confirm deletion'),
  143. {
  144. type: window.OC.dialogs.YES_NO_BUTTONS,
  145. confirm: displayName(nodes, view),
  146. confirmClasses: 'error',
  147. cancel: t('files', 'Cancel'),
  148. },
  149. (decision: boolean) => {
  150. resolve(decision)
  151. },
  152. )
  153. return
  154. }
  155. resolve(true)
  156. })
  157. // If the user cancels the deletion, we don't want to do anything
  158. if (confirm === false) {
  159. showInfo(t('files', 'Deletion cancelled'))
  160. return Promise.all(nodes.map(() => false))
  161. }
  162. return Promise.all(nodes.map(node => this.exec(node, view, dir)))
  163. },
  164. order: 100,
  165. })