diff options
Diffstat (limited to 'apps/files_sharing/src/views/SharingTab.vue')
-rw-r--r-- | apps/files_sharing/src/views/SharingTab.vue | 351 |
1 files changed, 277 insertions, 74 deletions
diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue index 7e58cb6401e..2ed44a4b5ad 100644 --- a/apps/files_sharing/src/views/SharingTab.vue +++ b/apps/files_sharing/src/views/SharingTab.vue @@ -15,8 +15,8 @@ <div v-show="!showSharingDetailsView" class="sharingTab__content"> <!-- shared with me information --> - <ul> - <SharingEntrySimple v-if="isSharedWithMe" v-bind="sharedWithMe" class="sharing-entry__reshare"> + <ul v-if="isSharedWithMe"> + <SharingEntrySimple v-bind="sharedWithMe" class="sharing-entry__reshare"> <template #avatar> <NcAvatar :user="sharedWithMe.user" :display-name="sharedWithMe.displayName" @@ -25,50 +25,123 @@ </SharingEntrySimple> </ul> - <!-- add new share input --> - <SharingInput v-if="!loading" - :can-reshare="canReshare" - :file-info="fileInfo" - :link-shares="linkShares" - :reshare="reshare" - :shares="shares" - @open-sharing-details="toggleShareDetailsView" /> - - <!-- link shares list --> - <SharingLinkList v-if="!loading" - ref="linkShareList" - :can-reshare="canReshare" - :file-info="fileInfo" - :shares="linkShares" - @open-sharing-details="toggleShareDetailsView" /> - - <!-- other shares list --> - <SharingList v-if="!loading" - ref="shareList" - :shares="shares" - :file-info="fileInfo" - @open-sharing-details="toggleShareDetailsView" /> - - <!-- inherited shares --> - <SharingInherited v-if="canReshare && !loading" :file-info="fileInfo" /> - - <!-- internal link copy --> - <SharingEntryInternal :file-info="fileInfo" /> - - <!-- projects --> - <CollectionList v-if="projectsEnabled && fileInfo" - :id="`${fileInfo.id}`" - type="file" - :name="fileInfo.name" /> - </div> - - <!-- additional entries, use it with cautious --> - <div v-for="(section, index) in sections" - v-show="!showSharingDetailsView" - :ref="'section-' + index" - :key="index" - class="sharingTab__additionalContent"> - <component :is="section($refs['section-'+index], fileInfo)" :file-info="fileInfo" /> + <section> + <div class="section-header"> + <h4>{{ t('files_sharing', 'Internal shares') }}</h4> + <NcPopover popup-role="dialog"> + <template #trigger> + <NcButton class="hint-icon" + type="tertiary-no-background" + :aria-label="t('files_sharing', 'Internal shares explanation')"> + <template #icon> + <InfoIcon :size="20" /> + </template> + </NcButton> + </template> + <p class="hint-body"> + {{ internalSharesHelpText }} + </p> + </NcPopover> + </div> + <!-- add new share input --> + <SharingInput v-if="!loading" + :can-reshare="canReshare" + :file-info="fileInfo" + :link-shares="linkShares" + :reshare="reshare" + :shares="shares" + :placeholder="internalShareInputPlaceholder" + @open-sharing-details="toggleShareDetailsView" /> + + <!-- other shares list --> + <SharingList v-if="!loading" + ref="shareList" + :shares="shares" + :file-info="fileInfo" + @open-sharing-details="toggleShareDetailsView" /> + + <!-- inherited shares --> + <SharingInherited v-if="canReshare && !loading" :file-info="fileInfo" /> + + <!-- internal link copy --> + <SharingEntryInternal :file-info="fileInfo" /> + </section> + + <section> + <div class="section-header"> + <h4>{{ t('files_sharing', 'External shares') }}</h4> + <NcPopover popup-role="dialog"> + <template #trigger> + <NcButton class="hint-icon" + type="tertiary-no-background" + :aria-label="t('files_sharing', 'External shares explanation')"> + <template #icon> + <InfoIcon :size="20" /> + </template> + </NcButton> + </template> + <p class="hint-body"> + {{ externalSharesHelpText }} + </p> + </NcPopover> + </div> + <SharingInput v-if="!loading" + :can-reshare="canReshare" + :file-info="fileInfo" + :link-shares="linkShares" + :is-external="true" + :placeholder="externalShareInputPlaceholder" + :reshare="reshare" + :shares="shares" + @open-sharing-details="toggleShareDetailsView" /> + <!-- Non link external shares list --> + <SharingList v-if="!loading" + :shares="externalShares" + :file-info="fileInfo" + @open-sharing-details="toggleShareDetailsView" /> + <!-- link shares list --> + <SharingLinkList v-if="!loading && isLinkSharingAllowed" + ref="linkShareList" + :can-reshare="canReshare" + :file-info="fileInfo" + :shares="linkShares" + @open-sharing-details="toggleShareDetailsView" /> + </section> + + <section v-if="sections.length > 0 && !showSharingDetailsView"> + <div class="section-header"> + <h4>{{ t('files_sharing', 'Additional shares') }}</h4> + <NcPopover popup-role="dialog"> + <template #trigger> + <NcButton class="hint-icon" + type="tertiary-no-background" + :aria-label="t('files_sharing', 'Additional shares explanation')"> + <template #icon> + <InfoIcon :size="20" /> + </template> + </NcButton> + </template> + <p class="hint-body"> + {{ additionalSharesHelpText }} + </p> + </NcPopover> + </div> + <!-- additional entries, use it with cautious --> + <div v-for="(component, index) in sectionComponents" + :key="index" + class="sharingTab__additionalContent"> + <component :is="component" :file-info="fileInfo" /> + </div> + + <!-- projects (deprecated as of NC25 (replaced by related_resources) - see instance config "projects.enabled" ; ignore this / remove it / move into own section) --> + <div v-if="projectsEnabled" + v-show="!showSharingDetailsView && fileInfo" + class="sharingTab__additionalContent"> + <NcCollectionList :id="`${fileInfo.id}`" + type="file" + :name="fileInfo.name" /> + </div> + </section> </div> <!-- share details --> @@ -82,16 +155,26 @@ </template> <script> -import { CollectionList } from 'nextcloud-vue-collections' +import { getCurrentUser } from '@nextcloud/auth' +import { getCapabilities } from '@nextcloud/capabilities' +import { orderBy } from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' import { generateOcsUrl } from '@nextcloud/router' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' +import { ShareType } from '@nextcloud/sharing' + +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcCollectionList from '@nextcloud/vue/components/NcCollectionList' +import NcPopover from '@nextcloud/vue/components/NcPopover' +import InfoIcon from 'vue-material-design-icons/InformationOutline.vue' + import axios from '@nextcloud/axios' -import { loadState } from '@nextcloud/initial-state' +import moment from '@nextcloud/moment' -import Config from '../services/ConfigService.js' import { shareWithTitle } from '../utils/SharedWithMe.js' -import Share from '../models/Share.js' -import ShareTypes from '../mixins/ShareTypes.js' + +import Config from '../services/ConfigService.ts' +import Share from '../models/Share.ts' import SharingEntryInternal from '../components/SharingEntryInternal.vue' import SharingEntrySimple from '../components/SharingEntrySimple.vue' import SharingInput from '../components/SharingInput.vue' @@ -101,12 +184,18 @@ import SharingLinkList from './SharingLinkList.vue' import SharingList from './SharingList.vue' import SharingDetailsTab from './SharingDetailsTab.vue' +import ShareDetails from '../mixins/ShareDetails.js' +import logger from '../services/logger.ts' + export default { name: 'SharingTab', components: { + InfoIcon, NcAvatar, - CollectionList, + NcButton, + NcCollectionList, + NcPopover, SharingEntryInternal, SharingEntrySimple, SharingInherited, @@ -115,8 +204,7 @@ export default { SharingList, SharingDetailsTab, }, - - mixins: [ShareTypes], + mixins: [ShareDetails], data() { return { @@ -133,12 +221,17 @@ export default { sharedWithMe: {}, shares: [], linkShares: [], + externalShares: [], sections: OCA.Sharing.ShareTabSections.getSections(), projectsEnabled: loadState('core', 'projects_enabled', false), showSharingDetailsView: false, shareDetailsData: {}, returnFocusElement: null, + + internalSharesHelpText: t('files_sharing', 'Share files within your organization. Recipients who can already view the file can also use this link for easy access.'), + externalSharesHelpText: t('files_sharing', 'Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID.'), + additionalSharesHelpText: t('files_sharing', 'Shares from apps or other sources which are not included in internal or external shares.'), } }, @@ -149,15 +242,54 @@ export default { * @return {boolean} */ isSharedWithMe() { - return Object.keys(this.sharedWithMe).length > 0 + return !!this.sharedWithMe?.user + }, + + /** + * Is link sharing allowed for the current user? + * + * @return {boolean} + */ + isLinkSharingAllowed() { + const currentUser = getCurrentUser() + if (!currentUser) { + return false + } + + const capabilities = getCapabilities() + const publicSharing = capabilities.files_sharing?.public || {} + return publicSharing.enabled === true }, canReshare() { return !!(this.fileInfo.permissions & OC.PERMISSION_SHARE) || !!(this.reshare && this.reshare.hasSharePermission && this.config.isResharingAllowed) }, - }, + internalShareInputPlaceholder() { + return this.config.showFederatedSharesAsInternal && this.config.isFederationEnabled + // TRANSLATORS: Type as in with a keyboard + ? t('files_sharing', 'Type names, teams, federated cloud IDs') + // TRANSLATORS: Type as in with a keyboard + : t('files_sharing', 'Type names or teams') + }, + + externalShareInputPlaceholder() { + if (!this.isLinkSharingAllowed) { + // TRANSLATORS: Type as in with a keyboard + return this.config.isFederationEnabled ? t('files_sharing', 'Type a federated cloud ID') : '' + } + return !this.config.showFederatedSharesAsInternal && !this.config.isFederationEnabled + // TRANSLATORS: Type as in with a keyboard + ? t('files_sharing', 'Type an email') + // TRANSLATORS: Type as in with a keyboard + : t('files_sharing', 'Type an email or federated cloud ID') + }, + + sectionComponents() { + return this.sections.map((section) => section(undefined, this.fileInfo)) + }, + }, methods: { /** * Update current fileInfo and fetch new data @@ -169,7 +301,6 @@ export default { this.resetState() this.getShares() }, - /** * Get the existing shares infos */ @@ -207,7 +338,7 @@ export default { this.processSharedWithMe(sharedWithMe) this.processShares(shares) } catch (error) { - if (error.response.data?.ocs?.meta?.message) { + if (error?.response?.data?.ocs?.meta?.message) { this.error = error.response.data.ocs.meta.message } else { this.error = t('files_sharing', 'Unable to load the shares list') @@ -240,7 +371,7 @@ export default { updateExpirationSubtitle(share) { const expiration = moment(share.expireDate).unix() this.$set(this.sharedWithMe, 'subtitle', t('files_sharing', 'Expires {relativetime}', { - relativetime: OC.Util.relativeModifiedDate(expiration * 1000), + relativetime: moment(expiration * 1000).fromNow(), })) // share have expired @@ -260,16 +391,41 @@ export default { */ processShares({ data }) { if (data.ocs && data.ocs.data && data.ocs.data.length > 0) { - // create Share objects and sort by newest - const shares = data.ocs.data - .map(share => new Share(share)) - .sort((a, b) => b.createdTime - a.createdTime) - - this.linkShares = shares.filter(share => share.type === this.SHARE_TYPES.SHARE_TYPE_LINK || share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) - this.shares = shares.filter(share => share.type !== this.SHARE_TYPES.SHARE_TYPE_LINK && share.type !== this.SHARE_TYPES.SHARE_TYPE_EMAIL) + const shares = orderBy( + data.ocs.data.map(share => new Share(share)), + [ + // First order by the "share with" label + (share) => share.shareWithDisplayName, + // Then by the label + (share) => share.label, + // And last resort order by createdTime + (share) => share.createdTime, + ], + ) + + for (const share of shares) { + if ([ShareType.Link, ShareType.Email].includes(share.type)) { + this.linkShares.push(share) + } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) { + if (this.config.showFederatedSharesToTrustedServersAsInternal) { + if (share.isTrustedServer) { + this.shares.push(share) + } else { + this.externalShares.push(share) + } + } else if (this.config.showFederatedSharesAsInternal) { + this.shares.push(share) + } else { + this.externalShares.push(share) + } + } else { + this.shares.push(share) + } + } - console.debug('Processed', this.linkShares.length, 'link share(s)') - console.debug('Processed', this.shares.length, 'share(s)') + logger.debug(`Processed ${this.linkShares.length} link share(s)`) + logger.debug(`Processed ${this.shares.length} share(s)`) + logger.debug(`Processed ${this.externalShares.length} external share(s)`) } }, @@ -302,7 +458,7 @@ export default { // interval update this.expirationInterval = setInterval(this.updateExpirationSubtitle, 10000, share) } - } else if (this.fileInfo && this.fileInfo.shareOwnerId !== undefined ? this.fileInfo.shareOwnerId !== OC.currentUser : false) { + } else if (this.fileInfo && this.fileInfo.shareOwnerId !== undefined ? this.fileInfo.shareOwnerId !== getCurrentUser().uid : false) { // Fallback to compare owner and current user. this.sharedWithMe = { displayName: this.fileInfo.shareOwner, @@ -328,8 +484,18 @@ export default { addShare(share, resolve = () => { }) { // only catching share type MAIL as link shares are added differently // meaning: not from the ShareInput - if (share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) { + if (share.type === ShareType.Email) { this.linkShares.unshift(share) + } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) { + if (this.config.showFederatedSharesAsInternal) { + this.shares.unshift(share) + } if (this.config.showFederatedSharesToTrustedServersAsInternal) { + if (share.isTrustedServer) { + this.shares.unshift(share) + } + } else { + this.externalShares.unshift(share) + } } else { this.shares.unshift(share) } @@ -343,8 +509,8 @@ export default { removeShare(share) { // Get reference for this.linkShares or this.shares const shareList - = share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL - || share.type === this.SHARE_TYPES.SHARE_TYPE_LINK + = share.type === ShareType.Email + || share.type === ShareType.Link ? this.linkShares : this.shares const index = shareList.findIndex(item => item.id === share.id) @@ -365,7 +531,7 @@ export default { let listComponent = this.$refs.shareList // Only mail shares comes from the input, link shares // are managed internally in the SharingLinkList component - if (share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) { + if (share.type === ShareType.Email) { listComponent = this.$refs.linkShareList } const newShare = listComponent.$children.find(component => component.share === share) @@ -415,10 +581,47 @@ export default { &__content { padding: 0 6px; + + section { + padding-bottom: 16px; + + .section-header { + margin-top: 2px; + margin-bottom: 2px; + display: flex; + align-items: center; + padding-bottom: 4px; + + h4 { + margin: 0; + font-size: 16px; + } + + .visually-hidden { + display: none; + } + + .hint-icon { + color: var(--color-primary-element); + } + + } + + } + + & > section:not(:last-child) { + border-bottom: 2px solid var(--color-border); + } + } &__additionalContent { margin: 44px 0; } } + +.hint-body { + max-width: 300px; + padding: var(--border-radius-element); +} </style> |