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.

SharingDetailsTab.vue 35KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162
  1. <template>
  2. <div class="sharingTabDetailsView">
  3. <div class="sharingTabDetailsView__header">
  4. <span>
  5. <NcAvatar v-if="isUserShare"
  6. class="sharing-entry__avatar"
  7. :is-no-user="share.shareType !== SHARE_TYPES.SHARE_TYPE_USER"
  8. :user="share.shareWith"
  9. :display-name="share.shareWithDisplayName"
  10. :menu-position="'left'"
  11. :url="share.shareWithAvatar" />
  12. <component :is="getShareTypeIcon(share.type)" :size="32" />
  13. </span>
  14. <span>
  15. <h1>{{ title }}</h1>
  16. </span>
  17. </div>
  18. <div class="sharingTabDetailsView__wrapper">
  19. <div ref="quickPermissions" class="sharingTabDetailsView__quick-permissions">
  20. <div>
  21. <NcCheckboxRadioSwitch :button-variant="true"
  22. data-cy-files-sharing-share-permissions-bundle="read-only"
  23. :checked.sync="sharingPermission"
  24. :value="bundledPermissions.READ_ONLY.toString()"
  25. name="sharing_permission_radio"
  26. type="radio"
  27. button-variant-grouped="vertical"
  28. @update:checked="toggleCustomPermissions">
  29. {{ t('files_sharing', 'View only') }}
  30. <template #icon>
  31. <ViewIcon :size="20" />
  32. </template>
  33. </NcCheckboxRadioSwitch>
  34. <NcCheckboxRadioSwitch :button-variant="true"
  35. data-cy-files-sharing-share-permissions-bundle="upload-edit"
  36. :checked.sync="sharingPermission"
  37. :value="bundledPermissions.ALL.toString()"
  38. name="sharing_permission_radio"
  39. type="radio"
  40. button-variant-grouped="vertical"
  41. @update:checked="toggleCustomPermissions">
  42. <template v-if="allowsFileDrop">
  43. {{ t('files_sharing', 'Allow upload and editing') }}
  44. </template>
  45. <template v-else>
  46. {{ t('files_sharing', 'Allow editing') }}
  47. </template>
  48. <template #icon>
  49. <EditIcon :size="20" />
  50. </template>
  51. </NcCheckboxRadioSwitch>
  52. <NcCheckboxRadioSwitch v-if="allowsFileDrop"
  53. data-cy-files-sharing-share-permissions-bundle="file-drop"
  54. :button-variant="true"
  55. :checked.sync="sharingPermission"
  56. :value="bundledPermissions.FILE_DROP.toString()"
  57. name="sharing_permission_radio"
  58. type="radio"
  59. button-variant-grouped="vertical"
  60. @update:checked="toggleCustomPermissions">
  61. {{ t('files_sharing', 'File drop') }}
  62. <small class="subline">{{ t('files_sharing', 'Upload only') }}</small>
  63. <template #icon>
  64. <UploadIcon :size="20" />
  65. </template>
  66. </NcCheckboxRadioSwitch>
  67. <NcCheckboxRadioSwitch :button-variant="true"
  68. data-cy-files-sharing-share-permissions-bundle="custom"
  69. :checked.sync="sharingPermission"
  70. :value="'custom'"
  71. name="sharing_permission_radio"
  72. type="radio"
  73. button-variant-grouped="vertical"
  74. @update:checked="expandCustomPermissions">
  75. {{ t('files_sharing', 'Custom permissions') }}
  76. <small class="subline">{{ customPermissionsList }}</small>
  77. <template #icon>
  78. <DotsHorizontalIcon :size="20" />
  79. </template>
  80. </NcCheckboxRadioSwitch>
  81. </div>
  82. </div>
  83. <div class="sharingTabDetailsView__advanced-control">
  84. <NcButton id="advancedSectionAccordionAdvancedControl"
  85. type="tertiary"
  86. alignment="end-reverse"
  87. aria-controls="advancedSectionAccordionAdvanced"
  88. :aria-expanded="advancedControlExpandedValue"
  89. @click="advancedSectionAccordionExpanded = !advancedSectionAccordionExpanded">
  90. {{ t('files_sharing', 'Advanced settings') }}
  91. <template #icon>
  92. <MenuDownIcon v-if="!advancedSectionAccordionExpanded" />
  93. <MenuUpIcon v-else />
  94. </template>
  95. </NcButton>
  96. </div>
  97. <div v-if="advancedSectionAccordionExpanded"
  98. id="advancedSectionAccordionAdvanced"
  99. class="sharingTabDetailsView__advanced"
  100. aria-labelledby="advancedSectionAccordionAdvancedControl"
  101. role="region">
  102. <section>
  103. <NcInputField v-if="isPublicShare"
  104. autocomplete="off"
  105. :label="t('files_sharing', 'Share label')"
  106. :value.sync="share.label" />
  107. <template v-if="isPublicShare">
  108. <NcCheckboxRadioSwitch :checked.sync="isPasswordProtected" :disabled="isPasswordEnforced">
  109. {{ t('files_sharing', 'Set password') }}
  110. </NcCheckboxRadioSwitch>
  111. <NcPasswordField v-if="isPasswordProtected"
  112. autocomplete="new-password"
  113. :value="hasUnsavedPassword ? share.newPassword : ''"
  114. :error="passwordError"
  115. :helper-text="errorPasswordLabel"
  116. :required="isPasswordEnforced"
  117. :label="t('files_sharing', 'Password')"
  118. @update:value="onPasswordChange" />
  119. <!-- Migrate icons and remote -> icon="icon-info"-->
  120. <span v-if="isEmailShareType && passwordExpirationTime" icon="icon-info">
  121. {{ t('files_sharing', 'Password expires {passwordExpirationTime}', { passwordExpirationTime }) }}
  122. </span>
  123. <span v-else-if="isEmailShareType && passwordExpirationTime !== null" icon="icon-error">
  124. {{ t('files_sharing', 'Password expired') }}
  125. </span>
  126. </template>
  127. <NcCheckboxRadioSwitch v-if="canTogglePasswordProtectedByTalkAvailable"
  128. :checked.sync="isPasswordProtectedByTalk"
  129. @update:checked="onPasswordProtectedByTalkChange">
  130. {{ t('files_sharing', 'Video verification') }}
  131. </NcCheckboxRadioSwitch>
  132. <NcCheckboxRadioSwitch :checked.sync="hasExpirationDate" :disabled="isExpiryDateEnforced">
  133. {{ isExpiryDateEnforced
  134. ? t('files_sharing', 'Expiration date (enforced)')
  135. : t('files_sharing', 'Set expiration date') }}
  136. </NcCheckboxRadioSwitch>
  137. <NcDateTimePickerNative v-if="hasExpirationDate"
  138. id="share-date-picker"
  139. :value="new Date(share.expireDate ?? dateTomorrow)"
  140. :min="dateTomorrow"
  141. :max="maxExpirationDateEnforced"
  142. :hide-label="true"
  143. :placeholder="t('files_sharing', 'Expiration date')"
  144. type="date"
  145. @input="onExpirationChange" />
  146. <NcCheckboxRadioSwitch v-if="isPublicShare"
  147. :disabled="canChangeHideDownload"
  148. :checked.sync="share.hideDownload"
  149. @update:checked="queueUpdate('hideDownload')">
  150. {{ t('files_sharing', 'Hide download') }}
  151. </NcCheckboxRadioSwitch>
  152. <NcCheckboxRadioSwitch v-if="!isPublicShare"
  153. :disabled="!canSetDownload"
  154. :checked.sync="canDownload"
  155. data-cy-files-sharing-share-permissions-checkbox="download">
  156. {{ t('files_sharing', 'Allow download') }}
  157. </NcCheckboxRadioSwitch>
  158. <NcCheckboxRadioSwitch :checked.sync="writeNoteToRecipientIsChecked">
  159. {{ t('files_sharing', 'Note to recipient') }}
  160. </NcCheckboxRadioSwitch>
  161. <template v-if="writeNoteToRecipientIsChecked">
  162. <label for="share-note-textarea">
  163. {{ t('files_sharing', 'Enter a note for the share recipient') }}
  164. </label>
  165. <textarea id="share-note-textarea" :value="share.note" @input="share.note = $event.target.value" />
  166. </template>
  167. <ExternalShareAction v-for="action in externalLinkActions"
  168. :id="action.id"
  169. ref="externalLinkActions"
  170. :key="action.id"
  171. :action="action"
  172. :file-info="fileInfo"
  173. :share="share" />
  174. <NcCheckboxRadioSwitch :checked.sync="setCustomPermissions">
  175. {{ t('files_sharing', 'Custom permissions') }}
  176. </NcCheckboxRadioSwitch>
  177. <section v-if="setCustomPermissions" class="custom-permissions-group">
  178. <NcCheckboxRadioSwitch :disabled="!allowsFileDrop && share.type === SHARE_TYPES.SHARE_TYPE_LINK"
  179. :checked.sync="hasRead"
  180. data-cy-files-sharing-share-permissions-checkbox="read">
  181. {{ t('files_sharing', 'Read') }}
  182. </NcCheckboxRadioSwitch>
  183. <NcCheckboxRadioSwitch v-if="isFolder"
  184. :disabled="!canSetCreate"
  185. :checked.sync="canCreate"
  186. data-cy-files-sharing-share-permissions-checkbox="create">
  187. {{ t('files_sharing', 'Create') }}
  188. </NcCheckboxRadioSwitch>
  189. <NcCheckboxRadioSwitch :disabled="!canSetEdit"
  190. :checked.sync="canEdit"
  191. data-cy-files-sharing-share-permissions-checkbox="update">
  192. {{ t('files_sharing', 'Edit') }}
  193. </NcCheckboxRadioSwitch>
  194. <NcCheckboxRadioSwitch v-if="config.isResharingAllowed && share.type !== SHARE_TYPES.SHARE_TYPE_LINK"
  195. :disabled="!canSetReshare"
  196. :checked.sync="canReshare"
  197. data-cy-files-sharing-share-permissions-checkbox="share">
  198. {{ t('files_sharing', 'Share') }}
  199. </NcCheckboxRadioSwitch>
  200. <NcCheckboxRadioSwitch :disabled="!canSetDelete"
  201. :checked.sync="canDelete"
  202. data-cy-files-sharing-share-permissions-checkbox="delete">
  203. {{ t('files_sharing', 'Delete') }}
  204. </NcCheckboxRadioSwitch>
  205. </section>
  206. <div class="sharingTabDetailsView__delete">
  207. <NcButton v-if="!isNewShare"
  208. :aria-label="t('files_sharing', 'Delete share')"
  209. :disabled="false"
  210. :readonly="false"
  211. type="tertiary"
  212. @click.prevent="removeShare">
  213. <template #icon>
  214. <CloseIcon :size="16" />
  215. </template>
  216. {{ t('files_sharing', 'Delete share') }}
  217. </NcButton>
  218. </div>
  219. </section>
  220. </div>
  221. </div>
  222. <div class="sharingTabDetailsView__footer">
  223. <div class="button-group">
  224. <NcButton data-cy-files-sharing-share-editor-action="cancel"
  225. @click="$emit('close-sharing-details')">
  226. {{ t('files_sharing', 'Cancel') }}
  227. </NcButton>
  228. <NcButton type="primary"
  229. data-cy-files-sharing-share-editor-action="save"
  230. @click="saveShare">
  231. {{ shareButtonText }}
  232. <template v-if="creating" #icon>
  233. <NcLoadingIcon />
  234. </template>
  235. </NcButton>
  236. </div>
  237. </div>
  238. </div>
  239. </template>
  240. <script>
  241. import { getLanguage } from '@nextcloud/l10n'
  242. import { Type as ShareType } from '@nextcloud/sharing'
  243. import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
  244. import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
  245. import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
  246. import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
  247. import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js'
  248. import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
  249. import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
  250. import CircleIcon from 'vue-material-design-icons/CircleOutline.vue'
  251. import CloseIcon from 'vue-material-design-icons/Close.vue'
  252. import EditIcon from 'vue-material-design-icons/Pencil.vue'
  253. import EmailIcon from 'vue-material-design-icons/Email.vue'
  254. import LinkIcon from 'vue-material-design-icons/Link.vue'
  255. import GroupIcon from 'vue-material-design-icons/AccountGroup.vue'
  256. import ShareIcon from 'vue-material-design-icons/ShareCircle.vue'
  257. import UserIcon from 'vue-material-design-icons/AccountCircleOutline.vue'
  258. import ViewIcon from 'vue-material-design-icons/Eye.vue'
  259. import UploadIcon from 'vue-material-design-icons/Upload.vue'
  260. import MenuDownIcon from 'vue-material-design-icons/MenuDown.vue'
  261. import MenuUpIcon from 'vue-material-design-icons/MenuUp.vue'
  262. import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
  263. import ExternalShareAction from '../components/ExternalShareAction.vue'
  264. import GeneratePassword from '../utils/GeneratePassword.js'
  265. import Share from '../models/Share.js'
  266. import ShareRequests from '../mixins/ShareRequests.js'
  267. import ShareTypes from '../mixins/ShareTypes.js'
  268. import SharesMixin from '../mixins/SharesMixin.js'
  269. import {
  270. ATOMIC_PERMISSIONS,
  271. BUNDLED_PERMISSIONS,
  272. hasPermissions,
  273. } from '../lib/SharePermissionsToolBox.js'
  274. export default {
  275. name: 'SharingDetailsTab',
  276. components: {
  277. NcAvatar,
  278. NcButton,
  279. NcInputField,
  280. NcPasswordField,
  281. NcDateTimePickerNative,
  282. NcCheckboxRadioSwitch,
  283. NcLoadingIcon,
  284. CloseIcon,
  285. CircleIcon,
  286. EditIcon,
  287. ExternalShareAction,
  288. LinkIcon,
  289. GroupIcon,
  290. ShareIcon,
  291. UserIcon,
  292. UploadIcon,
  293. ViewIcon,
  294. MenuDownIcon,
  295. MenuUpIcon,
  296. DotsHorizontalIcon,
  297. },
  298. mixins: [ShareTypes, ShareRequests, SharesMixin],
  299. props: {
  300. shareRequestValue: {
  301. type: Object,
  302. required: false,
  303. },
  304. fileInfo: {
  305. type: Object,
  306. required: true,
  307. },
  308. share: {
  309. type: Object,
  310. required: true,
  311. },
  312. },
  313. data() {
  314. return {
  315. writeNoteToRecipientIsChecked: false,
  316. sharingPermission: BUNDLED_PERMISSIONS.ALL.toString(),
  317. revertSharingPermission: BUNDLED_PERMISSIONS.ALL.toString(),
  318. setCustomPermissions: false,
  319. passwordError: false,
  320. advancedSectionAccordionExpanded: false,
  321. bundledPermissions: BUNDLED_PERMISSIONS,
  322. isFirstComponentLoad: true,
  323. test: false,
  324. creating: false,
  325. ExternalShareActions: OCA.Sharing.ExternalShareActions.state,
  326. }
  327. },
  328. computed: {
  329. title() {
  330. switch (this.share.type) {
  331. case this.SHARE_TYPES.SHARE_TYPE_USER:
  332. return t('files_sharing', 'Share with {userName}', { userName: this.share.shareWithDisplayName })
  333. case this.SHARE_TYPES.SHARE_TYPE_EMAIL:
  334. return t('files_sharing', 'Share with email {email}', { email: this.share.shareWith })
  335. case this.SHARE_TYPES.SHARE_TYPE_LINK:
  336. return t('files_sharing', 'Share link')
  337. case this.SHARE_TYPES.SHARE_TYPE_GROUP:
  338. return t('files_sharing', 'Share with group')
  339. case this.SHARE_TYPES.SHARE_TYPE_ROOM:
  340. return t('files_sharing', 'Share in conversation')
  341. case this.SHARE_TYPES.SHARE_TYPE_REMOTE: {
  342. const [user, server] = this.share.shareWith.split('@')
  343. return t('files_sharing', 'Share with {user} on remote server {server}', { user, server })
  344. }
  345. case this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP:
  346. return t('files_sharing', 'Share with remote group')
  347. case this.SHARE_TYPES.SHARE_TYPE_GUEST:
  348. return t('files_sharing', 'Share with guest')
  349. default: {
  350. if (this.share.id) {
  351. // Share already exists
  352. return t('files_sharing', 'Update share')
  353. } else {
  354. return t('files_sharing', 'Create share')
  355. }
  356. }
  357. }
  358. },
  359. /**
  360. * Can the sharee edit the shared file ?
  361. */
  362. canEdit: {
  363. get() {
  364. return this.share.hasUpdatePermission
  365. },
  366. set(checked) {
  367. this.updateAtomicPermissions({ isEditChecked: checked })
  368. },
  369. },
  370. /**
  371. * Can the sharee create the shared file ?
  372. */
  373. canCreate: {
  374. get() {
  375. return this.share.hasCreatePermission
  376. },
  377. set(checked) {
  378. this.updateAtomicPermissions({ isCreateChecked: checked })
  379. },
  380. },
  381. /**
  382. * Can the sharee delete the shared file ?
  383. */
  384. canDelete: {
  385. get() {
  386. return this.share.hasDeletePermission
  387. },
  388. set(checked) {
  389. this.updateAtomicPermissions({ isDeleteChecked: checked })
  390. },
  391. },
  392. /**
  393. * Can the sharee reshare the file ?
  394. */
  395. canReshare: {
  396. get() {
  397. return this.share.hasSharePermission
  398. },
  399. set(checked) {
  400. this.updateAtomicPermissions({ isReshareChecked: checked })
  401. },
  402. },
  403. /**
  404. * Can the sharee download files or only view them ?
  405. */
  406. canDownload: {
  407. get() {
  408. return this.share.attributes.find(attr => attr.key === 'download')?.enabled || false
  409. },
  410. set(checked) {
  411. // Find the 'download' attribute and update its value
  412. const downloadAttr = this.share.attributes.find(attr => attr.key === 'download')
  413. if (downloadAttr) {
  414. downloadAttr.enabled = checked
  415. }
  416. },
  417. },
  418. /**
  419. * Is this share readable
  420. * Needed for some federated shares that might have been added from file drop links
  421. */
  422. hasRead: {
  423. get() {
  424. return this.share.hasReadPermission
  425. },
  426. set(checked) {
  427. this.updateAtomicPermissions({ isReadChecked: checked })
  428. },
  429. },
  430. /**
  431. * Does the current share have an expiration date
  432. *
  433. * @return {boolean}
  434. */
  435. hasExpirationDate: {
  436. get() {
  437. return this.isValidShareAttribute(this.share.expireDate)
  438. },
  439. set(enabled) {
  440. this.share.expireDate = enabled
  441. ? this.formatDateToString(this.defaultExpiryDate)
  442. : ''
  443. },
  444. },
  445. /**
  446. * Is the current share password protected ?
  447. *
  448. * @return {boolean}
  449. */
  450. isPasswordProtected: {
  451. get() {
  452. return this.config.enforcePasswordForPublicLink
  453. || !!this.share.password
  454. },
  455. async set(enabled) {
  456. if (enabled) {
  457. this.share.password = await GeneratePassword()
  458. this.$set(this.share, 'newPassword', this.share.password)
  459. } else {
  460. this.share.password = ''
  461. this.$delete(this.share, 'newPassword')
  462. }
  463. },
  464. },
  465. /**
  466. * Is the current share a folder ?
  467. *
  468. * @return {boolean}
  469. */
  470. isFolder() {
  471. return this.fileInfo.type === 'dir'
  472. },
  473. /**
  474. * @return {boolean}
  475. */
  476. isSetDownloadButtonVisible() {
  477. const allowedMimetypes = [
  478. // Office documents
  479. 'application/msword',
  480. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  481. 'application/vnd.ms-powerpoint',
  482. 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  483. 'application/vnd.ms-excel',
  484. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  485. 'application/vnd.oasis.opendocument.text',
  486. 'application/vnd.oasis.opendocument.spreadsheet',
  487. 'application/vnd.oasis.opendocument.presentation',
  488. ]
  489. return this.isFolder || allowedMimetypes.includes(this.fileInfo.mimetype)
  490. },
  491. isPasswordEnforced() {
  492. return this.isPublicShare && this.config.enforcePasswordForPublicLink
  493. },
  494. defaultExpiryDate() {
  495. if ((this.isGroupShare || this.isUserShare) && this.config.isDefaultInternalExpireDateEnabled) {
  496. return new Date(this.config.defaultInternalExpirationDate)
  497. } else if (this.isRemoteShare && this.config.isDefaultRemoteExpireDateEnabled) {
  498. return new Date(this.config.defaultRemoteExpireDateEnabled)
  499. } else if (this.isPublicShare && this.config.isDefaultExpireDateEnabled) {
  500. return new Date(this.config.defaultExpirationDate)
  501. }
  502. return new Date(new Date().setDate(new Date().getDate() + 1))
  503. },
  504. isUserShare() {
  505. return this.share.type === this.SHARE_TYPES.SHARE_TYPE_USER
  506. },
  507. isGroupShare() {
  508. return this.share.type === this.SHARE_TYPES.SHARE_TYPE_GROUP
  509. },
  510. isNewShare() {
  511. return !this.share.id
  512. },
  513. allowsFileDrop() {
  514. if (this.isFolder && this.config.isPublicUploadEnabled) {
  515. if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_LINK || this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
  516. return true
  517. }
  518. }
  519. return false
  520. },
  521. hasFileDropPermissions() {
  522. return this.share.permissions === this.bundledPermissions.FILE_DROP
  523. },
  524. shareButtonText() {
  525. if (this.isNewShare) {
  526. return t('files_sharing', 'Save share')
  527. }
  528. return t('files_sharing', 'Update share')
  529. },
  530. /**
  531. * Can the sharer set whether the sharee can edit the file ?
  532. *
  533. * @return {boolean}
  534. */
  535. canSetEdit() {
  536. // If the owner revoked the permission after the resharer granted it
  537. // the share still has the permission, and the resharer is still
  538. // allowed to revoke it too (but not to grant it again).
  539. return (this.fileInfo.sharePermissions & OC.PERMISSION_UPDATE) || this.canEdit
  540. },
  541. /**
  542. * Can the sharer set whether the sharee can create the file ?
  543. *
  544. * @return {boolean}
  545. */
  546. canSetCreate() {
  547. // If the owner revoked the permission after the resharer granted it
  548. // the share still has the permission, and the resharer is still
  549. // allowed to revoke it too (but not to grant it again).
  550. return (this.fileInfo.sharePermissions & OC.PERMISSION_CREATE) || this.canCreate
  551. },
  552. /**
  553. * Can the sharer set whether the sharee can delete the file ?
  554. *
  555. * @return {boolean}
  556. */
  557. canSetDelete() {
  558. // If the owner revoked the permission after the resharer granted it
  559. // the share still has the permission, and the resharer is still
  560. // allowed to revoke it too (but not to grant it again).
  561. return (this.fileInfo.sharePermissions & OC.PERMISSION_DELETE) || this.canDelete
  562. },
  563. /**
  564. * Can the sharer set whether the sharee can reshare the file ?
  565. *
  566. * @return {boolean}
  567. */
  568. canSetReshare() {
  569. // If the owner revoked the permission after the resharer granted it
  570. // the share still has the permission, and the resharer is still
  571. // allowed to revoke it too (but not to grant it again).
  572. return (this.fileInfo.sharePermissions & OC.PERMISSION_SHARE) || this.canReshare
  573. },
  574. /**
  575. * Can the sharer set whether the sharee can download the file ?
  576. *
  577. * @return {boolean}
  578. */
  579. canSetDownload() {
  580. // If the owner revoked the permission after the resharer granted it
  581. // the share still has the permission, and the resharer is still
  582. // allowed to revoke it too (but not to grant it again).
  583. return (this.fileInfo.canDownload() || this.canDownload)
  584. },
  585. // if newPassword exists, but is empty, it means
  586. // the user deleted the original password
  587. hasUnsavedPassword() {
  588. return this.share.newPassword !== undefined
  589. },
  590. passwordExpirationTime() {
  591. if (!this.isValidShareAttribute(this.share.passwordExpirationTime)) {
  592. return null
  593. }
  594. const expirationTime = moment(this.share.passwordExpirationTime)
  595. if (expirationTime.diff(moment()) < 0) {
  596. return false
  597. }
  598. return expirationTime.fromNow()
  599. },
  600. /**
  601. * Is Talk enabled?
  602. *
  603. * @return {boolean}
  604. */
  605. isTalkEnabled() {
  606. return OC.appswebroots.spreed !== undefined
  607. },
  608. /**
  609. * Is it possible to protect the password by Talk?
  610. *
  611. * @return {boolean}
  612. */
  613. isPasswordProtectedByTalkAvailable() {
  614. return this.isPasswordProtected && this.isTalkEnabled
  615. },
  616. /**
  617. * Is the current share password protected by Talk?
  618. *
  619. * @return {boolean}
  620. */
  621. isPasswordProtectedByTalk: {
  622. get() {
  623. return this.share.sendPasswordByTalk
  624. },
  625. async set(enabled) {
  626. this.share.sendPasswordByTalk = enabled
  627. },
  628. },
  629. /**
  630. * Is the current share an email share ?
  631. *
  632. * @return {boolean}
  633. */
  634. isEmailShareType() {
  635. return this.share
  636. ? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL
  637. : false
  638. },
  639. canTogglePasswordProtectedByTalkAvailable() {
  640. if (!this.isPublicShare || !this.isPasswordProtected) {
  641. // Makes no sense
  642. return false
  643. } else if (this.isEmailShareType && !this.hasUnsavedPassword) {
  644. // For email shares we need a new password in order to enable or
  645. // disable
  646. return false
  647. }
  648. // Is Talk enabled?
  649. return OC.appswebroots.spreed !== undefined
  650. },
  651. canChangeHideDownload() {
  652. const hasDisabledDownload = (shareAttribute) => shareAttribute.key === 'download' && shareAttribute.scope === 'permissions' && shareAttribute.enabled === false
  653. return this.fileInfo.shareAttributes.some(hasDisabledDownload)
  654. },
  655. customPermissionsList() {
  656. // Key order will be different, because ATOMIC_PERMISSIONS are numbers
  657. const translatedPermissions = {
  658. [ATOMIC_PERMISSIONS.READ]: this.t('files_sharing', 'Read'),
  659. [ATOMIC_PERMISSIONS.CREATE]: this.t('files_sharing', 'Create'),
  660. [ATOMIC_PERMISSIONS.UPDATE]: this.t('files_sharing', 'Edit'),
  661. [ATOMIC_PERMISSIONS.SHARE]: this.t('files_sharing', 'Share'),
  662. [ATOMIC_PERMISSIONS.DELETE]: this.t('files_sharing', 'Delete'),
  663. }
  664. return [ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.CREATE, ATOMIC_PERMISSIONS.UPDATE, ATOMIC_PERMISSIONS.SHARE, ATOMIC_PERMISSIONS.DELETE]
  665. .filter((permission) => hasPermissions(this.share.permissions, permission))
  666. .map((permission, index) => index === 0
  667. ? translatedPermissions[permission]
  668. : translatedPermissions[permission].toLocaleLowerCase(getLanguage()))
  669. .join(', ')
  670. },
  671. advancedControlExpandedValue() {
  672. return this.advancedSectionAccordionExpanded ? 'true' : 'false'
  673. },
  674. errorPasswordLabel() {
  675. if (this.passwordError) {
  676. return t('files_sharing', "Password field can't be empty")
  677. }
  678. return undefined
  679. },
  680. /**
  681. * Additional actions for the menu
  682. *
  683. * @return {Array}
  684. */
  685. externalLinkActions() {
  686. const filterValidAction = (action) => (action.shareType.includes(ShareType.SHARE_TYPE_LINK) || action.shareType.includes(ShareType.SHARE_TYPE_EMAIL)) && action.advanced
  687. // filter only the advanced registered actions for said link
  688. return this.ExternalShareActions.actions
  689. .filter(filterValidAction)
  690. },
  691. },
  692. watch: {
  693. setCustomPermissions(isChecked) {
  694. if (isChecked) {
  695. this.sharingPermission = 'custom'
  696. } else {
  697. this.sharingPermission = this.revertSharingPermission
  698. }
  699. },
  700. },
  701. beforeMount() {
  702. this.initializePermissions()
  703. this.initializeAttributes()
  704. console.debug('shareSentIn', this.share)
  705. console.debug('config', this.config)
  706. },
  707. mounted() {
  708. this.$refs.quickPermissions?.querySelector('input:checked')?.focus()
  709. },
  710. methods: {
  711. updateAtomicPermissions({
  712. isReadChecked = this.hasRead,
  713. isEditChecked = this.canEdit,
  714. isCreateChecked = this.canCreate,
  715. isDeleteChecked = this.canDelete,
  716. isReshareChecked = this.canReshare,
  717. } = {}) {
  718. // calc permissions if checked
  719. const permissions = 0
  720. | (isReadChecked ? ATOMIC_PERMISSIONS.READ : 0)
  721. | (isCreateChecked ? ATOMIC_PERMISSIONS.CREATE : 0)
  722. | (isDeleteChecked ? ATOMIC_PERMISSIONS.DELETE : 0)
  723. | (isEditChecked ? ATOMIC_PERMISSIONS.UPDATE : 0)
  724. | (isReshareChecked ? ATOMIC_PERMISSIONS.SHARE : 0)
  725. this.share.permissions = permissions
  726. },
  727. expandCustomPermissions() {
  728. if (!this.advancedSectionAccordionExpanded) {
  729. this.advancedSectionAccordionExpanded = true
  730. }
  731. this.toggleCustomPermissions()
  732. },
  733. toggleCustomPermissions(selectedPermission) {
  734. const isCustomPermissions = this.sharingPermission === 'custom'
  735. this.revertSharingPermission = !isCustomPermissions ? selectedPermission : 'custom'
  736. this.setCustomPermissions = isCustomPermissions
  737. },
  738. async initializeAttributes() {
  739. if (this.isNewShare) {
  740. if (this.isPasswordEnforced && this.isPublicShare) {
  741. this.$set(this.share, 'newPassword', await GeneratePassword())
  742. this.advancedSectionAccordionExpanded = true
  743. }
  744. /* Set default expiration dates if configured */
  745. if (this.isPublicShare && this.config.isDefaultExpireDateEnabled) {
  746. this.share.expireDate = this.config.defaultExpirationDate.toDateString()
  747. } else if (this.isRemoteShare && this.config.isDefaultRemoteExpireDateEnabled) {
  748. this.share.expireDate = this.config.defaultRemoteExpirationDateString.toDateString()
  749. } else if (this.config.isDefaultInternalExpireDateEnabled) {
  750. this.share.expireDate = this.config.defaultInternalExpirationDate.toDateString()
  751. }
  752. if (this.isValidShareAttribute(this.share.expireDate)) {
  753. this.advancedSectionAccordionExpanded = true
  754. }
  755. return
  756. }
  757. // If there is an enforced expiry date, then existing shares created before enforcement
  758. // have no expiry date, hence we set it here.
  759. if (!this.isValidShareAttribute(this.share.expireDate) && this.isExpiryDateEnforced) {
  760. this.hasExpirationDate = true
  761. }
  762. if (
  763. this.isValidShareAttribute(this.share.password)
  764. || this.isValidShareAttribute(this.share.expireDate)
  765. || this.isValidShareAttribute(this.share.label)
  766. ) {
  767. this.advancedSectionAccordionExpanded = true
  768. }
  769. },
  770. handleShareType() {
  771. if ('shareType' in this.share) {
  772. this.share.type = this.share.shareType
  773. } else if (this.share.share_type) {
  774. this.share.type = this.share.share_type
  775. }
  776. },
  777. handleDefaultPermissions() {
  778. if (this.isNewShare) {
  779. const defaultPermissions = this.config.defaultPermissions
  780. if (defaultPermissions === BUNDLED_PERMISSIONS.READ_ONLY || defaultPermissions === BUNDLED_PERMISSIONS.ALL) {
  781. this.sharingPermission = defaultPermissions.toString()
  782. } else {
  783. this.sharingPermission = 'custom'
  784. this.share.permissions = defaultPermissions
  785. this.advancedSectionAccordionExpanded = true
  786. this.setCustomPermissions = true
  787. }
  788. }
  789. },
  790. handleCustomPermissions() {
  791. if (!this.isNewShare && (this.hasCustomPermissions || this.share.setCustomPermissions)) {
  792. this.sharingPermission = 'custom'
  793. this.advancedSectionAccordionExpanded = true
  794. this.setCustomPermissions = true
  795. } else if (this.share.permissions) {
  796. this.sharingPermission = this.share.permissions.toString()
  797. }
  798. },
  799. initializePermissions() {
  800. this.handleShareType()
  801. this.handleDefaultPermissions()
  802. this.handleCustomPermissions()
  803. },
  804. async saveShare() {
  805. const permissionsAndAttributes = ['permissions', 'attributes', 'note', 'expireDate']
  806. const publicShareAttributes = ['label', 'password', 'hideDownload']
  807. if (this.isPublicShare) {
  808. permissionsAndAttributes.push(...publicShareAttributes)
  809. }
  810. const sharePermissionsSet = parseInt(this.sharingPermission)
  811. if (this.setCustomPermissions) {
  812. this.updateAtomicPermissions()
  813. } else {
  814. this.share.permissions = sharePermissionsSet
  815. }
  816. if (!this.isFolder && this.share.permissions === BUNDLED_PERMISSIONS.ALL) {
  817. // It's not possible to create an existing file.
  818. this.share.permissions = BUNDLED_PERMISSIONS.ALL_FILE
  819. }
  820. if (!this.writeNoteToRecipientIsChecked) {
  821. this.share.note = ''
  822. }
  823. if (this.isPasswordProtected) {
  824. if (this.hasUnsavedPassword && this.isValidShareAttribute(this.share.newPassword)) {
  825. this.share.password = this.share.newPassword
  826. this.$delete(this.share, 'newPassword')
  827. } else if (this.isPasswordEnforced && !this.isValidShareAttribute(this.share.password)) {
  828. this.passwordError = true
  829. }
  830. } else {
  831. this.share.password = ''
  832. }
  833. if (!this.hasExpirationDate) {
  834. this.share.expireDate = ''
  835. }
  836. if (this.isNewShare) {
  837. const incomingShare = {
  838. permissions: this.share.permissions,
  839. shareType: this.share.type,
  840. shareWith: this.share.shareWith,
  841. attributes: this.share.attributes,
  842. note: this.share.note,
  843. fileInfo: this.fileInfo,
  844. }
  845. incomingShare.expireDate = this.hasExpirationDate ? this.share.expireDate : ''
  846. if (this.isPasswordProtected) {
  847. incomingShare.password = this.share.password
  848. }
  849. this.creating = true
  850. const share = await this.addShare(incomingShare, this.fileInfo)
  851. this.creating = false
  852. this.share = share
  853. this.$emit('add:share', this.share)
  854. } else {
  855. this.queueUpdate(...permissionsAndAttributes)
  856. }
  857. if (this.$refs.externalLinkActions?.length > 0) {
  858. await Promise.allSettled(this.$refs.externalLinkActions.map((action) => {
  859. if (typeof action.$children.at(0)?.onSave !== 'function') {
  860. return Promise.resolve()
  861. }
  862. return action.$children.at(0)?.onSave?.()
  863. }))
  864. }
  865. this.$emit('close-sharing-details')
  866. },
  867. /**
  868. * Process the new share request
  869. *
  870. * @param {Share} share incoming share object
  871. * @param {object} fileInfo file data
  872. */
  873. async addShare(share, fileInfo) {
  874. console.debug('Adding a new share from the input for', share)
  875. try {
  876. const path = (fileInfo.path + '/' + fileInfo.name).replace('//', '/')
  877. const resultingShare = await this.createShare({
  878. path,
  879. shareType: share.shareType,
  880. shareWith: share.shareWith,
  881. permissions: share.permissions,
  882. expireDate: share.expireDate,
  883. attributes: JSON.stringify(share.attributes),
  884. ...(share.note ? { note: share.note } : {}),
  885. ...(share.password ? { password: share.password } : {}),
  886. })
  887. return resultingShare
  888. } catch (error) {
  889. console.error('Error while adding new share', error)
  890. } finally {
  891. // this.loading = false // No loader here yet
  892. }
  893. },
  894. async removeShare() {
  895. await this.onDelete()
  896. this.$emit('close-sharing-details')
  897. },
  898. /**
  899. * Update newPassword values
  900. * of share. If password is set but not newPassword
  901. * then the user did not changed the password
  902. * If both co-exists, the password have changed and
  903. * we show it in plain text.
  904. * Then on submit (or menu close), we sync it.
  905. *
  906. * @param {string} password the changed password
  907. */
  908. onPasswordChange(password) {
  909. this.passwordError = !this.isValidShareAttribute(password)
  910. this.$set(this.share, 'newPassword', password)
  911. },
  912. /**
  913. * Update the password along with "sendPasswordByTalk".
  914. *
  915. * If the password was modified the new password is sent; otherwise
  916. * updating a mail share would fail, as in that case it is required that
  917. * a new password is set when enabling or disabling
  918. * "sendPasswordByTalk".
  919. */
  920. onPasswordProtectedByTalkChange() {
  921. if (this.hasUnsavedPassword) {
  922. this.share.password = this.share.newPassword.trim()
  923. }
  924. this.queueUpdate('sendPasswordByTalk', 'password')
  925. },
  926. isValidShareAttribute(value) {
  927. if ([null, undefined].includes(value)) {
  928. return false
  929. }
  930. if (!(value.trim().length > 0)) {
  931. return false
  932. }
  933. return true
  934. },
  935. getShareTypeIcon(type) {
  936. switch (type) {
  937. case this.SHARE_TYPES.SHARE_TYPE_LINK:
  938. return LinkIcon
  939. case this.SHARE_TYPES.SHARE_TYPE_GUEST:
  940. return UserIcon
  941. case this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP:
  942. case this.SHARE_TYPES.SHARE_TYPE_GROUP:
  943. return GroupIcon
  944. case this.SHARE_TYPES.SHARE_TYPE_EMAIL:
  945. return EmailIcon
  946. case this.SHARE_TYPES.SHARE_TYPE_CIRCLE:
  947. return CircleIcon
  948. case this.SHARE_TYPES.SHARE_TYPE_ROOM:
  949. return ShareIcon
  950. case this.SHARE_TYPES.SHARE_TYPE_DECK:
  951. return ShareIcon
  952. case this.SHARE_TYPES.SHARE_TYPE_SCIENCEMESH:
  953. return ShareIcon
  954. default:
  955. return null // Or a default icon component if needed
  956. }
  957. },
  958. },
  959. }
  960. </script>
  961. <style lang="scss" scoped>
  962. .sharingTabDetailsView {
  963. display: flex;
  964. flex-direction: column;
  965. width: 100%;
  966. margin: 0 auto;
  967. position: relative;
  968. height: 100%;
  969. overflow: hidden;
  970. &__header {
  971. display: flex;
  972. align-items: center;
  973. box-sizing: border-box;
  974. margin: 0.2em;
  975. span {
  976. display: flex;
  977. align-items: center;
  978. h1 {
  979. font-size: 15px;
  980. padding-left: 0.3em;
  981. }
  982. }
  983. }
  984. &__wrapper {
  985. position: relative;
  986. overflow: scroll;
  987. flex-shrink: 1;
  988. padding: 4px;
  989. padding-right: 12px;
  990. }
  991. &__quick-permissions {
  992. display: flex;
  993. justify-content: center;
  994. width: 100%;
  995. margin: 0 auto;
  996. border-radius: 0;
  997. div {
  998. width: 100%;
  999. span {
  1000. width: 100%;
  1001. span:nth-child(1) {
  1002. align-items: center;
  1003. justify-content: center;
  1004. padding: 0.1em;
  1005. }
  1006. ::v-deep label {
  1007. span {
  1008. display: flex;
  1009. flex-direction: column;
  1010. }
  1011. }
  1012. /* Target component based style in NcCheckboxRadioSwitch slot content*/
  1013. :deep(span.checkbox-content__text.checkbox-radio-switch__text) {
  1014. flex-wrap: wrap;
  1015. .subline {
  1016. display: block;
  1017. flex-basis: 100%;
  1018. }
  1019. }
  1020. }
  1021. }
  1022. }
  1023. &__advanced-control {
  1024. width: 100%;
  1025. button {
  1026. margin-top: 0.5em;
  1027. }
  1028. }
  1029. &__advanced {
  1030. width: 100%;
  1031. margin-bottom: 0.5em;
  1032. text-align: left;
  1033. padding-left: 0;
  1034. section {
  1035. textarea,
  1036. div.mx-datepicker {
  1037. width: 100%;
  1038. }
  1039. textarea {
  1040. height: 80px;
  1041. margin: 0;
  1042. }
  1043. /*
  1044. The following style is applied out of the component's scope
  1045. to remove padding from the label.checkbox-radio-switch__label,
  1046. which is used to group radio checkbox items. The use of ::v-deep
  1047. ensures that the padding is modified without being affected by
  1048. the component's scoping.
  1049. Without this achieving left alignment for the checkboxes would not
  1050. be possible.
  1051. */
  1052. span {
  1053. ::v-deep label {
  1054. padding-left: 0 !important;
  1055. background-color: initial !important;
  1056. border: none !important;
  1057. }
  1058. }
  1059. section.custom-permissions-group {
  1060. padding-left: 1.5em;
  1061. }
  1062. }
  1063. }
  1064. &__delete {
  1065. >button:first-child {
  1066. color: rgb(223, 7, 7);
  1067. }
  1068. }
  1069. &__footer {
  1070. width: 100%;
  1071. display: flex;
  1072. position: sticky;
  1073. bottom: 0;
  1074. flex-direction: column;
  1075. justify-content: space-between;
  1076. align-items: flex-start;
  1077. background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--color-main-background));
  1078. .button-group {
  1079. display: flex;
  1080. justify-content: space-between;
  1081. width: 100%;
  1082. margin-top: 16px;
  1083. button {
  1084. margin-left: 16px;
  1085. &:first-child {
  1086. margin-left: 0;
  1087. }
  1088. }
  1089. }
  1090. }
  1091. }
  1092. </style>