nextcloud-server/apps/files_sharing/src/components/SharingEntryLink.vue

964 lines
27 KiB
Vue

<!--
- SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<li :class="{ 'sharing-entry--share': share }"
class="sharing-entry sharing-entry__link">
<NcAvatar :is-no-user="true"
:icon-class="isEmailShareType ? 'avatar-link-share icon-mail-white' : 'avatar-link-share icon-public-white'"
class="sharing-entry__avatar" />
<div class="sharing-entry__summary">
<div class="sharing-entry__desc">
<span class="sharing-entry__title" :title="title">
{{ title }}
</span>
<p v-if="subtitle">
{{ subtitle }}
</p>
<SharingEntryQuickShareSelect v-if="share && share.permissions !== undefined"
:share="share"
:file-info="fileInfo"
@open-sharing-details="openShareDetailsForCustomSettings(share)" />
</div>
<!-- clipboard -->
<NcActions v-if="share && (!isEmailShareType || isFileRequest) && share.token" ref="copyButton" class="sharing-entry__copy">
<NcActionButton :aria-label="copyLinkTooltip"
:title="copyLinkTooltip"
:href="shareLink"
@click.prevent="copyLink">
<template #icon>
<CheckIcon v-if="copied && copySuccess"
:size="20"
class="icon-checkmark-color" />
<ClipboardIcon v-else :size="20" />
</template>
</NcActionButton>
</NcActions>
</div>
<!-- pending actions -->
<NcActions v-if="!pending && pendingDataIsMissing"
class="sharing-entry__actions"
:aria-label="actionsTooltip"
menu-align="right"
:open.sync="open"
@close="onCancel">
<!-- pending data menu -->
<NcActionText v-if="errors.pending"
class="error">
<template #icon>
<ErrorIcon :size="20" />
</template>
{{ errors.pending }}
</NcActionText>
<NcActionText v-else icon="icon-info">
{{ t('files_sharing', 'Please enter the following required information before creating the share') }}
</NcActionText>
<!-- password -->
<NcActionCheckbox v-if="pendingPassword"
:checked.sync="isPasswordProtected"
:disabled="config.enforcePasswordForPublicLink || saving"
class="share-link-password-checkbox"
@uncheck="onPasswordDisable">
{{ config.enforcePasswordForPublicLink ? t('files_sharing', 'Password protection (enforced)') : t('files_sharing', 'Password protection') }}
</NcActionCheckbox>
<NcActionInput v-if="pendingEnforcedPassword || share.password"
class="share-link-password"
:label="t('files_sharing', 'Enter a password')"
:value.sync="share.password"
:disabled="saving"
:required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink"
:minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength"
autocomplete="new-password"
@submit="onNewLinkShare">
<template #icon>
<LockIcon :size="20" />
</template>
</NcActionInput>
<NcActionCheckbox v-if="hasDefaultExpirationDate"
:checked.sync="defaultExpirationDateEnabled"
:disabled="pendingEnforcedExpirationDate || saving"
class="share-link-expiration-date-checkbox"
@change="onDefaultExpirationDateEnabledChange">
{{ config.enforcePasswordForPublicLink ? t('files_sharing', 'Enable link expiration (enforced)') : t('files_sharing', 'Enable link expiration') }}
</NcActionCheckbox>
<!-- expiration date -->
<NcActionInput v-if="(hasDefaultExpirationDate || pendingEnforcedExpirationDate) && defaultExpirationDateEnabled"
class="share-link-expire-date"
:label="pendingEnforcedExpirationDate ? t('files_sharing', 'Enter expiration date (enforced)') : t('files_sharing', 'Enter expiration date')"
:disabled="saving"
:is-native-picker="true"
:hide-label="true"
:value="new Date(share.expireDate)"
type="date"
:min="dateTomorrow"
:max="maxExpirationDateEnforced"
@input="onExpirationChange /* let's not submit when picked, the user might want to still edit or copy the password */">
<template #icon>
<IconCalendarBlank :size="20" />
</template>
</NcActionInput>
<NcActionButton @click.prevent.stop="onNewLinkShare">
<template #icon>
<CheckIcon :size="20" />
</template>
{{ t('files_sharing', 'Create share') }}
</NcActionButton>
<NcActionButton @click.prevent.stop="onCancel">
<template #icon>
<CloseIcon :size="20" />
</template>
{{ t('files_sharing', 'Cancel') }}
</NcActionButton>
</NcActions>
<!-- actions -->
<NcActions v-else-if="!loading"
class="sharing-entry__actions"
:aria-label="actionsTooltip"
menu-align="right"
:open.sync="open"
@close="onMenuClose">
<template v-if="share">
<template v-if="share.canEdit && canReshare">
<NcActionButton :disabled="saving"
:close-after-click="true"
@click.prevent="openSharingDetails">
<template #icon>
<Tune :size="20" />
</template>
{{ t('files_sharing', 'Customize link') }}
</NcActionButton>
</template>
<NcActionButton :close-after-click="true"
@click.prevent="showQRCode = true">
<template #icon>
<IconQr :size="20" />
</template>
{{ t('files_sharing', 'Generate QR code') }}
</NcActionButton>
<NcActionSeparator />
<!-- external actions -->
<ExternalShareAction v-for="action in externalLinkActions"
:id="action.id"
:key="action.id"
:action="action"
:file-info="fileInfo"
:share="share" />
<!-- external legacy sharing via url (social...) -->
<NcActionLink v-for="({ icon, url, name }, actionIndex) in externalLegacyLinkActions"
:key="actionIndex"
:href="url(shareLink)"
:icon="icon"
target="_blank">
{{ name }}
</NcActionLink>
<NcActionButton v-if="!isEmailShareType && canReshare"
class="new-share-link"
@click.prevent.stop="onNewLinkShare">
<template #icon>
<PlusIcon :size="20" />
</template>
{{ t('files_sharing', 'Add another link') }}
</NcActionButton>
<NcActionButton v-if="share.canDelete"
:disabled="saving"
@click.prevent="onDelete">
<template #icon>
<CloseIcon :size="20" />
</template>
{{ t('files_sharing', 'Unshare') }}
</NcActionButton>
</template>
<!-- Create new share -->
<NcActionButton v-else-if="canReshare"
class="new-share-link"
:title="t('files_sharing', 'Create a new share link')"
:aria-label="t('files_sharing', 'Create a new share link')"
:icon="loading ? 'icon-loading-small' : 'icon-add'"
@click.prevent.stop="onNewLinkShare" />
</NcActions>
<!-- loading indicator to replace the menu -->
<div v-else class="icon-loading-small sharing-entry__loading" />
<!-- Modal to open whenever we have a QR code -->
<NcDialog v-if="showQRCode"
size="normal"
:open.sync="showQRCode"
:name="title"
:close-on-click-outside="true"
@close="showQRCode = false">
<div class="qr-code-dialog">
<VueQrcode tag="img"
:value="shareLink"
class="qr-code-dialog__img" />
</div>
</NcDialog>
</li>
</template>
<script>
import { emit } from '@nextcloud/event-bus'
import { generateUrl, getBaseUrl } from '@nextcloud/router'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { ShareType } from '@nextcloud/sharing'
import VueQrcode from '@chenfengyuan/vue-qrcode'
import moment from '@nextcloud/moment'
import Vue from 'vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js'
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js'
import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import Tune from 'vue-material-design-icons/Tune.vue'
import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue'
import IconQr from 'vue-material-design-icons/Qrcode.vue'
import ErrorIcon from 'vue-material-design-icons/Exclamation.vue'
import LockIcon from 'vue-material-design-icons/Lock.vue'
import CheckIcon from 'vue-material-design-icons/CheckBold.vue'
import ClipboardIcon from 'vue-material-design-icons/ContentCopy.vue'
import CloseIcon from 'vue-material-design-icons/Close.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import SharingEntryQuickShareSelect from './SharingEntryQuickShareSelect.vue'
import ExternalShareAction from './ExternalShareAction.vue'
import GeneratePassword from '../utils/GeneratePassword.ts'
import Share from '../models/Share.ts'
import SharesMixin from '../mixins/SharesMixin.js'
import ShareDetails from '../mixins/ShareDetails.js'
import { getLoggerBuilder } from '@nextcloud/logger'
export default {
name: 'SharingEntryLink',
components: {
ExternalShareAction,
NcActions,
NcActionButton,
NcActionCheckbox,
NcActionInput,
NcActionLink,
NcActionText,
NcActionSeparator,
NcAvatar,
NcDialog,
VueQrcode,
Tune,
IconCalendarBlank,
IconQr,
ErrorIcon,
LockIcon,
CheckIcon,
ClipboardIcon,
CloseIcon,
PlusIcon,
SharingEntryQuickShareSelect,
},
mixins: [SharesMixin, ShareDetails],
props: {
canReshare: {
type: Boolean,
default: true,
},
index: {
type: Number,
default: null,
},
},
data() {
return {
shareCreationComplete: false,
copySuccess: true,
copied: false,
defaultExpirationDateEnabled: false,
// Are we waiting for password/expiration date
pending: false,
ExternalLegacyLinkActions: OCA.Sharing.ExternalLinkActions.state,
ExternalShareActions: OCA.Sharing.ExternalShareActions.state,
logger: getLoggerBuilder()
.setApp('files_sharing')
.detectUser()
.build(),
// tracks whether modal should be opened or not
showQRCode: false,
}
},
computed: {
/**
* Link share label
*
* @return {string}
*/
title() {
// if we have a valid existing share (not pending)
if (this.share && this.share.id) {
if (!this.isShareOwner && this.share.ownerDisplayName) {
if (this.isEmailShareType) {
return t('files_sharing', '{shareWith} by {initiator}', {
shareWith: this.share.shareWith,
initiator: this.share.ownerDisplayName,
})
}
return t('files_sharing', 'Shared via link by {initiator}', {
initiator: this.share.ownerDisplayName,
})
}
if (this.share.label && this.share.label.trim() !== '') {
if (this.isEmailShareType) {
if (this.isFileRequest) {
return t('files_sharing', 'File request ({label})', {
label: this.share.label.trim(),
})
}
return t('files_sharing', 'Mail share ({label})', {
label: this.share.label.trim(),
})
}
return t('files_sharing', 'Share link ({label})', {
label: this.share.label.trim(),
})
}
if (this.isEmailShareType) {
if (!this.share.shareWith || this.share.shareWith.trim() === '') {
return this.isFileRequest
? t('files_sharing', 'File request')
: t('files_sharing', 'Mail share')
}
return this.share.shareWith
}
}
if (this.index > 1) {
return t('files_sharing', 'Share link ({index})', { index: this.index })
}
return t('files_sharing', 'Share link')
},
/**
* Show the email on a second line if a label is set for mail shares
*
* @return {string}
*/
subtitle() {
if (this.isEmailShareType
&& this.title !== this.share.shareWith) {
return this.share.shareWith
}
return null
},
/**
* Is the current share password protected ?
*
* @return {boolean}
*/
isPasswordProtected: {
get() {
return this.config.enforcePasswordForPublicLink
|| !!this.share.password
},
async set(enabled) {
// TODO: directly save after generation to make sure the share is always protected
Vue.set(this.share, 'password', enabled ? await GeneratePassword(true) : '')
Vue.set(this.share, 'newPassword', this.share.password)
},
},
passwordExpirationTime() {
if (this.share.passwordExpirationTime === null) {
return null
}
const expirationTime = moment(this.share.passwordExpirationTime)
if (expirationTime.diff(moment()) < 0) {
return false
}
return expirationTime.fromNow()
},
/**
* Is Talk enabled?
*
* @return {boolean}
*/
isTalkEnabled() {
return OC.appswebroots.spreed !== undefined
},
/**
* Is it possible to protect the password by Talk?
*
* @return {boolean}
*/
isPasswordProtectedByTalkAvailable() {
return this.isPasswordProtected && this.isTalkEnabled
},
/**
* Is the current share password protected by Talk?
*
* @return {boolean}
*/
isPasswordProtectedByTalk: {
get() {
return this.share.sendPasswordByTalk
},
async set(enabled) {
this.share.sendPasswordByTalk = enabled
},
},
/**
* Is the current share an email share ?
*
* @return {boolean}
*/
isEmailShareType() {
return this.share
? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL
: false
},
canTogglePasswordProtectedByTalkAvailable() {
if (!this.isPasswordProtected) {
// Makes no sense
return false
} else if (this.isEmailShareType && !this.hasUnsavedPassword) {
// For email shares we need a new password in order to enable or
// disable
return false
}
// Anything else should be fine
return true
},
/**
* Pending data.
* If the share still doesn't have an id, it is not synced
* Therefore this is still not valid and requires user input
*
* @return {boolean}
*/
pendingDataIsMissing() {
return this.pendingPassword || this.pendingEnforcedPassword || this.pendingEnforcedExpirationDate
},
pendingPassword() {
return this.config.enableLinkPasswordByDefault && this.isPendingShare
},
pendingEnforcedPassword() {
return this.config.enforcePasswordForPublicLink && this.isPendingShare
},
pendingEnforcedExpirationDate() {
return this.config.isDefaultExpireDateEnforced && this.isPendingShare
},
hasDefaultExpirationDate() {
return (this.config.defaultExpirationDate instanceof Date || !isNaN(new Date(this.config.defaultExpirationDate).getTime())) && this.isPendingShare
},
isPendingShare() {
return !!(this.share && !this.share.id)
},
shareRequiresReview() {
return this.defaultExpirationDateEnabled || this.config.enableLinkPasswordByDefault
},
sharePolicyHasRequiredProperties() {
return this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced
},
requiredPropertiesMissing() {
// Ensure share exist and the share policy has required properties
if (!this.sharePolicyHasRequiredProperties) {
return false
}
if (!this.share) {
// if no share, we can't tell if properties are missing or not so we assume properties are missing
return true
}
// If share has ID, then this is an incoming link share created from the existing link share
// Hence assume required properties
if (this.share.id) {
return true
}
// Check if either password or expiration date is missing and enforced
const isPasswordMissing = this.config.enforcePasswordForPublicLink && !this.share.password
const isExpireDateMissing = this.config.isDefaultExpireDateEnforced && !this.share.expireDate
return isPasswordMissing || isExpireDateMissing
},
// if newPassword exists, but is empty, it means
// the user deleted the original password
hasUnsavedPassword() {
return this.share.newPassword !== undefined
},
/**
* Return the public share link
*
* @return {string}
*/
shareLink() {
return generateUrl('/s/{token}', { token: this.share.token }, { baseURL: getBaseUrl() })
},
/**
* Tooltip message for actions button
*
* @return {string}
*/
actionsTooltip() {
return t('files_sharing', 'Actions for "{title}"', { title: this.title })
},
/**
* Tooltip message for copy button
*
* @return {string}
*/
copyLinkTooltip() {
if (this.copied) {
if (this.copySuccess) {
return ''
}
return t('files_sharing', 'Cannot copy, please copy the link manually')
}
return t('files_sharing', 'Copy public link of "{title}" to clipboard', { title: this.title })
},
/**
* External additionnai actions for the menu
*
* @deprecated use OCA.Sharing.ExternalShareActions
* @return {Array}
*/
externalLegacyLinkActions() {
return this.ExternalLegacyLinkActions.actions
},
/**
* Additional actions for the menu
*
* @return {Array}
*/
externalLinkActions() {
const filterValidAction = (action) => (action.shareType.includes(ShareType.Link) || action.shareType.includes(ShareType.Email)) && !action.advanced
// filter only the registered actions for said link
return this.ExternalShareActions.actions
.filter(filterValidAction)
},
isPasswordPolicyEnabled() {
return typeof this.config.passwordPolicy === 'object'
},
canChangeHideDownload() {
const hasDisabledDownload = (shareAttribute) => shareAttribute.scope === 'permissions' && shareAttribute.key === 'download' && shareAttribute.value === false
return this.fileInfo.shareAttributes.some(hasDisabledDownload)
},
isFileRequest() {
return this.share.isFileRequest
},
},
mounted() {
if (this.share) {
this.defaultExpirationDateEnabled = this.config.defaultExpirationDate instanceof Date
this.share.expireDate = this.defaultExpirationDateEnabled ? this.formatDateToString(this.config.defaultExpirationDate) : ''
}
},
methods: {
/**
* Create a new share link and append it to the list
*/
async onNewLinkShare() {
this.logger.debug('onNewLinkShare called (with this.share)', this.share)
// do not run again if already loading
if (this.loading) {
return
}
const shareDefaults = {
share_type: ShareType.Link,
}
if (this.config.isDefaultExpireDateEnforced) {
// default is empty string if not set
// expiration is the share object key, not expireDate
shareDefaults.expiration = this.formatDateToString(this.config.defaultExpirationDate)
}
this.logger.debug('Missing required properties?', this.requiredPropertiesMissing)
// Do not push yet if we need a password or an expiration date: show pending menu
// A share would require a review for example is default expiration date is set but not enforced, this allows
// the user to review the share and remove the expiration date if they don't want it
if ((this.sharePolicyHasRequiredProperties && this.requiredPropertiesMissing) || this.shareRequiresReview) {
this.pending = true
this.shareCreationComplete = false
this.logger.info('Share policy requires mandated properties (password)...')
// ELSE, show the pending popovermenu
// if password default or enforced, pre-fill with random one
if (this.config.enableLinkPasswordByDefault || this.config.enforcePasswordForPublicLink) {
shareDefaults.password = await GeneratePassword(true)
}
// create share & close menu
const share = new Share(shareDefaults)
const component = await new Promise(resolve => {
this.$emit('add:share', share, resolve)
})
// open the menu on the
// freshly created share component
this.open = false
this.pending = false
component.open = true
// Nothing is enforced, creating share directly
} else {
// if a share already exists, pushing it
if (this.share && !this.share.id) {
// if the share is valid, create it on the server
if (this.checkShare(this.share)) {
try {
this.logger.info('Sending existing share to server', this.share)
await this.pushNewLinkShare(this.share, true)
this.shareCreationComplete = true
this.logger.info('Share created on server', this.share)
} catch (e) {
this.pending = false
this.logger.error('Error creating share', e)
return false
}
return true
} else {
this.open = true
showError(t('files_sharing', 'Error, please enter proper password and/or expiration date'))
return false
}
}
const share = new Share(shareDefaults)
await this.pushNewLinkShare(share)
this.shareCreationComplete = true
}
},
/**
* Push a new link share to the server
* And update or append to the list
* accordingly
*
* @param {Share} share the new share
* @param {boolean} [update] do we update the current share ?
*/
async pushNewLinkShare(share, update) {
try {
// do nothing if we're already pending creation
if (this.loading) {
return true
}
this.loading = true
this.errors = {}
const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
const options = {
path,
shareType: ShareType.Link,
password: share.password,
expireDate: share.expireDate,
attributes: JSON.stringify(this.fileInfo.shareAttributes),
// we do not allow setting the publicUpload
// before the share creation.
// Todo: We also need to fix the createShare method in
// lib/Controller/ShareAPIController.php to allow file requests
// (currently not supported on create, only update)
}
console.debug('Creating link share with options', options)
const newShare = await this.createShare(options)
this.open = false
this.shareCreationComplete = true
console.debug('Link share created', newShare)
// if share already exists, copy link directly on next tick
let component
if (update) {
component = await new Promise(resolve => {
this.$emit('update:share', newShare, resolve)
})
} else {
// adding new share to the array and copying link to clipboard
// using promise so that we can copy link in the same click function
// and avoid firefox copy permissions issue
component = await new Promise(resolve => {
this.$emit('add:share', newShare, resolve)
})
}
await this.getNode()
emit('files:node:updated', this.node)
// Execute the copy link method
// freshly created share component
// ! somehow does not works on firefox !
if (!this.config.enforcePasswordForPublicLink) {
// Only copy the link when the password was not forced,
// otherwise the user needs to copy/paste the password before finishing the share.
component.copyLink()
}
showSuccess(t('files_sharing', 'Link share created'))
} catch (data) {
const message = data?.response?.data?.ocs?.meta?.message
if (!message) {
showError(t('files_sharing', 'Error while creating the share'))
console.error(data)
return
}
if (message.match(/password/i)) {
this.onSyncError('password', message)
} else if (message.match(/date/i)) {
this.onSyncError('expireDate', message)
} else {
this.onSyncError('pending', message)
}
throw data
} finally {
this.loading = false
this.shareCreationComplete = true
}
},
async copyLink() {
try {
await navigator.clipboard.writeText(this.shareLink)
showSuccess(t('files_sharing', 'Link copied'))
// focus and show the tooltip
this.$refs.copyButton.$el.focus()
this.copySuccess = true
this.copied = true
} catch (error) {
this.copySuccess = false
this.copied = true
console.error(error)
} finally {
setTimeout(() => {
this.copySuccess = false
this.copied = false
}, 4000)
}
},
/**
* Update newPassword values
* of share. If password is set but not newPassword
* then the user did not changed the password
* If both co-exists, the password have changed and
* we show it in plain text.
* Then on submit (or menu close), we sync it.
*
* @param {string} password the changed password
*/
onPasswordChange(password) {
this.$set(this.share, 'newPassword', password)
},
/**
* Uncheck password protection
* We need this method because @update:checked
* is ran simultaneously as @uncheck, so we
* cannot ensure data is up-to-date
*/
onPasswordDisable() {
this.share.password = ''
// reset password state after sync
this.$delete(this.share, 'newPassword')
// only update if valid share.
if (this.share.id) {
this.queueUpdate('password')
}
},
/**
* Menu have been closed or password has been submitted.
* The only property that does not get
* synced automatically is the password
* So let's check if we have an unsaved
* password.
* expireDate is saved on datepicker pick
* or close.
*/
onPasswordSubmit() {
if (this.hasUnsavedPassword) {
this.share.password = this.share.newPassword.trim()
this.queueUpdate('password')
}
},
/**
* Update the password along with "sendPasswordByTalk".
*
* If the password was modified the new password is sent; otherwise
* updating a mail share would fail, as in that case it is required that
* a new password is set when enabling or disabling
* "sendPasswordByTalk".
*/
onPasswordProtectedByTalkChange() {
if (this.hasUnsavedPassword) {
this.share.password = this.share.newPassword.trim()
}
this.queueUpdate('sendPasswordByTalk', 'password')
},
/**
* Save potential changed data on menu close
*/
onMenuClose() {
this.onPasswordSubmit()
this.onNoteSubmit()
},
onDefaultExpirationDateEnabledChange(enabled) {
this.share.expireDate = enabled ? this.formatDateToString(this.config.defaultExpirationDate) : ''
},
/**
* Cancel the share creation
* Used in the pending popover
*/
onCancel() {
// this.share already exists at this point,
// but is incomplete as not pushed to server
// YET. We can safely delete the share :)
if (!this.shareCreationComplete) {
this.$emit('remove:share', this.share)
}
},
},
}
</script>
<style lang="scss" scoped>
.sharing-entry {
display: flex;
align-items: center;
min-height: 44px;
&__summary {
padding: 8px;
padding-inline-start: 10px;
display: flex;
justify-content: space-between;
flex: 1 0;
min-width: 0;
}
&__desc {
display: flex;
flex-direction: column;
line-height: 1.2em;
p {
color: var(--color-text-maxcontrast);
}
&__title {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
&:not(.sharing-entry--share) &__actions {
.new-share-link {
border-top: 1px solid var(--color-border);
}
}
:deep(.avatar-link-share) {
background-color: var(--color-primary-element);
}
.sharing-entry__action--public-upload {
border-bottom: 1px solid var(--color-border);
}
&__loading {
width: 44px;
height: 44px;
margin: 0;
padding: 14px;
margin-inline-start: auto;
}
// put menus to the left
// but only the first one
.action-item {
~.action-item,
~.sharing-entry__loading {
margin-inline-start: 0;
}
}
.icon-checkmark-color {
opacity: 1;
color: var(--color-success);
}
}
// styling for the qr-code container
.qr-code-dialog {
display: flex;
width: 100%;
justify-content: center;
&__img {
width: 100%;
height: auto;
}
}
</style>