From 53ac9bdda17e5141fceaec1a895f733c13c73c7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Julius=20H=C3=A4rtl?= <jus@bitgrid.net>
Date: Tue, 29 Jan 2019 12:22:34 +0100
Subject: [PATCH] Implement frontend for search/rename
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Julius Härtl <jus@bitgrid.net>

Move to vuex

Signed-off-by: Julius Härtl <jus@bitgrid.net>
---
 apps/files_sharing/package.json               |   3 +-
 apps/files_sharing/src/CollaborationView.vue  |  36 ++++
 .../src/collaborationresources.js             |  37 +---
 .../src/components/CollectionList.vue         | 198 ++++++++++++++++++
 .../src/components/CollectionListItem.vue     |  70 +++++--
 apps/files_sharing/src/files_sharing.js       |  11 +-
 .../files_sharing/src/services/collections.js | 152 ++++++++++++--
 apps/files_sharing/src/sharetabview.js        |   1 +
 .../src/views/CollaborationView.vue           | 188 -----------------
 .../CollaborationResourcesController.php      |   2 +-
 core/src/OCP/collaboration.js                 |   2 +-
 11 files changed, 438 insertions(+), 262 deletions(-)
 create mode 100644 apps/files_sharing/src/CollaborationView.vue
 create mode 100644 apps/files_sharing/src/components/CollectionList.vue
 delete mode 100644 apps/files_sharing/src/views/CollaborationView.vue

diff --git a/apps/files_sharing/package.json b/apps/files_sharing/package.json
index 9aa194cbaed..f619d898249 100644
--- a/apps/files_sharing/package.json
+++ b/apps/files_sharing/package.json
@@ -17,7 +17,8 @@
   "dependencies": {
     "nextcloud-axios": "^0.1.3",
     "nextcloud-vue": "^0.5.0",
-    "vue": "^2.5.17"
+    "vue": "^2.5.17",
+    "vuex": "^3.1.0"
   },
   "devDependencies": {
     "@babel/core": "^7.1.0",
diff --git a/apps/files_sharing/src/CollaborationView.vue b/apps/files_sharing/src/CollaborationView.vue
new file mode 100644
index 00000000000..b7da5100650
--- /dev/null
+++ b/apps/files_sharing/src/CollaborationView.vue
@@ -0,0 +1,36 @@
+<!--
+  - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+  -
+  - @author Julius Härtl <jus@bitgrid.net>
+  -
+  - @license GNU AGPL version 3 or any later version
+  -
+  - 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>
+	<collection-list type="files"></collection-list>
+</template>
+
+<script>
+	import CollectionList from './components/CollectionList'
+
+	export default {
+		name: 'CollaborationView',
+		components: {
+			CollectionList
+		}
+	}
+</script>
diff --git a/apps/files_sharing/src/collaborationresources.js b/apps/files_sharing/src/collaborationresources.js
index 0370c367bb7..5ad35c192de 100644
--- a/apps/files_sharing/src/collaborationresources.js
+++ b/apps/files_sharing/src/collaborationresources.js
@@ -21,46 +21,19 @@
  */
 
 import Vue from 'vue'
+import Vuex from 'vuex'
 import { PopoverMenu } from 'nextcloud-vue'
 import ClickOutside from 'vue-click-outside'
 import { VTooltip } from 'v-tooltip'
+import { Store } from './services/collections';
 
 Vue.prototype.t = t;
 
 Vue.component('PopoverMenu', PopoverMenu)
 Vue.directive('ClickOutside', ClickOutside)
 Vue.directive('Tooltip', VTooltip)
+Vue.use(Vuex);
 
-import View from './views/CollaborationView'
+import View from './CollaborationView'
 
-let selectAction = {};
-let icons = {};
-let types = {};
-console.log('register types');
-
-/* TODO: temporary data for testing */
-window.OCP.Collaboration.registerType('calendar', {
-	action: () => {
-		return new Promise((resolve, reject) => {
-			var id = window.prompt("calendar id", "1");
-			resolve(id);
-		})
-	},
-	icon: 'icon-calendar-dark',
-	typeString: 'calendar',
-	link: (id) => '#' + id,
-});
-window.OCP.Collaboration.registerType('contact', {
-	action: () => {
-		return new Promise((resolve, reject) => {
-			var id = window.prompt("contacts id", "1");
-			resolve(id);
-		})
-	},
-	icon: 'icon-contacts-dark',
-	link: (id) => '#' + id,
-	/** used in "Link to a {typeString}" */
-	typeString: 'contact'
-});
-
-export { Vue, View }
+export { Vue, View, Store }
diff --git a/apps/files_sharing/src/components/CollectionList.vue b/apps/files_sharing/src/components/CollectionList.vue
new file mode 100644
index 00000000000..afadafec87f
--- /dev/null
+++ b/apps/files_sharing/src/components/CollectionList.vue
@@ -0,0 +1,198 @@
+<!--
+  - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+  -
+  - @author Julius Härtl <jus@bitgrid.net>
+  -
+  - @license GNU AGPL version 3 or any later version
+  -
+  - 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>
+	<ul id="shareWithList" class="shareWithList" v-if="collections">
+		<li @click="showSelect">
+			<div class="avatar"><span class="icon-category-integration icon-white"></span></div>
+			<multiselect v-model="value" :options="options" :placeholder="placeholder" tag-placeholder="Create a new collection" ref="select" @select="select" @search-change="search" label="title" track-by="title" :reset-after="true" :limit="5">
+				<template slot="singleLabel" slot-scope="props">
+					<span class="option__desc">
+						<span class="option__title">{{ props.option.title }}</span>
+					</span>
+				</template>
+				<template slot="option" slot-scope="props">
+					<span class="option__wrapper">
+						<span v-if="props.option.class" :class="props.option.class" class="avatar"></span>
+						<avatar v-else :displayName="props.option.title" :allowPlaceholder="true"></avatar>
+						<span class="option__title">{{ props.option.title }}</span>
+					</span>
+				</template>
+			</multiselect>
+		</li>
+		<collection-list-item v-for="collection in collections" :collection="collection" :key="collection.id" />
+	</ul>
+</template>
+
+<style lang="scss" scoped>
+	.multiselect {
+		width: 100%;
+		margin-left: 3px;
+	}
+	span.avatar {
+		padding: 16px;
+		display: block;
+		background-repeat: no-repeat;
+		background-position: center;
+		opacity: 0.7;
+		&:hover {
+			opacity: 1;
+		}
+	}
+
+	/** TODO provide white icon in core */
+	.icon-category-integration.icon-white {
+		filter: invert(100%);
+		padding: 16px;
+		display: block;
+		background-repeat: no-repeat;
+		background-position: center;
+	}
+
+	.option__wrapper {
+		display: flex;
+		.avatar {
+			display: block;
+			background-color: var(--color-background-darker) !important;
+		}
+		.option__title {
+			padding: 4px;
+		}
+	}
+
+</style>
+<style lang="scss">
+	/** TODO check why this doesn't work when scoped */
+	.shareWithList .multiselect:not(.multiselect--active ) .multiselect__tags {
+		border: none !important;
+		input::placeholder {
+			color: var(--color-main-text);
+		}
+	}
+</style>
+
+<script>
+	import { Multiselect, Avatar } from 'nextcloud-vue';
+	import CollectionListItem from '../components/CollectionListItem';
+
+	const METHOD_CREATE_COLLECTION = 0;
+	const METHOD_ADD_TO_COLLECTION = 1;
+	export default {
+		name: 'CollectionList',
+		components: {
+			CollectionListItem,
+			Avatar,
+			Multiselect: Multiselect,
+		},
+		props: {
+			'type': {
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {
+				selectIsOpen: false,
+				generatingCodes: false,
+				codes: undefined,
+				value: null,
+				model: {},
+				searchCollections: []
+			};
+		},
+		mounted() {
+			this.$store.dispatch('fetchCollectionsByResource', {
+				resourceType: this.type,
+				resourceId: this.$root.model.id
+			})
+		},
+		computed: {
+			collections() {
+				return this.$store.getters.collectionsByResource(this.type, this.$root.model.id)
+			},
+			placeholder() {
+				return t('files_sharing', 'Add to a collection');
+			},
+			options() {
+				let options = [];
+				let types = window.OCP.Collaboration.getTypes().sort();
+				for(let type in types) {
+					options.push({
+						method: METHOD_CREATE_COLLECTION,
+						type: types[type],
+						title: window.OCP.Collaboration.getLabel(types[type]),
+						class: window.OCP.Collaboration.getIcon(types[type]),
+						action: () => window.OCP.Collaboration.trigger(types[type])
+					})
+				}
+				for(let index in this.searchCollections) {
+					if (this.collections.findIndex((collection) => collection.id === this.searchCollections[index].id) === -1) {
+						options.push({
+							method: METHOD_ADD_TO_COLLECTION,
+							title: this.searchCollections[index].name,
+							collectionId: this.searchCollections[index].id
+						})
+					}
+				}
+				return options;
+			}
+		},
+		methods: {
+			select(selectedOption, id) {
+				if (selectedOption.method === METHOD_CREATE_COLLECTION) {
+					selectedOption.action().then((id) => {
+						this.$store.dispatch('createCollection', {
+							baseResourceType: this.type,
+							baseResourceId: this.$root.model.id,
+							resourceType: selectedOption.type,
+							resourceId: id,
+							name: this.$root.model.name,
+						})
+					}).catch((e) => {
+						console.error('No resource selected', e);
+					});
+				}
+
+				if (selectedOption.method === METHOD_ADD_TO_COLLECTION) {
+					this.$store.dispatch('addResourceToCollection', {
+						collectionId: selectedOption.collectionId, resourceType: this.type, resourceId: this.$root.model.id
+					})
+				}
+			},
+			search(query) {
+				this.$store.dispatch('search', query).then((collections) => {
+					this.searchCollections = collections
+				})
+			},
+			showSelect() {
+				this.selectIsOpen = true
+				this.$refs.select.$el.focus()
+			},
+			hideSelect() {
+				this.selectIsOpen = false
+			},
+			isVueComponent(object) {
+				return object._isVue
+			}
+		}
+	}
+</script>
diff --git a/apps/files_sharing/src/components/CollectionListItem.vue b/apps/files_sharing/src/components/CollectionListItem.vue
index f22d5238d6c..8dab92e356f 100644
--- a/apps/files_sharing/src/components/CollectionListItem.vue
+++ b/apps/files_sharing/src/components/CollectionListItem.vue
@@ -21,12 +21,16 @@
   -->
 
 <template>
-	<li class="collection-list">
+	<li class="collection-list" v-click-outside="hideDetails">
 		<avatar :displayName="collection.name" :allowPlaceholder="true"></avatar>
-		<span class="username" title="">{{ collection.name }}</span>
+		<span class="username" title="" @click="showDetails" v-if="this.newName === ''">{{ collection.name }}</span>
+		<form v-else @submit.prevent="renameCollection">
+			<input type="text" v-model="newName" autocomplete="off" autocapitalize="off">
+			<input type="submit" value="" class="icon-confirm">
+		</form>
 		<transition name="fade">
 			<div class="linked-icons" v-if="!detailsOpen">
-					<a v-for="resource in collection.resources" :href="getLink(resource)" v-tooltip="resource.name" :key="resource.id"><span :class="getIcon(resource)"></span></a>
+					<a v-for="resource in collection.resources" :href="resource.link" v-tooltip="resource.name" :key="resource.id"><span :class="getIcon(resource)"></span></a>
 			</div>
 		</transition>
 
@@ -40,10 +44,10 @@
 				</div>
 		</span>
 		<transition name="fade">
-			<ul class="resource-list-details" v-if="detailsOpen" v-click-outside="hideDetails">
+			<ul class="resource-list-details" v-if="detailsOpen">
 				<li v-for="resource in collection.resources">
-					<a :href="getLink(resource)"><span :class="getIcon(resource)"></span> {{ resource.name || '' }}</a>
-					<span class="icon-delete"></span>
+					<a :href="resource.link"><span :class="getIcon(resource)"></span><span class="resource-name">{{ resource.name || '' }}</span></a>
+					<span class="icon-delete" @click="removeResource(collection, resource)"></span>
 				</li>
 			</ul>
 		</transition>
@@ -51,6 +55,8 @@
 </template>
 
 <script>
+	import service from '../services/collections';
+
 	import { Avatar } from 'nextcloud-vue';
 
 	export default {
@@ -66,7 +72,8 @@
 		data() {
 			return {
 				isOpen: false,
-				detailsOpen: false
+				detailsOpen: false,
+				newName: '',
 			}
 		},
 		computed: {
@@ -81,21 +88,14 @@
 						text: t('files_sharing', 'Details'),
 					},
 					{
-						action: () => {  },
+						action: () => this.openRename(),
 						icon: 'icon-rename',
 						text: t('files_sharing', 'Rename collection'),
-					},{
-						action: () => {  },
-						icon: 'icon-delete',
-						text: t('files_sharing', 'Remove collection'),
 					}
 				]
 			},
 			getIcon() {
-				return (resource) => [window.OCP.Collaboration.getIcon(resource.type), resource.iconClass]
-			},
-			getLink() {
-				return (resource) => window.OCP.Collaboration.getLink(resource.type, resource.id)
+				return (resource) => [resource.iconClass]
 			}
 		},
 		methods: {
@@ -108,8 +108,27 @@
 			toggle() {
 				this.isOpen = !this.isOpen
 			},
+			showDetails() {
+				this.detailsOpen = true
+			},
 			hideDetails() {
 				this.detailsOpen = false
+			},
+			removeResource(collection, resource) {
+				this.$store.dispatch('removeResource', {
+					collectionId: collection.id, resourceType: resource.type, resourceId: resource.id
+				})
+			},
+			openRename() {
+				this.newName = this.collection.name;
+			},
+			renameCollection() {
+				this.$store.dispatch('renameCollection', {
+					collectionId: this.collection.id,
+					name: this.newName
+				}).then((collection) => {
+					this.newName = '';
+				});
 			}
 		}
 	}
@@ -138,7 +157,15 @@
 	}
 	.collection-list {
 		flex-wrap: wrap;
+		height: auto;
+
+		form, .username {
+			flex-basis: 10%;
+			flex-grow: 1;
+			display: flex;
+		}
 		.resource-list-details {
+			flex-basis: 100%;
 			width: 100%;
 			li {
 				display: flex;
@@ -146,14 +173,23 @@
 				a {
 					flex-grow: 1;
 					padding: 3px;
+					max-width: calc(100% - 30px);
+					display: flex;
 				}
 			}
 			span {
 				display: inline-block;
-				padding: 8px;
 				vertical-align: top;
 				margin-right: 10px;
 			}
+			span.resource-name {
+				text-overflow: ellipsis;
+				overflow: hidden;
+				position: relative;
+				vertical-align: top;
+				white-space: nowrap;
+				flex-grow: 1;
+			}
 		}
 	}
 </style>
diff --git a/apps/files_sharing/src/files_sharing.js b/apps/files_sharing/src/files_sharing.js
index 9009bdde7ec..56bd2f67613 100644
--- a/apps/files_sharing/src/files_sharing.js
+++ b/apps/files_sharing/src/files_sharing.js
@@ -1,10 +1,5 @@
-// CSP config for webpack dynamic chunk loading
-// eslint-disable-next-line
-__webpack_nonce__ = btoa(OC.requestToken)
 
-// Correct the root of the app for chunk loading
-// OC.linkTo matches the apps folders
-// eslint-disable-next-line
+__webpack_nonce__ = btoa(OC.requestToken)
 __webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')
 
 import '../js/app'
@@ -26,10 +21,8 @@ window.OCP.Collaboration.registerType('files', {
 			}, false);
 		})
 	},
-	link: (id) => OC.generateUrl('/f/') + id,
-	icon: 'nav-icon-files',
 	/** used in "Link to a {typeString}" */
-	typeString: 'file'
+	typeString: t('files_sharing', 'file')
 });
 
 window.OCA.Sharing = OCA.Sharing
diff --git a/apps/files_sharing/src/services/collections.js b/apps/files_sharing/src/services/collections.js
index bd45b8f3c32..9406ddfffc0 100644
--- a/apps/files_sharing/src/services/collections.js
+++ b/apps/files_sharing/src/services/collections.js
@@ -21,39 +21,165 @@
  */
 
 import axios from 'nextcloud-axios';
+import Vuex from 'vuex';
+import Vue from 'vue';
 
 class Service {
 	constructor() {
-		this.service = axios.create();
+		this.http = axios;
+		this.baseUrl = OC.linkToOCS(`collaboration/resources`);
 	}
 
 	listCollection(collectionId) {
-		return this.service.get(`/collaboration/resources/collections/${collectionId}`)
+		return this.http.get(`${this.baseUrl}collections/${collectionId}`)
 	}
 
-	addResource(collectionId, resource) {
-		return this.service.post(`/collaboration/resources/collections/${collectionId}`)
+	renameCollection(collectionId, collectionName) {
+		const resourceBase = OC.linkToOCS(`collaboration/resources/collections`, 2);
+		return this.http.put(`${resourceBase}${collectionId}?format=json`, {
+			collectionName
+		}).then(result => {
+			return result.data.ocs.data;
+		})
 	}
 
-	removeResource() {
-		return this.service.post(`/collaboration/resources/collections/${collectionId}`)
+	getCollectionsByResource(resourceType, resourceId) {
+		const resourceBase = OC.linkToOCS(`collaboration/resources/${resourceType}`);
+		return this.http.get(`${resourceBase}${resourceId}?format=json`)
+			.then(result => {
+				return result.data.ocs.data;
+			})
+			.catch(error => {
+				console.error(error);
+				return Promise.reject(error);
+			});
 	}
 
-	createCollectionOnResource(resourceType, resourceId) {
-		return this.service.post(`/collaboration/resources/${resourceType}/${resourceId}`)
+	createCollection(resourceType, resourceId, name) {
+		const resourceBase = OC.linkToOCS(`collaboration/resources/${resourceType}`, 2);
+		return this.http.post(`${resourceBase}${resourceId}?format=json`, {
+			name: name
+		})
+			.then((response) => {
+				return response.data.ocs.data
+			})
+			.catch(error => {
+				console.error(error);
+				return Promise.reject(error);
+			});
 	}
 
-	getCollectionByResource(resourceType, resourceId) {
-		return this.service.get(`/collaboration/resources/${resourceType}/${resourceId}`)
+	addResource(collectionId, resourceType, resourceId) {
+		resourceId = '' + resourceId;
+		const resourceBase = OC.linkToOCS(`collaboration/resources/collections`, 2);
+		return this.http.post(`${resourceBase}${collectionId}?format=json`, {
+			resourceType,
+			resourceId
+		}).then((response) => {
+			return response.data.ocs.data
+		});
 	}
 
-	getProviders() {
+	removeResource(collectionId, resourceType, resourceId) {
+		return this.http.delete(`${this.baseUrl}/collections/${collectionId}`, { params: { resourceType, resourceId } } )
+			.then((response) => {
+				return response.data.ocs.data
+			});
+	}
 
+	search(query) {
+		const searchBase = OC.linkToOCS(`collaboration/resources/collections/search`);
+		return this.http.get(`${searchBase}%25${query}%25?format=json`)
+			.then((response) => {
+				return response.data.ocs.data
+			});
 	}
 
-	search() {
+}
+
+const service = new Service();
 
+const StoreModule = {
+	state: {
+		collections: []
+	},
+	mutations: {
+		addCollections (state, collections) {
+			state.collections = collections;
+		},
+		addCollection (state, collection) {
+			state.collections.push(collection)
+		},
+		removeCollection (state, collectionId) {
+			state.collections = state.collections.filter(item => item.id !== collectionId)
+		},
+		updateCollection(state, collection) {
+			let index = state.collections.findIndex((_item) => _item.id === collection.id)
+			if (index !== -1) {
+				Vue.set(state.collections, index, collection);
+			} else {
+				state.collections.push(collection)
+			}
+		}
+	},
+	getters: {
+		collectionsByResource(state) {
+			return (resourceType, resourceId) => {
+				return state.collections.filter((collection) => {
+					return typeof collection.resources.find((resource) => resource && resource.id === ''+resourceId && resource.type === resourceType) !== 'undefined'
+				})
+			}
+		},
+		getSearchResults(state) {
+			return (term) => {
+				return state.collections.filter((collection) => collection.name.contains(term))
+			}
+		}
+	},
+	actions: {
+		fetchCollectionsByResource(context, {resourceType, resourceId}) {
+			return service.getCollectionsByResource(resourceType, resourceId).then((collections) => {
+				context.commit('addCollections', collections)
+				return collections;
+			});
+		},
+		createCollection(context, {baseResourceType, baseResourceId, resourceType, resourceId, name}) {
+			return service.createCollection(baseResourceType, baseResourceId, name).then((collection) => {
+				context.commit('addCollection', collection)
+				context.dispatch('addResourceToCollection', {
+					collectionId: collection.id,
+					resourceType, resourceId
+				})
+			})
+		},
+		renameCollection(context, {collectionId, name}) {
+			return service.renameCollection(collectionId, name).then((collection) => {
+				context.commit('updateCollection', collection)
+				return collection
+			})
+		},
+		addResourceToCollection(context, {collectionId, resourceType, resourceId}) {
+			return service.addResource(collectionId, resourceType, resourceId).then((collection) => {
+				context.commit('updateCollection', collection)
+				return collection
+			})
+		},
+		removeResource(context, {collectionId, resourceType, resourceId}) {
+			return service.removeResource(collectionId, resourceType, resourceId).then((collection) => {
+				if (collection.resources.length > 0) {
+					context.commit('updateCollection', collection)
+				} else {
+					context.commit('removeCollection', collectionId)
+				}
+			})
+		},
+		search(context, query) {
+			return service.search(query)
+		}
 	}
 }
 
-export default new Service;
+const Store = () => new Vuex.Store(StoreModule);
+
+export default service;
+export { StoreModule, Store };
diff --git a/apps/files_sharing/src/sharetabview.js b/apps/files_sharing/src/sharetabview.js
index 8f8a655b00e..eaed674775a 100644
--- a/apps/files_sharing/src/sharetabview.js
+++ b/apps/files_sharing/src/sharetabview.js
@@ -87,6 +87,7 @@
 					var vm = new Resources.Vue({
 						el: '#collaborationResources',
 						render: h => h(Resources.View),
+						store: Resources.Store(),
 						data: {
 							model: this.model.toJSON()
 						},
diff --git a/apps/files_sharing/src/views/CollaborationView.vue b/apps/files_sharing/src/views/CollaborationView.vue
deleted file mode 100644
index 99a5fb43df6..00000000000
--- a/apps/files_sharing/src/views/CollaborationView.vue
+++ /dev/null
@@ -1,188 +0,0 @@
-<template>
-	<div :class="{'icon-loading': !collections}">
-		<ul id="shareWithList" class="shareWithList" v-if="collections">
-			<li @click="showSelect">
-				<div class="avatar"><span class="icon-category-integration icon-white"></span></div>
-				<multiselect v-model="value" :options="options" :placeholder="placeholder" tag-placeholder="Create a new collection" ref="select" @select="select" label="title" track-by="title" :reset-after="true">
-					<template slot="singleLabel" slot-scope="props">
-						<span class="option__desc">
-							<span class="option__title">{{ props.option.title }}</span></span>
-					</template>
-					<template slot="option" slot-scope="props">
-						<span class="option__wrapper">
-							<span v-if="props.option.class" :class="props.option.class" class="avatar"></span>
-							<avatar v-else :displayName="props.option.title" :allowPlaceholder="true"></avatar>
-							<span class="option__title">{{ props.option.title }}</span>
-						</span>
-					</template>
-				</multiselect>
-			</li>
-			<collection-list-item v-for="collection in collections" :collection="collection" :key="collection.id" />
-		</ul>
-	</div>
-</template>
-
-<style lang="scss" scoped>
-	.multiselect {
-		width: 100%;
-		margin-left: 3px;
-	}
-	span.avatar {
-		padding: 16px;
-		display: block;
-		background-repeat: no-repeat;
-		background-position: center;
-		opacity: 0.7;
-		&:hover {
-			opacity: 1;
-		}
-	}
-
-	/** TODO provide white icon in core */
-	.icon-category-integration.icon-white {
-		filter: invert(100%);
-		padding: 16px;
-		display: block;
-		background-repeat: no-repeat;
-		background-position: center;
-	}
-
-	.option__wrapper {
-		display: flex;
-		.avatar {
-			display: block;
-			background-color: var(--color-background-darker) !important;
-		}
-		.option__title {
-			padding: 4px;
-		}
-	}
-
-</style>
-<style lang="scss">
-	/** TODO check why this doesn't work when scoped */
-	.shareWithList .multiselect:not(.multiselect--active ) .multiselect__tags {
-		border: none !important;
-		input::placeholder {
-			color: var(--color-main-text);
-		}
-	}
-</style>
-
-<script>
-	import { Multiselect, Avatar } from 'nextcloud-vue';
-	import CollectionListItem from '../components/CollectionListItem';
-	import axios from 'nextcloud-axios';
-
-	export default {
-		name: 'CollaborationView',
-		components: {
-			CollectionListItem,
-			Avatar,
-			Multiselect: Multiselect,
-		},
-		data() {
-			return {
-				selectIsOpen: false,
-				generatingCodes: false,
-				codes: undefined,
-				value: null,
-				model: {},
-				collections: null
-			};
-		},
-		mounted() {
-			let resourceId = this.$root.model.id
-			/** TODO move to service */
-			const resourceBase = OC.linkToOCS(`collaboration/resources/files`);
-			axios.get(`${resourceBase}${resourceId}?format=json`, {
-				headers: {
-					'OCS-APIRequest': true,
-					'Content-Type': 'application/json; charset=UTF-8'
-				}
-			}).then((response) => {
-				this.collections = response.data.ocs.data
-			});
-		},
-		computed: {
-			placeholder() {
-				return t('files_sharing', 'Add to a collection');
-			},
-			options() {
-				let options = [];
-				let types = window.OCP.Collaboration.getTypes();
-				for(let type in types) {
-					options.push({
-						type: types[type],
-						title: window.OCP.Collaboration.getLabel(types[type]),
-						class: window.OCP.Collaboration.getIcon(types[type]),
-						action: () => window.OCP.Collaboration.trigger(types[type])
-					})
-				}
-				for(let index in this.collections) {
-					options.push({
-						title: this.collections[index].name
-					})
-				}
-				return options;
-			}
-		},
-		created: function() {
-		},
-		methods: {
-			select(selectedOption, id) {
-				selectedOption.action().then((id) => {
-					console.log('Create a new collection with')
-					console.log('This file ', this.$root.model.id)
-					console.log('Selected resource ', selectedOption.type, id)
-					this.createCollection(this.$root.model.id, selectedOption.type, id)
-				}).catch((e) => {
-					console.error('No resource selected');
-				});
-			},
-			showSelect() {
-				this.selectIsOpen = true
-				this.$refs.select.$el.focus()
-			},
-			hideSelect() {
-				this.selectIsOpen = false
-			},
-			isVueComponent(object) {
-				return object._isVue
-			},
-			createCollection(resourceIdBase, resourceType, resourceId) {
-				/** TODO move to service */
-				const resourceBase = OC.linkToOCS(`collaboration/resources/files`, 2);
-				axios.post(`${resourceBase}${resourceIdBase}?format=json`, {
-					name: 'Example collection'
-				}, {
-					headers: {
-						'OCS-APIRequest': true,
-						'Content-Type': 'application/json; charset=UTF-8'
-					}
-				}).then((response) => {
-					let newCollection = response.data.ocs.data
-					console.log('Add new collection', newCollection)
-					this.collections.push(newCollection)
-					this.addResourceToCollection(newCollection.id, resourceType.toString(), resourceId.toString())
-				});
-			},
-			addResourceToCollection(collectionId, resourceType, resourceId) {
-				/** TODO move to service */
-				const resourceBase = OC.linkToOCS(`collaboration/resources/collections`, 2);
-				return axios.post(`${resourceBase}${collectionId}?format=json`, {
-					resourceType,
-					resourceId
-				}, {
-					headers: {
-						'OCS-APIRequest': true,
-						'Content-Type': 'application/json; charset=UTF-8'
-					}
-				}).then((response) => {
-					console.log('Add new collection', response.data.ocs.data)
-					this.collections.find((_item) => _item.id === collectionId).resources.push(response.data.ocs.data)
-				});
-			}
-		}
-	}
-</script>
diff --git a/core/Controller/CollaborationResourcesController.php b/core/Controller/CollaborationResourcesController.php
index 565e0ba4739..a15904a4c2a 100644
--- a/core/Controller/CollaborationResourcesController.php
+++ b/core/Controller/CollaborationResourcesController.php
@@ -193,7 +193,7 @@ class CollaborationResourcesController extends OCSController {
 		return [
 			'id' => $collection->getId(),
 			'name' => $collection->getName(),
-			'resources' => array_map([$this, 'prepareResources'], $collection->getResources()),
+			'resources' => array_values(array_filter(array_map([$this, 'prepareResources'], $collection->getResources()))),
 		];
 	}
 
diff --git a/core/src/OCP/collaboration.js b/core/src/OCP/collaboration.js
index a74834e741e..54b580406ed 100644
--- a/core/src/OCP/collaboration.js
+++ b/core/src/OCP/collaboration.js
@@ -49,7 +49,7 @@ export default {
 		return Object.keys(types);
 	},
 	getIcon(type) {
-		return types[type].icon;
+		return types[type].icon || '';
 	},
 	getLabel(type) {
 		return t('files_sharing', 'Link to a {label}', { label: types[type].typeString || type }, 1)
-- 
GitLab