SharingEntryLink.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  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. <template>
  23. <li :class="{'sharing-entry--share': share}" class="sharing-entry sharing-entry__link">
  24. <Avatar :is-no-user="true"
  25. :class="isEmailShareType ? 'icon-mail-white' : 'icon-public-white'"
  26. class="sharing-entry__avatar" />
  27. <div class="sharing-entry__desc">
  28. <h5>{{ title }}</h5>
  29. </div>
  30. <!-- clipboard -->
  31. <Actions v-if="share && !isEmailShareType && share.token"
  32. ref="copyButton"
  33. class="sharing-entry__copy">
  34. <ActionLink :href="shareLink"
  35. target="_blank"
  36. :icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
  37. @click.stop.prevent="copyLink">
  38. {{ clipboardTooltip }}
  39. </ActionLink>
  40. </Actions>
  41. <!-- pending actions -->
  42. <Actions v-if="!loading && (pendingPassword || pendingExpirationDate)"
  43. class="sharing-entry__actions"
  44. menu-align="right"
  45. :open.sync="open"
  46. @close="onNewLinkShare">
  47. <!-- pending data menu -->
  48. <ActionText v-if="errors.pending"
  49. icon="icon-error"
  50. :class="{ error: errors.pending}">
  51. {{ errors.pending }}
  52. </ActionText>
  53. <ActionText v-else icon="icon-info">
  54. {{ t('files_sharing', 'Please enter the following required information before creating the share') }}
  55. </ActionText>
  56. <!-- password -->
  57. <ActionText v-if="pendingPassword" icon="icon-password">
  58. {{ t('files_sharing', 'Password protection (enforced)') }}
  59. </ActionText>
  60. <ActionCheckbox v-else-if="config.enableLinkPasswordByDefault"
  61. :checked.sync="isPasswordProtected"
  62. :disabled="config.enforcePasswordForPublicLink || saving"
  63. class="share-link-password-checkbox"
  64. @uncheck="onPasswordDisable">
  65. {{ t('files_sharing', 'Password protection') }}
  66. </ActionCheckbox>
  67. <ActionInput v-if="pendingPassword || share.password"
  68. v-tooltip.auto="{
  69. content: errors.password,
  70. show: errors.password,
  71. trigger: 'manual',
  72. defaultContainer: '#app-sidebar'
  73. }"
  74. class="share-link-password"
  75. :value.sync="share.password"
  76. :disabled="saving"
  77. :required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink"
  78. :minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength"
  79. icon=""
  80. autocomplete="new-password"
  81. @submit="onNewLinkShare">
  82. {{ t('files_sharing', 'Enter a password') }}
  83. </ActionInput>
  84. <!-- expiration date -->
  85. <ActionText v-if="pendingExpirationDate" icon="icon-calendar-dark">
  86. {{ t('files_sharing', 'Expiration date (enforced)') }}
  87. </ActionText>
  88. <ActionInput v-if="pendingExpirationDate"
  89. v-model="share.expireDate"
  90. v-tooltip.auto="{
  91. content: errors.expireDate,
  92. show: errors.expireDate,
  93. trigger: 'manual',
  94. defaultContainer: '#app-sidebar'
  95. }"
  96. class="share-link-expire-date"
  97. :disabled="saving"
  98. :first-day-of-week="firstDay"
  99. :lang="lang"
  100. icon=""
  101. type="date"
  102. :not-before="dateTomorrow"
  103. :not-after="dateMaxEnforced">
  104. <!-- let's not submit when picked, the user
  105. might want to still edit or copy the password -->
  106. {{ t('files_sharing', 'Enter a date') }}
  107. </ActionInput>
  108. <ActionButton icon="icon-close" @click.prevent.stop="onCancel">
  109. {{ t('files_sharing', 'Cancel') }}
  110. </ActionButton>
  111. </Actions>
  112. <!-- actions -->
  113. <Actions v-else-if="!loading"
  114. class="sharing-entry__actions"
  115. menu-align="right"
  116. :open.sync="open"
  117. @close="onPasswordSubmit">
  118. <template v-if="share">
  119. <template v-if="isShareOwner">
  120. <!-- folder -->
  121. <template v-if="isFolder && fileHasCreatePermission && config.isPublicUploadEnabled">
  122. <ActionRadio :checked="share.permissions === publicUploadRValue"
  123. :value="publicUploadRValue"
  124. :name="randomId"
  125. :disabled="saving"
  126. @change="togglePermissions">
  127. {{ t('files_sharing', 'Read only') }}
  128. </ActionRadio>
  129. <ActionRadio :checked="share.permissions === publicUploadRWValue"
  130. :value="publicUploadRWValue"
  131. :disabled="saving"
  132. :name="randomId"
  133. @change="togglePermissions">
  134. {{ t('files_sharing', 'Allow upload and editing') }}
  135. </ActionRadio>
  136. <ActionRadio :checked="share.permissions === publicUploadWValue"
  137. :value="publicUploadWValue"
  138. :disabled="saving"
  139. :name="randomId"
  140. class="sharing-entry__action--public-upload"
  141. @change="togglePermissions">
  142. {{ t('files_sharing', 'File drop (upload only)') }}
  143. </ActionRadio>
  144. </template>
  145. <!-- file -->
  146. <ActionCheckbox v-else
  147. :checked.sync="canUpdate"
  148. :disabled="saving"
  149. @change="queueUpdate('permissions')">
  150. {{ t('files_sharing', 'Allow editing') }}
  151. </ActionCheckbox>
  152. <ActionCheckbox
  153. :checked.sync="share.hideDownload"
  154. :disabled="saving"
  155. @change="queueUpdate('hideDownload')">
  156. {{ t('files_sharing', 'Hide download') }}
  157. </ActionCheckbox>
  158. <!-- password -->
  159. <ActionCheckbox :checked.sync="isPasswordProtected"
  160. :disabled="config.enforcePasswordForPublicLink || saving"
  161. class="share-link-password-checkbox"
  162. @uncheck="onPasswordDisable">
  163. {{ config.enforcePasswordForPublicLink
  164. ? t('files_sharing', 'Password protection (enforced)')
  165. : t('files_sharing', 'Password protect') }}
  166. </ActionCheckbox>
  167. <ActionInput v-if="isPasswordProtected"
  168. ref="password"
  169. v-tooltip.auto="{
  170. content: errors.password,
  171. show: errors.password,
  172. trigger: 'manual',
  173. defaultContainer: '#app-sidebar'
  174. }"
  175. class="share-link-password"
  176. :class="{ error: errors.password}"
  177. :disabled="saving"
  178. :required="config.enforcePasswordForPublicLink"
  179. :value="hasUnsavedPassword ? share.newPassword : '***************'"
  180. icon="icon-password"
  181. autocomplete="new-password"
  182. :type="hasUnsavedPassword ? 'text': 'password'"
  183. @update:value="onPasswordChange"
  184. @submit="onPasswordSubmit">
  185. {{ t('files_sharing', 'Enter a password') }}
  186. </ActionInput>
  187. <!-- expiration date -->
  188. <ActionCheckbox :checked.sync="hasExpirationDate"
  189. :disabled="config.isDefaultExpireDateEnforced || saving"
  190. class="share-link-expire-date-checkbox"
  191. @uncheck="onExpirationDisable">
  192. {{ config.isDefaultExpireDateEnforced
  193. ? t('files_sharing', 'Expiration date (enforced)')
  194. : t('files_sharing', 'Set expiration date') }}
  195. </ActionCheckbox>
  196. <ActionInput v-if="hasExpirationDate"
  197. ref="expireDate"
  198. v-tooltip.auto="{
  199. content: errors.expireDate,
  200. show: errors.expireDate,
  201. trigger: 'manual',
  202. defaultContainer: '#app-sidebar'
  203. }"
  204. class="share-link-expire-date"
  205. :class="{ error: errors.expireDate}"
  206. :disabled="saving"
  207. :first-day-of-week="firstDay"
  208. :lang="lang"
  209. :value="share.expireDate"
  210. icon="icon-calendar-dark"
  211. type="date"
  212. :not-before="dateTomorrow"
  213. :not-after="dateMaxEnforced"
  214. @update:value="onExpirationChange">
  215. {{ t('files_sharing', 'Enter a date') }}
  216. </ActionInput>
  217. <!-- note -->
  218. <ActionCheckbox :checked.sync="hasNote"
  219. :disabled="saving"
  220. @uncheck="queueUpdate('note')">
  221. {{ t('files_sharing', 'Note to recipient') }}
  222. </ActionCheckbox>
  223. <ActionTextEditable v-if="hasNote"
  224. ref="note"
  225. v-tooltip.auto="{
  226. content: errors.note,
  227. show: errors.note,
  228. trigger: 'manual',
  229. defaultContainer: '#app-sidebar'
  230. }"
  231. :class="{ error: errors.note}"
  232. :disabled="saving"
  233. :value.sync="share.note"
  234. icon="icon-edit"
  235. @update:value="debounceQueueUpdate('note')" />
  236. </template>
  237. <components :is="action" v-for="(action, index) in externalActions" :key="index" />
  238. <ActionButton icon="icon-delete" :disabled="saving" @click.prevent="onDelete">
  239. {{ t('files_sharing', 'Delete share') }}
  240. </ActionButton>
  241. <ActionButton v-if="!isEmailShareType && canReshare"
  242. class="new-share-link"
  243. icon="icon-add"
  244. @click.prevent.stop="onNewLinkShare">
  245. {{ t('files_sharing', 'Add another link') }}
  246. </ActionButton>
  247. </template>
  248. <!-- Create new share -->
  249. <ActionButton v-else-if="canReshare"
  250. class="new-share-link"
  251. icon="icon-add"
  252. @click.prevent.stop="onNewLinkShare">
  253. {{ t('files_sharing', 'Create a new share link') }}
  254. </ActionButton>
  255. </Actions>
  256. <!-- loading indicator to replace the menu -->
  257. <div v-else class="icon-loading-small sharing-entry__loading" />
  258. </li>
  259. </template>
  260. <script>
  261. import { generateUrl } from '@nextcloud/router'
  262. import axios from '@nextcloud/axios'
  263. import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
  264. import ActionCheckbox from 'nextcloud-vue/dist/Components/ActionCheckbox'
  265. import ActionRadio from 'nextcloud-vue/dist/Components/ActionRadio'
  266. import ActionInput from 'nextcloud-vue/dist/Components/ActionInput'
  267. import ActionText from 'nextcloud-vue/dist/Components/ActionText'
  268. import ActionTextEditable from 'nextcloud-vue/dist/Components/ActionTextEditable'
  269. import ActionLink from 'nextcloud-vue/dist/Components/ActionLink'
  270. import Actions from 'nextcloud-vue/dist/Components/Actions'
  271. import Avatar from 'nextcloud-vue/dist/Components/Avatar'
  272. import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
  273. import Share from '../models/Share'
  274. import SharesMixin from '../mixins/SharesMixin'
  275. const passwordSet = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789'
  276. export default {
  277. name: 'SharingEntryLink',
  278. components: {
  279. Actions,
  280. ActionButton,
  281. ActionCheckbox,
  282. ActionRadio,
  283. ActionInput,
  284. ActionLink,
  285. ActionText,
  286. ActionTextEditable,
  287. Avatar
  288. },
  289. directives: {
  290. Tooltip
  291. },
  292. mixins: [SharesMixin],
  293. props: {
  294. canReshare: {
  295. type: Boolean,
  296. default: true
  297. }
  298. },
  299. data() {
  300. return {
  301. copySuccess: true,
  302. copied: false,
  303. publicUploadRWValue: OC.PERMISSION_UPDATE | OC.PERMISSION_CREATE | OC.PERMISSION_READ | OC.PERMISSION_DELETE,
  304. publicUploadRValue: OC.PERMISSION_READ,
  305. publicUploadWValue: OC.PERMISSION_CREATE,
  306. ExternalLinkActions: OCA.Sharing.ExternalLinkActions.state
  307. }
  308. },
  309. computed: {
  310. /**
  311. * Generate a unique random id for this SharingEntryLink only
  312. * This allows ActionRadios to have the same name prop
  313. * but not to impact others SharingEntryLink
  314. * @returns {string}
  315. */
  316. randomId() {
  317. return Math.random().toString(27).substr(2)
  318. },
  319. /**
  320. * Link share label
  321. * TODO: allow editing
  322. * @returns {string}
  323. */
  324. title() {
  325. // if we have a valid existing share (not pending)
  326. if (this.share && this.share.id) {
  327. if (!this.isShareOwner && this.share.ownerDisplayName) {
  328. return t('files_sharing', 'Shared via link by {initiator}', {
  329. initiator: this.share.ownerDisplayName
  330. })
  331. }
  332. if (this.share.label && this.share.label.trim() !== '') {
  333. return this.share.label
  334. }
  335. if (this.isEmailShareType) {
  336. return this.share.shareWith
  337. }
  338. }
  339. return t('files_sharing', 'Share link')
  340. },
  341. /**
  342. * Is the current share password protected ?
  343. * @returns {boolean}
  344. */
  345. isPasswordProtected: {
  346. get: function() {
  347. return this.config.enforcePasswordForPublicLink
  348. || !!this.share.password
  349. },
  350. set: async function(enabled) {
  351. // TODO: directly save after generation to make sure the share is always protected
  352. this.share.password = enabled ? await this.generatePassword() : ''
  353. this.share.newPassword = this.share.password
  354. }
  355. },
  356. /**
  357. * Is the current share an email share ?
  358. * @returns {boolean}
  359. */
  360. isEmailShareType() {
  361. return this.share
  362. ? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL
  363. : false
  364. },
  365. /**
  366. * Pending data.
  367. * If the share still doesn't have an id, it is not synced
  368. * Therefore this is still not valid and requires user input
  369. * @returns {boolean}
  370. */
  371. pendingPassword() {
  372. return this.config.enforcePasswordForPublicLink && this.share && !this.share.id
  373. },
  374. pendingExpirationDate() {
  375. return this.config.isDefaultExpireDateEnforced && this.share && !this.share.id
  376. },
  377. /**
  378. * Can the recipient edit the file ?
  379. * @returns {boolean}
  380. */
  381. canUpdate: {
  382. get: function() {
  383. return this.share.hasUpdatePermission
  384. },
  385. set: function(enabled) {
  386. this.share.permissions = enabled
  387. ? OC.PERMISSION_READ | OC.PERMISSION_UPDATE
  388. : OC.PERMISSION_READ
  389. }
  390. },
  391. // if newPassword exists, but is empty, it means
  392. // the user deleted the original password
  393. hasUnsavedPassword() {
  394. return this.share.newPassword !== undefined
  395. },
  396. /**
  397. * Is the current share a folder ?
  398. * TODO: move to a proper FileInfo model?
  399. * @returns {boolean}
  400. */
  401. isFolder() {
  402. return this.fileInfo.type === 'dir'
  403. },
  404. /**
  405. * Does the current file/folder have create permissions
  406. * TODO: move to a proper FileInfo model?
  407. * @returns {boolean}
  408. */
  409. fileHasCreatePermission() {
  410. return !!(this.fileInfo.permissions & OC.PERMISSION_CREATE)
  411. },
  412. /**
  413. * Return the public share link
  414. * @returns {string}
  415. */
  416. shareLink() {
  417. return window.location.protocol + '//' + window.location.host + generateUrl('/s/') + this.share.token
  418. },
  419. /**
  420. * Clipboard v-tooltip message
  421. * @returns {string}
  422. */
  423. clipboardTooltip() {
  424. if (this.copied) {
  425. return this.copySuccess
  426. ? t('files_sharing', 'Link copied')
  427. : t('files_sharing', 'Cannot copy, please copy the link manually')
  428. }
  429. return t('files_sharing', 'Copy to clipboard')
  430. },
  431. /**
  432. * External aditionnal actions for the menu
  433. * @returns {Array}
  434. */
  435. externalActions() {
  436. return this.ExternalLinkActions.actions
  437. },
  438. isPasswordPolicyEnabled() {
  439. return typeof this.config.passwordPolicy === 'object'
  440. }
  441. },
  442. methods: {
  443. /**
  444. * Create a new share link and append it to the list
  445. */
  446. async onNewLinkShare() {
  447. const shareDefaults = {
  448. share_type: OC.Share.SHARE_TYPE_LINK
  449. }
  450. if (this.config.isDefaultExpireDateEnforced) {
  451. // default is empty string if not set
  452. // expiration is the share object key, not expireDate
  453. shareDefaults.expiration = this.config.defaultExpirationDateString
  454. }
  455. if (this.config.enableLinkPasswordByDefault) {
  456. shareDefaults.password = await this.generatePassword()
  457. }
  458. // do not push yet if we need a password or an expiration date
  459. if (this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced) {
  460. this.loading = true
  461. // if a share already exists, pushing it
  462. if (this.share && !this.share.id) {
  463. if (this.checkShare(this.share)) {
  464. await this.pushNewLinkShare(this.share, true)
  465. return true
  466. } else {
  467. this.open = true
  468. OC.Notification.showTemporary(t('files_sharing', 'Error, please enter proper password and/or expiration date'))
  469. return false
  470. }
  471. }
  472. // ELSE, show the pending popovermenu
  473. // if password enforced, pre-fill with random one
  474. if (this.config.enforcePasswordForPublicLink) {
  475. shareDefaults.password = await this.generatePassword()
  476. }
  477. // create share & close menu
  478. const share = new Share(shareDefaults)
  479. const component = await new Promise(resolve => {
  480. this.$emit('add:share', share, resolve)
  481. })
  482. // open the menu on the
  483. // freshly created share component
  484. this.open = false
  485. this.loading = false
  486. component.open = true
  487. // Nothing enforced, creating share directly
  488. } else {
  489. const share = new Share(shareDefaults)
  490. await this.pushNewLinkShare(share)
  491. }
  492. },
  493. /**
  494. * Push a new link share to the server
  495. * And update or append to the list
  496. * accordingly
  497. *
  498. * @param {Share} share the new share
  499. * @param {boolean} [update=false] do we update the current share ?
  500. */
  501. async pushNewLinkShare(share, update) {
  502. try {
  503. this.loading = true
  504. this.errors = {}
  505. const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
  506. const newShare = await this.createShare({
  507. path,
  508. shareType: OC.Share.SHARE_TYPE_LINK,
  509. password: share.password,
  510. expireDate: share.expireDate
  511. // we do not allow setting the publicUpload
  512. // before the share creation.
  513. // Todo: We also need to fix the createShare method in
  514. // lib/Controller/ShareAPIController.php to allow file drop
  515. // (currently not supported on create, only update)
  516. })
  517. this.open = false
  518. console.debug('Link share created', newShare)
  519. // if share already exists, copy link directly on next tick
  520. let component
  521. if (update) {
  522. component = await new Promise(resolve => {
  523. this.$emit('update:share', newShare, resolve)
  524. })
  525. } else {
  526. // adding new share to the array and copying link to clipboard
  527. // using promise so that we can copy link in the same click function
  528. // and avoid firefox copy permissions issue
  529. component = await new Promise(resolve => {
  530. this.$emit('add:share', newShare, resolve)
  531. })
  532. }
  533. // Execute the copy link method
  534. // freshly created share component
  535. // ! somehow does not works on firefox !
  536. component.copyLink()
  537. } catch ({ response }) {
  538. const message = response.data.ocs.meta.message
  539. if (message.match(/password/i)) {
  540. this.onSyncError('password', message)
  541. } else if (message.match(/date/i)) {
  542. this.onSyncError('expireDate', message)
  543. } else {
  544. this.onSyncError('pending', message)
  545. }
  546. } finally {
  547. this.loading = false
  548. }
  549. },
  550. /**
  551. * On permissions change
  552. * @param {Event} event js event
  553. */
  554. togglePermissions(event) {
  555. const permissions = parseInt(event.target.value, 10)
  556. this.share.permissions = permissions
  557. this.queueUpdate('permissions')
  558. },
  559. /**
  560. * Generate a valid policy password or
  561. * request a valid password if password_policy
  562. * is enabled
  563. *
  564. * @returns {string} a valid password
  565. */
  566. async generatePassword() {
  567. // password policy is enabled, let's request a pass
  568. if (this.config.passwordPolicy.api && this.config.passwordPolicy.api.generate) {
  569. try {
  570. const request = await axios.get(this.config.passwordPolicy.api.generate)
  571. if (request.data.ocs.data.password) {
  572. return request.data.ocs.data.password
  573. }
  574. } catch (error) {
  575. console.info('Error generating password from password_policy', error)
  576. }
  577. }
  578. // generate password of 10 length based on passwordSet
  579. return Array(10).fill(0)
  580. .reduce((prev, curr) => {
  581. prev += passwordSet.charAt(Math.floor(Math.random() * passwordSet.length))
  582. return prev
  583. }, '')
  584. },
  585. async copyLink() {
  586. try {
  587. await this.$copyText(this.shareLink)
  588. // focus and show the tooltip
  589. this.$refs.copyButton.$el.focus()
  590. this.copySuccess = true
  591. this.copied = true
  592. } catch (error) {
  593. this.copySuccess = false
  594. this.copied = true
  595. console.error(error)
  596. } finally {
  597. setTimeout(() => {
  598. this.copySuccess = false
  599. this.copied = false
  600. }, 4000)
  601. }
  602. },
  603. /**
  604. * Update newPassword values
  605. * of share. If password is set but not newPassword
  606. * then the user did not changed the password
  607. * If both co-exists, the password have changed and
  608. * we show it in plain text.
  609. * Then on submit (or menu close), we sync it.
  610. * @param {string} password the changed password
  611. */
  612. onPasswordChange(password) {
  613. this.$set(this.share, 'newPassword', password)
  614. },
  615. /**
  616. * Uncheck password protection
  617. * We need this method because @update:checked
  618. * is ran simultaneously as @uncheck, so
  619. * so we cannot ensure data is up-to-date
  620. */
  621. onPasswordDisable() {
  622. this.share.password = ''
  623. // reset password state after sync
  624. this.$delete(this.share, 'newPassword')
  625. // only update if valid share.
  626. if (this.share.id) {
  627. this.queueUpdate('password')
  628. }
  629. },
  630. /**
  631. * Menu have been closed or password has been submited.
  632. * The only property that does not get
  633. * synced automatically is the password
  634. * So let's check if we have an unsaved
  635. * password.
  636. * expireDate is saved on datepicker pick
  637. * or close.
  638. */
  639. onPasswordSubmit() {
  640. if (this.hasUnsavedPassword) {
  641. this.share.password = this.share.newPassword
  642. this.queueUpdate('password')
  643. }
  644. },
  645. /**
  646. * Cancel the share creation
  647. * Used in the pending popover
  648. */
  649. onCancel() {
  650. // this.share already exists at this point,
  651. // but is incomplete as not pushed to server
  652. // YET. We can safely delete the share :)
  653. this.$emit('remove:share', this.share)
  654. }
  655. }
  656. }
  657. </script>
  658. <style lang="scss" scoped>
  659. .sharing-entry {
  660. display: flex;
  661. align-items: center;
  662. height: 44px;
  663. &__desc {
  664. display: flex;
  665. flex-direction: column;
  666. justify-content: space-between;
  667. padding: 8px;
  668. line-height: 1.2em;
  669. }
  670. &:not(.sharing-entry--share) &__actions {
  671. .new-share-link {
  672. border-top: 1px solid var(--color-border);
  673. }
  674. }
  675. .sharing-entry__action--public-upload {
  676. border-bottom: 1px solid var(--color-border);
  677. }
  678. &__loading {
  679. width: 44px;
  680. height: 44px;
  681. margin: 0;
  682. padding: 14px;
  683. margin-left: auto;
  684. }
  685. // put menus to the left
  686. // but only the first one
  687. .action-item {
  688. margin-left: auto;
  689. ~ .action-item,
  690. ~ .sharing-entry__loading {
  691. margin-left: 0;
  692. }
  693. }
  694. .icon-checkmark-color {
  695. opacity: 1;
  696. }
  697. }
  698. </style>