SharesMixin.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /**
  2. * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
  3. *
  4. * @author John Molakvoæ <skjnldsv@protonmail.com>
  5. *
  6. * @license GNU AGPL version 3 or any later version
  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 PQueue from 'p-queue'
  23. import debounce from 'debounce'
  24. import Share from '../models/Share'
  25. import SharesRequests from './ShareRequests'
  26. import ShareTypes from './ShareTypes'
  27. import Config from '../services/ConfigService'
  28. import { getCurrentUser } from '@nextcloud/auth'
  29. export default {
  30. mixins: [SharesRequests, ShareTypes],
  31. props: {
  32. fileInfo: {
  33. type: Object,
  34. default: () => {},
  35. required: true
  36. },
  37. share: {
  38. type: Share,
  39. default: null
  40. }
  41. },
  42. data() {
  43. return {
  44. config: new Config(),
  45. // errors helpers
  46. errors: {},
  47. // component status toggles
  48. loading: false,
  49. saving: false,
  50. open: false,
  51. // concurrency management queue
  52. // we want one queue per share
  53. updateQueue: new PQueue({ concurrency: 1 }),
  54. /**
  55. * ! This allow vue to make the Share class state reactive
  56. * ! do not remove it ot you'll lose all reactivity here
  57. */
  58. reactiveState: this.share && this.share.state,
  59. SHARE_TYPES: {
  60. SHARE_TYPE_USER: OC.Share.SHARE_TYPE_USER,
  61. SHARE_TYPE_GROUP: OC.Share.SHARE_TYPE_GROUP,
  62. SHARE_TYPE_LINK: OC.Share.SHARE_TYPE_LINK,
  63. SHARE_TYPE_EMAIL: OC.Share.SHARE_TYPE_EMAIL,
  64. SHARE_TYPE_REMOTE: OC.Share.SHARE_TYPE_REMOTE,
  65. SHARE_TYPE_CIRCLE: OC.Share.SHARE_TYPE_CIRCLE,
  66. SHARE_TYPE_GUEST: OC.Share.SHARE_TYPE_GUEST,
  67. SHARE_TYPE_REMOTE_GROUP: OC.Share.SHARE_TYPE_REMOTE_GROUP,
  68. SHARE_TYPE_ROOM: OC.Share.SHARE_TYPE_ROOM
  69. }
  70. }
  71. },
  72. computed: {
  73. /**
  74. * Does the current share have an expiration date
  75. * @returns {boolean}
  76. */
  77. hasExpirationDate: {
  78. get: function() {
  79. return this.config.isDefaultExpireDateEnforced || !!this.share.expireDate
  80. },
  81. set: function(enabled) {
  82. this.share.expireDate = enabled
  83. ? this.config.defaultExpirationDateString !== ''
  84. ? this.config.defaultExpirationDateString
  85. : moment().format('YYYY-MM-DD')
  86. : ''
  87. }
  88. },
  89. /**
  90. * Does the current share have a note
  91. * @returns {boolean}
  92. */
  93. hasNote: {
  94. get: function() {
  95. return !!this.share.note
  96. },
  97. set: function(enabled) {
  98. this.share.note = enabled
  99. ? t('files_sharing', 'Enter a note for the share recipient')
  100. : ''
  101. }
  102. },
  103. dateTomorrow() {
  104. return moment().add(1, 'days')
  105. },
  106. dateMaxEnforced() {
  107. return this.config.isDefaultExpireDateEnforced
  108. && moment().add(1 + this.config.defaultExpireDate, 'days')
  109. },
  110. /**
  111. * Datepicker lang values
  112. * https://github.com/nextcloud/nextcloud-vue/pull/146
  113. * TODO: have this in vue-components
  114. *
  115. * @returns {int}
  116. */
  117. firstDay() {
  118. return window.firstDay
  119. ? window.firstDay
  120. : 0 // sunday as default
  121. },
  122. lang() {
  123. // fallback to default in case of unavailable data
  124. return {
  125. days: window.dayNamesShort
  126. ? window.dayNamesShort // provided by nextcloud
  127. : ['Sun.', 'Mon.', 'Tue.', 'Wed.', 'Thu.', 'Fri.', 'Sat.'],
  128. months: window.monthNamesShort
  129. ? window.monthNamesShort // provided by nextcloud
  130. : ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'],
  131. placeholder: {
  132. date: 'Select Date' // TODO: Translate
  133. }
  134. }
  135. },
  136. isShareOwner() {
  137. return this.share && this.share.owner === getCurrentUser().uid
  138. }
  139. },
  140. methods: {
  141. /**
  142. * Check if a share is valid before
  143. * firing the request
  144. *
  145. * @param {Share} share the share to check
  146. * @returns {Boolean}
  147. */
  148. checkShare(share) {
  149. if (share.password) {
  150. if (typeof share.password !== 'string' || share.password.trim() === '') {
  151. return false
  152. }
  153. }
  154. if (share.expirationDate) {
  155. const date = moment(share.expirationDate)
  156. if (!date.isValid()) {
  157. return false
  158. }
  159. }
  160. return true
  161. },
  162. /**
  163. * ActionInput can be a little tricky to work with.
  164. * Since we expect a string and not a Date,
  165. * we need to process the value here
  166. *
  167. * @param {Date} date js date to be parsed by moment.js
  168. */
  169. onExpirationChange(date) {
  170. // format to YYYY-MM-DD
  171. const value = moment(date).format('YYYY-MM-DD')
  172. this.share.expireDate = value
  173. this.queueUpdate('expireDate')
  174. },
  175. /**
  176. * Uncheck expire date
  177. * We need this method because @update:checked
  178. * is ran simultaneously as @uncheck, so
  179. * so we cannot ensure data is up-to-date
  180. */
  181. onExpirationDisable() {
  182. this.share.expireDate = ''
  183. this.queueUpdate('expireDate')
  184. },
  185. /**
  186. * Delete share button handler
  187. */
  188. async onDelete() {
  189. try {
  190. this.loading = true
  191. this.open = false
  192. await this.deleteShare(this.share.id)
  193. console.debug('Share deleted', this.share.id)
  194. this.$emit('remove:share', this.share)
  195. } catch (error) {
  196. // re-open menu if error
  197. this.open = true
  198. } finally {
  199. this.loading = false
  200. }
  201. },
  202. /**
  203. * Send an update of the share to the queue
  204. *
  205. * @param {string} property the property to sync
  206. */
  207. queueUpdate(property) {
  208. if (this.share.id) {
  209. // force value to string because that is what our
  210. // share api controller accepts
  211. const value = this.share[property].toString()
  212. this.updateQueue.add(async() => {
  213. this.saving = true
  214. this.errors = {}
  215. try {
  216. await this.updateShare(this.share.id, {
  217. property,
  218. value
  219. })
  220. // clear any previous errors
  221. this.$delete(this.errors, property)
  222. // reset password state after sync
  223. this.$delete(this.share, 'newPassword')
  224. } catch ({ property, message }) {
  225. this.onSyncError(property, message)
  226. } finally {
  227. this.saving = false
  228. }
  229. })
  230. } else {
  231. console.error('Cannot update share.', this.share, 'No valid id')
  232. }
  233. },
  234. /**
  235. * Manage sync errors
  236. * @param {string} property the errored property, e.g. 'password'
  237. * @param {string} message the error message
  238. */
  239. onSyncError(property, message) {
  240. // re-open menu if closed
  241. this.open = true
  242. switch (property) {
  243. case 'password':
  244. case 'pending':
  245. case 'expireDate':
  246. case 'note': {
  247. // show error
  248. this.$set(this.errors, property, message)
  249. let propertyEl = this.$refs[property]
  250. if (propertyEl) {
  251. if (propertyEl.$el) {
  252. propertyEl = propertyEl.$el
  253. }
  254. // focus if there is a focusable action element
  255. const focusable = propertyEl.querySelector('.focusable')
  256. if (focusable) {
  257. focusable.focus()
  258. }
  259. }
  260. break
  261. }
  262. }
  263. },
  264. /**
  265. * Debounce queueUpdate to avoid requests spamming
  266. * more importantly for text data
  267. *
  268. * @param {string} property the property to sync
  269. */
  270. debounceQueueUpdate: debounce(function(property) {
  271. this.queueUpdate(property)
  272. }, 500)
  273. }
  274. }