diff options
Diffstat (limited to 'apps/files_sharing/src/views/SharingDetailsTab.vue')
-rw-r--r-- | apps/files_sharing/src/views/SharingDetailsTab.vue | 224 |
1 files changed, 140 insertions, 84 deletions
diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue index f50a533eeeb..b3a3b95d92e 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.vue +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -38,7 +38,7 @@ <NcCheckboxRadioSwitch :button-variant="true" data-cy-files-sharing-share-permissions-bundle="upload-edit" :checked.sync="sharingPermission" - :value="bundledPermissions.ALL.toString()" + :value="allPermissions" name="sharing_permission_radio" type="radio" button-variant-grouped="vertical" @@ -115,8 +115,8 @@ :helper-text="t('files_sharing', 'Set the public share link token to something easy to remember or generate a new token. It is not recommended to use a guessable token for shares which contain sensitive information.')" show-trailing-button :trailing-button-label="loadingToken ? t('files_sharing', 'Generating…') : t('files_sharing', 'Generate new token')" - @trailing-button-click="generateNewToken" - :value.sync="share.token"> + :value.sync="share.token" + @trailing-button-click="generateNewToken"> <template #trailing-button-icon> <NcLoadingIcon v-if="loadingToken" /> <Refresh v-else :size="20" /> @@ -128,7 +128,7 @@ </NcCheckboxRadioSwitch> <NcPasswordField v-if="isPasswordProtected" autocomplete="new-password" - :value="hasUnsavedPassword ? share.newPassword : ''" + :value="share.newPassword ?? ''" :error="passwordError" :helper-text="errorPasswordLabel || passwordHint" :required="isPasswordEnforced && isNewShare" @@ -169,7 +169,7 @@ @update:checked="queueUpdate('hideDownload')"> {{ t('files_sharing', 'Hide download') }} </NcCheckboxRadioSwitch> - <NcCheckboxRadioSwitch v-if="!isPublicShare" + <NcCheckboxRadioSwitch v-else :disabled="!canSetDownload" :checked.sync="canDownload" data-cy-files-sharing-share-permissions-checkbox="download"> @@ -183,6 +183,10 @@ :placeholder="t('files_sharing', 'Enter a note for the share recipient')" :value.sync="share.note" /> </template> + <NcCheckboxRadioSwitch v-if="isPublicShare && isFolder" + :checked.sync="showInGridView"> + {{ t('files_sharing', 'Show files in grid view') }} + </NcCheckboxRadioSwitch> <ExternalShareAction v-for="action in externalLinkActions" :id="action.id" ref="externalLinkActions" @@ -222,19 +226,6 @@ {{ t('files_sharing', 'Delete') }} </NcCheckboxRadioSwitch> </section> - <div class="sharingTabDetailsView__delete"> - <NcButton v-if="!isNewShare" - :aria-label="t('files_sharing', 'Delete share')" - :disabled="false" - :readonly="false" - type="tertiary" - @click.prevent="removeShare"> - <template #icon> - <CloseIcon :size="16" /> - </template> - {{ t('files_sharing', 'Delete share') }} - </NcButton> - </div> </section> </div> </div> @@ -245,8 +236,22 @@ @click="cancel"> {{ t('files_sharing', 'Cancel') }} </NcButton> + <div class="sharingTabDetailsView__delete"> + <NcButton v-if="!isNewShare" + :aria-label="t('files_sharing', 'Delete share')" + :disabled="false" + :readonly="false" + variant="tertiary" + @click.prevent="removeShare"> + <template #icon> + <CloseIcon :size="20" /> + </template> + {{ t('files_sharing', 'Delete share') }} + </NcButton> + </div> <NcButton type="primary" data-cy-files-sharing-share-editor-action="save" + :disabled="creating" @click="saveShare"> {{ shareButtonText }} <template v-if="creating" #icon> @@ -265,18 +270,18 @@ import { ShareType } from '@nextcloud/sharing' import { showError } from '@nextcloud/dialogs' import moment from '@nextcloud/moment' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' -import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js' -import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' -import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js' -import NcTextArea from '@nextcloud/vue/dist/Components/NcTextArea.js' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative' +import NcInputField from '@nextcloud/vue/components/NcInputField' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcPasswordField from '@nextcloud/vue/components/NcPasswordField' +import NcTextArea from '@nextcloud/vue/components/NcTextArea' import CircleIcon from 'vue-material-design-icons/CircleOutline.vue' import CloseIcon from 'vue-material-design-icons/Close.vue' -import EditIcon from 'vue-material-design-icons/Pencil.vue' +import EditIcon from 'vue-material-design-icons/PencilOutline.vue' import EmailIcon from 'vue-material-design-icons/Email.vue' import LinkIcon from 'vue-material-design-icons/Link.vue' import GroupIcon from 'vue-material-design-icons/AccountGroup.vue' @@ -368,7 +373,7 @@ export default { title() { switch (this.share.type) { case ShareType.User: - return t('files_sharing', 'Share with {userName}', { userName: this.share.shareWithDisplayName }) + return t('files_sharing', 'Share with {user}', { user: this.share.shareWithDisplayName }) case ShareType.Email: return t('files_sharing', 'Share with email {email}', { email: this.share.shareWith }) case ShareType.Link: @@ -379,6 +384,9 @@ export default { return t('files_sharing', 'Share in conversation') case ShareType.Remote: { const [user, server] = this.share.shareWith.split('@') + if (this.config.showFederatedSharesAsInternal) { + return t('files_sharing', 'Share with {user}', { user }) + } return t('files_sharing', 'Share with {user} on remote server {server}', { user, server }) } case ShareType.RemoteGroup: @@ -395,6 +403,9 @@ export default { } } }, + allPermissions() { + return this.isFolder ? this.bundledPermissions.ALL.toString() : this.bundledPermissions.ALL_FILE.toString() + }, /** * Can the sharee edit the shared file ? */ @@ -439,28 +450,29 @@ export default { this.updateAtomicPermissions({ isReshareChecked: checked }) }, }, + + /** + * Change the default view for public shares from "list" to "grid" + */ + showInGridView: { + get() { + return this.getShareAttribute('config', 'grid_view', false) + }, + /** @param {boolean} value If the default view should be changed to "grid" */ + set(value) { + this.setShareAttribute('config', 'grid_view', value) + }, + }, + /** * Can the sharee download files or only view them ? */ canDownload: { get() { - return this.share.attributes?.find(attr => attr.key === 'download')?.value ?? true + return this.getShareAttribute('permissions', 'download', true) }, set(checked) { - // Find the 'download' attribute and update its value - const downloadAttr = this.share.attributes?.find(attr => attr.key === 'download') - if (downloadAttr) { - downloadAttr.value = checked - } else { - if (this.share.attributes === null) { - this.$set(this.share, 'attributes', []) - } - this.share.attributes.push({ - scope: 'permissions', - key: 'download', - value: checked, - }) - } + this.setShareAttribute('permissions', 'download', checked) }, }, /** @@ -491,26 +503,6 @@ export default { }, }, /** - * Is the current share password protected ? - * - * @return {boolean} - */ - isPasswordProtected: { - get() { - return this.config.enforcePasswordForPublicLink - || !!this.share.password - }, - async set(enabled) { - if (enabled) { - this.share.password = await GeneratePassword(true) - this.$set(this.share, 'newPassword', this.share.password) - } else { - this.share.password = '' - this.$delete(this.share, 'newPassword') - } - }, - }, - /** * Is the current share a folder ? * * @return {boolean} @@ -556,9 +548,6 @@ export default { isGroupShare() { return this.share.type === ShareType.Group }, - isNewShare() { - return !this.share.id - }, allowsFileDrop() { if (this.isFolder && this.config.isPublicUploadEnabled) { if (this.share.type === ShareType.Link || this.share.type === ShareType.Email) { @@ -729,8 +718,15 @@ export default { [ATOMIC_PERMISSIONS.DELETE]: this.t('files_sharing', 'Delete'), } - return [ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.CREATE, ATOMIC_PERMISSIONS.UPDATE, ...(this.resharingIsPossible ? [ATOMIC_PERMISSIONS.SHARE] : []), ATOMIC_PERMISSIONS.DELETE] - .filter((permission) => hasPermissions(this.share.permissions, permission)) + const permissionsList = [ + ATOMIC_PERMISSIONS.READ, + ...(this.isFolder ? [ATOMIC_PERMISSIONS.CREATE] : []), + ATOMIC_PERMISSIONS.UPDATE, + ...(this.resharingIsPossible ? [ATOMIC_PERMISSIONS.SHARE] : []), + ...(this.isFolder ? [ATOMIC_PERMISSIONS.DELETE] : []), + ] + + return permissionsList.filter((permission) => hasPermissions(this.share.permissions, permission)) .map((permission, index) => index === 0 ? translatedPermissions[permission] : translatedPermissions[permission].toLocaleLowerCase(getLanguage())) @@ -741,7 +737,7 @@ export default { }, errorPasswordLabel() { if (this.passwordError) { - return t('files_sharing', "Password field can't be empty") + return t('files_sharing', 'Password field cannot be empty') } return undefined }, @@ -786,6 +782,42 @@ export default { }, methods: { + /** + * Set a share attribute on the current share + * @param {string} scope The attribute scope + * @param {string} key The attribute key + * @param {boolean} value The value + */ + setShareAttribute(scope, key, value) { + if (!this.share.attributes) { + this.$set(this.share, 'attributes', []) + } + + const attribute = this.share.attributes + .find((attr) => attr.scope === scope || attr.key === key) + + if (attribute) { + attribute.value = value + } else { + this.share.attributes.push({ + scope, + key, + value, + }) + } + }, + + /** + * Get the value of a share attribute + * @param {string} scope The attribute scope + * @param {string} key The attribute key + * @param {undefined|boolean} fallback The fallback to return if not found + */ + getShareAttribute(scope, key, fallback = undefined) { + const attribute = this.share.attributes?.find((attr) => attr.scope === scope && attr.key === key) + return attribute?.value ?? fallback + }, + async generateNewToken() { if (this.loadingToken) { return @@ -812,6 +844,13 @@ export default { isReshareChecked = this.canReshare, } = {}) { // calc permissions if checked + + if (!this.isFolder && (isCreateChecked || isDeleteChecked)) { + logger.debug('Ignoring create/delete permissions for file share — only available for folders') + isCreateChecked = false + isDeleteChecked = false + } + const permissions = 0 | (isReadChecked ? ATOMIC_PERMISSIONS.READ : 0) | (isCreateChecked ? ATOMIC_PERMISSIONS.CREATE : 0) @@ -834,7 +873,7 @@ export default { async initializeAttributes() { if (this.isNewShare) { - if (this.isPasswordEnforced && this.isPublicShare) { + if ((this.config.enableLinkPasswordByDefault || this.isPasswordEnforced) && this.isPublicShare) { this.$set(this.share, 'newPassword', await GeneratePassword(true)) this.advancedSectionAccordionExpanded = true } @@ -868,8 +907,9 @@ export default { this.advancedSectionAccordionExpanded = true } - if (this.share.note) { + if (this.isValidShareAttribute(this.share.note)) { this.writeNoteToRecipientIsChecked = true + this.advancedSectionAccordionExpanded = true } }, @@ -935,10 +975,7 @@ export default { this.share.note = '' } if (this.isPasswordProtected) { - if (this.hasUnsavedPassword && this.isValidShareAttribute(this.share.newPassword)) { - this.share.password = this.share.newPassword - this.$delete(this.share, 'newPassword') - } else if (this.isPasswordEnforced && this.isNewShare && !this.isValidShareAttribute(this.share.password)) { + if (this.isPasswordEnforced && this.isNewShare && !this.isValidShareAttribute(this.share.password)) { this.passwordError = true } } else { @@ -962,17 +999,40 @@ export default { incomingShare.expireDate = this.hasExpirationDate ? this.share.expireDate : '' if (this.isPasswordProtected) { - incomingShare.password = this.share.password + incomingShare.password = this.share.newPassword + } + + let share + try { + this.creating = true + share = await this.addShare(incomingShare) + } catch (error) { + this.creating = false + // Error is already handled by ShareRequests mixin + return + } + + // ugly hack to make code work - we need the id to be set but at the same time we need to keep values we want to update + this.share._share.id = share.id + await this.queueUpdate(...permissionsAndAttributes) + // Also a ugly hack to update the updated permissions + for (const prop of permissionsAndAttributes) { + if (prop in share && prop in this.share) { + try { + share[prop] = this.share[prop] + } catch { + share._share[prop] = this.share[prop] + } + } } - this.creating = true - const share = await this.addShare(incomingShare) - this.creating = false this.share = share + this.creating = false this.$emit('add:share', this.share) } else { + // Let's update after creation as some attrs are only available after creation + await this.queueUpdate(...permissionsAndAttributes) this.$emit('update:share', this.share) - this.queueUpdate(...permissionsAndAttributes) } await this.getNode() @@ -1049,10 +1109,6 @@ export default { * "sendPasswordByTalk". */ onPasswordProtectedByTalkChange() { - if (this.hasUnsavedPassword) { - this.share.password = this.share.newPassword.trim() - } - this.queueUpdate('sendPasswordByTalk', 'password') }, isValidShareAttribute(value) { |