Allow collaborator on create

old-stable24
Varun Patil 2022-10-27 01:22:00 -07:00
parent 9afd99de2a
commit 1d6cec1028
5 changed files with 502 additions and 11 deletions

17
package-lock.json generated
View File

@ -11,6 +11,7 @@
"dependencies": {
"@nextcloud/l10n": "^1.6.0",
"@nextcloud/paths": "^2.1.0",
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/vue": "7.0.0",
"camelcase": "^7.0.0",
"justified-layout": "^4.1.0",
@ -1955,6 +1956,14 @@
"core-js": "^3.6.4"
}
},
"node_modules/@nextcloud/sharing": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.1.0.tgz",
"integrity": "sha512-Cv4uc1aFrA18w0dltq7a5om/EbJSXf36rtO0LP3vi42E6l8ZDVCZwHLKrsZZa/TXNLeYErs1g/6tmWx5xiSSow==",
"dependencies": {
"core-js": "^3.6.4"
}
},
"node_modules/@nextcloud/typings": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-0.2.4.tgz",
@ -11212,6 +11221,14 @@
"core-js": "^3.6.4"
}
},
"@nextcloud/sharing": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.1.0.tgz",
"integrity": "sha512-Cv4uc1aFrA18w0dltq7a5om/EbJSXf36rtO0LP3vi42E6l8ZDVCZwHLKrsZZa/TXNLeYErs1g/6tmWx5xiSSow==",
"requires": {
"core-js": "^3.6.4"
}
},
"@nextcloud/typings": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-0.2.4.tgz",

View File

@ -31,6 +31,7 @@
"dependencies": {
"@nextcloud/l10n": "^1.6.0",
"@nextcloud/paths": "^2.1.0",
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/vue": "7.0.0",
"camelcase": "^7.0.0",
"justified-layout": "^4.1.0",

View File

@ -18,14 +18,15 @@
</template>
<script lang="ts">
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator';
import { Component, Emit, Mixins } from 'vue-property-decorator';
import GlobalMixin from '../../mixins/GlobalMixin';
import * as dav from '../../services/DavRequests';
import { showInfo } from '@nextcloud/dialogs';
import { IAlbum, IPhoto } from '../../types';
import AlbumPicker from './AlbumPicker.vue';
import AlbumPicker from './AlbumPicker.vue';
import Modal from './Modal.vue';
import GlobalMixin from '../../mixins/GlobalMixin';
import * as dav from '../../services/DavRequests';
@Component({
components: {

View File

@ -0,0 +1,472 @@
<!--
- @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
-
- @author Louis Chemineau <louis@chmn.me>
-
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div class="manage-collaborators">
<div class="manage-collaborators__subtitle">
{{ t('photos', 'Add people or groups who can edit your album') }}
</div>
<form class="manage-collaborators__form" @submit.prevent>
<NcPopover ref="popover"
:auto-size="true"
:distance="0">
<label slot="trigger" class="manage-collaborators__form__input">
<NcTextField :value.sync="searchText"
autocomplete="off"
type="search"
name="search"
:aria-label="t('photos', 'Search for collaborators')"
aria-autocomplete="list"
:aria-controls="`manage-collaborators__form__selection-${randomId} manage-collaborators__form__list-${randomId}`"
:placeholder="t('photos', 'Search people or groups')"
@input="searchCollaborators">
<Magnify :size="16" />
</NcTextField>
<NcLoadingIcon v-if="loadingCollaborators" />
</label>
<ul v-if="searchResults.length !== 0" :id="`manage-collaborators__form__list-${randomId}`" class="manage-collaborators__form__list">
<li v-for="collaboratorKey of searchResults" :key="collaboratorKey">
<NcListItemIcon :id="availableCollaborators[collaboratorKey].id"
class="manage-collaborators__form__list__result"
:title="availableCollaborators[collaboratorKey].id"
:search="searchText"
:user="availableCollaborators[collaboratorKey].id"
:display-name="availableCollaborators[collaboratorKey].label"
:aria-label="t('photos', 'Add {collaboratorLabel} to the collaborators list', {collaboratorLabel: availableCollaborators[collaboratorKey].label})"
tabindex="0"
@click="selectEntity(collaboratorKey)" />
</li>
</ul>
<NcEmptyContent v-else
key="emptycontent"
class="manage-collaborators__form__list--empty"
:title="t('photos', 'No collaborators available')">
<AccountGroup slot="icon" />
</NcEmptyContent>
</NcPopover>
</form>
<ul class="manage-collaborators__selection">
<li v-for="collaboratorKey of listableSelectedCollaboratorsKeys"
:key="collaboratorKey"
class="manage-collaborators__selection__item">
<NcListItemIcon :id="availableCollaborators[collaboratorKey].id"
:display-name="availableCollaborators[collaboratorKey].label"
:title="availableCollaborators[collaboratorKey].id"
:user="availableCollaborators[collaboratorKey].id">
<NcButton type="tertiary"
:aria-label="t('photos', 'Remove {collaboratorLabel} from the collaborators list', {collaboratorLabel: availableCollaborators[collaboratorKey].label})"
@click="unselectEntity(collaboratorKey)">
<Close slot="icon" :size="20" />
</NcButton>
</NcListItemIcon>
</li>
</ul>
<div class="actions">
<div v-if="allowPublicLink" class="actions__public-link">
<template v-if="isPublicLinkSelected">
<NcButton class="manage-collaborators__public-link-button"
:aria-label="t('photos', 'Copy the public link')"
:disabled="publicLink.id === ''"
@click="copyPublicLink">
<template v-if="publicLinkCopied">
{{ t('photos', 'Public link copied!') }}
</template>
<template v-else>
{{ t('photos', 'Copy public link') }}
</template>
<template #icon>
<Check v-if="publicLinkCopied" />
<ContentCopy v-else />
</template>
</NcButton>
<NcButton type="tertiary"
:aria-label="t('photos', 'Delete the public link')"
:disabled="publicLink.id === ''"
@click="deletePublicLink">
<NcLoadingIcon v-if="publicLink.id === ''" slot="icon" />
<Close v-else slot="icon" />
</NcButton>
</template>
<NcButton v-else
class="manage-collaborators__public-link-button"
@click="createPublicLinkForAlbum">
<Earth slot="icon" />
{{ t('photos', 'Share via public link') }}
</NcButton>
</div>
<div class="actions__slot">
<slot :collaborators="selectedCollaborators" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator';
import GlobalMixin from '../../mixins/GlobalMixin';
import Magnify from 'vue-material-design-icons/Magnify.vue'
import Close from 'vue-material-design-icons/Close.vue'
import Check from 'vue-material-design-icons/Check.vue'
import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
import Earth from 'vue-material-design-icons/Earth.vue'
import axios from '@nextcloud/axios'
import * as dav from '../../services/DavRequests';
import { showError } from '@nextcloud/dialogs'
import { getCurrentUser } from '@nextcloud/auth'
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
import { NcButton, NcListItemIcon, NcLoadingIcon, NcPopover, NcTextField, NcEmptyContent } from '@nextcloud/vue'
import { Type } from "@nextcloud/sharing";
type Collaborator = {
id: string,
label: string,
type: Type,
}
@Component({
components: {
Magnify,
Close,
AccountGroup,
ContentCopy,
Check,
Earth,
NcLoadingIcon,
NcButton,
NcListItemIcon,
NcTextField,
NcPopover,
NcEmptyContent,
}
})
export default class AddToAlbumModal extends Mixins(GlobalMixin) {
@Prop() private albumName: string;
@Prop() collaborators: Collaborator[];
@Prop() allowPublicLink: boolean;
private searchText = '';
private availableCollaborators: { [key: string]: Collaborator } = {};
private selectedCollaboratorsKeys: string[] = [];
private currentSearchResults = [];
private loadingAlbum = false;
private errorFetchingAlbum = null;
private loadingCollaborators = false;
private errorFetchingCollaborators = null;
private randomId = Math.random().toString().substring(2, 10);
private publicLinkCopied = false;
private config = {
minSearchStringLength: parseInt(window.OC.config['sharing.minSearchStringLength'], 10) || 0,
};
get searchResults(): string[] {
return this.currentSearchResults
.filter(({ id }) => id !== getCurrentUser().uid)
.map(({ type, id }) => `${type}:${id}`)
.filter(collaboratorKey => !this.selectedCollaboratorsKeys.includes(collaboratorKey))
}
get listableSelectedCollaboratorsKeys(): string[] {
return this.selectedCollaboratorsKeys
.filter(collaboratorKey => this.availableCollaborators[collaboratorKey].type !== Type.SHARE_TYPE_LINK)
}
get selectedCollaborators(): Collaborator[] {
return this.selectedCollaboratorsKeys
.map((collaboratorKey) => this.availableCollaborators[collaboratorKey])
}
get isPublicLinkSelected(): boolean {
return this.selectedCollaboratorsKeys.includes(`${Type.SHARE_TYPE_LINK}`)
}
get publicLink(): Collaborator {
return this.availableCollaborators[Type.SHARE_TYPE_LINK]
}
@Watch('collaborators')
collaboratorsChanged(collaborators) {
this.populateCollaborators(collaborators)
};
mounted() {
this.searchCollaborators()
this.populateCollaborators(this.collaborators)
}
/**
* Fetch possible collaborators.
*/
async searchCollaborators() {
if (this.searchText.length >= 1) {
(<any>this.$refs.popover).$refs.popover.show()
}
try {
if (this.searchText.length < this.config.minSearchStringLength) {
return
}
this.loadingCollaborators = true
const response = await axios.get(generateOcsUrl('core/autocomplete/get'), {
params: {
search: this.searchText,
itemType: 'share-recipients',
shareTypes: [
Type.SHARE_TYPE_USER,
Type.SHARE_TYPE_GROUP,
],
},
})
this.currentSearchResults = response.data.ocs.data
.map(collaborator => {
switch (collaborator.source) {
case 'users':
return { id: collaborator.id, label: collaborator.label, type: Type.SHARE_TYPE_USER }
case 'groups':
return { id: collaborator.id, label: collaborator.label, type: Type.SHARE_TYPE_GROUP }
default:
throw new Error(`Invalid collaborator source ${collaborator.source}`)
}
})
this.availableCollaborators = {
...this.availableCollaborators,
...this.currentSearchResults.reduce(this.indexCollaborators, {}),
}
} catch (error) {
this.errorFetchingCollaborators = error
showError(this.t('photos', 'Failed to fetch collaborators list.'))
} finally {
this.loadingCollaborators = false
}
}
/**
* Populate selectedCollaboratorsKeys and availableCollaborators.
*/
populateCollaborators(collaborators: Collaborator[]) {
const initialCollaborators = collaborators.reduce(this.indexCollaborators, {})
this.selectedCollaboratorsKeys = Object.keys(initialCollaborators)
this.availableCollaborators = {
3: {
id: '',
label: this.t('photos', 'Public link'),
type: Type.SHARE_TYPE_LINK,
},
...this.availableCollaborators,
...initialCollaborators,
}
}
/**
* @param {Object<string, Collaborator>} collaborators - Index of collaborators
* @param {Collaborator} collaborator - A collaborator
*/
indexCollaborators(collaborators: { [s: string]: Collaborator; }, collaborator: Collaborator) {
return { ...collaborators, [`${collaborator.type}${collaborator.type === Type.SHARE_TYPE_LINK ? '' : ':'}${collaborator.type === Type.SHARE_TYPE_LINK ? '' : collaborator.id}`]: collaborator }
}
async createPublicLinkForAlbum() {
this.selectEntity(`${Type.SHARE_TYPE_LINK}`)
await this.updateAlbumCollaborators()
try {
this.loadingAlbum = true
this.errorFetchingAlbum = null
} catch (error) {
if (error.response?.status === 404) {
this.errorFetchingAlbum = 404
} else {
this.errorFetchingAlbum = error
}
showError(this.t('photos', 'Failed to fetch album.'))
} finally {
this.loadingAlbum = false
}
}
async deletePublicLink() {
this.unselectEntity(`${Type.SHARE_TYPE_LINK}`)
this.availableCollaborators[3] = {
id: '',
label: this.t('photos', 'Public link'),
type: Type.SHARE_TYPE_LINK,
}
this.publicLinkCopied = false
await this.updateAlbumCollaborators()
}
async updateAlbumCollaborators() {
try {
const album = await dav.getAlbum(getCurrentUser()?.uid.toString(), this.albumName);
await dav.updateAlbum(album, {
albumName: this.albumName,
properties: {
collaborators: this.selectedCollaborators,
},
})
} catch (error) {
showError(this.t('photos', 'Failed to update album.'))
} finally {
this.loadingAlbum = false
}
}
async copyPublicLink() {
await navigator.clipboard.writeText(`${window.location.protocol}//${window.location.host}${generateUrl(`apps/photos/public/${this.publicLink.id}`)}`)
this.publicLinkCopied = true
setTimeout(() => {
this.publicLinkCopied = false
}, 10000)
}
selectEntity(collaboratorKey) {
if (this.selectedCollaboratorsKeys.includes(collaboratorKey)) {
return
}
(<any>this.$refs.popover).$refs.popover.hide()
this.selectedCollaboratorsKeys.push(collaboratorKey)
}
unselectEntity(collaboratorKey) {
const index = this.selectedCollaboratorsKeys.indexOf(collaboratorKey)
if (index === -1) {
return
}
this.selectedCollaboratorsKeys.splice(index, 1)
}
}
</script>
<style lang="scss" scoped>
.manage-collaborators {
display: flex;
flex-direction: column;
height: 500px;
&__title {
font-weight: bold;
}
&__subtitle {
color: var(--color-text-lighter);
}
&__public-link-button {
margin: 4px 0;
}
&__form {
margin-top: 4px 0;
display: flex;
flex-direction: column;
&__input {
position: relative;
display: block;
input {
width: 100%;
padding-left: 34px;
}
.loading-icon {
position: absolute;
top: calc(36px / 2 - 20px / 2);
right: 8px;
}
}
&__list {
padding: 8px;
height: 350px;
overflow: scroll;
&__result {
padding: 8px;
border-radius: 100px;
box-sizing: border-box;
&, & * {
cursor: pointer !important;
}
&:hover {
background: var(--color-background-dark);
}
}
&--empty {
margin: 100px 0;
}
}
}
&__selection {
display: flex;
flex-direction: column;
margin-top: 8px;
flex-grow: 1;
&__item {
border-radius: var(--border-radius-pill);
padding: 0 8px;
&:hover {
background: var(--color-background-dark);
}
}
}
.actions {
display: flex;
margin-top: 8px;
&__public-link {
display: flex;
align-items: center;
button {
margin-left: 8px;
}
}
&__slot {
flex-grow: 1;
display: flex;
justify-content: flex-end;
align-items: center;
}
}
}
</style>

View File

@ -46,7 +46,7 @@
</NcButton>
</span>
<span class="right-buttons">
<NcButton v-if="sharingEnabled && !editMode"
<NcButton v-if="sharingEnabled"
:aria-label="t('photos', 'Go to the add collaborators view.')"
type="secondary"
:disabled="albumName.trim() === '' || loading"
@ -69,9 +69,10 @@
</span>
</div>
</form>
<!-- <CollaboratorsSelectionForm v-else
<AlbumCollaborators v-else
:album-name="albumName"
:allow-public-link="false">
:allow-public-link="false"
:collaborators="[]">
<template slot-scope="{collaborators}">
<span class="left-buttons">
<NcButton :aria-label="t('photos', 'Back to the new album form.')"
@ -93,7 +94,7 @@
</NcButton>
</span>
</template>
</CollaboratorsSelectionForm> -->
</AlbumCollaborators>
</template>
<script lang="ts">
@ -102,11 +103,10 @@ import GlobalMixin from '../../mixins/GlobalMixin';
import { getCurrentUser } from '@nextcloud/auth'
import { NcButton, NcLoadingIcon, NcTextField } from '@nextcloud/vue'
import { IAlbum } from '../../types';
import moment from 'moment';
import * as dav from '../../services/DavRequests';
// import CollaboratorsSelectionForm from './CollaboratorsSelectionForm.vue'
import AlbumCollaborators from './AlbumCollaborators.vue'
import Send from 'vue-material-design-icons/Send.vue'
import AccountMultiplePlus from 'vue-material-design-icons/AccountMultiplePlus.vue'
@ -117,7 +117,7 @@ import AccountMultiplePlus from 'vue-material-design-icons/AccountMultiplePlus.v
NcButton,
NcLoadingIcon,
NcTextField,
// CollaboratorsSelectionForm,
AlbumCollaborators,
Send,
AccountMultiplePlus,